LeetCode - 二叉树、图

一. 二叉树

1. 树的递归

例题 104. 二叉树的最大深度。给定一个二叉树,找出其最大深度。

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

例题 110,平衡二叉树。给定一个二叉树,判断它是否是高度平衡的二叉树。本题中,一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。

  • 先求树的高度,再判断是否平衡。
class Solution {
    public boolean isBalanced(TreeNode root) {
        if (root == null) {
            return true;
        }
        if (Math.abs(depth(root.left) - depth(root.right)) > 1) {
            return false;
        }
        //继续递归判断
        return isBalanced(root.left) && isBalanced(root.right);
    }

	//树的高度
    public int depth(TreeNode node) {
        if (node == null) {
            return 0;
        }
        return Math.max(depth(node.left) + 1, depth(node.right) + 1);
    } 
}
  • 优化,当计算高度时发现已经不满足要求时可以剪枝。
class Solution {
    public boolean isBalanced(TreeNode root) {
         return depth(root) != -1;
    }

    public int depth(TreeNode node) {
        if (node == null) {
            return 0;
        }
        int l = depth(node.left);
        int r = depth(node.right);
        //无需继续计算,剪枝
        if (l == -1 || r == -1 || Math.abs(l - r) > 1) {
            return -1;
        }
        return Math.max(l, r) + 1;
    } 
}

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

  • 最大值为节点的左节点的高度+右节点高度;
  • 因此,在求节点高度的时候,不断更新直径即可。
class Solution {
    int diameter = 0;
    public int diameterOfBinaryTree(TreeNode root) {
        if (root == null) {
            return 0;
        }
        depth(root);
        return diameter;
    }

    public int depth(TreeNode node) {
        if (node == null) {
            return 0;
        }
        int l = depth(node.left);
        int r = depth(node.right);
        diameter = Math.max(l + r, diameter);
        return Math.max(l, r) + 1;
    }
}

例题 437. 路径总和 III。给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

  • 由于起始结点不一定从根节点开始,因此在节点处就需要遍历或递归;
  • 由于节点的值可能是负数,因此辅助函数也不能 node.val==targetSum 就返回,因为继续往下遍历可能还有满足条件的路径。
class Solution {
    public int pathSum(TreeNode root, int targetSum) {
        if (root == null) {
            return 0;
        }
        //注意这里需要以每个结点为起始结点进行遍历
        return sumToTarget(root, targetSum) + pathSum(root.left, targetSum) + pathSum(root.right, targetSum);
    }

    public int sumToTarget(TreeNode node, int targetSum) {
        if (node == null) {
            return 0;
        }
        //注意这里也不能 node.val==targetSum 就返回,因为继续往下遍历可能还有满足条件的路径
        int count = node.val == targetSum ? 1 : 0;
        count += sumToTarget(node.left, targetSum - node.val);
        count += sumToTarget(node.right, targetSum - node.val);
        return count;
    }
}

例题 101. 对称二叉树。给你一个二叉树的根节点 root , 检查它是否轴对称。

  • 开始想的是中序遍历结果对称,但实际不是;
  • 其实就是左节点的值要等于右节点的值,辅函数传入两个参数,分别代表左节点和右节点。(最开始想的是传入一个参数,不太好做)
class Solution {
    public boolean isSymmetric(TreeNode root) {
        if (root == null) {
            return true;
        }
        return symmetric(root.left, root.right);
    }

    public boolean symmetric(TreeNode left, TreeNode right) {
        if (left == null && right == null) {
            return true;
        }
        if (left == null || right == null) {
            return false;
        }
        return left.val == right.val && symmetric(left.left, right.right) && symmetric(left.right, right.left); 
    }
}

例题 1110. 删点成林。给出二叉树的根节点 root,树上每个节点都有一个不同的值。如果节点值在 to_delete 中出现,我们就把该节点从树上删去,最后得到一个森林(一些不相交的树构成的集合)。返回森林中的每棵树。你可以按任意顺序组织答案。

  • 重要的地方在于两点,第一需要后序遍历整棵树,不然遇到删除节点后,其子节点可能没有遍历完就返回了。(开始想的是用 stack 存储拆分出的节点,while循环stack,这样倒是不一定需要后序遍历);
  • 第二需要知道怎么断“删除节点”的上层指针,开始一直整不明白,都已经到node了,咋返回去删指针。看到解法是 node.left = trace(node.left, set, retList); 才恍然大悟!
