1. 程式人生 > >用js來實現那些資料結構14(樹02-AVL樹)

用js來實現那些資料結構14(樹02-AVL樹)

在使用二叉搜尋樹的時候會出現 一個問題,就是樹的一條分支會有很多層,而其他的分支卻只有幾層,就像下面這樣:

  如果資料量夠大,那麼我們在某條邊上進行增刪改查的操作時,就會消耗大量的時間。我們花費精力去構造一個可以提高效率的結構,反而事與願違。這不是我們想要的。所以,我們需要另外一種樹來解決這樣的問題,那就是自平衡二叉搜尋樹--Adelson-Velskii-Landi(AVL)。什麼意思呢?就是說這種樹的任何一個節點左右兩側子樹的高度之差最多為1。也就是說這種樹會在新增或刪除節點時儘量試著成為一棵完全樹。

  自平衡二叉搜尋樹和二叉搜尋樹的實現幾乎是一模一樣的,唯一的區別就在於每次在插入或者刪除節點的時候,我們需要檢測它的平衡因子

(因為只有再插入或者刪除的時候才有可能會影響到樹的平衡性)如果有需要,那麼就將其邏輯應用於樹的自平衡。

  首先我們需要知道這個平衡因子是如何計算的。平衡因子的計算是來自於每個節點的右子樹高度(hr)和左子樹高度(hl)的差值, 該值應為0,1,-1.如果不是這三個值,那麼說明需要平衡該AVL樹。這就是平衡因子的簡單計算方式。什麼意思呢?

   我們以上圖為例,根節點11的平衡因子6 - 3 = 3。左側子節點7的平衡因子是2 - 2 = 0;右側子節點18的平衡因子就是5 - 2 = 3;節點70的平衡因子是0,要記住所有的葉節點(外部節點)的平衡因子都是0。因為葉節點沒有子節點。還有一點一定要注意。我們所計算的平衡因子,是該節點的左右子樹的高度。

我們學會了如何去計算平衡因子,那麼我們下面進行一項及其重要的儀式......噢,sorry。是及其重要的知識——旋轉。

在開始講解旋轉之前,我們先來點開胃菜。看看我們插入子節點後,導致該樹不平衡的可能的情況有哪些。我會畫幾個圖,以便大家看得仔細透徹。

  

  首先,我們以上面這張圖(擷取前面樹結構的一部分)作為初始的樹,這棵樹絕對一定必然是平衡的。大家都沒意見吧。那麼RR,LL,RL,LR是什麼意思呢?那麼我們繼續往下看。

  第一種情況:RR。

    我們在18的右側子節點再加一個節點20,右側是要加入比父節點大的值的。

    在我們加入了一個節點20之後,我們發現這棵樹還是平衡的!唉?不對啊,跟我想要的結果好像不太一樣。我再加一個節點試試?

    嗯......現在絕壁不平衡了。那麼我們來看看怎麼回事。在加入節點21(19)後,11節點左側子樹的深度是1,而右側子樹的深度是1,2,3。是3沒錯。那麼1-3等於-2。嗯,十以內加減法應該不會算錯。我們確定在節點18後面加入了兩個右側(R)節點後,這棵樹就不平衡了。而現在有一個重要的問題。就是是哪一棵子樹導致這棵樹不平衡的呢?是在我們加入節點21(19)之後,也就是上圖我們用小圈圈詛咒它的那一部分。那麼我們可以用一句話來描述,我們在該樹右側子節點的右側子節點加入了一個右側子節點(如果加入的是左側子節點也是一樣的)之後,導致了該樹的不平衡,所以我們這時候需要去操作也就是旋轉右側的子節點也就是18節點,來使這顆自平衡樹來自平衡。換句話說,如果我們加入了一個節點(或者刪除了一個節點),導致了我們整顆樹的不平衡,那麼我們首先要找到最近的不平衡的樹來進行調整。

    上面RR情況的我分別加入了19,和21兩個位元組點,要說明一下,這兩個子節點是為了更為清晰的告訴大家在root的右節點的右節點下,無論插入的是左節點還是右節點都屬於RR的情況。下同。在具體旋轉的時候會給大家詳細介紹。

    換句話說,我們判斷在增刪節點的時候是否會導致不平衡的情況,由插入節點的前兩個父節點來確定!大家要注意噢!很重要!

