1、概述
二叉查找樹是一種含有附加屬性的二叉樹,即其左孩子小於父結點,父結點小於或等於右孩子
(二叉查找樹的定義是二叉樹定義的擴展)
2、 用鏈表實現二叉查找樹html
addElement操做:
addElement方法根據給定元素的值,在樹中的恰當位置添加該元素,若是這個元素不是 comparable,則addElement方法會拋出NoComparableElemementException;
若是樹爲空,則這個新元素就將成爲根結點;
若是樹非空,這個新元素會與樹根元素進行比較:
若是它小於根結點中存儲的那個元素且根的左孩子爲null,則這個新元素就將成爲根的左孩子。
若是這個新元素小於根結點中存儲的那個元素且根的左孩子不是null,則會遍歷根的左孩子,並再次進行比較操做;
若是這個新元素大於或等於樹根存儲的那個元素且根的右孩子爲null,則這個新元素會成爲根的右孩子,
若是這個新元素大於或等於樹根處存儲的那個元素且根的右孩子不是null,則會遍歷根的右孩子,並再次進行比較操做
如圖:
java
private BinaryTreeNode<T> replacement(BinaryTreeNode<T> node) { BinaryTreeNode<T> result = null; if ((node.left == null) && (node.right == null)) { result = null; } else if ((node.left != null) && (node.right == null)) { result = node.left; } else if ((node.left == null) && (node.right != null)) { result = node.right; } else { BinaryTreeNode<T> current = node.right;// 初始化右側第一個結點 BinaryTreeNode<T> parent = node; // 獲取右邊子樹的最左邊的結點 while (current.left != null) { parent = current; current = current.left; } current.left = node.left; // 若是當前待查詢的結點 if (node.right != current) { parent.left = current.right;// 總體的樹結構移動就能夠了 current.right = node.right; } result = current; } return result; }
removeAllOccurrences操做
removeAllOccurrences方法負責從二叉查找樹中刪除指定元素的全部存在;
或者,當在樹中找不到指定元素時,則拋出 ElementNotFoundException異常;
若是指定的元素不是Comparable,則removeAllOccurrences方法也會拋出 ClassCastException異常,該方法會調用一次 removeElement方法,以此確保當樹中根本不存在指定元素時會拋出異常,
只要樹中還含有目標元素,就會再次調用 removeElement方法,注意, removeAllOccurrences方法使用了 LinkedBinaryTree類的 contans方法,還要注意,在 LinkedBinaryTree類中的find方法已經被重載了,以便利用二又查找樹的有序屬性node
3、用有序列表實現二叉查找樹git
4、平衡二叉查找樹算法
蛻化樹:看起來更像一個鏈表,但實際上它的效率比鏈表的還低,由於每一個結點還附帶額外的開銷
如圖(b)所示:
編程
右旋
要平衡化該樹,咱們須要:
使樹根的左孩子元素成爲新的根元素
使原根元素成爲這個新樹根的右孩子元素。
使樹根的左孩子的右孩子,成爲原樹根的新的左孩子
數據結構
左旋
要平衡化該樹,咱們須要:
使樹根的右孩子元素成爲新的根元素
使原根元素成爲這個新樹根的左孩子元素。
使原樹根右孩子結點的左孩子,成爲原樹根的新的右孩子。
性能
右左旋
並不是全部的不平衡問題均可以只進行某一種旋轉就能解決
對於由樹根右孩子的左子樹中較長路徑而致使的不平衡,
咱們必須先讓樹根右孩子的左孩子,繞着樹根的右孩子進行一次右旋,而後再讓所得的樹根右孩子繞着樹根進行一次左旋
學習
左右旋
對於由樹根左孩子的右子樹中較長路徑而致使的不平衡,
咱們必須先讓樹根左孩子的右孩子繞着樹根的左孩子進行一次左旋,而後再讓所得的樹根左孩子繞着樹根進行一次右旋
字體
5、實現二叉查找樹:AVL樹
6、實現二叉查找樹:紅黑樹
(1). 紅黑樹:一種平衡二叉查找樹,其中的每一個結點存儲一種顏色(紅或黑,用布爾值實現,false等價於紅色)
控制結點顏色的規則:
(2). 在某種程度上,紅黑樹的平衡限制沒有AVL樹那麼嚴格,可是,他們的序仍然是logn
(3). 紅黑樹中的元素插入
紅黑樹的插入操做相似於前面的addElement方法,可是這裏老是把插入的新元素顏色設置爲紅色,
插入新元素以後,必要時將從新平衡化該樹,根據須要改變元素的顏色以便維持紅黑樹的屬性
形式1:current == root (current是當前正在處理的結點) 咱們老是設置根結點顏色爲黑色,而全部路徑都包括樹根,所以不能違背各條路徑都擁有一樣數目黑色元素這一規則 形式2:current.parent.color == black(即當前結點的父結點顏色爲黑色) current所指向的結點老是一個紅色結點,這意味着,若是當前結點的父結點是黑色,則可知足全部規則,由於紅色結點並不影響路徑中的黑色結點數目; 另外因爲是從插入點處上溯處理,所以早已平衡了當前結點下面的子樹
可能一:
若是是左孩子,利用 current.parent.parent.left.color 獲得顏色信息(null元素的顏色爲黑色),且存在兩種狀況:
父結點的兄弟爲紅色或黑色:這兩種狀況下,咱們闡述的處理步驟都將發生在一個循環內部(該循環的終止條件如前所述)
若是父結點的兄弟爲紅色,這時的處理步驟以下:
設置current的父親的顏色爲 black 設置父結點的兄弟的顏色爲black 設置 current的祖父的顏色爲red 設置 current指向current的祖父
若是父結點的兄弟爲黑色,首先要查看current是左孩子仍是右孩子:
若是current是右孩子,則必須設置current等於其父親,在繼續以前還要再向左旋轉current.right;
後面的步驟,與開始時current爲左孩子同樣
若是current是左孩子: 設置current的父親的顏色爲black 設置current的祖父的顏色爲red 若是current的祖父不等於null,則讓current的父親繞着current的祖父向右旋轉
可能二:
若是是右孩子,存在兩種狀況:
父結點的兄弟爲紅色或黑色:這兩種狀況下,咱們闡述的處理步驟都將發生在一個循環內部(該循環的終止條件如前所述)
若是父結點的兄弟爲紅色,這時的處理步驟以下(同當前結點的父結點的兄弟顏色爲紅時):
設置current的父親的顏色爲 black 設置父結點的兄弟的顏色爲black 設置 current的祖父的顏色爲red 設置 current指向current的祖父
若是父結點的兄弟爲黑色,首先要查看current是左孩子仍是右孩子(與當前結點的父結點的兄弟顏色爲黑時,操做對稱):
若是current是左孩子,則必須設置current等於其父親,在繼續以前還要再向右旋轉current.left;
後面的步驟,與開始時current爲右孩子同樣
若是current是右孩子: 設置current的父親的顏色爲black 設置current的祖父的顏色爲red 若是current的祖父不等於null,則讓current的父親繞着current的祖父向左旋轉
(4). 紅黑樹中的元素刪除
與元素插入的那些狀況同樣,刪除的兩種狀況也是對稱的——取決於 current是左孩子仍是右孩子。
當 current爲右孩子時:
(在插入時,咱們最關注的是當前結點的父親的兄弟的顏色)
而對刪除而言,焦點要放在當前結點的兄弟的顏色上(用 current.parent. left.color來指代這種顏色):
還要觀察該兄弟的孩子的顏色,要注意的重要一點是:顏色的默認值是black;
這樣,任什麼時候刻若是試圖取得mull對象的顏色,結果都將是black
其餘的狀況很容易推導出來,只要把上述狀況中的「左」換成「右」、「右」換成「左」便可
若是兄弟的顏色是red,則在作其餘事以前必須完成以下處理步驟:
* 設置兄弟的顏色爲black * 設置current的父親的顏色爲red * 讓兄弟繞着 current的父親向右旋轉 * 設置兄弟等於 current的父親的左孩子
下面再繼續處理過程:無論這個初始兄弟是red仍是 black,這裏的處理會根據兄弟的孩子的顏色分紅兩種狀況:
若是兄弟的兩個孩子都是black(或null),則須要
* 設置兄弟的顏色爲red * 設置 current等於 current的父親
若是兄弟的兩個孩子不全爲black,則將查看兄弟的左孩子是不是black,若是是,則在繼續以前必須完成以下步驟:
* 設置兄弟的右孩子的顏色爲 black * 設置兄弟的顏色爲red * 讓兄弟的右孩子繞着兄弟自己向右旋轉 * 設置兄弟等於cumt的父親的左孩子
最後是兄弟的兩個孩子都不爲 black這一狀況,這時必須:
* 設置兄弟的顏色爲 current的父親的顏色。 * 設置 current的父親的顏色爲black * 設置兄弟的左孩子的顏色爲black * 讓兄弟繞着 current的父親向右旋轉 * 設置 current等於樹根
該循環終止以後,咱們要酬除該結點,並設置其父親的孩子引用爲mull
如今所明白的是紅黑樹是爲了讓二叉樹路徑不會過長而致使查找等操做的效率太低,經過紅黑結點的限制,使得紅黑樹的最大長度約爲2logn
可是對於紅黑樹的設計思想來源不是很明白,爲何要這樣設計,以及這樣設計真的能達到效果,真的是對的嘛?
像AVL樹那樣,每次插入刪除都要進行旋轉使得樹平衡,這不就夠了嗎?爲何還要再弄一個紅黑樹?
百度了相關的資料,而後總結匯總在這裏:
3-節點則是擴充版,包含2個元素和三條連接:兩個元素A、B,左邊的連接指向小於A的節點,中間的連接指向介於A、B值之間的節點,右邊的連接指向大於B的節點。
在這兩種節點的配合下,2-3樹能夠保證在插入值過程當中,任意葉子節點到根節點的距離都是相同的。徹底實現了矮胖矮胖的目標。怎麼配合的呢,下面來看2-3樹的構造過程。
所謂構造,就是從零開始一個節點一個節點的插入。
在二叉查找樹中,插入過程從根節點開始比較,小於節點值往右繼續與左子節點比,大於則繼續與右子節點比,直到某節點左或右子節點爲空,把值插入進去。這樣沒法避免偏向問題。在2-3樹中,插入的過程是這樣的。
若是將值插入一個2-節點,則將2-節點擴充爲一個3-節點。
若是將值插入一個3-節點,分爲如下幾種狀況。
(1).3-節點沒有父節點,即整棵樹就只有它一個三節點。此時,將3-節點擴充爲一個4-節點,即包含三個元素的節點,而後將其分解,變成一棵二叉樹。
此時二叉樹依然保持平衡。
(2).3-節點有一個2-節點的父節點,此時的操做是,3-節點擴充爲4-節點,而後分解4-節點,而後將分解後的新樹的父節點融入到2-節點的父節點中去。
(3).3-節點有一個3-節點的父節點,此時操做是:3-節點擴充爲4-節點,而後分解4-節點,新樹父節點向上融合,上面的3-節點繼續擴充,融合,分解,新樹繼續向上融合,直到父節點爲2-節點爲止,若是向上到根節點都是3-節點,將根節點擴充爲4-節點,而後分解爲新樹,至此,整個樹增長一層,仍然保持平衡。
第三種狀況稍微複雜點,爲了便於直觀理解,如今咱們從零開始構建2-3樹,囊括上面全部的狀況,看完因此步驟後,你也能夠本身畫一畫。
咱們將{7,8,9,10,11,12}中的數值依次插入2-3樹,畫出它的過程:
因此,2-3樹的設計徹底能夠保證二叉樹保持矮矮胖胖的狀態,保持其性能良好。可是,將這種直白的表述寫成代碼實現起來並不方便,由於要處理的狀況太多。這樣須要維護兩種不一樣類型的節點,將連接和其餘信息從一個節點複製到另外一個節點,將節點從一種類型轉換爲另外一種類型等等。
所以,紅黑樹出現了,紅黑樹的背後邏輯就是2-3樹的邏輯,可是因爲用紅黑做爲標記這個小技巧,最後實現的代碼量並不大。(可是,要直接理解這些代碼是如何工做的以及背後的道理,就比較困難了。因此你必定要理解它的演化過程,才能真正的理解紅黑樹)
看看紅黑樹和2-3樹的關聯,首先,紅和黑的含義:紅黑樹中,全部的節點都是標準的2-節點,爲了體現出3-節點,這裏將3-節點的兩個元素用左斜紅色的連接鏈接起來,即鏈接了兩個2-節點來表示一個3-節點。這裏紅色節點標記就表明指向其的連接是紅連接,黑色標記的節點就是普通的節點。因此纔會有那樣一條定義,叫「從任一節點到其每一個葉子的全部簡單路徑都包含相同數目的黑色節點」,由於紅色節點是能夠與其父節點合併爲一個3-節點的,紅黑樹實現的實際上是一個完美的黑色平衡,若是你將紅黑樹中全部的紅色連接放平,那麼它全部的葉子節點到根節點的距離都是相同的。因此它並非一個嚴格的平衡二叉樹,可是它的綜合性能已經很優秀了。
【參考資料】
清晰理解紅黑樹的演變---紅黑的含義
二叉查找樹是有特定屬性的,即一個結點的左孩子小於當前結點,當前結點又小於等於其右孩子的
因此一棵樹,最大值必定在樹的最右邊,最小值在樹的最左邊,在此基礎上能夠分析代碼:
public T removeMax() { T result = null; if (isEmpty()) throw new EmptyCollectionException("LinkedBinarySearchTree");//判斷樹是否爲空,空則拋出異常 else {//不空的話,則判斷樹根的右孩子是否爲空 if (root.right == null) {//根的右孩子爲空,則最大值即爲根元素,而後執行刪除根元素操做 result = root.element; root = root.left;//讓根的左孩子等於根,即完成刪除操做 } else {//根的右孩子不爲空,則執行遞歸操做,一直找到整棵樹最右邊的元素(即最大元素) BinaryTreeNode<T> parent = root; BinaryTreeNode<T> current = root.right; while (current.right != null) { parent = current; current = current.right; } result = current.element;//找到最大元素current,賦給result parent.right = current.left;//把要刪除的元素的左孩子賦給要刪除元素的雙親的右孩子,即完成刪除當前元素的操做 } modCount--; } return result; }
這個刪除最大值的代碼與找最大值最小值的操做findMin,findMax差很少,都是同樣的思路,只是找到以後,不會進行刪除操做而已
以下是找最大值的代碼實現,最小值的實現與之對稱:
public T findMax() { T result = null; if (isEmpty()) throw new EmptyCollectionException("LinkedBinarySearchTree"); else { if (root.right == null) { result = root.element; } else { BinaryTreeNode<T> parent = root; BinaryTreeNode<T> current = root.right; while (current.right != null) { parent = current; current = current.right; } result = current.element; } } return result; }
最後的結果運行如圖:
首先要理解什麼是AVL樹,什麼樣的樹算AVL樹
樹都是用來存儲數據的,普通的二叉樹能夠按照要求在任一能夠放置孩子的結點處放置結點
而AVL樹對放置結點的位置有要求,即放置完後會對結點進行調整,左旋、右旋,使樹達到平衡
而且致使二叉樹失衡的可能只有兩種操做:插入、刪除
因此設計的關鍵在於:如何經過旋轉,使得失衡點處從新平衡
根據書上的介紹,旋轉一共有四種狀況:左旋,右旋,左右旋,右左旋,以知足對失衡點處的失衡狀況的可能性分析
即四種旋轉狀況對應四種可能的失衡狀況:
假設結點X爲失衡點:(原先的二叉樹代碼中沒有刪除操做,這裏也只討論插入帶來的失衡問題)
① 在結點X的左孩子結點的左子樹中插入元素
② 在結點X的左孩子結點的右子樹中插入元素
③ 在結點X的右孩子結點的左子樹中插入元素
④ 在結點X的右孩子結點的右子樹中插入元素
參照上面課本知識梳理中四種旋轉的平衡化技術,對應這四種狀況:第①狀況和第④狀況是對稱的,能夠經過單旋轉來解決,而第②種狀況和第③狀況是對稱的,須要雙旋轉來解決
左單旋代碼實現:
private AVLNode<T> singleRotateLeft(AVLNode<T> x){ //把w結點旋轉爲根結點 AVLNode<T> w= x.left; //同時w的右子樹變爲x的左子樹 x.left=w.right; //x變爲w的右子樹 w.right=x; //從新計算x/w的高度 x.height=Math.max(height(x.left),height(x.right))+1; w.height=Math.max(height(w.left),x.height)+1; return w;//返回新的根結點 }
右單旋代碼實現:
private AVLNode<T> singleRotateRight(AVLNode<T> w){ AVLNode<T> x=w.right; w.right=x.left; x.left=w; //從新計算x/w的高度 w.height=Math.max(height(w.left),height(w.right))+1; x.height=Math.max(height(x.left),w.height)+1; //返回新的根結點 return x; }
利用上面寫好的代碼能夠直接用於雙旋的狀況:
左右旋代碼實現:
private AVLNode<T> doubleRotateWithLeft(AVLNode<T> x){ //w先進行RR旋轉 x.left=singleRotateRight(x.left); //再進行x的LL旋轉 return singleRotateLeft(x); }
右左旋代碼實現:
private AVLNode<T> doubleRotateWithRight(AVLNode<T> x){ //先進行LL旋轉 x.right=singleRotateLeft(x.right); //再進行RR旋轉 return singleRotateRight(x); }
平衡化技術的代碼實現以後,再實現插入操做便可,可是要分兩步(由於是AVL樹嘛):
首先由於是二叉查找樹嘛,確定要對要插入的元素找到合適的位置嘛。。。(這裏跟二叉樹同樣能夠用遞歸算法來找)
而後呢就是平衡判斷,判斷插入元素以後,樹是否平衡(只要評估子樹便可),不平衡則經過上述的四種旋轉來使樹從新平衡
插入操做的代碼以下:
public void insert(T data) { if (data==null){ throw new RuntimeException("data can\'t not be null "); } this.root=insert(data,root); } private AVLNode<T> insert(T data , AVLNode<T> p){ //說明已沒有孩子結點,能夠建立新結點插入了. if(p==null){ p=new AVLNode<T>(data); }else if(data.compareTo(p.data)<0){//向左子樹尋找插入位置 p.left=insert(data,p.left); //插入後計算子樹的高度,等於2則須要從新恢復平衡,因爲是左邊插入,左子樹的高度確定大於等於右子樹的高度 if(height(p.left)-height(p.right)==2){ //判斷data是插入點的左孩子仍是右孩子 if(data.compareTo(p.left.data)<0){ //進行LL旋轉 p=singleRotateLeft(p); }else { //進行左右旋轉 p=doubleRotateWithLeft(p); } } }else if (data.compareTo(p.data)>0){//向右子樹尋找插入位置 p.right=insert(data,p.right); if(height(p.right)-height(p.left)==2){ if (data.compareTo(p.right.data)<0){ //進行右左旋轉 p=doubleRotateWithRight(p); }else { p=singleRotateRight(p); } } } else ;//if exist do nothing //從新計算各個結點的高度 p.height = Math.max( height( p.left ), height( p.right ) ) + 1; return p;//返回根結點 }
【參考資料】
二叉樹與AVL樹
java數據結構與算法之平衡二叉樹(AVL樹)的設計與實現
錯題1:
錯題1解析:選擇排序經過重複地將下一個最小的元素放到最後排序的位置來對列表進行排序
錯題2:
錯題2解析:與錯題1重複
錯題3:
錯題3解析:在從二叉搜索樹中刪除元素時,必須提高另外一個節點來替換要刪除的節點。
錯題4:
錯題4解析:與錯題3重複
代碼行數(新增/累積) | 博客量(新增/累積) | 學習時間(新增/累積) | 重要成長 | |
---|---|---|---|---|
目標 | 5000行 | 30篇 | 400小時 | |
第一週 | 0/0 | 1/1 | 4/4 | |
第二週 | 560/560 | 1/2 | 6/10 | |
第三週 | 415/975 | 1/3 | 6/16 | |
第四周 | 1055/2030 | 1/4 | 14/30 | |
第五週 | 1051/3083 | 1/5 | 8/38 | |
第六週 | 785/3868 | 1/6 | 16/54 | |
第七週 | 733/4601 | 1/7 | 20/74 |