算法(5):二叉樹

??終于到傻叉樹啦~ 這個是我想好好探討的一種數(shù)據(jù)結構,小伙子我覺得它有難度瓶竭,不管是前中后序以及層級遍歷督勺,還是各種遞歸迭代的妙用渠羞,總之,很酷智哀!
??加油加油堵未,怎么也得寫個十多篇算法系列才行~



??雖然本系列著重講算法,但還是需要個引子盏触,所以小編先簡單說一下啥是二叉樹渗蟹。二叉樹是每個結點最多有兩個子樹的樹結構。通常子樹被稱作“左子樹”(left subtree)和“右子樹”(right subtree)赞辩。如下圖所示雌芽,便是一個二叉樹結構。


二叉樹

??既然大家知道什么是二叉樹了辨嗽,那么我們首先來看一下二叉樹的前序遍歷(Pre-order Traversal)世落、中序遍歷(In-order Traversal)以及后序遍歷(Post-order Traversal)。

  • 前序遍歷:先根節(jié)點糟需,再左節(jié)點屉佳,最后右節(jié)點。(也可以右左根洲押,然后翻轉)
  • 中序遍歷:先左節(jié)點武花,再根節(jié)點,最后右節(jié)點杈帐。(也可以右根左体箕,然后翻轉)
  • 后序遍歷:先左節(jié)點,在右節(jié)點挑童,最后根節(jié)點累铅。(也可以根右左,然后翻轉)

??說白了站叼,就是看遍歷根節(jié)點的先后順序娃兽。而不論是哪種遍歷方法,都是左節(jié)點比右節(jié)點先遍歷尽楔。當然投储,如果按照括號里給出的操作方式,也可以得到相同結果~ 那么翔试,既然知道了如何遍歷轻要,根據(jù)上面的二叉樹圖复旬,大家可以寫出不同遍歷方法的的結果嗎垦缅?
??算了,我也不知道你們會不會驹碍,索性先把答案給各位放出來壁涎,有興趣的小伙伴可以先試試自己寫凡恍,然后和我給的答案對照一下~ 蛤?你說你的答案跟我的不一樣怔球?那肯定是你錯了......

  • 前序遍歷結果:F B A D C E G I H
  • 中序遍歷結果:A B C D E F G H I
  • 后序遍歷結果:A C E D B H I G F

??既然有這三種遍歷方法嚼酝,那么,它們都有何作用呢竟坛?什么時候用前序遍歷闽巩,什么時候又需要后序遍歷呢?大家且聽我慢慢道來:

  • 前序遍歷:在第一次遍歷到節(jié)點時就執(zhí)行操作担汤,一般只是想遍歷執(zhí)行操作(或輸出結果)可選用先序遍歷涎跨,如輸出某個文件夾下所有文件與文件夾的名稱:遍歷文件夾,先輸出文件夾名崭歧,然后再依次輸出該文件夾下的所有文件(包括子文件夾)隅很,如果有子文件夾,則再進入該子文件夾率碾,輸出該子文件夾下的所有文件名叔营。這是一個典型的先序遍歷過程。

  • 中序遍歷:對于二分搜索樹所宰,中序遍歷的操作順序(或輸出結果順序)是符合從小到大(或從大到腥拮稹)順序的,故要遍歷輸出排序好的結果需要使用中序遍歷仔粥。除此之外垒酬,還有中綴表達式,一個通用的算術或邏輯公式表示方法件炉。符合人類的邏輯(例:2 + 3).

  • 后序遍歷:后續(xù)遍歷的特點是執(zhí)行操作時勘究,肯定已經(jīng)遍歷過該節(jié)點的左右子節(jié)點。1. 故適用于進行破壞性操作的情況斟冕,比如刪除節(jié)點(因為我們在刪除節(jié)點時需要先刪除左右兩子節(jié)點口糕,再刪除該節(jié)點)。2. 其次磕蛇,如果我們要統(tǒng)計文件夾大小景描,也需要后序遍歷來實現(xiàn):若要知道某文件夾的大小必須先知道該文件夾下所有文件的大小,如果有子文件夾秀撇,若要知道該子文件夾大小超棺,必須先知道子文件夾所有文件的大小。這是一個典型的后序遍歷過程呵燕。3. 既然有了中綴表達式棠绘,那么也有相應的后綴表達式當然也有前綴表達式,后綴表達式是一種不需要括號的表達法氧苍。符合計算機的邏輯(例:2 3 +) 夜矗。

??關于中綴和后綴表達式,我愿意再給大家詳細說明一下让虐,各位官爺請看下圖:

數(shù)學表達式

