導師計劃--數據結構和算法系列(上)

structure-banner

導師計劃已經開始一個月了,本身的講解的課程選擇了數據結構和算法。這個系列的講解分爲上下兩章javascript語言輔助。本篇文章爲上章,涉及的內容是基本的數據結構。在日本,晚上沒事安排@…@,時間仍是充足的...,因而本身整理下本系列知識點的上章內容。javascript

moiunt-Fuji

如下爲正文:前端

數據結構是計算機存儲、組織數據的方式。數據結構是指相互直接存在一種或多種特殊關係的數據元素的集合。一般狀況下,精心選擇數據結構能夠帶來更高的運行或者存儲效率。做爲一名程序猿,更須要了解下數據結構。AND WHY?能夠參考這篇文章【譯】編程不容易中的性能和優化部份內容。java

講到數據結構,咱們都會談到線性結構和非線性結構。node

1.線性結構是一個有序數據元素的集合。它應該知足下面的特徵:git

  • 集合中必存在惟一的一個「第一個元素」
  • 集合中必存在惟一的一個「最後的元素」
  • 除最後一元素以外,其它數據元素均有惟一的「後繼」
  • 除第一個元素以外,其它數據元素均有惟一的「前驅」

按照百度百科的定義,咱們知道符合條件的數據結構就有棧、隊列和其它。es6

2.非線性結構其邏輯特徵是一個節點元素能夠有多個直接前驅或多個直接後繼。github

那麼,符合條件的數據結構就有圖、樹和其它。算法

嗯~瞭解一下就行。咱們進入正題:編程

數組

數組是一種線性結構,以十二生肖(鼠、牛、虎、兔、龍、蛇、馬、羊、猴、雞、狗、豬)排序爲例:數組

array_demo

咱們來建立一個數組並打印出結果就一目瞭然了:

let arr = ['鼠', '牛', '虎', '兔', '龍', '蛇', '馬', '羊', '猴', '雞', '狗', '豬'];
arr.forEach((item, index) => {
	console.log(`[ ${index} ] => ${item}`);
});

// [ 0 ] => 鼠
// [ 1 ] => 牛
// [ 2 ] => 虎
// [ 3 ] => 兔
// [ 4 ] => 龍
// [ 5 ] => 蛇
// [ 6 ] => 馬
// [ 7 ] => 羊
// [ 8 ] => 猴
// [ 9 ] => 雞
// [ 10 ] => 狗
// [ 11 ] => 豬
複製代碼

數組中經常使用的屬性和一些方法以下,直接調用相關的方法便可。這裏不作演示~

經常使用的屬性

  • length : 表示數組的長度

經常使用的方法

  • splice(index, howmany, item, ... itemx)

    splice方法自認爲是數組中最強大的方法。能夠實現數組元素的添加、刪除和替換。參數index爲整數且必需,規定添加/刪除項目的位置,使用負數可從數組結尾處規定位置;參數howmany爲必需,爲要刪除的項目數量,若是設置爲 0,則不會刪除項目;item1, ... itemx爲可選,向數組添加新的項目。

  • indexOf(searchValue, fromIndex)

    indexOf方法返回某個指定字符串值在數組中的位置。searchValue是查詢的字符串;fromIndex是查詢的開始位置,默認是0。若是查詢不到,會返回-1。

  • concat(array1, ... arrayn)

    concat方法用於鏈接兩個或者多個數組。

  • push(newElement1, ... newElementN)

    push方法可向數組的末尾添加一個或者多個元素。

  • unshift(newElement1, ... newElementN)

    unshift方法可向數組的開頭添加一個或者多個元素。

  • pop()

    pop方法用於刪除並返回數組的最後一個元素

  • shift()

    shift方法能夠刪除數組的第一個元素

  • reverse()

    reverse方法用於數組的反轉

  • sort(sortFn)

    sort方法是對數組的元素排序。參數sortFn可選,其規定排序順序,必須是函數。

let values = [0, 1, 5, 10, 15];
values.sort();
console.log(values); // [0, 1, 10, 15, 5]
// 爲何會出現這種排序結果呢❓
// 由於在忽略sortFn的狀況下,元素會按照轉換爲字符串的各個字符的Unicode位點進行排序,以下
let equalValues = ['0', '1', '5', '10', '15'];
equalValues.sort();
console.log(equalValues); //  ["0", "1", "10", "15", "5"]

let arr = [0, 10, 5, 1, 15];
function compare(el1, el2){
    return el1 - el2; // 升序排列
}
arr.sort(compare);
console.log(arr); // [0, 1, 5, 10, 15]

