數據結構基礎--二叉樹

目錄

  • 基本概念
  • 二叉樹的重點
  • 二叉樹的遍歷
  • 實現先序遍歷
  • 實現中序遍歷
  • 實現後序遍歷
  • 以每層換行的方式進行廣度遍歷
  • 二叉樹的序列化和反序列化
  • 前序遍歷的歸檔&&解歸檔
  • 廣度遍歷歸檔&&解歸檔
  • 二叉樹的子樹
  • 平衡二叉樹(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
}

二叉樹的序列化和反序列化

  • 序列化方式
  1. 先序遍歷序列化
  2. 中序遍歷序列化
  3. 後序遍歷序列化
  4. 按層遍歷序列化
  • 一棵樹序列化的結果和反序列化生成的二叉樹都是唯一的
  • 序列化和遍歷二叉樹的區別
  1. 序列化時需要轉化成字符串,所以每個節點之間需要用符號進行分割
  2. 序列化時需要記錄空節點,需要特殊符號進行記錄

舉個例子(用!分割,用#表空):

//序列化
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. 不爲空的二叉樹。其中所有的子樹,左右兩側高度差不超過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


  • 最大距離只有三種情況
  1. head左子樹上的最大距離
  2. head右子樹上的最大距離
  3. head左子樹上離head左孩子最遠的距離,加上head自身節點,再加上head右子樹上離head右孩子最遠的距離。也就是兩個節點分別來自不同子樹的情況。

三個情況下最大的結果,就是以head爲頭節點的整棵樹上最遠的距離。


參考資料

Swift 算法實戰之路:二叉樹
左神牛課網算法課

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章