??如果我們用中序遍歷紊撕,那么可以非常迅速的得到4 * 7 - 2 + 5這么一個式子。但是赡突,每個運算符的優(yōu)先級对扶,很難寫程序去判定。比如惭缰,我們想表示4 * (7 - 2) + 5的式子辩稽,即讓減號的優(yōu)先級最高,該如何計算呢从媚?這時逞泄,就需要后綴表達式出場了。我們可以得到4 7 2 - * 5 +這么一串結果拜效。屏幕前的你們可能已經(jīng)開始認真思考喷众,內心也產(chǎn)生了寶貴的想法:zhe ta ma shen me ji ba wan yi er... 但是計算機可是如獲至寶,開心的像個兩百斤的孩子一樣紧憾。因為它可以很輕易的算出結果:建立一個棧(stack到千,這玩意遵循LIFO,即 last in赴穗,first out 原則)憔四,不停的將節(jié)點值壓進棧里,當節(jié)點值為運算符時般眉,它便彈出最新壓入的值做運算了赵,并將結果再壓入棧中。so easy甸赃!哪里不會點哪里柿汛,媽媽再也不用擔心我的學習啦...

??洋洋灑灑上千字,沒點代碼怎么行埠对。


問題1:二叉樹前序遍歷(Binary Tree Preorder Traversal)
輸入:二叉樹根節(jié)點
輸出:[1, 2, 4, 3, 5]

    1
   / \
  2   3
  \   /
  4   5

??前序遍歷比較簡單络断,一邊遍歷,一邊裝節(jié)點值项玛。以下代碼用了非遞歸方式貌笨,大伙可以試試如何用遞歸寫~(遞歸寫起來方便些,但是執(zhí)行效率會偏低)

class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None

def preorderTraversal(root):
    cur_node = root
    stack = []
    res = []

    while stack or cur_node:
        if cur_node:
            res.append(cur_node.val)
            stack.append(cur_node)
            cur_node = cur_node.left
        else:
            cur_node = stack.pop()
            cur_node = cur_node.right
    return res

if __name__ == '__main__':
    node = head = TreeNode(1)
    node.left =  TreeNode(2)
    node.right = TreeNode(3)
    node.left.right = TreeNode(4)
    node.right.left = TreeNode(5)

    ans = preorderTraversal(head)
    print(ans)

問題2:二叉樹中序遍歷(Binary Tree Inorder Traversal)
輸入:同上
輸出:[2, 4, 1, 5, 3]

class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None

def inorderTraversal(root: TreeNode) -> list:
    cur_node = root
    stack = []
    res = []

    while stack or cur_node:
        if cur_node:
            stack.append(cur_node)
            cur_node = cur_node.left
        else:
            cur_node = stack.pop()
            res.append(cur_node.val)
            cur_node = cur_node.right
    return res

if __name__ == '__main__':
    node = head = TreeNode(1)
    node.left =  TreeNode(2)
    node.right = TreeNode(3)
    node.left.right = TreeNode(4)
    node.right.left = TreeNode(5)

    ans = inorderTraversal(head)
    print(ans)

問題3:二叉樹后序遍歷(Binary Tree Postorder Traversal)
輸入:同上
輸出:[4, 2, 5, 3, 1]
??后序遍歷的話運用了倒裝方式襟沮,先裝節(jié)點值锥惋,再裝右節(jié)點昌腰,最后裝左節(jié)點。返回結果時將列表翻轉即為答案净刮。

class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None

def postorderTraversal(root: TreeNode) ->list:
    cur_node = root
    stack = []
    res = []

    while stack or cur_node:
        if cur_node:
            stack.append(cur_node)
            res.append(cur_node.val)
            cur_node = cur_node.right
        else:
            cur_node = stack.pop()
            cur_node = cur_node.left
    return res[::-1]

if __name__ == '__main__':
    node = head = TreeNode(1)
    node.left =  TreeNode(2)
    node.right = TreeNode(3)
    node.left.right = TreeNode(4)
    node.right.left = TreeNode(5)

    ans = postorderTraversal(head)
    print(ans)

問題4:二叉樹逐層遍歷(Binary Tree Level Order Traversal)。這里是將二叉樹一層一層硅则,從左往右遍歷淹父,用到了廣度優(yōu)先搜索(Breadth-First Search)的思想。
輸入:同上
輸出:[[1], [2, 3], [4, 5]]

class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None


def levelOrder(root: TreeNode) -> list:
    if root == None: return []
    queue = [[root]]
    res = []
    while queue:
        curlevel = queue.pop()
        newlevel = []
        val = []
        for node in curlevel:
            if node:
                val.append(node.val)
                newlevel.append(node.left)
                newlevel.append(node.right)
        if newlevel != []:
            queue.append(newlevel)
        if val != []:
            res.append(val)
    return res

