零
LeetCode樹提計划開始有幾天了。
今天對「樹」的進度做一個簡短的小結,群里親愛的小伙伴進行的怎么樣了呢?我這邊預計在整個「樹」的階段,預計會進行四個小結以及一個完整的復盤,所以,應該是 5 份總結資料。
分布如下:
- 「樹」的基礎遍歷,重點在於「樹」的遞歸的理解
- 模塊1:基礎遍歷,對LeetCode中進行刷題標記
- 模塊2:遍歷變種-自頂向下,對這些題目進行解釋和代碼編寫
- 模塊3:遍歷變種-非自頂向下,同樣也是對這些題目進行解釋和代碼編寫
- 最終的復盤總結「最重要」
還是把咱們的計划列出來:
所以,今天會是先序、中序、后續、層次遍歷的基礎代碼編寫
今天內容相對來說比較容易,就是「樹」的 4 種遍歷。但是,再強調,多看看遞歸的寫法,多深入理解遞歸的代碼流程,因為,可以說這是后面大多數題目的基礎思維邏輯。
后面基本都會使用Python來進行代碼的邏輯實現,比較容易以及大眾,畢竟算法方面學習的是思想,至於怎么實現的,任何語言都可以對其進行復現
今天是「樹」的遍歷,咱們先來定義一個樹的結構類,以及一顆完整的二叉樹!
# 樹結點類
class TreeNode:
def __init__(self, value):
self.value = value
self.left = None
self.right = None
構建一棵完整的二叉樹:
if __name__ == "__main__":
# 新建節點
root = TreeNode('A')
node_B = TreeNode('B')
node_C = TreeNode('C')
node_D = TreeNode('D')
node_E = TreeNode('E')
node_F = TreeNode('F')
node_G = TreeNode('G')
node_H = TreeNode('H')
node_I = TreeNode('I')
# 構建二叉樹
# A
# / \
# B C
# / \ / \
# D E F G
# / \
# H I
root.left, root.right = node_B, node_C
node_B.left, node_B.right = node_D, node_E
node_C.left, node_C.right = node_F, node_G
node_D.left, node_D.right = node_H, node_I
首先有一個小提醒:
今天的代碼會使用棧或者隊列來輔助實現,在 Python 中,這里使用 list 來操作
# 棧
stack = []
# 棧 - 壓棧
stack.append('結點')
# 棧 - 彈出棧頂元素
stack.pop()
# 隊列
queue = []
# 棧 - 入隊
queue.append('結點')
# 棧 - 出隊
queue.pop(0)
甜點
很甜,試着深入理解遞歸
遞歸在很多人看來不容易理解,尤其是處於學生時期的同學,以及一些初學者。其實很多工作幾年的人也不是太容易理解遞歸,而且遞歸有時候真的會很不容易解釋,非得自己去想清楚才能真正轉化為自己的一個思維邏輯。
這里我想試着說說看,能不能說清楚,咳、、、盡量吧...
咱們這里用后續遍歷舉例子,其他遞歸方式自己燒腦理解哈!
核心代碼(以下用代碼1、2、3、4來表示每一行):
def post_order_traverse(root):
代碼1 | if not root: return
代碼2 | post_order_traverse(root.left)
代碼3 | post_order_traverse(root.right)
代碼4 | print(root.value, end=" ")
當執行到圖中步驟 1 的時候,一定是執行了代碼1和代碼2,遞歸調用到最后,判斷結點H左右孩子都為空,執行了 if not root: return
,隨后又執行了代碼4,將結點 H 打印了出來。
同理,當執行到圖中的步驟 2 的時候,也是相同的邏輯,遞歸調用,判斷兩個孩子都為空,直接返回,隨后將結點 I 打印了出來。
再往上,結點H 和 結點I 打印並且返回之后,進行回溯,將 結點D 進行打印
依次類推...
(上述描述理解起來還是不太容易,有需要討論的,下面直接加我微信,備注“LeetCode刷題”,我拉群里一起討論哈)
【我的二維碼】
一
先序遍歷
遞歸遍歷過程:
a. 訪問根節點;
b. 先序遍歷其左子樹;
c. 先序遍歷其右子樹;
然后就是一直遞歸下去,在訪問到節點的時候,可以進行節點的相關處理,比如說簡單的訪問節點值
下圖是一棵二叉樹,我們來手動模擬一下遍歷過程
按照上圖中描述,根據順序能夠得到它的一個先序遍歷的過程,得到先序遍歷序列:
A B D H I E C F G
復現上述邏輯:
class Solution:
def pre_order_traverse(self, root):
if not root:
return
print(root.value, end=" ")
self.pre_order_traverse(root.left)
self.pre_order_traverse(root.right)
在整個遞歸中,看似整齊,閱讀性極高的 3 行代碼,其實對於初學者來說,腦子里理解它的的實現流程是比較困難的!
如果不太清晰,建議深入理解上面給到的【甜點】,用心理解,不懂的可以群里直接討論哈!
下面再看看非遞歸的遍歷過程:
a. 訪問根結點。
b. 判斷是否有右孩子,如果有右孩子,壓棧
c. 判斷否則有左孩子,如果有左孩子,訪問它,否則,彈出棧頂元素
d. 循環執行 2 和 3
非遞歸的遍歷,重點在於利用棧來實現將稍后要訪問結點入棧,先遍歷根結點,再將右孩子入棧,最后訪問左孩子這樣的思想
class Solution:
def pre_order_traverse_no_recursion(self, root):
if not root:
return
stack = [root]
while stack:
print(root.value, end=" ") # 訪問根結點
if root.right:
stack.append(root.right) # 判斷是否有右孩子,如果有右孩子,壓棧
if root.left: # 判斷否則有左孩子,如果有左孩子,訪問它,否則,彈出棧頂元素
root = root.left
else:
root = stack.pop()
這種思路其實也是遞歸的變形,將遞歸中使用到的棧自己定義了出來。
二
中序遍歷
咱們還是先來遞歸的實現流程
a. 先序遍歷其左子樹;
b. 訪問根節點;
c. 先序遍歷其右子樹;
然后就是一直遞歸下去,在訪問到節點的時候,可以進行節點的相關處理,比如說簡單的訪問節點值
下圖是一棵二叉樹,我們來手動模擬一下中序遍歷過程
按照上述中序遍歷的遞歸過程,得到中序遍歷序列:
H D I B E A F C G
下面繼續用 Python 來復現上述邏輯:
class Solution:
def in_order_traverse(self, root):
if not root:
return
self.in_order_traverse(root.left)
print(root.value, end=" ")
self.in_order_traverse(root.right)
和先序遍歷很類似,只是把要被訪問結點的 print 語句進行了位置置換。
下面再來看中序遍歷的非遞歸過程:
a. 當遍歷到一個結點時,就壓棧,然后繼續去遍歷它的左子樹;
b. 當左子樹遍歷完成后,從棧頂彈出棧頂元素(左子樹最后一個元素)並訪問它;
c. 最后按照當前指正的右孩子繼續中序遍歷,若沒有右孩子,繼續彈出棧頂元素。
class Solution:
def in_order_traverse_no_recursion(self, root):
if not root:
return
stack = []
while stack or root:
while root:
stack.append(root)
root = root.left
if stack:
tmp = stack.pop()
print(tmp.value, end=" ")
root = tmp.right
相信上述的 3 個步驟已經說的足夠清楚了,但是還是用更加朴素的語言簡單描述一下:
中序遍歷的非遞歸過程也是利用了一個「棧」來實現,由於是中序遍歷,那么首先要訪問左孩子,進而一定要把每個子結構的根結點入棧,然后訪問左孩子,彈出棧頂元素(訪問根結點),再進行訪問右孩子,訪問右孩子的時候,繼續將每個子結構的根結點入棧,然后訪問左孩子...這樣循環下去,直到棧為空或者指向的根結點為空。
三
后續遍歷
依然先用遞歸來實現
a. 先序遍歷其左子樹;
b. 先序遍歷其右子樹;
c. 訪問根節點;
然后就是一直遞歸下去,在訪問到節點的時候,可以進行節點的相關處理,比如說簡單的訪問節點值
下圖是一棵二叉樹,我們來手動模擬一下后序遍歷過程
按照上述后序遍歷的過程,得到后序遍歷序列:
H I D E B F G C A
咱們用代碼來實現一下邏輯:
class Solution:
def post_order_traverse(self, root):
if not root:
return
self.post_order_traverse(root.left)
self.post_order_traverse(root.right)
print(root.value, end=" ")
依然是很簡潔,依然是將訪問結點的代碼語句的位置進行了調整。
下面來輪到非遞歸來實現的流程
后續遍歷的非遞歸過程比較曲折,后續遍歷需要先訪問左右子結點后,才能訪問該結點,而這也是非遞歸的難點所在。可以使用一個輔助棧來實現,但理解起來沒有使用 2 個棧實現起來清晰,今天就用 2 個棧來實現非遞歸的后續遍歷。
借助2個棧:s1 和 s2
a. 初始化根結點到s1中
b. 將 s1 棧頂元素 T 彈出,到棧 s2 中
c. 判斷 T 是否有左右孩子,如果有依次入棧 s1,否則,執行 b
下面借助圖,還是一樣的樹結構,來梳理一下思路(長圖發放,耐心看完,看完之后會發現思路很清晰):
有了這個思路就應該會很清晰了,下面就按照這個思維邏輯來編寫代碼:
class Solution:
def post_order_traverse_no_recursion1(self, root):
s1, s2 = [], []
s1.append(root) # 初始化根結點到S1中
while s1:
T = s1.pop() # 將 S1 棧頂元素 T 彈出,到棧 S2 中
s2.append(T)
if T.left: # 判斷 T 是否有左右孩子,如果有依次入棧 s1
s1.append(T.left)
if T.right:
s1.append(T.right)
while s2:
print(s2.pop().value, end=" ")
看起來 2 個棧像是在忽悠人,其實思路很清晰,代碼很容易就實現了!
四
層次遍歷
層次遍歷屬於 BFS 的范疇,層次遍歷就是按照「樹」的層級進行每一層的掃盪。
遍歷從根結點開始,首先將根結點入隊,然后開始執行循環:
- 將頭結點入隊
- 彈出隊首元素,如果被彈出的隊首元素有左右孩子,將它們依次入隊
- 循環第 2 直到隊列為空
下面借助一幅圖來描述其遍歷過程:
這樣是不是很清晰,有時候會覺得這種長圖會比動圖好看一些,能清晰看到每一步,而且中間可以有很詳細的解釋。關於圖像展示方面大家可以給出參考意見,這方面確實可以更進一步。
先看代碼吧:
class Solution:
def level_order_traverse(self, head):
if not head:
return
queue = [head]
while len(queue) > 0:
tmp = queue.pop(0)
print(tmp.value, end=" ")
if tmp.left:
queue.append(tmp.left)
if tmp.right:
queue.append(tmp.right)
今天全部描述完畢!
最后
1.深入理解遞歸,一定一定多思考,咳咳、、我都上了每天上午10點的鬧鈴了(書讀百遍,其義自見);
2.「樹」的非遞歸遍歷所引導的思維方式很重要;
3.下期進行【基礎遍歷】中LeetCode題目羅列以及利用樹遞歸的方式,會產生一些計算樹相關的變形問題
代碼和本文的文檔都在 https://github.com/xiaozhutec/share_leetcode , 需要的小伙伴可以自行下載代碼運行跑起來!有空可以幫我點點 star。謝過大家!