趁熱打鐵,上面解釋了RR的情況,那麼其實下面的LL,LR,RL等情況也是一樣的。思路沒有任何區別。但是這個時候我想打斷大家一下。問大家兩個問題。這兩個問題的解決會為後面的學習帶來極大的便利。

    1、在AVL樹或者其他樹中,是否可以出現重複的值,比如樹中已經有了一個11,我還想再加入一個11,是否允許?是否可以?

    2、看上圖(RR情況圖),是否有可能出現除了這四種情況外的其他情況?或者說,節點的平衡因子是否可能出現大於2或者小於-2的情況?(這種情況我們要旋轉樹超過兩次,也就超出了我們這四種情況之外。)

    OK。希望大家閉上眼睛,想一想你的夢中情人,哦不對。想一想你的答案。

    不賣關子了,但是我真的希望大家想一想,因為這很必要也很重要。

    好吧,我開始回答第一個問題。其實在前一篇實現的樹中是不允許重複的值出現的,我們可以去看一下上一篇的程式碼,如果相等則會覆蓋。那麼可能有人會問,我想要這棵樹儲存重複的值(當然其實這種情況出現的話大多數都是你的設計有問題。。。沒有唯一標識了啊......需求還怎麼實現)。那麼我記得在hashMap篇中有一個解決衝突的辦法,是不是可以通過連結串列來儲存key值相同的對映呢?是否還可以使用其他的儲存方式?答案比較開放。所以是否可以存放重複的值,看你的實際需求咯。

    第二個問題的答案,不可能出現,因為大家一定要記住一個前提,就是我們在插入了一個導致該樹不平衡的節點前,該樹一定是平衡的。為什麼這麼說呢?因為我們的AVL樹,是自平衡二叉搜尋樹,如果在插入之前就是不平衡的,那你告訴我你這是啥?趕緊回頭看程式碼,有BUG了親。

    這裡希望大家已經解除了心中不少的疑惑,如果還有問題,大家可以繼續留言探討。

    那麼我們下面繼續,把其它幾種情況的圖示畫完。

  第二種情況:LL。

 

  第三種情況:LR。

 

  第四種情況:RL。

 

   那麼看完上面這幾幅圖想必大家都瞭解了在插入節點的時候影響到樹的平衡的4種可能性。那麼為了面對這4種可能性。我們給出了與之相對應的4種解決不平衡的方法(其實就兩種)。那麼這裡我們就要進入本篇最重要的內容了,旋轉。在開始之前,希望大家記住一句話。那就是,什麼情況導致的不平衡,那就用相反方向的旋轉。什麼意思呢,比如是LL導致的不平衡,那麼我們就向右旋轉。如果是RR導致的不平衡我們就向左,如果是RL,我們就LL再RR。如果是LR,我們就先RR再LL。好了,下面我們來看看究竟是如何旋轉的吧。

   那麼下面就有點意思了。希望大家可以仔細看。

一、RR情況的左旋轉

我們還得來看圖說話,我儘量把圖畫的讓人容易誤解,哦不,容易理解。

  本人那個,畫圖工具用得還不是太熟練,拐歪的曲線沒畫出來,我拿嘴說吧......

  大家看上圖,左旋是以18為軸心整個樹的左部分向左旋轉,這樣就使18變成了根節點,11變成了18的左側子節點。這樣旋轉一下,就相當於減少了一層右側字樹的一層深度,從而使整顆樹變成了平衡樹。那麼可能還有下面的這種情況,但其實是一樣的。

  那麼這種情況是要旋轉的軸心節點(18),還有左側子節點,在旋轉之後,18的左側子節點13就會變成11的右側子節點。其實可以簡單的認為是左旋過後被節點11給“”過來的。