arr.sort((el1, el2) => {
    return el2 - el1; // 降序排列
}); 
console.log(arr); // [15, 10, 5, 1, 0]
複製代碼
  • forEach(fn(currentValue, index, arr), thisValue)

    forEach方法用於調用數組的每一個元素,並將元素傳遞給回調函數。參數function(currentValue, index, arr){}是一個回調函數。thisValue可選,傳遞給函數的值通常用 "this" 值,若是這個參數爲空, "undefined" 會傳遞給 "this" 值。

  • every(fn(currentValue, index, arr), thisValue)

    every方法用於檢測數組中全部元素是否符合指定條件,若是數組中檢測到有一個元素不知足,則整個表達式返回false,且剩餘的元素再也不檢查。若是全部的元素都知足條件,則返回true

  • some(fn(currentValue,index,arr),thisValue)

    some方法用於檢測數組中元素是否知足指定條件。只要有一個符合就返回true,剩餘的元素再也不檢查。若是全部元素都不符合條件,則返回false

  • reduce(fn(accumulator, currentValue, currentIndex, arr), initialValue)

    reduce方法接收一個函數做爲累加器,數組中的每一個值(從左到右)開始縮減,最終爲一個值。回調函數的四個參數的意義以下:accumulator,必需,累計器累計回調的返回值, 它是上一次調用回調時返回的累積值,或initialValue;currentValue,必需,數組中正在處理的元素;currentIndex,可選,數組中正在處理的當前元素的索引,若是提供了initialValue,則起始索引號爲0,不然爲1;arr,可選,當前元素所屬的數組對象。initialValue,可選,傳遞給函數的初始值。

let arr = [1, 2, 3, 4];
let reducer = (accumulator, currentValue) => accumulator + currentValue;

// 1 + 2 + 3 + 4
console.log(arr.reduce(reducer)); // 10

// 5 + 1 + 2 + 3 + 4
console.log(arr.reduce(reducer, 5)); // 15
複製代碼

是一種後進先出(LIFO)線性表,是一種基於數組的數據結構。(ps:其實後面講到的數據結構或多或少有數組的影子)

  • LIFO(Last In First Out)表示後進先出,後進來的元素第一個彈出棧空間。相似於自動餐托盤,最後放上去的托盤,每每先被拿出來使用。
  • 僅容許在表的一端進行插入和移除元素。這一端被稱爲棧頂,相對地,把另外一端稱爲棧底。以下圖的標識。
  • 向一個棧插入新元素稱做進棧、入棧或壓棧,這是將新元素放在棧頂元素上面,使之成爲新的棧頂元素。
  • 從一個棧刪除元素又稱爲出棧或退棧,它是把棧頂元素刪除掉,使其相鄰的元素成爲新的棧頂元素。

stack_demo

咱們代碼寫下,熟悉下棧:

class Stack {
    constructor(){
        this.items = [];
    }
    // 入棧操做
    push(element = ''){
        if(!element) return;
        this.items.push(element);
        return this;
    }
    // 出棧操做
    pop(){
        this.items.pop();
        return this;
    }
    // 對棧一瞥,理論上只能看到棧頂或者說即將處理的元素
    peek(){
        return this.items[this.size() - 1];
    }
    // 打印棧數據
    print(){
        return this.items.join(' ');
    }
    // 棧是否爲空
    isEmpty(){
        return this.items.length == 0;
    }
    // 返回棧的元素個數
    size(){
        return this.items.length;
    }
}
let stack = new Stack(),
    arr = ['鼠', '牛', '虎', '兔', '龍', '蛇', '馬', '羊', '猴', '雞', '狗', '豬'];
arr.forEach(item => {
    stack.push(item);
});
console.log(stack.print()); // 鼠 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬
console.log(stack.peek()); // 豬
stack.pop().pop().pop().pop();
console.log(stack.print()); // 鼠 牛 虎 兔 龍 蛇 馬 羊
console.log(stack.isEmpty()); // false
console.log(stack.size()); // 8
複製代碼

⚠️ 注意:棧這裏的push和pop方法要和數組方法的push和pop方法區分下。

說到,這也讓我想到了翻譯的一篇文章JS的執行上下文和環境棧是什麼?,感興趣的話能夠戳進去看下。

隊列

隊列是一種先進先出(FIFO)受限的線性表。受限體如今於其容許在表的前端(front)進行刪除操做,在表的末尾(rear)進行插入【優先隊列這些排除在外】操做。

queue_demo

代碼走一遍:

class Queue {
    constructor(){
        this.items = [];
    }
    // 入隊操做
    enqueue(element = ''){
        if(!element) return;
        this.items.push(element);
        return this;
    }
    // 出隊操做
    dequeue(){
        this.items.shift();
        return this;
    }
    // 查看隊前元素或者說即將處理的元素
    front(){
        return this.items[0];
    }
    // 查看隊列是否爲空
    isEmpty(){
        return this.items.length == 0;
    }
    // 查看隊列的長度
    len(){
        return this.items.length;
    }
    // 打印隊列數據
    print(){
        return this.items.join(' ');
    }
}

