阅读 58

二叉树-前序遍历思考记录

分析例子

遍历的结果应该是: 4、1、3、2、5、7、6、8

遍历过程:

  1. 从根节点 4 出发,一直找左节点,直到没有左节点为止,所以就会遍历到 4、1

  2. 如果当前节点还有右节点,那就从右节点开始遍历左节点,直到没有左节点为止,所有就会遍历到 3、2

  3. 如果当前节点也没有右节点,那就找当前节点的父节点,看它有没有右节点,如果没有,那就就继续找父节点,这样就走过了 3

  4. 当走到 1 时,发现 1 有右节点,但是 3 是已经遍历过的节点,如果再次访问 1 的右节点,那就会死循环了

所以我们需要区分哪些节点已经访问过了

遍历之所以复杂是因为,对每个节点的左、中、右的访问被分开了,所以我们要记录下来节点的访问情况

通常的做法是:把访问过的结点都放进栈中

先祭出代码

swift 实现

func preorder(doWhat:(Node) -> ()) {
        var curNode: Node? = self
        var todos: [Node] = []
        while curNode != nil || !todos.isEmpty {
            if let cur = curNode {
                doWhat(cur)
                todos.append(cur)
                curNode = cur.leftNode()
            } else {
                if let parent = todos.popLast() {
                    curNode = parent.rightNode()
                }
            }
        }
    }
复制代码

以下是每一遍循环中,关键变量的值以及解释:

  1. cur = 4
  2. cur = 1, todos.add(4) // cur = 1 就相当于 4 的左子树已经处理了,因为在下一轮循环中就会处理 cur,也就是 1;而且 4 的中也处理过了,所以现在 4 只剩下右子树没有处理了,我们认为 todos 中存放的是左子树和中都已经处理过,并且右子树还没有处理的节点
  3. cur = todos.pop().right // 因为 1.left 为空,所以转而处理 1 的右子树,既然要处理右子树,那 todos 中肯定不能存放 1 了,所以 cur = todos.pop().right
  4. cur = 3, todos.add(3) // 此时 todos = [4, 3]
  5. cur = 2, todos.add(2) // 此时 todos = [4, 3, 2]

...

这段代码理解的难点在于:

  • todos 数组是用来存放什么的:左子树和中都已经处理过,并且右子树还没有处理的节点
  • curNode 是用来保存什么的:下一次循环中要处理的节点,不存在的节点,也可以当成是一种节点,不过就是如果这个节点有值的话,我们会调用 doWhat 去访问它,如果它没值的话,我们就什么都不做,这也可以理解成一种处理,这样理解就可以把两种情况归于一种了,有利于简化逻辑

经验总结

算法是很难一蹴而就的,我觉得采用这样的步骤会更加容易些:

  1. 根据例子总结出规律
  2. 搞清楚每一个变量的用途,尤其是循环之外的变量
文章分类
阅读
文章标签