理解二叉樹

什么是樹

樹是由結(jié)點(diǎn)和邊組成的民轴,不存在環(huán)的一種數(shù)據(jù)結(jié)構(gòu)涝开』澄牵看下圖:

image

與樹相關(guān)的術(shù)語

  • 樹的結(jié)點(diǎn)(node):包含一個(gè)數(shù)據(jù)元素及若干指向子樹的分支;

  • 孩子結(jié)點(diǎn)(child node):結(jié)點(diǎn)的子樹的根稱為該結(jié)點(diǎn)的孩子曾沈;

  • 雙親結(jié)點(diǎn):B 結(jié)點(diǎn)是A 結(jié)點(diǎn)的孩子这嚣,則A結(jié)點(diǎn)是B 結(jié)點(diǎn)的雙親鸥昏;

  • 兄弟結(jié)點(diǎn):同一雙親的孩子結(jié)點(diǎn)塞俱;堂兄結(jié)點(diǎn):同一層上結(jié)點(diǎn);

  • 葉子結(jié)點(diǎn):也叫終端結(jié)點(diǎn)吏垮,是度為 0 的結(jié)點(diǎn)障涯;

  • 樹的深度:樹中最大的結(jié)點(diǎn)層數(shù);

二叉樹是什么

二叉樹是一種被高頻使用的特殊樹膳汪。在二叉樹中唯蝶,每個(gè)結(jié)點(diǎn)最多有兩個(gè)分支,即每個(gè)結(jié)點(diǎn)最多有兩個(gè)子結(jié)點(diǎn)遗嗽,分別稱作左子結(jié)點(diǎn)和右子結(jié)點(diǎn)粘我。

在二叉樹中,有下面兩個(gè)特殊的類型:

滿二叉樹痹换,定義為只有最后一層無任何子結(jié)點(diǎn)征字,其他所有層上的所有結(jié)點(diǎn)都有兩個(gè)子結(jié)點(diǎn)的二叉樹。

image

完全二叉樹娇豫,定義為除了最后一層以外匙姜,其他層的結(jié)點(diǎn)個(gè)數(shù)都達(dá)到最大,并且最后一層的葉子結(jié)點(diǎn)都靠左排列冯痢。

image

完全二叉樹看上去并不完全氮昧,但為什么這樣稱呼它呢框杜?這和二叉樹的存儲(chǔ)有關(guān)系。存儲(chǔ)二叉樹有兩種辦法袖肥,一種是基于指針的鏈?zhǔn)酱鎯?chǔ)法咪辱,另一種是基于數(shù)組的順序存儲(chǔ)法。

鏈?zhǔn)酱鎯?chǔ)法椎组,也就是像鏈表一樣梧乘,每個(gè)結(jié)點(diǎn)有三個(gè)字段,一個(gè)存儲(chǔ)數(shù)據(jù)庐杨,另外兩個(gè)分別存放指向左右子結(jié)點(diǎn)的指針选调,如下圖所示:

image

順序存儲(chǔ)法,就是按照規(guī)律把結(jié)點(diǎn)存放在數(shù)組里灵份,如下圖所示仁堪,為了方便計(jì)算,我們會(huì)約定把根結(jié)點(diǎn)放在下標(biāo)為 1 的位置填渠。隨后弦聂,B 結(jié)點(diǎn)存放在下標(biāo)為 2 的位置,C 結(jié)點(diǎn)存放在下標(biāo)為 3 的位置氛什,依次類推莺葫。

image

存儲(chǔ)方法:


image

根據(jù)這種存儲(chǔ)方法,我們可以發(fā)現(xiàn)如果結(jié)點(diǎn) X 的下標(biāo)為 i枪眉,那么 X 的左子結(jié)點(diǎn)總是存放在 2 * i 的位置捺檬,X 的右子結(jié)點(diǎn)總是存放在 2 * i + 1 的位置。

