JavaScript 二叉搜尋樹以及實現翻轉二叉樹

KSIG發表於2018-06-10

本文包括:二叉搜尋樹(建立、遍歷、搜尋、插入等)、JavaScript 實現翻轉二叉樹

歡迎關注我的:個人部落格Github

什麼是二叉樹?

二叉樹的定義:二叉樹的每個結點至多隻有二棵子樹(不存在度大於2的結點),二叉樹的子樹有左右之分,次序不能顛倒。

二叉查詢樹(BST):又稱為是二叉排序樹(Binary Sort Tree)或二叉搜尋樹。二叉查詢樹是二叉樹的一種,但是它只允許你在左側節點儲存(比父節點)小的值,在右側節點儲存(比父節點)大(或者等於)的值。

建立一個二叉查詢樹

首先建立一個 BinarySearchTree 類。

// 使用了 ES6 的 Class 語法
class BinarySearchTree {
  constructor() {
    this.root = null
  }

  Node(key) {
    let left = null
    let right = null
    return {
      key,
      left,
      right
    }
  }
}
複製程式碼

來看一下二叉查詢樹的資料結構組織方式(沒有找到二叉搜尋樹的先用二叉樹的代替一下):

JavaScript 二叉搜尋樹以及實現翻轉二叉樹

二叉樹是通過指標(指向下一個節點)來表示節點之間的關係的,所以需要在宣告 Node 的時候,定義兩個指標,一個指向左邊,一個指向右邊。 還需要宣告一個 root 來儲存樹的根元素。

向樹中插入一個鍵(節點)

class BinarySearchTree  {
  // ...省略前面的
  
  insert (key) {
    let newNode = this.Node(key)
    if (this.root === null) {
      // 如果根節點為空,那麼插入的節點就為根節點
      this.root = newNode
    } else {
      // 如果根節點不為空
      this.insertNode(this.root, newNode)
    }
  }

  insertNode (node, newNode) {
    // 當新節點比父節點小,插入左邊
    if (newNode.key < node.key) {
      // 左邊沒有內容則插入
      if (node.left === null) {
        node.left = newNode
      } else {
        // 有內容就繼續遞迴,直到沒有內容然後可以插入
        this.insertNode(node.left, newNode)
      }
    } else {
      // 右邊和左邊相同,不重複說明
      if (node.right === null) {
        node.right = newNode
      } else {
        this.insertNode(node.right, newNode)
      }
    }
  }
}
複製程式碼

因為使用了 class 所以沒有學過 class 的同學可以先看一下 ES6 的 class,再來看文章。

仔細分析上面的程式碼,多看幾遍就可以瞭解其中的奧妙(也可以自己在遊覽器中執行一下,插入幾個值試一下)。

執行一遍試一下:

let m = new BinarySearchTree()
m.insert(5)
m.insert(4)
m.insert(3)
m.insert(6)
m.insert(7)
複製程式碼

會得到這樣的結構:

{
  key: 5,
  left: {
    key: 4,
    left: {
      key: 3,
      left: null,
      right: null
    },
    right: null
  },
  right: {
    key: 6,
    left: null,
    right: {
      key: 7,
      left: null,
      right: null
    }
  }
}
複製程式碼

emmm,真複雜(自己看的都頭暈),還是畫個圖吧。

JavaScript 二叉搜尋樹以及實現翻轉二叉樹

會生成這樣一個二叉查詢樹~,插入功能就算完成啦!

樹的遍歷

遍歷一棵樹是指訪問樹的每個節點並對它們進行某種操作的過程。訪問樹會有三種方法:中序、先序、後續。下面分別講解

中序遍歷

中序遍歷是一種以上行順序訪問 BST 所有節點的遍歷方式,也就是從最小到最大的順序進行訪問所有節點。具體方法,看程式碼吧,配上圖多看兩遍程式碼就能明白了(我是這麼認為的)。

class BinarySearchTree  {
  // ...省略前面的
  
  inOrderTraverse (callback) {
    this.inOrderTraverseNode(this.root, callback)
  }
  
