美文网首页SwiftBlogswift成长之路iOS奋斗
Swift 算法实战之路:二叉树

Swift 算法实战之路:二叉树

作者: 故胤道长 | 来源:发表于2016-07-01 08:29 被阅读2805次

    之前我们探索了数组、字典、字符串、链表、栈、队列的处理和应用。今天我们来讲讲平常相对很少用到,面试中却是老面孔的数据结构:二叉树。本期的内容有:

    • 基本概念:实现,深度 ,二叉查找树
    • 遍历
    • 苹果面试题:在iOS中展示二叉树

    概念


    首先介绍下二叉树。二叉树中每个节点最多有两个子节点,一般称为左子节点和右子节点,并且二叉树的子树有左右之分,其次序不能任意颠倒。下面是节点的Swift实现:

    public class TreeNode {
      public var val: Int
      public var left: TreeNode?
      public var right: TreeNode?
      public init(_val: Int) {
        self.val = val
      }
    }
    

    一般在面试中,会给定二叉树的根节点。要访问任何其他节点,只要从起始节点开始往左/右走即可。
    在树中,节点的层次从根开始定义,根为第一层,树中节点的最大层次为树的深度

    // 计算树的最大深度
    func maxDepth(root: TreeNode?) -> Int {
      guard let root = root else {
        return 0
      }
      return max(maxDepth(root.left), maxDepth(root.right)) + 1
    }
    

    面试中,最常见的是二叉查找树,它是一种特殊的二叉树。它的特点就是左子树中节点的值都小于根节点的值,右子树中节点的值都大于根节点的值。那么问题来了,给你一棵二叉树,怎么判断它是二叉查找树?我们根据定义,可以写出以下解法:

    // 判断一颗二叉树是否为二叉查找树
    func isValidBST(root: TreeNode?) -> Bool {
      return _helper(root, nil, nil)
    }
        
    private func _helper(node: TreeNode?, _ min: Int?, _ max: Int?) -> Bool {
      guard let node = node else {
        return true
      }
      // 所有右子节点都必须大于根节点
      if let min = min, node.val <= min {
        return false
      }
      // 所有左子节点都必须小于根节点
      if let max = max, node.val >= max {
        return false
      }
            
      return _helper(node.left, min, node.val) && _helper(node.right, node.val, max)
    }
    

    上面的代码有这几个点指点注意:

    1. 二叉树本身是由递归定义的,所以原理上所有二叉树的题目都可以用递归来解
    2. 二叉树这类题目很容易就会牵涉到往左往右走,所以写helper函数要想到有两个相对应的参数
    3. 记得处理节点为nil的情况,尤其要注意根节点为nil的情况

    遍历

    最常见的树的遍历有三种,前序、中序、后序遍历。这三种写法相似,无非是递归的顺序略有不同。面试时候有可能考察的是用非递归的方法写这三种遍历:用栈实现。

    // 用栈实现的前序遍历
    func preorderTraversal(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 {
          node = stack.removeLast().right
        }
      }
            
      return res
    }
    

    除了这三种最常见的遍历之外,还有一种遍历是层级遍历(广度优先遍历),请看下图:

    这棵树的层级遍历结果为[[1], [2, 3], [4, 5, 6, 7], [8, 9, 10, 11, 12, 13, 14, 15]]。
    层级遍历相比于以上三种常见遍历的好处在于:如果构建一棵树,那么至少要知道中序遍历和前序/后序遍历中的一种,也就是至少要知道两种遍历方式;但是层级遍历自己便可以唯一确定一棵树。我们来看下面一道苹果公司的面试题。

    实战

    Given a binary tree, please design an iOS app to demo it.

    首先一个简单的app是mvc架构,所以我们就要想,在View的层面上表示一棵二叉树?我们脑海中浮现树的结构是这样的:


    用UITableView表现一棵树

    其中"#"表示空节点。明眼人可以看出,我们是按照层级遍历的方式布局整个UITableView。这种做法解决了上面两个问题:

    • 无需进行位置计算,UITableView提供复用Cell,效率大幅提高
    • 面对很多节点的问题,可以先处理一部分数据,然后用处理infinite scroll的方式来加载剩余数据

    接着问题来了,给你一棵二叉树,如何得到它的层级遍历?其实层级遍历就是图的广度优先遍历,而广度优先遍历很自然就会用到队列,所以我们不妨用队列来帮助实现树的层级遍历:

    func levelOrder(root: TreeNode?) -> [[Int]] {
      var res = [[Int]]()
      // 用数组来实现队列
      var queue = [TreeNode]()
    
      if let root = root {
        queue.append(root)
      }
            
      while queue.count > 0 {
        var size = queue.count
        var level = [Int]()
                
        for _ in 0 ..< size {
          let node = queue.removeFirst()
                    
          level.append(node.val)
          if let left = node.left {
            queue.append(left)
          }
          if let right = node.right {
            queue.append(right)
          }
        }
        res.append(level)
      }
            
      return res
    }
    

    总结

    到这里为止,我们已经把重要的数据结构都分析了一遍。要知道,这些数据结构都不是单独存在的,我们在解决二叉树的问题时,用到了队列;解决数组的问题,也会用到字典或是栈。在真正面试或是日常Coding中,最低的时间复杂度是首要考虑,接着是优化空间复杂度,其次千万不要忘记考虑特殊情况。在Swift中,用let和var的地方要区分清楚,该不该定义数据为optional,有没有处理nil的情况都是很容易忽略的,希望大家多多练习,融会贯通。

    相关文章

      网友评论

      本文标题:Swift 算法实战之路:二叉树

      本文链接:https://www.haomeiwen.com/subject/ysctjttx.html