淺入淺出二叉樹
樹的概述
樹是一種重要的非線性資料結構,直觀地看,它是資料元素(在樹中稱為結點)按分支關係組織起來的結構,很象自然界中的樹那樣。形同下圖。
樹有如下基本概念:
-
根結點
根結點是樹的一個組成部分,也叫樹根。每一顆樹都有且僅有一個根結點。它是同一棵樹中除本身外所有結點的祖先,沒有父結點。按上圖的樹結構來看,根節點就是 1。
-
父結點
也叫雙親結點,一個結點如果有上一級,則稱這個上一級是它的父結點,如果沒有上一級,則該結點無父結點。按上圖的樹結構來看,可以說是父節點有 1、2、3、5、6、22。
-
子結點
一個結點如果有下一級,則稱這個下一級是它的子結點,如果沒有下一級,則該結點無子結點。按上圖的樹結構來看,其實除了根結點以外,各個結點都是它們對應父結點的子結點。
-
路徑
從根結點訪問其他結點所需要經過的結點。比如從 1 要走到 31,則路徑是 1、3、31。
-
結點的度
一個結點擁有多少個子結點,就認為它的度是多少。比如根結點 1,它的度就是 5。
-
結點的權
結點的權值,圖中每個結點都有對應的數字,這些數字就是對應結點的權。
-
葉子結點
沒有子結點的結點,按上圖的樹結構來看,葉子結點有 21、221、222、223、31、51、52、61。
-
子樹
還是上圖,試著把結點 2 單獨拿出來看,會發現它和它的子結點也能構成樹結構。這顆樹在整顆大的樹裡邊,所以稱為子樹。
-
層
結點的層次從根開始定義起,根為第一層,根的孩子是二層,依次累計。上圖的樹結構層次就是 4。
-
樹的高度
樹的最大層數,上圖的樹結構最大層數就是從根結點開始,到最底層的葉子結點,高度是 4。
-
森林
多個樹組成的集合,想象現在有好多顆樹,每一顆樹結構不一,它們共同構成森林。
二叉樹的概述
任何子結點的數量都不超過 2,就是一顆二叉樹。比如之前舉例的圖,明顯就不是二叉樹。二叉樹的子結點分左結點和右結點,不能隨意顛倒位置。
二叉樹也有分類:
-
滿二叉樹
所有葉子結點都在最後一層,而且結點總數為 2^n - 1,n 是樹的高度。
-
完全二叉樹
所有葉子結點都在最後一層或倒數第二層,且最後一層葉子結點在左邊延續,倒數第二層的葉子結點在右邊連續。即最後一層的葉子結點總是從左往右,倒數第二層總是從右到左。滿二叉樹也是一顆完全二叉樹。
鏈式儲存的二叉樹
顧名思義,用連結串列的方式去實現二叉樹結構。用程式碼去實現,首先我們要建立一個結點類。
public class TreeNode {
// 節點的權
private int value;
// 左子結點
private TreeNode leftNode;
// 右子結點
private TreeNode rightNode;
public TreeNode(int value) {
this.value = value;
}
// 剩下的都是 set/get 方法了
...
}
其次,和連結串列有頭結點一樣,二叉樹也需要有根結點輔助操作,作為建立二叉樹的基礎。
public class BinaryTree {
private TreeNode root;
public TreeNode getRoot() {
return root;
}
public void setRoot(TreeNode root) {
this.root = root;
}
}
萬事具備,向根結點新增左右子結點即可。
public class TestBinaryTree {
public static void main(String[] args) {
// 建立二叉樹
BinaryTree binaryTree = new BinaryTree();
// 建立根結點
TreeNode root = new TreeNode(1);
// 設定根結點
binaryTree.setRoot(root);
// 建立一個左結點
TreeNode rootL = new TreeNode(2);
// 把新建立的結點設定為根結點的左子結點
root.setLeftNode(rootL);
// 建立一個右結點
TreeNode rootR = new TreeNode(3);
// 把新建立的結點設定為根結點的右子結點
root.setRightNode(rootR);
// 為第二層的左結點建立兩個子結點
rootL.setLeftNode(new TreeNode(4));
rootL.setRightNode(new TreeNode(5));
// 為第二層的右結點建立兩個子結點
rootR.setLeftNode(new TreeNode(6));
rootR.setRightNode(new TreeNode(7));
}
}
遍歷二叉樹
二叉樹的遍歷方式有三種,分別是:前序遍歷、中序遍歷、後序遍歷。
以上圖為例,記住一點,所謂的前序、中序、後序都是參考當前結點的位置。前序遍歷,即是先取當前結點的權,然後是它的左子結點,最後是右子結點。從根結點開始,遍歷過程中每一個結點都要遵守這個規矩。
因此上圖前序遍歷得到的結果是:1、2、4、5、3、6、7;中序遍歷得到的結果是:4、2、5、1、6、3、7;後序遍歷得到的結果是:4、5、2、6、3、7、1。程式碼實現用到遞迴的思想。
public class TestBinaryTree {
public static void main(String[] args) {
// 之前建立二叉樹的程式碼,這裡就省略不寫了
...
// 前序遍歷樹
binaryTree.frontShow();
System.out.println("-----------------");
// 中序遍歷樹
binaryTree.midShow();
System.out.println("-----------------");
// 後序遍歷樹
binaryTree.afterShow();
}
}
public class BinaryTree {
private TreeNode root;
public TreeNode getRoot() {
return root;
}
public void setRoot(TreeNode root) {
this.root = root;
}
// 前序遍歷
public void frontShow() {
if (root != null) {
root.frontShow();
}
}
// 中序遍歷
public void midShow() {
if (root != null) {
root.midShow();
}
}
// 後序遍歷
public void afterShow() {
if (root != null) {
root.afterShow();
}
}
}
public class TreeNode {
// 節點的權
private int value;
// 左子結點
private TreeNode leftNode;
// 右子結點
private TreeNode rightNode;
public TreeNode(int value) {
this.value = value;
}
// set/get 方法
...
// 前序遍歷
public void frontShow() {
// 先輸出當前結點內容
System.out.println(value);
// 輸出左結點內容
if (leftNode != null) {
leftNode.frontShow();
}
// 輸出右結點內容
if (rightNode != null) {
rightNode.frontShow();
}
}
// 中序遍歷
public void midShow() {
// 輸出左結點內容
if (leftNode != null) {
leftNode.midShow();
}
// 輸出當前結點內容
System.out.println(value);
// 輸出右結點內容
if (rightNode != null) {
rightNode.midShow();
}
}
// 後序遍歷
public void afterShow() {
// 輸出左結點內容
if (leftNode != null) {
leftNode.afterShow();
}
// 輸出右結點內容
if (rightNode != null) {
rightNode.afterShow();
}
// 輸出當前結點內容
System.out.println(value);
}
}
二叉樹中結點的查詢
查詢結點,實際就是把整顆二叉樹遍歷一次,依次比對,找出結果並返回。這裡以前序查詢為例,其餘的大同小異。
public class TestBinaryTree {
public static void main(String[] args) {
// 建立二叉樹
...
// 前序查詢
TreeNode result = binaryTree.frontSearch(5);
System.out.println(result);
}
}
public class BinaryTree {
private TreeNode root;
public TreeNode getRoot() {
return root;
}
public void setRoot(TreeNode root) {
this.root = root;
}
/**
* 前序查詢
* @return 目標結點
*/
public TreeNode frontSearch(int value) {
return root.frontSearch(value);
}
}
public class TreeNode {
// 節點的權
private int value;
// 左子結點
private TreeNode leftNode;
// 右子結點
private TreeNode rightNode;
public TreeNode(int value) {
this.value = value;
}
/**
* 前序查詢
* @return 目標結點
*/
public TreeNode frontSearch(int value) {
TreeNode target = null;
// 返回本結點
if (this.value == value) {
return this;
}
// 向左子結點方向查詢
if (leftNode != null) {
target = leftNode.frontSearch(value);
}
// 如果不為空,證明找到結點,返回
if (target != null) {
return target;
}
// 向右子結點方向查詢
if (rightNode != null) {
target = rightNode.frontSearch(value);
}
return target;
}
}
刪除二叉樹的結點
對於一顆普通的二叉樹而言,刪除一個結點就等同於把對應的整顆子樹一併刪掉。之後講到二叉排序樹時,就不是這樣操作了。
刪除時要區分是根結點還是其他結點。如果是根結點的話,直接置為 null 就好了。但如果不是,則依次比較左右兩個子結點,符合就直接置為 null。如果都不符合,那就遞迴呼叫左右結點的 delete 方法。
public class TestBinaryTree {
public static void main(String[] args) {
// 建立一顆子樹
...
// 刪除一個結點
binaryTree.delete(5);
binaryTree.frontShow();
}
}
public class BinaryTree {
private TreeNode root;
...
/**
* 根據權值刪除結點
* @param value 依據權值
*/
public void delete(int value) {
// 要刪除的是根結點
if (root.getValue() == value) {
root = null;
} else {
// 要刪除的是其他結點
root.delete(value);
}
}
}
public class TreeNode {
// 節點的權
private int value;
// 左子結點
private TreeNode leftNode;
// 右子結點
private TreeNode rightNode;
...
/**
* 根據權值刪除結點
* @param value 依據權值
*/
public void delete(int value) {
TreeNode parent = this;
// 判斷左子結點
if (parent.leftNode != null && parent.leftNode.value == value) {
parent.leftNode = null;
return;
}
// 判斷右子結點
if (parent.rightNode != null && parent.rightNode.value == value) {
parent.rightNode = null;
return;
}
parent = leftNode;
if (parent != null) {
parent.delete(value);
}
parent = rightNode;
if (parent != null) {
parent.delete(value);
}
}
}
順序儲存的二叉樹
二叉樹還可以用陣列實現,或者說,任意一個數組都可以轉化為二叉樹。就上圖二叉樹而言,它對應的陣列實現就是 [1,2,3,4,5,6,7]。
並不是每顆二叉樹都長得這麼規矩,有可能會出現缺胳膊少腿的情況。通常情況下,順序儲存的二叉樹只考慮完全二叉樹(滿二叉樹也是完全二叉樹),否則沒有意義。
順序儲存的二叉樹還有其對應的性質公式,常用的有如下三個:
- 陣列中第 n 個元素的左子結點下標為:2*n + 1
- 陣列中第 n 個元素的左子結點下標為:2*n + 2
- 陣列中第 n 個元素的父節點下標為:(n-1)/ 2
順序儲存的二叉樹的遍歷
我們把一個數組當成二叉樹作前序遍歷,剩下的中序和後序遍歷也大同小異了。
public class TestBinaryTree {
public static void main(String[] args) {
int[] data = new int[]{1, 2, 3, 4, 5, 6, 7};
ArrayBinaryTree arrayBinaryTree = new ArrayBinaryTree(data);
// 前序遍歷
arrayBinaryTree.frontShow();
}
}
public class ArrayBinaryTree {
private int[] data;
public ArrayBinaryTree(int[] data) {
this.data = data;
}
public int[] getData() {
return data;
}
public void setData(int[] data) {
this.data = data;
}
public void frontShow() {
frontShow(0);
}
public void frontShow(int index) {
if (data == null || data.length == 0) {
return;
}
// 先遍歷當前結點的內容
System.out.println(data[index]);
// 遍歷左子結點
if (2 * index + 1 < data.length) {
frontShow(2 * index + 1);
}
// 遍歷右子結點
if (2 * index + 2 < data.length) {
frontShow(2 * index + 2);
}
}
}