二叉樹(binary tree) 二叉樹(Binary Tree)是一種常見的樹狀數據結構,它由一組節點組成,每個節點最多有兩個子節點,分別稱為左子節點和右子節點。二叉樹具有以下特點: 每個節點最多有兩個子節點,分別稱為左子節點和右子節點。 左子樹和右子樹也是二叉樹,它們的結構與父節點類似。 二叉樹 ...
二叉樹(binary tree)
二叉樹(Binary Tree)是一種常見的樹狀數據結構,它由一組節點組成,每個節點最多有兩個子節點,分別稱為左子節點和右子節點。二叉樹具有以下特點:
- 每個節點最多有兩個子節點,分別稱為左子節點和右子節點。
- 左子樹和右子樹也是二叉樹,它們的結構與父節點類似。
- 二叉樹的順序不固定,可以是任意形狀。
兩種特殊形式
二叉樹還有兩種特殊形式,一個叫作滿二叉樹 ,另一個叫作完全二叉樹
滿二叉樹
如果該二叉樹的所有葉子節點都在最後一層,並且結點總數= 2^n -1
,n 為層數,則我們稱為滿二又樹。簡單點說,滿二叉樹的每一個分支都是滿的。
完全二叉樹
對一個有n個節點的二叉樹,按層級順序編號,則所有節點的編號為從1到n。如果這個樹所有節點和同樣深度的滿二叉樹的編號為從1到n的節點位置相同,則這個二叉樹為完全二叉樹。完全二叉樹的條件沒有滿二叉樹那麼苛刻: 滿二叉樹要求所有分支都是滿的;而完全二叉樹只需保證最後一個節點之前的節點都齊全即可。
遍歷二叉樹
在電腦程式中,遍歷本身是一個線性操作。所以遍歷同樣具有線性結構的數組或鏈表,是一件輕而易舉的事情。反觀二叉樹,是典型的非線性數據結構,遍歷時需要把非線性關聯的點轉化成一個線性的序列,以不同的方式來遍歷,遍歷出的序列順序也不同。
遍歷方式
深度優先和廣度優先這兩個概念不止局限於二叉樹,它們更是一種抽象的演算法思想,決定了訪問某些複雜數據結構的順序。在訪問樹、圖,或其他一些複雜數據結構時,這兩個概念常常被使用到。
1.深度優先遍歷(Depth-First Search,DFS)
所謂深度優先,顧名思義,就是偏向於縱深,“一頭扎到底”的訪問方式。可能這種說法有些抽象,下麵就通過二叉樹的前序遍歷、中序遍歷、後序遍歷 ,來看一看深度優先
2.廣度優先遍歷 (Breadth-First Search,BFS)
如果說深度優先遍歷是在一個方向上“一頭扎到底”,那麼廣度優先遍歷則恰恰相反:先在各個方向上各走出1步,再在各個方向上走出第2步、第3步......一直到各個方向全部走完。聽起來有些抽象,下麵讓我們通過二叉樹的層序遍歷
也是一種遍歷或搜索樹或圖的演算法。在廣度優先遍歷中,從根節點開始,按照層級順序逐層訪問節點,先訪問當前層的所有節點,然後再訪問下一層的節點。廣度優先遍歷可以使用隊列來實現。
前序遍歷
根節點 -> 左子樹 -> 右子樹
。在前序遍歷中,首先訪問根節點,然後按照左子樹和右子樹的順序遞歸地進行前序遍歷。
// 前序遍歷二叉樹(根-左-右)
public static void preOrderTraversal(TreeNode node) {
if (node == null) {
return;
}
System.out.print(node.val + " ");
preOrderTraversal(node.left);
preOrderTraversal(node.right);
}
中序遍歷
左子樹 -> 根節點 -> 右子樹
。在中序遍歷中,首先按照左子樹的順序遞歸地進行中序遍歷,然後訪問根節點,最後按照右子樹的順序遞歸地進行中序遍歷。
// 中序遍歷二叉樹(左-根-右)
public static void inOrderTraversal(TreeNode node) {
if (node == null) {
return;
}
inOrderTraversal(node.left);
System.out.print(node.val + " ");
inOrderTraversal(node.right);
}
後序遍歷
左子樹 -> 右子樹 -> 根節點
。在後序遍歷中,首先按照左子樹和右子樹的順序遞歸地進行後序遍歷,然後訪問根節點。
// 後序遍歷二叉樹(左-右-根)
public static void postOrderTraversal(TreeNode node) {
if (node == null) {
return;
}
postOrderTraversal(node.left);
postOrderTraversal(node.right);
System.out.print(node.val + " ");
}
public static void main(String[] args) {
// 創建一個二叉樹
TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
// 前序遍歷二叉樹
System.out.println("前序遍歷結果:");
preOrderTraversal(root);
// 中序遍歷二叉樹
System.out.println("\n中序遍歷結果:");
inOrderTraversal(root);
// 後序遍歷二叉樹
System.out.println("\n後序遍歷結果:");
postOrderTraversal(root);
}
層次遍歷
是一種廣度優先遍歷的應用,它按照層級順序逐層訪問二叉樹的節點。在層次遍歷中,從根節點開始,先訪問根節點,然後按照從左到右的順序逐層訪問每個節點的子節點,一層一層橫向遍歷各個節點,可以藉助於隊列實現。
public static void levelOrderTraversal(TreeNode root) {
if (root == null) {
return;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root); // 將根節點入隊
while (!queue.isEmpty()) {
int levelSize = queue.size(); // 當前層級的節點數量
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll(); // 出隊當前節點
System.out.print(node.val + " "); // 訪問當前節點
if (node.left != null) {
queue.offer(node.left); // 左子節點入隊
}
if (node.right != null) {
queue.offer(node.right); // 右子節點入隊
}
}
System.out.println(); // 換行表示進入下一層級
}
}
存儲結構
順序結構存儲
使用數組來表示二叉樹的結構。按照層級順序依次將二叉樹節點存儲到數組中,空缺位置用特定值(如null)表示。這種存儲結構適用於完全二叉樹,因為不是完全二叉樹會有空間的浪費,可以通過數組下標計算節點之間的關係。
特點:
-
順序二叉樹通常只考慮完全二叉樹
-
第n個元素的左子節點為
2* n +1
-
第n個元素的右子節點為
2* n + 2
-
第n個元素的父節點為
(n-1)/2
n: 表示二又樹中的第幾個元素(按0開始編號如圖所示)
代碼示例
public class ArrayBinaryTree {
private int[] treeArray;
private int size;
public ArrayBinaryTree(int capacity) {
treeArray = new int[capacity];
size = 0;
}
public void insert(int data) {
if (size >= treeArray.length) {
System.out.println("The tree is full");
return;
}
treeArray[size] = data;
size++;
}
public void inorderTraversal(int index) {
if (index >= size) {
return;
}
inorderTraversal(index * 2 + 1); // 訪問左子樹
System.out.print(treeArray[index] + " "); // 訪問當前節點
inorderTraversal(index * 2 + 2); // 訪問右子樹
}
public static void main(String[] args) {
ArrayBinaryTree tree = new ArrayBinaryTree(10);
tree.insert(1);
tree.insert(2);
tree.insert(3);
tree.insert(4);
tree.insert(5);
System.out.print("Inorder Traversal: ");
tree.inorderTraversal(0); // 輸出: 4 2 5 1 3
}
}
鏈式存儲
用節點對象和指針來表示二叉樹的結構。每個節點包含一個數據元素和左右子節點的指針。用鏈表來表示一棵二叉樹,即用鏈來指示元素的邏輯關係。 通常的方法是鏈表中每個結點由三個域組成,數據域和左右指針域,左右指針分別用來給出該結點左孩子和右孩子所在的鏈結點的存儲地址 。這種存儲結構可以靈活地表示任意形狀的二叉樹。
class TreeNode {
int val;
TreeNode left;
TreeNode right;
public TreeNode(int val) {
this.val = val;
this.left = null;
this.right = null;
}
}
線索化二叉樹
線索二叉樹是對普通二叉樹的一種擴展,它通過在空指針位置存儲指向當前節點的前驅節點和後繼節點的指針,使得二叉樹的遍歷更加方便。可以有效地減少遍歷的次數,同時也可以避免空指針的浪費。
-
n個結點的二又鏈表中含有
n+1
[公式2n-(n-1)=n+1
] 個空指針域。利用二又鏈表中的空指針域,存放指向該結點在某種遍歷次序下的前驅和後繼結點的指針(這種附加的指針稱為"線索") -
這種加上了線索的二叉鏈表稱為線索鏈表,相應的二叉樹稱為線索二叉樹(Threaded BinaryTree)。根據線索性質的不同,線索二叉樹可分為前序線索叉樹、中序線索二叉樹和後序線索二叉樹三種
-
一個結點的前一個結點,稱為前驅結點,一個結點的後一個結點,稱為後繼結點
代碼實現
class TreeNode {
int val;
TreeNode left;
TreeNode right;
private int leftType; // 0:左子樹 1:前驅節點
private int rightType; // 0:右子樹 1:後繼節點
public TreeNode(int val) {
this.val = val;
this.left = null;
this.right = null;
}
// 省略get set方法
}
class ThreadedBinaryTree {
private TreeNode root;
//為了實現線索化,需要創建要給指向當前結點的前驅結點的指針
//在遞歸進行線索化時,pre 總是保留前一個結點
private TreeNode pre = null;
public void setRoot(TreeNode root) {
this.root = root;
}
public void threadedNodes(){
this.threadedNodes(root);
}
// 線索化節點 中序線索化
public void threadedNodes(TreeNode node){
if (node == null){
return;
}
// 先線索化左子樹
threadedNodes(node.getLeft());
// 線索化當前節點
if (node.getLeft() == null){
//讓當前結點的左指針指向前驅結點
node.setLeft(pre);
node.setLeftType(1);
}
//處理後繼結點
if (pre != null && pre.getRight() == null) {
//讓前驅結點的右指針指向當前結點
pre.setRight(node);
//修改前驅結點的右指針類型
pre.setRightType(1);
}
// 讓當前結點是下一個結點的前驅結點
pre = node;
pre = node;
// 線索化右子樹
threadedNodes(node.getRight());
}
}
遍歷線索化二叉樹
因為線索化後,各個結點指向有變化,因此原來的遍歷方式不能使用,這時需要使用新的方式遍歷線索化二叉樹,各個節點可以通過線型方式遍歷,因此無需使用遞歸方式,這樣也提高了遍歷的效率。 遍歷的次序應當和中序遍歷保持一致。
代碼實現
// 遍歷線索化二叉樹
public void threadedNodeList(){
//定義一個變數,存儲當前遍歷的結點,從root開始
TreeNode node = root;
while (node != null){
while (node.getLeftType() == 0){
node = node.getLeft();
}
System.out.println(node);
while (node.getRightType() == 1){
node = node.getRight();
System.out.println(node);
}
node = node.getRight();
}
}
以上面圖中的例子,進行代碼測試
public class ThreadedBinaryTreeDemo {
public static void main(String[] args) {
TreeNode root = new TreeNode(1);
TreeNode node2 = new TreeNode(3);
TreeNode node3 = new TreeNode(6);
TreeNode node4 = new TreeNode(8);
TreeNode node5 = new TreeNode(10);
TreeNode node6 = new TreeNode(14);
root.setLeft(node2);
root.setRight(node3);
node2.setLeft(node4);
node2.setRight(node5);
node3.setLeft(node6);
//測試中序線索化
ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree();
threadedBinaryTree.setRoot(root);
threadedBinaryTree.threadedNodes();
//測試: 以10號節點測試
TreeNode leftNode = node5.getLeft();
TreeNode rightNode = node5.getRight();
System.out.println("10號結點的前驅結點是 =" + leftNode); //3
System.out.println("10號結點的後繼結點是=" + rightNode); //1
// 測試遍歷
System.out.println("使用線索化的方式遍歷 線索化二叉樹");
threadedBinaryTree.threadedNodeList(); // 8, 3, 10, 1, 14, 6
}
}