樹是一種分層數據的抽象模型。
現實生活中最常見的例子是家譜,或是公司的組織架構。
一個樹結構包含一系列父子關係的節點。每個節點都有一個父節點(除了頂部的第一個節點)以及零個或多個子節點。
來看下圖中的樹結構:
位於樹頂部的節點叫做根節點,它沒有父節點。
樹中的每個元素都叫做節點,節點分爲內部節點和外部節點。
至少有一個子節點的節點稱爲內部節點。
沒有子元素的稱爲外部節點或也葉結點。
子樹由節點和它的後代構成。
節點的一個屬性是深度,節點的深度取決於它的祖先節點的數量。比如,節點3有3個祖先節點(5、7和11),所以它的深度爲3。
樹的高度取決於所有節點深度的最大值。
二叉樹
二叉樹中的節點最多只能有兩個子節點:一個是左側子節點,一個是右側子節點。
二叉搜索樹(BST)是二叉樹的一種,但是它只允許在左側節點存儲(比父節點)小的值,在右側節點存儲(比父節點)大(或者等於)的值。
接下來,創建一個二叉樹,基本結構如下:
/*二叉搜索樹*/
function BinarySearchTree () {
/*
聲明一個Node類,來表示樹的每個節點。
*/
var Node = function(key){
this.key = key;
this.left = null;
this.right = null;
};
var root = null;//根元素
/*
insert(key)
像樹中插入一個新的鍵
*/
this.insert = function(key){};
/*
search(key)
向樹中查找一個鍵。
如果節點存在,則返回true;如果不存在,則返回false。
*/
this.search = function(key){};
/*
inOrderTraverse()
通過中序遍歷方式遍歷所有節點
*/
this.inOrderTraverse = function(){};
/*
preOrderTraverse()
通過先序遍歷方式遍歷所有節點
*/
this.preOrderTraverse = function(){};
/*
postOrderTraverse()
通過後序遍歷方式遍歷所有節點
*/
this.postOrderTraverse = function(){};
/*
min()
返回樹中最小的值/鍵
*/
this.min = function(){};
/*
max()
返回樹中最大的值/鍵
*/
this.max = function(){};
/*
remove(key)
從樹中移除某個鍵
*/
this.remove = function(key){};
}
1、向樹中插入一個鍵
this.insert = function(key){
var newNode = new Node(key);
//如果插入的節點是第一個節點,就將根節點指向新節點。
if (root === null) {
root = newNode;
}
else {
insertNode(root,newNode);
}
};
//輔助函數,幫助我們找到新節點應該插入的正確位置。
var insertNode = function(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);
}
}
};
下面的代碼創建了上面圖中的樹。
var tree = new BinarySearchTree();
/*向樹中插入鍵*/
tree.insert(11);
tree.insert(7);
tree.insert(15);
tree.insert(5);
tree.insert(3);
tree.insert(9);
tree.insert(8);
tree.insert(10);
tree.insert(13);
tree.insert(12);
tree.insert(14);
tree.insert(20);
tree.insert(18);
tree.insert(25);
tree.insert(6);
2、樹的遍歷
1)中序遍歷
以上行順序訪問BST所有節點的遍歷方式,也就是以最小到最大的順序訪問所有節點。
中序遍歷的一種應用就是對樹進行排序操作。
this.inOrderTraverse = function(callback){
inOrderTraverseNode(root,callback);
};
var inOrderTraverseNode = function(node,callback){
if (node !== null) {
inOrderTraverseNode(node.left,callback);
callback(node.key);
inOrderTraverseNode(node.right,callback);
}
};
測試代碼如下:
/*中序遍歷*/
function printNode(value){
console.log(value);
}
tree.inOrderTraverse(printNode);
結果如下:
中序遍歷的訪問路徑如下圖所示。
2)先序遍歷
以優先於後代節點的順序訪問每個節點,也就是說,先訪問節點本身,再訪問它的左側子節點,最後是右側子節點。
先序遍歷的一種應用是打印一個結構化文檔。
this.preOrderTraverse = function(callback){
preOrderTraverseNode(root,callback);
};
var preOrderTraverseNode = function(node,callback){
if (node !== null) {
callback(node.key);
preOrderTraverseNode(node.left,callback);
preOrderTraverseNode(node.right,callback);
}
};
測試代碼如下:
/*先序遍歷*/
function printNode(value){
console.log(value);
}
tree.preOrderTraverse(printNode);
結果如下:
訪問路徑如下所示。
3)後序遍歷
先訪問節點的後代節點,再訪問節點本身。也就是說,先訪問左側子節點,然後是右側子節點,最後是父節點本身。
後序遍歷的一種應用是計算一個目錄和它的子目錄中所有文件所佔空間的大小。
this.postOrderTraverse = function(callback){
postOrderTraverseNode(root,callback);
};
var postOrderTraverseNode = function(node,callback){
if (node !== null) {
postOrderTraverseNode(node.left,callback);
postOrderTraverseNode(node.right,callback);
callback(node.key);
}
};
測試代碼如下:
/*後序遍歷*/
function printNode(value){
console.log(value);
}
tree.postOrderTraverse(printNode);
結果如下:
訪問路徑如下所示。
3、搜索最小值和最大值
1)最小值
根據二叉樹的特點,可以發現,樹的最小值在樹的最左端。
this.min = function(){
return minNode(root);
};
var minNode = function(node){
if (node) {
while (node && node.left !== null) {
node = node.left;
}
return node.key;
}
return null;
};
2)最大值
根據二叉樹的特點,可以發現,樹的最大值在樹的最右端。
this.max = function(){
return maxNode(root);
};
var maxNode = function(node){
if (node) {
while (node && node.right !== null) {
node = node.right;
}
return node.key;
}
};
測試代碼:
/*最小值*/
console.log(tree.min());
/*最大值*/
console.log(tree.max());
輸出結果如下:
4、搜索一個特定的值
this.search = function(key){
return searchNode(root,key);
};
var searchNode = function(node,key){
//驗證作爲參數傳入的node是否合法
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;
}
};
測試代碼:
console.log(tree.search(1) ? 'Key 1 found.' : 'Key 1 not found.');
console.log(tree.search(8) ? 'Key 8 found.' : 'Key 8 not found.');
輸出結果:
5、移除一個節點
this.remove = function(key){
root = removeNode(root,key);
};
var removeNode = function(node,key){
//檢測的節點爲null,則說明鍵不存在於樹中
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;//給要移除的節點賦爲null值
return node;//將對應的父節點指針賦爲null值
}
//移除一個只有一個子節點的節點
/*跳過這個節點,直接將父節點指向它的指針指向子節點*/
//沒有左側子節點,只有一個右側子節點
if (node.left === null) {
node = node.right;
return node;
}
//沒有右側子節點,只有一個左側子節點
else if (node.right === null) {
node = node.left;
return node;
}
//移除有兩個子節點的節點(左側子節點和右側子節點)
var findMinNode = function(node){
if (node) {
while (node && node.left !== null) {
node = node.left;
}
return node;//返回節點
}
return null;
};
var aux = findMinNode(node.right);//找到右邊子樹最小的節點
node.key = aux.key;//用右側子樹中最小節點的鍵區更新這個節點的值
node.right = removeNode(node.right,aux.key);//把右側子樹中的最小節點移除
return node;//向移動後的右側樹最小節點的父節點返回更新後節點的引用
}
};
測試代碼:
tree.remove(6);
console.log(tree.search(6) ? 'Key 6 found.' : 'Key 6 not found.');
tree.remove(5);
console.log(tree.search(5) ? 'Key 5 found.' : 'Key 5 not found.');
tree.remove(15);
console.log(tree.search(15) ? 'Key 15 found.' : 'Key 15 not found.');
下面來分析一下移除這3個節點的過程,如下所示。
輸出結果:
BST存在一個問題:取決於你添加的節點樹,樹的一條邊可能會非常深;也就是說,樹的一條分支會有很多層,而其他的分支卻只有幾層。
這會在需要在某條邊上添加、移除和搜索某個節點時引起一些性能問題。