《数据结构与算法》之树结构二(遍历二叉树与线索二叉树)

前言

众所周知,二叉树的结点是由三个基本元素(根结点,左子树,右子树)组成的。因此,只要能依次遍历这三个元素,就能遍历整个二叉树。
若限定先左后右,那么只有三种访问方式:
1. 先序
先访问根结点,再访问左子树,最后右子树
2. 中序
先访问左子树,再访问根结点,最后右子树
3. 后序
先访问左子树,再访问右子树,最后根结点

满二叉树(图来自维基百科)

基于递归实现

先序

用前言的图作为例子

先序递归示例代码

    public List<Integer> recursion(TreeNode root) {
        List<Integer> list = new LinkedList<>();
        if (null == root) {
            return list;
        }
        // 先根结点
        list.add(root.getVal());
        if (null != root.getLeft()) {
            // 左子树
            list.addAll(recursionPrint(root.getLeft()));
        }
        if (null != root.getRight()) {
            // 右子树
            list.addAll(recursionPrint(root.getRight()));
        }
        return list;
    }
先序运行结果

中序

用前言的图作为例子

中序递归示例代码

    public List<Integer> recursion(TreeNode root) {
        List<Integer> list = new LinkedList<>();
        if (null == root) {
            return list;
        }
        if (null != root.getLeft()) {
            // 左子树
            list.addAll(recursion(root.getLeft()));
        }
        // 根结点
        list.add(root.getVal());
        if (null != root.getRight()) {
            // 右子树
            list.addAll(recursion(root.getRight()));
        }
        return list;
    }
中序运行结果

后序

用前言的图作为例子

后序递归示例代码

    public List<Integer> recursion(TreeNode root) {
        List<Integer> list = new LinkedList<>();
        if (null == root) {
            return list;
        }
        if (null != root.getLeft()) {
            // 左子树
            list.addAll(recursion(root.getLeft()));
        }
        if (null != root.getRight()) {
            // 右子树
            list.addAll(recursion(root.getRight()));
        }
        // 根结点
        list.add(root.getVal());
        return list;
    }
后序运行结果

非递归实现

用递归可以让程序更加简洁,对于树结构的数据处理也十分便利。但是循环的效率往往比递归更优异。因此,有时我们需要将递归转化成非递归。当然有些编译器支持尾递归优化,可以选择使用尾递归(笔者使用的是 JDK8,这个版本不支持尾递归优化)。

先序

用前言的图作为例子

示例代码

    public List<Integer> unRecursion(TreeNode root) {
        List<Integer> list = new LinkedList<>();
        // 游标初始化指向整棵树的根结点
        TreeNode current = root;
        // 通过栈的特性实现递归的回溯能力
        Stack<TreeNode> stack = new Stack<>();
        while (null != current || !stack.isEmpty()) {
            while (null != current) {
                // 加载结点
                list.add(current.getVal());
                stack.push(current);
                // 游标指向左节点
                current = current.getLeft();
            }
            if (!stack.isEmpty()) {
                // 回到前驱节点
                current = stack.pop();
                // 游标指向右节点
                current = current.getRight();
            }
        }
        return list;
    }
非递归先序遍历运行结果

中序

用前言的图作为例子

示例代码

    public List<Integer> unRecursion(TreeNode root) {
        List<Integer> list = new LinkedList<>();
        // 游标初始化指向整棵树的根结点
        TreeNode current = root;
        // 通过栈的特性实现递归的回溯能力
        Stack<TreeNode> stack = new Stack<>();
        while (null != current || !stack.isEmpty()) {
            while (null != current) {
                stack.push(current);
                // 游标指向左节点
                current = current.getLeft();
            }
            if (!stack.isEmpty()) {
                // 回到前驱节点
                current = stack.pop();
                // 加载结点
                list.add(current.getVal());
                // 游标指向右节点
                current = current.getRight();
            }
        }
        return list;
    }
非递归中序运行结果

后序

用前言的图作为例子

示例代码

    public List<Integer> unRecursion(TreeNode root) {
        List<Integer> list = new LinkedList<>();
        // 游标初始化指向整棵树的根结点
        TreeNode current = root;
        // 通过栈的特性实现递归的回溯能力
        Stack<TreeNode> stack = new Stack<>();
        // 由于后序遍历的特性决定了结点需要第二次出栈才可以被加载
        Map<TreeNode, Integer> map = new HashMap<>();
        while (null != current || !stack.isEmpty()) {
            while (null != current) {
                stack.push(current);
                // 游标指向左节点
                current = current.getLeft();
            }
            if (!stack.isEmpty()) {
                TreeNode pop = stack.pop();
                Integer count = map.getOrDefault(pop, 0);
                if (count == 0) {
                    // 第一次出栈
                    current = pop.getRight();
                    stack.push(pop);
                    map.put(pop, count + 1);
                } else {
                    // 第二次出栈
                    list.add(pop.getVal());
                }
            }
        }
        return list;
    }
非递归后序运行结果

后序可能会绕一点,为什么是第二次出栈时才加载呢?
先想下递归,递归很容易就解决了,是因为我们将无数次场景的规模缩小,但是对于每一个结点的处理逻辑其实是一样的。这时我们想将递归转化成非递归,要用循环解决,那么我们也需要模仿递归的思路去做。显然,后序决定了根结点需要第二次访问才能加载,那么为了可以“一套逻辑打天下”,就需要所有结点都是第二次访问才加载。

按层遍历树