let queue = new Queue(),
    arr = ['鼠', '牛', '虎', '兔', '龍', '蛇', '馬', '羊', '猴', '雞', '狗', '豬'];
arr.forEach(item => {
    queue.enqueue(item);
});
console.log(queue.print()); // 鼠 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬
console.log(queue.isEmpty()); // false
console.log(queue.len()); // 12
queue.dequeue().dequeue();
console.log(queue.front()); // 虎
console.log(queue.print()); // 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬
複製代碼

鏈表

在進入正題以前,咱們先來聊聊數組的優缺點。

優勢:

  • 存儲多個元素,比較經常使用
  • 訪問便捷,使用下標[index]便可訪問

缺點:

  • 數組的建立一般須要申請一段連續的內存空間,而且大小是固定的(大多數的編程語言數組都是固定的),因此在進行擴容的時候難以掌控。(通常狀況下,申請一個更大的數組,會是以前數組的倍數,好比兩倍。而後,再將原數組中的元素複製過去)
  • 插入數據越是靠前,其成本很高,由於須要進行大量元素的位移。

相對數組,鏈表亦能夠存儲多個元素,並且存儲的元素在內容中沒必要是連續的空間;在插入和刪除數據時,時間複雜度能夠達到O(1)。在查找元素的時候,仍是須要從頭開始遍歷的,比數組在知道下表的狀況下要快,可是數組若是不肯定下標的話,那就另說了...

咱們使用十二生肖來了解下鏈表:

linklist_demo

鏈表是由一組節點組成的集合。每一個節點都使用一個對象的引用指向它的後繼。如上圖。下面用代碼實現下:

// 鏈表
class Node {
    constructor(element){
        this.element = element;
        this.next = null;
    }
}

class LinkedList {
    constructor(){
        this.length = 0; // 鏈表長度
        this.head = new Node('head'); // 表頭節點
    }
    /** * @method find 查找元素的功能,找不到的狀況下直接返回鏈尾節點 * @param { String } item 要查找的元素 * @return { Object } 返回查找到的節點 */
    find(item = ''){
        let currNode = this.head;
        while(currNode.element != item && currNode.next){
            currNode = currNode.next;
        }
        return currNode;
    }
    /** * @method findPrevious 查找鏈表指定元素的前一個節點 * @param { String } item 指定的元素 * @return { Object } 返回查找到的以前元素的前一個節點,找不到節點的話返回鏈尾節點 */
    findPrevious(item){
        let currNode = this.head;
        while((currNode.next != null) && (currNode.next.element != item)){
            currNode = currNode.next;
        }
        return currNode;
    }
    /** * @method insert 插入功能 * @param { String } newElement 要出入的元素 * @param { String } item 想要追加在後的元素(此元素不必定存在) */
    insert(newElement = '', item){
        if(!newElement) return;
        let newNode = new Node(newElement),
            currNode = this.find(item);
        newNode.next = currNode.next;
        currNode.next = newNode;
        this.length++;
        return this;
    }
    // 展現鏈表元素
    display(){
        let currNode = this.head,
            arr = [];
        while(currNode.next != null){
            arr.push(currNode.next.element);
            currNode = currNode.next;
        }
        return arr.join(' ');
    }
    // 鏈表的長度
    size(){
        return this.length;
    }
    // 查看鏈表是否爲空
    isEmpty(){
        return this.length == 0;
    }
    /** * @method indexOf 查看鏈表中元素的索引 * @param { String } element 要查找的元素 */
    indexOf(element){
        let currNode = this.head,
            index = 0;
        while(currNode.next != null){
            index++;
            if(currNode.next.element == element){
                return index;
            }
            currNode = currNode.next;
        }
        return -1;
    }
    /** * @method removeEl 移除指定的元素 * @param { String } element */
    removeEl(element){
        let preNode = this.findPrevious(element);
        preNode.next = preNode.next != null ? preNode.next.next : null;
    }
}

let linkedlist = new LinkedList();
console.log(linkedlist.isEmpty()); // true
linkedlist.insert('鼠').insert('虎').insert('牛', '鼠');
console.log(linkedlist.display()); // 鼠 牛 虎
console.log(linkedlist.find('豬')); // Node { element: '虎', next: null }
console.log(linkedlist.find('鼠')); // Node { element: '鼠', next: Node { element: '牛', next: Node { element: '虎', next: null } } }
console.log(linkedlist.size()); // 3
console.log(linkedlist.indexOf('鼠')); // 1
console.log(linkedlist.indexOf('豬')); // -1
console.log(linkedlist.findPrevious('虎')); // Node { element: '牛', next: Node { element: '虎', next: null } }
linkedlist.removeEl('鼠');
console.log(linkedlist.display()); // 牛 虎
複製代碼

