樹
樹是一種非常常見且非常重要的數據結構。與線性數據結構不同,樹是非線性數據結構,因而也比鏈表、數組等要複雜。但是學好樹也不難,學不好的話,那就只能找棵樹自掛東南枝了。
樹的定義:樹是由結點或頂點和邊組成的(可能是非線性的)且不存在着任何環的一種數據結構。沒有結點的樹稱爲空(null或empty)樹。一棵非空的樹包括一個根結點,還(很可能)有多個附加結點,所有結點構成一個多級分層結構。
1. 二叉樹
1.1定義
二叉樹(Binary Tree)是n(n>=0)個節點的有限集合,該集合或者空集(稱爲空二叉樹),或者由一個根節點和兩棵互不相交的,分別稱爲根節點的左子樹和右子樹的二叉樹組成。
滿二叉樹除最後一層無任何子節點外,每一層上的所有結點都有兩個子結點。也可以這樣理解,除葉子結點外的所有結點均有兩個子結點。節點數達到最大值,所有葉子結點必須在同一層上。
完全二叉樹 若設二叉樹的深度爲h,除第 h 層外,其它各層 (1~(h-1)層) 的結點數都達到最大個數,第h層所有的結點都連續集中在最左邊,這就是完全二叉樹。
1.2 性質
-
二叉樹性質
- 在非空二叉樹中,第i層的結點總數不超過2i-1, i>=1;
- 深度爲h的二叉樹最多有2h-1個結點(h>=1),最少有h個結點;
- 對於任意一棵二叉樹,如果其葉結點數爲N0,而度數爲2的結點總數爲N2,則N0=N2+1;
- 具有n個結點的完全二叉樹的深度爲log2(n+1);
- 有N個結點的完全二叉樹各結點如果用順序方式存儲,則結點之間有如下關係:
若I爲結點編號則 如果I>1,則其父結點的編號爲I/2;
如果2I<=N,則其左兒子(即左子樹的根結點)的編號爲2I;若2I>N,則無左兒子;
如果2I+1<=N,則其右兒子的結點編號爲2I+1;若2I+1>N,則無右兒子。 - 給定N個節點,能構成h(N)種不同的二叉樹,其中h(N)爲卡特蘭數的第N項,h(n)=C(2*n, n)/(n+1)。
- 設有i個枝點,I爲所有枝點的道路長度總和,J爲葉的道路長度總和J=I+2i。
-
滿二叉樹的性質:
- 一顆樹深度爲h,最大層數爲k,深度與最大層數相同,k=h;
- 葉子數爲2h;
- 第k層的結點數是:2k-1;
- 總結點數是:2k-1,且總節點數一定是奇數
-
完全二叉樹
- 完全二叉樹是效率很高的數據結構,堆是一種完全二叉樹或者近似完全二叉樹,所以效率極高,像十分常用的排序算法、Dijkstra算法、Prim算法等都要用堆才能優化,二叉排序樹的效率也要藉助平衡性來提高,而平衡性基於完全二叉樹。
2. 二叉查找樹
2.1 定義
又稱爲是二叉排序樹(Binary Sort Tree)或二叉搜索樹。可能是一棵空樹,或者是具有下列性質的二叉樹:
1) 若左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;
2) 若右子樹不空,則右子樹上所有結點的值均大於或等於它的根結點的值;
3) 左、右子樹也分別爲二叉排序樹;
4) 沒有鍵值相等的節點。
2.2 性質
- 對二叉查找樹進行中序遍歷,即可得到有序的數列。
這裏補充一下二叉樹的遍歷順序,首先看圖:- 前序遍歷:規則是若二叉樹爲空,則空操作返回,否則先訪問根結點,然後前序遍歷左子樹,再前序遍歷右子樹。如圖所示,遍歷的順序爲:ABDECF。
- 中序遍歷:規則是若樹爲空,則空操作返回,否則從根結點開始(注意並不是先訪問根結點),中序遍歷根結點的左子樹,然後是訪問根結點,最後中序遍歷右子樹。如圖所示,遍歷的順序爲:DBEAFC。
- 後序遍歷:規則是若樹爲空,則空操作返回,否則從左到右先葉子後結點的方式遍歷訪向左右子樹,最後是訪問根結點。如圖所示,遍歷的順序爲:DEBFCA。
- 二叉查找樹的時間複雜度:它和二分查找一樣,插入和查找的時間複雜度均爲O(logn),但是在最壞的情況下仍然會有O(n)的時間複雜度。原因在於插入和刪除元素的時候,樹沒有保持平衡。我們追求的是在最壞的情況下仍然有較好的時間複雜度,這就是平衡查找樹設計的初衷。
- 二叉查找樹的高度決定了二叉查找樹的查找效率。
2.3二叉查找樹的插入過程:
- 若當前的二叉查找樹爲空,則插入的元素爲根節點;
- 若插入的元素值小於根節點值,則將元素插入到左子樹中;
- 若插入的元素值不小於根節點值,則將元素插入到右子樹中。
2.4 二叉查找樹的刪除
2.4.1 刪除葉子節點
p爲葉子節點,直接刪除該節點,再修改其父節點的指針(注意分是根節點和不是根節點)
刪除葉子節點25,則
2.4.2 刪除單支節點
p爲單支節點(即只有左子樹或右子樹)。讓p的子樹與p的父親節點相連,刪除p即可(注意分是根節點和不是根節點)
刪除上圖的值爲16節點
2.4.3 節點完整的點
刪除有左右子樹的節點,有兩個辦法,即用左子樹的最大節點取代待刪除節點,或者用最小右子樹的節點取代待刪除節點。
- 用左子樹的最大節點取代待刪除節點
- 用右子樹的最小節點取代待刪除節點
那就是用36取代33。即
這兩種方法的所產生的二叉樹形態看起來似乎不一樣,那麼爲什麼有這兩種結果呢?其實這隻有一種結果,判斷其是否正確的依據是刪除該節點之後,二叉樹的中序遍歷結果是否保持相對一致。在刪除之前,二叉搜索樹的中序遍歷結果是
1 -> 3 -> 5 -> 10 -> 12 -> 15 -> 17 -> 28-> 33-> 36-> 38
當我們刪除33節點時,中序遍歷的結果就是28後面是36。對於這兩個方法產生的中序遍歷結果,都是這樣的。
3.代碼實現
alk is cheap, show me the code.
Node.java
public class Node<T extends Comparable> {
public T val;
public Node left;
public Node right;
public Node(T val){
this.val = val;
}
@Override
public String toString() {
return "Node{" +
"val=" + val +
'}';
}
}
BInarySearchTree.java
public class BinarySearchTree {
//二叉搜索樹根節點
public static Node root;
public BinarySearchTree() {
this.root = null;
}
/**
* 查找
* * 樹深(N) O(lgN)
* * 1. 從root節點開始
* * 2. 比當前節點值小,則找其左節點
* * 3. 比當前節點值大,則找其右節點
* * 4. 與當前節點值相等,查找到返回TRUE
* * 5. 查找完畢未找到, * *
*
* @param key * @return
*/
public Node search(Node key) {
Node current = root;
int res = 0;
while (current != null && ((res
= key.val.compareTo(current.val)) != 0)) {
if (res < 0) current = current.left;
else current = current.right;
}
return current;
}
/**
* 插入節點
* @param key
* @return
*/
public Node insert(Node key) {
//當前節點
Node current = root;
//上個節點
Node parent = null;
//如果根節點爲空,那麼插入的節點爲跟節點
if (current == null) {
root = key;
return key;
}
while (true) {
parent = current;
int res = key.val.compareTo(current.val);
if (res < 0) {
current = current.left;
if (current == null) {
parent.left = key;
return key;
}
} else {
current = current.right;
if (current == null) {
parent.right = key;
return key;
}
}
}
}
/**
* 1.找到刪除節點
* 2.如果刪除節點左節點爲空 , 右節點也爲空;
* 3.如果刪除節點只有一個子節點 右節點 或者 左節點
* 4.如果刪除節點左右子節點都不爲空
* @param key
* @return
*/
public Node delete(Node key) {
Node parent = root;
Node current = root;
boolean isLeft = false;
//找到待刪除節點,以及是否是左子樹
while (!key.val.equals(current.val)) {
parent = current;
int res = key.val.compareTo(current.val);
if (res < 0) {
isLeft = true;
current = current.left;
} else {
isLeft = false;
current = current.right;
}
if (current == null) return null;
} //如果刪除的左右節點都爲空
if (current.left == null && current.right == null) {
if (current == root)
root = null;
//該節點在左子樹
if (isLeft) parent.left = null;
else parent.right = null;
} else if (current.left == null) {
// 該節點爲右子樹
if (current == root) root = current.right;
else if (isLeft) {
parent.left = current.right;
} else {
parent.right = current.right;
}
} else if (current.right == null) {
if (current == root) root = current.left;
else if (isLeft) parent.right = current.left;
else parent.left = current.left;
} else if (current.right != null && current.left != null) {
Node successor = getDeleteSuccessor(current);
if (current == root) {
root = successor;
} else if (isLeft) {
parent.left = successor;
} else {
parent.right = successor;
}
successor.left = current.left;
}
return current;
}
private Node getDeleteSuccessor(Node deleteNode) {
Node successor = null;
Node successorParent = null;
Node current = deleteNode.right;
while (current != null) {
successorParent = successor;
successor = current;
current = current.left;
}
// 檢查後繼者(不可能有左節點樹)是否有右節點樹
// 如果它有右節點樹,則替換後繼者位置,加到後繼者父親節點的左節點.
if (!successor.equals(deleteNode.right)) {
successorParent.left = successor.right;
successor.right = deleteNode.right;
}
return successor;
}
public void toString(Node root) {
if (root != null) {
toString(root.left);
System.out.print("value = " + root.val + " -> ");
toString(root.right);
}
}
}
測試類,首先是
Person.java
public class Person implements Comparable<Person> {
public int age;
public String name;
public Person() {
}
public Person(int age,String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age;
}
@Override
public int hashCode() {
return Objects.hash(age);
}
@Override
public int compareTo(Person o) {
return this.age - o.age;
}
}
BSTTest.java
public class BSTTest {
public static void main(String[] args) {
BinarySearchTree b = new BinarySearchTree();
b.insert(new Node(new Person(3, "zhangsan")));
b.insert(new Node(new Person(8, "lisi")));
b.insert(new Node(new Person(1, "wangwu")));
b.insert(new Node(new Person(4, "zhaoer")));
b.insert(new Node(new Person(6, "qianba")));
b.insert(new Node(new Person(2, "sunjiu")));
b.insert(new Node(new Person(10, "guyi")));
b.insert(new Node(new Person(9, "chenqi")));
b.insert(new Node(new Person(20, "wuda")));
b.insert(new Node(new Person(25, "qiao")));
// 打印二叉樹
b.toString(b.root);
System.out.println();
// 是否存在節點值10
Node node01 = b.search(new Node(new Person(10, "guyi")));
System.out.println("是否存在節點值爲10 => " + node01.val);
// 是否存在節點值11
Node node02 = b.search(new Node(new Person(11, "ergou")));
System.out.println("是否存在節點值爲11 => " + node02);
// 刪除節點8
Node node03 = b.delete(new Node(new Person(8, "lisi")));
//Node node03 = b.delete1(b.root, new Node(new Person(8,"lisi")));
System.out.println("刪除節點8 => " + node03.val);
b.toString(b.root);
}
}
運行結果如圖