資料結構與演算法(十一)
樹的應用
二叉排序樹
給你一個數列 7, 3, 10, 12, 5, 1, 9
,要求能夠高效的完成對資料的查詢和新增。
在 為什麼需要該資料結構 中講解了陣列、連結串列資料結構的優缺點,簡單說:
-
陣列訪問快,增刪慢
新增或移除時,需要整體移動資料
-
連結串列增刪快,訪問慢
只能從頭開始遍歷查詢
那麼利用 二叉排序樹(Binary Sort/Search Tree),既可以保證資料的檢索速度,同時也可以保證資料的插入、刪除、修改 的速度
二叉排序樹介紹
二叉排序樹(Binary Sort/Search Tree),簡稱 BST。
對於二叉排序樹的任何一個 非葉子節點,要求如下:
- 左節點,比父節點小
- 右節點,比父節點大
特殊說明:如果有有相同的值,可以將該節點放在左節點或右節點。當然,最理想的是沒有重複的值,比如 Mysql 中的 B 樹索引,就是以主鍵 ID 來排序的。
比如對下面這個二叉樹增加一個節點:
- 從根節點開始,發現比 7 小,直接往左子樹查詢,相當於直接折半了
- 比 3 小,再次折半
- 比 1 大:直接掛在 1 的右節點
建立、遍歷、刪除、查詢
//定義節點 class Node{ int value; Node left; Node right; public Node(int value) { this.value = value; } //新增節點 public void add(Node node){ if(node == null){ return; } if(node.value < this.value){ if(this.left == null){ this.left = node; }else{ this.left.add(node);//遞迴查詢新增 } }else{ if(this.right == null){ this.right = node; }else{ this.right.add(node); } } } //查詢要刪除的父節點 public Node searchParent(int value){ if(this.left != null && this.left.value == value || this.right != null && this.right.value == value){ return this; }else if(this.left != null && this.value > value){//左遞迴查詢 return this.left.searchParent(value); }else if(this.right != null && this.value < value){//右遞迴查詢 return this.right.searchParent(value); }else{ return null; } } //查詢要刪除的節點 public Node search(int value){ if(this.value == value){ return this; }else if(value < this.value){ if(this.left != null){ return this.left.search(value);//向左遞迴查詢 }else{ return null; } }else { if(this.right != null){ return this.right.search(value); }else{ return null; } } } //中序遍歷 public void infixOrder(){ if(this.left != null){ this.left.infixOrder(); } System.out.println(this); if(this.right != null){ this.right.infixOrder(); } } @Override public String toString() { return "Node{" + "value=" + value + '}'; } } class BinarySortTree{ private Node root; public Node getRoot() { return root; } public void add(Node node){ if(root == null){ root = node; }else{ root.add(node); } } public void infixOrder(){ if(root != null){ root.infixOrder(); }else{ System.out.println("二叉排序樹為空"); } } //查詢要刪除的節點 public Node search(int value){ if(root != null){ return root.search(value); }else{ return null; } } //查詢要刪除的父節點 public Node searchParent(int value){ if(root != null){ return root.searchParent(value); }{ return null; } } //找到目標節點為根節點最小的那個節點,並刪除其節點,然後返回其值 public int delRightMin(Node root){ Node target = root; while(target.left != null){ target = target.left; } delNode(target.value); return target.value; } //刪除節點 public void delNode(int value){ Node targetNode = search(value); //要刪除的節點是根節點 if(targetNode == root){ root = null; return; } Node parentNode = searchParent(value); if(targetNode.left == null && targetNode.right == null){//刪除的葉子節點 if(parentNode.left == targetNode){//當目標節點為父節點的左子節點 parentNode.left = null; }else{ parentNode.right = null; } }else if(targetNode.left != null && targetNode.right != null){//刪除的節點為有兩個子節點的非葉子節點 targetNode.value = delRightMin(targetNode.right); }else{//刪除的節點為只有一個節點的非葉子節點 if(targetNode.left != null){ if(parentNode != null){ if(parentNode.left == targetNode){ parentNode.left = targetNode.left; }else{ parentNode.right = targetNode.left; } }else{ root = targetNode.left; } }else { if(parentNode != null){ if(parentNode.left == targetNode){ parentNode.left = targetNode.right; }else{ parentNode.right = targetNode.right; } }else{ root = targetNode.right; } } } } }
完整平衡二叉樹(AVL樹)
二叉排序樹可能的問題
一個數列 {1,2,3,4,5,6}
,建立一顆二叉排序樹(BST)
建立完成的樹如上圖所示,那麼它存在的問題有以下幾點:
-
左子樹全部為空,從形式上看,更像一個單鏈表
-
插入速度沒有影響
-
查詢速度明顯降低
因為需要依次比較,不能利用二叉排序樹的折半優勢。而且每次都還要比較左子樹,可能比單鏈表查詢速度還慢。
那麼解決這個劣勢的方案就是:平衡二叉樹(AVL)。
基本介紹
平衡二叉樹也叫 平衡二叉搜尋樹(Self-balancing binary search tree),又被稱為 AVL 樹,可以保證 查詢效率較高。它是解決 二叉排序
它的特點:是一顆空樹或它的 左右兩個子樹的高度差的絕對值不超過 1,並且左右兩個子樹都是一顆平衡二叉樹。
平衡二叉樹的常用實現方法有:
- 紅黑樹
- AVL(演算法)
- 替罪羊樹
- Treap
- 伸展樹
如下所述,哪些是平衡二叉樹?
-
是平衡二叉樹:
- 左子樹高度為 2
- 右子樹高度為 1
他們差值為 1
-
也是平衡二叉樹
-
不是平衡二叉樹
- 左子樹高度為 3
- 右子樹高度為 1
他們差值為 2,所以不是
單旋轉(左旋轉)
一個數列 4,3,6,5,7,8
,創建出它對應的平衡二叉樹。
思路分析:下圖紅線部分是調整流程。
按照規則調整完成之後,形成了下面這樣一棵樹
完整流程如下圖所示:
插入 8 時,發現左右子樹高度相差大於 1,則進行左旋轉:
- 建立一個新的節點
newNode
,值等於當前 根節點 的值(以 4 建立) - 把新節點的 左子樹 設定為當前節點的 左子樹
- 把新節點的 右子樹 設定為當前節點的 右子樹的左子樹
- 把 當前節點 的值換為 右子節點 的值
- 把 當前節點 的右子樹設定為 右子樹的右子樹
- 把 當前節點 的左子樹設定為新節點
注:左圖是調整期,右圖是調整後。注意調整期的 6 那個節點,調整之後,沒有節點指向他了。也就是說,遍歷的時候它是不可達的。那麼將會自動的被垃圾回收掉。
樹高度計算
前面說過,平衡二叉樹是為了解決二叉排序樹中可能出現的查詢效率問題,那麼基本上的程式碼都可以在之前的二叉排序樹上進行優化。那麼下面只給出與當前主題相關的程式碼,最後放出一份完整的程式碼。
樹的高度計算,我們需要得到 3 個高度:
- 這顆樹的整體高度
- 左子樹的高度
- 右子樹的高度
//Node
//獲得左子樹的高度
public int leftHeight(){
if(left == null){
return 0;
}
return left.height();
}
//獲得右子樹的高度
public int rightHeight(){
if(right == null){
return 0;
}
return right.height();
}
//獲得當前節點高度
public int height(){
return Math.max(left == null ? 0 : left.height(),right == null ? 0 : right.height())+1;
}
旋轉
說下旋轉的時機:也就是什麼時機採取做旋轉的操作?
當然是:當 右子樹高度 - 左子樹高度 > 1
時,才執行左旋轉。
這裡就得到一些資訊:
-
每次新增完一個節點後,就需要檢查樹的高度
-
滿足
右子樹高度 - 左子樹高度 > 1
,那麼一定滿足下面的條件:- 左子樹高度為 1
- 右子樹高度為 3
也就是符合這張圖
//左旋轉
public void leftRotate(){
//使用當前跟節點建立一個新節點
Node newNode = new Node(value);
//當前根節點的左子樹設定為新節點的左子樹
newNode.left = left;
//當前根節點的右子樹的左子樹設定為當前新節點的右子樹
newNode.right = right.left;
//將右子樹的值拷貝到當前根節點
value = right.value;
//將跟節點的右子樹設定為右子樹的右子樹
right = right.right;
//將根節點左子樹設定為新節點
left = newNode;
}
右旋轉
其實這個就很好理解了:
- 左旋轉:
右 - 左 > 1
,把右邊的往左邊旋轉一層 - 右旋轉:
左 - 右 > 1
,把左邊的往右邊旋轉一層
他們其實是反著來的,那麼右旋轉的思路如下:
- 建立一個新的節點
newNode
,值等於當前 根節點 的值(以 4 建立) - 把新節點的 右子樹 設定為當前節點的 右子樹
- 把新節點的 左子樹 設定為當前節點的 左子樹的右子樹
- 把 當前節點 的值換為 左子節點 的值
- 把 當前節點 的左子樹設定為 左子樹的左子樹
- 把 當前節點 的右子樹設定為新節點
//右旋轉
public void rightRotate(){
Node newNode = new Node(value);
newNode.right = right;
newNode.left = left.right;
value = left.value;
left = left.left;
right = newNode;
}
雙旋轉
在前面的例子中,使用單旋轉(即一次旋轉)就可以將非平衡二叉樹轉換為平衡二叉樹。
但是在某些情況下,就無法做到。比如下面這兩組數列
左側這個樹滿足 leftHeight - rightHeight > 1
,也就是滿足右旋轉,旋轉之後,樹結構變化了。但是還是一個非平衡二叉樹。
它的主要原因是:root 左子樹的 左子樹高度 小於 右子樹的高度。即:節點 7 的左子樹高度小於右子樹的高度。
解決辦法:
- 先將 7 這個節點為 root 節點,進行左旋轉
- 再將原始的 root 節點進行右旋轉
過程示意圖如下:
其實可以參考下前面兩個單旋轉的圖例,它有這樣一個特點:
- 右旋轉:
- root 的 left 左子樹高度 大於 右子樹高度
- 右旋轉的時候,會將 left.right 旋轉到 right.left 節點上
- 左旋轉:
- root 的 right 右子樹高度 大於 左子樹高度
- 左旋轉的時候,會將 right.left 旋轉到 left.right 上。
如果不滿足這個要求,在第二個操作的時候,就會導致 2 層的高度被旋轉到 1 層的節點下面,導致不平衡了。
//左旋轉
if(rightHeight() - leftHeight() > 1){
//如果右子樹的左子樹高度大於右子樹的右子樹,先進行右旋轉,再進行左旋轉
if(right != null && right.leftHeight() > right.rightHeight()){
right.rightRotate();
leftRotate();//左旋轉
}else{
leftRotate();
}
return;//進行了旋轉之後就不用進行下面的再次旋轉
}
//右旋轉
if(leftHeight() - rightHeight() > 1){
//如果左子樹的右子樹高度,大於左子樹的左子樹先進行左旋轉,再進行右旋轉
if(left != null && left.rightHeight() > left.leftHeight()){
left.leftRotate();//左子樹左旋轉
rightRotate();//右旋轉
}else{
rightRotate();
}
}
完整程式碼
//AVL樹
class AVLTree{
private Node root;
public Node getRoot() {
return root;
}
public void add(Node node){
if(root == null){
root = node;
}else{
root.add(node);
}
}
public void infixOrder(){
if(root != null){
root.infixOrder();
}else{
System.out.println("二叉排序樹為空");
}
}
//查詢要刪除的節點
public Node search(int value){
if(root != null){
return root.search(value);
}else{
return null;
}
}
//查詢要刪除的父節點
public Node searchParent(int value){
if(root != null){
return root.searchParent(value);
}{
return null;
}
}
//找到目標節點為根節點最小的那個節點,並刪除其節點,然後返回其值
public int delRightMin(Node root){
Node target = root;
while(target.left != null){
target = target.left;
}
delNode(target.value);
return target.value;
}
//刪除節點
public void delNode(int value){
Node targetNode = search(value);
//要刪除的節點是根節點
if(targetNode == root){
root = null;
return;
}
Node parentNode = searchParent(value);
if(targetNode.left == null && targetNode.right == null){//刪除的葉子節點
if(parentNode.left == targetNode){//當目標節點為父節點的左子節點
parentNode.left = null;
}else{
parentNode.right = null;
}
}else if(targetNode.left != null && targetNode.right != null){//刪除的節點為有兩個子節點的非葉子節點
targetNode.value = delRightMin(targetNode.right);
}else{//刪除的節點為只有一個節點的非葉子節點
if(targetNode.left != null){
if(parentNode != null){
if(parentNode.left == targetNode){
parentNode.left = targetNode.left;
}else{
parentNode.right = targetNode.left;
}
}else{
root = targetNode.left;
}
}else {
if(parentNode != null){
if(parentNode.left == targetNode){
parentNode.left = targetNode.right;
}else{
parentNode.right = targetNode.right;
}
}else{
root = targetNode.right;
}
}
}
}
}
//節點
class Node{
int value;
Node left;
Node right;
public Node(int value) {
this.value = value;
}
//獲得左子樹的高度
public int leftHeight(){
if(left == null){
return 0;
}
return left.height();
}
//獲得右子樹的高度
public int rightHeight(){
if(right == null){
return 0;
}
return right.height();
}
//獲得當前節點高度
public int height(){
return Math.max(left == null ? 0 : left.height(),right == null ? 0 : right.height())+1;
}
//左旋轉
public void leftRotate(){
//使用當前跟節點建立一個新節點
Node newNode = new Node(value);
//當前根節點的左子樹設定為新節點的左子樹
newNode.left = left;
//當前根節點的右子樹的左子樹設定為當前新節點的右子樹
newNode.right = right.left;
//將右子樹的值拷貝到當前根節點
value = right.value;
//將跟節點的右子樹設定為右子樹的右子樹
right = right.right;
//將根節點左子樹設定為新節點
left = newNode;
}
//右旋轉
public void rightRotate(){
Node newNode = new Node(value);
newNode.right = right;
newNode.left = left.right;
value = left.value;
left = left.left;
right = newNode;
}
public void add(Node node){
if(node == null){
return;
}
if(node.value < this.value){
if(this.left == null){
this.left = node;
}else{
this.left.add(node);//遞迴查詢新增
}
}else{
if(this.right == null){
this.right = node;
}else{
this.right.add(node);
}
}
//左旋轉
if(rightHeight() - leftHeight() > 1){
//如果右子樹的左子樹高度大於右子樹的右子樹,先進行右旋轉,再進行左旋轉
if(right != null && right.leftHeight() > right.rightHeight()){
right.rightRotate();
leftRotate();//左旋轉
}else{
leftRotate();
}
return;//進行了旋轉之後就不用進行下面的再次旋轉
}
//右旋轉
if(leftHeight() - rightHeight() > 1){
//如果左子樹的右子樹高度,大於左子樹的左子樹先進行左旋轉,再進行右旋轉
if(left != null && left.rightHeight() > left.leftHeight()){
left.leftRotate();//左子樹左旋轉
rightRotate();//右旋轉
}else{
rightRotate();
}
}
}
}