之所以稱為完全二叉樹贸铜,是從存儲(chǔ)空間利用效率的視角來看的堡纬。對(duì)于一棵完全二叉樹而言,僅僅浪費(fèi)了下標(biāo)為 0 的存儲(chǔ)位置蒿秦。而如果是一棵非完全二叉樹烤镐,則會(huì)浪費(fèi)大量的存儲(chǔ)空間。

image

樹的基本操作

  • 樹的遍歷
    遍歷一棵樹棍鳖,有非常經(jīng)典的三種方法炮叶,分別是前序遍歷、中序遍歷渡处、后序遍歷镜悉。這里的序指的是父結(jié)點(diǎn)的遍歷順序,前序就是先遍歷父結(jié)點(diǎn)骂蓖,中序就是中間遍歷父結(jié)點(diǎn)积瞒,后序就是最后遍歷父結(jié)點(diǎn)。不管哪種遍歷登下,都是通過遞歸調(diào)用完成的茫孔。

前序遍歷叮喳,對(duì)樹中的任意結(jié)點(diǎn)來說,先打印這個(gè)結(jié)點(diǎn)缰贝,然后前序遍歷它的左子樹馍悟,最后前序遍歷它的右子樹。

中序遍歷剩晴,對(duì)樹中的任意結(jié)點(diǎn)來說锣咒,先中序遍歷它的左子樹,然后打印這個(gè)結(jié)點(diǎn)赞弥,最后中序遍歷它的右子樹毅整。

后序遍歷,對(duì)樹中的任意結(jié)點(diǎn)來說绽左,先后序遍歷它的左子樹竖独,然后后序遍歷它的右子樹氮帐,最后打印它本身。

image

三種遍歷方式的代碼方式:

// 先序遍歷
func preOrder( _ root: TreeNode?)  {

    guard let rt = root else {
        return
    }

    print(rt.val)

    preOrder(rt.left)

    preOrder(rt.right)
 }

// 中序遍歷
func inOrderTraverse(_ root: TreeNode?) {

    guard let rt = root else {
        return
    }

    preOrder(rt.left)
   
    print(rt.val)
  
    preOrder(rt.right)

}

// 后序遍歷
func postOrderTraverse(_ root: TreeNode?) {

    guard let rt = root else {
        return
    }

    preOrder(rt.left)
  
    preOrder(rt.right)
    
    print(rt.val)

}

二叉樹遍歷過程中废恋,每個(gè)結(jié)點(diǎn)都被訪問了一次刊侯,其時(shí)間復(fù)雜度是 O(n)年鸳。執(zhí)行增加和刪除數(shù)據(jù)的操作時(shí)妖枚,在找到位置后,我們只需要通過指針建立連接關(guān)系就可以了运提。對(duì)于沒有任何特殊性質(zhì)的二叉樹而言,拋開遍歷的時(shí)間復(fù)雜度以外改含,真正執(zhí)行增加和刪除操作的時(shí)間復(fù)雜度是 O(1)情龄。

這里穿插講下遞歸

遞歸是指在函數(shù)的定義中使用函數(shù)自身的方法。

遞歸有兩層含義:

  • 遞歸問題必須可以分解為若干個(gè)規(guī)模較小候味、與原問題形式相同的子問題刃唤。并且這些子問題可以用完全相同的解題思路來解決;
  • 遞歸問題的演化過程是一個(gè)對(duì)原問題從大到小進(jìn)行拆解的過程白群,并且會(huì)有一個(gè)明確的終點(diǎn)(臨界點(diǎn))。一旦原問題到達(dá)了這個(gè)臨界點(diǎn)硬霍,就不用再往更小的問題上拆解了帜慢。最后,從這個(gè)臨界點(diǎn)開始唯卖,把小問題的答案按照原路返回粱玲,原問題便得以解決。