class Solution {
    public List<TreeNode> delNodes(TreeNode root, int[] to_delete) {
        //set比list快很多
        Set<Integer> set = new HashSet<>();
        for (int delete : to_delete) {
            set.add(delete);
        }
        List<TreeNode> retList = new ArrayList<>();
        retList.add(root);
        trace(root, set, retList);
        return retList;
    }
	
	//带返回值的后序遍历
    public TreeNode trace(TreeNode node, Set<Integer> set, List<TreeNode> retList) {
        if (node == null) {
            return node;
        }
        //如果删除了节点,可以断上层指针
        node.left = trace(node.left, set, retList);
        node.right = trace(node.right, set, retList);
        if (set.contains(node.val)) {
            retList.remove(node);
            if (node.left != null) {
                retList.add(node.left);
            }
            if (node.right != null) {
                retList.add(node.right);
            }
            node = null;
        }
        return node;
    }
}

2. 层次遍历

例题 637. 二叉树的层平均值。给定一个非空二叉树的根节点 root , 以数组的形式返回每一层节点的平均值。与实际答案相差 10-5 以内的答案可以被接受。

  • 简单的层次遍历题,注意一下 LinkedList 表示的队列的插入和弹出的方法。
public List<Double> averageOfLevels(TreeNode root) {
        List<Double> retList = new ArrayList<>();
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);
        while (!queue.isEmpty()) {
            int size = queue.size();
            double sum = 0;
            for (int i = 0; i < size; i++) {
                TreeNode node = queue.poll();
                if (node.left != null) {
                    queue.offer(node.left);
                }
                if (node.right != null) {
                    queue.offer(node.right);
                }
                sum += node.val;
            }
            retList.add(sum / size);
        }
        return retList;
    }

3. 前中后序遍历

例题 105. 从前序与中序遍历序列构造二叉树。给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。

  • 思路是先查前序节点,再找前序节点在中序列表中的位置,以该节点为根节点,该位置左边的都在其左节点处,右边的都在其右节点处。
  • 由于是重建树,因此递归内部需要不断创建节点。以当前遍历到的前序列表中的元素为值创建节点,需要精确找到其左节点和右节点的元素。左节点所在前序数组中的位置就是i+1,右节点所在前序数组中的位置是当前节点的位置+左边数组长度+1。
class Solution {
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        if (preorder.length == 0) {
            return null;
        }
        //用map存储中序数组的位置关系,方便查找
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < inorder.length; i++) {
            map.put(inorder[i], i);
        }
        TreeNode root = build(map, preorder, 0, 0, inorder.length - 1);
        return root;
    }

    //i表示元素在前序中的位置,x表示中序数组的左端点,y表示中序数组的右端点,记录x y 主要是为了计算左右子树长度,用于寻找节点的右节点
    public TreeNode build(Map<Integer, Integer> map, int[] preorder, int i, int x, int y) {
        if (x > y || i >= preorder.length) {
            return null;
        }
        //tmp为当前元素在中序数组中的位置
        int val = preorder[i];
        int tmp = map.get(val);
        //创建以当前元素为值的节点
        TreeNode node = new TreeNode(val);
        //左节点所在前序数组中的位置就是i+1
        node.left = build(map, preorder, i+1, x, tmp-1);
        //右节点所在前序数组中的位置是当前节点的位置+左节点数组长度+1, i+tmp-x+1
        node.right = build(map, preorder, i+tmp-x+1, tmp+1, y);
        return node;
    }
}

例题 889. 根据前序和后序遍历构造二叉树。给定两个整数数组,preorder 和 postorder ,其中 preorder 是一个具有 无重复 值的二叉树的前序遍历,postorder 是同一棵树的后序遍历,重构并返回二叉树。如果存在多个答案,您可以返回其中任何 一个。

  • 和上题一样,这种根据两个遍历顺序重建二叉树的题目,思想就是精确找到当前节点的左右子节点,及当前节点的左子树边界和右子树边界。
