《剑指 Offer I》刷题笔记 41 ~ 50 题

小标题以 _ 开头的题目和解法代表独立想到思路及编码完成,其他是对题解的学习。

VsCode 搭建的 Java 环境中 sourcePath 配置比较麻烦,可以用 java main.java 运行(JDK 11 以后)

Go 的数据结构:LeetCode 支持 https://godoc.org/github.com/emirpasic/gods 第三方库。

go get github.com/emirpasic/gods

排序(中等)

41. 最小的k个数#

题目:剑指 Offer 40. 最小的k个数

目前对排序的题目先用 API 做,手撕排序算法后面再说…

_解法1:排序 API + 数组复制 API

class Solution {
    
    
    public int[] getLeastNumbers(int[] arr, int k) {
    
    
        Arrays.sort(arr);
        return Arrays.copyOf(arr, k);
    }
}

42. 数据流中的中位数

题目:剑指 Offer 41. 数据流中的中位数

_解法1:暴力

思路:使用 List 存储数据

class MedianFinder {
    
    
    List<Integer> list;

    public MedianFinder() {
    
    
        list = new ArrayList<>();
    }

    public void addNum(int num) {
    
    
        list.add(num);
    }

    public double findMedian() {
    
    
        Collections.sort(list);
        if ((list.size() & 1) == 1) // 奇数
            return list.get(list.size() / 2);
        else {
    
     // 偶数
            int size = list.size();
            return (list.get(size / 2 - 1) + list.get(size / 2)) / 2.0;
        }
    }
}

思路:使用 int[] 数组存储数据

class MedianFinder {
    
    

    int[] arr;
    int size = 0;
    int medium = 0;

    public MedianFinder() {
    
    
        arr = new int[50000];
    }

    public void addNum(int num) {
    
    
        arr[size++] = num;
    }

    public double findMedian() {
    
    
        Arrays.sort(arr, 0, size);
        if ((size & 1) == 0)
            return (arr[(size >> 1) - 1] + arr[size >> 1]) / 2.0;// 偶
        else
            return arr[size >> 1]; // 奇
    }
}

搜索和回溯算法(中等)

题目:剑指 Offer 55 - I. 二叉树的深度

43. 二叉树的深度

_解法1:递归 DFS

思路:现在这种题目的递归都是很简单的了。

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

_解法2:BFS

思路:BFS 利用队列遍历,这个思路也已经很熟悉了。

class Solution {
    
    
    public int maxDepth1(TreeNode root) {
    
    
        if (root == null) return 0;
        Queue<TreeNode> queue = new LinkedList<>() {
    
    {
    
     offer(root); }};
        int res = 0;
        while (!queue.isEmpty()) {
    
    
            res++;
            for (int i = queue.size() - 1; i >= 0; i--) {
    
    
                TreeNode node = queue.poll();
                if (node.left != null)
                    queue.offer(node.left);
                if (node.right != null)
                    queue.offer(node.right);
            }
        }
        return res;
    }
}

44. 平衡二叉树

题目:剑指 Offer 55 - II. 平衡二叉树

_解法1:DFS + 判断深度

思路:这题配合上一题 剑指 Offer 55 - I. 二叉树的深度 还是比较容易做出来的

class Solution {
    
    
    public boolean isBalanced(TreeNode root) {
    
    
        if (root == null) return true;
      	// 左右子树深度差 > 1 则不平衡
        if (Math.abs(maxDepth(root.left) - maxDepth(root.right)) > 1)
            return false;
        return isBalanced(root.left) && isBalanced(root.right);
    }
		/**
		 * 获取二叉树的深度
		 */
    int maxDepth(TreeNode node) {
    
    
        if (node == null) return 0;
        return Math.max(maxDepth(node.left), maxDepth(node.right)) + 1;
    }
}

解法2:后序遍历 + 剪枝

题解:面试题55 - II. 平衡二叉树(从底至顶、从顶至底,清晰图解)

class Solution {
    
    
    public boolean isBalanced(TreeNode root) {
    
    
        if (root == null) return true;
        return dfs(root) != -1;
    }

    /**
     * 计算树的深度 + 剪枝
     * 当有左右子树高度差 >= 2 的情况
     * 直接返回 -1 表示不平衡
     */
    int dfs(TreeNode node) {
    
    
        if (node == null) return 0;
        int left = dfs(node.left);
        if (left == -1) return -1; // 剪枝
        int right = dfs(node.right);
        if (right == -1) return -1; // 剪枝
        return Math.abs(left - right) < 2 ? Math.max(left, right) + 1 : -1;
    }

}

45. 求 1 + 2 + … + n

题目:剑指 Offer 64. 求1+2+…+n

_解法1:数学函数 + 求和公式

class Solution {
    
    
    public int sumNums(int n) {
    
    
        return (int) (Math.pow(n, 2) + n) >> 1;
    }
}

解法2:短路效应实现递归

题解:面试题64. 求 1 + 2 + … + n(逻辑符短路,清晰图解)

常规的递归做法:

class Solution {
    
    
  	public int sumNums(int n) {
    
    
      	if (n == 1) return 1;
      	return sumNums(n - 1) + n;
		}
}