字典

字典的主要特色是鍵值一一對應的關係。能夠比喻成咱們現實學習中查不一樣語言翻譯的字典。這裏字典的鍵(key)理論上是可使用任意的內容,但仍是建議語意化一點,好比下面的十二生肖圖:

dictionary_demo

class Dictionary {
    constructor(){
        this.items = {};
    }
    /** * @method set 設置字典的鍵值對 * @param { String } key 鍵 * @param {*} value 值 */
    set(key = '', value = ''){
        this.items[key] = value;
        return this;
    }
    /** * @method get 獲取某個值 * @param { String } key 鍵 */
    get(key = ''){
        return this.has(key) ? this.items[key] : undefined;
    }
    /** * @method has 判斷是否含有某個鍵的值 * @param { String } key 鍵 */
    has(key = ''){
        return this.items.hasOwnProperty(key);
    }
    /** * @method remove 移除元素 * @param { String } key */
    remove(key){
        if(!this.has(key))  return false;
        delete this.items[key];
        return true;
    }
    // 展現字典的鍵
    keys(){
        return Object.keys(this.items).join(' ');
    }
    // 字典的大小
    size(){
        return Object.keys(this.items).length;
    }
    // 展現字典的值
    values(){
        return Object.values(this.items).join(' ');
    }
    // 清空字典
    clear(){
        this.items = {};
        return this;
    }
}

let dictionary = new Dictionary(),
    // 這裏須要修改
    arr = [{ key: 'mouse', value: '鼠'}, {key: 'ox', value: '牛'}, {key: 'tiger', value: '虎'}, {key: 'rabbit', value: '兔'}, {key: 'dragon', value: '龍'}, {key: 'snake', value: '蛇'}, {key: 'horse', value: '馬'}, {key: 'sheep', value: '羊'}, {key: 'monkey', value: '猴'}, {key: 'chicken', value: '雞'}, {key: 'dog', value: '狗'}, {key: 'pig', value: '豬'}];
    arr.forEach(item => {
        dictionary.set(item.key, item.value);
    });
console.log(dictionary.keys()); // mouse ox tiger rabbit dragon snake horse sheep monkey chicken dog pig
console.log(dictionary.values()); // 鼠 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬
console.log(dictionary.has('dragon')); // true
console.log(dictionary.get('tiger')); // 虎
console.log(dictionary.remove('pig')); // true
console.log(dictionary.size()); // 11
console.log(dictionary.clear().size()); // 0
複製代碼

集合

集合一般是由一組無序的,不能重複的元素構成。 一些常見的集合操做如圖:

set_demo

es6中已經封裝好了可用的Set類。咱們手動來寫下相關的邏輯:

// 集合
class Set {
    constructor(){
        this.items = [];
    }
    /** * @method add 添加元素 * @param { String } element * @return { Boolean } */
    add(element = ''){
        if(this.items.indexOf(element) >= 0) return false;
        this.items.push(element);
        return true;
    }
    // 集合的大小
    size(){
        return this.items.length;
    }
    // 集合是否包含某指定元素
    has(element = ''){
        return this.items.indexOf(element) >= 0;
    }
    // 展現集合
    show(){
        return this.items.join(' ');
    }
    // 移除某個元素
    remove(element){
        let pos = this.items.indexOf(element);
        if(pos < 0) return false;
        this.items.splice(pos, 1);
        return true;
    }
    /** * @method union 並集 * @param { Array } set 數組集合 * @return { Object } 返回並集的對象 */
    union(set = []){
        let tempSet = new Set();
        for(let i = 0; i < this.items.length; i++){
            tempSet.add(this.items[i]);
        }
        for(let i = 0; i < set.items.length; i++){
            if(tempSet.has(set.items[i])) continue;
            tempSet.items.push(set.items[i]);
        }
        return tempSet;
    }
    /** * @method intersect 交集 * @param { Array } set 數組集合 * @return { Object } 返回交集的對象 */
    intersect(set = []){
        let tempSet = new Set();
        for(let i = 0; i < this.items.length; i++){
            if(set.has(this.items[i])){
                tempSet.add(this.items[i]);
            }
        }
        return tempSet;
    }
    /** * @method isSubsetOf 【A】是【B】的子集❓ * @param { Array } set 數組集合 * @return { Boolean } 返回真假值 */
    isSubsetOf(set = []){
        if(this.size() > set.size()) return false;
        this.items.forEach*(item => {
            if(!set.has(item)) return false;
        });
        return true;
    }
}

