二叉查詢樹概念及實現

寒東設計師發表於2018-03-29

起手詩

從今天起,做一個牛X的人,早起,健身,修煉演算法
從今天起,關心程式碼質量,我有一個夢想,朝九晚五,年薪百萬
從今天起,和每一個親人通訊,告訴他們我的決心
那成功的天使告訴我的
我想告訴每一個人
給每一個檔案、每一個變數取一個溫暖的名字
陌生人,我也為你祝福
願你有一個燦爛的前程
願你的頭髮不再減少
願你明天仍是公司棟樑
而我,只願朝九晚五,年薪百萬

前言

      書接上文從今天起,每天詳解一道演算法題,今天實現一個二叉查詢樹。

基本概念

      二叉查詢樹是二叉樹的一種特殊情況,二叉查詢樹的左邊節點的值總是比右邊的小,先看下定義:

二叉查詢樹(Binary Search Tree),(又:二叉搜尋樹,二叉排序樹)它或者是一棵空樹,或者是具有下列性質的二叉樹: 若它的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值; 若它的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值; 它的左、右子樹也分別為二叉查詢樹。

二叉查詢樹概念及實現

      節點:包含一個資料元素及若干指向子樹的分支;
      根節點:第一層的節點(圖中的8);
      葉子節點:結點的子樹的根稱為該結點的葉子節點,這是一個相對概念;
      結點層:根結點的層定義為1;根的孩子為第二層結點,依此類推;
      樹的深度:樹中最大的結點層;
      結點的度:結點子樹的個數(二叉樹為2);
      樹的度:樹中最大的結點度(二叉樹為2);

程式碼實現

      實現二叉樹,先要實現上面最重要的一個概念--節點:
      它有三個屬性,值、左節點的指標、右節點的指標:

class Node {
    constructor(data, left, right) {
        this.value = data
        this.left = left
        this.right = right
    }
    show(){
        return this.data
    }
}
複製程式碼

      下面我們實現二叉查詢樹的類:
      我們可以想向到它應該有一個根節點,還應該有一些操作該類的方法,我會分別解釋,先看程式碼(為了避免程式碼太長,我把各個方法分開寫了,他們應該都在BST類裡的):

insert方法:

      該方法負責向該二叉樹中插入一條資料,實現的思路是:
      1、通過Node類建立包含該條資料的一個節點;
      2、判斷如果該例項的根節點root為null,則把這個節點設定為根節點;
      3、如果根節點不為null,則建立一個指向當前節點的指標(第一次指向根節點),然後比較data是否大於當前節點的data;
      4、如果小於當前節點的data,則說明這個節點應該往左邊放,否則說明應該往右邊放;
      5、通過迴圈比較,最終當前節點會找到null,這時候把新節點插入到這裡(也就是讓當parentNode的子節點指向新節點);

class BST {
    constructor() {
        this.root = null
    }
    insert(data) {
        let newNode = new Node(data, null, null)
        if (root == null) {
            this.root = newNode
        } else {
            let currentNode = this.root
            let parentNode
            while (true) {
                parentNode = currentNode
                if (data < currentNode.data) {
                    currentNode = currentNode.left
                    if (currentNode == null) parentNode.left = newNode
                    break
                } else {
                    currentNode = currentNode.right
                    if (currentNode == null) parentNode.right = newNode
                    break
                }
            }
        }
    }
}
複製程式碼

遍歷方法:

      樹的遍歷分為3種型別:中序、先序、後序,其中“中”、“先”、“後”指的是訪問根節點的時機。
      下面程式碼中的each方法中有三個console,從上至下他們分別對應先序、後序、中序。他們分別代表訪問根節點(2)的時機。也就是一個在最先,一個在最後,一個在中間。

each(node) {
        node = node || this.root
        //console.log(node.data) // 2,1,3
        if (node.left) this.each(node.left)
        //console.log(node.data) // 1,2,3
        if (node.right) this.each(node.right)
        //console.log(node.data) // 1,3,2
    }
複製程式碼

查詢最大最小值

      這兩個方法比較簡單,最大值就是遍歷右子樹,找到最後一個;最小值就是遍歷左子樹找到最後一個
      注:max和min方法有引數,這是為了查詢制定節點的最大最小值,下面的remove方法中用到了min方法查詢制定節點的最小值

max(node) {
    let currentNode = node || this.root
    while (currentNode.right) {
        currentNode = currentNode.right
    }
    return currentNode.data
}
min(node) {
    let currentNode = node || this.root
    while (currentNode.left) {
        currentNode = currentNode.left
    }
    return currentNode.data
}
複製程式碼

查詢給定值的節點

      has方法和上面的insert方法的思路類似,都是建立一個指標指向根節點,比較傳入的data值,如果比根節點大就把指標向右移,否則向左。
      一旦找到相等的值就返回ture,如果遍歷到最後一個節點都沒有找到,返回false。

has(data) {
    let currentNode = this.root
    while (currentNode) {
        if (data == currentNode.data) {
            return true
        } else if (data < currentNode.data) {
            currentNode = currentNode.left
        } else {
            currentNode = currentNode.right
        }
    }
    return false
}
複製程式碼

刪除給定值的節點

      刪除方法相對較複雜,思路為:
      1、定義remove方法,方法中把根節點更改為一個遞迴方法removeNode的返回值
      2、removeNode是一個被刪除了制定節點的新樹,它的實現需要考慮以下幾種情況:
          a:如果輸入節點為null,則返回null
          b:如果data與node的data相等,說明這個節點就是需要被刪除的節點(這種情況又分為幾種情況,下面單獨說)
          c:如果data比node的data小,則說明需要刪除的節點在node的左邊,那麼就需要遞迴它左邊這個節點,直到找到相等的或者null
          d:如果data比node的data大,思路同c

      接下來說b(命中相等節點)的情況:
      1、如果這個節點沒有子節點,那麼直接刪除即可
      2、如果這個節點只有一個節點,那麼它的這個子節點應該取代它的位置(程式碼中第二、三行註釋下的程式碼)
      3、如果這個節點有左右兩個節點,那麼我們找到它的右子樹的最小值minData,這個節點的值就應該為minData,同時讓它的右子樹為它之前的右子樹(刪除最小值之後的)。

remove(data) {
    this.root = removeNode(this.root,data)
}
removeNode(node, data) {
        if (node == null) {
            return null
        }
        if (data == node.data) {
            //沒有節點,返回null
            if (node.left == null && node.right == null) {
                return null
            }
            //沒有左節點,右節點替換當前節點
            if (node.left == null) {
                return node.right
            }
            //沒有右節點,左節點替換當前節點    
            if (node.right == null) {
                return node.left
            }
            //左右節點都存在
            let minData = this.min(node.right)
            node.data = minData
            node.right = this.removeNode(node.right, minData)
            return node
        } else if (data < node.data) {
            node.left = this.removeNode(node.left, data)
            return node
        } else if (data > node.data) {
            node.right = this.removeNode(node.right, data)
            return node
        }
    }
複製程式碼

參考資料

《資料結構與演算法-javascript描述》
《學習javascript資料結構與演算法》

相關文章