大學的東西都忘的差很少了吧?下面咱們一塊兒用js簡單複習一下大學裏《數據結構與算法》中的樹。 本文僅使用js實現二叉樹,實際工做中可能並用不到(瞎說什麼大實話),可是面試挺喜歡問的,畢竟這是計算機相關專業的基礎嘛。同時,代碼裏用了不少遞歸,這對後期對代碼量優化仍是頗有幫助的。 虛擬dom,使用的就是樹結構。 若是你對二叉樹很熟悉,那麼此文對你可能毫無價值。node
參考資料:學習JavaScript數據結構與算法面試
生活中常見樹結構有企業的組織架構圖、家譜圖等。 一個樹結構包含一系列存在父子關係的節點。每一個節點都有一個父節點(除了頂部的第一個節點,稱爲根結點)以及零個或多個子節點:算法
二叉樹中的節點最多隻能有兩個子節點:一個是左側子節點,另外一個是右側子節點。瀏覽器
二叉搜索樹(BST)是二叉樹的一種,可是它只容許你在左側節點存儲(比父節點)小的值, 在右側節點存儲(比父節點)大(或者等於)的值。數據結構
二叉搜索樹將是本文所複習的主要內容。架構
二話不說,直接上代碼,實現一個二叉搜索樹的類,複製代碼,放到瀏覽器便可。能夠直接看代碼,後面再細談。dom
function BinarySearchTree() {
// 初始化根結點root爲null
let root = null;
// 用於初始化節點,key爲值,left right分別爲左右子節點
function Node(key) {
this.key = key;
this.left = null;
this.right = null;
};
// 獲取樹並打出
this.getRoot = function () {
return root
}
// 向樹中插入新數據
this.insert = function (key) {
let newNode = new Node(key);
if (root === null) {
root = newNode;
} else {
insertNode(root, newNode);
}
};
// 插值處理遞歸函數
function insertNode(node, newNode) {
if (newNode.key < node.key) {
if (node.left === null) {
node.left = newNode;
} else {
insertNode(node.left, newNode);
}
} else {
if (node.right === null) {
node.right = newNode;
} else {
insertNode(node.right, newNode);
}
}
};
// 中序遍歷
this.inorderTraversal = function (callback) {
inorderTraversalNode(root, callback);
};
// 中序遍歷處理遞歸函數
function inorderTraversalNode(node, callback) {
if (node !== null) {
inorderTraversalNode(node.left, callback);
callback(node.key);
inorderTraversalNode(node.right, callback);
}
};
// 先序遍歷
this.preorderTraversal = function (callback) {
preorderTraversalNode(root, callback);
};
// 先序遍歷遞歸函數
function preorderTraversalNode(node, callback) {
if (node !== null) {
callback(node.key);
preorderTraversalNode(node.left, callback);
preorderTraversalNode(node.right, callback);
}
};
// 後序遍歷
this.postorderTraversal = function (callback) {
postorderTraversalNode(root, callback);
};
// 後序遍歷遞歸函數
function postorderTraversalNode(node, callback) {
if (node !== null) {
postorderTraversalNode(node.left, callback);
postorderTraversalNode(node.right, callback);
callback(node.key);
}
};
// 查詢節點,若存在則返回true 反之 false
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.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;
};
// 移除一個節點
this.remove = function (key) {
root = removeNode(root, key);
}
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 { //鍵等於node.key
//第一種狀況——一個葉節點
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;
}
//第三種狀況——一個有兩個子節點的節點
var aux = findMinNode(node.right);
node.key = aux.key;
node.right = removeNode(node.right, aux.key);
return node;
}
};
function findMinNode(node) {
while (node && node.left !== null) {
node = node.left;
}
return node;
};
}
// 上面代碼已經實現二叉搜索樹,下面開始用起來!
// 先new一個樹
let tree = new BinarySearchTree();
console.log('原始樹爲', tree.getRoot())
// 依次向樹中插入數據
tree.insert(10);
tree.insert(2);
tree.insert(5);
tree.insert(3);
tree.insert(9);
tree.insert(8);
tree.insert(13);
tree.insert(12);
tree.insert(14);
// 獲取當前樹
console.log('插值後的樹爲:', tree.getRoot())
// 用於遍歷的回調函數
function printNode(value) {
console.log(value);
}
// 先序遍歷
console.log('下方爲先序遍歷結果')
tree.preorderTraversal(printNode);
// 中序遍歷
console.log('下方爲中序遍歷結果')
tree.inorderTraversal(printNode);
// 後序遍歷
console.log('下方爲後序遍歷結果')
tree.postorderTraversal(printNode);
// 查詢
console.log('查詢10的結果', tree.search(10))
// 最大值最小值
console.log('最大值爲:', tree.max())
console.log('最小值爲:', tree.min())
// 移除節點
console.log('移除節點 10 以前', tree.getRoot())
tree.remove(10)
console.log('移除節點 10 以後', tree.getRoot())
複製代碼
二叉樹須要記錄當前節點的值,以及其2個子節點,此處以left right 分別表明左子節點和右子節點。 當建立新當節點時,new 一個Node類便可。koa
function Node(key) {
this.key = key;
this.left = null;
this.right = null;
};
複製代碼
插入一個新的值,就是插入一個新節點。這個時候就須要new一個Node類了。若是根結點不存在,即root爲null,則表示當前值爲第一個節點。不然,則須要一個插值函數用來循環插值。 在二叉樹中,基本都是遞歸,因此這塊須要詳細思考一下代碼的具體運行方式。 代碼解釋見註釋:函數
// 向樹中插入新數據
this.insert = function (key) {
let newNode = new Node(key);
if (root === null) {
root = newNode;
} else { // 若是根結點不爲空,則就須要調用插值函數來計算往哪插值了
insertNode(root, newNode);
}
};
// 插值處理遞歸函數
function insertNode(node, newNode) {
// 若是要插入的值小於當前所在節點的值
if (newNode.key < node.key) {
// 若是新值小於當前節點的值且左側子節點爲空,則當前節點的左子節點就爲新插入的值 (1)
if (node.left === null) {
node.left = newNode;
} else {
// 若是當前節點左側子節點不爲空,則把當前節點的左子節點傳入insertNode中,開始遞歸,直到知足上面的 (1)才能順利插入值
insertNode(node.left, newNode);
}
} else { // 若是要插入的值大於當前所在節點的值,則說明要插的值須要在右側子節點,遞歸邏輯同上
if (node.right === null) {
node.right = newNode;
} else {
insertNode(node.right, newNode);
}
}
};
複製代碼
這3種遍歷沒有本質區別,只不過是回調函數的位置不一樣而已。對比代碼一看即懂。post
// 中序遍歷
this.inorderTraversal = function (callback) {
// 傳root,完整的樹,做爲初始值
inorderTraversalNode(root, callback);
};
// 下面纔是遍歷的真正方法
function inorderTraversalNode(node, callback) {
// 最開始root爲完整的樹,當node不爲空,就一直遞歸。爲空時,則說明遍歷完畢
if (node !== null) {
// node不爲空時,繼續調用,查看左子節點是否爲空。若是左子節點不爲空,則還會再次進入方法,直到左子節點爲null
inorderTraversalNode(node.left, callback);
// 這個回調函數的調用有點相似koa的洋蔥圈模型。當inorderTraversalNode遞歸到左子節點爲空時,纔不會繼續調用。
// 因此最早最早執行 callback(node.key) 中的節點值是最小的,而後依次愈來愈大。
callback(node.key);
inorderTraversalNode(node.right, callback);
}
};
複製代碼
節點查詢就比較簡單了,就是循環全部節點,看看是否有相同的值,若是有就返回true,不然false
// 查詢節點
this.search = function (key) {
return searchNode(root, key);
};
// 查詢節點遞歸函數
function searchNode(node, key) {
// 此處每次遞歸時,都會走到。若是全部節點都走完了,也沒走到(1) ,那就放棄吧,確實沒有
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; // 相等(1)
}
};
複製代碼
這個也比較簡單,在二叉樹中,最小值確定在左側,最大值確定在右側。因此查詢最小值,只要循環樹左側的節點,直到節點沒了。下圖的箭頭分別表明尋找最大最小值的訪問路徑。 這裏有個特殊狀況須要處理,那就是樹自己就爲null,直接返回null。最大值同理。
// 查詢最小值
this.min = function () {
return minNode(root);
};
function minNode(node) {
if (node) {
// 若是節點存在且左子節點不爲空,則一直循環,把node設爲node.left
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.remove = function (key) {
// 刪除key後,新的樹爲removeNode的返回值
root = removeNode(root, key);
}
function removeNode(node, key) {
if (node === null) {
return null;
}
if (key < node.key) {
// 若是要刪除的值小於當前節點的值,則說明還沒找到那個要刪除的節點。
// 遞歸removeNode時,只有找到了那個節點,即執行了 (1),纔會有返回值。並把當前節點的左子節點設爲返回值。而後返回node。
// 注意,給node.left賦值時,是遞歸賦值,node在不一樣的循環指向不一樣的節點
node.left = removeNode(node.left, key);
return node;
} else if (key > node.key) {
node.right = removeNode(node.right, key);
return node;
} else { //鍵等於node.key (1)
/* * 若是邏輯走到了這邊的代碼,則說明已經找到了須要刪除的節點。 * 可是,該節點有3種狀況。 * 1. 該節點沒有子節點:則說明該節點爲直接設爲null便可,也就是說,他的父節點的left設爲null, node.left = null; * 2. 該節點有一個左或者右子節點:若左子節點爲null,右子節點直接上移,替換該節點便可node = node.right;,其餘相似 * 3. 該節點有2個子節點。這個時候,須要在他的子樹中(注意,是第一層子樹),右子樹尋找最小的節點,用這個最小的節點替換須要刪除的節點。 */
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;
}
//第三種狀況——一個有兩個子節點的節點
var aux = findMinNode(node.right);
node.key = aux.key;
node.right = removeNode(node.right, aux.key);
return node;
}
};
// 這個方法用來尋找子樹中的最小節點
function findMinNode(node) {
while (node && node.left !== null) {
node = node.left;
}
return node;
};
複製代碼
此類後續再詳細講。