數據結構之樹從入門到如土(二)----帶你從頭碾壓 一顆AVL樹(多圖)

AVL樹 解決了二叉樹的什麼問題?

AVL 和 二叉樹的 最大區別是能自平衡,簡單的說就是多了一步插入的時候會按照插入的值做 自平衡 防止 插入 向 1 ,3 ,4, 6, 8, 9 這樣 左邊的值 比右邊小 就會變成 鏈表了。
在這裏插入圖片描述

AVL樹的定義:AVL樹是二叉樹,其各個節點的左右子樹的高度相差不超過1。

AVL樹是最早提出的自平衡二叉樹,在AVL樹中任何節點的兩個子樹的高度最大差別爲一,所以它也被稱爲高度平衡樹。AVL樹得名於它的發明者G.M. Adelson-Velsky和E.M. Landis。

  1. AVL樹查找、插入和刪除在平均和最壞情況下都是O(log n),增加和刪除可能需要通過一次或多次樹旋轉來重新平衡這個樹。
  2. AVL 比較適合 不需要刪除 值插入的場景。
平衡因子:

二叉樹和AVL樹不同體現在,AVL樹的平衡因子不會超過1 而二叉樹的平衡 因子就沒有上限了,如果 平衡 因子 + 1 和 你插入的 數據長度一樣 就變成鏈表了。 那什麼是 平衡 因子呢,

劃重點

平衡因子 : 左邊子樹的高度 - 右邊子樹的高度
那怎麼定義 高度呢?
高度定義 = max{左子樹.高度,右子樹.高度} + 1
特殊情況: 如果沒有左右子節點了 那麼 返回 -1
在這裏插入圖片描述
有了 定義 我們就可以 來實現一個最簡單的計算AVL樹的 平衡因子的功能了,來一步步實現。
首先 我們看下 基本的數據結構 和樹的定義一樣,node節點要有左邊和右邊的樹枝和 一個 存儲Key的變量 ,然後我們再定義一個 一顆樹的根節點的結構,要有根節點 和包含節點的數量。

type avltree struct {
	//定義根節點
	root *node
	//包含節點的數量
	size int
}
//定義一顆二叉樹
func NewAvTree() *avltree{
	rt := NewAvlNode()
	return &avltree{
		root:rt,
	}
}
type node struct {
	Key int //值
	left *node //左子樹
	right *node //右子樹
	height int //當前樹的高度 = max{left.height,right.height}
}
//新建avl樹的節點
func NewAvlNode() *node{
	return &node{
		Key: 0,
		left:  nil,
		right: nil,
		height:0,
	}
}

二叉樹的遞歸 之 查找

在這裏插入圖片描述

遞歸是一種計算過程,如果其中每一步都要用到前一步或前幾步的結果,稱爲遞歸的。用遞歸過程定義的函數,稱爲遞歸函數,例如連加、連乘及階乘等。凡是遞歸的函數,都是可計算的,即能行的 .
從上面圖上可以看出,一顆樹 從上至下 就只有左子樹 和 右子樹 只能往左 往右走
那麼 往左往右.那麼 我們就可以通過遞歸 實現 便利 整顆數的結構。

下面 我們 實現AVL樹其中最簡單的功能,同時 下面的增刪 使用的的遞歸的方式也是大同小異的。

func (avlt *avltree) Find(key int) *node{
	return find(avlt.root,key)
}
func find(avlnode *node,key int) *node{
	if avlnode == nil{
		return nil
	}
	if avlnode.Key > key{
		ret := find(avlnode.left,key)
		return ret
	}else if avlnode.Key < key{
		ret :=find(avlnode.right,key)
		return ret
	}else{
		return avlnode
	}
}

上面 3 的 判斷 分別是大於小於 和等於 只有等於才返回當前node的值,所以 如果沒找到返回值是nil。

實現二叉樹insert方法

有了 樹 我們 還要將數據 加入到 樹纔行,下面 將要實現 Insert方法
插入 就是比大小,遞歸其實很簡單。
在這裏插入圖片描述