let set = new Set(),
    arr = ['鼠', '牛', '虎', '兔', '龍', '蛇', '馬', '羊', '猴'];
arr.forEach(item => {
    set.add(item);
});
console.log(set.show()); // 鼠 牛 虎 兔 龍 蛇 馬 羊 猴
console.log(set.has('豬')); // false
console.log(set.size()); // 9
set.remove('鼠');
console.log(set.show()); // 牛 虎 兔 龍 蛇 馬 羊 猴
let setAnother = new Set(),
    anotherArr = ['馬', '羊', '猴', '雞', '狗', '豬'];
anotherArr.forEach(item => {
    setAnother.add(item);
});
console.log(set.union(setAnother).show()); // 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬
console.log(set.intersect(setAnother).show()); // 馬 羊 猴
console.log(set.isSubsetOf(setAnother)); // false
複製代碼

散列表/哈希表

散列是一種經常使用的存儲技術,散列使用的數據結構叫作散列表/哈希表。在散列表上插入、刪除和取用數據都很是快,可是對於查找操做來講卻效率低下,好比查找一組數據中的最大值和最小值。查找的這些操做得求助其它數據結構,好比下面要講的二叉樹。

切入個案例感覺下哈希表:

假如一家公司有1000個員工, 如今咱們須要將這些員工的信息使用某種數據結構來保存起來。你會採用什麼數據結構呢?

  • 方案一:數組

    • 按照順序將全部員工信息依次存入一個長度爲1000的數組中。每一個員工的信息都保存在該數組的某個位置上。
    • 可是咱們要查看某個員工的信息怎麼辦呢?一個個查找嗎?不太好找。
    • 數組最大的優點是什麼?經過下標值獲取信息。
    • 因此爲了能夠經過數組快速定位到某個員工,最好給員工信息中添加一個員工編號,而編號對應的就是員工的下標值
    • 當查找某個員工信息時,經過員工號能夠快速定位到員工的信息位置。
  • 方案二:鏈表

    • 鏈表對應插入和刪除數據有必定的優點。
    • 可是對於獲取員工的信息,每次都必須從頭遍歷到尾,這種方式顯然不是特別適合咱們這裏。
  • 最終方案:

    • 這麼看最終方案彷佛就是數組了,可是數組仍是有缺點,什麼缺點呢?
    • 假如咱們想查看下張三這位員工的信息,可是咱們不知道張三的員工編號,怎麼辦呢?
    • 固然,咱們能夠問他的員工編號。可是咱們每查找一個員工都是要問一下這個員工的編號嗎?不合適。【那咱們還不如直接問他的信息嘞】
    • 能不能有一種辦法,讓張三的名字和他的員工編號產生直接的關係呢?
    • 也就是經過張三這個名字,咱們就能獲取到他的索引值,而再經過索引值咱們就能獲取張三的信息呢?
    • 這樣的方案已經存在了,就是使用哈希函數,讓某個key的信息和索引值對應起來。

那麼散列表的原理和實現又是怎樣的呢,咱們來聊聊。

咱們的哈希表是基於數組完成的,咱們從數組這裏切入解析下。數組能夠經過下標直接定位到相應的空間,哈希表的作法就是相似的實現。哈希表把key(鍵)經過一個固定的算法函數(此函數稱爲哈希函數/散列函數)轉換成一個整型數字,而後就將該數字對數組長度進行取餘,取餘結果就看成數組的下標,將value(值)存儲在以該數字爲下標的數組空間裏,而當使用哈希表進行查詢的時候,就是再次使用哈希函數將key轉換爲對應的數組下標,並定位到該空間獲取value

結合下面的代碼,也許你會更容易理解:

// 哈希表
class HashTable {
    constructor(){
        this.table = new Array(137);
    }
    /** * @method hashFn 哈希函數 * @param { String } data 傳入的字符串 * @return { Number } 返回取餘的數字 */
    hashFn(data){
        let total = 0;
        for(let i = 0; i < data.length; i++){
            total += data.charCodeAt(i);
        }
        return total % this.table.length;
    }
    /** * * @param { String } data 傳入的字符串 */
    put(data){
        let pos = this.hashFn(data);
        this.table[pos] = data;
        return this;
    }
    // 展現
    show(){
        this.table && this.table.forEach((item, index) => {
            if(item != undefined){
                console.log(index + ' => ' + item);
            }
        })
    }
    // ...獲取值get函數等看官感興趣的話本身補充測試啦
}

let hashtable = new HashTable(),
    arr = ['mouse', 'ox', 'tiger', 'rabbit', 'dragon', 'snake', 'horse', 'sheep', 'monkey', 'chicken', 'dog', 'pig'];
