樹
樹形結構:
- 每個結點有零個或多個子結點;
- 沒有父結點的結點稱爲根結點;
- 每一個非根結點有且只有一個父結點;
- 除了根結點外,每個子結點可以分爲多個不相交的子樹;
關於樹形結構中的一些術語:
- 節點的高度=節點到葉子節點的最長路徑(邊數)
- 節點的深度=根節點到這個節點所經歷的邊的個數
- 節點的層數=節點的深度+1
- 樹的高度=根節點的高度
二叉樹
每個節點最多有兩個子節點
滿二叉樹:一棵二叉樹,除葉子節點外,每個節點都有左右兩個子節點,總節點個數=2^n - 1,n爲層數
完全二叉樹:一棵二叉樹,所有葉子節點都在最後一層或者倒數第二層,最後一層的葉子節點都靠左排列,並且除了最後一層,其他各層節點個數都要達到最大
二叉樹的遍歷和查找
- 深度優先遍歷(DFS):
前序遍歷: 先輸出父節點,再遍歷左子樹和右子樹
中序遍歷: 先遍歷左子樹,再輸出父節點,再遍歷右子樹
後序遍歷: 先遍歷左子樹,再遍歷右子樹,最後輸出父節點 - 廣度優先遍歷(BFS)
按照高度順序,從上往下逐層遍歷節點;先遍歷上層節點再遍歷下層節點
中序+後序、中序+先序可以唯一確定一棵二叉樹
public class Node {
public int val;
public Node left;
public Node right;
public Node(int val) {
this.val = val;
}
@Override
public String toString() {
return "Node{" +
"val=" + val +
'}';
}
}
public class BinaryTreeDemo {
public static void main(String[] args) {
Node node1 = new Node(1);
Node node2 = new Node(2);
Node node3 = new Node(3);
Node node4 = new Node(4);
Node node5 = new Node(5);
node1.left = node2;
node1.right = node3;
node2.left = node4;
node2.right = node5;
preOrder(node1);
System.out.println("-------------");
midOrder(node1);
System.out.println("-------------");
tailOrder(node1);
int target = 5;
System.out.println(preOrderSearch(node1, target));
System.out.println(midOrderSearch(node1, target));
System.out.println(tailOrderSearch(node1, target));
}
//前序遍歷
public static void preOrder(Node root) {
if(null == root) return;
System.out.println(root.val);
preOrder(root.left);
preOrder(root.right);
}
//中序遍歷
public static void midOrder(Node root) {
if(null == root) return;
midOrder(root.left);
System.out.println(root.val);
midOrder(root.right);
}
//後序遍歷
public static void tailOrder(Node root) {
if(null == root) return;
tailOrder(root.left);
tailOrder(root.right);
System.out.println(root.val);
}
//前序遍歷-查找
public static Node preOrderSearch(Node root, int target) {
if(null == root) return null;
//如果在當前節點找到目標值,則返回
if(root.val == target) {
return root;
}
Node result = null;
//當前節點沒找到,繼續到左子節點查找
result = preOrderSearch(root.left, target);
//如果在左子節點找到,則返回
if(result != null) {
return result;
}
//左子節點沒找到,返回右子節點的查找結果
return preOrderSearch(root.right, target);
}
//中序遍歷-查找
public static Node midOrderSearch(Node root, int target) {
if(null == root) return null;
Node result = null;
//先在左子節點中找,找到則返回
result = midOrderSearch(root.left, target);
if(result != null) {
return result;
}
//左子節點沒找到,繼續在當前節點中找
if(root.val == target) {
return root;
}
//左子節點沒找到,返回右子節點查找結果
return midOrderSearch(root.right, target);
}
//後序遍歷-查找
public static Node tailOrderSearch(Node root, int target) {
if(null == root) return null;
Node result = null;
result = tailOrderSearch(root.left, target);
if(result != null) {
return result;
}
result = tailOrderSearch(root.right, target);
if(result != null) {
return result;
}
return (root.val == target) ? root : null;
}
}
順序存儲二叉樹
從數據存儲來看,數組存儲方式和樹的存儲方式可以相互轉換,即數組可以轉換成樹,樹也可以轉換成數組。
數組{1,2,3,4,5,6,7}和下面的二叉樹可以相互轉換:
順序存儲二叉樹的特點:
- 順序存儲二叉樹通常只考慮完全二叉樹
- 第n個元素的左子節點爲 2 * n + 1
- 第n個元素的右子節點爲 2 * n + 2
- 第n個元素的父節點爲 (n-1) / 2
- n : 表示二叉樹中的第幾個元素(從0計數)
順序存儲二叉樹的前序遍歷:
public class ArrBinaryTreeDemo {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7};
ArrBinaryTree arrBinaryTree = new ArrBinaryTree(arr);
arrBinaryTree.preOrder(1);
}
}
class ArrBinaryTree{
private int[] arr;
public ArrBinaryTree(int[] arr) {
this.arr = arr;
}
public void preOrder() {
this.preOrder(0);
}
/**
* 前序遍歷
* @param index 數組下標
*/
public void preOrder(int index) {
if(null == arr || 0 == arr.length) return;
System.out.println(arr[index]);
//向左遍歷
if(index * 2 + 1 < arr.length) {
preOrder(index * 2 + 1);
}
//向右遍歷
if(index * 2 + 2 < arr.length) {
preOrder(index * 2 + 2);
}
}
}
線索化二叉樹
線索化二叉樹是爲了充分利用各個節點的左右指針,含有n個節點的二叉樹有n+1個空指針域,利用這些空指針域存放指向該節點在某種遍歷次序下的前驅和後繼節點的指針,這種附加的指針稱爲“線索”。
加上了線索的二叉鏈表稱爲線索鏈表,相應的,二叉樹稱爲線索二叉樹(Thread BinaryTree)。根據線索性質的不同,線索二叉樹可分爲前序線索二叉樹、中序線索二叉樹、後序線索二叉樹。
此外,一個節點的前一個節點稱爲前驅節點,後一個節點稱爲後繼節點,這裏的前後指的是遍歷過程中的前後。
比如下圖中左邊這顆二叉樹的中序遍歷序列爲{8, 3, 10, 1, 14, 6},加上中序遍歷線索之後如右圖所示:
public class ThreadedBinaryTreeDemo {
public static void main(String[] args) {
ThreadedNode node1 = new ThreadedNode(1);
ThreadedNode node3 = new ThreadedNode(3);
ThreadedNode node6 = new ThreadedNode(6);
ThreadedNode node8 = new ThreadedNode(8);
ThreadedNode node10 = new ThreadedNode(10);
ThreadedNode node14 = new ThreadedNode(14);
node1.left = node3;
node1.right = node6;
node3.left = node8;
node3.right = node10;
node6.left = node14;
ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree();
threadedBinaryTree.midThreadedTree(node1);
threadedBinaryTree.midThreadedTreeList(node1);
}
}
class ThreadedBinaryTree{
private ThreadedNode preNode; //存儲當前節點的前驅節點
//中序線索化二叉樹
public void midThreadedTree(ThreadedNode node) {
if(null == node) return;
//1. 線索化左子樹
midThreadedTree(node.left);
//2. 線索化當前節點
//2.1 線索化左指針
if(null == node.left) {
node.left = preNode; //左指針指向前驅節點
node.leftType = 1;
}
//這裏還不能線索化當前節點的右指針,因爲還不知道當前節點的後繼節點
//2.2 因爲當前節點是前驅節點的後繼節點,所以要在這裏線索化前驅節點的右指針
//從這裏可以看出,一個節點的後繼節點是當遍歷到它的後繼節點的時候纔去處理的
if(preNode != null && null == preNode.right) {
preNode.right = node;
preNode.rightType = 1;
}
//當前節點設置爲前驅節點
preNode = node;
//3. 線索化右子樹
midThreadedTree(node.right);
}
//遍歷中序線索化二叉樹
public void midThreadedTreeList(ThreadedNode root) {
ThreadedNode node = root;
while(node != null) {
//從左子樹中找到第一個leftType=1的節點
while(0 == node.leftType) {
node = node.left;
}
System.out.println(node.val);
//遍歷輸出當前節點的後繼節點
while(1 == node.rightType) {
node = node.right;
System.out.println(node.val);
}
//當前節點的右指針不再指向後繼節點,因此再從當前節點的右節點開始下一次循環
node = node.right;
}
}
}
二叉搜索樹BST(二叉排序樹、二叉查找樹)
二叉查找樹(Binary Search Tree),(又:二叉搜索樹,二叉排序樹)它或者是一棵空樹,或者是具有下列性質的二叉樹: 若它的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值; 若它的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值; 它的左、右子樹也分別爲二叉排序樹。
/**
* 二叉搜索樹(二叉查找樹、二叉排序樹)
*/
public class BinarysearchtreeDemo {
public static void main(String[] args) {
Node root = new Node(10);
Node n9 = new Node(9);
Node n14 = new Node(14);
Node n13 = new Node(13);
Node n16 = new Node(16);
Node n11 = new Node(11);
root.left = n9;
root.right = n14;
n14.left = n13;
n14.right = n16;
n13.left = n11;
BinarySearchTree binarySearchTree = new BinarySearchTree();
//遞歸查找
Node result1 = binarySearchTree.searchRecursion(root, 14);
System.out.println(result1);
//非遞歸查找
Node result2 = binarySearchTree.search(root, 14);
System.out.println(result2);
//插入
binarySearchTree.insert(root, 8);
//刪除
Node result3 = binarySearchTree.delete(root, 10);
if(result3 != null) { //刪除的是根節點
root = result3;
}
}
}
class BinarySearchTree{
//遞歸查找
public Node searchRecursion(Node node, int target) {
if(null == node) return null;
if(target < node.val) {
return searchRecursion(node.left, target);
}else if(target > node.val) {
return searchRecursion(node.right, target);
}else{
return node;
}
}
//非遞歸查找
public Node search(Node node, int target) {
Node currentNode = node;
while(currentNode != null) {
if(target < currentNode.val) {
currentNode = currentNode.left;
}else if(target > currentNode.val) {
currentNode = currentNode.right;
}else{
return currentNode;
}
}
return null;
}
//插入元素(如果存在值相等的元素,則向右子樹查找)
public void insert(Node root, int val) {
if(null == root) {
root = new Node(val);
}
Node currentNode = root;
//設置爲true,因爲循環內部一定會達到退出條件
while(true) {
if(val < currentNode.val) {
if(null == currentNode.left) {
currentNode.left = new Node(val);
return;
}
currentNode = currentNode.left;
}else{
if(null == currentNode.right) {
currentNode.right = new Node(val);
return;
}
currentNode = currentNode.right;
}
}
}
/**
* 刪除元素(設節點爲D),分爲如下幾種情況:
* 1. D是葉子節點,則直接刪除
* 2. D只有一個子節點,則讓D節點的父節點指向該子節點
* 3. D有左右兩個子節點,這種情況可以轉換爲第1種或者第2種情況:
* 3.1 遍歷D的右子節點,從中找到最小的節點記爲T(找到的這個節點肯定是葉子結點或者只有一個子節點)
* 3.2 將T節點值賦值給D節點
* 3.3 轉換爲刪除T節點
*/
public Node delete(Node root, int target) {
Node deleteNode = root;
Node deleteNParent = null;
//找到要刪除的節點和其父節點
while(deleteNode != null && deleteNode.val != target) {
deleteNParent = deleteNode;
if(target < deleteNode.val) {
deleteNode = deleteNode.left;
}else{
deleteNode = deleteNode.right;
}
}
//沒有找到要刪除的節點
if(null == deleteNode) return null;
//刪除節點有左右兩個子節點
if(deleteNode.left != null && deleteNode.right != null) {
deleteNParent = deleteNode;
Node minNode = deleteNode.right; //minNode指向右子樹中最小節點
while(minNode.left != null) {
deleteNParent = minNode;
minNode = minNode.left;
}
deleteNode.val = minNode.val;
deleteNode = minNode;
}
Node child; //要刪除節點的子節點
if(null == deleteNode.left && null == deleteNode.right) {
child = null;
}else{
child = deleteNode.left != null ? deleteNode.left : deleteNode.right;
}
if(null == deleteNParent) { //刪除的是根節點
return child;
}else if(deleteNParent.left == deleteNode) {
deleteNParent.left = child;
}else{
deleteNParent.right = child;
}
return null;
}
}
二叉搜索樹是常用的一種二叉樹,它支持快速插入、刪除、查找操作,各個操作的時間複雜度和樹的高度成正比,理想情況下,時間複雜度是O(logn)。但是,二叉搜索樹在頻繁動態更新過程中,可能會出現樹的高度遠大於logn的情況,極端情況下二叉樹還會退化成鏈表一樣的結構。
數列{1,2,3,4,5,6},要求創建一顆二叉排序樹(BST):
這顆BST樹存在的問題分析:
- 左子樹全部爲空,從形式上看,更像一個單鏈表.
- 插入速度沒有影響,但是查詢速度明顯降低(因爲需要依次比較), 不能發揮BST的優勢,因爲每次還需要比較左子樹,其查詢速度比單鏈表還慢
解決方案-平衡二叉樹(AVL)
平衡二叉樹(AVL樹)
平衡二叉樹也叫平衡二叉搜索樹(Self-balancing binary search tree)又被稱爲AVL樹, 可以保證查詢效率較高。具有以下特點:
- 它是一 棵空樹或它的左右兩個子樹的高度差的絕對值不超過1
- 左右兩個子樹都是一棵平衡二叉樹
平衡二叉樹的常用實現方法有AVL、紅黑樹、替罪羊樹、Treap、伸展樹等。在工程中,一般會使用紅黑樹實現平衡二叉樹。但是很多平衡二叉查找樹其實並沒有嚴格符合上面的定義(樹中任意一個節點的左右子樹的高度相差不能大於1),比如紅黑樹,它從根節點到各個葉子節點的最長路徑,有可能會比最短路徑大一倍。如果我們現在設計一個新的平衡二叉查找樹,只要樹的高度不比log2n大很多(以2爲底n的對數,一棵極其平衡的滿二叉樹或完全二叉樹的高度大約是log2n),儘管它不符合我們前面講的嚴格的平衡二叉查找樹的定義,但我們仍然可以說,這是一個合格的平衡二叉查找樹。比如紅黑樹,它的高度接近2log2n。
紅黑樹
紅黑樹的英文是“Red-Black Tree”,簡稱R-B Tree。它是一種不嚴格的平衡二叉查找樹。紅黑樹中的節點,一類被標記爲黑色,一類被標記爲紅色。除此之外,一棵紅黑樹還需要滿足這樣幾個要求:
- 根節點是黑色的;
- 每個葉子節點都是黑色的空節點(NIL),也就是說,葉子節點不存儲數據;
- 任何相鄰的節點都不能同時爲紅色,也就是說,紅色節點是被黑色節點隔開的;
- 每個節點,從該節點到達其可達葉子節點的所有路徑,都包含相同數目的黑色節點;
爲什麼使用紅黑樹而不是AVL樹呢?
AVL樹嚴格平衡,雖然查找效率高,但是插入和刪除節點效率低,因爲要維持平衡而大量調整節點。紅黑樹只滿足近似平衡,要證明紅黑樹是近似平衡的,我們只需要分析,紅黑樹的高度是否比較穩定地趨近log2n就好了。紅黑樹的查找效率只比AVL樹低一點,但是插入和刪除的效率比AVL高很多。
紅黑樹的高度只比高度平衡的AVL樹的高度(log2n)僅僅大了一倍,而實際上紅黑樹的總體性能更好。