【算法】树

其实,树相关的题是我觉得应该第一个刷的,刷树相关的题目,可以很好的培养我们的递归思维和增加我们对指针操作的熟练度,为我们之后的算法学习,打下一个坚实的基础。

在刷树的相关题目之前,我们先把树的数据结构熟悉一下(这里的树,统一为二叉树,如果是多叉树我们再另说)

public class TreeNode {
    public int val;
    public TreeNode left;
    public TreeNode right;
  
    public TreeNode() {}
  
    public TreeNode(int val) {
        this.val = val;
    }
  
    TreeNode(int val, TreeNode left, TreeNode right) {
          this.val = val;
          this.left = left;
          this.right = right;
    }
}

一、树的递归

104 - 二叉树的最大深度

题目

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

示例:
给定二叉树 [3,9,20,null,null,15,7]

    3
   / \
  9  20
    /  \
   15   7

返回它的最大深度 3 。

思考

树的递归,分为深度优先和广度优先

使用广度优先,我们必须要借助数据结构 队列,需要开辟额外的空间,这是我们不希望发生的

而是用深度优先,我们除了可以使用栈以外,还可以使用递归

因为完全遍历一个树的时间复杂度是 O(n),所以说,两个方法我们选其中空间复杂度小的,即使用 DFS

这里还要多提一句,在实际生产环境中,我们还是建议使用栈来替代递归,一是因为递归一旦出错,难以排错;二是因为递归深度一旦较大,就有出现栈溢出异常的风险

题解

这道题,各位一定要牢牢掌握

这可以说是二叉树中最经典的题目了

如果记不住,背也要把它背下来

public int maxDepth(TreeNode root) {
  if (root==null) return 0;
  return recursion(root,0,Integer.MIN_VALUE);
}

public int recursion(TreeNode root,int depth,int max) {
  // 递归终止条件
  // 在终止判断之前,我们还需要比较当前深度与之前记录的最大深度
  if (root==null) {
    if (depth>max) {
      max=depth;
    }
    return max;
  }
  int lMax=recursion(root.left,depth+1,max);
  int rMax=recursion(root.right,depth+1,max);
  return Math.max(lMax,rMax);
}

写成上面那样,只是为了让各位看得清楚一点

其实,这道题完全可以进行写成一行代码的骚操作:

public int maxDepth(TreeNode root) {
		return root==null ? 0 : Math.max(maxDepth(root.left)+1,maxDepth(root.right)+1);
}

110 - 平衡二叉树

题目

给定一个二叉树,判断它是否是高度平衡的二叉树。

本题中,一棵高度平衡二叉树定义为:

一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。

示例 1:

图1

输入:root = [3,9,20,null,null,15,7]
输出:true

示例 2:

图2

输入:root = [1,2,2,3,3,null,null,4,4]
输出:false

示例 3:

输入:root = []
输出:true
思考

我们的第一反应,肯定是找到树的最大深度和最小深度,如果最大最小深度差值不超过1,说明整棵树肯定是平衡的,反之则不平衡

但是,这种方法,需要进行两次遍历,才能得出结果,明显不是我们希望的

我们其实可以采取分治的思想,将问题由大化小

一颗子树,如果其左子树和右子树最长边的高度差,如果不大于1,则当前子树是符合规范的

但是前提是,该子树的子树,直到叶子节点,也都必须是合乎规范的

基于这个逻辑,我们就可以编写出递归的代码

题解
public boolean isBalanced(TreeNode root) {
  return recursion(root)==-1?false:true;
}

public int recursion(TreeNode root) {
  if (root==null) return 0;
  int lMax = recursion(root.left);
  int rMax = recursion(root.right);
  // 如果当前子树不符合规范,直接返回-1,达到剪枝的效果
  if (lMax==-1 || rMax==-1 || Math.abs(lMax-rMax)>1) return -1;

  // 这里的 +1,可以理解为把当前层加上去
  return Math.max(lMax,rMax)+1;
}

543 - 二叉树的直径

题目

给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。

示例 :
给定二叉树

      1
     / \
    2   3
   / \     
  4   5    

返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。

思考

