剑指offer题解
- 1. 数组中重复的数字
- 2. 二维数组中的查找
- 3. 替换空格
- 4. 从尾到头打印链表
- 5. 重建二叉树
- 6. 用两个栈实现队列
- 7. I_斐波那契数列
- 8. II_青蛙跳台阶问题
- 9. 旋转数组的最小数字
- 10. 矩阵中的路径
- 11. 机器人的运动范围
- 12. I_剪绳子
- 13. II_剪绳子
- 14. 二进制中1的个数
- 15. 数值的整数次方
- 16. 打印从1到最大的n位数
- 17. 删除链表的节点
- 18. 正则表达式匹配
- 19. 表示数值的字符串
- 20. 调整数组顺序使奇数位于偶数前面
- 21. 链表中倒数第k个节点
- 22. 反转链表
- 23. 合并两个排序的链表
- 24. 树的子结构
- 25. 二叉树的镜像
- 26. 对称的二叉树
- 27. 顺时针打印矩阵
- 28. 栈的压入_弹出序列
- 29. I_从上到下打印二叉树
- 30. II_从上到下打印二叉树
- 31. III_从上到下打印二叉树
- 32. 二叉搜索树的后序遍历序列
- 33. 二叉树中和为某一值的路径
- 34. 复杂链表的复制
- 35. 二叉搜索树与双向链表
- 36. 序列化二叉树
- 37. 字符串的排列
- 38. 数组中出现次数超过一半的数字
- 39. 最小的k个数
- 40. 数据流中的中位数
- 41. 连续子数组的最大和
- 42. 1到n整数中1出现的次数
- 43. 把数组排成最小的数
- 44. 把数字翻译成字符串
- 45. 礼物的最大价值
- 46. 最长不含重复字符的子字符串
- 47. 丑数
- 48. 第一个只出现一次的字符
- 49. 数组中的逆序对
- 50. 两个链表的第一个公共节点
- 51. I_在排序数组中查找数字
- 52. II_缺失的数字
- 53. 二叉搜索树的第k大节点
- 54. I_二叉树的深度
- 55. II_平衡二叉树
- 56. I_数组中数字出现的次数
- 57. II_数组中数字出现的次数
- 58. II_和为s的连续正数序列
- 59. 和为s的两个数字
- 60. I_翻转单词顺序
- 61. II_左旋转字符串
- 62. I_滑动窗口的最大值
- 63. II_队列的最大值
- 64. n个骰子的点数
- 65. 扑克牌中的顺子
- 66. 圆圈中最后剩下的数字
- 67. 股票的最大利润
- 68. 求1到n的和
- 69. 不用加减乘除做加法
- 70. 构建乘积数组
- 71. 把字符串转换成整数
- 72. I_二叉搜索树的最近公共祖先
- 73. 二叉树的最近公共祖先
- 补充类似题
1. 数组中重复的数字
/**
* 思路: 原地哈希,将nums[i]不断与nums[nums[i]]交换,直到nums[i] = i为止
* 如果在交换过程中出现nums[nums[i]]位置已经有元素了,那么就返回结果nums[i]
* 时间复杂度: O(n)
* 空间复杂度: O(1)
*/
public int findRepeatNumber(int[] nums) {
for (int i = 0; i < nums.length; i++) {
while(nums[i] != i) {
if(nums[nums[i]] == nums[i]) {
return nums[i];
}
int temp = nums[i];
nums[i] = nums[nums[i]];
nums[temp] = temp;
}
}
return -1;
}
2. 二维数组中的查找
// 暴力解法,时间复杂度O(n*m) 空间复杂度O(1)
/*public boolean findNumberIn2DArray(int[][] matrix, int target) {
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
if(matrix[i][j] == target) {
return true;
}
}
}
return false;
}*/
/**
* 思路: 二分查找
* 枚举每行的最后一个元素x,进行二分查找
* 如果 x == target, 则直接返回
* 如果 x > target, 则排除当前整一列(纵坐标减1)
* 如果 x < target, 则排除当前整一行(横坐标加1)
*
* 时间复杂度: O(n+m)
* 空间复杂度: O(1)
*/
public boolean findNumberIn2DArray(int[][] matrix, int target) {
if(matrix == null || matrix.length == 0) {
return false;
}
int i = 0, j = matrix[0].length-1;
while(i <= matrix.length && j >= 0) {
if(matrix[i][j] == target) {
return true;
} else if(matrix[i][j] > target) {
j--;
} else {
i++;
}
}
return false;
}
3. 替换空格
/**
* 思路: 遍历字符串,遇到空格,则添加%20
* 时间: O(n)
* 额外空间: O(n)
*/
public String replaceSpace(String s) {
StringBuilder res = new StringBuilder();
for(int i = 0; i < s.length(); i++) {
if(s.charAt(i) == ' ') {
res.append("%20");
} else {
res.append(s.charAt(i));
}
}
return res.toString();
}
4. 从尾到头打印链表
/*
* 思路: 用栈存储遍历得到的数据,然后再依次出栈添加到结果数组中
* 时间: O(n)
* 空间:O(n)
*/
/*public int[] reversePrint(ListNode head) {
Stack<Integer> stack = new Stack<>();
while(head != null) {
stack.push(head.val);
head = head.next;
}
int[] res = new int[stack.size()];
int idx = 0;
while(!stack.isEmpty()) {
res[idx++] = stack.pop();
}
return res;
}*/
/**
* 思路: 两次遍历链表
* 时间: O(n)
* 空间: O(1)
*/
public int[] reversePrint(ListNode head) {
int count = 0;
ListNode node = head;
while(node != null) {
count++;
node = node.next;
}
int[] res = new int[count];
node = head;
for (int i = count-1; i >= 0; i--) {
res[i] = node.val;
node = node.next;
}
return res;
}
5. 重建二叉树
/**
* 思路: 递归
* 1. 取前序遍历的第一个元素作为根节点
* 2. 先切割中序数组,根据根节点查找中序数组中的位置,切割成中序左数组[i_start,i_root_index)和中序右数组[i_root_index+1,i_end) 【左闭右开】
* 3. 再切割前序数组,根据中序数组中左数组的长度进行切割,切割成前序左数组[p_start+1, p_start+1+(i_root_index-i_start))和前序右数组[p_start+1+(i_root_index-i_start),p_end)
*/
// 存储前序遍历中的根节点在中序遍历的下标位置
HashMap<Integer, Integer> map = new HashMap<>();
public TreeNode buildTree(int[] preorder, int[] inorder) {
// 初始化赋值
for (int i = 0; i < inorder.length; i++) {
map.put(inorder[i], i);
}
// 左闭右开区间
return buildTreeHelper(preorder, 0, preorder.length, inorder, 0, inorder.length);
}
public TreeNode buildTreeHelper(int[] preorder, int p_start, int p_end, int[] inorder, int i_start, int i_end) {
// 递归终止条件,前序数组遍历完,直接返回null
if(p_start == p_end) {
return null;
}
int root_val = preorder[p_start];
// 中序数组中根节点的下标位置, 根据此下标划分中序数组中的 中序左数组 和 中序右数组
int i_root_index = map.get(root_val);
// 构建根节点
TreeNode root = new TreeNode(root_val);
// 递归构造左子树
root.left = buildTreeHelper(preorder, p_start+1, p_start + 1 + (i_root_index-i_start), inorder, i_start, i_root_index);
// 递归构造右子树
root.right = buildTreeHelper(preorder, p_start+1+(i_root_index-i_start), p_end, inorder, i_root_index+1, i_end);
return root;
}
6. 用两个栈实现队列
Stack<Integer> pushStack;
Stack<Integer> popStack;
public _剑指_Offer_09_用两个栈实现队列() {
this.pushStack = new Stack<>();
this.popStack = new Stack<>();
}
public void appendTail(int value) {
pushStack.add(value);
}
public int deleteHead() {
// 弹出元素的栈中仍有元素,则返回出栈元素
if(popStack.size() > 0) {
return popStack.pop();
}
// 弹出元素的栈为空,入栈元素的栈也为空,返回-1
if(popStack.size() == 0) return -1;
// 入栈元素的栈为空,则不断从出栈的栈弹出元素
while(pushStack.size() > 0) {
popStack.add(pushStack.pop());
}
return popStack.pop();
}
7. I_斐波那契数列
public int fib(int n) {
if(n < 2) {
return n;
}
int a = 0;
int b = 1;
for(int i = 2; i <= n; i++) {
int sum = (a + b) % 1000000007;
a = b;
b = sum;
}
return b;
}
8. II_青蛙跳台阶问题
/**
* 思路: 动态规划
* 1. dp[i] = dp[i-1] + dp[i-2]
* 2. 由于dp[i]只与前面两项相关,所以用三个变量sum,a,b记录,优化空间,降为O(1)
*
* 时间:O(n)
* 空间:O(1)
*/
public int numWays(int n) {
if(n < 2) {
return 1;
}
int a = 1;
int b = 1;
int sum = 0;
for(int i = 2; i <= n; i++) {
sum = (a + b) % 1000000007;
a = b;
b = sum;
}
return b;
}
9. 旋转数组的最小数字
/**
* 思路: 二分查找
* 将中间元素mid和右边元素right相比较,缩小搜索的范围
* numbers[mid] < numbers[right], 则 right = mid
* numbers[mid] > numbers[right], 则 left = mid + 1
* numbers[mid] = numbers[right], 则 right--
*/
public int minArray(int[] numbers) {
int left = 0;
int right = numbers.length-1;
while(left < right) {
int mid = (left + right) >>> 1;
if(numbers[mid] < numbers[right]) {
right = mid;
} else if(numbers[mid] > numbers[right]) {
left = mid + 1;
} else {
right--;
}
}
return numbers[left];
}
10. 矩阵中的路径
/**
*
* 思路: 深度优先搜索
* 1. 枚举矩阵中每个位置
* 2. 然后从当前位置开始深度优先搜索,从上下左右四个方向开始搜索
* 3. 过程中把遍历过的位置设置为一个特殊的标志符
* 时间复杂度: O(MN * k^3)
* 需要枚举MN个起点,时间复杂度为(MN)
* 方案数计算: 设字符串长度为 K ,搜索中每个字符有上、下、左、右四个方向可以选择,舍弃回头(上个字符)的方向,剩下 3 种选择,因此方案数的复杂度为 O(3^K)
* 空间复杂度: O(K)
* 最差K=MN
*/
int[] dx = {-1, 0, 1, 0}; // 上右下左
int[] dy = {0, 1, 0, -1};
public boolean hasPath (char[][] matrix, String word) {
if(matrix == null || matrix.length == 0) {
return false;
}
// 遍历矩阵中的每个元素,判断路径是否可达
for(int i = 0; i< matrix.length; i++) {
for(int j = 0; j < matrix[0].length; j++) {
if(dfs(matrix, word, i, j, 0)) {
return true;
}
}
}
return false;
}
private boolean dfs(char[][] matrix, String word, int x, int y, int k) {
// 不满足
if(matrix[x][y] != word.charAt(k)) {
return false;
}
if(k == word.length() - 1) {
return true;
}
char temp = matrix[x][y];
matrix[x][y] = '/';
// 上下左右四个方向
for(int i = 0; i < 4; i++) {
int a = x + dx[i];
int b = y + dy[i];
if(a >= 0 && a < matrix.length && b >= 0 && b < matrix[0].length) {
if(dfs(matrix, word, a, b, k+1)) {
return true;
}
}
}
// 若不满足,则进行回溯
matrix[x][y] = temp;
return false;
}
11. 机器人的运动范围
/**
* 思路: 深度优先搜索
*
* 1. 设置一个visited布尔数组记录已经被访问过的节点
* 2. 从(0,0)位置开始出发,因此只会往右和往下两个方向走
* 3. 设置一个全局变量res,记录dfs过程中满足条件的位置
*
* 时间: O(n*m)
* 空间: O(n*m)
*/
/*int res = 0;
int[] dx = {0,1}; // 向右 和 向下
int[] dy = {1,0};
public int movingCount(int m, int n, int k) {
if(m <= 0 || n <= 0 || k < 0) {
return 0;
}
boolean[][] visited = new boolean[m][n];
dfs(visited, 0, 0, m, n, k);
return res;
}
private void dfs(boolean[][] visited, int x, int y, int m, int n, int k) {
// 已被遍历过 返回
if(visited[x][y]) {
return;
}
visited[x][y] = true;
res++;
for (int i = 0; i < 2; i++) {
int a = x + dx[i];
int b = y + dy[i];
// 满足条件向右和向下搜索
if(a >= 0 && a < m && b >= 0 && b < n && digitSum(a) + digitSum(b) <= k) {
dfs(visited, a, b, m, n, k);
}
}
}
private int digitSum(int x) {
int sum = 0;
while(x > 0) {
sum += x % 10;
x /= 10;
}
return sum;
}*/
/**
* 思路: 宽度优先搜索(bfs)
* 1. 创建visited[][]数组记录已被访问过的节点
* 2. 队列中存放可被访问的位置,每次弹出一个位置坐标(x,y),判断是否被访问过,
* 如果被访问过,则continue(易错点,应该在这里),并将其下方和右下方可以被访问的位置入队
* 3. 全局变量res记录队列中可以到达的格子数量
* 4. 队列为空结束循环,返回res
*
* 时间: O(n*m)
* 空间: O(n*m)
*/
public int movingCount(int m, int n, int k) {
if(m <= 0 || n <= 0 || k < 0) {
return 0;
}
int res = 0;
boolean[][] visited = new boolean[m][n];
// 存储每个节点的横纵坐标
Queue<int[]> queue = new LinkedList<>();
queue.add(new int[]{0,0});
int[] dx = {0,1};
int[] dy = {1,0};
while(!queue.isEmpty()) {
int[] x = queue.poll();
if(visited[x[0]][x[1]]) {
continue;
}
res++;
visited[x[0]][x[1]] = true;
for(int i = 0; i < 2; i++) {
int a = x[0] + dx[i];
int b = x[1] + dy[i];
if(a >= 0 && a < m && b >= 0 && b < n && digitSum(a) + digitSum(b) <= k) {
queue.add(new int[]{a,b});
}
}
}
return res;
}
private int digitSum(int x) {
int sum = 0;
while(x > 0) {
sum += x % 10;
x /= 10;
}
return sum;
}
// 如果给的数组是一维数组,需要将二维数组转化为一维数组
// 深度优先搜索
/*int res = 0;
int[] dx = {0, 1}; // 右下
int[] dy = {1, 0};
public int movingCount(int threshold, int rows, int cols) {
if(rows <= 0 || cols <= 0 || threshold < 0) return 0;
// 一维代替二维
boolean[] visited = new boolean[rows * cols];
dfs(visited, rows, cols, 0, 0, threshold);
return res;
}
public void dfs(boolean[] visited, int rows, int cols, int row, int col, int threshold) {
// 不满足条件
int index = row * cols + col;
visited[index] = true;
res++;
for(int i = 0; i < 2; i++) {
int a = row + dx[i]; // 下一个位置的横坐标
int b = col + dy[i]; // 下一个位置的纵坐标
if(a >= 0 && a < rows && b >= 0 && b < cols && !visited[a * cols + b] && digitSum(a) + digitSum(b) <= threshold) {
dfs(visited, rows, cols, a, b, threshold);
}
}
}
private int digitSum(int x) {
int sum = 0;
while(x > 0) {
sum += x % 10;
x /= 10;
}
return sum;
}*/
// 宽度优先搜索
/*public int movingCount(int threshold, int rows, int cols) {
if(rows <= 0 || cols <= 0 || threshold < 0) {
return 0;
}
int res = 0;
// 存储每个点的x,y轴
Queue<int[]> queue = new LinkedList<>();
queue.add(new int[]{0,0});
// 方向
int[] dx = {0, 1};// 右 下
int[] dy = {1, 0};
// 访问过的标志
boolean[] visited = new boolean[rows * cols];
while(queue.size() > 0) {
int[] x = queue.poll();
if(visited[x[0] * cols + x[1]]) {
continue;
}
visited[x[0] * cols + x[1]] = true;
res++;
for(int i = 0; i < 2; i++) {
int a = x[0] + dx[i];
int b = x[1] + dy[i];
// 条件满足才能入队
if(a >= 0 && a < rows && b >= 0 && b < cols && digitSum(a) + digitSum(b) <= threshold) {
queue.add(new int[]{a,b});
}
}
}
return res;
}
private int digitSum(int x) {
int sum = 0;
while(x > 0) {
sum += x % 10;
x /= 10;
}
return sum;
}*/
12. I_剪绳子
/**
* 思路: 动态规划
* 1. 定义dp数组以及下标含义
* dp[i]:表示长度为i剪成m段后的最大乘积
* 2. 确定递推公式
* 先把绳子剪掉第一段(长度为j),如果只剪长度为1,对最后乘积无益处,所以从长度为2开始剪
* 剪了第一段后,剩下(i-j)长度可以剪可以不剪,取两者最大值, max(j*(i-j), j * dp[i-j])
* 第一段长度区间可以取值范围区间为[2,i),对所有j不同的情况取最大值
* 最终dp[i]的转移方程为: dp[i] = max(dp[i],max(j*(i-j),j*dp[i-j]))
* 3. 初始化
* dp[2] = 1
* 4. 最后返回dp[n]即可
*
* 时间复杂度:O(n^2)
* 空间复杂度:O(n)
*/
/*public int cuttingRope(int n) {
int[] dp = new int[n+1];
dp[2] = 1;
*//*for (int i = 3; i <= n; i++) {
for (int j = 2; j < i; j++) {
dp[i] = Math.max(dp[i], Math.max(j * (i-j), j * dp[i-j]));
}
}*//*
// 优化
// i=3时,是特殊情况,要单独初始化
if(n >= 3) {
dp[3] = 2;
}
for (int i = 3; i <= n; i++) {
for (int j = 2; j < i/2+1; j++) {
dp[i] = Math.max(dp[i], Math.max(j * (i-j), j * dp[i-j]));
}
}
return dp[n];
}*/
/**
* 思路2: 贪心
* 尽可能将绳子长度分为长度为3的小段,这样乘积最大
*
* 1. 当n=2时,返回1;当n=3时,返回2; 两个合并,n < 4时, return n-1
* 2. 当n=4时,返回4
* 3. 当n>4时,尽可能分为长度为3,累乘
* 2和3步可以合并
*/
public int cuttingRope(int n) {
if(n < 4) {
return n-1;
}
int res = 1;
while(n > 4) {
res *= 3;
n -= 3;
}
return res * n;
}
13. II_剪绳子
/**
* 思路: 动态规划
* 大数取余
* 1. 确定dp函数以及下标含义 dp[i]表示长度为i的最大乘积
* 2. 确定递推公式
* 先切割第一段长度为j,j的取值范围为[2,i)
* 第二段长度为i-j可切,可不切中选择最大的
* dp[i] = Math.max(dp[i], Math.max(j * dp[i-j], j * (i-j)))
*/
/*public int cuttingRope(int n) {
BigInteger[] dp = new BigInteger[n+1];
// 初始化
Arrays.fill(dp, BigInteger.valueOf(1));
dp[2] = BigInteger.valueOf(1);
for (int i = 3; i <= n; i++) {
for (int j = 2; j < i; j++) {
dp[i] = dp[i].max(BigInteger.valueOf(j).multiply(dp[i-j]).max( BigInteger.valueOf(j * (i-j))));
}
}
return dp[n].mod(BigInteger.valueOf(1000000007)).intValue();
}*/
/**
* 思路: 贪心
* 尽可能将绳子分成长度为3的小段,这样乘积最大
*
*/
public int cuttingRope(int n) {
if(n < 4) {
return n-1;
}
long res = 1;
while(n > 4) {
res = res * 3 % 1000000007;
n -= 3;
}
return (int) (res * n % 1000000007);
}
14. 二进制中1的个数
/*public int hammingWeight(int n) {
int res = 0;
// 注意负数的情况
while(n != 0){
if((n & 1) == 1) {
res++;
}
n >>= 1;
}
return res;
}*/
/**
* 思路: n & (n-1)每次都会吧最后一个1变成0
*/
public int hammingWeight(int n) {
int res = 0;
while(n != 0) {
n = n & (n-1);
res++;
}
return res;
}
15. 数值的整数次方
/**
* 思路: 快速幂(递归)
* 1. 如果n == 0,返回1
* 2. 如果n < 0,最终结果为 1 / (x * myPow(x, -n-1)); 即 1/x^(-n)
* 3. 如果n为奇数,最终结果为 x * myPow(x, n-1); 即x * x^(n-1)
* 4. 如果n为偶数,最终结果为 myPow(x * x, n >> 1); 即 (x^2)^(n/2)
*
* 因为有n=Integer.MIN_VALUE的存在,取反后会溢出,所以提取一个x出来,这样就不会溢出了
*/
/* public double myPow(double x, int n) {
if(n == 0) {
return 1;
}
if(n < 0) {
// 如果x = Integer.MIN_VALUE -2147483648,则取反还是本身
// return myPow(1/x, -n);
return 1 / (x * myPow(x, -n-1));
}
// 偶数
if((n & 1) == 0) {
return myPow(x * x, n >> 1);
} else {
// 奇数
return x * myPow(x, n-1);
}
}*/
/**
* 思路: 快速幂(递归)
* 1. 提前对n为Integer.MIN_VALUE作判断,如果x==±1,那么返回1,否则返回0
* 2. 如果n == 0,返回1
* 3. 如果n < 0,最终结果为 myPow(1/x, -n); 即 (1/x)^(-n)
* 4. 如果n为奇数,最终结果为 x * myPow(x, n-1); 即x * x^(n-1)
* 5. 如果n为偶数,最终结果为 myPow(x * x, n >> 1); 即 (x^2)^(n/2)
*
* 因为有n=Integer.MIN_VALUE的存在,取反后会溢出,所以提取一个x出来,这样就不会溢出了
*/
public double myPow(double x, int n) {
// 除了1和-1都是为1外,其余都是0
if(n == Integer.MIN_VALUE) {
return (x == 1 || x == -1) ? 1 : 0;
}
if(n == 0) {
return 1;
}
if(n < 0) {
// 如果x = Integer.MIN_VALUE -2147483648,则取反还是本身,除了第一种方案往外提出一个数外,还可以单独对这个条件判断
return myPow(1/x, -n);
}
// 偶数
if((n & 1) == 0) {
return myPow(x * x, n >> 1);
} else {
// 奇数
return x * myPow(x, n-1);
}
}
16. 打印从1到最大的n位数
/**
* 思路: 如果String[], 则大数的话,要全排列
*
* 由于返回值是int,为了测试通过,最后把字符串变成int,其实应该返回字符串数组
*
* 1. 为了避免数字开头0出现,先把首位first固定,first取值范围是1~9
* 2. 用digit表示要生成的数字的位数,从1位数一值生成n位数,对每种数字的位数都生成下一个首位,所以有个双重for循环,
* 生成首位之后进入递归生成剩下的digit-1位数,从0~9中取值
* 3. 递归终止条件是已生成了digit位的数字,即index=digit,将此时的数num转为int加到结果res中
* 比如n=2, digit = 1,2 当digit=1时,先固定首位,首位范围是1~9
* 当digit=2时,先固定首位(1~9),那么第二位对每种数字生成(0~9)
* 时间复杂度: O(10^n) -> 1~10^n-1都遍历了一遍
* 额外空间: O(n) -> num数组和递归栈
*/
public static void main(String[] args) {
_剑指_Offer_17_打印从1到最大的n位数 a = new _剑指_Offer_17_打印从1到最大的n位数();
a.printNumbers(1);
}
int[] res;
int count = 0;
public int[] printNumbers(int n) {
res = new int[(int)Math.pow(10,n) - 1];
for (int digit = 1; digit <= n; digit++) {
for (char first = '1'; first <= '9'; first++) {
char[] num = new char[digit];
num[0] = first;
// 生成首位之后进入递归生成剩下的digit-1位数,从0~9中取值
dfs(1, num, digit);
}
}
return res;
}
private void dfs(int index, char[] num, int digit) {
// 递归终止条件
if(index == digit) {
res[count++] = Integer.parseInt(String.valueOf(num));
return;
}
// 从0~9中取值
for (char i = '0'; i <= '9'; i++) {
num[index] = i;
dfs(index + 1, num, digit);
}
}
17. 删除链表的节点
/**
* 思路: 设置一个哑节点
* 1. 设置一个哑节点dummy,新链表的头结点为dummy.next
* 2. 初始化两个节点pre = dummy, cur = head;
* 3. cur!=val时,一直移动两个指针,pre=cur, cur=cur.next
* 4. cur==val时,pre.next = cur.next;
* 5. 返回新链表头节点
*
* 时间:O(n)
* 空间:O(1)
*/
public ListNode deleteNode(ListNode head, int val) {
ListNode dummy = new ListNode();
dummy.next = head;
ListNode pre = dummy;
ListNode cur = head;
while(cur.val != val) {
pre = cur;
cur = cur.next;
}
return dummy.next;
}
18. 正则表达式匹配
/**
* 思路: 动态规划
* 1. dp[i][j]:表示s的前i个和p的前j个是否匹配
* 2. 手动求二维矩阵的每个值,通过计算可以发现:
* - 第0列,除了dp[0][0]=true,其余dp[i][0]=false
* - s从0开始算,p从1开始算
* - 过程中考虑dp[i][j]由哪个值得来
* 3. 需要考虑p的当前字符p[j]
* a. 当前字符是字母
* b. 当前字符是'.'
* 由a和b判断两个字符是否匹配
* 字符匹配: s[i] == p[j] || p[j] == '.'
* 字符不匹配: s[i] != p[j]
* c. 当前字符是'*'
* '*'表示前面字符可以出现0次或者出现多次
* (注意j从2开始)
* 前面字符与s[i]匹配时,则使用'*': 代表前面字符出现一次或者多次: s当前位置可以由s的前i-1和p的前j个是否可以匹配,即dp[i][j] = dp[i-1][j]
* 前面字符与s[i]不匹配时,则不使用'*': 代表前面字符出现0次: s当前位置可以由s的前i和p的前j-2个是否可以匹配,即dp[i][j] = dp[i][j-2]
* 4. 初始化
* dp[0][0] = true
* 初始化第0行,为了后面方便计算,所以当遇到'*'且j>=2时,则dp[0][j] = dp[0][j-2]
* 因为*前面的字符可以出现0次或者多次,匹配第一行,s为空,所以这里出现0次的话,则匹配j-2就可以
*
* 时间: O(n*m)
* 空间: O(n*m)
*/
public static boolean isMatch(String s, String p) {
if(s == null && p == null) {
return true;
}
if(p == null) {
return false;
}
int m = s.length();
int n = p.length();
boolean[][] dp = new boolean[m+1][n+1];
// 初始化
dp[0][0] = true;
for (int j = 2; j <= n; j++) {
if(p.charAt(j-1) == '*') {
dp[0][j] = dp[0][j-2];
}
}
for (int i = 1; i <= m; i++) {
char charS = s.charAt(i-1);
for (int j = 1; j <= n; j++) {
char charP = p.charAt(j-1);
if(charP != '*') {
if(charS == charP || charP == '.') {
dp[i][j] = dp[i-1][j-1];
}
} else {
// 不使用'*': 代表前面字符出现0次
if(j >= 2) {
dp[i][j] = dp[i][j-2];
}
// 使用'*': 代表当前位置s[i]匹配一次,然后匹配剩余的s[i-1]是否与p[j]匹配即可
if(j >= 2 && (charS == p.charAt(j-2) || p.charAt(j-2) == '.')) {
dp[i][j] = dp[i-1][j];
}
}
}
}
return dp[m][n];
}
19. 表示数值的字符串
/**
* 思路: 判断否false而不是判断true,只要有一个条件不满足就可以判断false
*
* 1. 定义四个flag,对应四种字符
* - 是否有符号: hasSign
* - 是否有数字: hasNum
* - 是否有点: hasDot
* - 是否有e: hasE
* 2. 还需要定义长度n和索引index
* 3. 先处理开头空格,index后移
* 4. 进入循环,遍历字符串
* - 当前字符c是'+'或'-': 如果已经出现过符号、出现过点、出现过数字,则返回false,否则,令hasSign = true
* - 当前字符c是数字: 令hasNum=true,一直移动index,直到出现非数字或者遍历到末尾,如果已遍历到末尾,返回true
* - 当前字符c是'.': 如果已经出现过'.'、出现过E(e),则返回false,否则,hasDot = true
* - 当前字符c是e或E: 如果已经出现e或者e之前没有出现过数字,则返回false,否则hasE = true,并将其它三个符号位设置为false,因为要开始遍历e后面的数字了
* 5. 处理空格,index相应的后移
* 6. 如果当前index与字符串长度相等,说明到达了末尾,还要满足hasNum为true才能最终返回true
*
*
* 时间: O(n)
* 空间: O(1)
*/
public boolean isNumber(String s) {
if(s == null || s.length() == 0) {
return false;
}
boolean hasSign = false;
boolean hasNum = false;
boolean hasDot = false;
boolean hasE = false;
int index = 0;
int n = s.length();
// 先处理空格
while(index < n && s.charAt(index) == ' ') {
index++;
}
while(index < n) {
// 当前字符c是数字
while(index < n && s.charAt(index) >= '0' && s.charAt(index) <= '9') {
index++;
hasNum = true;
}
if(index == n) {
break;
}
char c = s.charAt(index);
// 当前字符c是'+'或'-'
if(c == '+' || c == '-') {
if(hasSign || hasDot || hasNum) {
return false;
}
hasSign = true;
} else if(c == '.') {
// 当前字符c是'.'
if(hasDot || hasE) {
return false;
}
hasDot = true;
} else if(c == 'e' || c == 'E') {
// 当前字符是'e'或'E'
if(hasE || !hasNum) {
return false;
}
hasE = true;
// 开始遍历e后面的新数字
hasNum = false;
hasDot = false;
hasSign = false;
} else if(c == ' ') {
// 结束当前循环,继续判断循环外的情况
break;
} else {
// 出现其它字符,返回false
return false;
}
index++;
}
while(index < n && s.charAt(index) == ' ') {
index++;
}
return index == n && hasNum;
}
20. 调整数组顺序使奇数位于偶数前面
/**
* 思路: 头尾双指针
* 1. left指向数组头,right指向数组尾
* 2. left右移直到遇到偶数
* 3. right左移直到遇到奇数
* 4. 如果此时left > right 则结束循环
* 5. 交换left和right所指数字
* 6. 继续以上步骤,直到left > right
* 时间: O(n)
* 空间: O(1)
*/
public static int[] exchange(int[] nums) {
if(nums == null || nums.length == 0) {
return new int[0];
}
int left = 0;
int right = nums.length-1;
while(left < right) {
// 左边索引一直递增,直到找到偶数为止
while(left < right && nums[left] % 2 == 1) {
left++;
}
// 右边索引一直递减,直到找到奇数为止
while(left < right && nums[right] % 2 == 0) {
right--;
}
if(left > right) {
break;
}
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
return nums;
}
21. 链表中倒数第k个节点
/**
* 思路: 快慢指针
* 1. 让fast指针先走k步
* 2. 然后while循环中fast和slow均一次走一步,直到fast为空
* 3. 此时slow指向倒数第k个位置元素
*
* 时间: O(n)
* 空间: O(1)
*/
public ListNode getKthFromEnd(ListNode head, int k) {
if(head == null || head.next == null) {
return head;
}
ListNode fast = head;
ListNode slow = head;
for(int i = 0; i < k; i++) {
fast = fast.next;
}
while(fast != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
22. 反转链表
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null) {
return head;
}
ListNode next = reverseList(head.next);
head.next.next = head;
head.next = null;
return next;
}
23. 合并两个排序的链表
/**
* 思路: 迭代
* 1. 设置dummy哑节点,放置新链表之前,cur为当前节点,从dummy开始
* 2. 当两个链表为非空时进入循环,令新链表的下一个节点cur.next为val更小的节点,相应的链表节点后移一位
* 3. 每次循环cur也要后移一位
* 4. 循环结束后还有链表非空,cur指向非空链表
* 5. 返回dummy.next
*
* 时间:O(n+m)
* 空间:O(1)
*/
/*public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1 == null && l2 == null) {
return null;
}
if(l1 == null) {
return l2;
}
if(l2 == null) {
return l1;
}
ListNode dummy = new ListNode();
ListNode cur = dummy;
while(l1 != null && l2 != null) {
if(l1.val < l2.val) {
cur.next = l1;
l1 = l1.next;
} else {
cur.next = l2;
l2 = l2.next;
}
cur = cur.next;
}
cur.next = l1 == null ? l2 : l1;
return dummy.next;
}*/
/**
* 思路: 递归法
* 1. 递归终止条件: 有一个链表为空,则返回另一个链表
* 2. 比较两个链表头节点值,进行链表合并
* 时间: O(m+n)
* 空间: O(m+n)
*/
/*public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1 == null || l2 == null) {
return l1 == null ? l2 : l1;
}
if(l1.val <= l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
24. 树的子结构
/**
* 思路: 递归法/DFS
*
* 1. 遍历树A的节点,寻找与B根节点相同的节点
* 2. 如果nodeA.val = nodeB.val,说明已经找到一个相同的节点,进入helper方法判断接下来的节点是否相同
* 相同,则返回true,不相同则继续遍历A,找到下一个相同的节点
* 3. 继续遍历nodeA的左右节点,只要两者有一个为true则返回true
* 时间: O(n*m) n为A树节点数量,m为B树节点数量
* 空间: O(m)
*/
/*TreeNode nodeB;
public boolean isSubStructure(TreeNode A, TreeNode B) {
if(B == null) {
return false;
}
this.nodeB = B;
return dfs(A);
}
// 找到A中与B的第一个节点相同的节点
// 前序遍历: 根左右
private boolean dfs(TreeNode nodeA) {
if(nodeA == null) {
return false;
}
if(nodeA.val == nodeB.val) {
// 判断nodeA中是否存在与nodeB相同的结构
if(helper(nodeA, nodeB)) {
return true;
}
}
return dfs(nodeA.left) || dfs(nodeA.right);
}
// 判断从A的子树是否有和B相同的部分
private boolean helper(TreeNode nodeA, TreeNode nodeB) {
// nodeB遍历完为空
if(nodeB == null) {
return true;
}
if(nodeA == null || nodeA.val != nodeB.val) {
return false;
}
return helper(nodeA.left, nodeB.left) && helper(nodeA.right, nodeB.right);
}*/
/**
* 思路: BFS(广度优先搜索)
*
* 1. 先遍历树A,如果遍历到和B节点相同的节点,进入helper方法判断接下来的节点是否都相同
* 2. 节点都相同返回true,不相同则返回false,并且继续遍历树A找下一个相同的节点
* 3. 如果遍历完了A还没有返回过true,则返回false
*
* 时间: O(m*n) m为A树总节点数,n为B数总节点数
* 空间: O(m) 最差情况下遍历A中所有节点入队
*/
public boolean isSubStructure(TreeNode A, TreeNode B) {
if(B == null || A == null) {
return false;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(A);
while(!queue.isEmpty()) {
TreeNode node = queue.poll();
if(node.val == B.val) {
if(helper(node, B)) {
return true;
}
}
if(node.left != null) {
queue.offer(node.left);
}
if(node.right != null) {
queue.offer(node.right);
}
}
return false;
}
private boolean helper(TreeNode nodeA, TreeNode nodeB) {
Queue<TreeNode> queueA = new LinkedList<>();
Queue<TreeNode> queueB = new LinkedList<>();
queueA.offer(nodeA);
queueB.offer(nodeB);
while(!queueB.isEmpty()) {
nodeA = queueA.poll();
nodeB = queueB.poll();
if(nodeA == null || nodeA.val != nodeB.val) {
return false;
}
if(nodeB.left != null) {
queueA.offer(nodeA.left);
queueB.offer(nodeB.left);
}
if(nodeB.right != null) {
queueA.offer(nodeA.right);
queueB.offer(nodeB.right);
}
}
return true;
}
25. 二叉树的镜像
/**
* 思路: dfs/递归
* 后序遍历 先局部翻转后整体
* 1. 特判: 如果root为空,则返回
* 2. 把root的左子树放到mirrorTree中镜像一下
* 3. 把root的右子树放到mirrorTree中镜像一下
* 4. 交换左右子树
*
* 先交换还是先镜像都可以,先交换则是前序遍历,先镜像则是后序遍历
* 时间:O(n) n为树的节点个数
* 空间:O(h) h为树的深度
*/
/*public TreeNode mirrorTree(TreeNode root) {
if(root == null) {
return root;
}
TreeNode leftTree = mirrorTree(root.left);
TreeNode rightTree = mirrorTree(root.right);
root.left = rightTree;
root.right = leftTree;
return root;
}*/
/*public TreeNode mirrorTree(TreeNode root) {
if(root == null) {
return root;
}
root.left = mirrorTree(root.left);
root.right = mirrorTree(root.right);
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
return root;
}*/
// 前序遍历 先整体后局部翻转
/*public TreeNode mirrorTree(TreeNode root) {
if(root == null) {
return root;
}
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
mirrorTree(root.left);
mirrorTree(root.right);
return root;
}*/
/**
* 思路: bfs
* 层序遍历
* 只要遍历到所有节点并且对每个节点都交换一下左右子树
* 先交换还是先加入队列都可以,不影响最终结果
*
* 时间: O(n)
* 空间: O(n)
*/
public TreeNode mirrorTree(TreeNode root) {
if(root == null) {
return root;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()) {
TreeNode node = queue.poll();
if(node.left != null) {
queue.offer(node.left);
}
if(node.right != null) {
queue.offer(node.right);
}
TreeNode tmp = node.left;
node.left = node.right;
node.right = tmp;
}
return root;
}
26. 对称的二叉树
/**
* 思路: 递归
* 1. dfs如果两个节点都不存在,返回true
* 2. 如果有一个节点不存在而另一个节点存在 或者 两个节点值不相同,返回false
* 3. 递归进入左子树的左节点和右子树的右节点,以及左子树的右节点和右子树的左节点比较
*
* 时间: O(n)
* 空间: O(n)
*/
/*public boolean isSymmetric(TreeNode root) {
if(root == null)
return true;
return dfs(root.left, root.right);
}
private boolean dfs(TreeNode left, TreeNode right) {
if(left == null && right == null) {
return true;
}
if(left == null || right == null || left.val != right.val) {
return false;
}
return dfs(left.left, right.right) && dfs(left.right, right.left);
}*/
/**
* 思路: 迭代法
* 1. 维护两个队列q1,q2
* 2. 把root的左节点加入q1,右节点加入q2
*
* 时间: O(n)
* 空间: O(n)
*/
public boolean isSymmetric(TreeNode root) {
if(root == null) {
return true;
}
Queue<TreeNode> queue1 = new LinkedList<>();
Queue<TreeNode> queue2 = new LinkedList<>();
queue1.offer(root.left);
queue2.offer(root.right);
while(!queue1.isEmpty() && !queue2.isEmpty()) {
TreeNode node1 = queue1.poll();
TreeNode node2 = queue2.poll();
if(node1 == null && node2 == null)
continue;
if(node1 == null || node2 == null || node1.val != node2.val) {
return false;
}
queue1.offer(node1.left);
queue1.offer(node1.right);
queue2.offer(node2.right);
queue2.offer(node2.left);
}
return true;
}
27. 顺时针打印矩阵
/*public int[] spiralOrder(int[][] matrix) {
if(matrix == null || matrix.length == 0)
return new int[0];
int n = matrix.length * matrix[0].length;
int[] res = new int[n];
int count = 0;
int flag = 1; // 1向右 2向下 3向左 4向上
int x = 0, y = 0; // 坐标
boolean[][] visit = new boolean[matrix.length][matrix[0].length];
while(count < n) {
// 越界判断
if(y >= matrix[0].length || y < 0 || x >= matrix.length || x < 0 || visit[x][y]) {
if(flag == 1) {
y--;
x++;
flag = 2;
} else if(flag == 2) {
x--;
y--;
flag = 3;
} else if(flag == 3) {
y++;
x--;
flag = 4;
} else {
x++;
y++;
flag = 1;
}
} else {
res[count] = matrix[x][y];
count++;
visit[x][y] = true;
if(flag == 1) {
y++;
} else if(flag == 2) {
x++;
} else if(flag == 3) {
y--;
} else {
x--;
}
}
}
return res;
}*/
/**
* 思路: 模拟打印
* 1. 每次一旦遍历到边界就转换方向,并且边界紧缩
* 2. 总共遍历四个方向
* 左->右, res.append(matrix[top][i]),遍历到边界后,top下移
* 上->下, res.append(matrix[i][right]),遍历到边界后,right左移
* 右->左, res.append(matrix[bottom][i]),遍历到边界后,bottom上移
* 下->上, res.append(matrix[i][left]),遍历到边界后,left右移
*
* 时间: O(m*n)
* 空间: O(1)
*/
public int[] spiralOrder(int[][] matrix) {
if(matrix == null || matrix.length == 0) {
return new int[0];
}
int m = matrix.length;
int n = matrix[0].length;
int[] res = new int[m * n];
int count = 0;
int left = 0;
int right = n-1;
int top = 0;
int bottom = m-1;
while(count < res.length) {
// 左->右
for (int i = left; i <= right; i++) {
res[count++] = matrix[top][i];
}
top++;
if(top > bottom) {
break;
}
// 上->下
for (int i = top; i <= bottom; i++) {
res[count++] = matrix[i][right];
}
right--;
if(left > right) {
break;
}
// 右->左
for (int i = right; i >= left; i--) {
res[count++] = matrix[bottom][i];
}
bottom--;
if(top > bottom) {
break;
}
// 下->上
for (int i = bottom; i >= top; i--) {
res[count++] = matrix[i][left];
}
left++;
if(left > right) {
break;
}
}
return res;
}
28. 栈的压入_弹出序列
/**
* 思路: 用一个新栈实时模拟进出栈操作
* 1. for循环入栈pushed元素,每push一次就检查能不能pop出来
* 2. 如果最后栈为空,说明一进一出刚刚好
*
* 时间: O(n)
* 空间: O(n)
*/
public static boolean validateStackSequences(int[] pushed, int[] popped) {
Stack<Integer> stack = new Stack<>();
int index = 0;
for (int num : pushed) {
stack.push(num);
while(!stack.isEmpty() && stack.peek() == popped[index]) {
stack.pop();
index++;
}
}
return stack.isEmpty();
}
29. I_从上到下打印二叉树
// 层序遍历
// 时间:O(n)
// 空间:O(n)
public int[] levelOrder(TreeNode root) {
if(root == null) {
return new int[0];
}
ArrayList<Integer> list = new ArrayList<>();
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()) {
TreeNode node = queue.poll();
list.add(node.val);
if(node.left != null) {
queue.offer(node.left);
}
if(node.right != null) {
queue.offer(node.right);
}
}
int[] res = new int[list.size()];
for(int i = 0; i < list.size(); i++) {
res[i] = list.get(i);
}
return res;
}
30. II_从上到下打印二叉树
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if(root == null) {
return res;
}
LinkedList<TreeNode> queue = new LinkedList<>();
queue.add(root);
int count = 0;
while(!queue.isEmpty()) {
count = queue.size();
List<Integer> tmp = new ArrayList<>();
for (int i = 0; i < count; i++) {
TreeNode node = queue.pop();
tmp.add(node.val);
if(node.left != null) {
queue.add(node.left);
}
if(node.right != null) {
queue.add(node.right);
}
}
res.add(tmp);
}
return res;
}
31. III_从上到下打印二叉树
public List<List<Integer>> levelOrder(TreeNode root) {
if(root == null) {
return new ArrayList<>();
}
List<List<Integer>> res = new ArrayList<>();
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int level = 0;
while(!queue.isEmpty()) {
int size = queue.size();
List<Integer> tmp = new ArrayList<>();
while(size > 0) {
TreeNode node = queue.poll();
if(level % 2 == 0) {
tmp.add(node.val);
} else {
tmp.add(0, node.val);
}
if(node.left != null) {
queue.offer(node.left);
}
if(node.right != null) {
queue.offer(node.right);
}
size--;
}
level++;
res.add(tmp);
}
return res;
}
32. 二叉搜索树的后序遍历序列
/**
* 思路: 二叉搜索树的定义,左子树值都小于根节点,右子树值都大于根节点
* 1. 设置k计数项,用于记录后序遍历序列中左子树的选取范围,以此划分左右子树
* 2. postorder[k] > postorder[r],此时k为左子树数量,左子树范围是[l,k-1],右子树范围是[k,r-1]
* 3. 判断右子树的值是否都大于postorder[r]根节点的值,不是则返回false
* 4. 递归传入左子树和右子树,设置终止条件(当l >= r,则返回true)
*/
public boolean verifyPostorder(int[] postorder) {
if(postorder == null || postorder.length == 0) {
return false;
}
return dfs(0, postorder.length-1, postorder);
}
private boolean dfs(int l, int r, int[] postorder) {
// 递归终止条件
if(l >= r) {
return true;
}
// 根节点的值
int root = postorder[r];
// 左子树起点下标
int k = l;
// 找到左子树节点的范围
while(k < r && postorder[k] < root) {
k++;
}
// 右子树起点,判断是否都大于根节点,否则直接返回false
for (int i = k; i < r; i++) {
if(postorder[i] < root) {
return false;
}
}
// 递归判断左子树和右子树是否满足
return dfs(l, k-1, postorder) && dfs(k, r-1, postorder);
}
33. 二叉树中和为某一值的路径
/**
* 思路: 遍历二叉树,并通过两个动态数组用于记录节点(path)和存储结果集(res)
* 1. 读取当前节点值,将当前节点加入到记录节点动态数组中(path)
* 2. 判断当前节点是不是叶子节点并且满足target目标值,如果是则加入到结果集中
* 3. 递归先进入左子树,并且传入当前计算结果target
* 4. 递归进入右子树,并且传入当前结算结果target
* 5. 子树遍历完后,移除记录节点动态数组(path)中当前节点值(即最后一个元素),进行回溯
*/
// 结果集
List<List<Integer>> res = new ArrayList<>();
// 路径
List<Integer> path = new ArrayList<>();
public List<List<Integer>> pathSum(TreeNode root, int target) {
dfs(root, target);
return res;
}
private void dfs(TreeNode root, int target) {
if(root == null) {
return;
}
// 添加当前二叉树的节点值
path.add(root.val);
// 计算target剩余值
target -= root.val;
// 满足条件,到达叶子节点并且taget==0时
if(root.left == null && root.right == null && target == 0) {
res.add(new ArrayList<>(path));
}
// 递归左子树
dfs(root.left, target);
// 递归右子树
dfs(root.right, target);
// 子树遍历完毕后,进行回溯,移除动态数组当前节点值
path.remove(path.size()-1);
}
34. 复杂链表的复制
/**
* 思路: 通过遍历当前链表,逐个复制当前遍历得到的节点
* 1. 遍历当前链表,并依据得到的节点值,新建节点clone并添加到链表中
* 2. 再次遍历链表,当指针cur指向的random不空为时,则为下一个节点(cur.next)的random指针赋予对应random指向对象的克隆
* cur.next.random = cur.random.next
* (cur = cur.next.next)
* 3. 拆分链表,用res存储新链表的头节点,再创建指针pre/cur,通过修改pre与cur的next指向拆分链表,注意最后pre.next要指向空
*
*/
public Node1 copyRandomList(Node1 head) {
if(head == null) {
return null;
}
Node1 cur = head;
// 拷贝完全相同的节点添加到链表中
while(cur != null) {
Node1 clone = new Node1(cur.val);
clone.next = cur.next;
cur.next = clone;
cur = clone.next;
}
cur = head;
// 设置新节点的random值
while(cur != null) {
if(cur.random != null) {
cur.next.random = cur.random.next;
}
cur = cur.next.next;
}
// 拆分链表
Node1 pre = head;
cur = head.next;
Node1 res = head.next;
while(cur.next != null) {
pre.next = pre.next.next;
cur.next = cur.next.next;
cur = cur.next;
pre = pre.next;
}
pre.next = null;
return res;
}
35. 二叉搜索树与双向链表
Node pre;
Node head;
public Node treeToDoublyList(Node root) {
if(root == null) {
return null;
}
dfs(root);
pre.right = head;
head.left = pre;
return head;
}
private void dfs(Node cur) {
if(cur == null) {
return;
}
dfs(cur.left);
if(pre == null) {
head = cur;
} else {
cur.left = pre;
pre.right = cur;
}
pre = cur;
dfs(cur.right);
}
36. 序列化二叉树
/**
* 思路: BFS
* 序列化:
* 1. 用BFS遍历树,不管node的左右子节点是否存在,都加入到队列中
* 2. 节点出队时,如果节点不存在,在返回值res加入一个"null",如果节点存在,则加入节点的字符串形式
* 反序列化:
* 1. 利用队列新建二叉树
* 2. 将data转化为列表,然后遍历,只要不为null将节点按顺序加入二叉树中,同时还要将节点入队
* 3. 队列为空时,遍历结束,返回根节点
*
* 时间: O(n)
* 空间: O(n)
*/
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
if(root == null) {
return "[]";
}
StringBuilder res = new StringBuilder();
res.append("[");
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()) {
TreeNode node = queue.poll();
if(node == null) {
res.append("null,");
} else {
res.append(node.val + ",");
queue.offer(node.left);
queue.offer(node.right);
}
}
res.deleteCharAt(res.length()-1);
res.append("]");
return res.toString();
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
if(data == null || data.equals("[]") || data.length() == 0) {
return null;
}
data = data.substring(1, data.length()-1);
String[] nums = data.split(",");
// 构建根节点
TreeNode root = new TreeNode(Integer.valueOf(nums[0]));
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int index = 1;
while(!queue.isEmpty()) {
TreeNode node = queue.poll();
if(!nums[index].equals("null")) {
node.left = new TreeNode(Integer.valueOf(nums[index]));
queue.offer(node.left);
}
index++;
if(!nums[index].equals("null")) {
node.right = new TreeNode(Integer.valueOf(nums[index]));
queue.offer(node.right);
}
index++;
}
return root;
}
/**
* 思路: DFS
*
* 序列化:
* 1. 递归终止条件,为空时返回"null"
* 2. 序列化结果: 根节点值 + "," + 左子节点值(进入递归) + "," + 右子节点值(进入递归)
*
* 比如 1
* / \
* 2 3
* 序列化结果是:1,2,null,null,3,null,null
* 反序列:
* 1. 先把字符串转换为队列
* 2. 进入递归
* 2.1 队列出队
* 2.2 如果元素为"null",返回null
* 2.3 不为"null",新建一个值为弹出元素的新节点
* 2.4 其左子节点为队列的下一个元素,其右子节点为队列的下下个元素
*
* 时间: O(n)
* 空间: O(n)
*/
/*public String serialize(TreeNode root) {
if(root == null) {
return "null";
}
return root.val + "," + serialize(root.left) + "," + serialize(root.right);
}
public TreeNode deserialize(String data) {
if(data == null || data.equals("[]") || data.length() == 0) {
return null;
}
Queue<String> queue = new LinkedList<>(Arrays.asList(data.split(",")));
return dfs(queue);
}
private TreeNode dfs(Queue<String> queue) {
String val = queue.poll();
if("null".equals(val)){
return null;
}
TreeNode root = new TreeNode(Integer.parseInt(val));
root.left = dfs(queue);
root.right = dfs(queue);
return root;
}*/
37. 字符串的排列
/**
* 思路: 全排列重复元素问题(回溯 + 排序 + 剪枝)
*
* 1. 对s字符串转化为数组后进行排序,比如aacb,排序后aabc
* 2. 对aabc进行排列
* 3. 设置布尔类型的used数组标记元素是否已被选择过
* 4. 同一树层中如果arr[i] == arr[i-1] && used[i-1] == false,则进行剪枝,因为同一树层已使用过
* 5. 如果同一树枝中没被使用过(used[i]=false]),则开始进行处理
* 设置当前元素used[i]=true并且加入到路径path中,接着递归进入下一层,最后进行回溯,则删除路径中当前元素,used[i]=false
*/
// 存放符合条件结果的集合
List<String> res = new ArrayList<>();
public String[] permutation(String s) {
if(s == null || s.length() == 0) {
return new String[0];
}
// 用来存放符合条件结果
StringBuilder path = new StringBuilder();
boolean[] used = new boolean[s.length()];
char[] newS = s.toCharArray();
Arrays.sort(newS);
backtrack(newS, path, used);
return res.toArray(new String[0]);
}
private void backtrack(char[] arr, StringBuilder path, boolean[] used) {
if(path.length() == arr.length) {
res.add(new String(path));
return;
}
for (int i = 0; i < arr.length; i++) {
// used[i-1] == false,说明同一树层nums[i-1]使用过
if(i > 0 && arr[i] == arr[i-1] && !used[i-1]) {
continue;
}
// 同一树枝s.chatAt(i)没使用过开始处理
if(!used[i]) {
used[i] = true;
path.append(arr[i]);
backtrack(arr, path, used);
path.deleteCharAt(path.length() - 1);
used[i] = false;
}
}
}
38. 数组中出现次数超过一半的数字
/**
* 思路: 摩尔投票法
*
* 时间: O(n)
* 空间: O(1)
*/
public int majorityElement(int[] nums) {
int vote = 0;
int count = 0;
for (int i = 0; i < nums.length; i++) {
if(count == 0) {
vote = nums[i];
}
count += vote == nums[i] ? 1 : -1;
}
return vote;
}
39. 最小的k个数
/**
* 思路: 最大堆
*
* 时间: O(NlogK)
* 空间: O(K)
*/
/*public int[] getLeastNumbers(int[] arr, int k) {
if(arr == null || arr.length == 0) {
return new int[0];
}
PriorityQueue<Integer> queue = new PriorityQueue<>((o1, o2) -> (o2 - o1));
for (int num : arr) {
if(queue.isEmpty() || queue.size() < k) {
queue.offer(num);
} else if (queue.peek() > num) {
queue.poll();
queue.offer(num);
} else {
continue;
}
}
int[] res = new int[k];
for (int i = 0; i < k; i++) {
res[i] = queue.poll();
}
return res;
}*/
/**
* 思路: 快排
* 时间: O(n)
*/
public int[] getLeastNumbers(int[] arr, int k) {
if(arr == null || arr.length == 0 || k == 0) {
return new int[0];
}
if(arr.length <= k) {
return arr;
}
return quickSearch(arr, 0, arr.length-1, k-1);
}
private int[] quickSearch(int[] arr, int left, int right, int k) {
int mid = partition(arr, left, right);
if(k == mid) {
return Arrays.copyOf(arr, k+1);
}
if(k < mid) {
return quickSearch(arr, left, mid-1, k);
} else {
return quickSearch(arr, mid+1, right, k);
}
}
private int partition(int[] arr, int left, int right) {
// 备份轴点元素
int pivot = arr[left];
while(left < right) {
while(left < right) {
if(arr[right] > pivot) {
right--;
} else {
arr[left++] = arr[right];
break;
}
}
while(left < right) {
if(arr[left] < pivot ) {
left++;
} else {
arr[right--] = arr[left];
break;
}
}
}
arr[left] = pivot;
return left;
}
40. 数据流中的中位数
/**
* 思路: 堆的应用
* 例如: 1 2 4 6 8 10 划分左右半区两个堆,4是最大堆中的最大值,6是最小堆的最小值
* 1. 维护一个最大堆和一个最小堆
* - 最大堆: 最小的k个数
* - 最小堆: 最大的k个数
* 2. 每次插入数据时保证两个堆的数量均衡
* 3. 最大堆数量 == 最小堆数量,新的数据插入到最小堆中
* 最大堆数量 != 最小堆数量,新的数据插入到最堆堆中
*
* 时间: O(logn)
*/
/** initialize your data structure here. */
PriorityQueue<Integer> minHeap;
PriorityQueue<Integer> maxHeap;
public _剑指_Offer_41_数据流中的中位数() {
// 默认最小堆
this.minHeap = new PriorityQueue<>();
this.maxHeap = new PriorityQueue<>((o1, o2) -> (o2 - o1));
}
public void addNum(int num) {
// 插入到最小堆中
if(minHeap.size() == maxHeap.size()) {
// 插入时,先插入到最大堆中,弹出最大堆最大值插入到最小堆中,因为最小堆维护最大的k个数
maxHeap.offer(num);
minHeap.offer(maxHeap.poll());
} else {
// 插入到最大堆中
// 插入时,先插入到最小堆中,弹出最小堆最小值插入到最大堆中,因为最大堆维护最小的k个数
minHeap.offer(num);
maxHeap.offer(minHeap.poll());
}
}
public double findMedian() {
if(minHeap.size() == maxHeap.size()) {
return (minHeap.peek() + maxHeap.peek()) / 2.0;
} else {
return minHeap.peek();
}
}
41. 连续子数组的最大和
/**
* 思路: 动态规划
*
* 1. sum表示存储以前一个nums[i]结尾的子数组中,和最大的是多少
* 2. 如果sum < 0时, 将 sum = 0
* 3. 如果sum >= 0时, 则 sum += num
* 4. 每一次迭代,都要更新res,记录最大值
* 时间: O(n)
* 空间: O(1)
*/
public static int maxSubArray(int[] nums) {
if(nums == null || nums.length == 0) {
return 0;
}
int sum = 0;
int res = Integer.MIN_VALUE;
for (int num : nums) {
if(sum < 0) {
sum = 0;
}
sum += num;
res = Math.max(res, sum);
}
return res;
}
/**
* 思路: 动态规划
*
* 1. 确定dp数组以及下标含义
* dp[i]:表示以nums[i]结尾的连续子数组的最大和
* 2. 确定递推公式
* - dp[i-1] > 0, dp[i] = dp[i-1] + nums[i]
* - dp[i-1] <= 0, dp[i] = nums[i]
* 3. 初始化
* 4. 确定遍历顺序
*
* 时间: O(n)
* 空间: O(n)
*/
/*public static int maxSubArray(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
// dp[i]以nums[i]结尾的连续子数组的最大和
int[] dp = new int[nums.length];
// 初始化
dp[0] = nums[0];
int res = nums[0];
for (int i = 1; i < nums.length; i++) {
if(dp[i-1] > 0) {
dp[i] = dp[i-1] + nums[i];
} else {
dp[i] = nums[i];
}
res = Math.max(res, dp[i]);
}
return res;
}
*/
42. 1到n整数中1出现的次数
/**
* 思路: 数学问题
* 例子3 1 0 1 5 9 2
* high cur low
* 左边high = n / base / 10, 当前位cur = n / base % 10, 右边low = n % base
*
* 1. cur此时指向3101(5)92 即百位时为5
* 此时cur>1时, base=100,则假设百位为1时
* 左边高位范围(0,3101),右边低位范围(0,99)
* 当cur>1时,方案数(high + 1) * base 方案
*
* 2. cur此时指向310(1)592 即千位时为1
* 此时cur==1时,base=1000,则假设千位为1时
* 分两种情况讨论
* 2.1 左边高位范围(0,309),右边低位范围(0,999) 方案数为 high * base
* 2.2 左边高位范围(310,310),右边低位范围(0,592) 方案数为 low + 1
* 综上,当cur==1时,方案数=hig * base + low + 1
*
* 3. cur此时指向31(0)1592 即万位时为0
* 此时cur==0, base=10000,则假设万位为1时
* 左边高位范围(0,30) 右边低位范围(0,9999)
* 当cur==0时, 方案数 high * base
*/
public int countDigitOne(int n) {
long base = 1;
int res = 0;
while(base <= n) {
long high = n / base / 10;
long low = n % base;
long cur = n / base % 10;
if(cur > 1) {
res += (high + 1) * base;
} else if(cur == 1) {
res += high * base + low + 1;
} else {
res += high * base;
}
base *= 10;
}
return res;
}
43. 把数组排成最小的数
/**
* 思路: 字符串的快排
* 例子: [3,30]
* 330 > 303,所以两者交换顺序,小的排在前面
* 最后结果为303
*
* 因此,利用快速排序定义数组,传入比较规则
*
* 时间: O(nlogn)
* 空间: O(n)
*/
public String minNumber(int[] nums) {
String[] stringNums = new String[nums.length];
for(int i = 0; i < nums.length; i++) {
stringNums[i] = String.valueOf(nums[i]);
}
Arrays.sort(stringNums, (o1, o2) -> (o1 + o2).compareTo(o2 + o1));
StringBuilder res = new StringBuilder();
for(String num : stringNums) {
res.append(num);
}
return res.toString();
}
44. 把数字翻译成字符串
/**
* 思路: 动态规划
*
* x1x2....X(i-2) X(i-1) Xi ..... X(n-1) Xn
* 比如 123
* 递推公式: 如果X(i-1)和Xi可翻译,即整体被翻译,那么dp[i] = dp[i-2]
* 如果Xi单独翻译,那么dp[i] = dp[i-1]
* 所以综上,当Xi-1Xi能够被翻译时, dp[i] = dp[i-1] + dp[i-2],
* 如果Xi-1Xi不能被翻译时(比如04、05、26、27等不满足条件的两位数),dp[i] = dp[i-1]
*
* 初始化时, dp[0] = 1, dp[1] = 1(一个数字时只有一种翻译方法)
* dp[0] = 1是为了推导dp[2]
*
* 时间: O(n)
* 空间: O(n)
*/
/*public int translateNum(int num) {
String s = String.valueOf(num);
// dp[i]表示以Xi为结尾的数字的翻译方案数量
int[] dp = new int[s.length() + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= s.length(); i++) {
int curSum = Integer.valueOf(s.charAt(i-2) - '0') * 10 + Integer.valueOf(s.charAt(i-1) - '0');
if(curSum >= 10 && curSum <= 25) {
dp[i] = dp[i-1] + dp[i-2];
} else {
dp[i] = dp[i-1];
}
}
return dp[s.length()];
}*/
// 空间优化
public int translateNum(int num) {
String s = String.valueOf(num);
int a = 1;
int b = 1;
for (int i = 2; i <= s.length(); i++) {
int curSum = Integer.valueOf(s.charAt(i-2) - '0') * 10 + Integer.valueOf(s.charAt(i-1) - '0');
int sum = 0;
if(curSum >= 10 && curSum <= 25) {
sum = a + b;
} else {
sum = b;
}
a = b;
b = sum;
}
return b;
}
45. 礼物的最大价值
/**
* 思路: 动态规划
*
* 1. 只能向下或向右走,所以(i,j)由左边和上边过来,因此dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]) + grid[i-1][j-1];
*
* 时间: O(m*n)
* 空间: O(m*n)
*/
/*public int maxValue(int[][] grid) {
if(grid == null || grid.length == 0) {
return 0;
}
int m = grid.length;
int n = grid[0].length;
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]) + grid[i-1][j-1];
}
}
return dp[m][n];
}*/
// 优化到一维
public int maxValue(int[][] grid) {
if(grid == null || grid.length == 0) {
return 0;
}
int m = grid.length;
int n = grid[0].length;
int[] dp = new int[n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
dp[j] = Math.max(dp[j-1], dp[j]) + grid[i-1][j-1];
}
}
return dp[n];
}
46. 最长不含重复字符的子字符串
/**
* 思路: 滑动窗口,双指针法
*
* 1. left和right指针,最开始指向下标为0,然后right开始往右移动
* 2. 把扫描过的元素放到map中,如果right扫描过的元素没有重复就一直后移,顺便记录一下最大值
* 3. 如果right扫描过的元素有重复元素,则改变left指针,取Math.max(left, 原来元素下标值+1)
*
* 时间: O(n)
* 空间: O(1) 字符最多128个
*
*/
public int lengthOfLongestSubstring(String s) {
if(s == null || s.length() == 0) {
return 0;
}
int left = 0;
int right = 0;
int res = 0;
// 存储元素对应的下标值
Map<Character, Integer> map = new HashMap<>();
while(right < s.length()) {
if(map.containsKey(s.charAt(right))) {
// 取最大值是因为可能出现 a b b a情况时, 当right = 3时, 此时left = 2,若不取最大值,则left会变为1
left = Math.max(left, map.get(s.charAt(right)) + 1);
}
map.put(s.charAt(right), right);
res = Math.max(res, right - left +1);
right++;
}
return res;
}
47. 丑数
/**
* 思路: 三指针法
*
* 2x展开 (2 * 1) (2 * 2) (2 * 3) ......
* 3x展开 (3 * 1) (3 * 2) (3 * 3) ......
* 5x展开 (5 * 1) (5 * 2) (5 * 3) ......
* 因此我们可以用三个指针来表示x, 通过动态规划来进行排序存储
*
* p2代表对应dp数组值 * 2
* p3代表对应dp数组值 * 3
* p5代表对应dp数组值 * 5
*/
public int nthUglyNumber(int n) {
if(n == 1) {
return 1;
}
int[] dp = new int[n];
dp[0] = 1;
int p2 = 0, p3 = 0, p5 = 0;
for (int i = 1; i < n; i++) {
dp[i] = Math.min(Math.min(dp[p2] * 2, dp[p3] * 3), dp[p5] * 5);
if(dp[i] == dp[p2] * 2) {
p2++;
}
if(dp[i] == dp[p3] * 3) {
p3++;
}
if(dp[i] == dp[p5] * 5) {
p5++;
}
}
return dp[n-1];
}
48. 第一个只出现一次的字符
/**
* 思路: 哈希表
*
*/
public char firstUniqChar(String s) {
if(s == null || s.length() == 0) {
return ' ';
}
Map<Character, Boolean> map = new HashMap<>();
char[] chars = s.toCharArray();
for (char c : chars) {
map.put(c, !map.containsKey(c));
}
for (char c : chars) {
if(map.get(c)) {
return c;
}
}
return ' ';
}
49. 数组中的逆序对
int res = 0;
public int reversePairs(int[] nums) {
if(nums == null || nums.length == 0) {
return 0;
}
int[] tmp = new int[nums.length];
mergeSort(nums, 0, nums.length-1, tmp);
return res;
}
private void mergeSort(int[] nums, int left, int right, int[] tmp) {
if(left < right) {
int mid = (left + right) >> 1;
// 递归划分左区间
mergeSort(nums, left, mid, tmp);
// 递归划分右区间
mergeSort(nums, mid+1, right, tmp);
// 合并已经排好序的部分
merge(nums, left, mid, right, tmp);
}
}
private void merge(int[] nums, int left, int mid, int right, int[] tmp) {
// 标记左半区第一个未排序的元素
int l_pos = left;
// 标记右半区第一个未排序的元素
int r_pos = mid+1;
// 临时数组下标
int pos = left;
while(l_pos <= mid && r_pos <= right) {
if(nums[l_pos] <= nums[r_pos]) {
tmp[pos++] = nums[l_pos++];
} else {
res += mid - l_pos + 1;
tmp[pos++] = nums[r_pos++];
}
}
// 合并左区间剩余元素
while(l_pos <= mid) {
tmp[pos++] = nums[l_pos++];
}
// 合并右区间剩余元素
while(r_pos <= right) {
tmp[pos++] = nums[r_pos++];
}
// 拷贝临时数组到原数组中
while(left <= right) {
nums[left] = tmp[left++];
}
}
50. 两个链表的第一个公共节点
/**
* 思路: 双指针法
* 1. p、q同时走,p或q一旦遇到空时,就将指针指向对方的起始点
* 2. 相当于p走了a+b步, q走了b+a步
*/
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode p = headA;
ListNode q = headB;
while(p != q) {
if(p == null) {
p = headB;
} else {
p = p.next;
}
if(q == null) {
q = headA;
} else {
q = q.next;
}
}
return p;
}
51. I_在排序数组中查找数字
/**
* 思路: 二分法
*
* 1. 通过二分法找到右边界j
* 其中nums[mid] == target时, left = mid + 1
* 2. 通过二分法找到左边界i
* 其中nums[mid] == target时, right = mid - 1;
* 3. 返回结果j - i - 1
*
* 时间: O(logn)
* 空间: O(1)
*/
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length-1;
// 1. 二分找到右边界
while(left <= right) {
int mid = (left + right) >> 1;
if(nums[mid] > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
int j = left;
left = 0;
right = nums.length-1;
// 2. 二分找到左边界
while(left <= right) {
int mid = (left + right) >> 1;
if(nums[mid] >= target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
int i = right;
return j - i - 1;
}
52. II_缺失的数字
/**
* 思路: 二分法
*
* 1. 如果中间元素的值和下标相等,那么下一轮查找只需要查找右半边;
* 2. 如果中间元素的值和下标不相等,那么下一轮就只查找左半边。
*
* 时间: O(logn)
* 空间: O(1)
*/
public int missingNumber(int[] nums) {
int left = 0;
int rihgt = nums.length-1;
while(left <= rihgt) {
int mid = (left + rihgt) / 2;
if(nums[mid] == mid) {
left = mid + 1;
} else {
rihgt = mid - 1;
}
}
return left;
}
53. 二叉搜索树的第k大节点
/**
* 思路: 中序遍历
*
* 右根左遍历
*/
int k = 0;
int res = Integer.MIN_VALUE;
public int kthLargest(TreeNode root, int k) {
this.k = k;
dfs(root);
return res;
}
private void dfs(TreeNode root) {
if(root == null) return;
dfs(root.right);
k--;
if(k == 0) {
res = root.val;
}
dfs(root.left);
}
54. I_二叉树的深度
/**
* 思路: 递归法
*/
/*public int maxDepth(TreeNode root) {
if(root == null) return 0;
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}*/
/**
* 思路: bfs迭代法
*/
public int maxDepth(TreeNode root) {
if(root == null) return 0;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int res = 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);
}
size--;
}
res++;
}
return res;
}
55. II_平衡二叉树
/**
* 思路: 递归
* 1. 根节点的左子树和右子树的高度差的绝对值要 <= 1
* 2. 要保证左子树和右子树都是平衡!!
*/
/*public boolean isBalanced(TreeNode root) {
if(root == null) {
return true;
}
return Math.abs(getDepth(root.left) - getDepth(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right);
}
private int getDepth(TreeNode root) {
if(root == null) {
return 0;
}
return Math.max(getDepth(root.left), getDepth(root.right)) + 1;
}*/
/**
* 思路: 后序 + 剪枝
*/
public boolean isBalanced(TreeNode root) {
if(root == null) return true;
if(dfs(root) == -1) {
return false;
} else {
return true;
}
}
private int dfs(TreeNode root) {
if(root == null) {
return 0;
}
int left = dfs(root.left);
if(left == -1) {
return -1;
}
int right = dfs(root.right);
if(right == -1) {
return -1;
}
return Math.abs(left - right) <= 1 ? Math.max(left, right) + 1 : -1;
}
56. I_数组中数字出现的次数
/**
* 思路: 分组位运算
*
* 1. 遍历nums进行异或运算, 比如4 4 3 3 1 7 最后异或结果为1和7
* 2. 循环左移计算m, 因为1和7肯定有一个二进制位不相同,将m与第一步异或结果进行与运算,结果为0时(二进制位相同),就左移m,直到不为0为止(二进制位不相同)
* 3. 拆分nums为两个子数组, 根据第二步计算m的结果, 与num进行与运算,结果为0的划分为一个数组,不为0的划分为另一个数字,这样1和7就被划分到不同的数组中了
* 4. 对拆分出来的数组进行异或运算,这样4 4 1结果为1, 3 3 7结果为7
*
* 时间: O(n)
* 空间: O(1)
*/
public int[] singleNumbers(int[] nums) {
int x = 0, y = 0;
int m = 1;
int z = 0;
for (int num : nums) {
z ^= num;
}
// z = x ^ y
while((z & m) == 0) {
m <<= 1;
}
for (int num : nums) {
if((num & m) == 0) {
x ^= num;
} else {
y ^= num;
}
}
return new int[]{x, y};
}
57. II_数组中数字出现的次数
/**
* 思路: 位运算
* 1. int类型是32位,统计所有数组在某一个位置的和能否被3整除,如果不能被3整除,说明那个只出现1次的数字对应的那个位置二进制位为1
* 2. 所以统计所有的32位,遍历判断即可
*/
public int singleNumber(int[] nums) {
int res = 0;
for (int i = 0; i < 32; i++) {
int oneCount = 0;
for (int j = 0; j < nums.length; j++) {
oneCount += (nums[j] >>> i) & 1;
}
if(oneCount % 3 == 1) {
res |= 1 << i;
}
}
return res;
}
58. II_和为s的连续正数序列
/**
* 思路: 滑动窗口
* 1. 使用两个指针left和right, left指向1,right指向1,分别表示窗口左边界和右边界,然后计算窗口内元素的和
* 2. 如果窗口内的值大于target,说明窗口大了,left右移
* 如果窗口内的值小于target,说明窗口小了,right右移
* 如果窗口内的值等于target,说明找到了一组序列,加入到列表中
*
* 记住左闭右开区间,找到一组序列时,此时right值是右边界,不纳入范围内
*
* 左闭右开区间
*/
/*public int[][] findContinuousSequence(int target) {
List<int[]> res = new ArrayList<>();
int left = 1; // 滑动窗口左边界
int right = 1; // 滑动窗口右边界
int sum = 0;
while(left <= target/2) {
if(sum < target) {
sum += right;
right++;
} else if(sum > target) {
sum -= left;
left++;
} else {
int[] tmp = new int[right-left];
for (int k = left; k < right; k++) {
tmp[k - left] = k;
}
res.add(tmp);
sum -= left;
left++;
}
}
return res.toArray(new int[res.size()][]);
}*/
/**
* 思路: 滑动窗口
* 1. 使用两个指针left和right, left指向1,right指向2,分别表示窗口左边界和右边界,然后计算窗口内元素的和
* 2. 如果窗口内的值大于target,说明窗口大了,left右移
* 如果窗口内的值小于target,说明窗口小了,right右移
* 如果窗口内的值等于target,说明找到了一组序列,加入到列表中
*
* 左闭右闭区间
*/
public int[][] findContinuousSequence(int target) {
List<int[]> res = new ArrayList<>();
int left = 1; // 滑动窗口左边界
int right = 2; // 滑动窗口右边界
int sum = left + right;
while(left <= target/2) {
if(sum < target) {
sum += ++right;
} else if(sum > target) {
sum -= left;
left++;
} else {
int[] tmp = new int[right-left+1];
for (int k = left; k <= right; k++) {
tmp[k - left] = k;
}
res.add(tmp);
sum -= left;
left++;
}
}
return res.toArray(new int[res.size()][]);
}
59. 和为s的两个数字
/**
* 思路: 双指针法
* 1. 左指针与右指针的数字和如果大于target,则右指针--
* 2. 左指针与右指针的数字和如果小于target,则左指针++
* 3. 左指针与右指针的数字和如果等于target,则直接返回
*/
public int[] twoSum(int[] nums, int target) {
int left = 0;
int right = nums.length-1;
while(left < right) {
if(nums[left] + nums[right] > target) {
right--;
} else if(nums[left] + nums[right] < target) {
left++;
} else {
return new int[]{nums[left], nums[right]};
}
}
return new int[0];
}
60. I_翻转单词顺序
/**
* 思路: 用StringBuilder拼接拆分后的字符串
* 1. 从后往前添加字符串
* 2. 遇到空字符串时,跳过
* 3. 最后利用trim()函数去除拼接出来多余的" "
*/
public String reverseWords(String s) {
String[] strs = s.split(" ");
StringBuilder res = new StringBuilder();
for(int i = strs.length-1; i >= 0; i--) {
if(strs[i].equals("")) {
continue;
}
res.append(strs[i] + " ");
}
return res.toString().trim();
}
61. II_左旋转字符串
/**
* 思路: 直接拼接法
* 时间: O(1)
* 空间: O(n)
*/
/*public String reverseLeftWords(String s, int n) {
return s.substring(n) + s.substring(0, n);
}*/
/**
* 思路: 利用StringBuilder拼接每个字符
* 时间: O(1)
* 空间: O(n)
*/
public String reverseLeftWords(String s, int n) {
StringBuilder res = new StringBuilder();
for (int i = n; i < n + s.length(); i++) {
res.append(s.charAt(i % s.length()));
}
return res.toString();
}
62. I_滑动窗口的最大值
/**
* 思路: 滑动窗口 + 暴力
*
* 时间: O(n * k)
* 空间: O(n)
*/
/*public static int[] maxSlidingWindow(int[] nums, int k) {
int left = 0;
int right = 0;
List<Integer> res = new ArrayList<>();
while(right < nums.length) {
while(right - left < k) {
right++;
}
int maxVal = Integer.MIN_VALUE;
for (int i = left; i < right; i++) {
maxVal = Math.max(maxVal, nums[i]);
}
res.add(maxVal);
left++;
}
int[] result = new int[res.size()];
for (int i = 0; i < res.size(); i++) {
result[i] = res.get(i);
}
return result;
}*/
/**
* 思路: 滑动窗口 + 单调队列
* 1. 遍历给定的数组元素,如果队列不为空且待加入的元素大于队尾元素,则移除队尾元素,直到队尾元素大于等于待加入元素,满足后,向队尾添加当前元素的下标值
* 2. 计算组成满足滑动窗口的左边界left值
* 3. 如果发现队首元素下标值小于left值,则表明队首元素已经不在满足滑动窗口当中了,则需要移除对应在滑动窗口的下标值
* 4. 如果left值大于等于0时,说明窗口刚好形成,向结果集中添加满足窗口的最大值,即队列中的首元素的下标值对应的元素值
* 5. 移动右指针,重复1,2,3,4步骤
*
* 时间: O(n)
* 空间: O(k) 队列的长度
*/
public static int[] maxSlidingWindow(int[] nums, int k) {
// 滑动窗口个数
int[] res = new int[nums.length-k+1];
int left = 0;
int right = 0;
// 存储窗口单调递减下标值
LinkedList<Integer> queue = new LinkedList<>();
// 移动窗口右边界,直到小于数组元素大小,代表结束
while(right < nums.length) {
// 队列不为空且新加入的元素值大于队列队尾的元素时,需要一直出队尾元素,保持队列单调递减
while(!queue.isEmpty() && nums[queue.peekLast()] < nums[right]) {
queue.removeLast();
}
// 添加元素的下标值
queue.addLast(right);
// 计算每次待加入窗口的左边界
left = right - k + 1;
// 当队首元素的下标小于左边界时,表明队首元素已经不再滑动窗口中,需要移除掉
if(queue.peekFirst() < left) {
queue.removeFirst();
}
// 窗口形成,等同于 right + 1 >= k
if(left >= 0) {
res[left] = nums[queue.peekFirst()];
}
right++;
}
return res;
}
63. II_队列的最大值
// 封装一个队列实现基本的功能
Queue<Integer> queue;
// 用于实现O(1)取得最大值
Deque<Integer> deque;
public _剑指_Offer_59_II_队列的最大值() {
this.queue = new LinkedList<>();
this.deque = new LinkedList<>();
}
public int max_value() {
// 最大值队列中不为空返回首个元素(单调递减)
if(deque.size() != 0) {
return deque.peekFirst();
} else {
// 为空时返回-1
return -1;
}
}
public void push_back(int value) {
// 元素入队基本队列中
queue.offer(value);
// 保持最大值队列的单调性,保持单调递减
while(!deque.isEmpty() && deque.peekLast() < value) {
deque.pollLast();
}
// 元素入队最大值队列
deque.offerLast(value);
}
public int pop_front() {
// 若基本队列为空时,直接返回-1
if(queue.isEmpty()) {
return -1;
}
// 判断出队首元素是否和最大值队列的队首元素是否相等,若相等,最大值队列也要出队
if(queue.peek().equals(deque.peekFirst())) {
deque.pollFirst();
}
// 基本队列出队首元素
return queue.poll();
}
64. n个骰子的点数
/**
* 思路: 动态规划
*
* 1. 确定dp数组以及下标含义, 用二维数组表示dp[i][j]代表投掷完i个骰子后,点数为j出现的次数
* 2. 确定递推公式, dp[i][j] 由前一阶段推出 即dp[i-1][j-{1,6}]
* 3. 初始化,投掷一个骰子时, dp[1][j]
* 4. 确定遍历顺序,从前往后,i从2开始递增到n
*
* 时间: O(n^2)
* 空间: O(n^2)
*/
public double[] dicesProbability(int n) {
// dp[i][j]表示投掷完i枚后,点数j出现的次数
int[][] dp = new int[n + 1][6 * n + 1];
// 递推公式
// 最后一个阶段的骰子数 由 前一个阶段的骰子数转化过来
// dp[i][j] = dp[i-1][j-1] + dp[i-1][j-2] + dp[i-1][j-3] + dp[i-1][j-4] + dp[i-1][j-5] + dp[i-1][j-6]
// 初始化
// 投掷一枚骰子时
for (int j = 1; j <= 6; j++) {
dp[1][j] = 1;
}
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= 6 * i; j++) {
for (int k = 1; k <= 6; k++) {
if(j - k <= 0) {
break;
}
dp[i][j] += dp[i-1][j-k];
}
}
}
double[] res = new double[6 * n - n + 1];
for (int i = n; i <= 6 * n; i++) {
res[i - n] = (double)dp[n][i] / Math.pow(6, n);
}
return res;
}
65. 扑克牌中的顺子
/**
* 思路: 模拟
*
* Set + 遍历
* 1. 构成顺子的条件
* - 无重复元素(大小王除外)
* - 最大值 - 最小值 < 5 (大小王除外)
*
* 时间: O(N) = O(5) = O(1)
* 空间: O(N) = O(5) = O(1)
*/
/*public boolean isStraight(int[] nums) {
Set<Integer> set = new HashSet<>();
int minVal = 14;
int maxVal= 0;
for (int num : nums) {
// 遇到大小王时则跳过
if(num == 0) {
continue;
}
if(set.contains(num)) {
return false;
}
set.add(num);
minVal = Math.min(minVal, num);
maxVal = Math.max(maxVal, num);
}
return maxVal - minVal < 5;
}*/
/**
* 思路: 模拟
*
* 快排 + 遍历
* 1. 构成顺子的条件
* - 无重复元素(大小王除外)
* - 最大值 - 最小值 < 5 (大小王除外)
*
* 时间: O(nlogn) = O(1)
* 空间: O(logn) = O(1)
*/
public boolean isStraight(int[] nums) {
Arrays.sort(nums);
int jokers = 0;
for (int i = 0; i < 4; i++) {
if(nums[i] == 0) {
jokers++;
} else if(nums[i+1] == nums[i]) {
return false;
}
}
// 最大值 - 最小值
return nums[4] - nums[jokers] < 5;
}
66. 圆圈中最后剩下的数字
/**
* 思路: 模拟
* 1. 将数据添加到ArrayList中,模拟链条循环
* 2. 下一个删除位置下标是(idx + m - 1 ) % n, 减1的原因是idx在删除一个元素后,从下一个元素的当前位置出发,所以仅需要加上 m-1步即可
* %n表示求模剩下元素大小
*/
public int lastRemaining(int n, int m) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < n; i++) {
list.add(i);
}
int idx = 0;
while(n > 1) {
// idx = 2 % 5 = 2; idx = 4 % 4 = 0; idx = 2 % 3 = 2; idx = 4 % 2 = 0
idx = (idx + m - 1) % n;
list.remove(idx);
n--;
}
return list.get(0);
}
67. 股票的最大利润
// 贪心算法
/*public int maxProfit(int[] prices) {
int left = Integer.MAX_VALUE;
int res = 0;
for (int i = 0; i < prices.length; i++) {
if(prices[i] < left) {
left = prices[i];
}
res = Math.max(res, prices[i]-left);
}
return res;
}*/
// 动态规划
public int maxProfit(int[] prices) {
if(prices.length == 0)
return 0;
// 1. 确定dp数组以及下标含义
// dp[i][j] 表示第i天 状态为j的最大现金价值
// dp[i][0] 持有股票 dp[i][1] 不持有股票
int[][] dp = new int[prices.length][2];
// 2. 确定递推公式
/*
dp[i][0] = max(dp[i-1][0], -prices[i])
dp[i][1] = max(dp[i-1][1], prices[i] + dp[i-1][0])
*/
// 3. 初始化
dp[0][0] = -prices[0];
dp[0][1] = 0;
// 4. 确定遍历顺序
for (int i = 1; i < prices.length; i++) {
dp[i][0] = Math.max(dp[i-1][0], -prices[i]);
dp[i][1] = Math.max(dp[i-1][1], prices[i] + dp[i-1][0]);
}
return dp[prices.length-1][1];
}
68. 求1到n的和
/**
* 思路: 逻辑符短路
*
* 1. 不能使用乘法和除法符号
* 2. 不能使用if,for,while
* 3. 所以使用&& 与逻辑符 判断终止条件,使得递归终止
*/
/*int sum = 0;
public int sumNums(int n) {
boolean x = n > 1 && sumNums(n-1) > 0;
sum += n;
return sum;
}*/
public int sumNums(int n) {
boolean x = n > 1 && (n += sumNums(n-1)) > 0;
return n;
}
69. 不用加减乘除做加法
/**
* 思路: 位运算
*
* 1. 将两个数的和拆为两步骤,
* 第一步骤是忽略进位,计算直接相加(^异或运算)
* 第二步骤是计算进位和
* 最后将第一步骤和第二步骤的结果相加即可(又重新回到两个数相加,那么重复第一和第二步骤)
*
* 2. 对于直接相加,可以直接两个数进行异或运算得到结果
* 3. 对于计算进位和, 先进行与运算,再左移一位
*
* 例子: 比如 1100(12) + 1111(15)
* 1. 直接相加结果 = 0011
* 2. 进位结果 = 11000
* 3. 最后结果 11011 (十进制27)
*/
public int add(int a, int b) {
while(b != 0) {
int x = a ^ b;
int y = (a & b) << 1;
a = x;
b = y;
}
return a;
}
70. 构建乘积数组
/**
* 思路: 左边的乘积乘以右边的乘积
*
* 1. 构造左边乘积数组,不包含当前元素
* 2. 构造右边乘积数组,不包含当前元素
* 3. 构造结果集,将左边乘积数组 与 右边乘积数组 对应元素相乘得到结果
*/
public int[] constructArr(int[] a) {
if(a == null || a.length == 0) {
return new int[0];
}
int length = a.length;
// 构造左边乘积
int[] resLeft = new int[length];
// 构造右边乘积
int[] resRight = new int[length];
resLeft[0] = 1;
resRight[length-1] = 1;
for (int i = 1; i < length; i++) {
resLeft[i] = resLeft[i-1] * a[i-1];
}
int[] res = new int[length];
for (int i = length-2; i >= 0; i--) {
resRight[i] = resRight[i+1] * a[i+1];
}
for (int i = 0; i < length; i++) {
res[i] = resLeft[i] * resRight[i];
}
return res;
}
71. 把字符串转换成整数
public static int strToInt(String str) {
char[] chars = str.trim().toCharArray();
if(chars.length == 0) {
return 0;
}
int res = 0;
// 1表示正数,-1表示负数
int sign = 1;
// 处理越界的情况
int a = Integer.MAX_VALUE / 10;
// 从非符号位开始
int index = 1;
if(chars[0] == '-') {
sign = -1;
} else if(chars[0] != '+') {
// 说明没有符号位,则默认为正数,下标从0开始
index = 0;
}
for (int i = index; i < chars.length; i++) {
// 遇到非数字的时候,结束循环
if(chars[i] < '0' || chars[i] > '9') {
break;
}
// 越界
// 若res = 214748365以上, 则 10×res > 2147483647
// 若res = 214748364, 且 后面紧跟着数字 > 7, 则 10×res > 2147483647
// 注意这里的 > 7 巧妙的把负数的情况也包含了进来,结果依然正确
if(res > a || (res == a) && (chars[i] - '0') > Integer.MAX_VALUE % 10) {
return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
}
res = res * 10 + (chars[i] - '0');
}
return res * sign;
}
72. I_二叉搜索树的最近公共祖先
/**
* 思路: 递归法(做法与二叉树一样) (后序遍历回溯法)
*/
/*public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || p == root || q == root) {
return root;
}
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if(left != null && right != null) {
return root;
} else if(left != null && right == null) {
return left;
} else if(left == null && right != null) {
return right;
} else {
return null;
}
}*/
/**
* 思路: 递归法(利用二叉搜索树的特性,前序遍历法)
* 这里递归函数有返回值,即标准的搜索一条边的写法,一旦遇到满足条件的情况,直接返回
*
*/
/*public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 递归终止条件 一定能找到祖先节点,无需处理空的情况
// 根
// 左子树 (左)
if(p.val < root.val && q.val < root.val) {
TreeNode left = lowestCommonAncestor(root.left, p, q);
if(left != null) {
return left;
}
} else if(p.val > root.val && q.val > root.val) {
// 右子树 (右)
TreeNode right = lowestCommonAncestor(root.right, p, q);
if(right != null) {
return right;
}
} else {
// 若p,q分散在左右子树,则root即为公共祖先节点
return root;
}
return null;
}*/
/**
* 思路: 迭代法
*/
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
while(root != null) {
if(root.val > p.val && root.val > q.val) {
root = root.left;
} else if(root.val < p.val && root.val < q.val) {
root = root.right;
} else {
return root;
}
}
return null;
}
73. 二叉树的最近公共祖先
/**
* 思路: (求最小公共祖先问题) 递归回溯法
* 1. 从底向上遍历,二叉树只能通过后序遍历实现
* 2. 回溯过程中,需要遍历整颗二叉树,即使已经找到结果了,依然要把其它节点遍历完,因为要使用递归函数的返回值做逻辑判断
* (递归函数什么时候需要返回值?
* 遍历整颗二叉树,不需要返回值,如果要搜索其中一条符合条件的路径,递归函数需要返回值,因为遇到了
* 符合条件的路径了就需要及时返回, 有了返回值还要做进一步处理。)
*
* 对于本题来说,需要在回溯的过程中递归函数的返回值做判断路径符不符合,所以需要返回值,而且要遍历整颗二叉树,因为left和right后序还要做逻辑处理
*
* 3. 左为空,右不为空,则返回右节点
* 左不为空,右为空,则返回左节点
* 左不为空,右不为空,则返回根节点
* 左为空,右为空,则返回空节点
*/
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null || root == p || root == q)
return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if(left != null && right != null) {
return root;
}
if(left == null && right != null) {
return right;
}
if(right == null && left != null) {
return left;
}
return root;
}
补充类似题
1. 合并K个升序链表
/**
* 思路:逐一合并两条链表
*
* 时间: O(NK) K条链表的总节点数是N
* 空间: O(1)
*/
/*public ListNode mergeKLists(ListNode[] lists) {
if(lists == null || lists.length == 0) {
return new ListNode();
}
ListNode res = lists[0];
for (int i = 1; i < lists.length; i++) {
res = mergeTwoList(res, lists[i]);
}
return res;
}*/
/**
* 思路: 归并排序
* 1. 将lists列表不断进行二分划分,最后划分到只剩下一个链表
* 2. 然后将划分出来的链表们进行两两合并,一直合并到生成最终的升序链表为止
*
* 时间: O(NlogK) 每一层归并的时间是O(N),归并层数最大为O(logK+1)
* 空间: O(logN) 递归栈的大小
*/
public ListNode mergeKLists(ListNode[] lists) {
if(lists.length == 0) {
return null;
}
return merge(lists, 0, lists.length-1);
}
private ListNode merge(ListNode[] lists, int left, int right) {
if(left == right) {
return lists[left];
}
int mid = left + (right - left) / 2;
ListNode l1 = merge(lists, left, mid);
ListNode l2 = merge(lists, mid + 1, right);
return mergeTwoList(l1, l2);
}
private ListNode mergeTwoList(ListNode res, ListNode list) {
// 迭代法合并
ListNode dummy = new ListNode();
ListNode cur = dummy;
while(res != null && list != null) {
if(res.val <= list.val) {
cur.next = res;
res = res.next;
} else {
cur.next = list;
list = list.next;
}
cur = cur.next;
}
cur.next = res == null ? list : res;
return dummy.next;
}
2. 通配符匹配
/**
* 思路: 动态规划(匹配问题、最长公共子串都可以用动态规划)
* 1. dp[i][j] : s的前i个能否被p的前j个匹配
* 2. 手动求二维矩阵的每个值,通过计算可以发现:
* - 第0列,除了dp[0][0]=true,其它的dp[i][0] = false
* - s从0开始算,p从1开始算
* - 过程中考虑dp[i][j]由哪个值得来
* 3. 需要考虑p的当前字符p[j]:
* a. 当前字符是字母
* b. 当前字符是'?'
* 由a和b判断两个字符是否匹配
* 字符匹配: s[i] == p[j] || p[j] == '?'
* 字符不匹配: s[i] != p[j]
* c. 当前字符是'*'
* '*'可构造一切
* 使用'*': 代表一个字符串: s的当前位置可以由'*'代替,所以看s的前i-1和p的前j个是否可以匹配,即dp[i][j] = dp[i-1][j]
* 不使用'*': 代表空字符: s的当前真假取决于s前i个,p的前j-1个,即dp[i][j] = dp[i][j-1]
*
*
* 综上a,b,c条件
* 字符是字母或'?'时: 匹配时dp[i][j] = dp[i-1][j-1],不匹配时dp[i][j] = false
* 字符是'*'时: dp[i][j] = dp[i-1][j] || dp[i][j-1]
*
* 时间: O(n*m)
* 空间: O(n*m)
*/
/*public boolean isMatch(String s, String p) {
if(s.length() == 0 && p.length() == 0) {
return true;
}
if(p.length() == 0) {
return false;
}
// 1. 定义dp数组
int m = s.length();
int n = p.length();
boolean[][] dp = new boolean[m+1][n+1];
// 2. 初始化 dp[i][0]=false
dp[0][0] = true;
// 为了处理边界,初始化0行
for (int j = 1; j <= n; j++) {
if(p.charAt(j-1) == '*') {
// 取决于上一个
dp[0][j] = dp[0][j-1];
}
// 不是'*'为false
}
// 3. 确定遍历顺序,先遍历s,后遍历p
for (int i = 1; i <= m; i++) {
char charS = s.charAt(i-1);
for (int j = 1; j <= n; j++) {
char charP = p.charAt(j - 1);
if(charP != '*') {
// p当前字符是字母或者?时
// s和p匹配时取决于dp[i-1][j-1],不匹配为false
if(charP == charS || charP == '?') {
dp[i][j] = dp[i - 1][j - 1];
}
} else {
// p字符是'*'时
// 可以代表一个字符串或者代表空字符
dp[i][j] = dp[i-1][j] || dp[i][j-1];
}
}
}
return dp[m][n];
}*/
/**
* 思路: 动态规划 + 矩阵压缩为一维空间
* 记录leftUp值,在更新dp[j]时,需要记录下原来的leftUp值
* 时间: O(n*m)
* 空间: O(n)
*/
public boolean isMatch(String s, String p) {
if(s.length() == 0 && p.length() == 0) {
return true;
}
if(p.length() == 0) {
return false;
}
// 1. 定义dp数组
int m = s.length();
int n = p.length();
boolean[] dp = new boolean[n+1];
// 2. 初始化 dp[i][0]=false
dp[0] = true;
// 为了处理边界,初始化0行
for (int j = 1; j <= n; j++) {
if(p.charAt(j-1) == '*') {
// 取决于上一个
dp[j] = dp[j-1];
}
// 不是'*'为false
}
boolean leftUp = dp[0];
dp[0] = false;
// 3. 确定遍历顺序,先遍历s,后遍历p
for (int i = 1; i <= m; i++) {
char charS = s.charAt(i-1);
for (int j = 1; j <= n; j++) {
char charP = p.charAt(j - 1);
if(charP != '*') {
// p当前字符是字母或者?时
// s和p匹配时取决于dp[i-1][j-1],不匹配为false
if(charP == charS || charP == '?') {
// 字符相等时,需要从左上角推出,所以需要一个临时变量存储当前位置的值
boolean temp = dp[j];
dp[j] = leftUp;
leftUp = temp;
} else {
leftUp = dp[j];
dp[j] = false;
}
} else {
// p字符是'*'时
// 可以代表一个字符串或者代表空字符
// 从左或上推出
leftUp = dp[j];
dp[j] = dp[j-1] || dp[j];
}
}
// 注意:每结束一行,leftUp值为上一行的最末尾,所以需要更新为dp[0]
leftUp = dp[0];
/*System.out.println(leftUp);
for (int j = 0; j <= n; j++) {
System.out.print(dp[j] + " ");
}
System.out.println();*/
}
return dp[n];
}
3. 爬楼梯
/**
* 思路1: 动态规划求解
* dp[i] = dp[i-1] + dp[i-2]
* 由于dp[i]只与前面两项相关,因此可以通过三个变量sum,a,b记录,优化空间
*
* 时间:O(n)
* 空间:O(1)
*/
/*public int climbStairs(int n) {
if(n < 2) {
return 1;
}
int a = 1;
int b = 1;
int sum = 0;
for(int i = 2; i <= n; i++) {
sum = a + b;
a = b;
b = sum;
}
return b;
}*/
/**
* 思路: 完全背包问题中的求排列问题
*
* n相当于背包容量
* 每次爬1,2阶相当于物品
*
* 1. 确定dp数组以及下标含义 dp[j]表示爬到j层的排列数
* 2. 确定递推公式, dp[j] += dp[j-nums[i]] 其中nums[i] = {1,2}
* 3. 进行初始化, dp[0] = 1,是递推公式的前提
* 4. 确定遍历顺序, 排列问题是先遍历背包容量,后遍历物品,外循环背包容量从0开始从前往后推增
* 5. 返回结果值dp[j]
*
* 时间: O(n*m) m=2
* 空间: O(n)
*
*/
public int climbStairs(int n) {
// 1. 确定dp数组以及下标含义
// dp[j]表示凑成n的排列数
int[] dp = new int[n+1];
// 2. 确定递推公式
// 物品 nums[i] = {1,2}
// dp[j] += dp[j-nums[i]]
// 3. 初始化
// dp[0]=1是递推公式的前提
dp[0] = 1;
int[] nums = {1,2};
// 4. 确定遍历顺序
// 排列问题: 先背包容量,后物品, 外循环背包容量从0从前往后遍历
for(int j = 0; j <= n; j++) {
for(int i = 0; i < nums.length; i++) {
if(j >= nums[i]) {
dp[j] += dp[j-nums[i]];
}
}
}
return dp[n];
}
4. 超级次方
/**
* 快速幂
* 比如a^[2,3,7,8]
* a^2378
* = (a^237)^10 * a^8
* = ((a^23)^10 * a^7) * a^8
* = ((a^2)^10 * a^3 * a^7) * a^8
* = a^0......
*/
public int superPow(int a, int[] b) {
Deque<Integer> queue = new ArrayDeque<>();
for (int i : b) {
queue.add(i);
}
// 重载该方法
return superPow(a, queue);
}
private int superPow(int a, Deque<Integer> queue) {
// [2,3,7,8]从后往前依次出栈,为空时,为0次幂
if(queue.isEmpty()) {
return 1;
}
// 比如a^[2,3,7,8] = a^2378
// 第一步(a^237)^10 * a^8
int lastBit = queue.removeLast();
// a^8
int part1 = myPow(a, lastBit);
// (a^237)^10
int part2 = myPow(superPow(a, queue), 10);
return (part1 * part2) % 1337;
}
private int myPow(int a, int b) {
if(b == 0) {
return 1;
}
// 奇数
a %= 1337;
if(b % 2 == 1) {
return (a * myPow(a, b-1)) % 1337;
} else {
return myPow(a*a, b/2);
}
}