//插入key
func (avlt *avltree) Insert(key int){
	//第一次插入 將key 插入root 
	if avlt.size == 0{
		avlt.root.Key = key
	}else{
		avlt.root = insert(avlt.root,key)
	}
	//二叉樹節點數
	avlt.size ++
}

func insert(root *node,key int) *node{
	//遞歸的 結束條件 當葉子節點時 插入節點
	if root == nil{
		//在葉子節點下面建立新節點
		nd := NewAvlNode()
		nd.Key = key
		//把葉子結點 返回給上一個節點
		return nd
	}
	//往右邊 遍歷
	if key > root.Key{
		root.right = insert(root.right,key)
	//往左邊 遍歷
	}else if key < root.Key{
		root.left = insert(root.left,key)
	}
	//返回節點
	return root
}
func main(){
	a := NewAvTree()
	a.Insert(1)
	a.Insert(2)
	a.Insert(3)
	a.Insert(4)
	fmt.Println(a)
}

在這裏插入圖片描述
我們 插入 4個節點,此時 構造的就是一顆不平衡 的二叉樹,節點只有往右一個方向.

二叉樹計算 高度和平衡因子

好了準備工作完成,我們來實現前面 定義的 2個公式吧 分別計算高度 和 平衡因子.
上面 我們講過 定義高度 的方法是 max{左子樹.高度,右子樹.高度} + 1, 假設我們是計算機 我們 可不可以直接知道 45 的高度呢,不行吧 如果你從第一個節點 看去 那麼你是沒法知道下面 有多少節點的.
在這裏插入圖片描述
我們 不能從上邊直接獲得節點的 高度 只能從葉子節點 自下而上 逐層返回 然後計算高度。
所以我們需要
那假設 這種情況下 我們是不是 可以知道 這些節點的 高度呢? 答案是肯定的 因爲 他們已經是葉子結點了 沒有子節點了,所以就能知道 他們的節點 高度爲0 ,所以 我們需要遞歸到葉子節點 然後通過return 讓他們 返回 字節的高度 告訴上一層,所以需要用到萬能的遞歸。
在這裏插入圖片描述

func getHeight(root *node) int{
	//如果 是nil 節點 返回-1
	if root == nil{
		return -1
	}
	//左邊節點的高度
	left := getHeight(root.left)
	//右邊節點的高度
	right := getHeight(root.right)
	//計算 葉子結點返回給到 我的高度 然後通過 max{root.right,root.left} + 1 計算出本節點的高度
	root.height = Max(right,left) + 1
	//返回本節點的高度 告訴給上一層
	return root.height
}

在這裏插入圖片描述
是不是 很簡單,有了 高度接下來 我們就可以計算平衡因子了。

平衡因子:通過左子樹的高度 - 右子樹的高度

那我們只需要在計算高度的時候加上,這條公式就好了。

func getHeight(root *node) int{
..........省略前面代碼
//新增以下代碼
	balancefractor := left - right
	fmt.Println("平衡因子:",balancefractor)
........
}

下面 我們測試下 看看:
在這裏插入圖片描述

func main(){
	a := NewAvTree()
	a.Insert(1)
	a.Insert(2)
	a.Insert(3)
	a.Insert(4)
	getHeight(a.root)
	fmt.Println(a)
}

在這裏插入圖片描述
可以看到 結果和我們圖上畫的是一樣的,我們構造了一顆 極度不平衡的數,他的平衡 從上圖中 我們可以得出 :當一棵樹的 平衡因子的絕對值 == 樹中包含節點的數量 -1 時 就退化成了鏈表。

同時 我們 可以得出 當 平衡因子 小於 -1 時需要左旋,當平衡 因子大於 1時 需要右旋
圖中 樹明顯 往右傾嚴重,我們需要調整讓部分節點分給左邊,使他們平衡,這樣稱爲左旋,右旋的話剛好相反。

什麼時候 二叉樹的平衡因子的絕對值 大於 1

