目錄
- 基本概念
- 二叉樹的重點
- 二叉樹的遍歷
- 實現先序遍歷
- 實現中序遍歷
- 實現後序遍歷
- 以每層換行的方式進行廣度遍歷
- 二叉樹的序列化和反序列化
- 前序遍歷的歸檔&&解歸檔
- 廣度遍歷歸檔&&解歸檔
- 二叉樹的子樹
- 平衡二叉樹(AVL樹)
- 搜索二叉樹
- 滿二叉樹
- 完全二叉樹
- 後序節點與前驅節點
- 二叉樹中兩節點間的距離
- 參考資料
基本概念
-
基本結構
本節點的值,左子節點,右子節點。(以及一個初始化賦值)
public class TreeNode {
public var val: Int
public var left: TreeNode?
public var right: TreeNode?
public init(_val: Int) {
self.val = val
}
}
二叉樹的重點
-
能夠結合隊列,棧,鏈表,字符串等很多數據結構出題。
-
基本遍歷方式:比如BFS(廣度),DFS(深度)。
-
遞歸的使用
二叉樹的遍歷
先序,中序,後序遍歷爲最常見的樹的三種遍歷方式。這三種寫法相似,無非是遞歸的順序略有不同。
-
先序遍歷
先序遍歷先從二叉樹的根開始,然後到左子樹,再到右子樹。
遍歷的結果是:ABDCEF
-
中序遍歷
中序遍歷先從左子樹開始,然後到根,再到右子樹。
遍歷的結果是:DBAECF
-
後續遍歷
後序遍歷先從左子樹開始,然後到右子樹,再到根。
遍歷的結果是:DBEFCA
實現先序遍歷
-
遞歸
打印自己,然後先遍歷左節點再遍歷右節點
/// 先序遍歷--遞歸
///
/// - Parameter node: 遍歷節點
func preorderRecur(node: TreeNode?) {
if node == nil {
return
}
print(node!.val)//打印當前節點
preorderRecur(node: node!.left)//遍歷左節點
preorderRecur(node: node!.right)//遍歷右節點
}
-
非遞歸
先嚐試將左元素入棧,若棧頂元素爲空則將棧頂推出然後嘗試遍歷右節點。直到棧爲空則遍歷結束。
這裏的棧用處是爲了保存二叉樹的結構,以彌補二叉樹無法獲取父節點的結構特性。
/// 先序遍歷--while
///
/// - Parameter root: 根節點
/// - Returns: 遍歷結果數組
func preorderTraversals(root: TreeNode?) -> [Int] {
var res = [Int]()
var stack = [TreeNode]() //遍歷用的棧
var node = root//遍歷的根節點
while !stack.isEmpty || node != nil {
if node != nil {
res.append(node!.val) //將當前節點的值記錄
stack.append(node!) //將當前節點加入棧中
node = node!.left //嘗試遍歷當前節點的左節點
} else {
let parentsNode = stack.removeLast() //取出當前節點的父節點
node = parentsNode.right //將棧頂節點推出,並嘗試遍歷其父元素的右節點。
}
}
return res
}
-
還有一種方式
這種方式純粹的利用棧的性質,每次彈出棧頂元素,並嘗試將其左右孩子入棧。
不過需要注意的是後入棧的爲左孩子,以保證優先遍歷左側。
func preorderTraversal(root: TreeNode?) -> [Int] {
var res = [Int]()
var stack = [TreeNode]() //遍歷用的棧
var node = root//遍歷的根節點
stack.append(root!)
while !stack.isEmpty{
res.append(stack.last!.val)
node = stack.removeLast()
if node!.right != nil {
stack.append(node!.right!)
}
if node!.left != nil {
stack.append(node!.left!)
}
}
return res
}
實現中序遍歷
-
遞歸
/// 中序遍歷--遞歸
///
/// - Parameter node: 遍歷節點
func inorderRecur(node: TreeNode?) {
if node == nil {
return
}
inorderRecur(node: node!.left)//遍歷左節
print(node!.val)//打印當前節點
inorderRecur(node: node!.right)//遍歷右節點
}
-
非遞歸
與前序遍歷相同,只是記錄的時間不一樣了
func inorderTraversal(root: TreeNode?) -> [Int] {
var res = [Int]()
var stack = [TreeNode]()
var node = root
while !stack.isEmpty || node != nil {
if node != nil {
stack.append(node!) //將當前節點依次入棧
node = node!.left //嘗試遍歷左節點
} else {
let parentsNode = stack.removeLast() //取出當前節點的父節點
res.append(parentsNode.val) //打印父節點
node = parentsNode.right //嘗試遍歷右節點
}
}
return res
}
-
先序遍歷與中序遍歷的非遞歸實現都是嘗試分解左邊界的過程
實現後序遍歷
-
遞歸
/// 後序遍歷--遞歸
///
/// - Parameter node: 遍歷節點
func posorderRecur(node: TreeNode?) {
if node == nil {
return
}
posorderRecur(node: node!.left)//嘗試遍歷左節
posorderRecur(node: node!.right)//嘗試遍歷右節點
print(node!.val)//打印當前節點
}
-
非遞歸
用兩個棧來實現。
第一個棧的處理順序爲,自上而下,自右而左。經過第二個棧的逆序,就變成了自下而上,自左而右。
-
另一種非遞歸
與之前兩種遍歷方式不同,我們需要引入一個新的變量
lastPrint
來記錄最後一次打印的節點。以此判斷左,右節點是否已經被打印。
func posorderTraversal(root: TreeNode?) -> [Int] {
if root == nil {
return []
}
var res = [Int]()
var stack = [TreeNode]()
var node = root
var lastPrint : TreeNode? //最後一次打印的節點
stack.append(node!)
while !stack.isEmpty{
node = stack.last
if node?.left != nil && node?.left != lastPrint && node?.right != lastPrint{
stack.append((node?.left)!) //node的左子樹一定沒有打印完畢
}else if node?.right != nil && node?.right != lastPrint {
stack.append((node?.right)!) //node的右子樹一定沒有打印完畢
}else {
//node的左右子樹全部打印完畢,尋找其父節點
res.append(stack.last!.val)
lastPrint = stack.removeLast()
}
}
return res
}
以每層換行的方式進行廣度遍歷
層數變換的記錄,需要兩個變量。
當前行最右節點(last)
以及下一行最右(nlast)
。
-
具體操作上
每次將新節點加入隊列時,將nlast更新成新節點。
噹噹前打印的節點等於last,執行換行並將last更新到下一行nlast。
-
代碼實現
func BFSTraversal(root: TreeNode?) -> String {
if root == nil {
return ""
}
var res = ""
var queue = [TreeNode]()
var last = root
var nlast = root
queue.append(root!)
while !queue.isEmpty {
let node = queue.removeFirst() //將隊首節點出隊
res += node.val.description + " " //打印隊首節點
if node.left != nil { //嘗試將左節點入隊
queue.append(node.left!)
nlast = node.left!
}
if node.right != nil { //嘗試將右節點入隊
queue.append(node.right!)
nlast = node.right!
}
if node == last { //換行
last = nlast
res += "\n"
}
}
return res
}
二叉樹的序列化和反序列化
-
序列化方式
- 先序遍歷序列化
- 中序遍歷序列化
- 後序遍歷序列化
- 按層遍歷序列化
-
一棵樹序列化的結果和反序列化生成的二叉樹都是唯一的
-
序列化和遍歷二叉樹的區別
- 序列化時需要轉化成字符串,所以每個節點之間需要用符號進行分割
- 序列化時需要記錄空節點,需要特殊符號進行記錄
舉個例子(用!
分割,用#
表空):
//序列化
5!12!20!#!#!22!#!#!17!21!#!#!23!#!33!40!#!#!
//遍歷
[5, 12, 20, 22, 17, 21, 23, 33, 40]
-
反序列化
將序列化字符串轉化成數組(比如這裏通過!
分割)
//字符串
5!12!20!#!#!22!#!#!17!21!#!#!23!#!33!40!#!#!
//數組
["5", "12", "20", "#", "#", "22", "#", "#", "17", "21", "#", "#", "23", "#", "33", "40", "#", "#"]
前序遍歷的歸檔&&解歸檔
-
歸檔
/// 先序遍歷歸檔--遞歸
///
/// - Parameter node: 遍歷節點
func preorderRecurArchive(node: TreeNode?) -> String {
if node == nil {
return "#!"
}
var res = (node?.val.description)! + "!"
res += preorderRecurArchive(node: node!.left)//遍歷左節點
res += preorderRecurArchive(node: node!.right)//遍歷右節點
return res
}
/// 先序遍歷格式化--while
///
/// - Parameter root: 根節點
/// - Returns: 序列化字符串
func preorderArchive(root: TreeNode?) -> String {
var res = ""
var stack = [TreeNode]() //遍歷用的棧
var node = root//遍歷的根節點
while !stack.isEmpty || node != nil {
if node != nil {
res += node!.val.description + "!" //將當前節點的值記錄
stack.append(node!) //將當前節點加入棧中
node = node!.left //嘗試遍歷當前節點的左節點
} else {
let parentsNode = stack.removeLast() //取出當前節點的父節點
node = parentsNode.right //將棧頂節點推出,並嘗試遍歷其父元素的右節點。
res += "#!" //記錄空節點
}
}
res += "#!" //記錄空節點
return res
}
-
解歸檔
遞歸
/// 前序遍歷解歸檔--遞歸
///
/// - Parameter str: 歸檔字符串
/// - Returns: 頭節點
func preorderRecurRearchive(str: String?) -> TreeNode? {
var treeQueue = (str?.components(separatedBy: "!"))!
treeQueue.removeLast() //這裏切割完畢之後最後數組的最後一位爲""
return preorderRecurRearchiveProcess(treeQueue: &treeQueue)
}
/// 根據前序隊列進行二叉樹重構
///
/// - Parameter treeQueue: 節點隊列
/// - Returns: 頭節點
func preorderRecurRearchiveProcess(treeQueue : inout [String]) -> TreeNode? {
let value = treeQueue.removeFirst()
if value == "#" { //頭節點爲空
return nil
}
let root = TreeNode.init(_val: Int(value)!) //設置根節點
root.left = preorderRecurRearchiveProcess(treeQueue: &treeQueue) //設置左節點
root.right = preorderRecurRearchiveProcess(treeQueue: &treeQueue) //設置右節點
return root
}
非遞歸
與遍歷時不同,我們無法通過節點是否爲nil判斷該構建哪一個子節點。
所以我們需要引入一個變量setleft
來確定下一次需要構建的節點方向。
需要注意的是:
每次構建新節點之後,下一次都會嘗試構建其左側節點。
而每次遇到空節點後,都會將頂元素推出,並嘗試構建其的右側節點。
/// 前序遍歷解歸檔
///
/// - Parameter str: 歸檔字符串
/// - Returns: 頭節點
func preorderRearchive(str: String?) -> TreeNode? {
var treeQueue = (str?.components(separatedBy: "!"))!
treeQueue.removeLast() //這裏切割完畢之後最後數組的最後一位爲""
var stack = [TreeNode]() //遍歷用的棧
var node : TreeNode //當前操作的節點
if treeQueue.isEmpty || treeQueue.first == "#" { //頭節點爲空
return nil
}
let root = TreeNode.init(_val: Int(treeQueue.removeFirst())!) //設置root節點
node = root//將頭節點記錄爲當前操作的節點
stack.append(root) //將頭節點記錄
var setleft = true //記錄當前需要構建的節點方向
while !treeQueue.isEmpty {
let value = treeQueue.removeFirst() //將隊列首元素推出
if value != "#" { //若當前節點不爲空
let newNode = TreeNode.init(_val: Int(value)!) //獲得新的節點
//與當前節點相連
if setleft {
node.left = newNode
}else {
node.right = newNode
}
node = newNode //記錄當前節點
stack.append(node) //記錄當前層級
setleft = true //下一次,嘗試構建左節點
}else {
if treeQueue.isEmpty {
return root //如果已經遍歷完成
}else {
node = stack.removeLast() //嘗試構建上層
}
setleft = false //下一次,嘗試構建右節點
}
}
return root //返回頭節點
}
廣度遍歷歸檔&&解歸檔
廣度遍歷的歸檔&&解歸檔比深度遍歷容易理解的多。
因爲他的隊列,只負責記錄下一次想要處理的節點。
並不需要在意左右與層級倒退,只需要處理節點爲空的情況即可。
-
歸檔
/// 廣度遍歷歸檔
///
/// - Parameter root: 頭節點
/// - Returns: 歸檔字符串
func BFSArchive(root: TreeNode?) -> String {
if root == nil {
return ""
}
var res = ""
var queue = [TreeNode]()
queue.append(root!)
res += root!.val.description + "!"
while !queue.isEmpty {
let node = queue.removeFirst() //將當前節點出隊
if node.left != nil { //嘗試將左節點入隊
queue.append(node.left!)
res += node.left!.val.description + "!" //打印當前節點
}else {
res += "#!" //打印當前節點
}
if node.right != nil { //嘗試將右節點入隊
queue.append(node.right!)
res += node.right!.val.description + "!" //打印當前節點
}else {
res += "#!" //打印當前節點
}
}
return res
}
-
解歸檔
/// 廣度遍歷解歸檔
///
/// - Parameter str: 歸檔字符串
/// - Returns: 頭節點
func BFSRearchive(str: String?) -> TreeNode?{
var treeQueue = (str?.components(separatedBy: "!"))!
var i = 0
treeQueue.removeLast() //這裏切割完畢之後最後數組的最後一位爲""
var queue = [TreeNode]()
if treeQueue.isEmpty || treeQueue.first == "#" { //頭節點爲空
return nil
}
let root = TreeNode.init(_val: Int(treeQueue[i])!) //設置root節點
i+=1
queue.append(root)
while !queue.isEmpty && i<treeQueue.count{
let node = queue.removeFirst() //將當前節點出隊
if treeQueue[i] != "#" { //嘗試構建左節點
node.left = TreeNode.init(_val: Int(treeQueue[i])!)
}
i+=1
if treeQueue[i] != "#" { //嘗試構建右節點
node.right = TreeNode.init(_val: Int(treeQueue[i])!)
}
i+=1
if node.left != nil { //嘗試將左節點入隊
queue.append(node.left!)
}
if node.right != nil { //嘗試將右節點入隊
queue.append(node.right!)
}
}
return root
}
二叉樹的子樹
在二叉樹中以任何一個節點爲頭部,其下方的整棵樹作爲二叉樹的子樹。
-
子樹
-
非子樹
平衡二叉樹(AVL樹)
- 空樹爲平衡二叉樹
- 不爲空的二叉樹。其中所有的子樹,左右兩側高度差不超過1。
如下圖中第三棵二叉樹。
2節點的子樹下方,左側高度爲2,右側高度爲0。所以不是一個平衡二叉樹。
-
判斷是否爲平衡二叉樹
通過遞歸的方式判斷每個子樹是否爲AVL樹
一旦一側子節點爲空,另一側若高度大於2,則判定爲否
/// 是否爲平衡二叉樹
///
/// - Parameter root: 子樹頭節點
/// - Returns: 子樹是否平衡
func isBalance(root : TreeNode?) -> Bool {
if root == nil { //空樹爲AVL樹
return true
}
let left = root?.left
let right = root?.right
if ((left?.left != nil) || (left?.right != nil)) && right == nil{
return false //左側比右側高2
}
if ((right?.left != nil) || (right?.right != nil)) && left == nil{
return false //右側比左側高2
}
//否則繼續判定子樹
if isBalance(root: left) && isBalance(root: right) {
return true
}else {
return false
}
}
搜索二叉樹
又叫二叉查找樹,二叉排序樹
特徵爲,每個子樹的頭節點>左節點,並且頭節點<右節點
二叉樹的中序排列,一定是一個有序數組。反之亦然
紅黑樹,平衡搜索二叉樹(平衡AVL樹)等,都是搜索二叉樹的不同實現。
目的都是提高搜索二叉樹的效率,調整代價降低。
-
判斷一個二叉樹是否爲搜索二叉樹
在中序遍歷中,如果上次的值小於當前的值,則證否
/// 判斷一個二叉樹樹否爲搜索二叉樹
///
/// - Parameter root: 根節點
/// - Returns: 結果
func isBST(root: TreeNode?) -> Bool {
var stack = [TreeNode]()
var node = root
var lastValue = -NSIntegerMax
while !stack.isEmpty || node != nil {
if node != nil {
stack.append(node!) //將當前節點依次入棧
node = node!.left //嘗試遍歷左節點
} else {
let parentsNode = stack.removeLast() //取出當前節點的父節點
if lastValue > parentsNode.val {
return false
}
lastValue = parentsNode.val
node = parentsNode.right //嘗試遍歷右節點
}
}
return true
}
-
復原一個交換了位置的搜索二叉樹
搜索二叉樹本身的中序遍歷是升序排序。一旦有兩節點交換了位置,就一定有一到兩個部分產生降序。
#1. 遍歷中出現了兩次局部降序
#1,2,3,4,5
#1,5,3,4,2
第一個錯誤的節點爲第一次降序較大的節點
第二個錯誤的節點爲第二次降序較小的節點
#2. 遍歷中只出現了一次局部降序
#1,2,3,4,5
#1,2,4,3,5
第一個錯誤的節點爲此次降序較大的節點
第二個錯誤的節點爲此次降序較小的節點
滿二叉樹
-
對於國內的滿二叉樹
除最後一層無任何子節點外,每一層上的所有結點都有兩個子結點二叉樹。
從圖形形態上看,滿二叉樹外觀上是一個三角形
國內的滿二叉樹屬於完全二叉樹
這種滿二叉樹的層數爲L,節點數爲N。
則N = 2^L-1 ,L = log(N+1)
-
對於國外的滿二叉樹
滿二叉樹的結點要麼是葉子結點,度爲0,要麼是度爲2的結點,不存在度爲1的結點。
完全二叉樹
在滿二叉樹的基礎上,最後一層所有的結點都連續集中在最左邊,這就是完全二叉樹。
-
判斷完全二叉樹
通過寬度遍歷的方式進行。
-
計算完全二叉樹的節點個數,要求複雜度小於O(N)
完全二叉樹的左右子樹,一定有一邊是滿二叉樹(左側高度H,右側高度H-1)。
先遍歷左子樹左邊界,再遍歷右子樹左邊界。從而判斷哪邊爲滿二叉樹。
滿二叉樹側,N=2^H。非滿二叉樹側,遞歸。
//完全二叉樹節點個數
func nodeNum(root: TreeNode?) -> Int {
if root == nil {
return 0
}
return bs(node: root!, level: 1, h: mostLeftLeve(node: root, level: 1))
}
/// 以node爲頭的所有節點個數
///
/// - Parameters:
/// - node: 當前節點
/// - level: 當前節點層數
/// - h: 總深度
/// - Returns: 節點個數
func bs(node: TreeNode,level: Int ,h: Int) -> Int {
if level == h {
return 1
}
//比較節點右子樹深度與當前樹深度
if mostLeftLeve(node: node.right, level: level+1) == h {
//左樹已滿。2^(h-level)+右樹節點數
return 1<<(h-level) + bs(node: node.right!, level: level+1, h: h)
}else {
//右樹已滿。2^(h-level-1)+左樹節點數
return 1<<(h-level-1) + bs(node: node.left!, level: level+1, h: h)
}
}
/// 獲取當前子樹總高度
///
/// - Parameters:
/// - node: 頭節點
/// - level: 當前層級
/// - Returns: 左邊界總高度
func mostLeftLeve(node: TreeNode?,level: Int) -> Int {
var node = node
var level = level
while node != nil {
node = node!.left!
level+=1
}
return level-1
}
每層只遍歷一個節點的子樹,總計LogN。
每個子樹獲取右子樹左邊界遍,需要經歷LogN次計算。
總複雜度O((LogN^2))
-
數組與完全二叉樹
如果從下標從1開始存儲,則編號爲i的結點的主要關係爲:
雙親:下取整 (i/2)
左孩子:2i
右孩子:2i+1
如果從下標從0開始存儲,則編號爲i的結點的主要關係爲:
雙親:下取整 ((i-1)/2)
左孩子:2i+1
右孩子:2i+2
#這個規律,通常用來對通過指定下標取得相關節點下標。
後序節點與前驅節點
中序遍歷中的下一個遍歷點與上一個遍歷點
2的後序節點爲3,2的前驅節點爲1
二叉樹中兩節點間的距離
可以向上或向下走,但每個節點只能經過一次。
下圖中2,1兩節點距離爲2。3,5節點距離爲5
-
最大距離只有三種情況
- head左子樹上的最大距離
- head右子樹上的最大距離
- head左子樹上離head左孩子最遠的距離,加上head自身節點,再加上head右子樹上離head右孩子最遠的距離。也就是兩個節點分別來自不同子樹的情況。
三個情況下最大的結果,就是以head爲頭節點的整棵樹上最遠的距離。