數(shù)據(jù)結(jié)構(gòu)(五)-- AVL樹的左旋與右旋

“不平衡”出現(xiàn)的時(shí)機(jī)

在上一篇 AVL樹基礎(chǔ) 文章中我們最后說到“平衡因子”概念胰柑。
在插入新元素后截亦,就可能出現(xiàn)“不平衡”,所以我們就需要去維護(hù)平衡柬讨。首先我們圖解分析“不平衡”出現(xiàn)的時(shí)機(jī)崩瓤。

本文首發(fā)于心安-XinAnzzZ 的個(gè)人博客,轉(zhuǎn)載請(qǐng)注明出處~

image

可以看到踩官,在插入新的節(jié)點(diǎn)之后却桶,導(dǎo)致了父節(jié)點(diǎn)的高度值變化,從而導(dǎo)致父節(jié)點(diǎn)或者祖先節(jié)點(diǎn)(父節(jié)點(diǎn)的父節(jié)點(diǎn))的左右子樹的高度差變化卖鲤,也就出現(xiàn)了“不平衡”的狀態(tài)肾扰。
所以,我們就應(yīng)該每次插入新節(jié)點(diǎn)的時(shí)候通過平衡因子的變化來判斷是否導(dǎo)致了父節(jié)點(diǎn)或者祖先節(jié)點(diǎn)“不平衡”蛋逾,從而來維護(hù)平衡因子集晚,使得這棵樹繼續(xù)平衡。
由于我們的插入的代碼邏輯是遞歸完成的区匣,所以我們維護(hù)了父節(jié)點(diǎn)的平衡偷拔,也就遞歸的維護(hù)了祖先節(jié)點(diǎn)的平衡。
在上一篇文章中亏钩,我們寫了兩個(gè)輔助函數(shù)莲绰,其中一個(gè)就是獲取節(jié)點(diǎn)的平衡因子,這里我們就可以用上了姑丑。
在我們遞歸的插入元素的時(shí)候蛤签,遞歸的判斷每一個(gè)節(jié)點(diǎn)的平衡因子,遞歸的維護(hù)平衡栅哀,從而使整棵樹始終保持平衡震肮。下面我們使用圖形和代碼來講解一下如何通過左旋與右旋使得節(jié)點(diǎn)保持平衡称龙。

圖片演示左旋與右旋

右旋

如上圖所示,插入一個(gè)新節(jié)點(diǎn)之后出現(xiàn)了“不平衡”戳晌,我們可以簡單的將上圖抽象為下圖:

image

如上圖鲫尊,T1、T2沦偎、T3疫向、T4分別為x、y豪嚎、z節(jié)點(diǎn)的子樹搔驼,根據(jù)二分搜索樹的性質(zhì),它們之間的大小關(guān)系應(yīng)該是

T1 < z < T2 < x < T3 < y < T4

右旋轉(zhuǎn)就是將x節(jié)點(diǎn)的右子樹先拿掉疙渣,然后讓x節(jié)點(diǎn)的有孩子等于y節(jié)點(diǎn)匙奴,最后y節(jié)點(diǎn)的左孩子等于剛剛拿掉的x節(jié)點(diǎn)的右孩子。
這里無論T1妄荔、T2泼菌、T3、T4都是可以為空的啦租,并不影響結(jié)果哗伯。右旋如下圖:

image
image
image

可以看到,經(jīng)過右旋之后篷角,二叉樹重新恢復(fù)平衡焊刹,并且上面的大小關(guān)系也沒有發(fā)生改變,仍然滿足二分搜索樹的性質(zhì)恳蹲。
由于添加節(jié)點(diǎn)是遞歸過程虐块,所以右旋之后的子樹的父親節(jié)點(diǎn)和祖先節(jié)點(diǎn)也會(huì)進(jìn)行相應(yīng)的右旋或者左旋使其自身達(dá)到平衡狀態(tài)。

根據(jù)上圖嘉蕾,我們可以編寫我們的右旋轉(zhuǎn)代碼贺奠,如下:

/**
 * LL
 *
 * / 對(duì)節(jié)點(diǎn)進(jìn)行右旋轉(zhuǎn)操作,返回右旋轉(zhuǎn)之后新的根節(jié)點(diǎn)
 * /        y                            x
 * /       / \                         /   \
 * /      x   T4     向右旋轉(zhuǎn)(y)      z     y
 * /     / \       -------------->  / \   /  \
 * /    z  T3                      T1 T2 T3  T4
 * /   / \
 * /  T1 T2
 */