这道题看似唬人,其实,将问题转换一下,就是找出左子树最大深度和右子树最大深度之和最大的节点

这里我们要注意,二叉树直径长度,是比路径长度小1的,这通过观察二叉树的数据结构可以看出来

直径长度

所以,如果我们找到的是路径长度的话,最后的直径长度要在路径长度的基础上减1

题解

在做这道题的时候,我们不得不去使用一个全局变量

如果是 c++ 玩家的话,可以用引用变量区避免这个问题

java 玩家,其实也可以自己设计一个类,去包装这个参数

这里,我还是使用一个全局变量

private int max;
public int diameterOfBinaryTree(TreeNode root) {
  recursion(root);
  // 我们要求的是直径,不是路径长度
  return max-1;
}

public int recursion(TreeNode root) {
  if (root==null) return 0;
  int lMax = recursion(root.left);
  int rMax = recursion(root.right);
  if (lMax+rMax+1>max) max=lMax+rMax+1;

  /**
		* 返回当前子树最长的边
    * 这里 +1,是为了加上当前的层
		*/
  return Math.max(lMax,rMax)+1;
}

101 - 对称二叉树

题目

给定一个二叉树,检查它是否是镜像对称的。

例如,二叉树 [1,2,2,3,4,4,3] 是对称的。

    1
   / \
  2   2
 / \ / \
3  4 4  3

但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:

    1
   / \
  2   2
   \   \
   3    3
思考

我们可以先找一个符合题意的镜像二叉树的特例

就拿 例子1 举例吧

我们对它进行中序遍历,可以发现,遍历顺序是 3,2,4,1,4,2,3,也是对称的,这就衍生出我们的第一个思路,即对二叉树进行中序遍历,记录遍历的节点,然后判断记录下来的节点是否是回文的

但是这种方法毕竟还是需要开辟额外的空间,第二种方法,我们尝试使用递归,题解我们也用递归去写

题解
public boolean isSymmetric(TreeNode root) {
  return root==null?true:recursion(root.left,root.right);
}

public boolean recursion(TreeNode left,TreeNode right) {
  if (left==null && right==null) return true;
  if (left==null || right==null) return false;
  if (left.val != right.val) return false;
  // 这里要注意,因为是镜像的,所以要注意对比的节点的匹配
  return recursion(left.left,right.right) &&
    recursion(left.right,right.left);
}

226 - 翻转二叉树

My personal favourite

关于这道题,有个小故事,macos 最泛用的程序–homebrew 的作者去应聘 google 的职位,但是因为写不出翻转二叉树的代码,最后被面试官当场怼了

原推截图

那我们就来看看,这个让大佬也为难的题目,究竟长什么样子

题目

翻转一棵二叉树。

示例:

输入:

     4
   /   \
  2     7
 / \   / \
1   3 6   9

输出:

     4
   /   \
  7     2
 / \   / \
9   6 3   1
思考

我们我们可以让左右子树交换

然后左右子树在进行递归反转

(当然,这个顺序反过来也无所谓)

题解
public TreeNode invertTree(TreeNode root) {
    if (root==null) return root;
    // 翻转
    recursion(root);
    return root;
}

public void recursion(TreeNode root) {
    if (root==null) return;
    recursion(root.left);
    recursion(root.right);
    TreeNode tmp = root.left;
    root.left=root.right;
    root.right=tmp;
}

有同学可能会说了:“我这到很快就 AC 了,能去 google 了吗?”

骚年,你还差写一个 homebrew 出来 : )

二、树的层次遍历

103 - 二叉树的锯齿形层次遍历

这道题可是经典字节

字节跳动特别喜欢考这题

题目

给定一个二叉树,返回其节点值的锯齿形层次遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。

