前言
總括: 本文講解了數據結構中的[樹]的概念,盡可能通俗易懂的解釋樹這種數據結構的概念,使用javascript實現了樹,如有紕漏,歡迎批評指正。
- 原文博客地址:學習javascript數據結構(四)——樹
- 知乎專欄&&簡書專題:前端進擊者(知乎)&&前端進擊者(簡書)
- 博主博客地址:Damonare的個人博客
人之所能,不能兼備,棄其所短,取其所長。
正文
樹簡介
在上一篇學習javascript數據結構(三)——集合中我們說了集合這種數據結構,在學習javascript數據結構(一)——棧和隊列和學習javascript數據結構(二)——鏈表說了棧和隊列以及鏈表這類線性表數據結構。接下來這一篇說的是樹
這種數據結構。首先想讓大家明白的是數據結構是個什么玩意兒,數據結構可以分為數據的邏輯結構和數據的物理結構,所謂的數據邏輯結構在我理解就是計算機對于數據的組織方式的研究。也就是說研究的是數據和數據之間的關系。而數據的物理結構是數據的邏輯結構在計算機中的具體實現,也就是說一種邏輯結構可能會有多種存儲結構與之相對應。
那么我們這一篇所說的樹
就是一種數據邏輯結構,即研究的是數據和數據之間的關系。之前所說的棧
、隊列
、鏈表
都是一種線性結構,相信大家也能發現這種線性結構的數據關系有一個共同點,就是數據都是一對一的,而上一篇說到的集合這種數據結構,數據是散亂的,他們之間的關系就是隸屬于同一個集合,如上一篇例子所說,這些小孩子都是同一個幼兒園的,但是這些小孩子之間的關系我們并不知道。線性表(棧、隊列、鏈表)就是對這些小孩子關系的一種表達(一對一)。而集合也是對于這些小孩子關系的一種表達。和線性表不同的是,樹這種數據結構是一對多的,也就是說他所描述的是某個小孩子和其它小孩子之間的關系。
樹這種結構實際上我們平時也有見到,比如下圖這種簡單的思維導圖:
如下也是一棵樹:
關于樹概念總結如下:
1)樹形結構是一對多的非線性結構。
2)樹形結構有樹和二叉樹兩種,樹的操作實現比較復雜,但樹可以轉換為二叉樹進行處理。
3)樹的定義:樹(Tree)是 n(n≥0)個相同類型的數據元素的有限集合。
4)樹中的數據元素叫節點(Node)。
5)n=0 的樹稱為空樹(Empty Tree);
6)對于 n>0 的任意非空樹 T 有:
(1)有且僅有一個特殊的節點稱為樹的根(Root)節點,根沒有前驅節點;
(2)若n>1,則除根節點外,其余節點被分成了m(m>0)個互不相交的集合
T1,T2,。。。,Tm,其中每一個集合Ti(1≤i≤m)本身又是一棵樹。樹T1,T2,。。。,Tm稱為這棵樹的子樹(Subtree)。
7)樹的定義是遞歸的,用樹來定義樹。因此,樹(以及二叉樹)的許多算法都使用了遞歸。
參看維基百科對于樹
的定義:
在計算機科學中,樹(英語:tree)是一種抽象數據類型(ADT)或是實作這種抽象數據類型的數據結構,用來模擬具有樹狀結構性質的數據集合。它是由n(n>=1)個有限節點組成一個具有層次關系的集合。把它叫做“樹”是因為它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的。它具有以下的特點:
- 每個節點有零個或多個子節點;
- 沒有父節點的節點稱為根節點;
- 每一個非根節點有且只有一個父節點;
- 除了根節點外,每個子節點可以分為多個不相交的子樹;
樹的種類:
- 無序樹:樹中任意節點的子節點之間沒有順序關系,這種樹稱為無序樹,也稱為自由樹;
- 有序樹:樹中任意節點的子節點之間有順序關系,這種樹稱為有序樹;
有關樹的術語:
- 節點的度:一個節點含有的子樹的個數稱為該節點的度;
- 樹的度:一棵樹中,最大的節點的度稱為樹的度;
- 葉節點或終端節點:度為零的節點;
- 非終端節點或分支節點:度不為零的節點;
- 父親節點或父節點:若一個節點含有子節點,則這個節點稱為其子節點的父節點;
- 孩子節點或子節點:一個節點含有的子樹的根節點稱為該節點的子節點;
- 兄弟節點:具有相同父節點的節點互稱為兄弟節點;
- 節點的層次:從根開始定義起,根為第1層,根的子節點為第2層,以此類推;
- 樹的高度或深度:樹中節點的最大層次;
- 堂兄弟節點:父節點在同一層的節點互為堂兄弟;
- 節點的祖先:從根到該節點所經分支上的所有節點;
- 子孫:以某節點為根的子樹中任一節點都稱為該節點的子孫。
- 森林:由m(m>=0)棵互不相交的樹的集合稱為森林;
(我是維基百科搬運工,哈哈哈)
二叉樹
二叉樹(英語:Binary tree)是每個節點最多有兩個子樹的樹結構。通常子樹被稱作“左子樹”(left subtree)和“右子樹”(right subtree)。二叉樹常被用于實現二叉查找樹和二元堆積。
我們主要研究的就是二叉樹,也就是數據為一對二的關系。那么在二叉樹中又有些分類;
二叉樹分類:
- 一棵深度為k,且有{\displaystyle 2^{\begin{aligned}k+1\end{aligned}}-1}
- 深度為k,有n個節點的二叉樹,當且僅當其每一個節點都與深度為k的滿二叉樹中,序號為1至n的節點對應時,稱之為完全二叉樹。
- 平衡二叉樹又被稱為AVL樹(區別于AVL算法),它是一棵二叉排序樹,且具有以下性質:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,并且左右兩個子樹都是一棵平衡二叉樹。
二叉樹的遍歷
1)一棵二叉樹由根結點、左子樹和右子樹三部分組成,
2) D、L、R 分別代表遍歷根結點、遍歷左子樹、遍歷右子樹,則二叉樹的
3) 遍歷方式有6 種:DLR、DRL、LDR、LRD、RDL、RLD。先左或先右算法基本一樣,所以就剩下三種DLR(先序或是前序)、LDR(中序)、LRD(后序)。
- 前序遍歷:首先訪問根節點,然后遍歷左子樹,最后遍歷右子樹,可記錄為根—左—右;
- 中序遍歷:首先訪問左子樹,然后訪問根節點,最后遍歷右子樹,可記錄為左—根—右;
- 后序遍歷:首先遍歷左子樹,然后遍歷右子樹,最后遍歷根節點,可記錄為左—右—根。
以上圖1為例解釋前序遍歷:
首先訪問根節點a
=>然后遍歷左子樹b
=>左子樹b
的左子樹d
=>d
的右孩子e
>此時b
的左子樹遍歷完,遍歷b
的右子樹f
=>f
的左孩子g
=>左子樹b
遍歷完,遍歷根節點的右孩子c
,完成=>abdefgc
中序遍歷,后序遍歷就不多說了,不同的只是訪問的順序。
注意:
(1)已知前序、后序遍歷結果,不能推導出一棵確定的樹;
(2)已知前序、中序遍歷結果,能夠推導出后序遍歷結果;
(3)已知后序、中序遍歷結果,能夠推導出前序遍歷結果;
二叉搜索樹的創建
二叉查找樹(BinarySearch Tree,也叫二叉搜索樹,或稱二叉排序樹Binary Sort Tree)或者是一棵空樹,或者是具有下列性質的二叉樹:
(1)若它的左子樹不為空,則左子樹上所有結點的值均小于它的根結點的值;
(2)若它的右子樹不為空,則右子樹上所有結點的值均大于它的根結點的值;
(3)它的左、右子樹也分別為二叉查找樹。
首先我們聲明一個BinarySearchTree類:
function BinarySearchTree() {
var Node = function(key){
this.key = key;
this.left = null;
this.right = null;
};
var root=null;
}
和鏈表一樣,二叉樹也通過指針來表示節點之間的關系。在雙向鏈表中,每一個節點有兩個指針,一個指向下一個節點,一個指向上一個節點。對于樹,使用同樣的方式,只不過一個指向左孩子,一個指向右孩子。現在我們給這棵樹弄一些方法:
- insert(key):向樹中插入一個新的鍵(節點);
- search(key):在書中查找一個鍵,如果節點存在,返回true;如果不存在,返回false;
- inOrdertraverse:通過中序遍歷方式遍歷所有節點;
- preorderTraverse:通過先序遍歷方式遍歷所有的節點;
- postOrdertraverse:通過后序遍歷的方式遍歷所有的節點;
- min:返回樹中的最小值;
- max:返回樹中的最大值;
- remove(key):從樹中移除某個鍵;
BinarySearchTree類的完整代碼(充分添加注釋):
function BinarySearchTree() {
var Node = function(key){
this.key = key;
this.left = null;
this.right = null;
};
var root = null;
this.insert = function(key){
var newNode = new Node(key);
//判斷是否是第一個節點,如果是作為根節點保存。不是調用inserNode方法
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);
}
}
};
this.getRoot = function(){
return root;
};
this.search = function(key){
return searchNode(root, key);
};
var searchNode = function(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 { //element is equal to node.item
return true;
}
};
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);
}
};
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);
}
};
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);
}
};
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;
};
this.max = function() {
return maxNode(root);
};
var maxNode = function (node) {
if (node){
while (node && node.right !== null) {
node = node.right;
}
return node.key;
}
return null;
};
this.remove = function(element){
root = removeNode(root, element);
};
var findMinNode = function(node){
while (node && node.left !== null) {
node = node.left;
}
return node;
};
var removeNode = function(node, element){
if (node === null){
return null;
}
if (element < node.key){
node.left = removeNode(node.left, element);
return node;
} else if (element > node.key){
node.right = removeNode(node.right, element);
return node;
} else {
//處理三種特殊情況
//1 - 葉子節點
//2 - 只有一個孩子的節點
//3 - 有兩個孩子的節點
//case 1
if (node.left === null && node.right === null){
node = null;
return node;
}
//case 2
if (node.left === null){
node = node.right;
return node;
} else if (node.right === null){
node = node.left;
return node;
}
//case 3
var aux = findMinNode(node.right);
node.key = aux.key;
node.right = removeNode(node.right, aux.key);
return node;
}
};
}
后記
樹是一種比較常見的數據結構,不管是考試還是日常編碼或是面試都是沒法避免的一個知識點,此篇總結不甚完善,紕漏之處還望指出方便之后更改。敬請期待數據結構篇最后一篇文章:[學習javascript數據結構(五)——圖]
參考文章
- [樹(數據結構)](https://zh.wikipedia.org/wiki/樹_(數據結構)
- 二叉樹