class Solution {
    public TreeNode constructFromPrePost(int[] preorder, int[] postorder) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < postorder.length; i++) {
            map.put(postorder[i], i);
        }
        TreeNode root = contruct(preorder, map, 0, 0, postorder.length-1);
        return root;
    }

    public TreeNode contruct(int[] preorder, Map<Integer, Integer> map, int i, int x, int y) {
        if (x > y) {
            return null;
        }
        if (x == y) {
            return new TreeNode(preorder[i]);
        }
        TreeNode node = new TreeNode(preorder[i]);
        int ind = map.get(preorder[i+1]);
        //找到当前节点的左节点位置为 i+1,及当前节点左子树的两端边界
        node.left = contruct(preorder, map, i+1, x, ind);
        //找到当前节点的右节点位置为 ind-x-i+2,及当前节点右子树的两端边界
        node.right = contruct(preorder, map, ind-x+i+2, ind+1, y-1);
        return node;
    }
}

例题 106,从中序与后序遍历序列构造二叉树。给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。

  • postorder 可以确定中心节点,inorder 可以确定左右子树;
class Solution {
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < inorder.length; i++) {
            map.put(inorder[i], i);
        }
        return build(postorder, map, postorder.length-1, 0, postorder.length-1);
    }

    //i 表示当前节点在postorder中的位置,x y表示当前子树在 inorder中的边界
    public TreeNode build(int[] postorder, Map<Integer, Integer> map, int i, int x, int y) {
        if (x > y) {
            return null;
        }
        TreeNode node = new TreeNode(postorder[i]);
        //post中的元素在inorder中的位置
        int ind = map.get(postorder[i]);
        //求右子树长度,用于找左子节点
        int rightLen = y - ind;
        node.left = build(postorder, map, i-rightLen -1, x, ind - 1);
        node.right = build(postorder, map, i-1, ind +1, y);
        return node;
    }
}

例题 144. 二叉树的前序遍历。给你二叉树的根节点 root ,返回它节点值的 前序 遍历。

  • 递归的本质是栈,所以可以通过栈来解决;
  • 另外需要先添加右子树,再添加左子树,才能保证左子树先遍历
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> resList = new ArrayList<>();
        if (root == null) {
            return resList;
        }
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        while(!stack.isEmpty()) {
            TreeNode node = stack.pop();
            resList.add(node.val);
            if (node.right != null) {
                stack.push(node.right);
            }
            if (node.left != null) {
                stack.push(node.left);
            }  
        }
        return resList;
    }
  • 更通用的方法处理前序、中序、后序遍历
  • 前序:先遍历左节点并将节点入栈,再弹出栈中节点访问其右节点。
//前序和中序
public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> resList = new ArrayList<>();
        if (root == null) {
            return resList;
        }
        Stack<TreeNode> stack = new Stack<>();
        TreeNode node = root;
        while(node != null || !stack.isEmpty()) {
            //while循环不断深入寻找左节点
            while (node != null) {
                resList.add(node.val); //前序加的位置
                //stack是为了存储节点用于后续弹出遍历其右节点
                stack.push(node);
                node = node.left;
            }
            node = stack.pop();
            //resList.add(node.val); //中序加的位置
            node = node.right;
        }
        return resList;
    }
  • 后序与前序的差别在于是先遍历右节点并将节点压入栈内,再出栈遍历左节点,最后反向输出。
//后序
public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> retList = new ArrayList<>();
        if (root == null) {
            return retList;
        }
        Stack<TreeNode> stack = new Stack<>();
        TreeNode node = root;
        while(node != null || !stack.isEmpty()) {
            while (node != null) {
                retList.add(node.val);
                stack.push(node);
                node = node.right;
            }
            node = stack.pop();
            node = node.left;
        }
        // 反转retList
		Collections.reverse(retList);
        return retList;
    }

4. 二叉查找树

  • 二叉查找树又叫二叉搜索树,每个节点的左子节点的值都小于等于当前节点,右子节点的值都大于等于当前节点
  • 中序遍历结果是顺序的

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

  • 最简单的方法可以先中序遍历这棵树,将节点存储在list中,再交换不是递增的节点;
  • O(1) 空间复杂度的做法是将 list 换成指针,用一个指针指向当前节点的前一节点,用于判断是否递增,再用两个节点分别存储最后需要交换的节点。中序遍历完成后,交换节点即可。
class Solution {
    /**
    中序遍历结果应该要是顺序的
     */
    TreeNode t1 = null;
    TreeNode t2 = null;
    TreeNode preNode = null;
    public void recoverTree(TreeNode root) {
        if (root == null) {
            return;
        }
        inorder(root);
        int tmp = t1.val;
        t1.val = t2.val;
        t2.val = tmp;
    }