其实递归出口就是 n == 1 的时候,通过 && 的短路效应实现递归出口:

class Solution {
    
    
  	public int sumNums(int n) {
    
    
      	boolean x = n > 1 && (n += sumNums(n - 1)) > 0;
      	return n;
		}
}

同样的思路,不同的写法:

class Solution {
    
    
    int res = 0; // 利用类变量计算值
    public int sumNums(int n) {
    
    
        boolean x = n > 1 && sumNums(n - 1) > 0;
        res += n;
        return res;
    }
}

46. 二叉搜索树的最近公共祖先

题目:剑指 Offer 68 - I. 二叉搜索树的最近公共祖先

说明:

  • 所有节点的值都是唯一的。
  • p、q 为不同节点且均存在于给定的二叉搜索树中。

_解法1:DFS + 回溯 + 剪枝

解法2:递归 / 迭代(相同思路)

题解:面试题68 - I. 二叉搜索树的最近公共祖先(迭代 / 递归,清晰图解)

这题由于二叉搜索树的特性,递归 和 迭代 实现的思路是一样的:

class Solution {
    
    
    /**
     * 递归
     */
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    
    
        // 递归出口,此题可以不判断,因为题目保证有解

        // p, q 都 < root, 说明公共祖先一定在 root 左边,往左找
        if (p.val < root.val && q.val < root.val)
            return lowestCommonAncestor(root.left, p, q);
        // p, q 都 > root, 说明公共祖先一定在 root 右边,往右找
        if (p.val > root.val && q.val > root.val)
            return lowestCommonAncestor(root.right, p, q);
        // p, q 分别在 root 的左右,则为公共祖先
        return root;
    }
}
class Solution {
    
    
    /**
     * 迭代 
     */
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    
    
        while (root != null) {
    
    
            if (p.val < root.val && q.val < root.val)
                root = root.left;
            else if (p.val > root.val && q.val > root.val)
                root = root.right;
            else break;
        }
        return root;
    }
}

47. 二叉树的最近公共祖先

题目:剑指 Offer 68 - II. 二叉树的最近公共祖先

说明:

  • 所有节点的值都是唯一的。
  • p、q 为不同节点且均存在于给定的二叉树中。

_解法1:暴力

class Solution {
    
    
		// 存储找到 p 为止走过的路径
    List<TreeNode> pList = new ArrayList<>();
    // 存储找到 q 为止走过的路径
    List<TreeNode> qList = new ArrayList<>();
    // 用来判断 pList, qList 是否继续存储路径
    boolean pFlag = false, qFlag = false; 

    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    
    
        dfs(root, p, q);
        // pList 从后往前找,直到找到 qList 中也包含的节点,即为 公共祖先
        for (int i = pList.size() - 1; i >= 0; i--) 
            if (qList.contains(pList.get(i))) 
                return pList.get(i);
        return null;
    }
    /**
     * 深度优先搜索 root, 同时将走过的路径分别加入 pList、qList
     * 直到 p、q 节点都已经遍历过
     */
    void dfs(TreeNode root, TreeNode p, TreeNode q) {
    
    
        if (root == null) return;
        if (!pFlag) pList.add(root);
        if (!qFlag) qList.add(root);
        // 找到某个节点后,以后就不再记录它的路径
        if (root.val == p.val) pFlag = true;
        if (root.val == q.val) qFlag = true;
        // 剪枝:都找到就结束递归
        if (pFlag && qFlag) return; // 剪枝
        dfs(root.left, p, q);
        dfs(root.right, p, q);
        // 回溯(只有没找到的节点需要回溯)
        if (!pFlag) pList.remove(pList.size() - 1);
        if (!qFlag) qList.remove(qList.size() - 1);
    }
}

解法2:递归

题解:剑指 Offer 68 - II. 二叉树的最近公共祖先(DFS ,清晰图解)

class Solution {
    
    
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    
    
        if (root == null) return null;
        // 如果 p, q 中有等于 root 的,那么它们的最近公共祖先为该节点
        if (root == p || root == q) return root;
        // 往左找公共祖先
        TreeNode left = lowestCommonAncestor(root.left, p, q);
        // 往右找公共祖先
        TreeNode right = lowestCommonAncestor(root.right, p, q);
        // 左边找不到, 则一定在右边
        if (left == null) return right;
        // 右边找不到, 则一定在左边
        if (right == null) return left;
        // 左右都找不到, 则当前 root 为最近公共祖先
        return root;
    }
}

分治算法(中等)

48. 重建二叉树**

题目:剑指 Offer 07. 重建二叉树

解法1:递归

例子是别的地方借的,题解思路是我写的:

对于以下数据:
preorder = [3,9,6,10,20,15,7] # 先序遍历
inorder = [6,9,10,3,15,20,7] # 中序遍历

1、根据[先序遍历],可以确定【3】为根节点,然后根据【3】在[中序遍历]进行划分:
- [6, 9, 10] 是【左子树】
- [3] 是此轮的【根节点】
- [15, 20, 7] 是【右子树】