例如:
给定二叉树 [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7

返回锯齿形层次遍历如下:

[
  [3],
  [20,9],
  [15,7]
]
思考

还是层次遍历

还是要记录每层的信息

区别在于,我们这里还要记录层数

奇数层从左往右遍历

偶数层从右往左遍历

题解
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
        List<List<Integer>> res = new ArrayList<>();
        if (root==null) return res;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        int lay = 1;
        while (!queue.isEmpty()) {
            List<Integer> rowList = new ArrayList<>();
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                TreeNode tmp = queue.poll();
                if (tmp.left!=null) queue.offer(tmp.left);
                if (tmp.right!=null) queue.offer(tmp.right);
                rowList.add(tmp.val);
            }
            // 如果是偶数层的话,需要反转
            if (lay%2==0) Collections.reverse(rowList);
            lay++;
            res.add(rowList);
        }
        return res;
    }

637 - 二叉树的层平均值

题目

给定一个非空二叉树, 返回一个由每层节点平均值组成的数组。

示例 1:

输入:
    3
   / \
  9  20
    /  \
   15   7
输出:[3, 14.5, 11]
解释:
第 0 层的平均值是 3 ,  第1层是 14.5 , 第2层是 11 。因此返回 [3, 14.5, 11] 。
思考

这道题要求我们在做到层次遍历的同时,还要记录每层的数据

想要实现这个效果,是有套路的:

1、使用队列进行存储层次遍历信息

2、每次在进行队列非空判断之前,都要先记录下队列的大小,这个大小,就是当前层的节点个数

题解
public List<Double> averageOfLevels(TreeNode root) {
        List<Double> res = new ArrayList<>();
        if (root==null) return res;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            // 要记录队列的长度,因为这是当前层的节点个数
            int size = queue.size();
            // 用来存储当前层的所有节点的值的和
            Double sum = 0d;
            for (int i = 0; i < size; i++) {
                TreeNode tmp = queue.poll();
                sum+=tmp.val;
                if (tmp.left!=null) queue.offer(tmp.left);
                if (tmp.right!=null) queue.offer(tmp.right);
            }
            res.add(Double.valueOf(sum/size));
        }
        return res;
    }

二叉树的层次遍历方法,一定要牢记,很多地方都会用到的

三、二叉树的前中后序遍历

前中后序的遍历是二叉树最最基础的内容,我在这里就不过多赘述了

感兴趣的小伙伴,可以上网查找相关信息

144 - 二叉树的前序遍历

我们先来看一看,二叉树的前序遍历是个什么样子的

题目

前序遍历应该都知道是什么吧?

我这里就不放题目了

思考

递归的版本想必大家都熟悉

这里,我们要尝试不使用迭代

使用迭代要注意节点入栈的顺序

题解
  • 递归版
public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        preOrderRecurson(root,list);
        return list;
    }

    public void preOrderRecurson(TreeNode root,List<Integer> list) {
        if (root==null) return;
        list.add(root.val);
        preOrderRecurson(root.left,list);
        preOrderRecurson(root.right,list);
    }
  • 非递归版

使用栈,可以实现非递归的前序遍历

public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        if (root==null) return res;
        LinkedList<TreeNode> stack = new LinkedList<>();
        stack.addLast(root);
        while (!stack.isEmpty()) {
            TreeNode tmp = stack.removeLast();
            res.add(tmp.val);
            // 我们想实现的是前序遍历,那栈里面要先放右,再放左
            if (tmp.right!=null) stack.addLast(tmp.right);
            if (tmp.left!=null) stack.addLast(tmp.left);
        }
        return res;
    }

迭代版本的入栈顺序

105 - 使用前序与中序遍历序列构造二叉树

题目

根据一棵树的前序遍历与中序遍历构造二叉树。

注意:
你可以假设树中没有重复的元素。

例如,给出

前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]

返回如下的二叉树:

    3
   / \
  9  20
    /  \
   15   7
思考

二叉树中,我最讨厌的类型,就是根据不同序列构造二叉树这类题了

解题思路我也不想写了,就放一个别人的吧

https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/solution/dong-hua-yan-shi-105-cong-qian-xu-yu-zhong-xu-bian/

这个思路不是最优解,因为需要开辟额外空间,其实还有更优的解法

但是,这个思路是最容易想,也最不容易出错的,因为最优解的思路,函数形参超级多,写着写着脑袋就晕了