    public void inorder(TreeNode node) {
        if (node == null) {
            return;
        }
        inorder(node.left);
        if (preNode != null && node.val < preNode.val) {
            //t1是记录首次出现降序的位置
            if (t1 == null) {
                t1 = preNode;           
            }
            //t2不断更新,记录末次出现降序的位置
            t2 = node;
        }
        preNode = node;
        inorder(node.right);
    }
}

注:这里注意一个问题,由于对 java 是值传递还是引用传递了解的不是特别透彻,因此开始是将代码中的 t1、t2、preNode 都作为参数传入 inorder 方法,结果发现执行完 inorder 之后主函数的 t1 和 t2 还是 null,并没有起到记录节点的作用。

百度后发现 Java 实际上只有值传递,并没有引用传递!!!首先基本数据类型都是值传递,这个毫无疑问。那对象类型是值传递还是引用传递呢?实际上也是值传递,那有人会问了,我传递一个 User 对象到一个方法中,方法里对 User 的 name 字段进行了修改,方法外该对象的 name 确实也被修改了,这不是引用传递吗?解释一下这个的原因:实际上 Java 每定义一个变量,都是在栈中存储,基本数据类型变量的值就是它定义的值,引用变量的值是它指向的对象的地址(对象是在堆中new出来的)。传参时,Java 都是 copy 一份变量的值传递进去,因此对象变量传递的是它指向对象的地址。上述 User 对象改变是对象本身的内容发生了变化,而变量的指向一直没有变化。比如方法内不是直接对对象进行修改,而是重新 new 了一个对象再修改,那么方法外的对象是不会有变化的。这就能理解为什么将 t1、t2 传入方法后最后出来还是 null,因为它的指向一直都是 null,方法里并没有改变null,而是改变了形参的指向。
参考:https://www.jb51.net/article/210037.htm

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

  • 出现二叉查找树就要知道需要利用它的特性进行优化剪枝;
  • 像这种会修改树结构的题一般都是 node.left = dfs(xx,xxx); node.right = dfs(xx,xxx); 来断连节点。
public TreeNode trimBST(TreeNode root, int low, int high) {
        if (root == null) {
            return root;
        }
        TreeNode node = root;
        if (node.val < low) {
            //去除当前节点,并且只需要继续查找其右子树
            return trimBST(node.right, low, high);
        } else if (node.val > high){
            //去除当前节点,并且只需要继续查找其左子树
            return trimBST(node.left, low, high);
        } else {
            //在范围内,保留当前节点,继续遍历左右子树
            node.left = trimBST(node.left, low, high);
            node.right = trimBST(node.right, low, high);
        }
        return node;
    }

5. 字典树

  • 字典树(Trie)用于判断字符串是否存在或者是否具有某种字符串前缀。可用于单词搜索、自动补完和拼写检查。

