算法:二叉树遍历类题目

算法:二叉树遍历类题目

树的遍历顺序是依赖于 节点的位置,前序遍历的顺序为 根左右,中序遍历的顺序为 左根右,后序遍历的顺序为 左右根。除此以外还存在层次遍历。

在树类算法中,很多题目是基于树的遍历和树的性质而产生的,熟悉树的遍历和性质是灵活应用树类题目的前提。

那么什么是树和二叉树?

树(tree)是n(n>=0)个结点的有穷集。n=0时称为空树。在任意一个非空树中:(1)每个元素称为结点(node);(2)仅有一个特定的结点被称为根结点或树根(root)。(3)当n>1时,其余结点可分为m(m≥0)个互不相交的集合T1,T2,……Tm,其中每一个集合Ti(1<=i<=m)本身也是一棵树,被称作根的子树(subtree)。可见树(tree)的定义本身就是迭代递归的定义。

二叉树是树中节点的度不大于2的有序树,它是一种最简单且最重要的树。

1. 前根遍历

1.1 前根的递归遍历

由树的定义可知,树天生具有可迭代的特性。

   // 由于方法中要方法结果列表,不可直接进行迭代,可以定义全局列表,或定义helper方法。
   List<Integer> res = new ArrayList<>(); 
   public List<Integer> preorderTraversal(TreeNode root) {
        if (root == null) {
          return res;
        }
        // 先打印根节点,然后打印左子树,最后再打印右子树
        res.add(root.val);
        preorderTraversal(root.left);
        preorderTraversal(root.right);
        return res;
    }

1.2 前根的迭代遍历

递归一般可以通过栈来进行描述,所以可以通过栈实现前根的迭代遍历。

  public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        if (root == null) {
          return res;
        }
        Stack<TreeNode> stack = new Stack<>();
        TreeNode cur = root;
        while(cur != null || !stack.empty()) {
            if (cur != null) {
                res.add(cur.val);
                stack.push(cur);
                cur = cur.left;
            } else {
                TreeNode node = stack.pop();
                if(node.right != null) {
                    cur = node.right;
                }
            }
        }
        return res;
    }

这种方式是使用先遍历左子树,如果左子树为NULL, 则回退到存在的右节点,再遍历其左子树。

  public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        if (root == null) {
            return res;
        }
        Stack<TreeNode> stack = new Stack<>();
        stack.add(root);
        while(!stack.empty()) {
            TreeNode node = stack.pop();
            res.add(node.val);
            // 先压入右节点,再压入左节点
            if (node.right != null) {
                stack.add(node.right);
            }
            if (node.left != null) {
                stack.add(node.left);
            }
        }
        return res;
    }

也可以采用在压栈时,先压入右节点,再压入左节点。这样在弹出时可以按照先序遍历的顺序从左到右进行弹出。

2. 中根遍历

2.1 中根的递归遍历

   // 由于方法中要方法结果列表,不可直接进行迭代,可以定义全局列表,或定义helper方法。
   List<Integer> res = new ArrayList<>(); 
   public List<Integer> inorderTraversal(TreeNode root) {
        if (root == null) {
          return res;
        }
        inorderTraversal(root.left);
        // 先打印根节点,然后打印左子树,最后再打印右子树
        res.add(root.val);
        inorderTraversal(root.right);
        return res;
    }

2.2 中根的迭代遍历

public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        if (root == null) {
          return res;
        }
        Stack<TreeNode> stack = new Stack<>();
        TreeNode cur = root;
        while(cur != null || !stack.empty()) {
            if (cur != null) {
                stack.push(cur);
                cur = cur.left;
            } else {
                TreeNode node = stack.pop();
                res.add(node.val);
                if(node.right != null) {
                    cur = node.right;
                }
            }
        }
        return res;
    }

中根遍历可以先压左节点,再压右节点。再回退弹出时将其放入列表。

3. 后根遍历

3.1 后根的递归遍历

   // 由于方法中要方法结果列表,不可直接进行迭代,可以定义全局列表,或定义helper方法。
   List<Integer> res = new ArrayList<>(); 
   public List<Integer> postorderTraversal(TreeNode root) {
        if (root == null) {
          return res;
        }
        postorderTraversal(root.left);
        postorderTraversal(root.right);
        // 先打印根节点,然后打印左子树,最后再打印右子树
        res.add(root.val);
        return res;
    }

3.2 后根的迭代遍历

public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        Stack<TreeNode> stack = new Stack<>();
        if(root == null) return res;
        stack.push(root);
        while(!stack.isEmpty()){
            TreeNode cur = stack.pop();
            res.add(cur.val);
            if(cur.left!=null) stack.push(cur.left);
            if(cur.right!=null) stack.push(cur.right);
        }
        Collections.reverse(res);
        return res;
    }