  inOrderTraverseNode (node, callback) {
    if (node !== null) {
      this.inOrderTraverseNode(node.left, callback)
      callback(node.key)
      this.inOrderTraverseNode(node.right, callback)
    }
  }
}
複製程式碼

同樣,用圖展示一下遍歷的過程,具體過程看程式碼多思考一下。

JavaScript 二叉搜尋樹以及實現翻轉二叉樹

先序遍歷

先序遍歷會先訪問節點本身,然後再訪問它的左側子節點,最後再訪問右側的節點。

class BinarySearchTree  {
  // ...省略前面的
  
  preOrderTraverse (callback) {
    this.preOrderTraverseNode(this.root, callback)
  }
  
  preOrderTraverseNode (node, callback) {
    if (node !== null) {
      callback(node.key)
      this.preOrderTraverseNode(node.left, callback)
      this.preOrderTraverseNode(node.right, callback)
    }
  }
}
複製程式碼

仔細看程式碼,發現和中序遍歷的區別不過是先執行了 callback 然後再遍歷左右。

JavaScript 二叉搜尋樹以及實現翻轉二叉樹

後序遍歷

後序遍歷則是先訪問節點的後代節點,然後再訪問節點本身。實現:

class BinarySearchTree  {
  // ...省略前面的
  
  postOrderTraverse (callback) {
    this.postOrderTraverseNode(this.root, callback)
  }
  
  postOrderTraverseNode (node, callback) {
    if (node !== null) {
      this.postOrderTraverseNode(node.left, callback)
      this.postOrderTraverseNode(node.right, callback)
      callback(node.key)
    }
  }
}
複製程式碼

再仔細看程式碼,發現和中序遍歷的區別不過是先執行了遍歷了左右,最後執行了 callback

慣例,畫張圖~

JavaScript 二叉搜尋樹以及實現翻轉二叉樹

三種遍歷方式講完啦,不懂的可以多看幾遍程式碼哦~

搜尋二叉搜尋樹中的值

在樹中,通常有三種經常使用的搜尋型別:

  • 搜尋最大值
  • 搜尋最小值
  • 搜尋特定值

下面一一列舉

搜尋最小和最大值

首先我們知道二叉搜尋樹中的最小值在最左邊,最大值在最右邊。既然知道這個,那麼實現搜尋最大和最小就十分簡單了。所以直接上程式碼:

class BinarySearchTree  {
  // ...省略前面的
  
  // 搜尋最小
  min () {
    return this.minNode(this.root)
  }
  
  minNode (node) {
    if (node) {
      // 如果節點存在,而且左邊不為 null
      while (node && node.left !== null) {
        node = node.left
      }
      
      return node.key
    }
    
    // 如果樹為空,則返回 null
    return null
  }
  
  // 搜尋最大
  max () {
    return this.maxNode(this.root)
  }
  
  maxNode (node) {
    if (node) {
      while (node && node.right !== null) {
        node = node.right
      }
      return node.key
    }
    
    return null
  }
}
複製程式碼

搜尋特定的值

基本上的思路和遍歷節點差不多,具體看程式碼。

class BinarySearchTree  {
  // ...省略前面的
  
  search (key) {
  	return this.searchNode(this.root, key)
  }
  
  searchNode (node, key) {
    if (node === null) {
      return false
    }
    
    // 如果 key 比節點的值小,那麼搜尋左邊的子節點,下面的相反
    if (key < node.key) {
      return this.searchNode(node.left, key)
    } else if (key > node.key) {
      return this.searchNode(node.right, key)
    } else {
      return true
    }
  }
}
複製程式碼

翻轉二叉樹

翻轉一個二叉樹,直觀上看,就是把二叉樹的每一層左右順序倒過來。

例如:

Input:

     4
   /   \
  2     7
 / \   / \
1   3 6   9
複製程式碼

Output:

     4
   /   \
  7     2
 / \   / \
9   6 3   1
複製程式碼