其實,18的左側子節點在旋轉過後會成為11的右側子節點還有一個原因,就是,18左側子節點的值一定是大於11小於18的(旋轉之前的圖)。為什麼自己想。那麼在旋轉過後,它也一定是大於11的,所以它可以成為11的右側子節點。

一、LL情況的右旋轉

  那麼LL情況的右旋轉就沒什麼好說的了,跟RR情況是一樣的,我們直接上圖吧。

  這絕壁沒問題吧,原理都是一樣的。只不過換了一個方向而已。

  這樣沒啥好說的了,對吧。下面我們看看其他地情況。雙旋轉......

三、LR情況的左旋(RR)右旋(LL)

  我們還是直接上圖,然後再解釋,解釋完這個RL情況的又不用再囉嗦了。挺好......挺省事,嘿嘿。

  其實讓人有點懵逼的是名字,我特意加了個括號,希望你別懵逼。

  不知道大家看沒看懂,總感覺這圖不是很友好啊,還有8節點的小瑕疵就不要在意了,反正都是虛線......還有指向節點10的那條線是虛線.....不影響.....嘿嘿。

  解釋一下,我們需要雙旋轉的情況下,第一次旋轉的是紅框部分,也就是說,如果我們需要雙旋轉,兩次旋轉的軸心點是不一樣的,第一次旋轉的軸心是插入節點的父節點,而第二次旋轉的軸心是插入節點的祖父節點大家一定要注意。

  那麼這裡可能會有一個疑問,就是8節點在第一次旋轉過後,為什麼會成為7節點的右側子節點。這裡十分重要,直接關係到你是否理解了AVL樹的旋轉。

  我們先看第一次旋轉,如果插入的是8節點而不是10節點,那麼在第一次左旋的時候,節點7會成為節點9的左側子節點,而這個時候8節點是無處可去的,因為7佔了我的位置,這咋整,不能因為一次平衡就刪除我這個節點啊,節點8肯定不幹,不然你插入我幹啥.....哎?感覺有點不對勁.....額咳咳....咱們繼續吧....而節點8這個位置一定比9小比7大,所以我們在旋轉過後,讓它成為7節點的右子節點就可以了。希望我說明白了。

  那麼這個時候可能還存在7節點有左側子節點的情況,上面沒畫,沒關係啊,你是7節點的左側子節點,左旋轉過後你還在原來的位置,沒人佔你的位置,你就不用動了。嗯,就這樣.....完畢!

四、RL情況的右旋(LL)旋(RR)

其實這裡真沒啥好說的了,我一點都不解釋,大家自己看,看不懂你就從頭看!

  唉.......說了一大堆,終於可以到最後的程式碼了,上程式碼!