通過二叉樹的定義和特性可知拜轨,二叉樹的遍歷可用遞歸的方式處理抽减,臨界點(diǎn)是葉子結(jié)點(diǎn)的左子樹和右子樹皆為空,為空就return返回了橄碾。

二叉查找樹

二叉查找樹(也稱作二叉搜索樹)具備以下幾個(gè)的特性:

  • 在二叉查找樹中的任意一個(gè)結(jié)點(diǎn)卵沉,其左子樹中的每個(gè)結(jié)點(diǎn)的值颠锉,都要小于這個(gè)結(jié)點(diǎn)的值。
  • 在二叉查找樹中的任意一個(gè)結(jié)點(diǎn)史汗,其右子樹中每個(gè)結(jié)點(diǎn)的值琼掠,都要大于這個(gè)結(jié)點(diǎn)的值。
  • 在二叉查找樹中停撞,會(huì)盡可能規(guī)避兩個(gè)結(jié)點(diǎn)數(shù)值相等的情況瓷蛙。
  • 對(duì)二叉查找樹進(jìn)行中序遍歷,就可以輸出一個(gè)從小到大的有序數(shù)據(jù)隊(duì)列戈毒。

二叉查找樹的查找操作

在利用二叉查找樹執(zhí)行查找操作時(shí)艰猬,我們可以進(jìn)行以下判斷:

  • 首先判斷根結(jié)點(diǎn)是否等于要查找的數(shù)據(jù),如果是就返回埋市。
  • 如果根結(jié)點(diǎn)大于要查找的數(shù)據(jù)姥宝,就在左子樹中遞歸執(zhí)行查找動(dòng)作,直到葉子結(jié)點(diǎn)恐疲。
  • 如果根結(jié)點(diǎn)小于要查找的數(shù)據(jù)腊满,就在右子樹中遞歸執(zhí)行查找動(dòng)作,直到葉子結(jié)點(diǎn)培己。

這樣的“二分查找”所消耗的時(shí)間復(fù)雜度就可以降低為 O(logn)碳蛋。代碼如下:

func searchBST(_ root: TreeNode?, _ val: Int) -> TreeNode? {
    guard let rt = root else {
        return nil
    }

    if rt.val > val {
        return searchBST(rt.left, val)
    } else if rt.val < val {
        return searchBST(rt.right, val)
    } else {
        return rt
    }
}

二叉查找樹的增加操作

從根結(jié)點(diǎn)開始,如果要插入的數(shù)據(jù)比根結(jié)點(diǎn)的數(shù)據(jù)大省咨,且根結(jié)點(diǎn)的右子結(jié)點(diǎn)不為空肃弟,則在根結(jié)點(diǎn)的右子樹中繼續(xù)嘗試執(zhí)行插入操作。直到找到為空的子結(jié)點(diǎn)執(zhí)行插入動(dòng)作零蓉。

二叉查找樹插入數(shù)據(jù)的時(shí)間復(fù)雜度是 O(logn)笤受。但這并不意味著它比普通二叉樹要復(fù)雜。原因在于這里的時(shí)間復(fù)雜度更多是消耗在了遍歷數(shù)據(jù)去找到查找位置上敌蜂,真正執(zhí)行插入動(dòng)作的時(shí)間復(fù)雜度仍然是 O(1)箩兽。

func insertIntoBST(_ root: TreeNode?, _ val: Int) -> TreeNode? {
       
        guard let currNode = root else {
            return TreeNode(val)
        }
        
        if val > currNode.val {
            
            root?.right = insertIntoBST(root?.right, val)
            
        } else {
            
            root?.left = insertIntoBST(root?.left, val)
            
        }
        
        return root
    }

二叉查找樹的刪除操作

