接下來讓咱們一塊兒來探討js數據結構中的樹。這裏的樹類比現實生活中的樹,有樹幹,樹枝,在程序中樹是一種數據結構,對於存儲須要快速查找的數據非有用,它是一種分層數據的抽象模型。一個樹結構包含一系列存在父子關係的節點。每一個節點都有一個父節點以及零個或多個子節點。以下因此爲一個樹結構:)前端
和樹相關的概念:1.子樹:由節點和他的後代構成,如上圖標示處。2.深度:節點的深度取決於它祖節點的數量,好比節點5有2個祖節點,他的深度爲2。3.高度:樹的高度取決於全部節點深度的最大值。vue
二叉樹中的節點最多隻能有2個子節點,一個是左側子節點,一個是右側子節點,這樣定義的好處是有利於咱們寫出更高效的插入,查找,刪除節點的算法。node
二叉搜索樹是二叉樹的一種,可是它只容許你在左側子節點存儲比父節點小的值,但在右側節點存儲比父節點大的值。接下來咱們將按照這個思路去實現一個二叉搜索樹。react
這裏咱們將使用構造函數去建立一個類:算法
function BinarySearchTree(){
// 用於建立節點的類
let Node = function(key) {
this.key = key;
this.left = null;
this.right = null;
}
// 根節點
let root = null;
}
複製代碼
咱們將使用和鏈表相似的指針方式去表示節點之間的關係,若是不瞭解鏈表,請看我後序的文章《如何實現單向鏈表和雙向鏈表》。vuex
// 插入一個鍵
this.insert = function(key) {
let newNode = new Node(key);
root === null ? (root = newNode) : (insertNode(root, newNode))
}
複製代碼
向樹中插入一個新的節點主要有如下三部分:1.建立新節點的Node類實例 --> 2.判斷插入操做是否爲根節點,是根節點就將其指向根節點 --> 3.將節點加入非根節點的其餘位置。vue-cli
insertNode的具體實現以下:typescript
function insertNode(node, newNode){
if(newNode.key < node.key) {
node.left === null ? (node.left = newNode) : (insertNode(node.left, newNode))
}else {
node.right === null ? (node.right = newNode) : (insertNode(node.right, newNode))
}
}
複製代碼
這裏咱們用到遞歸,接下來要實現的search,del等都會大量使用遞歸,因此說不了解的能夠先自行學習瞭解。咱們建立一個二叉樹實例,來插入一個鍵:gulp
let tree = new BinarySearchTree();
tree.insert(20);
tree.insert(21);
tree.insert(520);
tree.insert(521);
複製代碼
插入的結構會按照二叉搜索樹的規則去插入,結構相似於上文的第一個樹圖。api
訪問樹的全部節點有三種遍歷方式:中序,先序和後序。
根據以上的介紹,咱們能夠有如下的實現代碼。
this.inOrderTraverse = function(cb){
inOrderTraverseNode(root, cb);
}
// 輔助函數
function inOrderTraverseNode(node, cb){
if(node !== null){
inOrderTraverseNode(node.left, cb);
cb(node.key);
inOrderTraverseNode(node.right, cb);
}
}
複製代碼
使用中序遍歷能夠實現對樹進行從小到大排序的功能。
// 先序排序 --- 優先於後代節點的順序訪問每一個節點
this.preOrderTraverse = function(cb) {
preOrderTraverseNode(root, cb);
}
// 先序排序輔助方法
function preOrderTraverseNode(node, cb) {
if(node !== null) {
cb(node.key);
preOrderTraverseNode(node.left, cb);
preOrderTraverseNode(node.right, cb);
}
}
複製代碼
使用先序排序能夠實現結構化輸出的功能。
// 後續遍歷 --- 先訪問後代節點,再訪問節點自己
this.postOrderTraverse = function(cb) {
postOrderTraverseNode(root, cb);
}
// 後續遍歷輔助方法
function postOrderTraverseNode(node, cb) {
if(node !== null){
postOrderTraverseNode(node.left, cb);
postOrderTraverseNode(node.right, cb);
cb(node.key);
}
}
複製代碼
後序遍歷能夠用於計算有層級關係的全部元素的大小。
在樹中有三種常常執行的搜索類型:最大值,最小值,特定的值。
最小值經過定義能夠知道便是左側樹的最底端的節點,具體實現代碼以下:
// 最小值
this.min = function(){
return minNode(root)
}
function minNode(node) {
if(node) {
while(node && node.left !== null){
node = node.left;
}
return node.key
}
return null
}
複製代碼
類似的,實現最大值的方法以下:
// 最大值
this.max = function() {
return maxNode(root)
}
function maxNode(node) {
if(node){
while(node && node.right !== null){
node = node.right;
}
return node.key
}
return null
}
複製代碼
2.搜索一個特定的值
// 搜索樹中某個值
this.search = function(key) {
return searchNode(root, key)
}
// 搜索輔助方法
function searchNode(node, key){
if(node === null) {
return false
}
if(key < node.key) {
return searchNode(node.left, key)
} else if(key > node.key) {
return searchNode(node.right, key)
}else {
return true
}
}
複製代碼
this.remove = function(key){
root = removeNode(root, key);
}
// 發現最小節點
function findMinNode(node) {
if(node) {
while(node && node.left !== null){
node = node.left;
}
return node
}
return null
}
// 移除節點輔助方法
function removeNode(node, key) {
if(node === null) {
return null
}
if(key < node.key){
node.left = removeNode(node.left, key);
return node
} else if( key > node.key){
node.right = removeNode(node.right, key);
return node
} else {
// 一個頁節點
if(node.left === null && node.right === null) {
node = null;
return node
}
// 只有一個子節點的節點
if(node.left === null) {
node = node.right;
return node
}else if(node.right === null) {
node = node.left;
return node
}
// 有兩個子節點的節點
let aux = findMinNode(node.right);
node.key = aux.key;
node.right = removeNode(node.right, aux.key);
return node
}
}
複製代碼
刪除節點須要考慮的狀況比較多,這裏咱們會使用和min相似的實現去寫一個發現最小節點的函數,當要刪除的節點有兩個子節點時,咱們要將當前要刪除的節點替換爲子節點中最大的一個節點的值,而後將這個子節點刪除。
至此,一個二叉搜索樹已經實現,可是還存在一個問題,若是樹的一遍很是深,將會存在必定的性能問題,爲了解決這個問題,咱們能夠利用AVL樹,一種自平衡二叉樹,也就是說任何一個節點的左右兩側子樹的高度之差最多爲1。
若是想學習更多js算法和數據結構,能夠長按關注哦~