複製程式碼
//這是我們計算當前節點的高度的方法
    var heightNode = function (node) {
        // 如果沒有那就為-1
        if(node === null) {
            return -1;
        } else {
            // 如果存在執行邏輯
            //那麼說一下這裡我的理解吧,Math.max比較左節點和右節點的大小,返回大的那個值,然後 + 1。
            //為什麼要返回大的那個值呢?因為如果左節點存在,那麼值為0(-1 + 1);並且右節點是不存在的,那麼右節點為-1。
            //但是此時我們是有高度的,所以我們要選取有高度的那個節點,也就是值大的那一個。
            //那為什麼要+1呢?因為高度只能為0不能為-1。-1是我們通過相減計算得到的,而不是計算高度得到的。記住這裡是計算高度。
            console.log(Math.max(heightNode(node.left),heightNode(node.right)) + 1)
            return Math.max(heightNode(node.left),heightNode(node.right)) + 1;
        }
    }

    //RR:向左的單旋轉
    var rotationRR = function (node) {
        var tmp = node.right;
        node.right = tmp.left;
        tmp.left = node;
        return tmp;
    }

    //LL:向右的單旋轉
    var rotationLL = function (node) {
        var tmp = node.left;
        node.left = tmp.right;
        tmp.right = node;
        return tmp;
    }

    //LR:向右得到雙旋轉
    var rotationLR = function (node) {
        node.left = rotationRR(node.left);
        return www.taohuayuan178.com rotationLL(node);
    }

    //RL:向左的雙旋轉
    var rotationRL = function www.taohuayuan178.com (node) {
        node.right = rorarionLL(www.vboyule.cn  node.right);
        return rorarionRR(node)www.yisengyuLe.com;
    }

    var balanceInsertNode = function (node,element) {
        // 如果node的位置沒有值,那麼直接加入就好了。
        if(node === null) {
            node = newNode(element);
        // 如果假如的值是小於當前節點的話,說明我們要加在當前節點的左側。
        } else if(element < node.key) {
            node.left = insertNode(node.left,element);
            // 那麼下面就要判斷是否是null,如果是null,那麼沒問題,直接加上就好了。
            if(node.left !== null) {
                // 如果不是,我們就要計算node的左側高度減去右側高度是否大於1,如果是,說明不平衡,需要來呼叫平衡方法來平衡。
                if((heightNode(node.left) - heightNode(node.right)) > 1) {
                    // 如果當前插入的節點的值小於node.left的值,說明是LL的情況,我們需要右旋。否則的話我們就需要先左旋,再右旋。
                    if(element < www.boshenyl.cn   node.left.key) {
                        node = rorarionLL(node);
                    } else {
                        node = www.thd178.com/   rorarionLR(node);
                    }
                }
            }
        } else if(element > node.key) {
            node.right = insertNode(node.right,element);

            if(node.right !== null) {
                if((heightNode(node.right) www.leyouzaixan.cn - heightNode(node.left)) > 1) {
                    if(element > node.right.key) {
                        node = rorarionRR(node);
                    } else {
                        node = rorarionRL(node);
                    }
} } } }
複製程式碼

   程式碼中多了一個balanceInsertNode方法,這個方法是需要替換我們前面寫好的insertNode方法的,這樣寫是為了讓大家更好的對比下。這些程式碼不像以前那樣,寫了一大堆的註釋用來解釋。其實要說的很多,都在前面的圖和語言描述中說過了。所以大家看這個程式碼的時候。有不明白的地方,對照著前面的邏輯一點一點看,肯定就看明白了。比如rotationLL和rotationRR內部的替換以及為什麼要這樣替換,都在前面說過了。所以就不再在程式碼中囉嗦了。

  這一篇文章有點長,也花了我一點心思才完成。很重要,如果你想要對樹有一個不錯的瞭解,這些必須要會。我儘可能的用我理解的思路給大家講解,如果有什麼不清楚的地方,大家可以留言討論。

  哦對了,本來還要跟大家說說其他樹的,但是想了想也沒什麼必要,給大家一個連結,大家可以自行去做一些簡單的瞭解,比如紅黑樹,堆積樹,還有B樹等等等等。種類很多。要想都講完大概幾十篇都不夠,希望這兩篇樹結構的文章可以拋磚引玉。讓大家提起對資料結構的興趣。

  大家可以看一下這個瞭解https://zh. www.baohuayule.com  wikipedia.org/wiki/AVL%E6%A0%91,滑動到頁底,你就能看到其他的樹結構了。

  好了,終於,自平衡二叉搜尋樹到這裡基本就結束了。下一部分會講解最後一種也是最複雜的一種非線性資料結構——圖。

  最後,由於本人水平有限,能力與大神仍相差甚遠,若有錯誤或不明之處,還望大家不吝賜教指正。非常感謝!