arr.forEach(item => {
    hashtable.put(item);
});
hashtable.show();
// 5 => mouse
// 40 => dog
// 46 => pig
// 80 => rabbit
// 87 => dragon
// 94 => ox
// 111 => monkey
// 119 => snake
// 122 => sheep
// 128 => tiger
// 134 => horse

// 那麼問題來了,十二生肖裏面的_小雞_去哪裏了呢❓
// 被_小萌狗_給覆蓋了,由於其位置都是40(這個能夠本身證實下)
// 問題又來了,那麼應該如何解決這種被覆蓋的衝突呢❓
複製代碼

hashtable_demo

針對上面的問題,咱們存儲數據的時候,產生衝突的話咱們能夠像下面這樣解決:

1. 線性探測法

當發生碰撞(衝突)時,線性探測法檢查散列表中的下一個位置【有可能非順序查找位置,不必定是下一個位置】是否爲空。若是爲空,就將數據存入該位置;若是不爲空,則繼續檢查下一個位置,直到找到一個空的位置爲止。該技術是基於一個事實:每一個散列表都有不少空的單元格,可使用它們存儲數據。

2. 開鏈法

可是,當發生碰撞時,咱們任然但願將key(鍵)存儲到經過哈希函數產生的索引位置上,那麼咱們可使用開鏈法開鏈法是指實現哈希表底層的數組中,每一個數組元素又是一個新的數據結構,好比另外一個數組(這樣結合起來就是二位數組了),鏈表等,這樣就能存儲多個鍵了。使用這種技術,即便兩個key(鍵)散列後的值相同,依然是被保存在一樣的位置,只不過它們是被保存在另外一個數據結構上而已。以另外一個數據結構是數組爲例,存儲的數據以下:

open_link_method

二叉查找樹

  • 樹的定義:

    • 樹(Tree):n(n >= 0)個節點構成的有限集合。

      • n = 0時,稱爲空樹;
      • 對任意一棵空樹(n > 0),它具有如下性質:
      • 樹中有一個稱爲**根(Root)**的特殊節點,用r(root)表示;
      • 其他節點可分爲m(m > 0)個互不相交的有限集T1,T2,...Tm,其中每一個集合本省又是一棵樹,稱爲原來樹的子樹(SubTree)
    • 注意:

      • 子樹之間不能夠相交
      • 除了根節點外,每一個節點有且僅有一個父節點;
      • 一個N個節點的樹有N-1條邊。
  • 樹的術語:

    • 節點的度(Degree):節點的子樹個數。
    • 樹的度:樹的全部節點中最大的度數(樹的度一般爲節點個數的N-1)。
    • 葉節點(Leaf):度爲0的節點(也稱葉子節點)。
    • 父節點(Parent):有子樹的節點是其子樹的父節點。
    • 子節點(Child):若A節點是B節點的父節點,則稱B節點是A節點的子節點。
    • 兄弟節點(Sibling):具備同一個父節點的各節點彼此是兄弟節點。
    • 路徑和路徑長度:從節點n1nk的路徑爲一個節點序列n1,n2,n3,...,nknini+1的父節點。路徑所包含邊的個數爲路徑長度。
    • 節點的層次(Level):規定根節點在第0層,它的子節點是第1層,子節點的子節點是第2層,以此類推。
    • 樹的深度(Depth):樹中全部節點中的最大層次是這棵樹的深度(由於上面是從第0層開始,深度 = 第最大層數 + 1)

以下圖:

tree_intro

  • 二叉樹的定義:

    • 二叉樹能夠爲空,也就是沒有節點
    • 二叉樹若不爲空,則它是由根節點和稱爲其左子樹TL和右子樹RT的兩個不相交的二叉樹組成
    • 二叉樹每一個節點的子節點不容許超過兩個
  • 二叉樹的五種形態:

    • 只有根節點
    • 只有左子樹
    • 只有右子樹
    • 左右子樹均有

對應下圖(從左至右):

five_style_binary_tree

咱們接下來要講的是二叉查找樹(BST,Binary Search Tree)二叉查找樹,也稱二叉搜索樹或二叉排序樹,是一種特殊的二叉樹,相對值較的值保存在節點中,較的值保存在節點中。二叉查找樹特殊的結構使它可以快速的進行查找、插入和刪除數據。下面咱們來實現下:

