二叉樹的遍歷是在面試使比較常見的項目了。對於二叉樹的前中后層序遍歷,每種遍歷都可以遞歸和循環兩種實現方法,且每種遍歷的遞歸實現都比循環實現要簡潔。下面做一個小結。
一、中序遍歷
前中后序三種遍歷方法對於左右結點的遍歷順序都是一樣的(先左后右),唯一不同的就是根節點的出現位置。對於中序遍歷來說,根結點的遍歷位置在中間。
所以中序遍歷的順序:左中右
1.1 遞歸實現
每次遞歸,只需要判斷結點是不是None,否則按照左中右的順序打印出結點value值。
class Solution:
def inorderTraversal(self, root):
"""
:type root: TreeNode
:rtype: List[int]
"""
if not root:
return []
return self.inorderTraversal(root.left) + [root.val] + self.inorderTraversal(root.right)
1.2 循環實現
循環比遞歸要復雜得多,因為你得在一個函數中遍歷到所有結點。但是有句話很重要:
對於中序遍歷的循環實現,每次將當前結點(curr)的左子結點push到棧中,直到當前結點(curr)為None。這時,pop出棧頂的第一個元素,設其為當前結點,並輸出該結點的value值,且開始遍歷該結點的右子樹。
例如,對於上圖的一個二叉樹,其循環遍歷過程如下表:
No. | 輸出列表sol | 棧stack | 當前結點curr |
---|---|---|---|
1 | [] | [] | 1 |
2 | [] | [1] | 2 |
3 | [] | [1,2] | 4 |
4 | [] | [1,2,4] | None |
5 | [4] | [1,2] | 4 -> None(4的右結點) |
6 | [4,2] | [1] | 2 -> 5 |
7 | [4,2] | [1,5] | None(5的左結點) |
8 | [4,2,5] | [1] | 5 -> None(5的右結點) |
9 | [4,2,5,1] | [] | 3 |
10 | [4,2,5,1] | [3] | None |
11 | [4,2,5,1,3] | [] | None |
可見,規律為:當前結點curr不為None時,每一次循環將當前結點curr入棧;當前結點curr為None時,則出棧一個結點,且打印出棧結點的value值。整個循環在stack和curr皆為None的時候結束。
class Solution:
def inorderTraversal(self, root):
stack = []
sol = []
curr = root
while stack or curr:
if curr:
stack.append(curr)
curr = curr.left
else:
curr = stack.pop()
sol.append(curr.val)
curr = curr.right
return sol
二、前序遍歷和后序遍歷
按照上面的說法,前序遍歷指根結點在最前面輸出,所以前序遍歷的順序是:中左右
后序遍歷指根結點在最后面輸出,所以后序遍歷的順序是:左右中
2.1 遞歸實現
遞歸實現與中序遍歷幾乎完全一樣,改變一下打印的順序即可:
class Solution:
def preorderTraversal(self, root): ##前序遍歷
"""
:type root: TreeNode
:rtype: List[int]
"""
if not root:
return []
return [root.val] + self.inorderTraversal(root.left) + self.inorderTraversal(root.right)
def postorderTraversal(self, root): ##后序遍歷
"""
:type root: TreeNode
:rtype: List[int]
"""
if not root:
return []
return self.inorderTraversal(root.left) + self.inorderTraversal(root.right) + [root.val]
改動的地方只有return時函數的打印順序。
2.2 循環實現
為什么把前序遍歷和后序遍歷放在一起呢?Leetcode上前序遍歷是medium難度,后序遍歷可是hard難度呢!
實際上,后序遍歷不就是前序遍歷的“反過程”嘛!
先看前序遍歷。我們仍然使用棧stack,由於前序遍歷的順序是中左右,所以我們每次先打印當前結點curr,並將右子結點push到棧中,然后將左子結點設為當前結點。入棧和出棧條件(當前結點curr不為None時,每一次循環將當前結點curr入棧;當前結點curr為None時,則出棧一個結點)以及循環結束條件(整個循環在stack和curr皆為None的時候結束)與中序遍歷一模一樣。
再看后序遍歷。由於后序遍歷的順序是左右中,我們把它反過來,則遍歷順序變成中左右,是不是跟前序遍歷只有左右結點的差異了呢?然而左右的差異僅僅就是.left和.right的差異,在代碼上只有機械的差別。
我們來看代碼:
class Solution:
def preorderTraversal(self, root): ## 前序遍歷
stack = []
sol = []
curr = root
while stack or curr:
if curr:
sol.append(curr.val)
stack.append(curr.right)
curr = curr.left
else:
curr = stack.pop()
return sol
def postorderTraversal(self, root): ## 后序遍歷
stack = []
sol = []
curr = root
while stack or curr:
if curr:
sol.append(curr.val)
stack.append(curr.left)
curr = curr.right
else:
curr = stack.pop()
return sol[::-1]
代碼的主體部分基本就是.right和.left交換了順序,且后序遍歷在最后輸出的時候進行了反向(因為要從中右左變為左右中)
三、層序遍歷
層序遍歷也可以叫做寬度優先遍歷:先訪問樹的第一層結點,再訪問樹的第二層結點...然后一直訪問到最下面一層結點。在同一層結點中,以從左到右的順序依次訪問。
3.1 遞歸實現
遞歸函數需要有一個參數level,該參數表示當前結點的層數。遍歷的結果返回到一個二維列表sol=[[]]中,sol中的每一個子列表保存了對應index層的從左到右的所有結點value值。
class Solution:
def levelOrder(self, root):
"""
:type root: TreeNode
:rtype: List[List[int]]
"""
def helper(node, level):
if not node:
return
else:
sol[level-1].append(node.val)
if len(sol) == level: # 遍歷到新層時,只有最左邊的結點使得等式成立
sol.append([])
helper(node.left, level+1)
helper(node.right, level+1)
sol = [[]]
helper(root, 1)
return sol[:-1]
PS:
Q:如果仍然按層遍歷,但是每層從右往左遍歷怎么辦呢?
A:將上面的代碼left和right互換即可
Q:如果仍然按層遍歷,但是我要第一層從左往右,第二層從右往左,第三從左往右...這種zigzag遍歷方式如何實現?
A:將sol[level-1].append(node.val)
進行一個層數奇偶的判斷,一個用append()
,一個用insert(0,)
if level%2==1:
sol[level-1].append(node.val)
else:
sol[level-1].insert(0, node.val)
3.2 循環實現
這里的循環實現不能用棧了,得用隊列queue。因為每一層都需要從左往右打印,而每打印一個結點都會在隊列中依次添加其左右兩個子結點,每一層的順序都是一樣的,故必須采用先進先出的數據結構。
以下代碼的打印結果為一個一維列表,沒有采用二維列表的形式。
class Solution:
def levelOrder(self, root):
if not root:
return []
sol = []
curr = root
queue = [curr]
while queue:
curr = queue.pop(0)
sol.append(curr.val)
if curr.left:
queue.append(curr.left)
if curr.right:
queue.append(curr.right)
return sol
其實,如果需要打印成zigzag形式(相鄰層打印順序相反),則可以采用棧stack數據結構,正好符合先進后出的形式。不過在代碼上還要進行其他改動。