树形结构递归_数据结构(六):二叉树的遍历

本文介绍了二叉树的基本性质,包括层深、完全二叉树的概念,并详细讲解了二叉树的前序、中序、后序递归遍历。同时探讨了非递归遍历二叉树的方法,通过模拟递归函数栈实现。作业包括补充二叉树的遍历实现及按层遍历的代码。
摘要由CSDN通过智能技术生成

本节课我们继续二叉树的话题。

二叉树的性质

这节课,先介绍一下二叉树的几个相关的性质,这些性质很简单,但却是我们进行数据结构的性能分析的基础。

1. 在二叉树的第 i 层上至多有

个结点。

这个性质是显然的,第一层有1个,第二层最多只可能是第1层的两倍,第三层是第二层的两倍。

2. 深度为 k 的二叉树,最多有

个结点。

这条性质由 1 可以直接得出:将每一层的最大结点数相加。这是一个公比为 2 的等比数列,其和为

再介绍一个定义,如果一个深度为 k 的二叉树,正好有

个结点,那么这棵树就被称为
满二叉树。如果一个二叉树,只有当第 k 层已经达到了
的最大值以后,才能往第 k + 1 层自左向右地添加结点,这棵树就被称为
完全二叉树

例如,下图就是一棵满二叉树:

e2d41f9f3c13cea98aa9a94499373309.png

而下面这个二叉树就不是满二叉树,却是一个完全二叉树:

ed23bc432302951ff8422883df1beea8.png

3. 具有 n 个结点的完全二叉树的深度为

这个性质就是性质二的直接推论。不再多做解释了。但这个性质向我们揭示了一个问题:受二叉树的树形的影响,同样有n个结点的一个二叉树,它的高度可能差别很大。比如,我们上节课的作业第二题,如果二叉搜索树是以(1, 2, 3, 4, 5, 6)或者(6, 5, 4, 3, 2, 1)的顺序插入的话,二叉树的高度就是6,退化为链表。上节课的第一题,我们看到了,二叉树中的搜索效率与树的高度成正比。二叉树越矮,我们就能越快地找到目标,二叉树越高,要经过的结点就会越多。

当树形为完全二叉树时,在树中进行查找的时间复杂度是 O(log n),而当树形退化为链表时,查找的时间复杂度是 O(n),这个时间复杂度的差别是十分巨大的,大家可以算一下,如果有1000个结点,O(n)的时间复杂度,意味着1000次比较,而O(log n)的时间复杂度,只有10次比较,性能可以提升100倍。好的数据结构和算法设计的威力可见一斑。

另外,完全二叉树的定义是很重要的,因为我们后面会学习一种威力十分强大的数据结构:堆,它就是一个完全二叉树。所以请记住完全二叉树。

二叉树的遍历

在上一节课的课外阅读里,我们详细地讨论了递归程序。并且使用递归实现了二叉树的前序遍历:

    public void preOrder(Node<T> n) {
        System.out.println(n.data);

        if (n.left != null)
            preOrder(n.left);
        if (n.right != null)
            preOrder(n.right);
    }

这里,先介绍一下三种常用的遍历方式:

1. 前序遍历。先访问根结点,再前序遍历左子树,最后前序遍历右子树。可见,这个操作的定义就是递归的。

2. 中序遍历。先中序遍历左子树,再访问根结点,最后中序遍历右子树。由于左子树上的值都比根结点小,右子树上的值都比根结点大,所以,中序遍历一棵树所得到的结果,是从小到大有序的,可以根据这个特点,来检验你的中序遍历是否正确实现了。

3. 后序遍历。先后序遍历左子树,再后序遍历右子树,最后访问根结点。

定义非常简单,前,中,后,无非就是说的根结点在什么时机被访问而已。还有其他的遍历方式,但这三种是我们最常用的,希望大家彻底理解它。我这里再给出中序遍历的代码,大家自己完成后序的:

    public void midOrder(Node n) {
        if (n.left != null)
            midOrder(n.left);
        System.out.println(n.data);
        if (n.right != null)
            midOrder(n.right);
    }

非递归遍历二叉树

在很多笔试题中,二叉树的非递归遍历是一个很常见的考题。非递归遍历有很多种实现方式,掌握起来要耗费很大的精力,而且还容易忘记。这一节将会介绍一种模拟递归函数的函数栈,从而实现非递归遍历。

如果将未访问完的结点入栈,每次只对栈顶元素进行操作,一旦栈顶元素被全部处理完,就将其出栈。继续取栈顶元素进行处理,这就相当于回溯到了父结点。但是每次子结点全部访问完回溯到父结点时,都需要知道父结点已经执行到哪一步了,在递归程序中,这个值是放在子结点所对应栈的 old EIP 中的,因此,这是我们要模拟的一个值。由于不需要像真正的递归程序那样在回溯时恢复调用者的函数栈,所以 old EBP 和 old ESP 这两个值是不必模拟的。(要看懂这一节,一定要先把我前边那篇关于递归的文章读透)经过分析,我们可以给结点的定义加上一个变量state来模拟old EIP:

class Node<T> {
    public T data;
    public Node left;
    public Node right;
    public int state;

    public Node(T d) {
        this.data = d;
    }
}

为结点增加 state 变量,来标记当前结点已经访问到哪一步。每次从子结点回溯回来的时候,都可以直接从当前结点里取出,这样就可以更加简化这个程序。接下来,给出非递归的中序遍历的代码。

    public void midOrderWithoutRecurs() {
        if (root == null)
            return;

        Stack<Node<T>> s = new Stack<>(64);
        Node<T> current;

        s.push(root);

        while (! s.isEmpty()) {
            current = s.getTop();
            if (current.state == 0) {
                if (current.left != null)
                    s.push(current.left);
                current.state = 1;
            }
            else if (current.state == 1) {
                System.out.println(current.data);
                current.state = 2;
            }
            else if (current.state == 2) {
                if (current.right != null)
                    s.push(current.right);
                current.state = 3;
            }
            else if (current.state == 3) {
                s.pop();
                current.state = 0;
            }
        }
    }

今天的课程就到这里了。今天的作业:

1. 补全BinarySearchTree的中序遍历, 前序遍历和后序遍历的实现。

2. 补全非递归的前序遍历的实现。

3. 树的遍历,还有一种办法,那就是按层遍历,例如,对于下面这棵树

85bfa3c6136bcadf98c8d5790c409a75.png

按层遍历的结果,就是1, 2, 3, 4, 5, 6。请实现一下按层遍历。

上一节课:二叉树

下一节课:迭代器模式

目录:课程目录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值