题解
public TreeNode buildTree(int[] preorder, int[] inorder) {
        if (preorder.length==0 || inorder.length==0) return null;
        // 前序序列的第一个元素,一定是整棵树的根节点
        TreeNode root = new TreeNode(preorder[0]);
        for (int i = 0; i < inorder.length; i++) {
            /**
             * 找到中序遍历中,根节点的位置
             * 此时,i 就是左子树的长度
             * inorder.length - (i+1) 就是右子树的长度
             */
            if (preorder[0]==inorder[i]) {
                int[] pre_left = Arrays.copyOfRange(preorder, 1, i+1);
                int[] pre_right = Arrays.copyOfRange(preorder, i + 1, preorder.length);

                int[] in_left = Arrays.copyOfRange(inorder, 0, i);
                int[] in_right = Arrays.copyOfRange(inorder, i + 1, inorder.length);
                root.left=buildTree(pre_left,in_left);
                root.right=buildTree(pre_right,in_right);
                break;
            }
        }
        return root;
    }

四、二叉查找树

二叉查找树(Binary Search Tree, BST)是一种特殊的二叉树:

对于每个父节点,其左子节点 的值小于等于父结点的值,其右子节点的值大于等于父结点的值。因此对于一个二叉查找树,我 们可以在 O(n log n) 的时间内查找一个值是否存在。从根节点开始,若当前节点的值大于查找值 则向左下走,若当前节点的值小于查找值则向右下走。同时因为二叉查找树是有序的,对其中序遍历的结果即为排好序的数组(这一性质,经常是解二叉搜索树相关题目的关键)。

99 - 恢复二叉搜索树(未完成)

题目

给你二叉搜索树的根节点 root ,该树中的两个节点被错误地交换。请在不改变其结构的情况下,恢复这棵树。

**进阶:**使用 O(n) 空间复杂度的解法很容易实现。你能想出一个只使用常数空间的解决方案吗?

示例 1:

图1

输入:root = [1,3,null,null,2]
输出:[3,1,null,null,2]
解释:3 不能是 1 左孩子,因为 3 > 1 。交换 1 和 3 使二叉搜索树有效。

示例 2:

图2

输入:root = [3,1,4,null,null,2]
输出:[2,1,4,null,null,3]
解释:2 不能在 3 的右子树中,因为 2 < 3 。交换 2 和 3 使二叉搜索树有效。
思考

要做这道题,我们可以先看看有序数组中,两个值被调换位置,是个什么情况:

  • 非相邻位置

我们交换 2 和 6 两个数

原:1,2,3,4,5,6,7

换:1,6,3,4,5,2,7

我们发现,会出现 6>35>2两个异常位置

  • 相邻位置

我们交换 3 和 4 两个数

原:1,2,3,4,5,6,7

换:1,2,4,3,5,6,7

可以发现,相邻位置数字交换,只会出现一个异常位

这样我们就可以思考解题方法了:

中序遍历整个树,找到两个

题解

这道题我就不写了,留给各位做思考题。

669 - 修剪二叉搜索树

题目

