主要知識點:
- 樹的定義及常用術語
- 樹的存儲表示
- 二叉樹、滿二叉樹和完成二叉樹的定義
- 二叉樹的遍歷此操作實現
- 哈夫曼樹及其編碼
- 樹、森林與二叉樹之間的轉換
一、樹
1. 概念:
- 定義: 樹是由n(n≥0)個結點組成的有限集合
- 特點:
- 有且僅有一個稱為根(Root)的結點;
- 其余的結點可分為m(m≥0)個互不相交的子集Tl,T2,…,Tm,其中每個子集本身又是一棵樹,并稱其為根的子樹(Subree)。
- 樹的常用術語
結點(node)
- 由一個數據元素及關聯其子樹的邊組成
結點路徑
- 若樹中存在一個結點序列k1,k2,…,ki,使得ki是ki+1的雙親(1≤i<j),則稱該結點序列是從k1到kj的一條路徑(Path)或道路。
路徑的長度
- 指路徑所經過的邊(即連接兩個結點的線段)的數目
結點的度(degree)
- 結點擁有的子樹的數目
樹的度
- 一棵樹中最大的結點度數(擁有最多子樹的節點,即結點的度最大值)
葉子結點(leaf)
- 結點的度為0的結點(沒有子樹的節點),也叫終端結點
分支結點
- 結點的度不為0的結點(有子樹的節點),也叫非終端結點
孩子結點
- 一個結點的孩子結點是指這個結點的子樹的根結點
雙親結點(parents)
- 一個結點有孩子結點,則這個結點稱為它孩子結點的雙親結點
子孫結點
- 即一個結點A所有子樹的結點稱為該結點A的子孫結點
祖先結點
- 即一個結點A的祖先結點是指路徑中除結點A外的的結點
兄弟結點(sibling)
- 同一雙親結點的孩子結點之間互成為兄弟結點
結點的層次
- 從根結點算起,根為第一層,它的孩子為第二層
樹的深度
- 樹中結點的最大層次數
有序樹與無序樹
- 如果將樹中結點的各子樹看成從左至右是有次序的(即不能互換),則稱該樹為有序樹,否則稱為無序樹
森林
- m(m>=0)棵互不相交的樹的集合
二、 二叉樹
定義:
- 二叉樹(BinaryTree): 是n(n≥0)個結點的有限集, 它或者是空集(n=0),或者由一個根結點及兩棵互不相交的、分別稱作這個根的左子樹和右子樹的二叉樹組成。
- 滿二叉樹: 是二叉樹的特殊形態,除葉節點外的所有結點都有左右子樹的二叉樹,稱為滿二叉樹
- 完全二叉樹: 也是二叉樹的特殊形態,
- 單分支樹:所有節點都沒有左結點(或右結點)的二叉樹
二叉樹五種基本形態
5.4二叉樹的5種基本形態.png
二叉樹的性質
- 二叉樹第i層上的結點數目最多為
- 深度為k的二叉樹至多有
個結點。
- 在任意-棵二叉樹中,若終端結點的個數為n0,度為2的結點數為n2,則n0=n2+1。
- 具有n個結點的完全二叉樹,其深度為
或
- 對于具有n個結點的完全二叉樹,若從根結點開始自上而下,從左到右開始編號,對于任意編號i(0<=i<n)的結點有:
1. 若i=0,則結點為根結點,沒有雙親,若i>0,則它的雙親結點編號為
2. 若2i+1 >=n ,則編號i結點無左孩子,否則編號2i+1的結點就是它的左孩子
3. 若2i+2 >=n ,則編號i結點無右孩子,否則編號2i+2的結點就是它的右孩子
- 滿二叉樹和完全二叉樹示意圖
5.7滿二叉樹和完全二叉樹.png
二叉樹存儲結構
- 順序存儲結構示意圖
5.9二叉樹順序存儲結構示意圖.png
- 鏈式存儲結構示意圖
5.10二叉樹鏈式存儲的結點結構.png
5.11二叉樹及其三叉鏈式存儲結構.png
二叉樹的遍歷
- 前序遍歷
/**
* 遞歸的前序遍歷
* <p>
* 1. 從根結點出發
* 2. 先遍歷完左子樹
* 3. 再遍歷右子樹
* <p>
* (注意:順序: 中左右)
*
* @param treeNode
*/
public void preOrderTraverse(BiTreeNode treeNode) {
if (treeNode == null) return;
//結點數據
System.out.println(treeNode.data.toString());
//先遍歷左子樹
preOrderTraverse(treeNode.LChild);
//然后遍歷右子樹
preOrderTraverse(treeNode.RChild);
}
/**
* 非遞歸的前序遍歷
*/
public void preOrderTraverse() {
//獲取根結點
BiTreeNode<T> node = root;
if (node == null) return;
//構造一個棧,由于存儲右子樹結點
LinkStack<BiTreeNode> stack = new LinkStack<>();
stack.push(node);
while (!stack.isEmpty()) {
//彈出棧頂結點
node = stack.pop();
//訪問該結點
System.out.println(node.data.toString());
while (node != null) {
//如果左結點不為空,則訪問
if (node.LChild != null)
System.out.println(node.LChild.data);
//如果右結點不為空,則先壓入棧中
if (node.RChild != null)
stack.push(node.RChild);
//繼續遍歷左結點
node = node.LChild;
}
}
}
- 中序遍歷
/**
* 中序遍歷(遞歸方式)
* <p>
* 1. 從左子樹出發開始遍歷
* 2. 遍歷到根結點
* 3. 又從根結點出發,遍歷右子樹
* <p>
* (注意:順序: 左中右)
*
* @param treeNode
*/
public void inOrderTraverse(BiTreeNode treeNode) {
if (treeNode == null) return;
//遍歷左子樹
inOrderTraverse(treeNode.LChild);
//結點數據
System.out.println(treeNode.data.toString());
//遍歷右子樹
inOrderTraverse(treeNode.RChild);
}
/**
* 中序遍歷(非遞歸)
*/
public void inOrderTraverse() {
BiTreeNode<T> node = this.root;
if (node != null) {
LinkStack<BiTreeNode> stack = new LinkStack<>();
stack.push(node);
while (!stack.isEmpty()) {
while (stack.peek() != null)
stack.push(stack.peek().LChild); //把左結點入棧,直到最左下的結點
//彈出空結點
stack.pop();
if (!stack.isEmpty()) {
node = stack.pop();
//打印結點
System.out.print(node.data.toString());
//把該結點的右子結點入棧
stack.push(node.RChild);
}
}
}
}
- 后序遍歷
/**
* 后序遍歷
* <p>
* 1. 以從左到右的方式
* 2. 先遍歷左子樹
* 3. 然后遍歷右子樹
* 4. 最后遍歷右子樹
* <p>
* 順序: 左右中
*/
public void postOrderTraverse(BiTreeNode treeNode) {
if (treeNode == null) return;
postOrderTraverse(treeNode.LChild);
postOrderTraverse(treeNode.RChild);
System.out.println(treeNode.data.toString());
}
/**
* 后序遍歷(非遞歸)
*/
public void postOrderTraverse() {
//獲取根結點
BiTreeNode node = this.root;
if (node != null) {
LinkStack<BiTreeNode> stack = new LinkStack<>();
//將根結點入棧
stack.push(node);
//設置結點訪問標識
boolean flag;
//設置指針,指向訪問過的結點
BiTreeNode p = null;
while (!stack.isEmpty()) {
//將結點的左子結點入棧
while (stack.peek() != null)
stack.push(stack.peek().LChild);
//彈出空結點
stack.pop();
while (!stack.isEmpty()) {
//查看棧頂元素
node = stack.peek();
//如果該結點的右子結點為空,或已訪問過,則該結點可以出棧訪問
if (node.RChild == null || node.RChild == p) {
//訪問結點
System.out.print(node.data.toString());
//出棧
stack.pop();
//已訪問指針指向該結點
p = node;
//標識為已訪問
flag = true;
} else {
//否則,將該結點的右子結點入棧,
stack.push(node.RChild);
// 標識該結點還沒訪問
flag = false;
}
if (!flag) {
break;
}
}
}
}
}
- 層次遍歷
/**
* 層次遍歷
*/
public void levelTraverse() {
BiTreeNode<T> node = this.root;
if (node != null) {
//初始化隊列
LinkQueue<BiTreeNode> queue = new LinkQueue<>();
//根結點入隊列
queue.offer(node);
while (!queue.isEmpty()) {
//出隊列
node = queue.poll();
//訪問該結點
System.out.println(node.data.toString());
//該結點的左子結點入隊列
if (node.LChild != null) {
queue.offer(node.LChild);
}
//該結點的右子結點入隊列
if (node.RChild != null) {
queue.offer(node.RChild);
}
}
}
}
二叉樹的建立
- 由前序遍歷和中序遍歷,或后序遍歷和中序遍歷推導建立二叉樹
/**
* 二叉樹的建立
*
* @param preOrder 前序遍歷的序列
* @param inOrder 中序遍歷的序列
* @param preIndex 前序遍歷開始位置
* @param inIndex 中序遍歷開始位置
* @param count 結點數
*/
public LinkBiTree(String preOrder, String inOrder, int preIndex, int inIndex, int count) {
if (count > 0) {
//獲取前序遍歷的序列的根結點
char r = preOrder.charAt(preIndex);
//記錄根結點在中序遍歷中的位置
int i = 0;
for (; i < count; i++) {
if (r == inOrder.charAt(i + inIndex)) {
break;
}
}
root = new BiTreeNode(r);
root.LChild = new LinkBiTree(preOrder, inOrder, preIndex + 1, inIndex, i).root;
root.RChild = new LinkBiTree(preOrder, inOrder, preIndex + i + 1, inIndex + i + 1, count - i - 1).root;
}
}
- 由標明空子樹的前序遍歷建立二叉樹
/**
* 由標明的空子樹建立二叉樹
*
* @param preOrder
*/
private static int preIndex = 0;
public LinkBiTree(String preOrder) {
//獲取前序遍歷中的根結點
char c = preOrder.charAt(preIndex++);
//如果字符不為#
if ('#' != c) {
//創建根結點
root = new BiTreeNode(c);
//創建左子樹
root.LChild = new LinkBiTree(preOrder).root;
//創建右子樹
root.RChild = new LinkBiTree(preOrder).root;
} else {
root = null;
}
}
- 由完全二叉樹順序存儲序列建立二叉樹
/**
* 使用完全二叉樹的順序存儲結構建立二叉鏈式存儲結構
*
* @param sqBiTree 序列
* @param index 根結點標識
*/
public LinkBiTree(String sqBiTree, int index) {
if (index < sqBiTree.length()) {
root = new BiTreeNode(sqBiTree.charAt(index));
//建立左右子樹
root.LChild = new LinkBiTree(sqBiTree, 2 * index + 1).root;
root.RChild = new LinkBiTree(sqBiTree, 2 * index + 2).root;
}
}
三、哈夫曼樹及哈夫曼編碼
1. 基本概念:
- 樹的路徑長度: 是從樹根結點到樹中每一結點的路徑長度之和。在結點數目相同的二叉樹中,完全二叉樹的路徑長度最短。
- 結點的權:在一些應用中,賦予樹中結點的一個有某種意義的實數。
- 結點的帶權路徑長度:結點到樹根之間的路徑長度與該結點上權的乘積。
- 樹的帶權路徑長度(Weighted Path Length of Tree):定義為樹中所有葉結點的帶權路徑長度之和
- 公式: wpl=
-
: 第k個結點的權值
-
:根結點到第k個結點的路徑長度
- 最優二叉樹:二叉樹帶權路徑長度值最小,它就是一棵最優二叉樹或哈夫曼樹
- 赫夫曼樹中不存在度為1的結點(赫夫曼樹的每一分支結點都是由兩棵子樹合并產生的新結點)
2. 構造哈夫曼樹
- 步驟:(假設帶權值的葉子結點為 {E10,B15,A5,C40,D30})
- 先把這些葉子結點按權值從小到大排序,組成有序序列:A5, E10, B15, D30, C40
- 取兩權值最小的結點,作為新結點N1的左右孩子,注意:權值小的結點作為左孩子;新結點的權值為這兩個結點權值的和;即5+10=15;
- 把新結點N1 加入有序序列:
B15, D30, C40
- 重復步驟2,把N1和B結點作為新結點N2的左右孩子, 權值為:15+15=30
- 重復步驟3,有序序列:
` D30, C40
- 重復步驟2,把N2和D結點作為新結點N3的左右孩子, 權值為:30+30=60
- 重復步驟3,有序序列: C40,
`
- 重復步驟2,把C和N3結點作為新結點T的左右孩子, 權值為:40+60=100
- 因為結點T是二叉樹的根結點,所以完成的哈夫曼樹的構造
- 完成哈夫曼樹圖
- 帶權路徑長度為: WPL=401 + 302+153+104+5*4= 205
5.4哈夫曼樹構造過程.png
- 構造哈夫曼樹總結:
- 根據給定的n個權值{w1,w2,w3...,wn}構成n棵二叉樹的集合F={T1,T2,...,Tn},其中每棵二叉樹Ti中只有一個帶權值的根結點,其左右子樹為空
- 在集合F中選取權值最小的樹,作為左右子樹(權值較小的樹作為左子樹)構造一棵新的二叉樹,且新二叉樹的權值為其左右子樹權值的和
- 在F中刪除這兩棵樹,同時使用新的二叉樹加入F中
- 重復步驟2,3,直到F中只含一棵樹為止,則可以得到哈夫曼樹
3. 哈夫曼樹編碼
定義哈夫曼樹左分支代表0, 右分支代表1
從根結點到葉子結點所經過的路徑分支組成的0和1序列,就是哈夫曼樹編碼
哈夫曼樹編碼示意圖
5.25哈夫曼樹及編碼.png
四、樹、森林與二叉樹的轉換
1. 樹轉換為二叉樹
加線。所有兄弟結點之間加一條線
去線。對樹的每個結點,只保留它與第一個孩子結點的連線,刪除其它孩子結點連線
層次調整。以樹的根結點為軸心,順時針旋轉一定的角度,注意:第一個孩子結點作為二叉樹結點的左孩子結點,兄弟結點轉換過來的孩子結點作為右孩子結點。
轉換示意圖
5.29樹轉換二叉樹.png
2. 森林轉換二叉樹
- 將森林中的每棵樹轉換為二叉樹
- 第一棵二叉樹不動,從第二棵二叉樹開始,依次把后一棵二叉樹的根結點作為前一棵二叉樹的根結點的右孩子,用連接起來
- 重復步驟2,直到所有二叉樹連接起來,就得到森林轉換過來的二叉樹
- 示意圖
5.31森林轉換為二叉樹.png
- 二叉樹轉換為樹
- 加線。若某結點是其雙親結點的左孩子,則將該結點沿著右分支向下的所有結點與該結點的雙親結點用線連接
- 刪線。 將樹中所有雙親結點與右孩子結點的連線刪除
- 層次調整。以樹的根結點為軸心,逆時針旋轉一定的角度
- 示意圖
5.30二叉樹轉換為樹.png
- 二叉樹轉換為森林
- 從根結點開始, 若右孩子存在,則把右孩子結點的連線刪除,得分離的二叉樹后,看其右孩子是否存在,存在則刪除,直到所有右孩子連線都刪除為止
- 再將分離后的二叉樹轉換為樹,
- 示意圖
5.32二叉樹轉換森林.png
五、樹的存儲結構
表示法
-
雙親表示法
5.33雙親鏈表存儲結構.png -
孩子表示法
5.34孩子鏈表存儲結構.png -
雙親孩子表示法
5.35雙親孩子鏈表存儲結構.png -
孩子兄弟表示法(應用最廣泛)
5.33孩子兄弟鏈表存儲結構.png