// 二叉查找樹
// 輔助節點類
class Node {
    constructor(data, left, right){
        this.data = data;
        this.left = left;
        this.right = right;
    }
    // 展現節點信息
    show(){
        return this.data;
    }
}
class BST {
    constructor(){
        this.root = null;
    }
    // 插入數據
    insert(data){
        let n = new Node(data, null, null);
        if(this.root == null){
            this.root = n;
        }else{
            let current = this.root,
                parent = null;
            while(true){
                parent = current;
                if(data < current.data){
                    current = current.left;
                    if(current == null){
                        parent.left = n;
                        break;
                    }
                }else{
                    current = current.right;
                    if(current == null){
                        parent.right = n;
                        break;
                    }
                }
            }
        }
        return this;
    }
    // 中序遍歷
    inOrder(node){
        if(!(node == null)){
            this.inOrder(node.left);
            console.log(node.show());
            this.inOrder(node.right);
        }
    }
    // 先序遍歷
    preOrder(node){
        if(!(node == null)){
            console.log(node.show());
            this.preOrder(node.left);
            this.preOrder(node.right);
        }
    }
    // 後序遍歷
    postOrder(node){
        if(!(node == null)){
            this.postOrder(node.left);
            this.postOrder(node.right);
            console.log(node.show());
        }
    }
    // 獲取最小值
    getMin(){
        let current = this.root;
        while(!(current.left == null)){
            current = current.left;
        }
        return current.data;
    }
    // 獲取最大值
    getMax(){
        let current = this.root;
        while(!(current.right == null)){
            current = current.right;
        }
        return current.data;
    }
    // 查找給定的值
    find(data){
        let current = this.root;
        while(current != null){
            if(current.data == data){
                return current;
            }else if(data < current.data){
                current = current.left;
            }else{
                current = current.right;
            }
        }
        return null;
    }
    // 移除給定的值
    remove(data){
        root = this.removeNode(this.root, data);
        return this;
    }
    // 移除給定值的輔助函數
    removeNode(node, data){
        if(node == null){
            return null;
        }
        if(data == node.data){
            // 葉子節點
            if(node.left == null && node.right == null){
                return null; // 此節點置空
            }
            // 沒有左子樹
            if(node.left == null){
                return node.right;
            }
            // 沒有右子樹
            if(node.right == null){
                return node.left;
            }
            // 有兩個子節點的狀況
            let tempNode = this.getSmallest(node.right); // 獲取右子樹
            node.data = tempNode.data; // 將其右子樹的最小值賦值給刪除的那個節點值
            node.right = this.removeNode(node.right, tempNode.data); // 刪除指定節點的下的最小值,也就是置其爲空
            return node;
        }else if(data < node.data){
            node.left = this.removeNode(node.left, data);
            return node;
        }else{
            node.right = this.removeNode(node.right, data);
            return node;
        }
    }
    // 獲取給定節點下的二叉樹最小值的輔助函數
    getSmallest(node){
        if(node.left == null){
            return node;
        }else{
            return this.getSmallest(node.left);
        }
    }
}

let bst = new BST();
bst.insert(56).insert(22).insert(10).insert(30).insert(81).insert(77).insert(92);
bst.inOrder(bst.root); // 10, 22, 30, 56, 77, 81, 92
console.log('--中序和先序遍歷分割線--');
bst.preOrder(bst.root); // 56, 22, 10, 30, 81, 77, 92
console.log('--先序和後序遍歷分割線--');
bst.postOrder(bst.root); // 10, 30, 22, 77, 92, 81, 56
console.log('--後序遍歷和獲取最小值分割線--');
console.log(bst.getMin()); // 10
console.log(bst.getMax()); // 92
console.log(bst.find(22)); // Node { data: 22, left: Node { data: 10, left: null, right: null }, right: Node { data: 30, left: null, right: null } }
// 咱們刪除節點值爲22,而後用先序的方法遍歷,以下
console.log('--移除22的分割線--')
console.log(bst.remove(22).inOrder(bst.root)); // 10, 30, 56, 77, 81, 92
複製代碼

看了上面的代碼以後,你是否有些懵圈呢?咱們藉助幾張圖來了解下,或許你就豁然開朗了。

在遍歷的時候,咱們分爲三種遍歷方法--先序遍歷,中序遍歷和後序遍歷:

travel_tree

刪除節點是一個比較複雜的操做,考慮的狀況比較多:

  • 該節點沒有葉子節點的時候,直接將該節點置空;
  • 該節點只有左子樹,直接將該節點賦予左子樹
  • 該節點只有右子樹,直接將該節點賦予右子樹
  • 該節點左右子樹都有,有兩種方法能夠處理
    • 方案一:從待刪除節點的子樹找節點值最大的節點A,替換待刪除節點值,並刪除節點A
    • 方案二:從待刪除節點的子樹找節點值最小的節點A,替換待刪除節點值,並刪除節點A【👆上面的示例代碼中就是這種方案】

刪除兩個節點的圖解以下:

remove_tree_node

由邊的集合及頂點的集合組成。