有了判定二叉樹的定義我們還需要知道什麼時候二叉樹失去了平衡,這樣我們才知道該怎麼調整。
首先 我們需要 理解的是,
一:插入一棵樹 只能使其祖先節點失去平衡,他的父親節點或者非祖先節點是不可能失去平衡的

在 AV樹裏面只需要判斷 4中情況 ,分別是左旋 右旋,左右旋,和右左旋.
當我們 找到那麼不平衡的節點時,我們只需要對下面的 子節點 和子節點的子節點進行操作就可以了。

在這裏插入圖片描述
在樹失去平衡前它總是一顆平衡的數,那麼 他左邊的高度和右邊的高度差的絕對值 相差不大於1,那麼 我們抽象出 上圖一樣的三個節點,在插入前它是平衡的,當新插入一個節點是,他下面的子樹 高度發生了變化 這樣就會導致 上面的 非父節點 的平衡因子 都發生變化,這取決於 祖先節點 另一個子樹 的高度, 就像 天平的 2端 如果質量相近就不會失去平衡。
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

在這裏插入圖片描述
在這裏插入圖片描述
上面 是 二叉樹 出現不平衡所有可能出現的情況,
總結上面圖 所講述的:
1. 不平衡 必然發生在祖先節點上.
2. 任何產生不平衡節點,我們只需要判斷調整它下面的子節點,孫子節點就可以 使它繼續保持平衡(其實 產生 不平衡 就是因爲 你某一邊 發育的 比 另一邊好高度高 那麼最簡單的平衡方法 就是 讓我下面平衡的節點 做我的 父節點 我做子節點這樣 新插入的節點 和我就沒什麼關係了 我已經 不是 原來新插入節點的祖先了 那麼 就保持了 插入前 的平衡了)
3. 我們要判斷 到底需要哪種 旋轉, 只需要判斷 不平衡節點下面的 左邊或右邊 下面的左右節點的高度 誰的 高度 高 那麼 肯定 是發生了插入的節點,如果是 樹往左邊傾斜的情況 我們有2 種處理 方式 如果 左邊的左邊 插入了節點 那麼單次右旋(RR)就好了,如果左邊的右邊插入了節點 那麼 先左旋後右旋(LRR),如果樹往右邊傾斜 則
相反.

樹的左旋 和 右旋

一.右旋:

在這裏插入圖片描述

//右旋
func (avlnode *node) RightRotate() *node{
	tmp :=avlnode.left
	avlnode.left = tmp.right
	tmp.right = avlnode
	return avlnode
}
左旋

在這裏插入圖片描述

//左旋
func (avlnode *node) LeftRotate() *node{
	tmp :=avlnode.right
	avlnode.right = tmp.left
	tmp.left = avlnode
	return avlnode
}
左右旋轉

在這裏插入圖片描述

//左右旋
func (avlnode *node) RLRoate() *node{
	avlnode.left = avlnode.left.LeftRotate()
	return avlnode.RightRotate()
}
右左旋轉

在這裏插入圖片描述

//右左旋
func (avlnode *node) RLRoate() *node{
	avlnode.right = avlnode.right.RightRotate()
	return avlnode.LeftRotate()
}

下面 我們將 前面 getheight 稍微做些修改變成,自動掉整平衡因子,經過前面的分析 我們 知道 當節點出現不平衡時 需要 通過 調整和子節點之間的關係來,繼續使當前不平衡的節點 保持平衡, 所以 下一個 難點 是如何判斷 節點 該採取哪種旋轉 方法.
前面 我們已經 提到過 那就是判斷 左邊或右邊節點的左右節點的高度 的高度 左邊的左邊 節點高度 高 我們就用 一個右旋 如果 是左邊的右邊 或者 右邊的左邊 我們 就分別使用 左右 右左旋.

所以 核心 僞代碼:
balance == left.height - right.height
if balace == 2{
}else if balace == -2 {
}
上面代碼,就是計算 如果平衡因子使正的 說明 這棵樹 往左傾斜了,否則 就是右傾.
if left.left > left.right {
node = node.leftrotate
}else{
node = node.LeftThenRightrotate()
}
上面 代碼 主要判斷節點 是 插入了左邊的左邊 還是 插入了左邊的右邊,誰高 那麼就是插入了那一邊,分別對應2種情況.左旋和 右左旋 ,所以 右邊的 代碼 同理 當 balance =等於-2 時 就去判斷他的右節點的右節點的高度 和右節點的左節點的高度. 然後相應 的是 右旋 和 右左旋的情況.