仔細看就是先把最底下的節點反轉,然後上一個節點再翻轉。例如:1 - 3 反轉成 3 - 1,6 - 9 反轉成 9 - 6, 然後再讓 2 - 7 反轉。當然反過來也一樣,先反轉 2 - 7 也是可以的。

所以具體的過程是:

  1. 翻轉根節點的左子樹(遞迴呼叫當前函式)
  2. 翻轉根節點的右子樹(遞迴呼叫當前函式)
  3. 交換根節點的左子節點與右子節點

最後看一下實現的程式碼:

class BinarySearchTree  {
  // ...省略前面的
 
  invertTree (node = this.root) {
    if (node === null) {
      return
    }
    this.invertTree(node.left)
    this.invertTree(node.right)
    this.exchange(node)
  }
  
  exchange (node) {
    let temp = node.left
    node.left = node.right
    node.right = temp
  }
}
複製程式碼

這樣就簡單實現啦,舒服舒服~

程式碼

全部程式碼在這裡~

class BinarySearchTree {
  constructor() {
    this.root = null
  }

  Node(key) {
    let left = null
    let right = null
    return {
      key,
      left,
      right
    }
  }

  insert(key) {
    let newNode = this.Node(key)
    if (this.root === null) {
      // 如果根節點為空,那麼插入的節點就為根節點
      this.root = newNode
    } else {
      this.insertNode(this.root, newNode)
    }
  }

  insertNode(node, newNode) {
    console.log(node)
    if (newNode.key < node.key) {
      if (node.left === null) {
        node.left = newNode
      } else {
        this.insertNode(node.left, newNode)
      }
    } else {
      if (node.right === null) {
        node.right = newNode
      } else {
        this.insertNode(node.right, newNode)
      }
    }
  }

  inOrderTraverse(callback) {
    this.inOrderTraverseNode(this.root, callback)
  }

  inOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.inOrderTraverseNode(node.left, callback)
      callback(node.key)
      this.inOrderTraverseNode(node.right, callback)
    }
  }

  preOrderTraverse(callback) {
    this.preOrderTraverseNode(this.root, callback)
  }

  preOrderTraverseNode(node, callback) {
    if (node !== null) {
      callback(node.key)
      this.preOrderTraverseNode(node.left, callback)
      this.preOrderTraverseNode(node.right, callback)
    }
  }
  postOrderTraverse(callback) {
    this.postOrderTraverseNode(this.root, callback)
  }

  postOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.postOrderTraverseNode(node.left, callback)
      this.postOrderTraverseNode(node.right, callback)
      callback(node.key)
    }
  }

  // 搜尋最小
  min() {
    return this.minNode(this.root)
  }

  minNode(node) {
    if (node) {
      // 如果節點存在,而且左邊不為 null
      while (node && node.left !== null) {
        node = node.left
      }

      return node.key
    }

    // 如果樹為空,則返回 null
    return null
  }

  // 搜尋最大
  max() {
    return this.maxNode(this.root)
  }

  maxNode(node) {
    if (node) {
      while (node && node.right !== null) {
        node = node.right
      }

      return node.key
    }

    return null
  }
  
  search(key) {
    return this.searchNode(this.root, key)
  }

  searchNode(node, key) {
    console.log('node-', node, '---', node === null, '-key-', key)
    if (node === null) {
      return false
    }
    // 如果 key 比節點的值小,那麼搜尋左邊的子節點,下面的相反
    if (key < node.key) {
      return this.searchNode(node.left, key)
    } else if (key > node.key) {
      return this.searchNode(node.right, key)
    } else {
      console.log('didi')
      return true
    }
  }

  invertTree (node = this.root) {
    if (node === null) {
      return
    }
    this.invertTree(node.left)
    this.invertTree(node.right)
    this.exchange(node)
  }
  
  exchange (node) {
    let temp = node.left
    node.left = node.right
    node.right = temp
  }
}
複製程式碼

最後

文章是自己的學習的一個記錄,如果能夠順便幫助大家學習一下,那就再好不過了。

但是因為本人技術技術有限,所以文章難免會有疏漏,歡迎指出。

參考

相關文章