if __name__ == '__main__':
    node = head = TreeNode(1)
    node.left =  TreeNode(2)
    node.right = TreeNode(3)
    node.left.right = TreeNode(4)
    node.right.left = TreeNode(5)

    ans = levelOrder(head)
    print(ans)

問題5:判斷一個二叉樹是否為對稱二叉樹怎虫。
輸入:二叉樹暑认,如下所示:
輸出:True

    1
   / \
  2   2
  \   /
  3   3

代碼如下(就我個人理解,應該是屬于自頂向下的遞歸法大审,如果不滿足先決條件的話蘸际,將不再繼續(xù)遞歸,直接返回False結果):

class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None

def isSymmetric(root: TreeNode) -> bool:
    def helper(left, right):
        if not left and not right: return True
        if left and right and left.val == right.val:
            return helper(left.right, right.left) and helper(left.left, right.right)
        return False

    if not root: return True
    return helper(root.left, root.right)

if __name__ == '__main__':
    node = head = TreeNode(1)
    node.left =  TreeNode(2)
    node.right = TreeNode(2)
    node.left.right = TreeNode(3)
    node.right.left = TreeNode(3)

    ans = isSymmetric(head)
    print(ans)

自頂向下(top-down)和自底向上(bottom-up)的遞歸方法

  • 自頂向下:在每次遞歸調用時徒扶,我們利用該節(jié)點的信息計算出某些值粮彤,然后通過遞歸調用將這些值(這些傳遞的值一般是全局變量)傳遞給它的子節(jié)點,一直到葉子節(jié)點姜骡。
  • 自底向上:在使用遞歸法時导坟,我們先對子節(jié)點使用遞歸函數(shù),然后讓子節(jié)點返回一些值(這時一般就不傳遞全局變量)圈澈,最后我們利用子節(jié)點返回的值進行計算惫周。