//遞歸 傳遞node
func adjust(root *node) *node{
//如果爲 nil返回
	if root == nil{
		return nil
	}
	//左邊節點的高度
	root.left = getHeight(root.left)
	//右邊節點的高度
	root.right = getHeight(root.right)

	var left int
	var right int
	//如果左邊爲 nil
	if root.left == nil{
	//高度爲 -1
		left = -1
	}else{
	//否則高度爲 左子節點返回的高度
		left = root.left.height
	}
	//如果右邊爲 nil 用來處理 返回爲 nil的情況
	if root.right == nil{
	//高度爲 -1
		right = -1
	}else{
	//否則高度爲 右子節點返回的高度
		right =root.right.height
	}
	balance := left - right
	//左子樹 的高度 比右子樹的 高
	if balance == 2{
			//這裏主要比較 哪裏插入了新節點  判斷nil 是因爲 nil節點高度是獲取不到的 只能這樣判斷
			if root.left.right == nil ||( root.left.left != nil  && root.left.left.height > root.left.right.height) {
				root = root.RightRotate()
			}else{
			//如果 左邊的右邊 發生了插入 先進行一次 左旋再 再進行一次右旋
				root = root.LRRoate()
			}

	}else if balance == -2{
	//這裏主要比較 那裏插入了新節點
		if root.right.left == nil || ( root.right.right != nil && root.right.right.height > root.right.left.height){
				root = root.LeftRotate()
			}else{
			//如果 右邊的左邊 發生了插入 先進行一次 右旋 再進行一次左旋
				root = root.RLRoate()
		}
	}
	//計算 葉子結點返回給到 我的高度 然後通過 max{root.right,root.left} + 1 計算出本節點的高度
	root.height = Max(left,right) + 1
	//返回本節點的高度 告訴給上一層
	return root
}

二叉樹的刪除

刪除一個樹的節點分爲好幾種情況,除了左右都不爲nil 的情況其他的都很簡單 直接返回 它的子節點。
在這裏插入圖片描述

如果刪除的節點左右都有子節點 ,那 就需要找一個和被刪除節點擁有一樣性質的節點去替換 被刪除節點。
在這裏插入圖片描述

所以 如果被刪除的節點左邊 和 右邊都不爲 nil 那麼 ,我們 需要特殊處理
包括兩步:

  1. 找到 被刪除節點右邊的最小節點。

首先我們 寫一個 查找最小節點的方法,遞歸往左就能找到最小的節點.

//找到最小節點的上一個節點
func findMin(avlnode *node) *node{
	if avlnode.left == nil{
		return nil
	}
	min := findMin(avlnode.left)
	if min == nil{
		return avlnode
	}
	return avlnode
}

然後 我們實現下 刪除功能:

func delelte(avlnode *node,key int) *node{
	if avlnode == nil{
		return nil
	}
	if  avlnode.Key > key{
		avlnode.left =delelte(avlnode.left,key)
	}else if avlnode.Key < key{
		avlnode.right = delelte(avlnode.right,key)
	}else{
		if avlnode.left != nil && avlnode.right != nil {
			//如果 右邊節點已進是葉子節點 見如下圖
			if avlnode.right.left == nil {
				//替換成 右邊的節點的Key
				avlnode.Key = avlnode.right.Key
				//替換成 右邊的節點的右子樹
				avlnode.right = avlnode.right.right
				//上面2 步 後 當前節點就變成了右節點
			} else {
				//找到 最小節點的上面一個節點
				nodep := findMin(avlnode.right)
				//最小的一個節點
				minnode := nodep.left
				//處理左邊最小節點 可能擁有的右節點
				nodep.left = nodep.left.right
				//替換 成最小一個節點的值
				avlnode.Key = minnode.Key
			}
			//一遍 爲 nil 直接返回 另一邊
		}else if avlnode.left == nil{
			avlnode = avlnode.right
		}else if avlnode.right == nil{
			avlnode = avlnode.left
		}
	}
	return avlnode
}

