• 二叉树算法


    1. 几种二叉树的概念

    1.1. 满二叉树

    所有节点都长了叶子。

    e4d8dbbc99d54c7c9d38f32443d870e3.jpg

    1.2. 完全二叉树

    倒数第二层是满的,最后一层完全堆在左边。

    1.3. 平衡二叉树(AVL树)

    任意两个子树高度差不超过1的二叉树。

    2. 表示树的底层结构

    可以用数组或链表(有多个指针的链表)来表示树形结构

    2.1. 数组

    使用数组的下标计算来确定某个节点的左右孩子和父。

    • 如果数组用下标0来存根节点,则数组下标是i的节点,它的左孩子下标是i*2+1,右孩子下标是i*2+2,父节点下标是(i-1)/2,当然根节点的父节点要除外,因为这样算下来它的父节点下标是负数了。
    • 如果数组用下标1来存根节点(下标0不用),则数组下标是i的节点,它的左孩子下标是i*2,右孩子下标是i*2+1,父节点下标是i/2,当然根节点的父节点要除外,因为这样算下来它的父节点下标是0了。

    本文统一用数组下标1来存根节点的方式讨论。

    因为要用null占位,表示树的结构,所以数组来存储树形结构比较浪费空间。如果是完全二叉树,它是靠左紧凑的,我们可以不用null来占位,如下图所示。

    2.2. 链表

    用一个节点对象表示树形结构上的一个节点,该节点中包含3个指针来存储一个其与父、左右孩子的关系。

    用链表来存储树形结构,就不需要通过下标计算父、左右孩子的位置了,直接取指针就可以了。

    3. 树的遍历

    先总结一下,详细解释见下文:非递归的方式遍历,深度用栈、广度用队列

    3.1. 深度优先遍历(DFS)

    整个遍历过程是从根节点开始,先打到左边底,再慢慢回到根部,再打到右边底。

    根据遍历任何一个子树的时候,先遍历当前节点还是后遍历当前节点,可以分为先序、中序、后序3种遍历方式。

    可以看到,不管哪种方式,右孩子都在左孩子之后遍历。

    一般来说,这3种遍历方式都有递归非递归两种方式来实现。

    3.1.1. 递归实现3种深度优先遍历

    递归实现树的深度优先遍历,是很直观的。

    1. // 先序遍历
    2. dfsPre(Node node){
    3.     visit(node);//先访问当前节点,再访问其左右子树
    4.     dfsPre(node.left);
    5.     dfsPre(node.right);
    6. }
    1. // 中序遍历
    2. dfsMid(Node node){
    3.     dfsMid(node.left);
    4.     visit(node);//先访问其左子树,再访问当前节点,再访问其右子树
    5.     dfsMid(node.right);
    6. }
    1. // 后序遍历
    2. dfsPost(Node node){
    3.     dfsPost(node.left);
    4.     dfsPost(node.right);
    5.     visit(node);//先访问其左右子树,再访问当前节点
    6. }

    3.1.2. 非递归实现3种深度优先遍历

    非递归方式需要使用栈结构来实现(递归方式本质是用了计算机程序自己的调用栈)。

    非递归方式没有递归方式那么直观,可以说是用了3种思路分别实现的。它们3者的共同点有:

    1. 都要用栈
    2. 节点出栈时打印,出栈顺序代表打印顺序
    3. 先序和后序就是颠倒一下(用栈来颠倒,详见下文),代码大部分一样
    3.1.2.1. 非递归的先序遍历

    每次循环出栈一个节点。

    上来先把头结点丢到栈里,然后开始循环出栈。

    一次循环里面先放右节点再放左节点。

    1. void pollVisitPre(Node node) {
    2.     if (node == null) return;
    3.     Stack stack = new Stack<>();
    4.     stack.push(node);
    5.     while (!stack.isEmpty()) {
    6.         Node c = stack.pop();
    7.         System.out.print(c.val + ",");
    8.         if (c.right != null) stack.push(c.right);//因为后打印right,所以先放right
    9.         if (c.left != null) stack.push(c.left);
    10.     }
    11. }
    3.1.2.2. 非递归的中序遍历

    先把左树全部入栈,然后一个个地弹出来打印,然后入栈右树。

    可以把一棵二叉树想象成一个个的左子树的形式:

    代码如下

    1. void pollVisitMid(Node node) {
    2.     if (node == null) return;
    3.     Stack stack = new Stack<>();
    4.     while (node != null || !stack.isEmpty()) {
    5.         while (node != null) { // 这个while可以看做上图中的斜着的一列左子树
    6.             stack.push(node);
    7.             node = node.left;
    8.         }
    9.         node = stack.pop();
    10.         System.out.print(node.val + ",");
    11.         node = node.right;
    12.     }
    13. }

    其中比较难以理解的是两个while的判断条件里都有 node != null 的条件。

    3.1.2.3. 非递归的后序遍历

    上面非递归的先序遍历中,左右孩子是右先进,所以左先打。如果改成左先进,就是右先打,然后在用一个栈把它的结果倒来打印,就是后续遍历了,思路如下图所示:

    代码如下:

    1. void pollVisitPost(Node node){
    2.     if(node == null) return;
    3.     Stack savePrintResult = new Stack<>();//存储先序遍历时的出栈结果
    4.     Stack stack = new Stack<>();
    5.     stack.push(node);
    6.     while(!stack.isEmpty()){
    7.         node = stack.pop();
    8.         savePrintResult.push(node.val);//存储先序遍历时的出栈结果,待会儿打印
    9.         if(node.left!=null) stack.push(node.left);//先左进,所以会先打印右
    10.         if(node.right!=null) stack.push(node.right);
    11.     }
    12.     while(!savePrintResult.isEmpty()){//反向打印出来
    13.         System.out.print(savePrintResult.pop()+",");
    14.     }
    15. }

    3.2. 广度优先遍历(BFS)

    整个遍历过程是从根节点开始,一层一层地遍历。

    3.2.1. 非递归的广度优先遍历

    非递归的广度优先遍历比较简单明了,所以先介绍。

    这种方式使用了队列,也是节点出来的时候打印,同时把它的左右孩子放到队列里,出来一个打印一个、放一对(左右孩子),直到队列为空。

    1. void bfs(Node node) {
    2.     Queue queue = new LinkedList<>();
    3.     queue.add(node);
    4.     while (!queue.isEmpty()) {
    5.         Node n = queue.poll();
    6.         System.out.print(n.val + ",");
    7.         if (n.left != null) queue.add(n.left);
    8.         if (n.right != null) queue.add(n.right);
    9.     }
    10. }

    3.2.2. 递归的广度优先遍历

    思路是用一个List>的容器存储每一层的遍历结果,然后最后再打印出来。代码如下:

    1. void recurseBfs(Node node){
    2.     List> lists = new ArrayList<>();
    3.     doRecurseBfs(lists, 0, node);
    4.     for(List list:lists){
    5.         for(Integer i:list){
    6.             System.out.print(i+",");
    7.         }
    8.     }
    9. }
    10. void doRecurseBfs(List> lists, int i, Node node) {
    11.     if(node==null) return;
    12.     if(lists.size()==i){
    13.         lists.add(new ArrayList<>());
    14.     }
    15.     List list = lists.get(i);
    16.     list.add(node.val);
    17.     doRecurseBfs(lists, i+1, node.left);
    18.     doRecurseBfs(lists, i+1, node.right);
    19. }

    本文前面介绍的用数组存储的方式,其实是一种“广度优先”遍历后存下来的节点。如果不用广度优先,则位置i的左右孩子和父就不一定是2*i、2*i+1、i/2的关系了。

    4. 树的序列化和反序列化

    序列化就是把对象保存成字符串或二进制,反序列化就是把保存的字符串或二进制再转成内存里的对象。

    4.1. 树的序列化

    树的序列化需要记录下所有null节点(不然树的造型可能会变)如下图所示

    如果是完全二叉树,可以不记录null节点。

    序列化的过程跟前面遍历过程一致,只是把前面打印和返回空节点的地方,改成用字符串存起来(返回的空节点用"null"存储)。

    1. StringBuilder serializeNode(Node node) {
    2.     if (node == null) {
    3.         return new StringBuilder("null");
    4.     } else {
    5.         return new StringBuilder(node.val + "");
    6.     }
    7. }

    下面给一个先序遍历方式序列化的示例代码(递归、非递归,先/中/后序都有对应的序列化代码):

    1. StringBuilder serializeTreePre(Node node) {
    2.     if (node == null) {
    3.         return serializeNode(null);
    4.     }
    5.     return serializeNode(node).append(",")
    6.             .append(serializeTreePre(node.left)).append(",")
    7.             .append(serializeTreePre(node.right));
    8. }

    再给一个广度优先的序列化的示例代码(这里用非递归方式,递归方式也可以实现):

    1. StringBuilder serializeTreeBfs(Node node) {
    2.     if (node == null) {
    3.         return serializeNode(null);
    4.     }
    5.     StringBuilder sb = new StringBuilder();
    6.     Queue queue = new LinkedList<>();
    7.     queue.add(node);//广度优先的架子
    8.     while (!queue.isEmpty()) {//广度优先的架子
    9.         node = queue.poll();//广度优先的架子
    10.         sb.append(serializeNode(node)).append(",");
    11.         if (node != null) queue.add(node.left);//这里注意,空孩子也要丢到队列里输出
    12.         if (node != null) queue.add(node.right);
    13.     }
    14.     return sb;
    15. }

    4.2. 树的反序列化

    树的反序列化要与序列化方式对应起来用,比如用先序遍历序列化的,就要用先序遍历反序列化、用广度优先序列化的,就要用广度优先反序列化来还原。

    树的反序列化都要用到队列,从队列里一个个地poll出来,构造树的节点。

    4.2.1. 深度优先反序列化

    直接上代码,先序遍历的反序列化(中序、后序类似)

    1. Node deserializeTreePre(String txt) {
    2.     return doDeserializeTreePre(new LinkedList<>(Arrays.asList(txt.split(","))));
    3. }
    4. Node doDeserializeTreePre(Queue queue) {
    5.     if (queue.isEmpty()) return null;
    6.     String val = queue.poll();
    7.     if (val.equals("null")) {
    8.         return null;
    9.     }
    10.     
    11.     Node n = new Node(Integer.parseInt(val));//先序,先处理当前节点
    12.     Node left = doDeserializeTreePre(queue);//再处理左、右孩子
    13.     Node right = doDeserializeTreePre(queue);
    14.     // 把3个节点连起来
    15.     n.left = left;
    16.     n.right = right;
    17.     if (left != null) left.parent = n;
    18.     if (right != null) right.parent = n;
    19.     return n;
    20. }

    4.2.2. 广度优先反序列化

    广度优先反序列化,也是广度优先遍历的过程,所以需要一个广度优先遍历的队列,加上反序列化用于存储字符串的那个队列,就有两个队列了。

    代码如下,跟广度优先遍历过程的架子是一样的:

    1. Node deserializeTreeBfs(String txt) {
    2.     Queue strQueue = new LinkedList<>(Arrays.asList(txt.split(",")));
    3.     if(strQueue.isEmpty()||strQueue.peek().equals("null")) return null;
    4.     Queue resultQueue = new LinkedList<>(); //广度优先遍历的队列
    5.     Node head = new Node(Integer.parseInt(strQueue.poll()));
    6.     resultQueue.add(head); //与广度优先遍历架子一样
    7.     while(!resultQueue.isEmpty()){//广度优先的架子
    8.         Node n = resultQueue.poll();//广度优先的架子
    9.         String leftStr = strQueue.poll();
    10.         String rightStr = strQueue.poll();
    11.         if(!"null".equals(leftStr)){
    12.             Node left = new Node(Integer.parseInt(leftStr));
    13.             n.left = left;
    14.             left.parent = n;
    15.             resultQueue.add(left);
    16.         }
    17.         if(!"null".equals(rightStr)){
    18.             Node right = new Node(Integer.parseInt(rightStr));
    19.             n.right = right;
    20.             right.parent = n;
    21.             resultQueue.add(right);
    22.         }
    23.     }
    24.     return head;
    25. }

  • 相关阅读:
    (三)Python Range循环
    苹果macOS电脑版 植物大战僵尸游戏
    液压自动化成套设备比例阀放大器
    Docker的网络模式
    运营商大数据精准营销,击碎你的固化营销思维
    i.MX 6ULL 驱动开发 二十四:多点触摸屏
    Vue使用基本教程(基本介绍及对比,初步使用,构建项目,编辑器等)
    合并文档的 7 个免费 PDF 合并平台
    python中json的使用
    【Java SE】封装的详解
  • 原文地址:https://blog.csdn.net/liushuidehao/article/details/142282473