例题 208. 实现 Trie (前缀树)
Trie(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。请你实现 Trie 类:

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

解法:

  • 要创建字典树,最重要的是要想好应该建立怎样的 TrieNode 结构,如果 TrieNode 直接存储单词,那么会需要不断重建树(因为后面插入的单词可能是已插入单词的前缀);因此想到 TrieNode 只存储字母,结构见代码。
  • 建立字典树时并不是直接为插入的字符串建立节点,而是要为每个字母建立节点比如插入apple,应该插入 a->p->p->l->e 5个节点,然后在最后的 e 处插入标记,表明这是一个单词。
class Trie {
    //内部类,字典树节点
    private class TrieNode {
        private boolean isEnd; //标记到当前节点是否是一个插入的单词
        private TrieNode[] next; //子节点,最多有26个不同的字母
        public TrieNode() {
            isEnd = false;
            next = new TrieNode[26];
        }
    }
    
    //字典树的根节点
    private TrieNode root;

    public Trie() {
        root = new TrieNode();
    }
    
    public void insert(String word) {
        TrieNode node = root;
        //遍历word的每个字母,为每个字母建立节点
        for (int i = 0; i < word.length(); i++) {
            int c = word.charAt(i) - 'a';
            if (node.next[c] == null) {
                node.next[c] = new TrieNode();
            }
            node = node.next[c];
        }
        //为单词打上标记
        node.isEnd = true;
    }
    
    public boolean search(String word) {
        TrieNode node = root;
        for (int i = 0; i < word.length(); i++) {
            int c = word.charAt(i) - 'a';
            if (node.next[c] == null) {
                return false;
            }
            node = node.next[c];
        }
        return node.isEnd;
    }
    
    public boolean startsWith(String prefix) {
        TrieNode node = root;
        for (int i = 0; i < prefix.length(); i++) {
            int c = prefix.charAt(i) - 'a';
            if (node.next[c] == null) {
                return false;
            }
            node = node.next[c];
        }
        return true;
    }
}

6. 其他

例题 226. 翻转二叉树。给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

  • 这道题的核心在于你得先保存 rootleft 节点,用未改变的 rootleft 传入第二次递归参数中。
public TreeNode invertTree(TreeNode root) {
        if (root == null) {
            return root;
        }
        TreeNode rootleft = root.left;
        root.left = invertTree(root.right);
        root.right = invertTree(rootleft);
        return root;
    }

例题 617. 合并二叉树。给你两棵二叉树: root1 和 root2 。

想象一下,当你将其中一棵覆盖到另一棵之上时,两棵树上的一些节点将会重叠(而另一些不会)。你需要将这两棵树合并成一棵新二叉树。合并的规则是:如果两个节点重叠,那么将这两个节点的值相加作为合并后节点的新值;否则,不为 null 的节点将直接作为新二叉树的节点。返回合并后的二叉树。注意: 合并过程必须从两个树的根节点开始。

public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
        if (root1 == null || root2 == null) {
            return root1 == null ? root2 : root1;
        }
        root1.val = root1.val + root2.val;
        root1.left = mergeTrees(root1.left, root2.left);
        root1.right = mergeTrees(root1.right, root2.right);
        return root1;
    }

例题 572. 另一棵树的子树。给你两棵二叉树 root 和 subRoot 。检验 root 中是否包含和 subRoot 具有相同结构和节点值的子树。如果存在,返回 true ;否则,返回 false 。二叉树 tree 的一棵子树包括 tree 的某个节点和这个节点的所有后代节点。tree 也可以看做它自身的一棵子树。

  • 注意这道题是说 root 不仅需要包含 subRoot 结构,root 本身也需要到达叶子节点,即 subroot 不能只是 root 中间的一部分。
class Solution {
    public boolean isSubtree(TreeNode root, TreeNode subRoot) {
        if (subRoot == null && root == null) {
            return true;
        } else if (subRoot == null || root == null) {
            return false;
        }
        return isSub(root, subRoot) 
        || isSubtree(root.left, subRoot) 
        || isSubtree(root.right, subRoot);
    }

    public boolean isSub(TreeNode root, TreeNode subRoot) {
        if (subRoot == null && root == null) {
            return true;
        } else if (subRoot == null || root == null) {
            return false;
        }
        return root.val == subRoot.val 
        && isSub(root.left, subRoot.left) 
        && isSub(root.right, subRoot.right);
    }
}

例题 513. 找树左下角的值。给定一个二叉树的 根节点 root,请找出该二叉树的 最底层 最左边 节点的值。假设二叉树中至少有一个节点。

  • 最底层说明深度最大,最左边说明是该层第一个节点。
class Solution {
    private int res;
    private int maxLevel;
    public int findBottomLeftValue(TreeNode root) {
        res = root.val;
        maxLevel = 1;
        findBottom(root, 1);
        return res;
    }

    public void findBottom(TreeNode node, int level) {
        if (node == null) {
            return;
        }
        //找到第一个最底层节点
        if (level > maxLevel) {
            maxLevel = level;
            res = node.val;
        }
        findBottom(node.left, level + 1);
        findBottom(node.right, level + 1);
    }
}

例题 538. 把二叉搜索树转换为累加树。给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。

  • 观察可以发现,题目中说的累加是原树中序遍历结果的倒序。
class Solution {
    private int sum = 0;
    public TreeNode convertBST(TreeNode root) {
        if (root == null) {
            return root;
        }
        convertBST(root.right);
        root.val += sum;
        sum = root.val;
        convertBST(root.left);
        return root;
    }
}