需要 注意的 是 我們只要 獲取到 最小節點 的上一個節點這樣我們就可以很方便的刪除它了,並且 我們需要特殊處理一種情況。
在這裏插入圖片描述

爲了測試 我們需要寫一箇中序遍歷 在 進行插入後查看是否還是有序的輸出,代碼直接放在下面了。

在這裏插入圖片描述

全部代碼:

package main

import "fmt"

type avltree struct {
	//定義根節點
	root *node
	//包含節點的數量
	size int
}
//定義一顆二叉樹
func NewAvTree() *avltree{
	rt := NewAvlNode()
	return &avltree{
		root:rt,
	}
}
type node struct {
	Key int //值
	left *node //左子樹
	right *node //右子樹
	height int //當前樹的高度 = max{left.height,right.height}
}

func NewAvlNode() *node{
	return &node{
		Key: 0,
		left:  nil,
		right: nil,
		height:0,
	}
}
//插入key
func (avlt *avltree) Insert(key int){
	//第一次插入 將key 插入root
	if avlt.size == 0{
		avlt.root.Key = key
	}else{
		avlt.root = insert(avlt.root,key)
	}
	avlt.size ++
	avlt.root = Adjust(avlt.root)
	//avlt.root = adjust(avlt.root)
}

func insert(root *node,key int) *node{
	//遞歸的 結束條件 當葉子節點時 插入節點
	if root == nil{
		//在葉子節點下面建立新節點
		nd := NewAvlNode()
		nd.Key = key
		//把葉子結點 返回給上一個節點
		return nd
	}
	//往右邊 遍歷
	if key > root.Key{
		root.right = insert(root.right,key)
	//往左邊 遍歷
	}else if key < root.Key{
		root.left = insert(root.left,key)
	}
	//返回節點
	return root
}

func Adjust(root *node) *node{
	if root == nil{
		return nil
	}
	//左邊節點的高度
	root.left = Adjust(root.left)
	//右邊節點的高度
	root.right = Adjust(root.right)

	var left int
	var right int
	if root.left == nil{
		left = -1
	}else{
		left = root.left.height
	}
	if root.right == nil{
		right = -1
	}else{
		right =root.right.height
	}
	balance := left - right
	//左子樹 的高度 比右子樹的 高
	if balance == 2{
			if root.left.right == nil ||( root.left.left != nil  && root.left.left.height > root.left.right.height) {
				root = root.RightRotate()
			}else{
				root = root.LRRoate()
			}

	}else if balance == -2{
		if root.right.left == nil || ( root.right.right != nil && root.right.right.height > root.right.left.height){
				root = root.LeftRotate()
			}else{
				root = root.RLRoate()
		}
	}
	//計算 葉子結點返回給到 我的高度 然後通過 max{root.right,root.left} + 1 計算出本節點的高度
	root.height = Max(left,right) + 1
	//返回本節點的高度 告訴給上一層
	return root
}


func (avlnode *node) RightRotate() *node{
	tmp :=avlnode.left
	avlnode.left = tmp.right
	tmp.right = avlnode
	return tmp
}
func (avlnode *node) LeftRotate() *node{
	tmp := avlnode.right
	avlnode.right = tmp.left
	tmp.left = avlnode
	return tmp
}

func (avlnode *node) LRRoate() *node{
	avlnode.left = avlnode.left.LeftRotate()
	return avlnode.RightRotate()
}

