① 104.二叉树的最大深度
https://leetcode-cn.com/problems/maximum-depth-of-binary-tree
拿到这道题目,首先想的是要根据层序来遍历,但地总说了得要递归,就试着用了深度遍历的方法。
深度遍历的思路有两种
思路一:从上到下计算(前序遍历)
- 从根节点开始,每下降一层,就将深度+1
- 用全局变量来记录下最大深度
- 每当达到叶子节点时就与全局变量进行比较和更新。
class Solution {
int res = 0; // 全局变量,存放结果
public int maxDepth(TreeNode root) {
dfs(root, 0); // 深度遍历,一开始的深度为 0
return this.res; // 返回结果
}
public void dfs(TreeNode root, int length){
if (root == null){ // 如果到达叶子节点,就更新结果
res = Math.max(this.res, length);
return;
}
// 由于当前节点有值,故深度要+1
dfs(root.left, length+1); // 查找左节点
dfs(root.right, length+1); // 查找右节点
}
}
思路二:从下到上(后序遍历)
- 若我们知道两个叶子节点的长度,那么我们就能通过两个叶子的最大深度再+1,得到根节点的最大深度。
- 而先知道两个子节点,再知道根节点恰好符合后序遍历的特点。
class Solution {
public int maxDepth(TreeNode root) {
return dfs(root); // 返回当前节点的最大深度
}
public int dfs(TreeNode root){
if (root == null){ // 如果是空节点,则深度为 0
return 0;
}
int leftLength = dfs(root.left); // 遍历左节点得到左节点的最大深度
int rightLength = dfs(root.right); // 遍历右节点得到右节点的最大深度
return Math.max(leftLength, rightLength) + 1; // 返回两个节点的最大深度,再加根节点的深度(+1)
}
}
思路三:层序遍历
- 使用队列来存放节点
- 一开始先知道当前层数的节点个数,然后根据个数出队,并对其孩子节点入队
- 每经过一层,res + 1
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) return 0;
Deque<TreeNode> deque = new LinkedList<>(); // 队列
deque.offer(root); // 第一层
int res = 0;
while (!deque.isEmpty()){
res += 1; // 层数
int length = deque.size(); // 找到当前层的节点数
for (int i=0; i < length; i++){ // 清空当前层,并把孩子节点入队
TreeNode node = deque.pollFirst();
if (node.left != null)
deque.offer(node.left);
if (node.right != null)
deque.offer(node.right);
}
}
return res;
}
}
② 62. 不同路径
https://leetcode-cn.com/problems/unique-paths/
思路一:递归回溯 + 剪枝
这道题是经典的 dp 问题,一开始想到的也是 dp。但要求用递归来做,那么就想了一下。
- 一个最重要的步骤就是: dfs(i, j) = dfs(i+1, j) + dfs(i, j+1)
- 递归的出口就是当到达边界时,直接返回1。
- 如果是下边界,那么就只有一条路径(横着走到终点)
- 如果是右边界,那么就只有一条路径(竖着走到达终点)
- 还有一个重要的问题:需要剪枝。
所以这个问题也就是按照 回溯法 + 剪枝
的思路来弄。
剪枝的思路:
- 当这个点已经知道到终点总共有几条路径,就直接给出路径数就行。
- 要实现这个方法,就通过一个 map 来记录下点的路径数。
- 每个点的唯一化可以通过
i*n + j
来表示,故 map 可以为 HashMap<Integer, Integer>
class Solution {
HashMap<Integer, Integer> map; // 记录下点到终点的路径数
int m, n;
public int uniquePaths(int m, int n) {
map = new HashMap<>();
this.m = m;
this.n = n;
return dfs(0, 0);
}
public int dfs(int i, int j){
if (i == m-1 || j == n-1) // 如果到达边界
return 1;
int x = i*n + j; // 计算点的唯一标识
if (map.get(x) != null){ // 是否以算过这个点
return map.get(x);
}
map.put(x, dfs(i+1, j) + dfs(i, j+1)); // 将计算的点放到 map 中
return map.get(x);
}
}
完成这个题还是胆战心惊的。因为在测试数据时,输入 m=100, n=100
直接报了错如下:
再缩小范围到 m=60, n=60
,还是报错。
继续缩小,m=30, n=30
,还在报错!!!
到了 m=15, n=15
,才计算出来。这让我非常的胆怯。
去看了看题解,发现有些做得没有这方法好还过了,手抖的点了提交,就通过了!!!
原来提供的范围是骗人的!!
思路二:动态规划
思路:
- 如果已知从原点走到这个点,共有几个路径,那么递推过去就能知道从原点到终点共有几个路径。
- 状态转移方程为:dp[i][j] = dp[i-1][j] + dp[i][j-1]
- 初始化 dp,
- 第一行肯定都为 1,因为只能往下或往右走。
- 第一列肯定都为 1,原因如上。
- 在按状态转移方程就能得到。
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for (int i=0; i < m; i++){ // 初始化 dp
dp[i][0] = 1;
}
for(int i=0; i < n; i++){
dp[0][i] = 1;
}
for(int i=1; i < m; i++){
for(int j=1; j < n; j++){
dp[i][j] = dp[i-1][j] + dp[i][j-1]; // 根据状态转移方程得到
}
}
return dp[m-1][n-1];
}
}
思路三:状态压缩
从上面的状态转移方程中可看到,我们只需要两行的结果。故我们可以只创建两个一位数组,分别对应前一行和后一行,从而将二维数组转为两个一位数组。
再继续压缩,从状态转移方程中可看到,只用到了两个单元就能得到结果,并且这两个单元还是交叉的,故可以用一维数组来直接表示。
class Solution {
public int uniquePaths(int m, int n) {
int[] dp = new int[n];
for(int i=0; i<n; i++){ // 初始化 dp
dp[i] = 1;
}
for(int i=1; i < m; i++){ // 遍历多少行
for(int j=1; j < n; j++){
dp[j] = dp[j-1] + dp[j]; // dp[j-1] 是当前列,dp[j] 是前一列,结果是当前列
}
}
return dp[n-1];
}
}
③ 剑指 Offer 16. 数值的整数次方
https://leetcode-cn.com/problems/shu-zhi-de-zheng-shu-ci-fang-lcof/
这道题是典型的快速幂算法,但这道题坑比较多,除了要区别 n 是正数还是负数,还要考虑 n 取反溢出的问题。
思路:假设 求 7 ^ 11
- 若将 n 用二进制表示,则
11 = 1011
- 则 7 ^ 11 可以看成
7^1 * 7^2 * 7^8
- 故我们可以每次判断最后一位数字是否是 1
- 是的话结果就必须乘 x
- 若不是的话,就不处理
- 不管是不是,x 每一轮都要翻倍。
解决快速幂后,就解决正负数的问题。
- 一开始没有考虑到 2^31 次方的问题(取相反数后也是 2^31 次方)
- 故需要用一个 long 类型来接收 n,然后再去取相反数。
- 判断 n
- 正数,不用做操作
- 0,直接返回 0
- 负数,转为正数并将 x 倒过来。
思路一:递归版本
class Solution {
public double myPow(double x, int n) {
if (x == 0) // 若是 0,直接返回
return 0;
double res = 1.0; // 结果
long b = n; // 用 long 来存储 n
if (n < 0){ // 考虑负数情况
x = 1/x;
b = -b;
}
return quitPow(x, b);
}
public double quitPow(double x, long n){
if (n == 0)
return 1;
if ((n & 1) == 1)
return x * quitPow(x*x, n>>1);
return quitPow(x*x, n >> 1);
}
}
思路二:非递归版本
class Solution {
public double myPow(double x, int n) {
if (x == 0) // 若是 0,直接返回
return 0;
double res = 1.0; // 结果
long b = n; // 用 long 来存储 n
if (n < 0){ // 考虑负数情况
x = 1/x;
b = -b;
}
while (b > 0){ // 快速幂
if ((b & 1) == 1)
res *= x;
x *= x;
b >>= 1;
}
return res;
}
}
④ 4. 寻找两个正序数组的中位数
https://leetcode-cn.com/problems/median-of-two-sorted-arrays/
看到题目的时候,就想到了算法书上有一道很类似的题目,然后一想,这是 hard 难度的题吗?
点进去一看,原来是我天真了,这两个数组不是等长的。然后大概的感觉是用分治法,但如何都想不出怎么写。
去看了题解,才发现精髓就是要找到 k 项之前的数,并去掉。从而慢慢的缩减,直到 k = 1。
思路:
- 假设前 k 项是由
数组1
,数组2
平分掉的,所以数组1
取k/2
,数组2
取k-k/2
(k被数组1取后剩余的)。 - 但取
k/2
的前提是当前数组的长度不能小于k/2
,否则就只能是数组的长度项,数组2
的取项也要随之改变。 - 去判断
nums1[k/2]
和nums2[k-k/2]
,小的一项就证明其前面的也都在 k 项的前面。 - 于是我们就可以去缩小范围了,k 的范围也缩小。
- 直到
k == 1
或者数组1
或数组2
没有长度了,此时可以得到相应结果。
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int len = nums1.length + nums2.length; // 总数组长度
if ((len & 1) == 1){ // 奇数个,可以直接得出结果
return find(nums1, 0, nums2, 0, len/2+1); // len/2+1 表示要得到第 len/2 + 1 个
}
// 偶数个,则要得到两个中位数取平均
return (find(nums1, 0, nums2, 0, len/2) + find(nums1, 0, nums2, 0, len/2+1)) / 2;
}
public double find(int[] nums1, int i, int[] nums2, int j, int k){
if (nums1.length - i > nums2.length - j) // 先规定好现在的 nums1 长度小于 nums2 长度,否则调换下位置(可以减少讨论条件)
return find(nums2, j, nums1, i, k);
if (i == nums1.length) // 如果 nums1 没法再取元素,就只能都在 nums2 中取
return nums2[j+k-1];
if (k == 1) // 如果 k 只剩一个,那么可以找到最小的那一个作为第 k 个
return Math.min(nums1[i], nums2[j]);
int ki = Math.min(nums1.length, i+k/2); // 数组1 取 k/2 个,但前提是数组长度足够
int kj = j+k-k/2; // 数组2 取剩下的个数
if (nums1[ki-1] > nums2[kj-1]) // 由于 ki 表示的是第几个(从1开始),而数组是从 0 开始
return find(nums1, i, nums2, kj, k-(kj-j)); // 缩小范围,前面符合的都去掉
else
return find(nums1, ki, nums2, j, k-(ki-i)); // 缩小范围,前面符合的都去掉
}
}