private Node rightRotate(Node y) {
    Node x = y.left;
    // 保存x節(jié)點(diǎn)的右子樹错忱,即使右子樹為空儡率,也沒關(guān)系
    Node t3 = x.right;

    // 右旋
    x.right = y;
    // 將原本x的右子樹放在y的左子樹的位置
    y.left = t3;

    // 更新height
    y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
    x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
    return x;
}

左旋

上圖演示的只是其中一種情況,就是插入的新元素是在第一個(gè)不平衡的節(jié)點(diǎn)的左側(cè)的左側(cè)以清,所以可以使用右旋轉(zhuǎn)來解決儿普。
另外一種情況就是新插入的元素在第一個(gè)不平衡的節(jié)點(diǎn)的右側(cè)的右側(cè),這里筆者偷個(gè)懶掷倔,不再畫圖眉孩。
因?yàn)檫@個(gè)其實(shí)和上面的是對(duì)稱的,筆者可以自己在紙上畫一下勒葱,對(duì)比下面的代碼方便理解勺像。

/**
 * RR
 * <p>
 * / 對(duì)節(jié)點(diǎn)進(jìn)行左旋轉(zhuǎn)操作障贸,返回左旋轉(zhuǎn)之后新的根節(jié)點(diǎn)
 * /        y                            x
 * /       / \                         /   \
 * /      T1  x     向右旋轉(zhuǎn)(y)       y     z
 * /         / \    -------------->  / \   / \
 * /        T2  z                   T1 T2 T3 T4
 * /           / \
 * /          T3 T4
 */
private Node leftRotate(Node y) {
    Node x = y.right;
    Node t2 = x.left;

    // 右旋
    x.left = y;
    y.right = t2;

    //更新height
    y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
    x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
    return x;
}

可以看到,這里的代碼和上面的大同小異吟宦,也不需要過多的介紹,下面我們看一下最后兩種情況涩维。

LR

上面兩種情況分別是插入的元素在不平衡節(jié)點(diǎn)的左側(cè)的左側(cè)(LL)殃姓、插入的元素在不平衡節(jié)點(diǎn)的右側(cè)的右側(cè)(RR)
那么還有兩種情況就分別是插入的元素在不平衡節(jié)點(diǎn)的左側(cè)的右側(cè)(LR)瓦阐、插入的元素在不平衡節(jié)點(diǎn)的右側(cè)的左側(cè)(RL)

對(duì)于插入的元素在不平衡節(jié)點(diǎn)的左側(cè)的右側(cè)(LR)蜗侈,我們可以先進(jìn)行左旋轉(zhuǎn),使得其轉(zhuǎn)化為插入的元素在不平衡節(jié)點(diǎn)的左側(cè)的左側(cè)(LL)睡蟋,再進(jìn)行右旋轉(zhuǎn)即可踏幻。
圖解:

image

對(duì)x節(jié)點(diǎn)進(jìn)行左旋轉(zhuǎn)之后就轉(zhuǎn)化為了LL的情況,按照第一種情況進(jìn)行右旋處理即可戳杀。

image

RL

同樣的该面,和上面想對(duì)稱的就是RL的情況,對(duì)稱處理即可信卡。這里筆者也不再贅述隔缀。
直接看一下實(shí)際的代碼。上面我們已經(jīng)寫好了左旋與右旋的代碼傍菇,四種情況其實(shí)用到的都是左旋和右旋猾瘸,所以使用這兩個(gè)函數(shù)即可。

插入一個(gè)元素的代碼:

public void put(K key, V value) {
    Node newNode = new Node(key, value);
    root = put(root, newNode);
}

