數據工程師慣用python,然而數據結構仍是c++或者java比較經典。這就形成不少朋友並不太習慣。本文就從《劍指offer》這本書中的經典題型出發,來研究探討一下python 刷數據結構題型的一些慣用思路。java
可能有過幾年編程經驗的朋友們對於一些經常使用的程序瞭如指掌,卻老是以爲二叉樹,鏈表,堆棧這些略微遙遠。可是想要進階卻不得不跨過這道坎。那麼本文重點研究一下二叉樹的一些經常使用思路。node
二叉樹,通俗一點來講就是至多有兩個子結點的樹結構。結構並不複雜,可是爲何要使用這樣一個結構呢?簡單來比較一下在此以前的一些數據基本結構。python
計算機內部的儲存一直沒有算法更加受人矚目,由於咱們能夠將其看成黑盒來使用。可是我這裏要提一句,由於刷題的時候會有測試樣例,若是咱們不知道二叉樹是如何存在數據裏的,就不可理解測試樣例是什麼意思。c++
順序儲存:將二叉樹逐層從左至右自上而下遍歷,若是不是徹底二叉樹,有殘缺結點的話就補上‘#’。咱們來看下面這張圖: 算法
"ABD#C#E######F"
,值得注意的是,因爲F
爲最後一個結點了,因此其後的空結點並不寫出來。 鏈表式儲存:將二叉樹按照鏈表的形式儲存起來。這是最經常使用的儲存方法。在這種儲存方法中,二叉樹和鏈表同樣,是一種python的對象。對象內包括左子結點的指針,右子結點的指針,還有本身所表明的數值。咱們經常使用的定義鏈表式二叉樹代碼:編程
class TreeNode:
def __init__(self, x):
self.val = x
self.left = None
self.right = None
複製代碼
遍歷指的是咱們訪問且不重複訪問二叉樹的全部結點。通常經常使用的有三種:前序遍歷,中序遍歷和後序遍歷。簡單介紹下:數組
也可知,咱們經過某一給定的二叉樹,能夠獲得惟一的前,中,後序遍歷序列,可是反之,咱們只有三種遍歷序列中的一個時,並不能恢復成惟一的二叉樹,想要恢復,至少須要關於改二叉樹的兩個遍歷序列。舉個例子,只給定上圖的後序遍歷也能夠恢復成這樣: bash
題目內容:輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。例如輸入前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6},則重建二叉樹並返回。數據結構
思考:由前文可知,只知道前,中,後序遍歷中的某一種咱們是沒法恢復惟一的二叉樹的。而對於已知兩種遍歷,咱們能夠逐步分析出二叉樹的各類細節。app
咱們分析前序序列,前序序列的第一個數字是根節點的值,而中序序列根節點在序列中間。
如上圖,根據root結點咱們能夠分出左右子樹。則對左右子樹重複以上操做,即可獲得最終的二叉樹結構。這裏咱們有重複某一操做的操做,針對這種狀況,咱們經常使用遞歸方法。
遞歸法重建二叉樹
# -*- coding:utf-8 -*-
# 被註釋的代碼表示了系統構建鏈表式二叉樹
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
# 這裏定義函數,兩個參數分別是前序和中序遍歷的序列,兩個數組
def reConstructBinaryTree(self, pre, tin):
# 如下兩個if是爲了判斷一下特殊狀況
# 可是其最重要的做用實際上是整個函數的終止條件
# 由於遞歸調用,函數嵌套着函數,在運行第一遍函數的時候是不能直接獲得結果的
# 遞歸過程當中,子樹被不斷細化,直到長度爲1或者0的時候,經過這兩個if獲得答案
# 而後從嵌套中一個一個解開,使得整個遞歸過程獲得最終解而且終止
if len(pre) == 0:
return None
if len(pre) == 1:
return TreeNode(pre[0])
else:
root = tin.index(pre[0]) # 按照前序遍歷找到中序遍歷的root結點
res = TreeNode(pre[0]) # 利用TreeNode類來將這個數組中的root結點初始化成二叉樹結點
res.left = self.reConstructBinaryTree(pre[1: root + 1], tin[: root])
# 遞歸調用此函數,不斷細化子樹,直到遍歷到最後一個結點,那麼遞歸過程的
# 每個結點會相應解開
res.right = self.reConstructBinaryTree(pre[root + 1: ], tin[root + 1: ])
return res
複製代碼
題目描述:輸入兩棵二叉樹A,B,判斷B是否是A的子結構。(ps:咱們約定空樹不是任意一個樹的子結構)如圖,第二棵樹爲第一棵樹的子樹。
思考:咱們是要判斷B是否是A的子結構,則B是子樹,A爲原樹。咱們能夠先找到B的根結點在A樹中的位置,而後再看A中該節點之下有沒有與B樹結構同樣的子樹。那麼這道題就被拆解成兩個比較簡單的子題目。
def HasSubtree(self, pRoot1, pRoot2):
# 判斷一下特殊狀況
if pRoot1 == None or pRoot2 == None:
return False
result = False
# 此爲遞歸終止條件,函數沿着樹去尋找,直到符合此條件才返回值。
# 找到了就返回此結點,注意A和B的地位不同,因此返回pRoot1和pRoot2能夠考慮下
if pRoot1.val == pRoot2.val:
result = self.isSubtree(pRoot1, pRoot2)
if result == False:
# 沒找到的話,就向左子結點重複以上過程
result = self.HasSubtree(pRoot1.left, pRoot2)
if result == False:
# 左子結點尚未才找右邊的。能夠感覺一下這個順序
result = self.HasSubtree(pRoot1.right, pRoot2)
return result
複製代碼
def isSubtree(self, pRoot1, pRoot2):
# 這裏咱們要注意,因爲B是子,A是原,因此這兩個地位不同
# 區別能夠體如今Root1和Root2兩個結點的判斷上,具體看如下兩個if
if pRoot2 is None:
return True
if pRoot1 is None:
return False
if pRoot1.val != pRoot2.val:
return False
# 注意此條,兩個都爲真,才返回真。
return self.isSubtree(pRoot1.left, pRoot2.left) and self.isSubtree(pRoot1.right, pRoot2.right)
複製代碼
對這兩個部分都熟悉以後,咱們只須要拼起來就能夠了。如下爲完整代碼。
遞歸法判斷二叉樹的子結構
# -*- coding:utf-8 -*-
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def HasSubtree(self, pRoot1, pRoot2):
# write code here
if pRoot1 == None or pRoot2 == None:
return False
result = False
if pRoot1.val == pRoot2.val:
result = self.isSubtree(pRoot1, pRoot2)
if result == False:
result = self.HasSubtree(pRoot1.left, pRoot2)
if result == False:
result = self.HasSubtree(pRoot1.right, pRoot2)
return result
def isSubtree(self, pRoot1, pRoot2):
if pRoot2 is None:
return True
if pRoot1 is None:
return False
if pRoot1.val != pRoot2.val:
return False
return self.isSubtree(pRoot1.left, pRoot2.left) and self.isSubtree(pRoot1.right, pRoot2.right)
複製代碼
題目描述:操做給定的二叉樹,將其變換爲源二叉樹的鏡像。
源二叉樹
8
/ \
6 10
/ \ / \
5 7 9 11
鏡像二叉樹
8
/ \
10 6
/ \ / \
11 9 7 5
複製代碼
思考:看着官方給的鏡像實例,咱們可知,只須要將根節點下的每個左右子樹都調換一下就ok。那也一樣的,咱們使用遞歸,遍歷到全部結點,並對其調換左右子結點。這裏咱們要注意到可能有左右子結點不徹底的問題,不過沒有關係,空結點也能夠調換的。因此只需考慮root結點是否爲空這一個特殊條件就能夠了。
二叉樹的鏡像
# -*- coding:utf-8 -*-
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
# 返回鏡像樹的根節點
def Mirror(self, root):
if root == None:
return None
# python的調換語句倒不須要c那種的temp
# 其實最標準的這裏應該加上判斷若是left和right是否有值,
# 可是一樣的,空結點也能夠調換,因此只考慮根節點是否爲空。
root.left, root.right = root.right, root.left
if root.left:
# 對每一個子結點還進行一樣的遞歸操做便好
self.Mirror(root.left)
if root.right:
self.Mirror(root.right)
複製代碼
題目描述:從上往下打印出二叉樹的每一個節點,同層節點從左至右打印。
思考:這道題其實跟咱們二叉樹的順序儲存很相似,能夠看做是二叉樹的鏈表儲存對順序儲存方式的轉化吧。以下圖所示,咱們打印出來的爲【A,B,C,D,E,F,G】,咱們先從根節點開始,接下來打印根節點的兩個子結點,第三步得從第二層的每一個子結開始,分別打印每個子結點的左右子結點,如此循環。這裏咱們很容易看出,咱們得保存每一層的全部結點,做爲下一次遍歷時候使用。
循環實現從上往下打印二叉樹
# -*- coding:utf-8 -*-
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def PrintFromTopToBottom(self, root):
# write code here
if not root:
return []
list = [] # list 做爲最終遍歷結果保存的數組
level = [root] # level做爲保存每層root結點的數組
while level:
newlevel = []
for i in level:
if i.left:
newlevel.append(i.left)
if i.right:
newlevel.append(i.right)
list.append(i.val)
level = newlevel
# 咱們會發現level所表示的比list要更早一層
return list
複製代碼
這裏不少網上教程使用隊列,可能有些不太熟悉的朋友以爲很高級。其實很簡單,就一直將上一層的結點彈出棧以後再將下一層的壓入棧,跟咱們這種將數組置空再添下一層的作法其實殊途同歸。
這一題用循環簡潔明瞭。遞歸調用的話理解略微繁瑣。由於遞歸調用很容易向深處遍歷,而非在一個層。這就不太適合這道題的解法。固然,循環與遞歸本是同根生,我寫下遞歸的方法給你們參考一下。
思路是這樣:遞歸方法雖然喜歡向深處遍歷,可是咱們給每一個結點都打上深度的標籤,而後用二維數組將這些帶深度標籤的按深度歸類,是否是就能夠解決了。
遞歸法打印二叉樹
# -*- coding:utf-8 -*-
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
# 返回從上到下每一個節點值列表,例:[1,2,3]
def __init__(self):
self.res = list()
def Traversal(self, node, depth=0):
if not node:
return True
# 咱們這裏設置一個depth的概念,表明樹的深度。這裏咱們可知,根節點是第1層,可是depth = 0
# 因此發現depth老是小於len(self.res)的。一旦這二者相等,咱們即可知,上一層已經滿了,這個depth表明的是新的一層
# 咱們就須要在self.res中開一個新的數組了
if len(self.res) == depth:
self.res.append([node.val])
else:
self.res[depth].append(node.val)
# 咱們這裏return的值是一個boolean類型的,可是咱們不用去管這個具體是什麼。咱們須要的是self.res
# 這裏傳參的時候至關於給每一個結點都附上了depth值。
return self.Traversal(node.left, depth+1) and self.Traversal(node.right, depth+1)
def PrintFromTopToBottom(self, root):
# 咱們調用一下這個Traversal函數即可獲得咱們的最終序列,
# 可是這個序列裏面每一個元素都是一層數值的list,因此還得拆解一下
self.Traversal(root)
list = []
for level in self.res:
for num in level:
list.append(num)
return list
複製代碼
題目描述:請實現一個函數按照之字形打印二叉樹,即第一行按照從左到右的順序打印,第二層按照從右至左的順序打印,第三行按照從左到右的順序打印,其餘行以此類推。如圖之字形遍歷爲【A,B,C,F,E,D】
思考:這題有上面的題做爲基礎,就很容易了。遍歷好以後將奇數層反轉一下就能夠。
# -*- coding:utf-8 -*-
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def Print(self, pRoot):
res = list()
def Traversal(node, depth=0):
if not node:
return True
if len(res) == depth:
res.append([node.val])
else:
res[depth].append(node.val)
return Traversal(node.left, depth+1) and Traversal(node.right, depth+1)
Traversal(pRoot)
# 上面那部分和打印二叉樹題目是同樣的,接下來進行一個判斷奇數層取反轉的操做
for i, v in enumerate(res):
if i % 2 == 1:
v.reverse()
return res
複製代碼
題目描述:輸入一棵二叉樹,求該樹的深度。從根結點到葉結點依次通過的結點(含根、葉結點)造成樹的一條路徑,最長路徑的長度爲樹的深度。
思考:由以前的打印二叉樹,咱們能夠漸漸感受到遞歸調用對於二叉樹來講,很容易向深處遍歷。意思就是遞歸函數的嵌套,只會沿着指針前進的方向,而指針一直指向更深層的樹中去。那麼這一題求深度,就將遞歸的優點展示的淋漓盡致了。
那咱們就將每個分支都給遍歷到,求最深的便可。咱們遞歸能夠作到,對於每個結點,都給判斷一下左右結點所處深度,而後返回最大的。而循環在這一題中,很容易有重複冗餘的操做,劣勢顯現。咱們後續能夠比較一下。
遞歸求二叉樹深度
# -*- coding:utf-8 -*-
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def TreeDepth(self, pRoot):
if pRoot == None:
return 0
# 這裏只管遍歷
ldepth = self.TreeDepth(pRoot.left)
rdepth = self.TreeDepth(pRoot.right)
# 深度+1的操做在return時候實現,每次遍歷的時候都取一下左右節點最深的那個
# 注意初始的狀態,防止算出的值少了一層。
return max(ldepth, rdepth) + 1
複製代碼
遞歸方法十分簡潔,與之比較的是循環法。循環的思路是這樣子的。我和打印二叉樹同樣,將二叉樹的每一層都存在數組裏。而後看遍歷完之後數組中有多少層,那就是二叉樹的深度。代碼以下:
循環法求二叉樹深度
# -*- coding:utf-8 -*-
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
# 層次遍歷
def levelOrder(self, root):
# 存儲最後層次遍歷的結果
res = [] # 層數
count = 0 # 若是根節點爲空,則返回空列表
if root is None:
return count
q = [] # 模擬一個隊列儲存節點
q.append(root) # 首先將根節點入隊
while len(q) != 0: # 列表爲空時,循環終止
tmp = [] # 使用列表存儲同層節點
length = len(q) # 記錄同層節點的個數
for i in range(length):
r = q.pop(0) # 將同層節點依次出隊
if r.left is not None:
q.append(r.left) # 非空左孩子入隊
if r.right is not None:
q.append(r.right) # 非空右孩子入隊
tmp.append(r.val)
if tmp:
count += 1 # 統計層數
res.append(tmp)
return count
def TreeDepth(self, pRoot):
# 使用層次遍歷 當樹爲空直接返回0
if pRoot is None:
return 0
count = self.levelOrder(pRoot)
return count
複製代碼
這裏我使用的是隊列,層次遍歷,原理是每開始遍歷下一層時都將上一層的結點彈出棧,將下一層的結點壓入棧。咱們也可使用以前的每一層置空,而後從新添加結點的方法來作。
固然,兩種方法,孰優孰劣一眼便知。
遞歸和循環,在通常狀況下都是能夠轉化使用的。也就是遞歸能夠用循環來代替,反之亦然。只是二者寫法可能複雜性天壤之別。咱們經過這兩道題能夠感覺一下。何時遞歸好用,何時循環好用,內心得有感受。
題目要求:輸入一棵二叉樹,判斷該二叉樹是不是平衡二叉樹。(平衡二叉樹的定義見上文基礎部分)
思考:咱們由平衡二叉樹的定義可知,咱們若是知道左右子結點分別的深度就好辦了。相差不大於1,判斷一下就能夠解決。
平衡二叉樹
# -*- coding:utf-8 -*-
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
# 定義這個函數是爲了求出子結點如下的深度
def TreeDepth(self, pRoot):
if pRoot == None:
return 0
ldepth = self.TreeDepth(pRoot.left)
rdepth = self.TreeDepth(pRoot.right)
return max(ldepth, rdepth) + 1
def IsBalanced_Solution(self, pRoot):
# write code here
if pRoot == None:
return True
# 求一下兩邊的深度
ldepth = self.TreeDepth(pRoot.left)
rdepth = self.TreeDepth(pRoot.right)
# 判斷一下深度差
if ldepth-rdepth>1 or rdepth-ldepth>1:
return False
else:
# 繼續往下遍歷
return self.IsBalanced_Solution(pRoot.left) and self.IsBalanced_Solution(pRoot.right)
複製代碼
題目描述:輸入一個整數數組,判斷該數組是否是某二叉搜索樹的後序遍歷的結果。若是是則輸出Yes,不然輸出No。假設輸入的數組的任意兩個數字都互不相同。
思考:後序遍歷中,最後一個數字是根結點的值,數組的前面數字部分可分爲兩部分,一部分是左子樹,一部分是右子樹。咱們根據二叉搜索樹的性質可知,左子樹的值要比根節點小,而右子樹全部值要比根節點大。以此遞歸來判斷全部子樹。
二叉搜索樹的後序遍歷序列
# -*- coding:utf-8 -*-
class Solution:
def VerifySquenceOfBST(self, sequence):
# write code here
if not sequence:
return False
else:
return self.verify(sequence)
def verify(self, sequence):
if not sequence:
return True
# 根節點就是最後一個樹,獲取一下這個值,而後從數組中彈出去
root = sequence.pop()
# 找一下左右子樹
index = self.findIndex(sequence, root)
# 細分到沒有子結點做爲終止條件
if not sequence[index:]:
return True
# 若是右邊數組最小值都大於root,則說明沒有問題。進一步細分
elif min(sequence[index:]) > root:
left = sequence[:index]
right = sequence[index:]
return self.verify(left) and self.verify(right)
return False
# 定義一個函數,來找到左右子樹的分界線
# 左子樹的值比根節點小,右子樹的值比根節點大,以此爲左右子樹的界限
def findIndex(self, sequence, root):
for i, seq in enumerate(sequence):
if sequence[i]>root:
return i
return len(sequence)
複製代碼
題目描述:輸入一顆二叉樹的跟節點和一個整數,打印出二叉樹中結點值的和爲輸入整數的全部路徑。路徑定義爲從樹的根結點開始往下一直到葉結點所通過的結點造成一條路徑。(注意: 在返回值的list中,數組長度大的數組靠前)
二叉樹和爲某一固定值的路徑
# -*- coding:utf-8 -*-
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
# 返回二維列表,內部每一個列表表示找到的路徑
def __init__(self):
# 這裏咱們定義兩個列表。
# list表明的是遍歷過程當中結點的儲存狀況,這裏面結點隨時有變化,但不必定符合要求
# wholelist表明的是最後符合要求的路徑儲存的列表
self.list = []
self.wholeList = []
def FindPath(self, root, expectNumber):
if root == None:
return self.wholeList
# 將結點值加進來,目標值隨着結點值遞減
self.list.append(root.val)
expectNumber = expectNumber - root.val
if expectNumber == 0 and root.left == None and root.right == None:
newList = [] # newlist表達的是在遍歷過程當中符合要求的一條路徑,要被加進wholelist裏面
for i in self.list:
newList.append(i)
# 這裏不直接加self.list,
self.wholeList.append(newList)
# 向左右子結點遍歷
self.FindPath(root.left, expectNumber)
self.FindPath(root.right, expectNumber)
# 這裏就是不符合要求的了,返回父節點以前,刪除當前結點。
self.list.pop()
return self.wholeList
複製代碼
題目描述:請實現一個函數,用來判斷一顆二叉樹是否是對稱的。注意,若是一個二叉樹同此二叉樹的鏡像是一樣的,定義其爲對稱的。
對稱二叉樹
# -*- coding:utf-8 -*-
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def isSymmetrical(self, pRoot):
# write code here
if pRoot == None:
return True
return self.isSym(pRoot.left, pRoot.right)
def isSym(self,left,right):
if left == None and right == None:
return True
if left == None or right == None:
return False
# 重點就在這裏,判斷左右是否一致
if left.val == right.val:
# 一致的話返回 左的左 和 右的右 和 左的右 與 右的左
return self.isSym(left.left, right.right) and self.isSym(left.right, right.left)
複製代碼