二叉樹類型的題目爲常考題型
1、能夠結合隊列、棧、鏈表、字符串等很多數據結構。
2、需要掌握圖的基本遍歷方式,比如BFS和DFS。
3、需要掌握遞歸函數的使用,並自己設計出遞歸過程。
4、與實際工作結合緊密。
用遞歸和非遞歸的形式分別實現二叉樹的先序、中序和後序的遍歷打印。
實現代碼:
# -*- coding:utf-8 -*-
#打印二叉樹
#實現功能:
# 樹的構造
# 遞歸實現先序遍歷,中序遍歷,後序遍歷
# 堆棧實現先序遍歷,中序遍歷,後序遍歷
# 隊列實現層次遍歷
class Node():
#節點類
def __init__(self,data = -1):
self.data = data
self.left = None
self.right = None
class Tree():
#樹類
def __init__(self):
self.root = Node()
def add(self,data):
# 爲樹加入節點
node = Node(data)
if self.root.data == -1: #如果樹爲空,就對根節點賦值
self.root = node
else:
myQueue = []
treeNode = self.root
myQueue.append(treeNode)
while myQueue: #對已有的節點進行層次遍歷
treeNode = myQueue.pop(0)
if not treeNode.left:
treeNode.left = node
return
elif not treeNode.right:
treeNode.right = node
return
else:
myQueue.append(treeNode.left)
myQueue.append(treeNode.right)
def pre_order_recursion(self,root): #遞歸實現前序遍歷
if not root:
return
print root.data,
self.pre_order_recursion(root.left)
self.pre_order_recursion(root.right)
#非遞歸實現前序遍歷
# 1、首先申請一個新的棧,記爲stack。
# 2、然後將頭即誒安head壓入stack中。
# 3、每次從stack中彈出棧頂節點,記爲cur,然後打印cur節點的值。如果cur右孩子不爲空的話,
# 將cur的右孩子先壓入stack中。最後如果cur的左孩子不爲空的話,將cur的左孩子壓入stack中。
# 4、不斷重複步驟3,直到stack爲空,全部過程結束。
def pre_order_stack(self,root): #堆棧實現前序遍歷
if not root:
return
myStack = []
node = root
while myStack or node:
while node: #從根節點開始,一直尋找他的左子樹
print node.data,
myStack.append(node)
node = node.left
node = myStack.pop() #while結束表示當前節點node爲空,即前一個節點沒有左子樹了
node = node.right #開始查看它的右子樹
def in_order_recursion(self,root): #遞歸實現中序遍歷
if not root:
return
self.in_order_recursion(root.left)
print root.data,
self.in_order_recursion(root.right)
#非遞歸實現中序遍歷
# 1、申請一個新的棧,記爲stack,申請一個變量cur,初始時令cur等於頭節點。
# 2、先把cur節點壓入棧中,對以cur節點爲頭的政客子樹來說,一次把整棵樹的左邊界壓入棧中,
# 即不斷令cur = vur.left,然後重複步驟2。
# 3、不斷重複步驟2,直到發現cur爲空,此時stack中彈出一個節點,記爲node。
# 打印node的值,並讓cur = node.right,然後繼續重複步驟2
# 4、當stack爲空並且cur爲空時,整個過程結束。
def in_order_stack(self,root): #堆棧實現中序遍歷
if not root:
return
myStack = []
node = root
while myStack or node: #從根節點開始,一直尋找它的左子樹
while node:
myStack.append(node)
node = node.left
node = myStack.pop()
print node.data,
node = node.right
def post_order_recursion(self,root): #遞歸實現後序遍歷
if not root:
return
self.post_order_recursion(root.left)
self.post_order_recursion(root.right)
print root.data,
#非遞歸實現後序遍歷(前序遍歷的逆序)
# 1、申請一個棧,記爲s1,然後將頭節點壓入s1中。
# 2、從s1中彈出的節點記爲cur,然後先把cur的左孩子壓入s1中,然後把cur的左孩子壓入s1中,然後把cur1的右孩子壓入s1中。
# 3、在整個過程中,每一個從s1中彈出的節點都放進第二個棧s2中。
# 4、不斷重複步驟2和步驟3,直到s1爲空,過程停止。
def post_order_stack(self, root): # 堆棧實現後序遍歷
# 先遍歷根節點,再遍歷右子樹,最後是左子樹,這樣就可以轉化爲和先序遍歷一個類型了,最後只把遍歷結果逆序輸出就OK了。
if not root:
return
myStack1 = []
myStack2 = []
node = root
while myStack1 or node:
while node:
myStack2.append(node)
myStack1.append(node)
node = node.right
node = myStack1.pop()
node = node.left
while myStack2:
print myStack2.pop().data,
def level_order_queue(self,root):
if not root :
return
myQueue = []
node = root
myQueue.append(node)
while myQueue:
node = myQueue.pop(0)
print node.data,
if node.left:
myQueue.append(node.left)
if node.right:
myQueue.append(node.right)
if __name__ == '__main__':
#主函數
datas = [2,3,4,5,6,7,8,9]
tree = Tree() #新建一個樹對象
for data in datas:
tree.add(data) #逐個加入樹的節點
print '遞歸實現前序遍歷:'
tree.pre_order_recursion(tree.root)
print '\n堆棧實現前序遍歷'
tree.pre_order_stack(tree.root)
print "\n\n遞歸實現中序遍歷:"
tree.in_order_recursion(tree.root)
print "\n堆棧實現中序遍歷:"
tree.in_order_stack(tree.root)
print '\n\n遞歸實現後序遍歷:'
tree.post_order_recursion(tree.root)
print '\n堆棧實現後序遍歷:'
tree.post_order_stack(tree.root)
print '\n\n隊列實現層次遍歷:'
tree.level_order_queue(tree.root)
經典案例:
案例一
二叉樹的子樹
在二叉樹中以任何任何一個節點爲頭部的整棵樹稱做二叉樹的子樹。
案例二:
平衡二叉樹(AVL樹)
1、空樹是平衡二叉樹
2、如果一棵樹不爲空,並且其中所有的子樹都滿足各自的左子樹與右子樹的高度差都不超過1。
案例三:
二叉樹的序列化和反序列化
1、二叉樹----->字符串(序列化)
2、字符串----->二叉樹(反序列化)
序列化的方式:
根據先序遍歷序列化
根據中序遍歷序列化
根據後序遍歷序列化
按層序列化
這裏以先序遍歷爲例子:
- 假設序列化結果爲str,初始str爲空字符串
- 先序遍歷二叉樹時如果遇到空節點,在str末端加上"#!"
- 如果遇到不爲空的節點,假設節點值爲3,就在str的末尾加上"3!"
(這裏加上!的原因是爲了防止產生歧義,比如說12和1,2,序列化後都是12)
反序列化:
- 將字符串轉化爲數組
- 選擇用什麼樣的遍歷方式序列化,就選擇用什麼樣的方式反序列化
- 一棵樹序列化的結果是唯一的,唯一的結果生成的二叉樹也是唯一的
按層遍歷的方式對二叉樹進行序列化
- 用隊列進行二叉樹的按層遍歷,即寬度優先遍歷。
- 除了訪問節點的順序是按層遍歷之外,對結果字符串的處理,與之前介紹的處理方式一樣。
案例四:
給定一棵二叉樹的頭節點head,判斷一棵樹是否是平衡二叉樹。
思路:
1、左子樹是否爲平衡二叉樹(true or false)--如果左子樹爲false,則直接返回false
2、左子樹最深到哪一層,LH
3、右子樹是否爲平衡二叉樹(true or false)
4、右子樹最深到哪一層,RH
5、如果左子樹和右子樹都是平衡二叉樹,則比較LH和RH
案例五:
搜索二叉樹
特徵:
每棵樹的頭節點的值都比各自左子樹上的所有節點值要大,也都比各自右子樹上的所有節點值要小。
搜索二叉樹按照中序遍歷得到的序列,一定是從小到大排列的。(也可以利用這一性質判斷一棵樹是否爲平衡二叉樹)
紅黑樹、平衡搜索二叉樹(AVL樹)等,其實都是搜索二叉樹的不同實現。
給定一棵二叉樹的頭節點head,請判斷這棵樹是否爲搜索二叉樹。
思路:
1、改寫二叉樹的中序遍歷
2、遍歷到每個節點的值時,如果一直比上一個遍歷的節點值要大,則時搜索二叉樹。否則,不是搜索二叉樹。
3、爲了方便同時得到當前節點,和上一個遍歷的節點,二叉樹中序遍歷非遞歸的實現比較合適。
案例六:
滿二叉樹和完全二叉樹
滿二叉樹是除了最後一層的節點無任何子節點外,剩下每一層上的節點都有兩個子節點。
滿二叉樹的層數即爲L,節點樹即爲N,則N= 2^L - 1 , L = log2 ^(N+1)
完全二叉樹
完全二叉樹是指除了最後一層外,其他每一層的節點數都是滿的。最後一層如果也滿了,是一棵滿二叉樹,也是完全二叉樹。最後一層如果不滿,缺少的節點也全部的幾種在右邊,那也是一棵完全二叉樹。
以上三棵樹均爲完全二叉樹,其中第一個也爲滿二叉樹
給定一棵二叉樹的頭節點head,判斷這棵樹是否爲完全二叉樹?
思路:
1、採用層次遍歷二叉樹的方式,從每層的左邊向右邊一次遍歷所有的節點。
2、如果當前節點有右孩子,但是沒有左孩子,直接返回false。
3、如果當前節點並不是左右孩子全有,那之後的節點必須都爲葉節點,否則返回false。
4、遍歷過程如果不返回false,遍歷結束返回true即可。
注意:
面試中,二叉樹節點類型僅包括:數據項、左孩子、右孩子。
工程上的二叉樹節點類型,往往多一條指向父節點的指針。
一般默認面試中的二叉樹節點結構不包括指向父節點的指針,除非特別說明。
案例七:
後繼節點和前驅節點:
後繼節點:一個節點的後繼節點是指,這個節點在中序遍歷序列中的下一個節點。
中序遍歷的序列爲:DBEAFCG.
現在有一種新的二叉樹節點類型,定義如大家所見。
該結構比普通二叉樹節點結構多了一個指向父節點的parent指針。假設有一棵這種類型的節點組成的二叉樹,樹中每個節點的parent指針都正確地指向自己的父節點,頭節點的parent指向空。只給定在二叉樹中的某個節點node,該節點並不是頭節點,可能是樹中任何一個節點,請實現返回node的後繼節點的函數。
普通解法:
1、通過node節點的parent指針不斷向上找搭配頭節點。
2、通過找到的頭節點,做整棵樹的中序遍歷,生成中序遍歷序列。
3、在中序遍歷序列中,node節點的下一個節點滿就是其後續節點。
普通方法要遍歷所有節點,時間複雜度爲 O(N),額外空間複雜度爲O(N)。
最優解法:
如果node節點和node後繼節點之間實際距離爲L,最優解法只用走過L個節點,時間複雜度爲O(L),額外空間複雜度爲O(1)
情況一:
如果node有右子樹,那麼後繼節點就是右子樹上最左邊的節點。
情況二:
如果node沒有右子樹,那麼先看node 是不是node父節點的左孩子,如果是左孩子,那麼此時node的父節點就是node的後繼節點。如果是右孩子,就向上尋找node的後繼節點。假設向上移動到的節點記爲s,s的父節點記爲p,如果發現s是p的左孩子,那麼節點p就是node的後繼節點,否則就一直向上移動。
情況三:
如果一直向上尋找,都移動到空節點了,還是沒有發現node的後繼節點,說明node根本不存在後繼節點,返回空即可。
案例八:
請把一段紙條豎着放在桌子上,然後從紙條的下邊向上方對摺1次,壓出摺痕後展開,此時摺痕是凹下去的,即摺痕突起的方向指向紙條的背面。
如果從紙條的下邊向上方連續對摺2次,壓出摺痕後展開,此時有三條摺痕,從上到下一次是下摺痕、下摺痕和上摺痕。給定一個輸入參數N,代表紙條都從下邊向上方連續對摺N次,請從上到下打印所有摺痕的方向。
思路:
第一次對摺產生的摺痕:
第二次對摺產生的摺痕(會在第一次摺痕的上下兩側分別產生下摺痕和上摺痕):
第三次對摺產生的摺痕:會在每一條第二次對摺產生的摺痕的上下兩側產生上摺痕和下摺痕。
以後每次都這樣,對摺i次後,會在每一條i-1次對摺後產生的摺痕的上下兩側產生一個上摺痕和下摺痕。摺痕的結構是一個滿二叉樹結構,這個滿二叉樹的頭節點爲下摺痕,每一棵左子樹的頭節點都是上摺痕,每一棵右子樹的頭節點都是下摺痕。
實現先右再中、最後左的先序遍歷,就是所有摺痕的打印順序。(右、中、左)
案例九:
一棵二叉樹原本是搜索二叉樹,但是其中有兩個節點調換了位置,使得這棵二叉樹不再是搜索二叉樹,請找到這兩個錯誤節點。
1、對二叉樹進行中序遍歷,一次出現的節點值會一直升序,如果兩個節點值錯了,會出現降序。
2、如果在中序遍歷時節點值出現了兩次降序,第一個錯誤的節點爲第一次降序時較大的節點,第二個錯誤的節點爲第二次降序時較小的節點。比如原序列:1,2,3,4,5,出錯序列:1,5,3,4,2,錯誤節點分別爲5,3中的5,4,2中的2。
3、如果在中序遍歷時節點值只出現了一次降序,第一個錯誤的節點爲這次降序時較大的節點,第二個錯誤的節點爲這次降序時較小的節點。比如原序列:1,2,3,4,5,出錯序列:1,2,4,3,5,出錯序列:4,3
2和3可以總結爲:第一個錯誤節點爲第一次降序時較大的節點,第二個錯誤節點爲最後一次降序時較小的節點。
所以此題依然是改寫一個基本的二叉樹中序遍歷。
案例十:
從二叉樹的節點A出發,可以向上或者向下走,但沿途的節點只能經過一次,當到達節點B時,路徑上的節點數叫做A和B的距離。比如大家看到的圖中,節點4和節點2的距離爲2,節點5和節點6的距離爲5。給定一棵二叉樹的頭節點head,求整棵樹上節點間的最大距離。
思路:
一個以h爲頭的樹上,最大距離只可能來自以下三種情況:
情況一:
h的左子樹上的最大距離。
情況二:
h的右子樹上的最大距離。
情況三:
h左子樹上離h左孩子最遠的距離,加上h自身這個節點,再加上h右子樹上離h右孩子的最遠距離,也就是兩個節點分別來自h兩側子樹的情況。
三個值中最大的那個就是以h爲頭的整棵樹上最遠的距離。
步驟:
1、整個過程爲後序遍歷,在二叉樹的每棵子樹上執行步驟2.
2、假設子樹頭爲h,處理h左子樹,得到兩個信息,左子樹上的最大距離記爲LMax1,左子樹上的距離h左孩子的最遠距離記爲LMax2。處理h右子樹得到右子樹上的最大距離記爲RMax1,距離h右孩子的最遠距離記爲RMax2,那麼跨h節點情況下的最大距離爲LMax2 + 1 + RMAX2,這個值與LMax1和RMax1比較,最大值爲,以頭的樹上的最大距離。
3、LMax2 + 1 就是h左子樹上離h最遠的點到h的距離,RMax2 + 1 就是h右子樹上離h最遠的點到h的距離,選兩者中最大的一個作爲h樹上距離h最遠的距離返回。
4、用返回長度爲2的數組的方式,可以做的返回兩個值。
案例十一:
給定一棵二叉樹的頭節點head,已知其中所有節點的值都不一樣,找到含有節點最多的搜索二叉子樹,並返回這棵子樹的頭節點。例如,大家現在看到的圖1這棵樹,最大搜索子樹就是圖2這棵樹。
思路:
以節點node爲頭的樹中,最大的搜索二叉子樹只可能來自以下兩種情況:
1、來自node左子樹上的最大搜索二叉子樹是以node左孩子爲頭的,並且來自node右子樹上的最大搜索二叉子樹是以node右孩子爲頭的,node左子樹上的最大搜索二叉子樹的最大值小於node的節點值,node右子樹上的最大搜索二叉子樹的最小值大於node的節點值,那麼以節點node爲頭的整棵樹都是搜索二叉樹。
2、如果不滿足第一種情況,說明以節點node爲頭的樹整體不能連成搜索二叉樹。這種情況下,以node爲頭的樹上的最大搜索二叉子樹是來自node的左子樹上的最大搜索二叉子樹和來自node的右子樹上的最大搜索二叉子樹之間,節點數較多的那個。
通過以上分析,求解的具體過程如下:
1、整體過程是二叉樹的後序遍歷。
2、遍歷到當前節點記爲cur時,先遍歷cur的左子樹並收集4個信息,分別是左子樹上,最大搜索二叉子樹的頭節點記、節點數、樹上最小值和樹上最大值。再遍歷cur的右子樹收集4個信息,分別是右子樹上搜索二叉子樹的頭節點、節點數、最小值和最大值。
3、根據步驟2所收集的信息,判斷是否滿足第一種情況,也就是是否以cur爲頭的子樹,整體都是搜索二叉樹。如果滿足第一種情況,就返回cur節點,如果滿足第二種情況,就返回cur節點,如果滿足第二種情況,就返回左子樹和右子樹各自的最大搜索二叉樹中,節點數較多的那個樹的頭節點。
4、對於如何返回4個信息,可以使用全局變量更新的方式實現。