/*** 遞歸算法:插入一個(gè)元素丢习,返回插入新節(jié)點(diǎn)后樹的根 */
private Node put(Node node, Node newNode) {
    if (node == null) {
        size++;
        return newNode;
    }

    if (newNode.key.compareTo(node.key) < 0) {
        node.left = put(node.left, newNode);
    } else if (newNode.key.compareTo(node.key) > 0) {
        node.right = put(node.right, newNode);
    } else {
        node.value = newNode.value;
    }

    // 更新高度
    node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1;

    // 維護(hù)平衡
    int balanceFactor = getBalanceFactor(node);

    // LL
    if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0) {
        // 右旋轉(zhuǎn)
        return rightRotate(node);
    }

    // LR
    if (balanceFactor > 1 && getBalanceFactor(node.left) < 0) {
        // 先對(duì)當(dāng)前節(jié)點(diǎn)的左孩子進(jìn)行左旋轉(zhuǎn)轉(zhuǎn)變?yōu)長L牵触,然后在進(jìn)行右旋轉(zhuǎn)
        node.left = leftRotate(node.left);
        // 右旋轉(zhuǎn)
        return rightRotate(node);
    }

    // RR
    if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0) {
        // 左旋轉(zhuǎn)
        return leftRotate(node);
    }

    //RL
    if (balanceFactor < -1 && getBalanceFactor(node.right) > 0) {
        // 先對(duì)當(dāng)前節(jié)點(diǎn)的右孩子進(jìn)行右旋轉(zhuǎn)轉(zhuǎn)變?yōu)镽R,然后在進(jìn)行左旋轉(zhuǎn)
        node.right = rightRotate(node.right);
        // 左旋轉(zhuǎn)
        return leftRotate(node);
    }

    return node;
}

上面的代碼注釋寫的都比較清楚了咐低,讀者只要認(rèn)真思考一下揽思,自己畫一下各種樹的結(jié)構(gòu),都是可以理解代碼的思想的渊鞋。

總結(jié)

筆者認(rèn)為AVL樹的維持平衡的思想理解起來相對(duì)來說還是有點(diǎn)難度的绰更,所以這里做一下總結(jié),希望能幫助到讀者锡宋。

首先儡湾,AVL樹基于二分搜索樹,不同的是它要求每一個(gè)節(jié)點(diǎn)保持平衡执俩。
那么在插入新元素的時(shí)候就可能會(huì)破壞原本的平衡性(當(dāng)然刪除元素也會(huì)徐钠,刪除和添加是逆向操作,如果明白了本章內(nèi)容役首,刪除操作也是非常簡單的尝丐,這一部分內(nèi)容下一章再進(jìn)行補(bǔ)充)显拜,所以就需要我們使用左旋或者右旋進(jìn)行維護(hù)它的平衡。
那么不平衡的情況一共是四種爹袁,如下圖:

image
image
image
image

針對(duì)不同的情況远荠,做不同的旋轉(zhuǎn)即可。再次強(qiáng)調(diào)失息,由于是遞歸算法譬淳,所以其父節(jié)點(diǎn)及祖先節(jié)點(diǎn)都會(huì)相應(yīng)的做出改變來維護(hù)平衡。

本文到這里就結(jié)束了盹兢,后面筆者還會(huì)補(bǔ)充一下刪除元素的代碼邻梆。感謝閱讀,有任何問題你都可以在評(píng)論區(qū)進(jìn)行評(píng)論绎秒,筆者會(huì)在第一時(shí)間給你回復(fù)浦妄。
也歡迎加入筆者的qq群交流技術(shù)問題。

示例代碼Github

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末见芹,一起剝皮案震驚了整個(gè)濱河市剂娄,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌辆童,老刑警劉巖宜咒,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異把鉴,居然都是意外死亡故黑,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門庭砍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來场晶,“玉大人,你說我怎么就攤上這事怠缸∈幔” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵揭北,是天一觀的道長扳炬。 經(jīng)常有香客問我,道長搔体,這世上最難降的妖魔是什么恨樟? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮疚俱,結(jié)果婚禮上劝术,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好养晋,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布衬吆。 她就那樣靜靜地躺著,像睡著了一般绳泉。 火紅的嫁衣襯著肌膚如雪逊抡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天零酪,我揣著相機(jī)與錄音秦忿,去河邊找鬼。 笑死蛾娶,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的潜秋。 我是一名探鬼主播蛔琅,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼峻呛!你這毒婦竟也來了罗售?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤钩述,失蹤者是張志新(化名)和其女友劉穎寨躁,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體牙勘,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡职恳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了方面。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片放钦。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖恭金,靈堂內(nèi)的尸體忽然破棺而出操禀,到底是詐尸還是另有隱情,我是刑警寧澤横腿,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布颓屑,位于F島的核電站,受9級(jí)特大地震影響耿焊,放射性物質(zhì)發(fā)生泄漏揪惦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一搀别、第九天 我趴在偏房一處隱蔽的房頂上張望丹擎。 院中可真熱鬧,春花似錦、人聲如沸蒂培。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽护戳。三九已至翎冲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間媳荒,已是汗流浹背抗悍。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留钳枕,地道東北人缴渊。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像鱼炒,于是被迫代替她去往敵國和親衔沼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容