剑指Offer
1.数组中的重复数字
解题思路
使用一个哈希表用于记录某个数字出现的次数,**注意题目潜台词说明该数组必然出现重复的元素,因此 if 条件不可能不成立,最后的返回值任意返回即可 **
代码实现
public class s1 {
HashMap<Integer, Integer> map = new HashMap<>();
public int findRepeatNumber(int[] nums) {
int n = nums.length;
for (int i = 0; i < n; i++) {
if (map.containsKey(nums[i])) {
map.put(nums[i], map.get(nums[i]) + 1);
} else {
map.put(nums[i], 1);
}
if (map.get(nums[i]) >= 2) {
return nums[i];
}
}
return nums[n - 1];
}
}
2.二维数组中的查找
解题思路
首先理清题目,请注意每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。由这个规律,我们可以得出该二维数组左下的元素大于它上方的元素,小于它右方的元素,右上的元素与之相反,可以使用二分法
- 获得矩阵的长和宽,排除特殊情况
- 以左下角为起点,如果其小于目标元素,就往右移动寻找更大的元素,如果其大于目标元素就往上走找更小的元素,返回查询结果即可
代码实现
public class s2 {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
if (matrix.length == 0 || matrix[0].length == 0) {
return false;
}
int m = matrix.length;
int n = matrix[0].length;
//从左下角开始
for (int i = m - 1, j = 0; i >= 0 && j < n; ) {
//目标元素小于当前元素的值,往上走
if (matrix[i][j] > target) {
i--;
} else if (matrix[i][j] < target) {
//目标元素大于当前元素的值,往右走
j++;
} else {
return true;
}
}
return false;
}
}
3.替换空格
解题思路
因为 Java 中字符串不可变,因此需要 new 一个方便操作的字符串。然后将字符串转换为字符数组并进行遍历,如果碰到 空格 ,就向 res 中加入 “%20” , 否则就将该字符加入 res 中
代码实现
public class s3 {
public String replaceSpace(String s) {
StringBuilder res = new StringBuilder();
for(Character c : s.toCharArray()){
if(c == ' '){
res.append("%20");
}else {
res.append(c);
}
}
return res.toString();
}
}
4.从头到尾打印链表
解题思路
把链表中的数据存放在一个 ArrayList
数组中,反转这个数组,然后遍历这个 ArrayList
将其中的值放入 res 中即可
代码实现
public class s4 {
ArrayList<Integer> temp = new ArrayList<>();
int[] res;
public int[] reversePrint(ListNode head) {
if (head == null){
return res = new int[0];
}
ListNode index = head;
while (index != null){
temp.add(index.val);
index = index.next;
}
Collections.reverse(temp);
res = new int[temp.size()];
for(int i=0;i<temp.size();i++){
res[i] = temp.get(i);
}
return res;
}
}
5.重建二叉树
解题思路
前序遍历的顺序:根左右
中序遍历的顺序:左根右
按照这两个性质就可以写出重建二叉树的方法
代码实现
public class s5 {
HashMap<Integer, Integer> valToIndex = new HashMap<>();
public TreeNode buildTree(int[] preorder, int[] inorder) {
for (int i = 0; i < inorder.length; i++) {
valToIndex.put(inorder[i], i);
}
return build(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1);
}
public TreeNode build(int[] preorder, int pStr, int pEnd, int[] inorder, int iStr, int iEnd) {
if (pStr > pEnd) {
return null;
}
int rootVal = preorder[pStr];
int index = valToIndex.get(rootVal);
int leftSize = index - iStr;
TreeNode root = new TreeNode(rootVal);
root.left = build(preorder, pStr + 1, pStr + leftSize, inorder, iStr, index - 1);
root.right = build(preorder, pStr + leftSize + 1, pEnd, inorder, index + 1, iEnd);
return root;
}
}
6.用两个栈实现队列
解题思路
- 入栈就正常把元素压入 inStack
- 出栈首先判断 outStack 是否不为空,如果不空直接返回 outStack 里面的元素,如果是空的就把 inStack 里面的元素全部压入 outStack 然后返回栈顶元素即可
代码实现
public class s6 {
Deque<Integer> inStack;
Deque<Integer> outStack;
public s6() {
inStack = new ArrayDeque<Integer>();
outStack = new ArrayDeque<Integer>();
}
public void appendTail(int value) {
inStack.push(value);
}
public int deleteHead() {
if(!outStack.isEmpty()){
return outStack.pop();
}else{
while(!inStack.isEmpty()){
outStack.push(inStack.pop());
}
return outStack.isEmpty() ? -1 : outStack.pop();
}
}
}
7.斐波那契数列
解题思路
动态规划
base case:
dp[0] = 0;
dp[1] = 1;
状态转移方程
dp[i] = dp[i-1] + dp[i-2];
注意最后取模就行
代码实现
public class s7 {
public int fib(int n) {
if(n == 0){
return 0;
}
int[] dp = new int[n+1];
//base case
dp[0] = 0;
dp[1] = 1;
for(int i=2;i<=n;i++){
dp[i] = dp[i-1] + dp[i-2];
dp[i] %= 1000000007;
}
return dp[n];
}
}
8.青蛙跳台阶问题
解题思路
动态规划
base case:
dp[0] = 1;
dp[1] = 1;
状态转移方程
dp[i] = dp[i - 1] + dp[i - 2];
最后注意取模即可
代码实现
public class s8 {
public int numWays(int n) {
if (n == 0) {
return 1;
}
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
dp[i] %= 1000000007;
}
return dp[n];
}
}
9.旋转数组的最小数字
解题思路
平常的使用 sort 排序然后找第一个的方法太 low 了,这里不谈。
使用二分法进行最小数的查找
- 声明 i,j 双指针指向 nums 数组左右两端
- 开始循环,令 m = (i + j) / 2 为每次二分的中点,具体分为三种情况
- nums [m] > nums[j] 时:说明 m 一定在旋转数组的左边,即旋转点 x 一定在
[m + 1, j]
闭区间内,执行 i = m + 1 - nums[m] < nums[j] 时:说明 m 一定在旋转数组的右边,即旋转点 x 一定在
[i,m]
闭区间内,执行 j = m - nums[m] == nums[j] ,执行 j = j - 1
- nums [m] > nums[j] 时:说明 m 一定在旋转数组的左边,即旋转点 x 一定在
- 当 i == j 跳出循环,返回此时 nums[i] 的值即可
代码实现
public class s9 {
public int minArray(int[] numbers) {
int i = 0, j = numbers.length - 1;
while (i < j) {
int m = (i + j) / 2;
if (numbers[m] > numbers[j]) {
i = m + 1;
} else if (numbers[m] < numbers[j]) {
j = m;
} else {
j--;
}
}
return numbers[i];
}
}
10.矩阵中的路径
解题思路
采用深度优先搜索和剪枝的方式解决
明确终止条件
- 行或列索引越界
- 当前矩阵字符与目标字符不同
递归工作
- 标记当前访问过的元素,采用特殊符号标记的形式避免其他问题发生
- 搜索下一单元格,上下左右均可
- 在搜索完成后将特殊标记清除,恢复矩阵保证下次搜索的正常进行
代码实现
public boolean exist(char[][] board, String word) {
char[] words = word.toCharArray();
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
if (dfs(board, words, i, j, 0)) {
return true;
}
}
}
return false;
}
/**
* @param board 对象矩阵
* @param word 目标字符
* @param i 行索引
* @param j 列索引
* @param k 目标字符索引
*/
public boolean dfs(char[][] board, char[] word, int i, int j, int k) {
if (i >= board.length || i < 0 || j >= board[0].length ||
j < 0 || board[i][j] != word[k]) {
return false;
}
if (k == word.length - 1) {
return true;
}
board[i][j] = '\0';
boolean res = dfs(board, word, i + 1, j, k + 1) ||
dfs(board, word, i - 1, j, k + 1) ||
dfs(board, word, i, j + 1, k + 1) ||
dfs(board, word, i, j - 1, k + 1);
board[i][j] = word[k];
return res;
}
11.机器人的运动范围
解题思路
动态规划
base case:
dp[0][0] = true;
状态转移:
dp[i][j] |= dp[i - 1][j];
dp[i][j] |= dp[i][j - 1];
代码实现
public class s11 {
public int movingCount(int m, int n, int k) {
if (k == 0) {
return 1;
}
//dp[i][j] : (i,j)是否能够到达的布尔值
boolean[][] dp = new boolean[m][n];
int ans = 1;
dp[0][0] = true;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if ((i == 0 && j == 0) || get(i) + get(j) > k) {
continue;
}
if (i - 1 >= 0) {
dp[i][j] |= dp[i - 1][j];
}
if (j - 1 >= 0) {
dp[i][j] |= dp[i][j - 1];
}
ans += dp[i][j] ? 1 : 0;
}
}
return ans;
}
public int get(int x) {
int res = 0;
while (x != 0) {
res += x % 10;
x /= 10;
}
return res;
}
}
12.剪绳子
解题思路
动态规划
base case : 当绳长为2,最大乘积为被分为两段的乘积,为 2
dp[2] = 1
状态转移方程
dp[i] // 不剪绳子
j * (i - j) // 剪一段长度为 j ,另一段长度为 i - j
j * dp[i - j] // 剪一段长度为 j ,另一段也进行裁剪,即 dp[i-j]
代码实现
public class s12 {
public int cuttingRope(int n) {
//dp[i] : 记录从 0 到 i 绳子被剪成 m 段乘积的最大值
int[] dp = new int[n + 1];
//base case
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]));
}
}
return dp[n];
}
}
13.剪绳子-2
解题思路
循环取余+贪心
定义 mul 作为乘积,每次乘 3 后,n 要减3,以此实现有多少3乘多少3
需要讨论的是最后一段的取值,若最后一段余下2,3,4直接返回,因为特例最后一段为 1 时需要拆为 2 * 2
代码实现
public class s13 {
public int cuttingRope(int n) {
if (n <= 3) {
return n - 1;
}
//最大int 2147483647,为防止在某一次 mul乘3已经溢出,类型需要设为long
long mul = 1;
//3,3,3,3,3,4
//3,3,3,3,3,3
//3,3,3,3,3,2
//3,3,3,3,3,1 X 此种情况算在和前面 3+1 和为 4
while (n > 4) {
//每次乘积后取余防止大数
mul = mul * 3 % 1000000007;
n -= 3;
}
return (int) (mul * n % 1000000007);
}
}
14.二进制中1的个数
解题思路
对 int 的每一位进行检查,并统计 1 的个数
代码实现
public class s14 {
public int hammingWeight(int n) {
int ret = 0;
for (int i = 0; i < 32; i++) {
if ((n & (1 << i)) != 0) {
ret++;
}
}
return ret;
}
}
15.数值的整数次方
解题思路(搬运)
快速幂法
这里借用Krahets大佬的讲解
代码实现
public class s15 {
public double myPow(double x, int n) {
if (x == 0) {
return 0;
}
//防止int正负转换越界
long b = n;
double res = 1.0;
//负数转为正数计算
if (b < 0) {
x = 1 / x;
b = -b;
}
while (b > 0) {
// b % 2 == 1
if ((b & 1) == 1) {
res *= x;
}
// x = x ^ 2
x *= x;
// b //= 2(向下取整)
b >>= 1;
}
return res;
}
}
16.打印从1到最大的n位数
解题思路
通过观察发现最终生成的数字个数为 10 ^ n - 1
,因此生成一个对应大小的数组,遍历输入对应的值即可
代码实现
public class s16 {
int[] res;
public int[] printNumbers(int n) {
int end = (int) Math.pow(10, n) - 1;
res = new int[end];
for (int i = 0; i < end; i++) {
res[i] = i + 1;
}
return res;
}
}
18.删除链表的节点
解题思路
采用双指针结合虚拟头结点(dummy)的方式,首先让一个快指针指向 dummy.next ,让慢指针指向 dummy ,避免删除的是第一个节点导致空指针异常的错误,之后通过 while 循环寻找值为 val 的节点,找到了就让慢指针指向快指针的下一个节点。前进的方式是快慢指针每次同步前进一步,保证随时可以获取快指针指向节点的前一个节点。
代码实现
public class s17 {
public ListNode deleteNode(ListNode head, int val) {
ListNode dummy = new ListNode(-1), p = head, q = dummy;
dummy.next = head;
while (p != null) {
if (p.val == val) {
q.next = p.next;
break;
}
p = p.next;
q = q.next;
}
return dummy.next;
}
}
18.正则表达式匹配
解题思路
动态规划
base case:
dp[0][0] = true;
状态转移方程:
dp[i][j] = dp[i][j - 2];
dp[i][j] = dp[i][j] || dp[i - 1][j];
dp[i][j] = dp[i - 1][j - 1];
代码实现
public class s18 {
public boolean isMatch(String s, String p) {
int m = s.length();
int n = p.length();
boolean[][] dp = new boolean[m + 1][n + 1];
dp[0][0] = true;
for (int i = 0; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (p.charAt(j - 1) == '*') {
dp[i][j] = dp[i][j - 2];
if (matches(s, p, i, j - 1)) {
dp[i][j] = dp[i][j] || dp[i - 1][j];
}
} else {
if (matches(s, p, i, j)) {
dp[i][j] = dp[i - 1][j - 1];
}
}
}
}
return dp[m][n];
}
public boolean matches(String s, String p, int i, int j) {
if (i == 0) {
return false;
}
if (p.charAt(j - 1) == '.') {
return true;
}
return s.charAt(i - 1) == p.charAt(j - 1);
}
}
19.表示数值的字符串
解题思路
这里参考Krahets大佬的解法
本题使用有限状态自动机。根据字符类型和合法数值的特点,先定义状态,再画出状态转移图,最后编写代码即可。
字符类型:
空格 「 」、数字「0—9 」 、正负号 「 +− 」 、小数点 「 . 」 、幂符号 「 eE 」 。
状态定义:
按照字符串从左到右的顺序,定义以下 9 种状态。
开始的空格
幂符号前的正负号
小数点前的数字
小数点、小数点后的数字
当小数点前为空格时,小数点、小数点后的数字
幂符号
幂符号后的正负号
幂符号后的数字
结尾的空格
结束状态:
合法的结束状态有 2, 3, 7, 8 。
代码实现
这里展示Krahets大佬的代码
class s19 {
public boolean isNumber(String s) {
Map[] states = {
new HashMap<Character,Integer>() {{ put(' ', 0); put('s', 1); put('d', 2); put('.', 4); }}, // 0.
new HashMap<Character,Integer>() {{ put('d', 2); put('.', 4); }}, // 1.
new HashMap<Character,Integer>() {{ put('d', 2); put('.', 3); put('e', 5); put(' ', 8); }}, // 2.
new HashMap<Character,Integer>() {{ put('d', 3); put('e', 5); put(' ', 8); }}, // 3.
new HashMap<Character,Integer>() {{ put('d', 3); }}, // 4.
new HashMap<Character,Integer>() {{ put('s', 6); put('d', 7); }}, // 5.
new HashMap<Character,Integer>() {{ put('d', 7); }}, // 6.
new HashMap<Character,Integer>() {{ put('d', 7); put(' ', 8); }}, // 7.
new HashMap<Character,Integer>() {{ put(' ', 8); }} // 8.
};
int p = 0;
char t;
for(char c : s.toCharArray()) {
if(c >= '0' && c <= '9') t = 'd';
else if(c == '+' || c == '-') t = 's';
else if(c == 'e' || c == 'E') t = 'e';
else if(c == '.' || c == ' ') t = c;
else t = '?';
if(!states[p].containsKey(t)) return false;
p = (int)states[p].get(t);
}
return p == 2 || p == 3 || p == 7 || p == 8;
}
}
20.调整数组顺序使奇数位于偶数前面
解题思路
采用双指针的思想,i,j 分别指向 nums 两端,i 去寻找偶数,j 去寻找奇数,找到之后交换 i,j 指向元素的值
代码实现
public class s20 {
public int[] exchange(int[] nums) {
int i = 0, j = nums.length - 1;
while (i < j) {
while (i < j && (nums[i] % 2) != 0) {
i++;
}
while (i < j && (nums[j] % 2) == 0) {
j--;
}
swap(nums, i, j);
}
return nums;
}
public void swap(int[] nums, int a, int b) {
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
}