2、对【左子树】进行同样的操作,递归传入的数组:
preorder = [9, 6, 10] # 先序遍历
inorder = [6, 9, 10] # 中序遍历

3、对【右子树】进行同样的操作,递归传入的数组:
preorder = [20, 15, 7] # 先序遍历
inorder = [15, 20, 7] # 中序遍历
方法步骤归纳:
1. 由【先序遍历第一个数字】得出根节点
2. 由【中序遍历结合根节点的值得出】:哪些数字是左子树,哪些数字是右子树,并划分出来
3. 重复 1-2 步

代码:

class Solution {
    
    
    public TreeNode buildTree(int[] preorder, int[] inorder) {
    
    
        if (preorder.length == 0) return null;

        // [先序遍历]可以直接知道:根节点的值 rootVal
        int rootVal = preorder[0]; // 3
        // 在[中序遍历]中寻找 rootVal 的索引
        int rootIndex = 0;
        for (int i = 0; i < inorder.length; i++) {
    
    
            if (inorder[i] == rootVal) {
    
    
                rootIndex = i; // 3
                break;
            }
        }

        // 构造根节点
        TreeNode root = new TreeNode(rootVal);

        // 递归
        root.left = buildTree(
                // [9, 6, 10]
                Arrays.copyOfRange(preorder, 1, rootIndex + 1),
                // [6, 9, 10]
                Arrays.copyOfRange(inorder, 0, rootIndex)
        );
        root.right = buildTree(
                // [20, 15, 7]
                Arrays.copyOfRange(preorder, rootIndex + 1, preorder.length),
                // [15, 20, 7]
                Arrays.copyOfRange(inorder, rootIndex + 1, inorder.length)
        );

        return root;
    }
}

49. 数值的整数次方*

题目:剑指 Offer 16. 数值的整数次方

解法1:快速幂(递归)

class Solution {
    
    
    public double myPow(double x, int n) {
    
    
        // 递归出口
        if (n == 0) return 1;
        if (n == 1) return x;
        if (n == -1) return 1 / x;

        // 计算出 x^(n/2)
        // 则 x^n = x^(n/2) * x^(n/2)
        double half = myPow(x, n >> 1);
        half *= half;
        // 如果指数是奇数,由于是 2 倍的缩小,会遗漏一次乘 x
        return (n & 1) == 1 ? half * x : half;
    }
}

解法2:快速幂(迭代)

正常写法:

class Solution {
    
    
    public double myPow(double x, int n) {
    
    
        // 处理 n < 0 情况
        if (n < 0) {
    
    
            x = 1 / x;
            n = -n;
        }

        double res = 1.0;
        while (n != 0) {
    
    
            // 最后一位为1,需要乘上该位的权重
            if ((n & 1) == 1)
                res*= x;
            x *= x;
            n >>= 1;
        }
        return res;
    }
}

但是以上这种写法无法通过所有测试用例…因为存在以下这种测试用例:

1.00000
-2147483648

因此为了通过这些 n 极其大的用例,需要用 long 类型的变量来接收一下:

class Solution {
    
    
    public double myPow(double x, int n) {
    
    
        long b = n;

        // 处理 n < 0 情况
        if (b < 0) {
    
    
            x = 1 / x;
            b = -b;
        }

        double res = 1.0;
        while (b != 0) {
    
    
            // 最后一位为1,需要乘上该位的权重
            if ((b & 1) == 1)
                res*= x;
            x *= x;
            b >>= 1;
        }
        return res;
    }
}

50. 二叉搜索树的后序遍历序列**

题目:剑指 Offer 33. 二叉搜索树的后序遍历序列

解法1:递归

题解:递归和栈两种方式解决,最好的击败了100%的用户

class Solution {
    
    
    public boolean verifyPostorder(int[] postorder) {
    
    
	      // 特殊情况:只有1个节点
        if (postorder.length == 1) return true;
        return verify(postorder, 0, postorder.length - 1);
    }

    /**
     * 判断后序遍历数组中的指定区间, 是不是某二叉搜索树的后序遍历结果
     * @param postorder 后序遍历数组
     * @param left 开始索引
     * @param right 结束索引
     * @return 是不是某二叉搜索树的后序遍历结果
     */
    boolean verify(int[] postorder, int left, int right) {
    
    
        // 区间不合法直接返回 true
        if (left >= right) return true;

        // 当前树的根节点(后序遍历中最后一个值)
        int rootValue = postorder[right];

        // 从当前区域找到第一个 > 根节点的,说明后续区域数值都在右子树中
        int k = left;
        while (k < right && postorder[k] < rootValue) k++;

        // 判断后续的区域是否所有值都 > 当前根节点,出现 < 的值就直接返回 false
        for (int i = k; i < right; i++)
            if (postorder[i] < rootValue) return false;

        // 对左右子节点进行递归调用
        return verify(postorder, left, k -1) && verify(postorder, k, right - 1);
    }
}

解法2:单调栈(待续)

题解:递归和栈两种方式解决,最好的击败了100%的用户

猜你喜欢

转载自blog.csdn.net/weixin_43734095/article/details/123442216