例题 235. 二叉搜索树的最近公共祖先。给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。

  • 根据二叉搜索树的性质,当前节点的左节点小于当前节点,右节点大于当前节点;
  • 如果遍历时第一次出现某个节点的值在 p、q之间,那么该节点必定是二者的最近公共祖先;如果当前节点大于p、q节点,那么往右搜索;如果当前节点小于p、q节点,那么往左搜索;如果当前节点等于p或q,那么该节点就是最近公共祖先。(对照例子更容易理解)
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (root == null || p == null || q == null) {
            return root;
        }
        if (root.val == p.val) {
            return p;
        } 
        if (root.val == q.val) {
            return q;
        }
        if ((root.val > p.val && root.val < q.val)
        || (root.val < p.val && root.val > q.val)) {
            return root;
        } else if (root.val > p.val && root.val > q.val) {
            return lowestCommonAncestor(root.left, p, q);
        } else {
            return lowestCommonAncestor(root.right, p, q);
        }
    }

例题 236,二叉树的最近公共祖先。给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

  • 如果不是二叉搜索树,只是普通二叉树,就不能通过 root 和 p,q 的大小进行判断了。
  • 开始想的是用一个 map 存储所有节点的祖先节点,再进行判断,结果超时。
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        //当遇到叶子节点,或者遇到p、q其中一个节点时返回
        if (root == null || root == p || root == q) {
            return root;
        }
        TreeNode left = lowestCommonAncestor(root.left, p, q);
        TreeNode right = lowestCommonAncestor(root.right, p, q);
        //表示 p 和 q 分属当前节点两侧,那当前节点即为公共节点
        if (left != null && right != null) {
            return root;
        } else if (left != null) { //表示p q都在当前节点左侧
            return left;
        } else {
            //表示p q都在当前节点右侧
            return right;
        }
    }

例题 109. 有序链表转换二叉搜索树。给定一个单链表的头节点 head ,其中的元素 按升序排序 ,将其转换为高度平衡的二叉搜索树。本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差不超过 1。

  • 链表的中点即是二叉搜索树的根节点,不断递归找中点。中点通过快慢指针来找。
    public TreeNode sortedListToBST(ListNode head) {
        if (head == null) {
            return null;
        }
        if (head.next == null) {
            return new TreeNode(head.val);
        }
        ListNode slow = head;
        ListNode fast = head;
        ListNode pre = new ListNode(0);
        pre.next = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            pre = pre.next;
        }
        //slow此时指向中点,pre指向中点的前一个节点,断开连接
        pre.next = null;
        TreeNode node = new TreeNode(slow.val);
        node.left = sortedListToBST(head);
        node.right = sortedListToBST(slow.next);
        return node;
    }

例题 653,两数之和 IV - 输入 BST。给定一个二叉搜索树 root 和一个目标结果 k,如果 BST 中存在两个元素且它们的和等于给定的目标结果,则返回 true。

  • 方法1,先中序遍历存储节点,再用双指针找目标结果;
  • 方法2,无需考虑是二叉搜索树,一次遍历,用hashSet存储节点值,遍历二叉树,判断 k-node.val是否存在。
class Solution {
    Set<Integer> set = new HashSet<>();
    public boolean findTarget(TreeNode root, int k) {
        if (root == null) {
            return false;
        }
        if (set.contains(k - root.val)) {
            return true;
        }
        set.add(root.val);
        return findTarget(root.left, k) || findTarget(root.right, k);
    }
}

例题 450. 删除二叉搜索树中的节点。给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。

  • 删除节点已经会了,是通过递归返回断开其上层节点;这道题的难点是删除节点后如何维持二叉搜索树的性质不变;
  • 首先找到待删除节点后需要找到其替换节点,替换节点并不是它的右子节点(因为如果右子节点还有左子节点那就会形成三叉树了),而是该节点的右子节点的最后的左子节点;
  • 找到替换节点后,替换节点的左子节点即指向待删除节点的左子节点;但还需要继续找到替换节点的最后的右子节点,才能将最后的右子节点的右子节点指向待删除节点的右子节点;
  • 有点绕,多试验几种情况就能判断出来了。