舉個小小例子:計算某個二叉樹的深度。(如果是屏幕面前的你康栈,你會如何計算呢递递?感覺自己講話方式好 zhi zhang,啥么,登舞,

  • 如果是自頂向下(此時從上往下樹的深度執(zhí)行加一操作):
1. return if root is null
2. if root is a leaf node:
3.      answer = max(answer, depth)         // update the answer if needed
4. maximum_depth(root.left, depth + 1)      // call the function recursively for left child
5. maximum_depth(root.right, depth + 1)     // call the function recursively for right child
  • 這個時候你又想自底向上了(這時是從下往上深度不停加一):
1. return 0 if root is null                 // return 0 for null node
2. left_depth = maximum_depth(root.left)
3. right_depth = maximum_depth(root.right)
4. return max(left_depth, right_depth) + 1  // return depth of the subtree rooted at root

??大家可以看出,如果我們可以得到該節(jié)點的信息(如節(jié)點深度為前面總共深度再加一)悬荣,并且這些信息可以幫助到子節(jié)點的計算逊躁,那么就可以考慮自頂向下法;如果我們知道子節(jié)點的結果隅熙,并通過它來計算該節(jié)點的結果稽煤,那么便可以使用自底向上法∏羝荩總而言之一句話酵熙,這玩意不簡單吶。


例題6:歡迎大家收看例題沒完沒了系列之路徑求和(Path Sum)驰坊。給定一顆二叉樹匾二,如果從根節(jié)點到某個葉子節(jié)點這條路徑上的值之和等于目標值’target‘,則返回True,否則返回False察藐。
輸入:見例題1
輸出:True(1+2+4=7)皮璧,F(xiàn)alse,True(1+3+5=9)
這個代碼屬于自頂向下方法:

class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None

def hasPathSum(root: TreeNode, target: int) -> bool:
    def helper(node, ans):
        if not node: return False
        if not node.left and not node.right: return ans + node.val == target
        return helper(node.left, ans + node.val) or helper(node.right, ans + node.val)

    return helper(root, 0)

if __name__ == '__main__':
    node = head = TreeNode(1)
    node.left =  TreeNode(2)
    node.right = TreeNode(3)
    node.left.right = TreeNode(4)
    node.right.left = TreeNode(5)

    for i in range(7,10):
        print(hasPathSum(head,i))

例題7:根據(jù)中序遍歷和后序遍歷的列表分飞,構建該二叉樹悴务。
輸入:inorder = [9,3,15,20,7],postorder = [9,15,7,20,3]
輸出:二叉樹譬猫,如下所示讯檐。

    3
   / \
  9  20
    /  \
   15   7

??對于一棵樹的中序遍歷,根節(jié)點肯定處于遍歷結果的中間(假設這棵樹有左子樹)染服,后序遍歷的時候根節(jié)點肯定是最后一個遍歷的元素别洪。所以我們在后序遍歷列表當中,依次從最后位置取出根節(jié)點柳刮,并且找到該根節(jié)點在中序遍歷列表當中的位置挖垛。此時,該位置左邊就是這棵樹的左子樹元素秉颗,右邊元素就是這棵樹的右子樹元素晕换。如此遞歸下去,便可得到想要結果~
??大家可以思考一個問題站宗,那就是根據(jù)中序遍歷和前序遍歷是否也可以構建出唯一的二叉樹闸准?(答案是可以,做法也是幾乎一樣梢灭,在前序遍歷列表當中從前往后取根節(jié)點就可以~)那么前序和后序遍歷能構建唯一的一棵二叉樹嗎夷家?(答案是不行,因為你無法判斷節(jié)點屬于左子樹還是右子樹)

class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None

def buildTree( inorder: list, postorder: list) ->TreeNode:
    if not inorder or not postorder: return
    val = postorder.pop()
    node = TreeNode(val)
    idx = inorder.index(val)

    node.right = buildTree(inorder[idx + 1:], postorder)
    node.left = buildTree(inorder[:idx], postorder)

    return node

def preorder(head):
    def helper(node,ans):
        if not node:return
        ans.append(node.val)
        helper(node.left,ans)
        helper(node.right,ans)

    ans =[]
    helper(head,ans)
    return ans

if __name__ == '__main__':
    inorder = [9, 3, 15, 20, 7]
    postorder = [9, 15, 7, 20, 3]
    head = buildTree(inorder,postorder)

    ans = preorder(head)
    print(ans)

例題8:輸入兩棵二叉樹A敏释,B库快,判斷B是不是A的子結構。(ps:我們約定空樹不是任意一個樹的子結構)
輸入:A

    3
   / \
  9  20
    /  \
   15   7
       /  \
      4    9

輸入:B

    20
   /  \
  15   7

輸出:True

class Solution:
    def HasSubtree(self, pRoot1, pRoot2):
        # write code here
        if not pRoot1 or not pRoot2:
            return False
        return self.is_subtree(pRoot1, pRoot2) or self.HasSubtree(pRoot1.left, pRoot2) or self.HasSubtree(pRoot1.right, pRoot2)
      
    def is_subtree(self, A, B):
        if not B:
            return True
        if not A or A.val != B.val:
            return False
        return self.is_subtree(A.left,B.left) and self.is_subtree(A.right, B.right)

求求求求求钥顽,點贊和關注义屏!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蜂大,隨后出現(xiàn)的幾起案子闽铐,更是在濱河造成了極大的恐慌,老刑警劉巖奶浦,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件兄墅,死亡現(xiàn)場離奇詭異,居然都是意外死亡澳叉,警方通過查閱死者的電腦和手機隙咸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門沐悦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人五督,你說我怎么就攤上這事藏否。” “怎么了充包?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵副签,是天一觀的道長。 經(jīng)常有香客問我误证,道長继薛,這世上最難降的妖魔是什么修壕? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任愈捅,我火速辦了婚禮,結果婚禮上慈鸠,老公的妹妹穿的比我還像新娘蓝谨。我一直安慰自己,他們只是感情好青团,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布譬巫。 她就那樣靜靜地躺著,像睡著了一般督笆。 火紅的嫁衣襯著肌膚如雪芦昔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天娃肿,我揣著相機與錄音咕缎,去河邊找鬼。 笑死料扰,一個胖子當著我的面吹牛凭豪,可吹牛的內容都是我干的。 我是一名探鬼主播晒杈,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼嫂伞,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了拯钻?” 一聲冷哼從身側響起帖努,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎粪般,沒想到半個月后然磷,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡刊驴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年姿搜,在試婚紗的時候發(fā)現(xiàn)自己被綠了寡润。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡舅柜,死狀恐怖梭纹,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情致份,我是刑警寧澤变抽,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站氮块,受9級特大地震影響绍载,放射性物質發(fā)生泄漏。R本人自食惡果不足惜滔蝉,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一击儡、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蝠引,春花似錦阳谍、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至吊洼,卻和暖如春训貌,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背冒窍。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工递沪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人超燃。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓区拳,卻偏偏與公主長得像,于是被迫代替她去往敵國和親意乓。 傳聞我的和親對象是個殘疾皇子樱调,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354