func (avlnode *node) RLRoate() *node{
	avlnode.right = avlnode.right.RightRotate()
	return avlnode.LeftRotate()
}
//func adjust(root *node) *node{
//	if root == nil{
//		return nil
//	}
//	lnode :=  adjust(root.left)
//	rnode :=  adjust(root.right)
//	var balancefactor int
//	//左右都爲空 爲葉子節點
//	if lnode == nil && rnode == nil{
//		return root
//	}
//	if lnode == nil{
//		//如果左邊爲空 計算右邊節點的高度
//		 balancefactor =  0 - rnode.height
//	}else if rnode == nil{
//		 balancefactor = lnode.height - 0
//	}
//	fmt.Println("xxx",balancefactor)
//
//	//如果平衡因子 = +-2 了,進行調整
//	//等於 2 的話 樹 向右邊傾斜 需要右旋 -2向右邊傾斜 需要 左旋
//	if balancefactor == 2{
//		root = root.RightRotate()
//	}else if balancefactor == -2{
//		root =root.LeftRotate()
//	}
//	return root
//}

//找到最小節點的上一個節點
func findMin(avlnode *node) *node{
	if avlnode.left == nil{
		return nil
	}
	min := findMin(avlnode.left)
	if min == nil{
		return avlnode
	}
	return avlnode
}
func delelte(avlnode *node,key int) *node{
	if avlnode == nil{
		return nil
	}
	if  avlnode.Key > key{
		avlnode.left =delelte(avlnode.left,key)
	}else if avlnode.Key < key{
		avlnode.right = delelte(avlnode.right,key)
	}else{
		if avlnode.left != nil && avlnode.right != nil {
			//如果 右邊節點已進是頁子節點
			if avlnode.right.left == nil {
				//替換成 右邊的節點的Key
				avlnode.Key = avlnode.right.Key
				//替換成 右邊的節點的右子樹
				avlnode.right = avlnode.right.right
				//上面2 步 後 當前節點就變成了右節點
			} else {
				//找到 最小節點的上面一個節點
				nodep := findMin(avlnode.right)
				//最小的一個節點
				minnode := nodep.left
				//處理左邊最小節點 可能擁有的右節點
				nodep.left = nodep.left.right
				//替換 成最小一個節點的值
				avlnode.Key = minnode.Key
			}
			//一遍 爲 nil 直接返回 另一邊
		}else if avlnode.left == nil{
			avlnode = avlnode.right
		}else if avlnode.right == nil{
			avlnode = avlnode.left
		}
	}
	return avlnode
}

//中序遍歷
func (avlt *avltree) Delete(key int){
	avlt.root = delelte(avlt.root,key)
	avlt.root = Adjust(avlt.root)
}
//中序遍歷
func (avlt *avltree) InorderTraversal(){
	inordertraversal(avlt.root)

}
func inordertraversal(root *node){
	if root == nil{
		return
	}
	inordertraversal(root.left)
	fmt.Println(root.Key)
	inordertraversal(root.right)
}

func (avlt *avltree) Find(key int) *node{
	return find(avlt.root,key)
}
func find(avlnode *node,key int) *node{
	if avlnode == nil{
		return nil
	}
	if avlnode.Key > key{
		ret := find(avlnode.left,key)
		return ret
	}else if avlnode.Key < key{
		ret :=find(avlnode.right,key)
		return ret
	}else{
		return avlnode
	}
}
func main(){
	a := NewAvTree()

	a.Insert(50)

	a.Insert(66)
	a.Insert(40)
	a.Insert(21)
	a.Insert(60)
	a.Insert(86)
	a.Insert(55)
	a.Insert(12)
	a.Insert(70)
	a.Insert(31)
	a.Insert(80)
	fmt.Println(a.Find(12))
	//a.InorderTraversal()
	a.Delete(12)
	fmt.Println(a.Find(12))
	a.InorderTraversal()


}
/**
 * Created by @CaomaoBoy on 2020/3/24.
 *  email:<[email protected]>
 */

總結:上面爲了 方便說明學習,只是用int 實現 比較,實際上 可以傳入任何類型的Key值 ,只要相應的提供一個比較函數即可。AVL樹由於是嚴格平衡的所以在查詢效率上 比紅黑樹要高但是每一次插入 都要自動平衡 修改 也要調整 節點再 次 平衡所以在修改上 AVL樹性能不如紅黑樹.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章