咱們來了解下圖的相關術語:

  • 頂點:圖中的一個節點。
  • 邊:表示頂點和頂點之間的連線。
  • 相鄰頂點:由一條邊鏈接在一塊兒的頂點稱爲相鄰頂點。
  • 度:一個頂點的度是相鄰頂點的數量。好比0頂點和其它兩個頂點相連,0頂點的度就是2
  • 路徑:路徑是頂點v1,v2...,vn的一個連續序列。
    • 簡單路徑:簡單路徑要求不包含重複的頂點。
    • 迴路:第一個頂點和最後一個頂點相同的路徑稱爲迴路。
  • 有向圖和無向圖
    • 有向圖表示圖中的方向的。
    • 無向圖表示圖中的方向的。
  • 帶權圖和無權圖
    • 帶權圖表示圖中的邊有權重
    • 無權圖表示圖中的邊無權重

以下圖:

graph_concept_intro

圖能夠用於現實中的不少系統建模,好比:

  • 對交通流量建模
    • 頂點能夠表示街道的十字路口, 邊能夠表示街道.
    • 加權的邊能夠表示限速或者車道的數量或者街道的距離.
    • 建模人員能夠用這個系統來斷定最佳路線以及最可能堵車的街道.

圖既然這麼方便,咱們來用代碼實現下:

// 圖
class Graph{
    constructor(v){
        this.vertices = v; // 頂點個數
        this.edges = 0; // 邊的個數
        this.adj = []; // 鄰接表或鄰接表數組
        this.marked = []; // 存儲頂點是否被訪問過的標識
        this.init();
    }
    init(){
        for(let i = 0; i < this.vertices; i++){
            this.adj[i] = [];
            this.marked[i] = false;
        }
    }
    // 添加邊
    addEdge(v, w){
        this.adj[v].push(w);
        this.adj[w].push(v);
        this.edges++;
        return this;
    }
    // 展現圖
    showGraph(){
        for(let i = 0; i < this.vertices; i++){
            for(let j = 0; j < this.vertices; j++){
                if(this.adj[i][j] != undefined){
                    console.log(i +' => ' + this.adj[i][j]);
                }
            }
        }
    }
    // 深度優先搜索
    dfs(v){
        this.marked[v] = true;
        if(this.adj[v] != undefined){
            console.log("visited vertex: " + v);
        }
        this.adj[v].forEach(w => {
            if(!this.marked[w]){
                this.dfs(w);
            }
        })
    }
    // 廣度優先搜索
    bfs(v){
        let queue = [];
        this.marked[v] = true;
        queue.push(v); // 添加到隊尾
        while(queue.length > 0){
            let v = queue.shift(); // 從對首移除
            if(v != undefined){
                console.log("visited vertex: " + v);
            }
            this.adj[v].forEach(w => {
                if(!this.marked[w]){
                    this.marked[w] = true;
                    queue.push(w);
                }
            })
        }
    }
}

let graphFirstInstance = new Graph(5);
graphFirstInstance.addEdge(0, 1).addEdge(0, 2).addEdge(1, 3).addEdge(2, 4);
graphFirstInstance.showGraph();
// 0 => 1
// 0 => 2
// 1 => 0
// 1 => 3
// 2 => 0
// 2 => 4
// 3 => 1
// 4 => 2
// ❓爲何會出現這種數據呢?它對應的圖是什麼呢?能夠思考🤔下,動手畫畫圖什麼的
console.log('--展現圖和深度優先搜索的分隔線--');
graphFirstInstance.dfs(0); // 從頂點 0 開始的深度搜索
// visited vertex: 0
// visited vertex: 1
// visited vertex: 3
// visited vertex: 2
// visited vertex: 4
console.log('--深度優先搜索和廣度優先搜索的分隔線--');
let graphSecondInstance = new Graph(5);
graphSecondInstance.addEdge(0, 1).addEdge(0, 2).addEdge(1, 3).addEdge(2, 4);
graphSecondInstance.bfs(0); // 從頂點 0 開始的廣度搜索
// visited vertex: 0
// visited vertex: 1
// visited vertex: 2
// visited vertex: 3
// visited vertex: 4
複製代碼

對於搜索圖,在上面咱們介紹了深度優先搜索 - DFS(Depth First Search)廣度優先搜索 - BFS(Breadth First Search),結合下面的圖再回頭看下上面的代碼,你會更加容易理解這兩種搜索圖的方式。

graph_search

後話

文章中的一些案例來自coderwhy的數據結構和算法系列文章,感謝其受權

author_wechat_permission

繪圖軟件 Numbers,本篇文章用到的圖片繪圖稿感興趣能夠下載。

演示代碼存放地址 -- 數據結構文件夾 進入structure目錄能夠直接 node + filename 運行

文章首發 github.com/reng99/blog…

更多內容 github.com/reng99/blog…

參考

相關文章
相關標籤/搜索