class Solution {
    /**
    找到待删除的节点以后,需要将该节点的右子节点的最后的左子节点找到进行替换
     */
    public TreeNode deleteNode(TreeNode root, int key) {
        if (root == null) {
            return root;
        }
        if (root.val == key) {
            //保存待删除节点的左右节点
            TreeNode right = root.right;
            TreeNode left = root.left; 
            TreeNode node = root.right;
            //断开待删除节点的左右节点
            root.left = null;
            root.right = null;
            if (node == null) {
                return left;
            }
            if (node.left == null) {
                node.left = left;
                return node;
            }
            //pre存储一下上层节点
            TreeNode pre = new TreeNode(0);
            pre.left = node;
            //找到最后的左子节点,作为用于替换的节点
            while (node.left != null) {
                node = node.left;
                pre = pre.left;
            } 
            //找到替换的节点,断开其上层连接
            pre.left = null;
            //找到替换节点的最后的右子节点,将该节点的右节点指向right;
            TreeNode rNode = node;
            while (rNode.right != null) {
                rNode = rNode.right;
            }
            node.left = left;
            if(right != rNode) {
                rNode.right = right;
            }
            return node;
        }
        if (root.val > key) {
            root.left = deleteNode(root.left, key);
        } else {
            root.right = deleteNode(root.right, key);
        }
        return root;
    }
}

二. 图

图有两种表示方式,一种是邻接矩阵,可以建立一个 n× n 的矩阵 G,如果第 i 个节点连向第 j 个节点,则 G[i][j]= 1,反之为 0;如果图是无向的,则这个矩阵一定是对称矩阵,即 G[i][j] = G[j][i]。第二种表示方法是邻接链表:我们可以建立一个大小为 n 的数组,每个位置 i 储存一个数组或者链表,表示第 i 个节点连向的其它节点。邻接矩阵空间开销比邻接链表大,但是邻接链表不支持快速查找 i 和 j 是否相连。

1. 二分图

例题 785. 判断二分图。二分图 定义:如果能将一个图的节点集合分割成两个独立的子集 A 和 B ,并使图中的每一条边的两个节点一个来自 A 集合,一个来自 B 集合,就将这个图称为 二分图 。如果图是二分图,返回 true ;否则,返回 false 。

  • 这道题用的是邻接链表表示图;判断是否是二分图,可以用染色法,如果可以用两种颜色给图中所有节点染色,并且保证相邻节点的颜色都不同,则是二分图;
  • 用一个数组表示染色情况,0 表示未遍历到,1 表示一种颜色,2 表示另外一种颜色;
  • 要注意的是染色顺序不能从0 - n-1 顺序进行,而应该通过 队列 + 广度优先进行染色。即染了某个节点后,下一个染色的应该是它的邻接节点。
class Solution {
    public boolean isBipartite(int[][] graph) {
        //给节点涂颜色,0表示未遍历到,1 表示一种颜色,2表示另一种颜色
        int[] color = new int[graph.length];
        Queue<Integer> queue = new LinkedList<>();
        for(int i = 0; i < graph.length; i++) {
            if (color[i] == 0) {
                queue.offer(i);
                color[i] = 1;
            }
            while (!queue.isEmpty()) {
                int node = queue.poll();
                for (int j : graph[node]) {
                    if (color[j] == 0) {
                        queue.offer(j);
                        color[j] = color[node] == 1 ? 2 : 1;
                    } else if (color[j] == color[node]) {
                        return false;
                    }
                }
            }
        }
        return true;
    }
}

2. 拓扑排序

例题 210. 课程表 II。现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai 前 必须 先选修 bi 。例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1] 。
返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。

  • 维护每个节点的入度,入度为0表示没有前置课程,这些课程则可以添加到排序结果中;
  • 维护每个节点的后序节点列表,如果当前节点被加入到排序结果后,其后序节点的入度都要减一;
  • 用队列维护入度为0的节点,将其添加到排序结果中。
class Solution {
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        //维护每个节点对应的后序节点
        List<Integer>[] list = new ArrayList[numCourses];
        //维护每个节点的入度
        int[] indegree = new int[numCourses];
        for (int[] pre : prerequisites) {
            if (list[pre[1]] == null) {
                list[pre[1]] = new ArrayList<>();
            }
            list[pre[1]].add(pre[0]);
            indegree[pre[0]] ++;
        }
        int[] res = new int[numCourses];
        int idx = 0;
        //初始化队列,将入度为0的节点添加到队列
        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < numCourses; i++) {
            if (indegree[i] == 0) {
                queue.add(i);
            }
        }
        while (!queue.isEmpty()) {
            int node = queue.poll();
            res[idx++] = node;
            if (list[node] == null) {
                continue;
            }
            //后序节点的入度减一
            for (int degree : list[node]) {
                indegree[degree]--;
                if (indegree[degree] == 0) {
                    queue.add(degree);
                }
            }
        }
        //最后可能构不成正确答案,需要返回空数组
        return idx == numCourses ? res : new int[0];
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值