前面都是将一颗大树拆成一颗小树,然后再将最单元小树的三元素访问,从而遍历的。本质上就是DFS的处理方式。现在采取BFS的方式遍历。用前言的图作为例子

示例代码

    public List<Integer> ergodicByBfs(TreeNode root) {
        if (null == root) {
            return new ArrayList<>(0);
        }
        List<Integer> list = new LinkedList<>();
        // 一层层的遍历可以通过队列的特性实现,想象成按顺序拿珠子穿就好
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);
        while (!queue.isEmpty()) {
            TreeNode current = queue.poll();
            // 加载元素
            list.add(current.getVal());
            if (null != current.getLeft()) {
                // 将左孩子加入队列
                queue.add(current.getLeft());
            }
            if (null != current.getRight()) {
                // 将右孩子加入队列
                queue.add(current.getRight());
            }
        }
        return list;
    }
按层遍历运行结果

线索二叉树

前面为了遍历二叉树,不是递归,就是用了栈或者队列。如果数据结构可以更丰富些,那么我们实现起来就不用那么麻烦。这也很好地体现了一个合理的数据结构设计是多么的重要。

不管递归,还是栈,核心思想还是一条路走到黑,到了头再回到上一个结点(回溯),再往另一条路走。为了达到这个效果,使用了递归和栈的特性实现而已。如果每个结点都有额外信息可以存储前驱节点和后继结点的信息,那么实现起来就简单多了。因此,有了线索二叉树。

将二叉树转化成线索二叉树的本质就是将非线性结构的数据进行线性化操作,使得每个结点(除头结点和尾结点外)在这线性序列中有且仅有一个直接前驱和直接后继。

看图更直接,大家可以对比下上面代码处理的二叉树的数据结构和线索二叉树的数据结构,如下图


只有三个基本元素的二叉树数据结构
线索二叉树数据结构

对于先序,中序,后序这三种遍历,线索化得到的结果也是不同的,具体如下


还没线索化的二叉树

先序遍历的结果应为:1 2 4 7 3 5 6


先序索引二叉树

中序遍历的结果应为:4 7 2 1 5 3 6


中序索引二叉树

后序遍历的结果应为:7 4 2 5 6 3 1


后序索引二叉树

总结:
1. 当结点数是 n 时,额外存储了 2n 个引用(因为我们定义 TreeNode 一般都用同一个,所以头结点的前驱是 null,尾结点的后继是 null)
2. 当结点数是 n 时,其中 n-1 个引用存储着后继结点;还剩 n+1 个空引,用可用于记录需要回溯的前驱节点
3. 为什么需要增加 leftType & rightType?因为二叉树本身的 left 和 right 指的是左孩子和右孩子,当我们线性化以后,可能会造成当前结点的 left、right 不再是“孩子”的含义。这时,就需要使用一个 type 来标识。

将一颗二叉树线索化的示例代码

前序

@Data
public class ThreadTreeNode {
    private int data;
    private int leftType;
    private ThreadTreeNode left;
    private int rightType;
    private ThreadTreeNode right;

    public ThreadTreeNode(int data) {
        this.data = data;
    }

    public ThreadTreeNode(int data, ThreadTreeNode left, ThreadTreeNode right) {
        this.data = data;
        this.left = left;
        this.right = right;
    }

    public static ThreadTreeNode inThreading(ThreadTreeNode node) {
        if (null == node) {
            return null;
        }
        ThreadTreeNode leftNode = node.getLeft();
        ThreadTreeNode rightNode = node.getRight();
        node.setLeftType(0);
        node.setLeft(null);
        if (null != leftNode) {
            ThreadTreeNode threadLeftNode = inThreading(leftNode);
            threadLeftNode.setLeftType(1);
            threadLeftNode.setLeft(node);
            node.setRightType(1);
            node.setRight(threadLeftNode);
        }
        if (null != rightNode) {
            ThreadTreeNode threadRightNode = inThreading(rightNode);
            threadRightNode.setLeftType(1);
            if (0 == node.getRightType()) {
                threadRightNode.setLeft(node);
                node.setRightType(1);
                node.setRight(threadRightNode);
            } else {
                ThreadTreeNode lastNode = node.getRight().getLastNode();
                threadRightNode.setLeft(lastNode);
                lastNode.setRightType(1);
                lastNode.setRight(threadRightNode);
            }
        }
        return node;
    }

    private ThreadTreeNode getLastNode() {
        ThreadTreeNode last = getRight();
        while (null != last) {
            if (null == last.getRight()) {
                break;
            }
            last = last.getRight();
        }
        return last == null ? this : last;
    }

    public List<Integer> toDataList() {
        List<Integer> list = new LinkedList<>();
        list.add(getData());
        ThreadTreeNode last = getRight();
        while (null != last) {
            list.add(last.getData());
            if (null == last.getRight()) {
                break;
            }
            last = last.getRight();
        }
        return list;
    }

    public static void main(String[] args) {
        ThreadTreeNode root = new ThreadTreeNode(
                1,
                new ThreadTreeNode(
                        2,
                        new ThreadTreeNode(4, null, new ThreadTreeNode(7)),
                        null
                ),
                new ThreadTreeNode(
                        3,
                        new ThreadTreeNode(5),
                        new ThreadTreeNode(6)
                )
        );
        ThreadTreeNode threadTreeNode = ThreadTreeNode.inThreading(root);
        List<Integer> list = threadTreeNode.toDataList();
        System.out.println(JsonUtil.toJsonString(list));
        System.out.println("done");
    }
}

中序和后序就交由你们自己实现啦。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容