【數據結構】樹和二叉樹
本博客記錄下關於樹和二叉樹的基本概念。
本文主要轉載自:二叉樹-你可能需要知道這些
1. 什么是樹
樹是一種非線性的數據結構,是由n(n >=0)個結點組成的有限集合。
如果n==0,樹為空樹。
如果n>0,
樹有一個特定的結點,根結點。根結點只有直接后繼,沒有直接前驅。
除根結點以外的其他結點划分為m(m>=0)個互不相交的有限集合,T0,T1,T2,...,Tm-1,每個結合是一棵樹,稱為根結點的子樹。
2. 二叉樹
對於樹這種數據結構,使用最頻繁的是二叉樹這種數據結構。
2.1 什么是二叉樹
每個節點最多只有2個子節點的樹叫做二叉樹。
2.2 二叉樹的一些特性
A、在二叉樹的第i層上最多有2^(i-1)個結點(i>=1)。
B、高度為k的二叉樹,最多有2^k-1個結點(k>=0)。
C、對任何一棵二叉樹,如果其葉結點有n個,度為2的非葉子結點有m個,則n = m + 1。
D、具有n個結點的完全二叉樹的高度為logn + 1
E、對於有n個結點的完全二叉樹,按層次對結點進行編號(從上到下,從左到右),對於任意編號為i的結點:
2.3 二叉樹的存儲實現
// 節點
public class BinaryNode {
// 存放的信息
Object data;
// 左兒子
BinaryNode left;
// 右兒子
BinaryNode right;
}
2.4 二叉樹的遍歷
遍歷是對樹的一種最基本的運算,所謂遍歷二叉樹,就是按一定的規則和順序走遍二叉樹的所有結點,使每一個結點都被訪問一次,而且只被訪問一次。由於二叉樹是非線性結構,因此,樹的遍歷實質上是將二叉樹的各個結點轉換成為一個線性序列來表示。
對於樹的遍歷,按照訪問根節點的次序不同,主要有以下三種遍歷算法:
- 先序遍歷
- 后序遍歷
- 中序遍歷
除了上面最基本的三種遍歷方式外,二叉樹還有深度優先遍歷和廣度優先遍歷。
我們首先給出一個假設:
L:左子樹
D:根
R:右子樹
2.4.1 先序遍歷(DLR)
先序遍歷:根節點->左子樹->右子樹
public static void DLR(BinaryNode node) {
// 訪問根節點
System.out.print(node.data + " ");
// 遍歷左子樹
if (node.left != null) {
DLR(node.left);
}
// 遍歷右子樹
if (node.right != null) {
DLR(node.right);
}
}
2.4.2 后序遍歷(LRD)
后序遍歷:左子樹->右子樹->根節點
public static void LRD(BinaryNode node) {
// 遍歷左子樹
if (node.left != null) {
LRD(node.left);
}
// 遍歷右子樹
if (node.right != null) {
LRD(node.right);
}
// 訪問根節點
System.out.print(node.data + " ");
}
2.4.3 中序遍歷(LDR)
中序遍歷:左子樹->根節點->右子樹
// 遍歷左子樹
if (node.left != null) {
LDR(node.left);
}
// 訪問根節點
System.out.print(node.data + "");
// 遍歷右子樹
if (node.right != null) {
LDR(node.right);
}
2.4.4 深度優先遍歷
英文縮寫為DFS即Depth First Search.其過程簡要來說是對每一個可能的分支路徑深入到不能再深入為止,而且每個節點只能訪問一次。
深度優先遍歷需要使用到棧這種數據結構,棧具有先進后出的特點。
如上圖,我們來分析下深度優先遍歷的過程。
- 首先根節點A入棧,stack(A)。
- 將A節點彈出,因為A存在 B C兩個子節點,根據定義和棧特點,首先將C(右兒子)壓入棧中,然后將B(左兒子)壓入棧中,stack(C B)
- 彈出棧頂元素B節點彈出,將節點 E 和 D壓入棧中,stack(C E D)。
- 彈出棧頂元素D,由於節點D只存在一個子節點H,因此H直接入棧,stack(C E H).
- 彈出棧頂元素H,元素H不存在子元素,stack(C E).
- 彈出棧頂元素E,元素E不存在子元素,stack(C).
- 彈出棧頂元素C,子節點G F分別入棧,stack(G F).
- F出棧,stack(G)。
- G出棧,stack()。
- 遍歷結束。
深度優先遍歷的結果為: A B D H E C F G.
通過上面的分析,是不是覺得深度優先遍歷其實也沒那么難啊,下面我們來動手實現這段代碼(過程理解清楚了,代碼實現起來很簡單)。
private void depthFirst(AVLTreeNode<T> node) {
if (node == null) {
return;
}
Stack<AVLTreeNode> stack = new Stack<>();
// 根節點入棧,然后進行后續操作
stack.push(node);
while (!stack.isEmpty()) {
AVLTreeNode root = stack.pop();
// 彈出棧頂元素,進行訪問。
System.out.println(root.key + " ");
// 首先將右節點入棧
if (root.right != null) {
stack.push(node.right);
}
// 然后左節點入棧
if (root.left != null) {
stack.push(node.left);
}
}
}
2.4.5 廣度優先遍歷
英文縮寫為BFS即Breadth FirstSearch。其過程檢驗來說是對每一層節點依次訪問,訪問完一層進入下一層,而且每個節點只能訪問一次。對於上面的例子來說,廣度優先遍歷的 結果是:A,B,C,D,E,F,G,H(假設每層節點從左到右訪問)。
廣度優先遍歷需要使用到隊列這種數據結構,隊列具有先進先出的特點。
如上圖所示,我們來分析廣度優先遍歷的過程。
首先將A節點插入隊列中,queue(A);
將A節點彈出,同時將A的子節點B,C插入隊列中,此時B在隊列首,C在隊列尾部,queue(B,C);
將B節點彈出,同時將B的子節點D,E插入隊列中,此時C在隊列首,E在隊列尾部,queue(C,D,E);
將C節點彈出,同時將C的子節點F,G插入隊列中,此時D在隊列首,G在隊列尾部,queue(D,E,F,G);
將D節點彈出,同時將D節點的子節點H插入隊列中,此時E在隊列首,H在隊列尾部,queue(E,F,G,H);
E F G H分別彈出(這四個節點均不存在子節點)。
廣度優先遍歷結果為:A B C D E F G H
動手來實現廣度優先遍歷,代碼如下:
public void breadthFirst() {
breadthFirst(root);
}
private void breadthFirst(AVLTreeNode<T> node) {
if (node == null) {
return;
}
Queue<AVLTreeNode> queue = new ArrayDeque<>();
// 根節點入棧
queue.add(node);
while (!queue.isEmpty()) {
AVLTreeNode root = queue.poll();
System.out.print(node.key + " ");
if (root.left != null) {
queue.add(node.left);
}
if (root.right != null) {
queue.add(node.right);
}
}
}
2.5 完全二叉樹
定義:若設二叉樹的深度為h,除第 h 層外,其它各層 (1~h-1) 的結點數都達到最大個數,第 h 層所有的結點都連續集中在最左邊,這就是完全二叉樹。
完全二叉樹有如下特點:
- 只允許最后一層有空缺結點且空缺在右邊,即葉子結點只能在層次最大的兩層上出現。
- 對任一結點,如果其右子樹的深度為j,則其左子樹的深度必為j或j+1。 即度為1的點只有1個或0個。
2.6 滿二叉樹
除最后一層無任何子節點外,每一層上的所有結點都有兩個子結點二叉樹。
國內教程定義:一個二叉樹,如果每一個層的結點數都達到最大值,則這個二叉樹就是滿二叉樹。也就是說,如果一個二叉樹的層數為K,且結點總數是(2^k) -1 ,則它就是滿二叉樹。
滿二叉樹是一種特殊的完全二叉樹。
二叉樹的順序存儲,尋找后代節點和祖先節點都非常方便,但對於普通的二叉樹,順序存儲浪費大量的存儲空間,同樣也不利於節點的插入和刪除。因此順序存儲一般用於存儲完全二叉樹。
鏈式存儲相對順序存儲節省存儲空間,插入刪除節點時只需修改指針,但尋找指定節點時很不方便。不過普通的二叉樹一般是用鏈式存儲結構。