<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
前面我們介紹了二元樹這個資料結構以及二元樹的遍歷演演算法,這篇文章我們來學習一下一個特殊的二元樹——二元搜尋樹(BST Binary Search Tree),也叫二叉排序樹、二叉查詢樹。
二元搜尋樹首先它是一棵二元樹,而且還滿足下面這些特質:
如下圖所示:
上圖就是一顆二元搜尋樹,我們就拿根節點來說,根節點的值71,它的左子樹的值分別是22、35、46、53和66,這幾個都是滿足左子樹小於當前值;它的右子樹的值分別是78、87和98,這幾個值是滿足右子樹大於當前值的;以此類推,所以上圖就是一棵二元搜尋樹。
根據二元搜尋樹的特質,我們還能得到以下結論:
我們現在使用JavaScript來構建一顆二元搜尋樹,要知道一顆二元搜尋樹也是由一個一個節點組成,這裡我們通過class
建立一個節點類,
範例程式碼如下:
class BinarySearchTree { constructor() { // 初始化根節點 this.root = null } // 建立一個節點 Node(val) { return { left: null, // 左子樹 right: null, // 右子樹 parent: null, // 父節點 val, } } }
這裡一個節點由四部分組成,分別是指向左子樹的指標、指向右子樹的指標、指向父節點的指標以及當前值。
關於二元樹的遍歷操作我們在上一篇文章中已經介紹了,這裡不在重複,這裡主要介紹如下操作:
向一個二元搜尋樹插入資料實現思路如下:
root
是否為空,如果為空則建立root;root
非空,則需要判斷插入節點的val
比根節點的val
是大還是小;範例程式碼如下:
// 建立要給插入節點的方法 insertNode(val) { const that = this // 允許接受一個陣列,批次插入 if (Object.prototype.toString.call(val) === '[object Array]') { val.forEach(function (v) { that.insertNode(v) }) return } if (typeof val !== 'number') throw Error('插入的值不是一個數位') const newNode = this.Node(val) if (this.root) { // 根節點非空 this.#insertNode(this.root, newNode) } else { // 根節點是空的,直接建立 this.root = newNode } } // 私有方法,插入節點 #insertNode(root, newNode) { if (newNode.val < root.val) { // 新節點比根節點小,左子樹 if (root.left === null) { // 如果左子樹上沒有內容,則直接插入,如果有,尋找下一個插入位置 root.left = newNode root.left.parent = root } else { this.#insertNode(root.left, newNode) } } else { // 新節點比根節點大,右子樹 if (root.right === null) { // 如果右子樹上沒有內容,則直接插入,如果有,尋找下一個插入位置 root.right = newNode root.right.parent = root } else { this.#insertNode(root.right, newNode) } } }
在類中定義了insertNode
方法,這個方法接受數值或者數值型別的陣列,將其插入這個二元搜尋樹中;插入方法我們定義了一個私有的#insertNode
方法,用於節點的插入。
為了看到效果,我們這裡定義了一個靜態方法,用於中序遍歷(因為中序遍歷的順序是左根右,在二元搜尋樹中使用中序排序,最終結果是從小到大依次排序的)這個樹,並返回一個陣列,
範例程式碼如下:
// 中序遍歷這個樹 static inorder(root) { if (!root) return const result = [] const stack = [] // 定義一個指標 let p = root // 如果棧中有資料或者p不是null,則繼續遍歷 while (stack.length || p) { // 如果p存在則一致將p入棧並移動指標 while (p) { // 將 p 入棧,並以移動指標 stack.push(p) p = p.left } const node = stack.pop() result.push(node.val) p = node.right } return result }
測試程式碼如下:
const tree = new BinarySearchTree()
tree.insertNode([71, 35, 87, 22, 53, 46, 66, 78, 98])
const arr = BinarySearchTree.inorder(tree.root)
console.log(arr) // [ 22, 35, 46, 53, 66,71, 78, 87, 98 ]
最終的樹結構如下:
現在我們封裝一個find
方法,用於查詢二元搜尋樹中的某個資料,假如我們查詢66這個資料,利用上面那個樹,
其查詢思路如下圖所示:
遞迴方式實現如下:
/** * 根據 val 查詢節點 * @param {number} val 需要查詢的數值 * @returns 如果找到返回當前節點的參照,如果未找到返回 undefined */ find(val) { if (typeof val !== 'number') throw Error('插入的值不是一個數位') function r(node, val) { // console.log(node) if (!node) return if (node.val < val) { return r(node.right, val) } else if (node.val > val) { return r(node.left, val) } else { return node } } return r(this.root, val) }
迭代方式實現如下:
/** * 根據 val 查詢節點 * @param {number} val 需要查詢的數值 * @returns 如果找到返回當前節點的參照,如果未找到返回 undefined */ find(val) { if (typeof val !== 'number') throw Error('插入的值不是一個數位') let node = this.root while (node) { if (node.val < val) { // 進入右子樹 node = node.right } else if (node.val > val) { // 進入左子樹 node = node.left } else { return node } } return }
兩者相對來說,使用迭代的方式更優一些。
在開始刪除二元搜尋樹中的某個節點之前,我們先來了解一下什麼是前驅和後繼節點;
如下圖所示:
瞭解了什麼是前驅和後繼節點之後,現在我們來開始刪除某個節點。
當刪除的節點是葉子節點時,只需要將指向它的指標修改為null
,即可,如下圖所示:
當需要刪除的節點存在一個子節點時, 需要將要刪除節點的子節點的parent
指標指向要刪除節點的父節點,然後將當前要刪除節點的父節點指向子節點即可,
如下圖所示:
當需要刪除的節點存在一個子節點時, 刪除步驟如下:
如下圖所示:
現在我們將這些情況已經分析完成了,現在通過程式碼實現一下。
實現程式碼如下:
remove(val) { // 1. 刪除節點 const cur = this.find(val) if (!val) return false // 未找到需要刪除的節點 if (!cur.left && !cur.right) { // 1. 當前節點是葉子節點的情況 this.#removeLeaf(cur) } else if (cur.left && cur.right) { // 2. 當前節點存在兩個子節點 // 2.1 找到當前節點的後繼節點 const successorNode = this.#minNode(cur.right) // 2.2 將後繼節點的值賦值給當前值 cur.val = successorNode.val if (!successorNode.left && !successorNode.right) { // 2.3 後繼節點是葉子節點,直接刪除 this.#removeLeaf(successorNode) } else { // 2.4 後繼節點不是葉子節點 // 2.4.1記錄該節點的子節點, let child = successorNode.left !== null ? successorNode.left : successorNode.right // 2.4.2 記錄該節點的父節點 let parent = successorNode.parent // 2.4.3 如果當前父節點的left是後繼結點,則把後繼結點的父節點的left指向後繼結點的子節點 if (parent.left === successorNode) { parent.left = child } else { // 2.4.4 如果不是,則讓父節點的right指向後繼結點的子節點 parent.right = child } // 2.4.5 修改子節點的parent指標 child.parent = parent } // 2.3 刪除後繼節點 } else { // 記錄當前節點的是否是父節點的左子樹 const isLeft = cur.val < cur.parent.val // 3. 僅存在一個子節點 if (cur.left) { // 3.1 當前節點存在左子樹 cur.parent[isLeft ? 'left' : 'right'] = cur.left cur.left.parent = cur.parent } else if (cur.right) { // 3.2 當前節點存在右子樹 cur.parent[isLeft ? 'left' : 'right'] = cur.right cur.right.parent = cur.parent } } } // 刪除葉子節點 #removeLeaf(node) { if (!node) return const parent = node.parent if (node.val < parent.val) { // 當前要刪除的葉子節點是左節點 parent.left = null } else { // 當前要刪除的葉子節點是右節點 parent.right = null } } // 查詢最小值 #minNode(node) { if (!node) return if (!node.left) return node let p = node.left while (p.left) { p = p.left } return p }
本篇文章中的完整程式碼如下:
class BinarySearchTree { constructor() { // 初始化根節點 this.root = null } // 建立一個節點 Node(val) { return { left: null, // 左子樹 right: null, // 右子樹 parent: null, // 父節點 val, } } /** * 建立要給插入節點的方法 * @param {number | array[number]} val * @returns */ insertNode(val) { const that = this // 允許接受一個陣列,批次插入 if (Object.prototype.toString.call(val) === '[object Array]') { val.forEach(function (v) { that.insertNode(v) }) return } if (typeof val !== 'number') throw Error('插入的值不是一個數位') const newNode = this.Node(val) if (this.root) { // 根節點非空 this.#insertNode(this.root, newNode) } else { // 根節點是空的,直接建立 this.root = newNode } } /** * 私有方法,插入節點 * @param {Object{Node}} root * @param {Object{Node}} newNode */ #insertNode(root, newNode) { if (newNode.val < root.val) { // 新節點比根節點小,左子樹 if (root.left === null) { // 如果左子樹上沒有內容,則直接插入,如果有,尋找下一個插入位置 root.left = newNode root.left.parent = root } else { this.#insertNode(root.left, newNode) } } else { // 新節點比根節點大,右子樹 if (root.right === null) { root.right = newNode root.right.parent = root } else { this.#insertNode(root.right, newNode) } } } /** * 根據 val 查詢節點 * @param {number} val 需要查詢的數值 * @returns 如果找到返回當前節點的參照,如果未找到返回 undefined */ find(val) { if (typeof val !== 'number') throw Error('插入的值不是一個數位') let node = this.root while (node) { if (node.val < val) { // 進入右子樹 node = node.right } else if (node.val > val) { // 進入左子樹 node = node.left } else { return node } } return } // /** // * 根據 val 查詢節點 遞迴版 // * @param {number} val 需要查詢的數值 // * @returns 如果找到返回當前節點的參照,如果未找到返回 undefined // */ // find(val) { // if (typeof val !== 'number') throw Error('插入的值不是一個數位') // function r(node, val) { // // console.log(node) // if (!node) return // if (node.val < val) { // return r(node.right, val) // } else if (node.val > val) { // return r(node.left, val) // } else { // return node // } // } // return r(this.root, val) // } remove(val) { // 1. 刪除節點 const cur = this.find(val) if (!val) return false // 未找到需要刪除的節點 if (!cur.left && !cur.right) { // 1. 當前節點是葉子節點的情況 this.#removeLeaf(cur) } else if (cur.left && cur.right) { // 2. 當前節點存在兩個子節點 // 2.1 找到當前節點的後繼節點 const successorNode = this.#minNode(cur.right) // 2.2 將後繼節點的值賦值給當前值 cur.val = successorNode.val if (!successorNode.left && !successorNode.right) { // 2.3 後繼節點是葉子節點,直接刪除 this.#removeLeaf(successorNode) } else { // 2.4 後繼節點不是葉子節點 // 2.4.1記錄該節點的子節點, let child = successorNode.left !== null ? successorNode.left : successorNode.right // 2.4.2 記錄該節點的父節點 let parent = successorNode.parent // 2.4.3 如果當前父節點的left是後繼結點,則把後繼結點的父節點的left指向後繼結點的子節點 if (parent.left === successorNode) { parent.left = child } else { // 2.4.4 如果不是,則讓父節點的right指向後繼結點的子節點 parent.right = child } // 2.4.5 修改子節點的parent指標 child.parent = parent } // 2.3 刪除後繼節點 } else { // 記錄當前節點的是否是父節點的左子樹 const isLeft = cur.val < cur.parent.val // 3. 僅存在一個子節點 if (cur.left) { // 3.1 當前節點存在左子樹 cur.parent[isLeft ? 'left' : 'right'] = cur.left cur.left.parent = cur.parent } else if (cur.right) { // 3.2 當前節點存在右子樹 cur.parent[isLeft ? 'left' : 'right'] = cur.right cur.right.parent = cur.parent } } } // 刪除葉子節點 #removeLeaf(node) { if (!node) return const parent = node.parent if (node.val < parent.val) { // 當前要刪除的葉子節點是左節點 parent.left = null } else { // 當前要刪除的葉子節點是右節點 parent.right = null } } // 查詢最小值 #minNode(node) { if (!node) return if (!node.left) return node let p = node.left while (p.left) { p = p.left } return p } // 中序遍歷這個樹 static inorder(root) { if (!root) return const result = [] const stack = [] // 定義一個指標 let p = root // 如果棧中有資料或者p不是null,則繼續遍歷 while (stack.length || p) { // 如果p存在則一致將p入棧並移動指標 while (p) { // 將 p 入棧,並以移動指標 stack.push(p) p = p.left } const node = stack.pop() result.push(node.val) p = node.right } return result } } const tree = new BinarySearchTree() tree.insertNode([71, 35, 84, 22, 53, 46, 66, 81, 83, 82, 88, 98]) console.log(BinarySearchTree.inorder(tree.root)) // [ 22, 35, 46, 53, 66, 71, 81, 82, 83, 84, 88, 98 ] tree.remove(71 console.log(BinarySearchTree.inorder(tree.root)) // [ 22, 35, 46, 53, 66, 81, 82, 83, 84, 88, 98 ]
文章介紹了二元搜尋樹的性質以及二元搜尋樹的構建、查詢和刪除
到此這篇關於JavaScript二元搜尋樹構建操作詳解的文章就介紹到這了,更多相關JS二元搜尋樹內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45