/*

    根據(jù)二叉搜索樹的性質(zhì)

    如果目標(biāo)節(jié)點(diǎn)大于當(dāng)前節(jié)點(diǎn)值,則去右子樹中刪除章喉;

    如果目標(biāo)節(jié)點(diǎn)小于當(dāng)前節(jié)點(diǎn)值汗贫,則去左子樹中刪除;

    如果目標(biāo)節(jié)點(diǎn)就是當(dāng)前節(jié)點(diǎn)秸脱,分為以下三種情況:

    其無左子:其右子頂替其位置落包,刪除了該節(jié)點(diǎn);

    其無右子:其左子頂替其位置摊唇,刪除了該節(jié)點(diǎn)咐蝇;

    其左右子節(jié)點(diǎn)都有:其左子樹轉(zhuǎn)移到其右子樹的最左節(jié)點(diǎn)的左子樹上,然后右子樹頂替其位置巷查,由此刪除了該節(jié)點(diǎn)有序。

     */
func deleteNode(_ root: TreeNode?, _ key: Int) -> TreeNode? {
        guard var root = root else {
            return nil
        }
        
        if key > root.val {
            root.right = deleteNode(root.right, key)
        } else if key < root.val{
            root.left = deleteNode(root.left, key)
        } else {
            if nil == root.right {
                return root.left
            } else if nil == root.left {
                return root.right
            } else if let _ = root.right, let _ = root.left {
                var temp = root.right
                while let _ = temp?.left {
                    temp = temp?.left
                }
                temp?.left = root.left
                if let right = root.right {
                    root = right
                }
            }
        }        
        return root
    }

總結(jié)

本文主要講解的是二叉樹的基本理解和操作抹腿,理解二叉樹的各類操作無外乎查增刪,鏈?zhǔn)蕉鏄湓鰟h的時(shí)間復(fù)雜度都是0(1),真正耗時(shí)的是查笔呀,時(shí)間復(fù)雜度是O(logn)幢踏,所以二叉樹的增刪查的時(shí)間復(fù)雜度都是0(logn)。另外二叉樹的查找分兩種许师,深度優(yōu)先遍歷還是廣度優(yōu)先遍歷房蝉,本文講的都是深度優(yōu)先的查找操作即遞歸。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末微渠,一起剝皮案震驚了整個(gè)濱河市搭幻,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌逞盆,老刑警劉巖檀蹋,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異云芦,居然都是意外死亡俯逾,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門舅逸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來桌肴,“玉大人,你說我怎么就攤上這事琉历∽蛊撸” “怎么了?”我有些...
    開封第一講書人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵旗笔,是天一觀的道長彪置。 經(jīng)常有香客問我,道長蝇恶,這世上最難降的妖魔是什么拳魁? 我笑而不...
    開封第一講書人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮艘包,結(jié)果婚禮上的猛,老公的妹妹穿的比我還像新娘。我一直安慰自己想虎,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開白布叛拷。 她就那樣靜靜地躺著舌厨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪忿薇。 梳的紋絲不亂的頭發(fā)上裙椭,一...
    開封第一講書人閱讀 49,764評(píng)論 1 290
  • 那天躏哩,我揣著相機(jī)與錄音,去河邊找鬼揉燃。 笑死扫尺,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的炊汤。 我是一名探鬼主播正驻,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼抢腐!你這毒婦竟也來了姑曙?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤迈倍,失蹤者是張志新(化名)和其女友劉穎伤靠,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體啼染,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡宴合,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了迹鹅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片卦洽。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖徒欣,靈堂內(nèi)的尸體忽然破棺而出逐样,到底是詐尸還是另有隱情,我是刑警寧澤打肝,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布脂新,位于F島的核電站,受9級(jí)特大地震影響粗梭,放射性物質(zhì)發(fā)生泄漏争便。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一断医、第九天 我趴在偏房一處隱蔽的房頂上張望滞乙。 院中可真熱鬧,春花似錦鉴嗤、人聲如沸斩启。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽兔簇。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間垄琐,已是汗流浹背边酒。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留狸窘,地道東北人墩朦。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像翻擒,于是被迫代替她去往敵國和親氓涣。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

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