二叉查找樹 BST : https://blog.csdn.net/cj_286/article/details/90183298
二叉平衡樹 AVL : https://blog.csdn.net/cj_286/article/details/90217072
紅黑樹 RBT : https://blog.csdn.net/cj_286/article/details/90245150
爲什麼需要AVL樹
BST與TreeMap的效率對比
1.隨機序列的存取 (他們的存取速度差不多)
2.升序或降序序列的存取 (兩萬的數據量TreeMap的存取(先存後取)速度是BST的四百倍左右)
BST TreeMap
隨機序列 OK OK
升序或降序序列 Slow OK
爲什麼BST在極端情況下存取速度會如此的慢呢,因爲在極端情況下,BST會退化爲鏈表(升序或降序),時間複雜度會由原來的O(logN)退化爲O(N),所以查詢速度會變慢
4 1 7
/ \ \ /
2 6 O(logN)--> 2 O(N)--> 6 O(N)
/ \ / \ \ /
1 3 5 7 3 5
\ /
4 4
\ /
5 3
\ /
6 2
\ /
7 1
BST隨機存儲 BST升序存儲 BST降序存儲
在升序或降序的情況下BST明顯是滿足不了需求的,那麼有沒有哪種數據結構,對於任何插入節點或者刪除節點的操作都能自動的保持樹的平衡,這時AVL樹就誕生了,AVL樹它是一種自平衡的樹。
性質
以下AVL的代碼是基於BST代碼的,只是添加了使其平衡的代碼
在計算機科學中,AVL樹是最先發明的自平衡二叉查找樹。在AVL樹中任何節點的兩個子樹的高度最大差別爲1,所以它也被稱爲高度平衡樹。增加和刪除可能需要通過一次或多次樹旋轉來重新平衡這個樹。AVL樹得名於它的發明者G. M. Adelson-Velsky和E. M. Landis
AVL樹本質上還是一棵二叉搜索樹,它的特點是:
1.本身首先是一棵二叉搜索樹。
2.帶有平衡條件:每個結點的左右子樹的高度之差的絕對值(平衡因子Balance Factor)最多爲1。
3.空樹、左右子樹都是AVL
也就是說,AVL樹,本質上是帶了平衡功能的二叉查找樹(二叉排序樹,二叉搜索樹)。
對比AVL樹和非AVL樹
由圖可知,一棵AVL樹不一定是完全二叉樹,AVL樹它的每個子節點的平衡因子的絕對值都是小於等於1的,它的每個子節點都是一個AVL樹
非AVL樹轉爲AVL樹
爲了簡化操作,只考慮三個節點的情況
三個節點單旋轉
以3爲根節點順時針旋轉,旋轉之後,原來的根節點3變成了原來的左子樹2的右子樹,原來的左子樹2變成了根節點,這時二叉樹就恢復平衡變成AVL樹
三個節點雙旋轉
首先先處理節點1,將節點1進行左旋轉,原來的右子樹2變成了新的根節點,原來的1變成了2的左子樹,這時的情況和上面的單旋轉情況一樣了,以3節點右旋就變成了一個AVL樹了
JDK TreeMap右旋源碼解析
紅色節點是相對位置發生了改變,l原本是左子樹,右旋過後,l代替了p,p變成了l的右子樹,原本l.right是l的右孩子,右旋之後,l.right變成了p的左孩子。
private void rotateRight(Entry<K,V> p) {
if (p != null) {
Entry<K,V> l = p.left; //取得p的左孩子l
p.left = l.right; //l的右孩子l.right變成p的左孩子
if (l.right != null) l.right.parent = p; //l.right的父節點設置爲p
l.parent = p.parent; //l的父節點設置爲p的父節點p.parent
if (p.parent == null)
root = l;
else if (p.parent.right == p)
p.parent.right = l; //p.parent的左孩子或者右孩子設置爲l
else p.parent.left = l;
l.right = p; //l的右子樹設置爲p
p.parent = l;//p的父節點設置爲l
}
}
JDK TreeMap左旋源碼解析
紅色節點是相對位置發生了改變,r原本是右子樹,左旋過後,r替代了p,p變成了r的左孩子,原本的r.left是r的左孩子,左旋之後,r.left變成了p的右孩子
左旋和右旋完全對稱
private void rotateLeft(Entry<K,V> p) {
if (p != null) {
Entry<K,V> r = p.right;
p.right = r.left;
if (r.left != null)
r.left.parent = p;
r.parent = p.parent;
if (p.parent == null)
root = r;
else if (p.parent.left == p)
p.parent.left = r;
else
p.parent.right = r;
r.left = p;
p.parent = r;
}
}
什麼時候需要旋轉
1,插入關鍵字key後,結點p的平衡因子由原來 的1或者-1,變成了2或者-2,則需要旋轉:值考慮插入key到左子樹left的情況,即平衡因子是2
情況1:key < left.key,即插入到left的左子樹,需要進行單旋轉,將結點p右旋 (圖:avl-1-4-1)
情況2:key > left.key,即插入到left的右子樹,需要進行雙旋轉,先將left左旋,再將p右旋 (圖:avl-1-4-2 ,avl-1-4-3)
2,插入到右子樹right、平衡因子爲-2,完全對稱
平衡因子是2的情況示圖如下
情況1示圖
情況2示圖(1)
情況2示圖(2)
平衡因子是-2的情況和2的情況正好相反
插入
AVL的插入與BST完全相同,都是自頂向下的
檢測是否平衡並旋轉的調整過程:
1.AVL性質2決定了在檢測結點p是否平衡之前,必須先保證 左右子樹已經平衡
2.子問題必須成立 推導出 總問題是否成立,則說明是自底向上(這個很重要,自底向上,在遞歸或者循環去實現左旋或者右旋都是自底向上的去計算高度(height),這樣計算高度纔不會出錯,所以在代碼中計算高度只右+1而沒有-1,先增的葉子節點的高度都是固定的1)
3.有parent指針,直接向上回溯
4.無parent指針,後續遍歷框架,遞歸
5.無parent指針,棧實現非遞歸
實現 (以下AVL的代碼是基於BST代碼的,只是添加了使其平衡的代碼)
1.AVLEntry增加height屬性,表示樹的高度,平衡因子可以實時計算
2.單旋轉:右旋rotateRight、左旋rotateLeft
3.雙旋轉:先左後右firstLeftThenRight、先右後左firstRightThenLeft
4.實現非遞歸,需要輔助棧Stack,將插入時候所經過的路徑壓棧
5.插入調整函數fixAfterInsertion
6.輔助函數checkBalance,斷言AVL樹的平衡性,檢測算法的正確性
AVLMap中添加height屬性,表示樹的高度,添加獲取節點高度的方法
/**
* 返回一個結點的高度
* @param p
* @return
*/
public int getHeight(AVLEntry<K,V> p){
return p == null ? 0 : p.height;
}
旋轉調整
單旋轉,右旋代碼實現
/**
* 右旋(單旋轉)
* 該方法需要右返回值,因爲AVLEntry中沒有parent指針(JDK中是有parent指針的,所以不需要有返回值),旋轉之後它的新的根節點需要返回
* @param p
* @return
*/
private AVLEntry<K,V> rotateRight(AVLEntry<K,V> p) {
AVLEntry<K,V> left = p.left;
p.left = left.right;
left.right = p;
p.height = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
left.height = Math.max(getHeight(left.left),p.height) + 1;
return left;//新的根節點
}
單旋轉,左旋代碼實現(和右旋完全對稱)
和圖:avl-1-2-1完全對稱
/**
* 左旋(單旋轉)
* @param p
* @return
*/
private AVLEntry<K, V> rotateLeft(AVLEntry<K, V> p) {
AVLEntry<K,V> right = p.right;
p.right = right.left;
right.left = p;
p.height = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
right.height = Math.max(p.height,getHeight(right.right)) + 1;
return right;//新的根節點
}
雙旋轉,先左旋再右旋代碼實現
/**
* 先左旋再右旋
* 先將p.left進行左旋,再將p進行右旋
* @param p
* @return
*/
private AVLEntry<K,V> firstLeftThenRight(AVLEntry<K,V> p) {
p.left = rotateLeft(p.left);
p = rotateRight(p);
return p;
}
雙旋轉,先右旋再左旋代碼實現
和圖:avl-1-2-2完全對稱
/**
* 先右旋再左旋
* 先將p.right進行右旋,再將p進行左旋
* @param p
* @return
*/
private AVLEntry<K, V> firstRightThenLeft(AVLEntry<K, V> p) {
p.right = rotateRight(p.right);
p = rotateLeft(p);
return p;
}
旋轉代碼寫完,下面實現插入平衡的代碼,要實現插入調整樹平衡,需要引入棧Stack,使用棧可以實現插入調整的非遞歸算法
private LinkedList<AVLEntry<K,V>> stack = new LinkedList<>();//用於實現插入調整的非遞歸算法
插入調整函數實現
插入的時候需要將其走的所有路徑不斷壓棧
public V put(K key,V value) {
if (root == null) {
root = new AVLEntry<K,V>(key,value);
stack.push(root);//需要將put走的路徑全部壓棧,爲了fixAfterInsertion實現平衡
size ++;
}else{
AVLEntry<K,V> p = root;
while (p != null) {
stack.push(p);//需要將put走的路徑全部壓棧,爲了fixAfterInsertion實現平衡
int cmp = compare(key,p.key);
if (cmp < 0) {
if (p.left == null) {
p.left = new AVLEntry<K,V>(key,value);
stack.push(p.left);//需要將put走的路徑全部壓棧,爲了fixAfterInsertion實現平衡
size ++;
break;
}else{
p = p.left;//再次循環比較
}
} else if (cmp > 0) {
if (p.right == null) {
p.right = new AVLEntry<K,V>(key,value);
stack.push(p.right);//需要將put走的路徑全部壓棧,爲了fixAfterInsertion實現平衡
size ++;
break;
}else{
p = p.right;
}
}else{
p.setValue(value);//替換舊值
break;
}
}
}
fixAfterInsertion(key);
//不管是插入的是新值還是重複值,都返回插入的值,這個和JDK TreeMap不一樣
return value;
}
/**
* 插入調整,使其二叉搜索樹達到平衡
* @param key
*/
private void fixAfterInsertion(K key){
AVLEntry<K,V> p = root;
while (!stack.isEmpty()) {
p = stack.pop();//插入所走的路徑不斷彈棧
p.height = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
int d = getHeight(p.left) - getHeight(p.right);//計算平衡因子
if (Math.abs(d) <= 1) { //改樹平衡無需調整(旋轉)
continue;
}else{
if (d == 2) {
if (compare(key, p.left.key) < 0) { //插入到了左子樹的左子樹
p = rotateRight(p);//單旋轉:右旋rotateRight
}else{//插入到了左子樹的右子樹
p = firstLeftThenRight(p); //雙旋轉:先左後右firstLeftThenRight
}
}else{ //d == -2
if (compare(key, p.right.key) > 0) { //插入到了右子樹的右子樹
p = rotateLeft(p);//單旋轉:左旋rotateLeft
}else{//插入到了右子樹的左子樹
p = firstRightThenLeft(p);//雙旋轉:先右後左firstRightThenLeft
}
}
//旋轉過後,需要判斷走的是左子樹還是右子樹,也就是檢測爺爺結點,也就是p.parent要設置左子樹還是右子樹
if (!stack.isEmpty()) {
if (compare(key, stack.peek().key) < 0) { //表明插入到了左子樹
stack.peek().left = p;
}else{
stack.peek().right = p;
}
}
}
}
root = p;//重新設置根節點
}
插入調整插入調整優化
/**
* 插入調整,使其二叉搜索樹達到平衡
* @param key
*/
private void fixAfterInsertion(K key){
AVLEntry<K,V> p = root;
while (!stack.isEmpty()) {
p = stack.pop();//插入所走的路徑不斷彈棧
//優化
//**************************************************************
int newHeight = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
if (p.height > 1 /*保證p不是葉子節點*/ && newHeight == p.height/*高度沒有改變*/) {
stack.clear();
return;
}
//**************************************************************
p.height = newHeight;//Math.max(getHeight(p.left),getHeight(p.right)) + 1;
int d = getHeight(p.left) - getHeight(p.right);//計算平衡因子
if (Math.abs(d) <= 1) { //改樹平衡無需調整(旋轉)
continue;
}else{
if (d == 2) {
if (compare(key, p.left.key) < 0) { //插入到了左子樹的左子樹
p = rotateRight(p);//單旋轉:右旋rotateRight
}else{//插入到了左子樹的右子樹
p = firstLeftThenRight(p); //雙旋轉:先左後右firstLeftThenRight
}
}else{ //d == -2
if (compare(key, p.right.key) > 0) { //插入到了右子樹的右子樹
p = rotateLeft(p);//單旋轉:左旋rotateLeft
}else{//插入到了右子樹的左子樹
p = firstRightThenLeft(p);//雙旋轉:先右後左firstRightThenLeft
}
}
//旋轉過後,需要判斷走的是左子樹還是右子樹,也就是檢測爺爺結點,也就是p.parent要設置左子樹還是右子樹
if (!stack.isEmpty()) {
if (compare(key, stack.peek().key) < 0) { //表明插入到了左子樹
stack.peek().left = p;
}else{
stack.peek().right = p;
}
}
}
}
root = p;//重新設置根節點
}
AVL插入平衡算法改進與時間複雜度分析
1,彈棧的時候,一旦發現某個節點的高度未發生改變,則立即停止回溯
2,指針回溯次數,最壞情況O(logN),最好情況O(1),平均任然是O(logN)
3,旋轉次數,無旋轉O(0),單旋轉O(1),雙旋轉O(2),不會超過兩次,平均O(1) (AVL樹插入旋轉不會超過兩次)
4,時間複雜度:BST的插入O(logN) + 指針回溯O(logN) + 旋轉O(1) = O(logN)
5,空間複雜度:有parent爲O(1),無parent爲O(logN)
插入平衡練習
將給定的排序數組轉化爲平衡二叉樹,左右子樹高度差的絕對值不超過1
實現方式1:使用AVLMap中的put實現方式
時間複雜度O(NlogN),空間複雜度O(N)
/**
* 108(https://leetcode.com/problems/convert-sorted-array-to-binary-search-tree/)
* 給定排序數組,將它轉化爲平衡二叉樹
* 要求左右子樹高度差的絕對值不超過1(性質2)
*
* 實現方式1
* AVLMap的put實現方式
*
*/
public class ConvertSortedArrayToBinarySearchTree {
class LeetCodeAVL{
private int size;
private TreeNode root;
private LinkedList<TreeNode> stack = new LinkedList<>();
public LeetCodeAVL() {
}
public int size() {
return this.size;
}
public boolean isEmpty() {
return this.size == 0;
}
public void put(int key) {
if (root == null) {
root = new TreeNode(key);
stack.push(root);//需要將put走的路徑全部壓棧,爲了fixAfterInsertion實現平衡
size ++;
}else{
TreeNode p = root;
while (p != null) {
stack.push(p);//需要將put走的路徑全部壓棧,爲了fixAfterInsertion實現平衡
int cmp = key - p.val;
if (cmp < 0) {
if (p.left == null) {
p.left = new TreeNode(key);
size ++;
stack.push(p.left);//需要將put走的路徑全部壓棧,爲了fixAfterInsertion實現平衡
break;
}else{
p = p.left;//再次循環比較
}
} else if (cmp > 0) {
if (p.right == null) {
p.right = new TreeNode(key);
size ++;
stack.push(p.right);//需要將put走的路徑全部壓棧,爲了fixAfterInsertion實現平衡
break;
}else{
p = p.right;
}
}else{
break;
}
}
}
fixAfterInsertion(key);
}
private HashMap<TreeNode,Integer> heightMap = new HashMap<>();
/**
* 返回一個結點的高度
*/
public int getHeight(TreeNode p) {
return heightMap.containsKey(p) ? heightMap.get(p):0;
}
/**
* 右旋(單旋轉)
* 該方法需要右返回值,因爲AVLEntry中沒有parent指針(JDK中是有parent指針的,所以不需要有返回值),旋轉之後它的新的根節點需要返回
* @param p
* @return
*/
private TreeNode rotateRight(TreeNode p) {
TreeNode left = p.left;
p.left = left.right;
left.right = p;
heightMap.put(p,Math.max(getHeight(p.left),getHeight(p.right)) + 1);
heightMap.put(left,Math.max(getHeight(left.left),getHeight(p)) + 1);
return left;//新的根節點
}
/**
* 左旋(單旋轉)
* @param p
* @return
*/
private TreeNode rotateLeft(TreeNode p) {
TreeNode right = p.right;
p.right = right.left;
right.left = p;
heightMap.put(p,Math.max(getHeight(p.left),getHeight(p.right)) + 1);
heightMap.put(right,Math.max(getHeight(p),getHeight(right.right)) + 1);
return right;//新的根節點
}
/**
* 先左旋再右旋
* 先將p.left進行左旋,再將p進行右旋
* @param p
* @return
*/
private TreeNode firstLeftThenRight(TreeNode p) {
p.left = rotateLeft(p.left);
p = rotateRight(p);
return p;
}
/**
* 先右旋再左旋
* 先將p.right進行右旋,再將p進行左旋
* @param p
* @return
*/
private TreeNode firstRightThenLeft(TreeNode p) {
p.right = rotateRight(p.right);
p = rotateLeft(p);
return p;
}
/**
* 插入調整,使其二叉搜索樹達到平衡
* @param key
*/
private void fixAfterInsertion(int key){
TreeNode p = root;
while (!stack.isEmpty()) {
p = stack.pop();//插入所走的路徑不斷彈棧
int newHeight = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
if (heightMap.containsKey(p) && getHeight(p) > 1 /*保證p不是葉子節點*/ && newHeight == getHeight(p)/*高度沒有改變*/) {
stack.clear();
return;
}
heightMap.put(p,newHeight);//Math.max(getHeight(p.left),getHeight(p.right)) + 1;
int d = getHeight(p.left) - getHeight(p.right);//計算平衡因子
if (Math.abs(d) <= 1) { //改樹平衡無需調整(旋轉)
continue;
}else{
if (d == 2) {
if (key - p.left.val < 0) { //插入到了左子樹的左子樹
p = rotateRight(p);//單旋轉:右旋rotateRight
}else{//插入到了左子樹的右子樹
p = firstLeftThenRight(p); //雙旋轉:先左後右firstLeftThenRight
}
}else{ //d == -2
if (key - p.right.val > 0) { //插入到了右子樹的右子樹
p = rotateLeft(p);//單旋轉:左旋rotateLeft
}else{//插入到了右子樹的左子樹
p = firstRightThenLeft(p);//雙旋轉:先右後左firstRightThenLeft
}
}
//旋轉過後,需要判斷走的是左子樹還是右子樹,也就是檢測爺爺結點,也就是p.parent要設置左子樹還是右子樹
if (!stack.isEmpty()) {
if (key - stack.peek().val < 0) { //表明插入到了左子樹
stack.peek().left = p;
}else{
stack.peek().right = p;
}
}
}
}
root = p;//重新設置根節點
}
}
public TreeNode sortedArrayToBST(int[] nums){
if (nums == null || nums.length == 0) { //邊界檢測
return null;
}
LeetCodeAVL avl = new LeetCodeAVL();
for (int num : nums) {
avl.put(num);
}
return avl.root;
}
}
實現方式2:遞歸構建AVL + BST
參考TreeMap中的buildFromSorted
時間複雜度O(N),空間複雜度O(logN)
二分快排歸併的遞歸算法實現方式二
public class ConvertSortedArrayToBinarySearchTree {
/**
* 模仿TreeMap中的buildFromSorted
* 時間複雜度O(N)
* 空間複雜度O(logN)
* @param nums
* @return
*/
public TreeNode sortedArrayToBST(int[] nums){
if (nums == null || nums.length == 0) {
return null;
}
return buildFromSorted(0,nums.length - 1,nums);
}
private TreeNode buildFromSorted(int lo, int hi, int[] nums) {
if (hi < lo) {
return null;
}
int mid = (lo + hi) / 2;
TreeNode left = null;
if (lo < mid) {
left = buildFromSorted(lo, mid - 1, nums);
}
TreeNode middle = new TreeNode(nums[mid]);
if (left != null) {
middle.left = left;
}
if (mid < hi) {
TreeNode right = buildFromSorted(mid + 1, hi, nums);
middle.right = right;
}
return middle;
}
}
計算完整二叉樹的高度
JDK TreeMap源碼中的通過節點個數計算樹的層數,實現原理使用的是二分法
時間複雜度O(logN)
private static int computeRedLevel(int sz) {
int level = 0;
for (int m = sz - 1; m >= 0; m = m / 2 - 1)
level++;
return level;
}
刪除
AVL的刪除
AVL的刪除只需在BST的刪除基礎上加上刪除平衡即可
1,類似插入,假設刪除了p右子樹的某個結點,引起了p的平衡因子d[p]=2,分析p的左子樹left,三種情況如下:
情況1:left的平衡因子d[left]=1,將p右旋 (圖:avl-1-6-1)
情況2:left的平衡因子d[left]=0,將p右旋 (圖:avl-1-6-2)
情況3:left的平衡因子d[left]=-1,先左旋left,再右旋p (圖:avl-1-6-3)
2,刪除左子樹,即d[p]=-2的情況,與d[p]=2對稱
代碼實現
刪除節點後,調整該節點,使其整棵樹保持平衡
/**
* 刪除調整
* 1,類似插入,假設刪除了p右子樹的某個結點,引起了p的平衡因子d[p]=2,分析p的左子樹left,三種情況如下:
* 情況1:left的平衡因子d[left]=1,將p右旋
* 情況2:left的平衡因子d[left]=0,將p右旋
* 情況3:left的平衡因子d[left]=-1,先左旋left,再右旋p
* 2,刪除左子樹,即d[p]=-2的情況,與d[p]=2對稱
*
* 刪除算法是遞歸的,所以該方法是在遞歸中調用的
* @param p
* @return
*/
private AVLEntry<K, V> fixAfterDeletion(AVLEntry<K, V> p) {
if (p == null) return null;
else{
p.height = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
int d = getHeight(p.left) - getHeight(p.right);
if (d == 2) { //說明p.left一定不爲null
if (getHeight(p.left.left) - getHeight(p.left.right) >= 0) {
p = rotateRight(p);
}else{
p = firstLeftThenRight(p);
}
} else if (d == -2) {//說明p.right一定不爲null
if (getHeight(p.right.right) - getHeight(p.right.left) >= 0) {
p = rotateLeft(p);
}else{
p = firstRightThenLeft(p);
}
}
return p;
}
}
源碼:
https://github.com/xiaojinwei/java-learning/blob/master/src/com/cj/learn/tree/avl/AVLMap.java