可以采用先压左节点,再压右节点,最后再进行反转列表。另外如果使用的是LinkedList<>,则可以使用addFirst的方法。

public List<Integer> postorderTraversal(TreeNode root) {
        LinkedList<Integer> res = new LinkedList<>();
        Stack<TreeNode> stack = new Stack<>();
        if(root == null) return res;

        stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode cur = stack.pop();
            res.addFirst(cur.val);
            if(cur.left != null) stack.push(cur.left);
            if(cur.right != null) stack.push(cur.right);
        }
        return res;
    }

另外可以将每一个父节点后加一个NULL节点作为标识,在弹出时将其添加到结果List中。

public List<Integer> postorderTraversal(TreeNode root) {
    List<Integer> res = new ArrayList<>();
    if (root == null) {
        return res;
    }
    Stack<TreeNode> stack = new Stack<>();
    stack.add(root);
    while(!stack.empty()) {
        TreeNode node = stack.pop();
        if (node != null) {
            // 将每一个父节点重新压入, 这里相当于进行了两遍的压栈
            stack.push(node);
            stack.push(null);
            if (node.right != null) {
                stack.add(node.right);
            }
            if (node.left != null) {
                stack.add(node.left);
            }
        } else {
            // 将父节点进行添加
            TreeNode r = stack.pop();
            res.add(r.val);
        }
    }
    return res;
}

4. 层次遍历

4.1 基于递归的层次遍历

public List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> res = new ArrayList<>();
    levelHelper(res, root, 0);
    return res;
}

private void levelHelper(List<List<Integer>> res, TreeNode root, int level) {
    if (root == null)
        return;
    // level表示的是层数,如果level >= list.size(),说明到下一层了,所以
    // 要先把下一层的list初始化,防止下面add的时候出现空指针异常
    if (level >= res.size()) {
        res.add(new ArrayList<>());
    }
    // level表示的是第几层,这里访问到第几层,我们就把数据加入到第几层
    res.get(level).add(root.val);
    // 当前节点访问完之后,再使用递归的方式分别访问当前节点的左右子节点
    levelHelper(res, root.left, level + 1);
    levelHelper(res, root.right, level + 1);
}

传入参数为结果list, 遍历节点root, 和遍历的层数level。如果level大于等于res列表中的。

4.2 基于迭代的层次遍历

public List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> result = new ArrayList<>();
    if (root == null) {
        return result;
    }
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(root);
    while (!queue.isEmpty()) {
        int size = queue.size();
        List<Integer> list = new ArrayList<>();
        for (int i=0;i < size; i++) {
            TreeNode node = queue.poll();
            list.add(node.val);
            if (node.left != null) {
                queue.add(node.left);
            }
            if (node.right != null) {
                queue.add(node.right);
            }
        }
        result.add(list);
    }
    return result;
}

使用queue的先进先出的性质,进行层次遍历。

基于树的递归应用问题

由树的定义可知,树的定义是递归的,所以可以通过递归去解决一些类树的问题。使用递归解决问题一般有两种思路:1. 自顶向下。2. 自底向上。

  • 自顶向下意味着在每一个递归层级,先方法当前节点应用计算一些值。
  • 自底向上意味着先进行递归遍历,利用递归的回溯的原理,在递归的回溯过程中应用计算。
  1. 二叉树的最大深度

自顶向下:

int maxDepth = 0;
public int maxDepth(TreeNode root) {
    if (root == null) {
        return 0;
    }
    callDepth(root, 1);
    return maxDepth;
}

private void callDepth(TreeNode root, int level) {
    // 处理特殊null节点,返回特定值。
    if (root == null) {
        return;
    }
    // 如果当前节点满足条件,更新结果值
    if (root.left == null && root.right == null) {
        maxDepth = Math.max(maxDepth, level);
    }
    // 左右子树进行递归
    callDepth(root.left, level + 1);
    callDepth(root.right, level + 1);
}

自底向上:

public int maxDepth(TreeNode root) {
  // 处理特殊的null节点,返回特定值
  if (root == null) {
      return 0;
  }
  // 处理特定值作为递归出口值
  if (root.left == null && root.right == null) {
      return 1;
  }
  // 左右子树进行递归
  return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
  1. 对称的二叉树

自顶向下:

public boolean isSymmetric(TreeNode root) {
   if (root == null) {
       return false;
   }
   return isSymmetricCore(root.left, root.right);
}

private boolean isSymmetricCore(TreeNode left, TreeNode right) {
    if (left == null && right == null) {
        return true;
    }
    if (left == null || right == null || left.val != right.val) {
        return false;
    }
    return isSymmetricCore(left.left, right.right) && isSymmetricCore(left.right, right.left);
}
  1. 路径总和

自低向上

public boolean hasPathSum(TreeNode root, int targetSum) {
    if (root == null) {
        return false;
    }
    if (root.left == null && root.right == null) {
        return targetSum == root.val;
    }
    return hasPathSum(root.left, targetSum - root.val) ||
    hasPathSum(root.right, targetSum - root.val);
}
  1. 由中序和后序遍历构造二叉树

自顶向下:

public TreeNode buildTree(int[] inorder, int[] postorder) {
    if (postorder.length == 0) {
        return null;
    }
    int len = postorder.length;
    return buildTreeCore(inorder, 0, len - 1, postorder, 0, len - 1);
}

private TreeNode buildTreeCore(int[] inorder, int s1, int e1, int[] postorder, int s2, int e2) {
    if (s2 > e2) {
        return null;
    }
    int rootVal = postorder[e2];
    TreeNode root = new TreeNode(rootVal);
    // 算出当前根节点到s1的距离
    int mid = 0;
    while (inorder[s1 + mid] != rootVal) mid++;
    root.left = buildTreeCore(inorder, s1, s1 + mid - 1, postorder, s2, s2 + mid - 1);
    root.right = buildTreeCore(inorder, s1 + mid + 1, e1, postorder, s2 + mid, e2 - 1);
    return root;
}
  1. 由前序和中序遍历生成二叉树
public TreeNode buildTree(int[] preorder, int[] inorder) {
    if (preorder.length == 0) {
        return null;
    }
    int len = preorder.length;
    return buildTreeCore(preorder, 0, len - 1, inorder, 0, len - 1);
}

private TreeNode buildTreeCore(int[] preorder, int s1, int e1, int[] inorder, int s2, int e2) {
    if (s1 > e1) {
        return null;
    }
    int rootVal = preorder[s1];
    TreeNode root = new TreeNode(rootVal);
    if (s1 == e1) {
        return root;
    }
    // 算出当前根节点到s1的距离
    int mid = 0;
    while (inorder[s2 + mid] != rootVal) mid++;
    root.left = buildTreeCore(preorder, s1 + 1, s1 + mid, inorder, s2 , s2 + mid -1);
    root.right = buildTreeCore(preorder, s1 + mid + 1, e1, inorder, s2 + mid + 1, e2);
    return root;
}

由上面的题目可以看出,大部分的算法题目都可以通过这两种方法实现。但是两种方式并不能适应于所有的题目。如果题目可以在当前的任意节点就可以判断出返回的结果,则适合使用自顶向下(增加剪枝效果)。如果题目必须遍历到叶子节点后才能计算出中间值,则需要使用自底向上。当然如果是根据叶子节点的值的状态,或者在遍历过程中并不需要更新结果值,那么这两种方式其实是很混淆的。

基于树的迭代应用问题

基于迭代进行处理,一般围绕层次遍历,先序,后序遍历或中序的迭代遍历进行展开的。

  1. 二叉树的最大深度
public int maxDepth(TreeNode root) {
    if (root == null) {
        return 0;
    }
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(root);
    int count = 0;
    while (!queue.isEmpty()) {
        int size = queue.size();
        while (size -- > 0) {
            TreeNode node = queue.poll();
            if (node.left != null) {
                queue.offer(node.left);
            }
            if (node.right != null) {
                queue.offer(node.right);
            }
        }
        count ++;
    }
    return maxDepth;
}

使用层次遍历的方式实现获取二叉树的深度的算法。

  1. 镜像二叉树
public boolean isSymmetric2(TreeNode root) {
    if (root == null) {
        return false;
    }
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(root.left);
    queue.add(root.right);
    while (!queue.isEmpty()) {
        TreeNode left = queue.poll();
        TreeNode right = queue.poll();

        if (left == null && right == null) {
            continue;
        }
        if (left == null || right == null || left.val != right.val) {
            return false;
        }

        queue.add(left.left);
        queue.add(right.right);
        queue.add(left.right);
        queue.add(right.left);
    }
    return false;
}

只判断为false的情况,为true的情况下进行跳过。

  1. 路径总和
public boolean hasPathSum(TreeNode root, int targetSum) {
        if (root == null) {
            return false;
        }
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        while (!stack.empty()) {
            TreeNode node = stack.pop();
            if (node.left == null && node.right == null) {
                if (targetSum == node.val) return true;
            }
            if (node.left != null) {
                node.left.val = node.left.val + node.val;
                stack.push(node.left);
            }
            if (node.right != null) {
                node.right.val = node.right.val + node.val;
                stack.push(node.right);
            }
        }
        return false;
    }

由于只使用一次,所以可以使用TreeNode 的val作为临时存储值的地方。

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

推荐阅读更多精彩内容