给你二叉搜索树的根节点 root ,同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在[low, high]中。修剪树不应该改变保留在树中的元素的相对结构(即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在唯一的答案。

所以结果应当返回修剪好的二叉搜索树的新的根节点。注意,根节点可能会根据给定的边界发生改变。

示例 1:

图1

输入:root = [1,0,2], low = 1, high = 2
输出:[1,null,2]

示例 2:

图2

输入:root = [3,0,4,null,2,null,null,1], low = 1, high = 3
输出:[3,2,null,1]

示例 3:

输入:root = [1], low = 1, high = 2
输出:[1]

示例 4:

输入:root = [1,null,2], low = 1, high = 3
输出:[1,null,2]

示例 5:

输入:root = [1,null,2], low = 2, high = 4
输出:[2]
思考

如果光看题目描述,这道题确实很难看懂

但是有那两张图,就一目了然了

我们还是进行中序遍历

当遍历到某个节点 node

如果 node 的左节点值小于 low,舍弃左节点

如果 node 的右节点值大于 high,舍弃右节点

因为有 图2 那种情况

图2

我们还需要在剪左节点的时候,考虑左节点的右子树

在剪右节点的时候,考虑右节点的左子树

题解
public TreeNode trimBST(TreeNode root, int low, int high) {
        if (root==null) return null;
        // 如果当前节点的值小于 low ,结果肯定在右枝出现
        if (root.val<low) return trimBST(root.right,low,high);
        // 如果当前节点的值大于 high ,结果肯定在左枝出现
        if (root.val>high) return trimBST(root.left,low,high);

        root.left=trimBST(root.left,low,high);
        root.right=trimBST(root.right,low,high);
        return root;
}

五、字典树

字典树(Trie)用于判断字符串是否存在或者是否具有某种字符串前缀。

字典树

如果有大量的英文单词存储请求,使用字典树,可以极大的压缩存储空间,并且查找的时间复杂度是 O(N)<N是英文单词的平均长度>

208 - 实现 Tire Tree

题目

请你实现 Trie 类:

  • Trie() 初始化前缀树对象。
  • void insert(String word) 向前缀树中插入字符串 word
  • boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false
  • boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false

示例:

输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出
[null, null, true, false, true, null, true]

解释
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple");   // 返回 True
trie.search("app");     // 返回 False
trie.startsWith("app"); // 返回 True
trie.insert("app");
trie.search("app");     // 返回 True
思考

其实就是个字典树,题中说了word 和 prefix 仅由小写英文字母组成,所以我们还可以把它看做是一颗26叉树,也就是说每个节点最多有26个子节点

我们先来看一下单个节点的数据结构:

class Trie {
    private Trie[] children;
    private boolean isEnd;

    public Trie() {
        children = new Trie[26];
        isEnd = false;
    }
}

其成员变量为两个,一个表示26个字母的节点个数

一个用来标识当前位置是不是字母的结尾

TrieTree 的属性中,没有用来表示字符的,这是因为,可以通过父节点,获取子节点的字符信息(父节点在 1 号位存值,说明是存入了字母 A,2 号位存值,说明是存入了字母 B 以此类推)

也正是因为如此,我们的根节点不能存储字符数据

了解了 Trie 的数据结构后,我们就可以开始来写一写题了

题解

其实按照一般思路,我们的 Trie 节点应该和 TrieTree 是分开来的

就像是二叉树中,我们的 BinTree 和 TreeNode 都是分开的

但是力扣中为了将题目写在一个类中,就把二者并在一块儿了

那我们的根节点该怎么处理?

这里,我们就将当前对象作为根节点(一会儿可以看一下 insert 函数的处理)

class Trie {
      private Trie[] children;
      private boolean isEnd;

    /** Initialize your data structure here. */
    public Trie() {
        children=new Trie[26];
        isEnd=false;
    }
    
    /** Inserts a word into the trie. */
    public void insert(String word) {
        // 将当前对象作为根节点
        Trie node = this;
        if (word==null || word.length()==0) return;
        for (int i = 0; i < word.length(); i++) {
            // 将字符的信息,转换为下标的信息
            int charIndex = word.charAt(i) - 'a';
            if (node.children[charIndex]==null) {
                node.children[charIndex]=new Trie();
            }
            node=node.children[charIndex];
        }
        // 插入到最后位置的字符,要标注 isEnd
        node.isEnd=true;
    }
    
    /** Returns if the word is in the trie. */
    public boolean search(String word) {
        Trie trie = find(word);
        if (trie==null) return false;
        return trie.isEnd;
    }
    
    /** Returns if there is any word in the trie that starts with the given prefix. */
    public boolean startsWith(String prefix) {
        Trie trie = find(prefix);
        return trie!=null;
    }

  /**
   * 我们需要自己去写一个工具函数
   * 这和函数的作用,是去查找传入的字符串的最后一个字符所在的节点
   * 如果没有该字符串,则返回 null
   * @param word
   * @return
   */
    public Trie find(String word) {
        Trie node = this;
        if (word==null || word.length()==0) return null;
        for (int i = 0; i < word.length(); i++) {
            int charIndex = word.charAt(i) - 'a';
            if (node.children[charIndex]==null) return null;
            node=node.children[charIndex];
        }
        return node;
    }
}

TrieTree

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FARO_Z

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值