資料結構07 二叉樹
這篇文章開始總結 樹和二叉樹。
什麼是樹呢?
1、樹的定義
(1)有且僅有一個特定的稱為根(root) 的節點。
(2)當 n>1 時,其餘節點可分為 m(m>0) 個互不相交的集合。其中每個集合本身又是一個棵樹,並稱為根的子樹。
2、樹的表示方法
最常見的是 樹形表示法 和 廣義表表示法,下面是樹形表示法,如圖所示。
上圖的 廣義表表示法為:(A(B(D,E),C(F,G)))
3、常見的術語
(1)父節點,孩子節點,兄弟節點。以上圖為例,A是B和C的父節點,B和C是A的孩子節點,B和C之間互為兄弟節點。
(2)節點的度和樹的度。節點的度即節點有幾個分支,比如節點A有兩個分支B和C,那麼節點A的度就是2,樹的度即為一棵樹中節點的最大度數,所以上圖的樹的度也是2。
(3)有序樹和無序樹。如果將樹中節點的子樹看成是從左至右依次有序且不能交換,則稱該樹為有序樹,否則稱為無序樹。
(4)森林。在上圖中,如果將根節點A拿掉,那麼B子樹和C子樹合併就是森林了。
(5)二叉樹。二叉樹是一種特殊的樹。它的每個節點最多隻有兩棵子樹。
4、二叉樹的常見性質
性質1:在二叉樹的第 i 層上最多有 2i-1 個節點 (i>=1)
性質2:深度為 k 的二叉樹最多有 2k-1 個節點 (k>=1)
性質3:滿二叉樹:一棵深度為 k 且有 2k-1 個節點的二叉樹稱為 滿二叉樹。
完全二叉樹:一棵深度為 k 的二叉樹,其前 k-1 層是一棵滿二叉樹,而最下面一層(即第k層) 上的節點都集中在該層最左邊
滿二叉樹一定是完全二叉樹,但完全二叉樹不一定是滿二叉樹。
下面是 滿二叉樹 和 完全二叉樹 的圖示:
5、二叉樹的兩種儲存結構
5-1:順序儲存
對於完全二叉樹而言,可以使用順序儲存結構。但對於一般的二叉樹而言,使用順序儲存結構會有兩個缺點:一、如果不是完全二叉樹,則必須將其轉化為完全二叉樹;二、增加了很多虛節點,浪費資源空間。
5-2:鏈式儲存
鏈式儲存是最常用的一種二叉樹儲存結構。每個節點設定三個域,分別是值域、左指標域和右指標域,用 data 表示值域,lchild 和 rchild 分別表示指向左子樹、右子樹的指標域。如下圖:
6、二叉樹的常見操作
6-1:插入節點
思路:首先找到要插入節點的父節點,然後確定插入到父節點的左邊還是右邊,最後將節點插入。
6-2:查詢節點
思路:運用遞迴查詢。
6-3:計算樹的深度
思路:分別遞迴左子樹和右子樹,取長度較大的那一個作為整個樹的深度。
6-4:遍歷之 先序遍歷
思路:先訪問根節點,然後遍歷左子樹,再遍歷右子樹
6-5:遍歷之 中序遍歷
思路:先遍歷左子樹,再訪問根節點,最後遍歷右子樹
6-6:遍歷之 後序遍歷
思路:先遍歷左子樹,再遍歷右子樹,最後訪問根節點
6-7:遍歷之 按層遍歷
思路:從上到下,從左到右遍歷節點
7、二叉樹的常見操作的程式碼實現
程式碼如下:
節點類Node.java
public class Node {
public int data; // 資料域
public Node left; // 左孩子
public Node right; // 右孩子
}
BinTree.java
import java.util.Scanner;
public class BinTree {
// 按層遍歷的儲存空間長度
public static int length;
/**
* 生成根節點
*/
public static Node createRoot() {
Node root = new Node();
System.out.println("請輸入根節點,以便生成樹:");
root.data = new Scanner(System.in).nextInt();
System.out.println("根節點生成成功");
return root;
}
/**
* 插入節點
*
* @param root 二叉樹的根節點
* @return 返回二叉樹的根節點
* @throws Exception
*/
public static Node insert(Node root) throws Exception {
while (true) {
System.out.println("請輸入待插入節點的資料:");
Node node = new Node();
node.data = new Scanner(System.in).nextInt();
// 獲取父節點資料
System.out.println("請輸入它(待插入節點) 的父節點:");
int parentNodeData = new Scanner(System.in).nextInt();
// 確定插入方向
System.out.println("請確定要插入到父節點的:1 左側, 2 右側");
int direction = new Scanner(System.in).nextInt();
// 插入節點
root = insertNode(root, node, parentNodeData, direction);
if (root == null) {
System.out.println("未找到父節點,請重新輸入!");
continue;
}
System.out.println("插入成功,是否繼續? 1繼續,2退出");
if (new Scanner(System.in).nextInt() == 1)
continue;
else
break; // 退出迴圈
}
return root;
}
/**
* 從insert()方法抽取出來的需要遞迴的部分
*
* @param root 二叉樹的根節點
* @param node 待插入節點
* @param parentNodeData 待插入節點的父節點
* @param direction 插入到左邊還是右邊
* @return 返回二叉樹的根節點
* @throws Exception
*/
public static Node insertNode(Node root, Node node, int parentNodeData, int direction) throws Exception {
if (root == null)
return null;
// 找到父節點
if (root.data == parentNodeData) {
switch (direction) {
case 1:
if (root.left != null)
throw new Exception("左節點已存在,不能插入!");
else
root.left = node;
break;
case 2:
if (root.right != null)
throw new Exception("右節點已存在,不能插入!");
else
root.right = node;
break;
}
}
// 向左子樹查詢父節點(遞迴)
insertNode(root.left, node, parentNodeData, direction);
// 向右子樹查詢父節點(遞迴)
insertNode(root.right, node, parentNodeData, direction);
return root;
}
/**
* 查詢節點
*/
public static boolean getNode(Node root, int data) {
if (root == null)
return false;
// 查詢成功
if (root.data == data)
return true;
// 遞迴查詢
boolean result1 = getNode(root.left, data);
boolean result2 = getNode(root.right, data);
return result1 || result2;
}
/**
* 獲取二叉樹的深度
* 思路:分別遞迴左子樹和右子樹,取長度較大的那一個作為整個樹的深度
*
* @param root 二叉樹的根節點
* @return
*/
public static int getLength(Node root) {
if (root == null)
return 0;
int leftLength, rightLength;
// 遞迴左子樹的深度
leftLength = getLength(root.left);
// 遞迴右子樹的深度
rightLength = getLength(root.right);
if (leftLength > rightLength)
return leftLength + 1;
else
return rightLength + 1;
}
/**
* 先序遍歷
* 思路:先訪問根節點,然後遍歷左子樹,再遍歷右子樹
*
* @param root 根節點
*/
public static void DLR(Node root) {
if (root == null)
return;
// 輸出節點的值
System.out.print(root.data + " ");
// 遞迴遍歷左子樹
DLR(root.left);
// 遞迴遍歷右子樹
DLR(root.right);
}
/**
* 中序遍歷
* 思路:先遍歷左子樹,再訪問根節點,最後遍歷右子樹
*
* @param root 根節點
*/
public static void LDR(Node root) {
if (root == null)
return;
// 遍歷左子樹
LDR(root.left);
// 輸出節點的值
System.out.print(root.data + " ");
// 遍歷右子樹
LDR(root.right);
}
/**
* 後序遍歷
* 思路:先遍歷左子樹,再遍歷右子樹,最後訪問根節點
*
* @param root 根節點
*/
public static void LRD(Node root) {
if (root == null)
return;
// 遍歷左子樹
LRD(root.left);
// 遍歷右子樹
LRD(root.right);
// 輸出節點的值
System.out.print(root.data + " ");
}
/**
* 按層遍歷
* 思路:從上到下,從左到右遍歷節點
*
* @param root 根節點
*/
public static void traversalLevel(Node root) {
if (root == null)
return;
int head = 0;
int tail = 0;
// 申請儲存空間
Node[] nodeList = new Node[length];
// 將當前二叉樹儲存到陣列中
nodeList[tail] = root;
// 計算tail的位置
tail = (tail + 1) % length; // 除留餘數法
while (head != tail) {
Node tempNode = nodeList[head];
// 計算head的位置
head = (head + 1) % length;
// 輸出節點的值
System.out.print(tempNode.data + " ");
// 如果左子樹不為空,則將左子樹儲存到陣列的tail位置
if (tempNode.left != null) {
nodeList[tail] = tempNode.left;
// 重新計算tail的位置
tail = (tail + 1) % length;
}
// 如果右子樹不為空,則將右子樹儲存到陣列的tail位置
if (tempNode.right != null) {
nodeList[tail] = tempNode.right;
// 重新計算tail的位置
tail = (tail + 1) % length;
}
}
}
}
BinTreeTest.java
public class BinTreeTest {
public static void main(String[] args) throws Exception {
System.out.println("*******鏈式儲存的二叉樹*******");
// 建立根節點
Node root = BinTree.createRoot();
// 1、插入節點
System.out.println("********插入節點*******");
BinTree.insert(root);
// 2、查詢節點
System.out.print("查詢data為2的節點是否存在:");
boolean result = BinTree.getNode(root, 2);
System.out.println(result);
// 3、獲取二叉樹的深度
System.out.println("當前二叉樹的深度為:" + BinTree.getLength(root));
// 4、先序遍歷
System.out.print("先序遍歷:");
BinTree.DLR(root);
System.out.println("");
// 5、中序遍歷
System.out.print("中序遍歷:");
BinTree.LDR(root);
System.out.println("");
// 6、後序遍歷
System.out.print("後序遍歷:");
BinTree.LRD(root);
System.out.println("");
// 7、按層遍歷
System.out.print("按層遍歷:");
BinTree.length = 100;
BinTree.traversalLevel(root);
System.out.println("");
}
}
執行結果: