第2章_面试需要的基础知识
q02_实现单例
题目
实现一个单例设计模式
解答
public class Singleton {
/**
* volatile保证可见性、防止指令重排,不保证原子性
*/
private volatile static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
// 双端锁外层锁保证singleton实例被创建后,才会加锁,提高效率
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
q03_数组中重复的数字
题目
找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
示例
输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3
解答
public class Solution {
/**
* 题目:nums长度为n,数字为0到n-1,有些数字重复了,找出任意重复的一个数字
*/
public int findRepeatNumber(int[] nums) {
int index = 0;
while (index < nums.length) {
// 数字范围为0到n-1,重排后,数字i应该放在下标为i的位置上
// 0下标放元素0,不是重复元素,跳过本次循环,移动指针
if (nums[index] == index) {
index++;
continue;
}
// x下标没有元素x
// 如果元素x等于对应的x下标上元素,就是重复元素,返回
if (nums[index] == nums[nums[index]]) {
return nums[index];
} else {// 否则,交换元素x来到它对应的x下标位置
int temp = nums[index];
nums[index] = nums[temp];
nums[temp] = temp;
}
}
return -1;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {2, 3, 1, 0, 2, 5, 3};
System.out.println(solution.findRepeatNumber(nums));
}
}
q04_二维数组查找
题目
在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
示例
[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
解答
public class Solution {
/**
* 二维数组中的查找,元素值从上到小、从左到右递增
*/
public boolean findNumberIn2DArray(int[][] matrix, int target) {
if (matrix == null || matrix.length == 0) {
return false;
}
// 最左下角坐标:[matrix.length-1][0]
int i = matrix.length - 1;
int j = 0;
while (i >= 0 && j <= matrix[0].length - 1) {
if (matrix[i][j] < target) {
j++;
} else if (matrix[i][j] > target) {
i--;
} else {
return true;
}
}
return false;
}
}
q05_替换空格
题目
请实现一个函数,把字符串 s
中的每个空格替换成"%20"。
示例
输入:s = "We are happy."
输出:"We%20are%20happy."
解答
public class Solution {
// 替换空格
public String replaceSpace(String s) {
// 单线程用StringBuilder更快
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) != ' ') {
sb.append(s.charAt(i));
} else {
sb.append("%20");
}
}
return sb.toString();
}
}
q06_从尾到头打印链表
题目
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
示例
输入:head = [1,3,2]
输出:[2,3,1]
解答
public class Solution {
/**
* 输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)
*/
public int[] reversePrint(ListNode head) {
// LinkedList继承了队列、栈,可以使用栈的push,pop的API
LinkedList<Integer> stack = new LinkedList<>();
while (head != null) {
stack.push(head.val);
head = head.next;
}
return stack.stream().mapToInt(i -> i).toArray();
}
}
q07_重建二叉树
题目
输入某二叉树的前序遍历和中序遍历的结果,请构建该二叉树并返回其根节点。
假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
示例
Input: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
Output: [3,9,20,null,null,15,7]
解答
public class Solution {
/**
* 保存前序序列
*/
private int[] preorder;
/**
* 查找root在中序遍历中的下标
*/
private Map<Integer, Integer> inMap;
/**
* 根据前序和中序遍历结果,重建二叉树
*/
public TreeNode buildTree(int[] preorder, int[] inorder) {
this.preorder = preorder;
this.inMap = new HashMap<>();
// map存中序遍历元素值和索引,提高查找效率
for (int i = 0; i < inorder.length; i++) {
inMap.put(inorder[i], i);
}
return recur(0, 0, inorder.length - 1);
}
/**
* 根据前序和中序,递归生成二叉树
*/
private TreeNode recur(int root, int left, int right) {
// base case:左子树索引越过右子树索引,代表没法形成结点,递归返回null
if (left > right) {
return null;
}
TreeNode node = new TreeNode(preorder[root]);
int i = inMap.get(preorder[root]);
// node的左子树递归:左子树根节点root+1,左子树范围[left,i-1]
node.left = recur(root + 1, left, i - 1);
// node的右子树递归:右子树根节点root+i-left+1,右子树范围[i+1,right]
node.right = recur(root + i - left + 1, i + 1, right);
// 递归回溯,当前node作为上一层的左or右节点
return node;
}
}
q09_两个栈实现队列
题目
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )
示例
输入:
["CQueue","appendTail","deleteHead","deleteHead"]
[[],[3],[],[]]
输出:[null,null,3,-1]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/yong-liang-ge-zhan-shi-xian-dui-lie-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
解答
public class CQueue {
private final LinkedList<Integer> stack1;
private final LinkedList<Integer> stack2;
public CQueue() {
stack1 = new LinkedList<>();
stack2 = new LinkedList<>();
}
public void appendTail(int value) {
// 往队列中添加元素,只用往栈1中添加即可
stack1.push(value);
}
public int deleteHead() {
if (stack1.isEmpty() && stack2.isEmpty()) {
return -1;
}
// 队列出队,栈2为空就要往里面倒入元素保证先进先出
if (stack2.isEmpty()) {
while (!stack1.isEmpty()) {
stack2.push(stack1.pop());
}
}
return stack2.pop();
}
}
q10_I_斐波拉契数列
题目
写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下:
F(0) = 0, F(1) = 1 F(N) = F(N - 1) + F(N - 2), 其中 N > 1. 斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例
输入:n = 5
输出:5
解答
public class Solution {
/**
* 斐波那契数列
* 迭代法
*/
public int fib1(int n) {
if (n < 2) {
return n;
}
int a = 0;
int b = 1;
int sum = 0;
for (int i = 2; i <= n; i++) {
// 剑指Offer的斐波拉契要取模
sum = (a + b) % 1000000007;
a = b;
b = sum;
}
return sum;
}
/**
* 斐波那契数列
* 动态规划法
*/
public int fib2(int n) {
if (n < 2) {
return n;
}
// dp[0]表示第0个斐波那契数,需要返回第n个斐波拉契数,所以需要长度n+1
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = (dp[i - 1] + dp[i - 2]) % 1000000007;
}
return dp[n];
}
}
q10_II_青蛙跳台阶
题目
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例
输入:n = 2
输出:2
解答
public class Solution {
/**
* 青蛙跳台阶:一次可以跳1个台阶或者2个台阶
*/
public int numWays(int n) {
// 青蛙跳台阶,第一个台阶需要1次,第二个台阶需要2次
// 隐式的告诉我们假设有第0个台阶,它也需要第跳1次,才能满足f(n)=f(n-1)+f(n-2)
if (n == 0 || n == 1) {
return 1;
}
// 由于前两项是f(0),f(1)都等于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 sum;
}
}
q11_旋转数组的最小数字
题目
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
给你一个可能存在 重复 元素值的数组 numbers ,它原来是一个升序排列的数组,并按上述情形进行了一次旋转。请返回旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一次旋转,该数组的最小值为1。
示例
输入:[3,4,5,1,2]
输出:1
解答
public class Solution {
/**
* 获取旋转递增数组后的最小值
*/
public int minArray(int[] numbers) {
int left = 0;
int right = numbers.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
// 数组部分旋转,目标值改为数组最右边元素
int target = numbers[right];
if (numbers[mid] < target) {
// 中间值<右边值 = 右边递增
// 最小值在左边,取得到mid
right = mid;
} else if (numbers[mid] > target) {
// 中间值>右边值 = 右边递减
// 最小值在右边,取不到mid
left = mid + 1;
} else {
// 中间值=右边值,无法判断在左边还是右边,但最小值一定靠近左边,缩小mid=缩小target=right--
right--;
}
}
// 返回值是left位置的数
return numbers[left];
}
}
q12_矩阵中的路径
题目
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例
输入:board = [["A","B","C","E"],
["S","F","C","S"],
["A","D","E","E"]], word = "ABCCED"
输出:true
解答
public class Solution {
/**
* 判断矩阵中,是否存在一条路径与word相同,该起点可以是矩阵中任意一个结点,但是访问过的结点不能再访问
*
* @param board 矩阵
* @param word 待匹配的单词
* @return 是否
*/
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;
}
/**
* 从(i,j)结点出发经过index步,是否能匹配word;当前(i,j)如果匹配,index+1
*/
private boolean dfs(char[][] board, char[] word, int i, int j, int index) {
// 递归失败:(i,j)越界或者该字符不匹配
if (i < 0 || i >= board.length || j < 0 || j >= board[0].length || board[i][j] != word[index]) {
return false;
}
// 递归成功:未越界+board[row][col] = word[k]+k遍历到单词末尾
if (index == word.length - 1) {
return true;
}
// 设置一个特殊值,防止重复访问(剪枝)
board[i][j] = '\0';
// 四个方向开始递归,记录结果给res
boolean res = (dfs(board, word, i + 1, j, index + 1) || dfs(board, word, i - 1, j, index + 1)
|| dfs(board, word, i, j + 1, index + 1) || dfs(board, word, i, j - 1, index + 1));
// 回溯:将设置的特殊值还原
board[i][j] = word[index];
return res;
}
}
q13_机器人的运动范围
题目
地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?
示例
输入:m = 2, n = 3, k = 1
输出:3
解答
public class Solution {
public int movingCount(int m, int n, int k) {
boolean[][] visited = new boolean[m][n];
return dfs(visited, 0, 0, k);
}
/**
* 明确概念:机器人从(0,0)出发,行列数位和小于k的格子数量有多少个
*/
private int dfs(boolean[][] visited, int i, int j, int k) {
// 递归结束,返回0: 坐标越界 or 行列坐标数位和超过k or 已经访问过
if (i >= visited.length || j >= visited[0].length || digitSum(i) + digitSum(j) > k || visited[i][j]) {
return 0;
}
// 未访问过,就设置为true,代表访问过
visited[i][j] = true;
// +1:当前访问位置就是一个可以访问单元格的数量,所以加1
// i+1/j+1:机器人从(0,0)出发,所有可达解均在下边或右边,所以只用递归i+1或j+1
return 1 + dfs(visited, i + 1, j, k) + dfs(visited, i, j + 1, k);
}
/**
* 求一个数的所有数字之和,比如35,返回3+5=8
*/
private int digitSum(int num) {
int sum = 0;
while (num != 0) {
// 加上num的个位数,然后num/10
sum += num % 10;
num = num / 10;
}
return sum;
}
}
q14_剪绳子I
题目
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m-1] 。请问 k[0]k[1]…*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
示例
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1
解法
- 解法1
public class Solution {
/**
* 就n划分为m段(m,n均>1),求划分成m段后,各段乘积最大值
* n的取值范围:2 <= n <= 58
*/
public int cuttingRope(int n) {
// n=1,2,3,划为至少2段,乘积最大值为1,1,2
if (n <= 1) {
return n;
}
if (n == 2) {
return 1;
}
if (n == 3) {
return 2;
}
// 动态规划法:i从4开始,dp[i]:把i划分为m段后的,乘积的最大值
int[] dp = new int[n + 1];
// i从4开始,初始化前3段存n的长度≠存前3段乘积最大值
dp[0] = 0;
dp[1] = 1;
dp[2] = 2;
dp[3] = 3;
for (int i = 4; i <= n; i++) {
int max = 0;
// dp[i]=max(dp[j],dp[i-j])
for (int j = 1; j <= (i / 2); j++) {
int temp = dp[j] * dp[i - j];
max = Math.max(max, temp);
}
dp[i] = max;
}
return dp[n];
}
public static void main(String[] args) {
Solution solution = new Solution();
System.out.println(solution.cuttingRope(4));
System.out.println(solution.cuttingRope(5));
System.out.println(solution.cuttingRope(6));
}
}
- 解法2
public class Solution1 {
/**
* 就n划分为m段(m,n均>1),求划分成m段后,各段乘积最大值
* n的取值范围:2 <= n <= 58
*/
public int cuttingRope(int n) {
// 贪心解法
// n=2,m最小为2,乘积1*1=1,返回1
// n=3,m最小为2,乘积1*2=2,返回2
if (n <= 3) {
return n - 1;
}
// 尽可能将绳子n以3等分时,乘积最大
int a = n / 3;
// 求n三等分后最后一段3的余数
int b = n % 3;
// 余数有以下三种情况
if (b == 0) {// 余0,直接返回3^a为最大乘积
// 2 <= n <= 58,乘积结果不会越界
return (int) Math.pow(3, a);
} else if (b == 1) {// 余1,将三等分后倒数第二段中的3+最后一段的1转换为2乘2,因为3*1<2*2
return (int) Math.pow(3, a - 1) * (2 * 2);
}
// 余2,直接返回3^a*(2),最后一段不需要拆分
return (int) Math.pow(3, a) * (2);
}
public static void main(String[] args) {
Solution1 solution = new Solution1();
System.out.println(solution.cuttingRope(4));
System.out.println(solution.cuttingRope(5));
System.out.println(solution.cuttingRope(6));
}
}
q14_剪绳子II
题目
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m - 1] 。请问 k[0]k[1]…*k[m - 1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1
解答
- 最大乘积可能越界,需要学习"循环取余"取模,与上一个方法解法2通用
public class Solution {
/**
* 就n划分为m段(m,n均>1),求划分成m段后,各段乘积最大值
* n的取值范围:2 <= n <= 1000,此题的pow过程会越界
* 答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
*/
public int cuttingRope(int n) {
if (n <= 3) {
return n - 1;
}
// 取a中3个个数-1,因为后面rem*3等,留了一个空位出来
int a = n / 3 - 1;
int b = n % 3;
int x = 3;
// 题目规定的取余数
int p = 1000000007;
// 循环求余法
long rem = reminder(x, a, p);
// 剩下和剪绳子I类似啦,将I中的pow换成rem即可
if (b == 0) {
return (int) (rem * (3) % p);
} else if (b == 1) {
return (int) (rem * (2 * 2) % p);
}
return (int) (rem * (3 * 2) % p);
}
/**
* 循环求余法:(x^a)%p的余数=rem
*/
private long reminder(int x, int a, int p) {
long rem = 1;
for (int i = 1; i <= a; i++) {
rem = (rem * x) % p;
}
return rem;
}
public static void main(String[] args) {
Solution solution = new Solution();
System.out.println(solution.reminder(1000000006, 1, 1000000007));
System.out.println(solution.reminder(1000000006, 2, 1000000007));
// System.out.println(solution.cuttingRope(5));
}
}
第3章_高质量代码
q15_二进制中1的个数
题目
编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 ‘1’ 的个数(也被称为 汉明重量).)。
示例
输入:n = 11 (控制台输入 00000000000000000000000000001011)
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'
解答
public class Solution {
/**
* 整数n的二进制中1的个数
*/
public int hammingWeight(int n) {
int count = 0;
while (n != 0) {
count++;
// n-1 = 将n的二进制最右边的1变成0且如果该1右边有0,把所有1变成1
// n&(n-1)将n的二进制最右边的1变成0,其余不变
n &= n - 1;
}
return count;
}
}
q16_数值的整数次方
题目
实现 pow(x, n) ,即计算 x 的 n 次幂函数(即,xn)。不得使用库函数,同时不需要考虑大数问题。
示例
输入:x = 2.00000, n = 10
输出:1024.00000
解答
public class Solution {
/**
* 求x^n方,考虑n可能是负数、0、正数
* 快速幂法:思考x^9=x^1001=x^(1*1+0*2+0*4+1*8)推导
*/
public double myPow(double x, int n) {
if (x == 0) {
return 0.0;
}
if (x == 1) {
return 1.0;
}
// b指向幂次n,由于n=−2147483648时, n=-n会溢出,所以设b是long类型
long b = n;
// n为负数幂时,x取倒数,b=-b变成正数
if (b < 0) {
x = 1 / x;
b = -b;
}
double res = 1.0;
// 这里b=|b|,循环条件>0即可;如果b能为负数,循环条件是b!=0
while (b > 0) {
// 依次判断指数的二进制最后一位是1or0
// 是1,就需要乘x的倍数;是0,就无须乘
if ((b & 1) == 1) {
res *= x;
} else {
res *= 1;
}
// b有多少位二进制数,x就乘多少次
x *= x;
// b二进制位右移一位,因为此时b=|b|,有符号右移还是无符号右移都行
b >>= 1;
}
return res;
}
}
q17_打印1到最大的n位数
题目
输入数字 n
,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。
示例
输入: n = 1
输出: [1,2,3,4,5,6,7,8,9]
解答
public class Solution {
/**
* 打印从1到n位数的最大值,比如n=3,3位数最大值为999,则打印1,2,3到999
* 方法:自己计算n位数,但是n很大时,n位数可能会越界;考虑用字符串表示大数
*/
public int[] printNumbers(int n) {
// n位数最大值:(int) Math.pow(10, n) - 1
int[] res = new int[(int) Math.pow(10, n) - 1];
// 遍历res的指针
int index = 0;
StringBuilder sb = new StringBuilder();
// 初始化字符串的每一位都为"0"
for (int i = 0; i < n; i++) {
sb.append("0");
}
while (!strInsertOneAndIsPassMax(sb)) {
// 先删除字符串前面多余的0,找到非0的坐标
int sbStart = 0;
while (sbStart < sb.length() && sb.charAt(sbStart) == '0') {
sbStart++;
}
// 非0元素赋值回原数组
res[index++] = Integer.parseInt(sb.substring(sbStart));
}
return res;
}
/**
* 将字符串表示的大数+1,并返回是否产生进位
*/
private boolean strInsertOneAndIsPassMax(StringBuilder sb) {
// 是否越过字符串表示大数的最大值
boolean isPassMax = false;
// 字符串的len-1位=对应大数的最低位,依次开始遍历
for (int i = sb.length() - 1; i >= 0; i--) {
char ch = (char) (sb.charAt(i) + 1);
if (ch > '9') {
// 进位后已经超过9,就将原位置替换为0
// replace:[start,end)取不到end位
sb.replace(i, i + 1, "0");
// 如果i到达0坐标,说明字符串表示的大数最高位发生了进位,返回true
if (i == 0) {
isPassMax = true;
}
} else {
sb.replace(i, i + 1, String.valueOf(ch));
// 没有超过9的自增直接break返回
break;
}
}
return isPassMax;
}
public static void main(String[] args) {
// StringBuilder str = new StringBuilder("001");
// str.replace(0, 1, "2");
// System.out.println(str);
Solution solution = new Solution();
System.out.println(Arrays.toString(solution.printNumbers(2)));
}
}
q18_删除链表结点
题目
给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。
返回删除后的链表的头节点。
示例
输入: head = [4,5,1,9], val = 5
输出: [4,1,9]
解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.
解答
public class Solution {
/**
* 删除链表中指定结点值
*/
public ListNode deleteNode(ListNode head, int val) {
// 空链表
if (head == null) {
return null;
}
// 如果待删除结点是头结点且链表不止一个结点
if (head.val == val) {
return head.next;
}
ListNode cur = head;
while (cur.next != null && cur.next.val != val) {
cur = cur.next;
}
if (cur.next != null) {
cur.next = cur.next.next;
}
return head;
}
}
q19_正则表达式匹配
题目
请实现一个函数用来匹配包含’. ‘和’‘的正则表达式。模式中的字符’.‘表示任意一个字符,而’'表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"abaca"匹配,但与"aa.a"和"ab*a"均不匹配。
示例
输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。
输入:
s = "ab"
p = ".*"
输出: true
解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。
解答
public class Solution {
/**
* 实现匹配.和*的正则表达式;.表示任意一个字符,*表示前面的字符可以出现任意次(包含0次)
* 动态规划法:s=aaa,p=ab*.*,返回true
*/
public boolean isMatch(String s, String p) {
int m = s.length();
int n = p.length();
// dp[i][j]表示s前i个字符,p前j个字符是否匹配
boolean[][] dp = new boolean[m + 1][n + 1];
// 初始化dp[0][0] :两个空串是匹配的
dp[0][0] = true;
// 初始化首行:i=0,将s为看为空串;j=2开始遍历,步长为2
for (int j = 2; j < n + 1; j += 2) {
// 偶数位匹配且p的奇数位全为*
dp[0][j] = dp[0][j - 2] && p.charAt(j - 1) == '*';
}
// 动态转移
for (int i = 1; i < m + 1; i++) {
for (int j = 1; j < n + 1; j++) {
// 当p[j-1]=*时,有三种情况
if (p.charAt(j - 1) == '*') {
// (p[j-2]*)出现0次=p[]
if (dp[i][j - 2]) {
dp[i][j] = true;
// p[j-2]多出现一次能否匹配
} else if (dp[i - 1][j] && s.charAt(i - 1) == p.charAt(j - 2)) {
dp[i][j] = true;
// 万能匹配:.*表示任意一个字符出现任意次
} else if (dp[i - 1][j] && p.charAt(j - 2) == '.') {
dp[i][j] = true;
}
} else {// 当p[j-1]!=*时,有两种情况
// 前面元素之前都匹配 且 当前元素也相同
if (dp[i - 1][j - 1] && s.charAt(i - 1) == p.charAt(j - 1)) {
dp[i][j] = true;
// 前面元素之前都匹配 且 p的当期元素是.
} else if (dp[i - 1][j - 1] && p.charAt(j - 1) == '.') {
dp[i][j] = true;
}
}
}
}
return dp[m][n];
}
}
q20_表示数值的字符串
题目
请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。
数值(按顺序)可以分成以下几个部分:
- 若干空格
- 一个 小数 或者 整数
- (可选)一个 ‘e’ 或 ‘E’ ,后面跟着一个整数
- 若干空格
小数(按顺序)可以分成以下几个部分:
- (可选)一个符号字符(’+’ 或 ‘-’)
- 下述格式之一:
- 至少一位数字,后面跟着一个点 ‘.’
- 至少一位数字,后面跟着一个点 ‘.’ ,后面再跟着至少一位数字一个点 ‘.’
- 一个’.’,后面跟着至少一位数字
整数(按顺序)可以分成以下几个部分:
- (可选)一个符号字符(’+’ 或 ‘-’)至少一位数字部分数值列举如下:
- 至少一位数字
部分数值列举如下:
["+100", "5e2", "-123", "3.1416", "-1E-16", "0123"]
部分非数值列举如下:
["12e", "1a3.14", "1.2.3", "+-5", "12e+5.4"]
示例
输入:s = " .1 "
输出:true
输入:s = "e"
输出:false
解答
有限状态机不太好懂,就看了一个大佬写的普通解法
- 由于判断为true的条件太多了,思考判断false的情况
- 记得清空前后的空格
- 整数:判断数字即可
- E:不能先出现E、不能没有整数
- 正负号:不能先出现正负号、小数点、整数
- 小数点:不能先出现小数点、E
- 最后判断:有数字且index遍历到n
public class Solution {
/**
* 判断一个字符串是否是整数
*/
public boolean isNumber(String s) {
if (s == null) {
return false;
}
s = s.trim();
int n = s.length();
int index = 0;
boolean hasNum = false;
boolean hasE = false;
boolean hasSign = false;
boolean hasDot = false;
while (index < n) {
// 越过数字
while (index < n && s.charAt(index) >= '0' && s.charAt(index) <= '9') {
hasNum = true;
index++;
}
// 不包含特殊符号,只含数字,返回true
if (index == n) {
return true;
}
// 判断特殊符号:正负号、E、小数点
char ch = s.charAt(index);
if (ch == '+' || ch == '-') {
if (hasSign || hasDot || hasNum) {
return false;
}
hasSign = true;
} else if (ch == 'E' || ch == 'e') {
if (hasE || !hasNum) {
return false;
}
hasE = true;
hasSign = false;
hasDot = false;
hasNum = false;
} else if (ch == '.') {
if (hasDot || hasE) {
return false;
}
hasDot = true;
} else {
// 其他情况直接返回失败
return false;
}
index++;
}
return hasNum && index == n;
}
}
q21_调整数组顺序使奇数位于偶数前面
题目
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数在数组的前半部分,所有偶数在数组的后半部分。
实例
输入:nums = [1,2,3,4]
输出:[1,3,2,4]
注:[3,1,2,4] 也是正确的答案之一。
解答
public int[] exchange(int[] nums) {
// 定义两个指针,一个指向数组第一个元素,一个指向最后一个元素
int left = 0;
int right = nums.length - 1;
// 最中间的位置左右指针一旦越过,就又交换回去了,所以是left<right
while (left < right) {
// 循环里面嵌套循环,需要再次判断left<right
// 左指针越过奇数,找偶数
while (left < right && (nums[left] & 1) != 0) {
left++;
}
// 右指针越过偶数,找奇数
while (left < right && (nums[right] & 1) == 0) {
right--;
}
// 交换
if (left < right) {
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
}
return nums;
}
q22_链表倒数第K个节点
题目
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。
示例
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
解答
public class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode fast = head;
ListNode slow = head;
// 快指针先走k-1步(K>=1)
while (k > 0) {
// 快指针每次都要判断是否为null,注意边界问题
if (fast == null) {
return null;
}
fast = fast.next;
k--;
}
// 当快指针走到末尾节点的下一个节点=null时,slow走到倒数第K个节点
while (fast != null) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
}
q23_链表中环的入口结点
题目
判断链表是否有环
解答
public class Solution {
/**
* 判断链表是否有环
*/
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null || head.next.next == null) {
return false;
}
// 初始化:快指针在next.next,慢指针在next上
ListNode fast = head.next.next;
ListNode slow = head.next;
while (fast != slow) {
if (fast.next == null || fast.next.next == null) {
return false;
}
fast = fast.next.next;
slow = slow.next;
}
return true;
}
}
q24_反转链表
题目
定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
示例
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
解答
public class Solution {
/**
* 反转单链表
* 迭代法
*/
public ListNode reverseList1(ListNode head) {
ListNode cur = head;
// pre = 待反转结点的前一个结点,最后返回它
ListNode pre = null;
while (cur != null) {
// 一定是先记录cur后一个节点
ListNode next = cur.next;
// 从cur开始改变指向
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
/**
* 反转单链表
* 递归法
*/
public ListNode reverseList2(ListNode head) {
// base case:参数head为null,或者此时递归的head无下一个结点
if (head == null || head.next == null) {
return head;
}
// 开始递归,head.next为下一轮的head1,当head1.next=null递归停止
// 此时head1位整个链表的末尾节点(反转链表后的头结点),这一轮的head为倒数第二个节点
ListNode ret = reverseList2(head.next);
// head为倒数第二个节点,开始反转最后的两个链表
head.next.next = head;
// 倒数第二个节点next判空,供上一层调用
head.next = null;
return ret;
}
}
q25_合并两个有序链表
题目
输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。
示例
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
解答
public class Solution {
// 和力扣21相同
// 法1:迭代法
public ListNode mergeTwoLists1(ListNode l1, ListNode l2) {
// 设定一个哑结点,方便返回值
ListNode dummyNode = new ListNode(-1);
// cur指针指向每次比较的较小值结点
ListNode cur = dummyNode;
while (l1 != null && l2 != null) {
// 判断较小值,cur指向它
if (l1.val <= l2.val) {
cur.next = l1;
l1 = l1.next;
} else {
cur.next = l2;
l2 = l2.next;
}
// 判断完后移cur
cur = cur.next;
}
// 循环结束,cur指向非空链表头部
cur.next = (l1 == null) ? l2 : l1;
return dummyNode.next;
}
// 法2:递归法
public ListNode mergeTwoLists2(ListNode l1, ListNode l2) {
// 递归结束情况1:某个链表遍历到了末尾
if (l1 == null || l2 == null) {
return l1 == null ? l2 : l1;
}
// 递归判断,每一次都返回最小值结点进递归栈
if (l1.val <= l2.val) {
l1.next = mergeTwoLists2(l1.next, l2);
// 递归结束条件2:返回某个链表的最小值结点
return l1;
} else {
l2.next = mergeTwoLists2(l1, l2.next);
// 递归结束条件2:返回某个链表的最小值结点
return l2;
}
}
}
q26_树的子结构
题目
输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)
示例
输入:A = [3,4,5,1,2], B = [4,1]
输出:true
解答
public class Solution {
/**
* 判断B是否是A的子树
*/
public boolean isSubStructure(TreeNode A, TreeNode B) {
// base case:空节点,返回false
if (A == null || B == null) {
return false;
}
// B是A的子结构||以A为根结点包含B
return isSubStructure(A.left, B) || isSubStructure(A.right, B) || isContainB(A, B);
}
/**
* 判断A中以root为根节点的树,是否能匹配上match
*/
private boolean isContainB(TreeNode root, TreeNode match) {
// base1:match越过叶子,说明匹配完成,返回true
if (match == null) {
return true;
}
// base2:root为null或者两者值不相同,未匹配成功,返回false
if (root == null || root.val != match.val) {
return false;
}
// 子结构:必须是左左对应,右右对应。不能是左右分开对应
return isContainB(root.left, match.left) && isContainB(root.right, match.right);
}
}
第4章_解决面试题的思路
q27_二叉树的镜像
题目
请完成一个函数,输入一个二叉树,该函数输出它的镜像
示例
输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]
解答
public class Solution {
/**
* 输出二叉树的镜像
* 递归法:前序遍历依次进行交换左右子树
*/
public TreeNode mirrorTree(TreeNode root) {
if (root == null) {
return null;
}
// 先遍历到的结点的左右子树发生交换
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
mirrorTree(root.left);
mirrorTree(root.right);
return root;
}
/**
* 输出二叉树的镜像
* 将递归改成栈
*/
public TreeNode mirrorTree1(TreeNode root) {
if (root == null) {
return null;
}
LinkedList<TreeNode> stack = new LinkedList<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode top = stack.pop();
// 交换
TreeNode temp = top.left;
top.left = top.right;
top.right = temp;
if (top.left != null) {
stack.push(top.left);
}
if (top.right != null) {
stack.push(top.right);
}
}
return root;
}
}
q28_对称二叉树
题目
请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
示例
输入:root = [1,2,2,3,4,4,3]
输出:true
解答
public class Solution {
/**
* 判断二叉树是否对称
*/
public boolean isSymmetric(TreeNode root) {
if (root == null) {
return true;
}
return recur(root.left, root.right);
}
private boolean recur(TreeNode left, TreeNode right) {
// base1:如果两者同时越过叶子结点,对称成功,返回true
if (left == null && right == null) {
return true;
}
// base2:左右结点一个到达null,另一个不到达,或者左右结点值不相同,对称失败,返回false
if (left == null || right == null || left.val != right.val) {
return false;
}
// 前序遍历和对称前序遍历序列是否相同:左右遍历 && 右左遍历
return recur(left.left, right.right) && recur(left.right, right.left);
}
}
q29_顺时针打印矩阵
题目
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
示例
输入:matrix = [[1,2,3],
[4,5,6],
[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
解答
public class Solution {
public int[] spiralOrder(int[][] matrix) {
// 输入空列,返回空数组
if (matrix.length == 0) {
return new int[0];
}
// 初始化左上角、右下角坐标
int tR = 0, tC = 0;
int dR = matrix.length - 1, dC = matrix[0].length - 1;
// 结果二维数组大小=原始数组大小
int[] res = new int[matrix.length * matrix[0].length];
// 数组遍历坐标
int index = 0;
while (tR <= dR && tC <= dC) {
index = spiralMatrix(matrix, index, res, tR++, tC++, dR--, dC--);
}
return res;
}
private int spiralMatrix(int[][] matrix, int index, int[] res, int tR, int tC, int dR, int dC) {
// 子矩阵只有一行,就复制列
if (tR == dR) {
for (int i = tC; i <= dC; i++) {
res[index++] = matrix[tR][i];
}
// 子矩阵只有一列,就复制行
} else if (tC == dC) {
for (int i = tR; i <= dR; i++) {
res[index++] = matrix[i][tC];
}
} else {// 一般情况,取不到=号
for (int i = tC; i < dC; i++) {
res[index++] = matrix[tR][i];
}
for (int i = tR; i < dR; i++) {
res[index++] = matrix[i][dC];
}
for (int i = dC; i > tC; i--) {
res[index++] = matrix[dR][i];
}
for (int i = dR; i > tR; i--) {
res[index++] = matrix[i][tC];
}
}
return index;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[][] m = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}, {13, 14, 15, 16}};
int[] res = solution.spiralOrder(m);
// 正确:[1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10]
System.out.println(Arrays.toString(res));
}
}
q30_包含min函数的栈
题目
定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。
示例
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.min(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.min(); --> 返回 -2
解答
public class MinStack {
/**
* 栈1:数据栈,正常压入弹出
*/
private LinkedList<Integer> stack1;
/**
* 栈2:辅助栈,栈顶保持栈1中的最小值
*/
private LinkedList<Integer> stack2;
public MinStack() {
stack1 = new LinkedList<>();
stack2 = new LinkedList<>();
}
public void push(int x) {
// 如果栈2空,或者x比栈2顶还小,就压入栈2
if (stack2.isEmpty() || x <= min()) {
stack2.push(x);
}
// 栈2始终保持与栈1同步压入数据,但是栈2顶保持最小值
if (x > min()) {
stack2.push(min());
}
// 栈1是每次都要压入的
stack1.push(x);
}
public void pop() {
if (stack1.isEmpty()) {
throw new RuntimeException("MinStack is empty");
}
// 出栈是两个辅助栈都要出
stack1.pop();
stack2.pop();
}
public int top() {
if (stack1.isEmpty()) {
throw new RuntimeException("MinStack is empty");
}
return stack1.peek();
}
public int min() {
if (stack2.isEmpty()) {
throw new RuntimeException("MinStack is empty");
}
return stack2.peek();
}
}
q31_栈的压入弹出序列
题目
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。
示例
输入:pushed = [1,2,3,4,5], popped = [4,3,5,1,2]
输出:false
解释:1 不能在 2 之前弹出。
解答
public class Solution {
/**
* 给一个压入数组和一个弹出数组,判断两者是不是一个栈的压入与弹出序列
* pushed=[1,2,3,4,5],popped=[4,5,3,2,1]返回true,[4,5,3,1,2]返回false
*/
public boolean validateStackSequences(int[] pushed, int[] popped) {
// 用辅助栈来模拟压栈操作
LinkedList<Integer> stack = new LinkedList<>();
int i = 0;
for (int num : pushed) {
// 辅助栈先存入压栈元素
stack.push(num);
// 辅助栈顶等于出栈系列元素,辅助栈出栈,出栈序列遍历指针后移
while (!stack.isEmpty() && stack.peek() == popped[i]) {
stack.pop();
i++;
}
}
// 如果全部匹配,辅助为空;否则失败
return stack.isEmpty();
}
}
q32_I_从上到下打印二叉树
题目
从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。
示例
3
/ \
9 20
/ \
15 7
输出:[3,9,20,15,7]
解答
public class Solution {
/**
* 从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。
* 不分行,从上到下打印一个二叉树
*/
public int[] levelOrder(TreeNode root) {
if (root == null) {
return new int[]{};
}
List<Integer> res = new ArrayList<>();
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
// 不分行,就是层次遍历
TreeNode node = queue.poll();
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
res.add(node.val);
}
int[] arr = new int[res.size()];
for (int i = 0; i < arr.length; i++) {
arr[i] = res.get(i);
}
return arr;
// return res.stream().mapToInt(i -> i).toArray();
}
}
q32_II_从上到下打印二叉树
题目
从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。
示例
3
/ \
9 20
/ \
15 7
输出:
[
[3],
[9,20],
[15,7]
]
解答
public class Solution {
/**
* 从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。
* 分行,从上到下打印一个二叉树
*/
public List<List<Integer>> levelOrder(TreeNode root) {
if (root == null) {
return new ArrayList<>();
}
List<List<Integer>> res = new ArrayList<>();
LinkedList<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
// temp存每一行的数据
List<Integer> temp = new ArrayList<>();
// 分行打印,从队列长度往下-1遍历
// 因为queue的长度每次循环内部都在改变,所以不能以size为遍历结束条件
for (int i = queue.size(); i > 0; i--) {
TreeNode node = queue.poll();
if (node.left != null) {
queue.add(node.left);
}
if (node.right != null) {
queue.add(node.right);
}
temp.add(node.val);
}
res.add(temp);
}
return res;
}
}
q32_III_从上到下打印二叉树
题目
请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。
示例
3
/ \
9 20
/ \
15 7
输出:
[
[3],
[20,9],
[15,7]
]
解答
public class Solution {
/**
* 之字型打印二叉树
*/
public List<List<Integer>> levelOrder(TreeNode root) {
if (root == null) {
return new ArrayList<>();
}
List<List<Integer>> res = new ArrayList<>();
Deque<TreeNode> deque = new LinkedList<>();
deque.add(root);
while (!deque.isEmpty()) {
// temp存每一行的数据
List<Integer> temp = new ArrayList<>();
// 打印奇数层:从左到右
for (int i = deque.size(); i > 0; i--) {
// 从头部存的奇数层,就从头部取
TreeNode node = deque.removeFirst();
temp.add(node.val);
// 下一层是偶数层,放尾部
if (node.left != null) {
deque.addLast(node.left);
}
if (node.right != null) {
deque.addLast(node.right);
}
}
res.add(temp);
// 此时如果双端队列为空,说明node没有左右孩子,遍历结束
if (deque.isEmpty()) {
break;
}
// 清空暂存数组
temp = new ArrayList<>();
// 保存偶数层:从右到左
for (int i = deque.size(); i > 0; i--) {
// 从尾部存的偶数层,就从尾部取
TreeNode node = deque.removeLast();
temp.add(node.val);
// 下一层是奇数层,放头部
if (node.right != null) {
deque.addFirst(node.right);
}
if (node.left != null) {
deque.addFirst(node.left);
}
}
res.add(temp);
}
return res;
}
}
q33_二叉搜索树的后序遍历
题目
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true
,否则返回 false
。假设输入的数组的任意两个数字都互不相同。
示例
输入: [1,6,3,2,5]
输出: false
输入: [1,3,2,6,5]
输出: true
解答
public class Solution {
/**
* 输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果
*/
public boolean verifyPostorder(int[] postorder) {
if (postorder == null || postorder.length == 0) {
return true;
}
return recur(postorder, 0, postorder.length - 1);
}
/**
* 二叉搜索树后序遍历:左子树、右子树、根节点;并且左子树 < 根节点;右子树 >根节点
*/
private boolean recur(int[] post, int left, int right) {
// base case:左指针越过右指针,说明子树数量<=1,单个数一定是单个二叉树节点的后序遍历结果,返回true
if (left >= right) {
return true;
}
// i寻找右子树的第一个结点,将数组分为[0...i-1|i...right-1|right],其中根节点=post[right]
int i = left;
while (post[i] < post[right]) {
i++;
}
// j遍历[右子树部分],其中每个值都应该>根节点,如果没有遍历到right不是BST
int j = i;
while (post[j] > post[right]) {
j++;
}
// 后续匹配成功如下:
// 1.遍历指针j是否到达根节点right,判断是否是二叉搜索树
// 2.左孩子区间满足分治
// 3.右孩子区间满足分治
return j == right && recur(post, left, i - 1) && recur(post, i, right - 1);
}
}
q34_二叉树中和为某一值的路径
题目
给你二叉树的根节点 root
和一个整数目标和 targetSum
,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径
示例
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:[[5,4,11,2],[5,8,4,5]]
解答
public class Solution {
private List<List<Integer>> res;
/**
* 保存节点经过的路径
*/
private List<Integer> path;
/**
* 给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
*/
public List<List<Integer>> pathSum(TreeNode root, int target) {
res = new ArrayList<>();
path = new ArrayList<>();
recur(root, target);
return res;
}
private void recur(TreeNode root, int target) {
if (root == null) {
return;
}
// 要算路径和,先访问那个节点就加入该节点值,所以想到先序遍历
// 并且路径和,需要知道之前访问哪些节点,所以保存路径进path
path.add(root.val);
target -= root.val;
// 从根节点到叶子节点:root.left == null && root.right == null
// 目标和:target减为0
if (target == 0 && root.left == null && root.right == null) {
// new ArrayList<>(path)形成新链表放入结果集
res.add(new ArrayList<>(path));
}
if (root.left != null) {
recur(root.left, target);
}
if (root.right != null) {
recur(root.right, target);
}
// 回溯需要移除path末尾元素
path.remove(path.size() - 1);
}
}
q35_复杂链表的复制
题目
请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。
示例
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vQWhFgho-1647909100369)(/users/songshenglin/downloads/markdown/剑指offer-第2版合集.assets/image-20220103193815423.png “image-20220103193815423”)]
解答
public class Solution {
/**
* 请实现 copyRandomList 函数,复制一个复杂链表。
* 在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。
*/
public Node copyRandomList(Node head) {
if (head == null) {
return null;
}
Node cur = head;
Node next;
// 不使用辅助空间,直接在原结点后面连接一个复制结点
// 第一次遍历,使原链表结点后接上一个复制val的结点
while (cur != null) {
next = cur.next;
cur.next = new Node(cur.val);
cur.next.next = next;
cur = next;
}
// 第二次遍历,更新复制链表的random结点
cur = head;
Node copy;
while (cur != null) {
// 先记录cur后面的next、copy结点
next = cur.next.next;
copy = cur.next;
// 复制random指针时先判null
copy.random = (cur.random != null) ? cur.random.next : null;
cur = next;
}
// 第三次遍历,使原链表和复制链表分离
cur = head;
// 记录复制链表的返回值
Node copyHead = cur.next;
while (cur != null) {
next = cur.next.next;
copy = cur.next;
// cur连接回原来的next
cur.next = next;
// copy结点连接时先判next是否null
copy.next = (next != null) ? next.next : null;
cur = next;
}
return copyHead;
}
}
q36_二叉搜索树和双向链表
题目
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。
示例
输入:root = [4,2,5,1,3]
输出:[1,2,3,4,5]
解答
public class Solution {
/**
* pre记录dfs中root的前驱=root.left的位置
*/
private Node pre;
/**
* head记录返回双向链表的头结点
*/
private Node head;
/**
* 将一个 二叉搜索树 就地转化为一个 已排序的双向循环链表 。
*/
public Node treeToDoublyList(Node root) {
if (root == null) {
return null;
}
// 中序遍历形成双向链表
dfs(root);
// 更新首尾指针,形成循环链表
head.left = pre;
pre.right = head;
return head;
}
/**
* 中序遍历二叉树,将二叉树分为:左孩子,根节点,右孩子
*/
private void dfs(Node node) {
if (node == null) {
return;
}
dfs(node.left);
// 二叉搜索树中序遍历:保证从小到大
// 更新pre的后继是否指向node
if (pre == null) {
head = node;
} else {// pre!=null,说明当前node有pre,更新pre的后继
pre.right = node;
}
// 更新node的前驱是否指向pre
node.left = pre;
// pre完成迭代
pre = node;
dfs(node.right);
}
}
q37_序列化与反序列化二叉树
题目
请实现两个函数,分别用来序列化和反序列化二叉树。
你需要设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
提示:输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。
示例
输入:root = [1,2,3,null,null,4,5]
输出:[1,2,3,null,null,4,5]
解答
public class Solution {
/**
* 序列化二叉树,可以按照力扣的格式:[1,2,null,4,5]
*/
public String serialize(TreeNode root) {
if (root == null) {
return "[]";
}
StringBuilder sb = new StringBuilder("[");
LinkedList<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
// node值不为null
if (node != null) {
sb.append(node.val).append(",");
// 队列中每次放入node的左右孩子即可,不用考虑左右孩子是否为null
queue.add(node.left);
queue.add(node.right);
} else {
// node值为null,结果字符串加null,记得删除最后一个逗号即可
sb.append("null,");
}
}
// 删除最后一个逗号
sb.deleteCharAt(sb.length() - 1);
sb.append("]");
return sb.toString();
}
/**
* 反序列化二叉树
*/
public TreeNode deserialize(String data) {
if ("[]".equals(data)) {
return null;
}
// 去掉头尾的"[]",并根据逗号分离成字符串数组
String[] split = data.substring(1, data.length() - 1).split(",");
// 生产根节点
TreeNode root = new TreeNode(Integer.parseInt(split[0]));
// 队列维持左右孩子
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
// 根节点已生成,遍历指针从split的下标1开始
int index = 1;
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
if (!"null".equals(split[index])) {
node.left = new TreeNode(Integer.parseInt(split[index]));
queue.add(node.left);
}
index++;
if (!"null".equals(split[index])) {
node.right = new TreeNode(Integer.parseInt(split[index]));
queue.add(node.right);
}
index++;
}
return root;
}
public static void main(String[] args) {
Solution solution = new Solution();
TreeNode n1 = new TreeNode(1);
TreeNode n2 = new TreeNode(2);
TreeNode n3 = new TreeNode(3);
n1.left = n2;
n1.right = n3;
String res = solution.serialize(n1);
System.out.println(res);
}
}
q38_字符串的排列
题目
输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
示例
输入:s = "abc"
输出:["abc","acb","bac","bca","cab","cba"]
解答
public class Solution {
private char[] cs;
private List<String> res;
/**
* 求一个字符串所有字符的排列
* 比如:s="aab",返回["aba","aab","baa"]
* 判断不加set,会返回["aab","aba","aab","aba","baa","baa"]
*/
public String[] permutation(String s) {
if (s == null || s.length() == 0) {
return new String[]{};
}
res = new ArrayList<>();
cs = s.toCharArray();
dfs(0);
// 结果列表转换为字符串数组list.toArray(目标数组构造器)
return res.toArray(new String[0]);
}
/**
* dfs:将cs中pos位置固定,然后开始递归后面数组组成排列
*/
private void dfs(int pos) {
// base case:固定位置来到最后一个字符
if (pos == cs.length - 1) {
res.add(String.valueOf(cs));
return;
}
// 当字符串存在重复字符时,排列中也会出现重复排列
// 为排除重复方案,固定某个字符时,保证每种字符只在此位置出现一次=剪枝
Set<Character> set = new HashSet<>();
// 从固定位置pos往后开始递归其他分支
for (int i = pos; i < cs.length; i++) {
// 先判断i位置上之前递归是否出现过
if (set.contains(cs[i])) {
continue;
}
set.add(cs[i]);
// 交换,将cs[i]固定在pos位置
swap(i, pos);
// 递归固定后续index+1的位置
dfs(pos + 1);
// 回溯:交换回原数组顺序
swap(i, pos);
}
}
private void swap(int a, int b) {
char temp = cs[a];
cs[a] = cs[b];
cs[b] = temp;
}
public static void main(String[] args) {
Solution solution = new Solution();
String s = "aab";
System.out.println(Arrays.toString(solution.permutation(s)));
}
}
第5章_优化时间和空间效率
q39_数组中出现次数超一半的数
题目
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例
输入: [1, 2, 3, 2, 2, 2, 5, 4, 2]
输出: 2
解答
public class Solution {
/**
* 出现次数超过一半的数,一定不会被抵消掉,最后留下的一定是它
* 摩尔投票法
*/
public int majorityElement(int[] nums) {
// 摩尔投票法,众数一定不会被抵消掉
int x = 0, votes = 0;
for (int num : nums) {
// 投票数为0时,假设当前数为众数,x==它
if (votes == 0) {
x = num;
}
// 当前数=众数,票数+1,否则-1
votes += x == num ? 1 : -1;
}
// nums不一定有众数,需要判断一下x是否次数超过数组一半
int count = 0;
for (int num : nums) {
if (x == num) {
count++;
}
}
return count > nums.length / 2 ? x : 0;
}
public static void main(String[] args) {
int[] arr = {1, 2, 3, 2, 2, 2, 5, 4, 2};
Solution solution = new Solution();
System.out.println(solution.majorityElement(arr));
}
}
q40_最小的k个数
题目
输入整数数组 arr
,找出其中最小的 k
个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
示例
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]
解答
- 快速排序法
public class Solution {
/**
* 最小的k个数
* 快速排序法:只用返回坐标k左边的数即可
*/
public int[] getLeastNumbers(int[] arr, int k) {
if (k < 0 || k > arr.length) {
return new int[]{};
}
return quickSortK(arr, 0, arr.length - 1, k);
}
private int[] quickSortK(int[] arr, int L, int R, int k) {
int i = L;
int j = R;
// while循环,将arr划分为[l,i]<arr[l],arr[l],arr[i+1,r]>arr[l]三个区域
while (i < j) {
// 注意:arr[L]作为基准,先移动j后移动i
// 原因:arr[L]作为基准,必须先找到<区域的最后一个数位置,才能交换基准与该位置
while (i < j && arr[j] >= arr[L]) {
// 从后往前找到第一个arr[j]<arr[l]=从前往后找最后一个<区域的数
j--;
}
while (i < j && arr[i] <= arr[L]) {
i++;
}
swap(arr, i, j);
}
// 交换基准arr[l]和arr[i],保证划分区间
swap(arr, i, L);
// 若i>k ,说明小于k个数的边界在左边,移动右边界
if (i > k) {
quickSortK(arr, L, i - 1, k);
} else if (i < k) {
// i<k,移动左边界
quickSortK(arr, i + 1, R, k);
}
// 若i==k,前k个数就是k下标前面的所有数,返回这个状态是无序的且修改了原数组arr
return Arrays.copyOf(arr, k);
}
private void swap(int[] arr, int i, int j) {
if (i == j) {
// 防止i = j = len,越过了len-1长度无法交换
return;
}
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] arr = {4, 5, 1, 6, 2, 7, 3, 8};
System.out.println(Arrays.toString(solution.getLeastNumbers(arr, 4)));
}
}
- 堆排序:使用库函数
public class Solution1 {
/**
* 最小的k个数
* 最小堆:使用库函数
*/
public int[] getLeastNumbers(int[] arr, int k) {
if (k < 0 || k > arr.length) {
return new int[]{};
}
Queue<Integer> minHeap = new PriorityQueue<>();
for (int num : arr) {
minHeap.offer(num);
}
int[] res = new int[k];
for (int i = 0; i < k; i++) {
res[i] = minHeap.poll();
}
return res;
}
public static void main(String[] args) {
Solution1 solution1 = new Solution1();
int[] arr = {4, 5, 1, 6, 2, 7, 3, 8};
System.out.println(Arrays.toString(solution1.getLeastNumbers(arr, 4)));
}
}
- 堆排序:手写堆排序
public class Solution2 {
/**
* 最小的k个数
* 最小堆:手写堆排序
*/
public int[] getLeastNumbers(int[] arr, int k) {
if (k < 0 || k > arr.length) {
return new int[]{};
}
// 手写堆排序,将arr进行从小到大排序
heapSort(arr);
// 复制前k个数返回
int[] res = new int[k];
System.arraycopy(arr, 0, res, 0, k);
return res;
}
/**
* 实现小根堆排序
*/
private void heapSort(int[] arr) {
if (arr.length < 2) {
return;
}
// 小根堆,堆顶保持最大值
for (int parent = (arr.length - 2) / 2; parent >= 0; parent--) {
heapify(arr, parent, arr.length);
}
// 将堆顶最大值依次放回数组末尾
for (int i = arr.length - 1; i >= 0; i--) {
swap(arr, 0, i);
heapify(arr, 0, i);
}
}
private void heapify(int[] arr, int parent, int n) {
while (2 * parent + 1 < n) {
int left = 2 * parent + 1;
if (left + 1 < n && arr[left + 1] > arr[left]) {
left++;
}
if (arr[parent] >= arr[left]) {
break;
}
swap(arr, parent, left);
parent = left;
}
}
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
Solution2 solution1 = new Solution2();
int[] arr = {4, 5, 1, 6, 2, 7, 3, 8};
System.out.println(Arrays.toString(solution1.getLeastNumbers(arr, 4)));
}
}
q41_数据流的中位数
题目
得到一个数据流中的中位数.
[2,3,4] 的中位数是 3
[2,3] 的中位数是 (2 + 3) / 2 = 2.5
示例
输入:
["MedianFinder","addNum","addNum","findMedian","addNum","findMedian"]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]
解答
public class MedianFinder {
/**
* 大根堆存较小的N/2个数
*/
private Queue<Integer> maxHeap;
/**
* 小根堆存较大的N/2个数
*/
private Queue<Integer> minHeap;
/**
* 得到一个数据流中的中位数
* [2,3,4]中位数是3
* [2,3]中位数是2.5,返回double类型
*/
public MedianFinder() {
this.maxHeap = new PriorityQueue<>((a, b) -> b - a);
this.minHeap = new PriorityQueue<>();
}
public void addNum(int num) {
if (maxHeap.isEmpty() || num <= maxHeap.peek()) {
maxHeap.add(num);
} else {
minHeap.add(num);
}
// 每次添加元素,都要发生调整操作
modifyHeap();
}
public double findMedian() {
if (maxHeap.isEmpty()) {
return -1.0;
}
if (maxHeap.size() == minHeap.size()) {
return (maxHeap.peek() + minHeap.peek()) / 2.00000;
} else {
return maxHeap.size() > minHeap.size() ? maxHeap.peek() : minHeap.peek();
}
}
private void modifyHeap() {
// 差距达到2才会发生调整
if (maxHeap.size() == minHeap.size() + 2) {
minHeap.add(maxHeap.poll());
}
if (minHeap.size() == maxHeap.size() + 2) {
maxHeap.add(minHeap.poll());
}
}
public static void main(String[] args) {
MedianFinder solution = new MedianFinder();
solution.addNum(2);
solution.addNum(3);
System.out.println(solution.findMedian());
solution.addNum(4);
System.out.println(solution.findMedian());
}
}
q42_连续子数组最大值
题目
输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
要求时间复杂度为O(n)。
示例
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
解答
public class Solution {
/**
* 求一个数组中,数组中一个或多个连续整数组组成一个子数组,求所有子数组和的最大值
* 数组长度>=1
* 动态规划法
*/
public int maxSubArray(int[] nums) {
if (nums.length == 1) {
return nums[0];
}
// 题目:连续整数,提示我们用动态规划
// 初始化dp:dp[i]表示nums中前i个元素的最大和
int[] dp = new int[nums.length];
// 根据动态转移dp[i]与dp[i-1]有关,思考初始化dp[0]
dp[0] = nums[0];
// 初始化max=nums[0],千万别是0或者整型最小值
int max = nums[0];
for (int i = 1; i < nums.length; i++) {
// 前i-1元素的最大和<=0,产生负影响,舍弃
if (dp[i - 1] <= 0) {
dp[i] = nums[i];
} else {// 前i-1元素的最大和>0,产生正影响
dp[i] = dp[i - 1] + nums[i];
}
// 更新最大值
max = Math.max(max, dp[i]);
}
return max;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] arr = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
System.out.println(solution.maxSubArray(arr));
}
}
q43_1到n中1出现的次数
题目
输入一个整数 n ,求1~n这n个整数的十进制表示中1出现的次数。
例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。
示例
输入:n = 12
输出:5
解答
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CL6VpLpM-1647909100371)(/users/songshenglin/downloads/markdown/剑指offer-第2版合集.assets/image-20220103210257992.png “image-20220103210257992”)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xz9cCu3r-1647909100372)(/users/songshenglin/downloads/markdown/剑指offer-第2版合集.assets/image-20220103210306455.png “image-20220103210306455”)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bAP6Ll6L-1647909100372)(/users/songshenglin/downloads/markdown/剑指offer-第2版合集.assets/image-20220103210319468.png “image-20220103210319468”)]
public class Solution {
/**
* 输入一个整数 n ,求1~n这n个整数的十进制表示中1出现的次数。
* 例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。
*/
public int countDigitOne(int n) {
// 初始化:low,cur,high,digit
int low = 0;
int cur = n % 10;
int high = n / 10;
// 第几位:个位、十位、百位等等,初始化为10^0=1
int digit = 1;
int res = 0;
// high和cur同时为0,越过了最后一个高位,循环结束
while (high != 0 || cur != 0) {
// cur有三种情况:0,1,>1,自己用纸推出这三种表达式
if (cur == 0) {
res += high * digit;
} else if (cur == 1) {
res += high * digit + low + 1;
} else if (cur > 1) {
res += (high + 1) * digit;
}
// 更新low,cur,high,digit
low += cur * digit;
cur = high % 10;
high = high / 10;
digit *= 10;
}
return res;
}
}
q44_数字序列中的某一位数字
题目
数字以0123456789101112131415…的格式序列化到一个字符序列中。在这个序列中,第5位(从下标0开始计数)是5,第13位是1,第19位是4,等等。
请写一个函数,求任意第n位对应的数字。
示例
输入:n = 3
输出:3
输入:n = 11
输出:0
解答
public class Solution {
/**
* 数字序列中某一位的数字
*/
public int findNthDigit(int n) {
// 明确以下概念:数字num,数位digit,该数位下起始位置start,数位总数count
// 条件给的序列:0123456789101112...,每一位记为数位
// num:我们常见的数字10,11,12,称为数字num
// digit:每个数字的所属的位数,比如数字10是一个两位数,记为digit=2
// start:每位digit表示的位数起始值(即1,10,100),记为start
// count:截止到返回值的数位总数,规律总结为=9×start×digit,显然初始化为9
// 起始数字为1,数位为1,数位总量为9
long start = 1;
int digit = 1;
long count = 9;
// 根据count = 9×start×digit,算出n所在的start和digit
// 1.确定所求数位的所在数字的位数
while (n > count) {
n -= count;
digit++;
start *= 10;
count = 9 * start * digit;
}
// 2.确定所求数位所在的数字num
long num = start + (n - 1) / digit;
// 3.确定所求数位在num的哪个数位
int index = (n - 1) % digit;
// 返回值规定为int,定位到char再-'0'即可
return String.valueOf(num).charAt(index) - '0';
}
public static void main(String[] args) {
Solution solution = new Solution();
int n = 11;
System.out.println(solution.findNthDigit(n));
}
}
q45_把数组排成最小的数
题目
输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。
示例
输入: [3,30,34,5,9]
输出: "3033459"
解答
public class Solution {
/**
* 把数组排成最小的数
*/
public String minNumber(int[] nums) {
if (nums == null || nums.length == 0) {
return "";
}
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
int x = nums[i];
int y = nums[j];
// 由于xy、yx相加可能存在int移除问题
// 我们将其转换为字符串,再用Long接受就没有int溢出问题
long num1 = Long.parseLong(x + "" + y);
long num2 = Long.parseLong(y + "" + x);
// 12 < 21,则不用改
// 21 > 12,说明要将y放在前面
if (num1 > num2) {
nums[i] = y;
nums[j] = x;
}
}
}
StringBuilder sb = new StringBuilder();
for (int num : nums) {
sb.append(num);
}
return sb.toString();
}
}
q46_把数字翻译成字符
题目
给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
示例
输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi"和"mzi"
解答
public class Solution {
/**
* 给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。
* 一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
* 动态规划
*/
public int translateNum(int num) {
if (num == 0) {
return 1;
}
// 将num转换为字符串
String str = String.valueOf(num);
int n = str.length();
// dp[i]代表以xi为结尾的翻译数量,dp长度为n+1
int[] dp = new int[n + 1];
// 初始化dp[0]=dp[1]=1,表示“无数字”和"str第一位的翻译数量"
// dp[0]=1怎么推?因为dp[1]=1,dp[2]要么=1,要么=2,当dp[2]=2时,dp[0]必为1
dp[0] = dp[1] = 1;
// 从第三位数开始遍历dp
for (int i = 2; i <= n; i++) {
// 原串str中拆分xi-1+xi组成的字符串subStr,注意下标变换
String subStr = str.substring(i - 2, i);
// 如果subStr可以整体翻译,说明subStr必须满足两位数10-25的要求,才能整体翻译
if (subStr.compareTo("10") >= 0 && subStr.compareTo("25") <= 0) {
dp[i] = dp[i - 2] + dp[i - 1];
} else {// 否则sub不能被翻译
dp[i] = dp[i - 1];
}
}
return dp[n];
}
public static void main(String[] args) {
Solution solution = new Solution();
int n = 12258;
System.out.println(solution.translateNum(n));
}
}
q47_礼物的最大值
题目
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
示例
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物
解答
public class Solution {
/**
* 在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。
* 你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。
* 给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
*/
public int maxValue(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
// dp[i][j]表示到达grid[i][j]时的礼物最大值
int[][] dp = new int[m][n];
// 状态转移:dp[i][j]=Max{dp[i-1][j],dp[i][j-1]}+grid[i][j]
// 根据状态转移,初始化dp[0][0]、第一行、第一列
dp[0][0] = grid[0][0];
for (int i = 1; i < n; i++) {
dp[0][i] = dp[0][i - 1] + grid[0][i];
}
for (int i = 1; i < m; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
// 常规情况
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
// 左边或上边的最大值+grid[i][j]
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
return dp[m - 1][n - 1];
}
}
q48_最长无重复子串长度
题目
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
示例
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
解答
public class Solution {
/**
* 请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
*/
public int lengthOfLongestSubstring(String s) {
if (s == null || "".equals(s)) {
return 0;
}
char[] cs = s.toCharArray();
// map<该字符,该字符上次出现的位置下标>,初始化value全为-1
Map<Character, Integer> map = new HashMap<>();
for (char c : cs) {
map.put(c, -1);
}
// pre:str[i-1]为结尾的,无重复子串的起始位置的前一个位置,初试值为-1
// 为什么是前一个位置,因为便于根据两个坐标计算长度
int pre = -1;
// 记录每一个字符结尾下的,无重复子串的最大长度
int res = 0;
for (int i = 0; i < cs.length; i++) {
// 假设map.get(chars[i])=i‘,如果i‘在pre右边,说明[i’+1,i]一定是chars[i]结尾的无重复子串
// pre和i‘谁在右边,更新谁=作为下一轮chars[i-1]的起始位置前一位
if (map.get(cs[i]) >= pre) {
pre = map.get(cs[i]);
}
// 如果i‘在pre左边,说明[pre+1,i]一定是chars[i]结尾的无重复子串
res = Math.max(res, i - pre);
// 更新map,记录元素与它最近出现的位置
map.put(cs[i], i);
}
return res;
}
public static void main(String[] args) {
Solution solution = new Solution();
String str = "aabcd";
int len1 = solution.lengthOfLongestSubstring(str);
System.out.println(len1);
}
}
q49_丑数
题目
我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。
示例
输入: n = 10
输出: 12
解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。
解答
public class Solution {
/**
* 我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。
*/
public int nthUglyNumber(int n) {
if (n == 0) {
return 0;
}
// 初始化:三个指针,a指向2倍数,b指向3倍数,c指向5倍数
int a = 0, b = 0, c = 0;
// dp[i]表示第i+1个丑数
int[] dp = new int[n];
// 初始化dp:dp[0]=1,第一个丑数是1
dp[0] = 1;
for (int i = 1; i < n; i++) {
// 公式:dp[i]=min{dp[a]*2,dp[b]*3,dp[c]*5}
int n1 = dp[a] * 2;
int n2 = dp[b] * 3;
int n3 = dp[c] * 5;
dp[i] = Math.min(Math.min(n1, n2), n3);
// 接下来移动指针,哪个指针被选中作为该论的丑数,谁就后移一位
if (dp[i] == n1) {
a++;
}
if (dp[i] == n2) {
b++;
}
if (dp[i] == n3) {
c++;
}
}
return dp[n - 1];
}
}
q50_第一个只出现一次的字符
题目
在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母
示例
输入:s = "abaccdeff"
输出:'b'
解答
public class Solution {
/**
* 在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母
*/
public char firstUniqChar(String s) {
if (s == null || "".equals(s)) {
return ' ';
}
// 尽量不使用特殊的数据结构,用数组存次数比较快
int[] map = new int[256];
for (int i = 0; i < s.length(); i++) {
map[s.charAt(i)]++;
}
for (int i = 0; i < s.length(); i++) {
if (map[s.charAt(i)] == 1) {
return s.charAt(i);
}
}
return ' ';
}
}
q51_数组中的逆序对
题目
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
示例
输入: [7,5,6,4]
输出: 5
解答
public class Solution {
private int res;
/**
* 在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
* 归并排序法,原理是利用nums[i]>nums[j],那么[i,mid]中都是逆序对个数
*/
public int reversePairs(int[] nums) {
int[] temp = new int[nums.length];
res = 0;
mergeSort(nums, 0, nums.length - 1, temp);
return res;
}
private void mergeSort(int[] nums, int l, int r, int[] temp) {
if (l >= r) {
return;
}
int mid = l + (r - l) / 2;
mergeSort(nums, l, mid, temp);
mergeSort(nums, mid + 1, r, temp);
if (nums[mid] > nums[mid + 1]) {
merge(nums, l, mid, r, temp);
}
}
private void merge(int[] nums, int l, int mid, int r, int[] temp) {
System.arraycopy(nums, l, temp, l, r - l + 1);
int p = l, q = mid + 1;
for (int i = l; i <= r; i++) {
if (p > mid) {
nums[i] = temp[q++];
} else if (q > r) {
nums[i] = temp[p++];
} else if (temp[p] <= temp[q]) {
// <=区域不会形成逆序对,所以和归并排序过程一样
nums[i] = temp[p++];
} else {
// p到mid中间元素的个数,与q构成逆序对
// 注意:力扣题不要求% 1000000007
res += mid - p + 1;
nums[i] = temp[q++];
}
}
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] arr = {1, 3, 2};
System.out.println(solution.reversePairs(arr));
}
}
归并排序
public class MergeSort {
private MergeSort() {
}
/**
* 学习完逆序对问题,学习归并排序
*/
public static void mergeSort(int[] arr) {
// 临时数组一开始就创建,传递到merge将arr复制给temp数组
int[] temp = new int[arr.length];
mergeSort(arr, 0, arr.length - 1, temp);
}
private static void mergeSort(int[] arr, int l, int r, int[] temp) {
if (l >= r) {
return;
}
// 先递归,划分区域
int mid = l + (r - l) / 2;
mergeSort(arr, l, mid, temp);
mergeSort(arr, mid + 1, r, temp);
// 再归并,mid>前一个数才归并
if (arr[mid] > arr[mid + 1]) {
merge(arr, l, mid, r, temp);
}
}
private static void merge(int[] arr, int l, int mid, int r, int[] temp) {
System.arraycopy(arr, l, temp, l, r - l + 1);
int p = l, q = mid + 1;
for (int i = l; i <= r; i++) {
// 先判断两个指针越界的情况
if (p > mid) {
arr[i] = temp[q++];
} else if (q > r) {
arr[i] = temp[p++];
} else if (temp[p] <= temp[q]) {
arr[i] = temp[p++];
} else {
arr[i] = temp[q++];
}
}
}
}
q52_两个链表的第一个相交结点
题目
输入两个链表,找出它们的第一个公共节点。
示例
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cnybd5m5-1647909100372)(/users/songshenglin/downloads/markdown/剑指offer-第2版合集.assets/image-20220103222705676.png “image-20220103222705676”)]
解答
public class Solution {
/**
* 输入两个链表,找出它们的第一个公共节点。
* 备注:力扣本题是无环链表的相交问题
*/
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) {
return null;
}
ListNode cur1 = headA;
ListNode cur2 = headB;
// len记录两个链表长度差值
int len = 0;
while (cur1.next != null) {
len++;
cur1 = cur1.next;
}
while (cur2.next != null) {
len--;
cur2 = cur2.next;
}
// 如果两个遍历指针遍历到末尾不相等,两个链表必不交
if (cur1 != cur2) {
return null;
}
// cur1指向较长的链表
cur1 = (len > 0 ? headA : headB);
// cur2指向较短的链表
cur2 = (cur1 == headA ? headB : headA);
// 长度差可能为负,len取绝对值
len = Math.abs(len);
// 由于cur1指向长的,先走len步
while (len > 0) {
cur1 = cur1.next;
len--;
}
// cur1和cur2再一起走
while (cur1 != cur2) {
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
}
}
第6章_面试中的各项能力
q53_I_在排序数组中查找数字
题目
统计一个数字在排序数组中出现的次数。
示例
输入: nums = [5,7,7,8,8,10], target = 8
输出: 2
解答
public class Solution {
/**
* 统计一个数字在排序数组中出现的次数。
* 数组已排序,优化二分法,可以查找出第一个t和最后一个t
*/
public int search(int[] nums, int target) {
// 问题:[5,7,7,8,8,10],t=8的出现的次数
// 8的次数可以为10的下标-第一个8出现的下标
// getRightMargin(nums, 8)返回大于8的第一个下标,就是10的下标
// getRightMargin(nums, 7)返回大于7的第一个下标,就是第一个8的下标
return getNextTargetFirstIndex(nums, target) - getNextTargetFirstIndex(nums, target - 1);
}
/**
* 修改二分法:让它返回>target的第一个数的下标
*/
private int getNextTargetFirstIndex(int[] arr, int target) {
int left = 0;
int right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
// 经验:二分法中<=就是返回t的右边界
if (arr[mid] <= target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
// left <= right打破的时候,left来到>target的第一个数的下标
return left;
}
}
q53_II_缺失的数字
题目
一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
示例
输入: [0,1,2,3,4,5,6,7,9]
输出: 8
解答
public class Solution {
/**
* 一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。
* 在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
*/
public int missingNumber(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
// 中间元素值与下标相等,下一轮查找在右边
if (nums[mid] == mid) {
left = mid + 1;
} else {
// 中间元素与下标不相等
// mid==0 或 中间前一个元素与下标相等,mid就是缺失数字
if (mid == 0 || nums[mid - 1] == mid - 1) {
return mid;
}
// 中间元素前一个元素与下标不相等,下一轮遍历左边
right = mid - 1;
}
}
// left来到数组长度位置,说明长度n-1全部匹配,[0,1,2],返回数组长度
if (left == nums.length) {
return nums.length;
}
// 无效的输入,返回-1
return -1;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] arr = {0, 1, 2, 3, 4, 5, 6, 7, 9};
System.out.println(solution.missingNumber(arr));
}
}
q54_二叉搜索树的第K个节点
题目
给定一棵二叉搜索树,请找出其中第 k
大的节点的值。
示例
输入: root = [3,1,4,null,2], k = 1
3
/ \
1 4
\
2
输出: 4
解答
public class Solution {
/**
* 给定一棵二叉搜索树,请找出其中第 k 大的节点的值。
*/
public int kthLargest(TreeNode root, int k) {
if (k < 0) {
return -1;
}
// 二叉搜索树的中序遍历,用链表存数值,返回第size-k个元素就是第k大的元素
List<Integer> list = new ArrayList<>();
inOrder(root, list);
// 第1大的数->排序后第size-1个数
// 第k大的数->排序后第size-k个数
return list.get(list.size() - k);
}
/**
* 二叉搜索树,用中序遍历,保存二叉树元素进list
*/
private void inOrder(TreeNode root, List<Integer> list) {
if (root == null) {
return;
}
inOrder(root.left, list);
list.add(root.val);
inOrder(root.right, list);
}
}
q55_I_二叉树的深度
题目
输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度
示例
3
/ \
9 20
/ \
15 7
返回:3
解答
public class Solution {
/**
* 输入一棵二叉树的根节点,求该树的深度。
* 从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度
*/
public int maxDepth(TreeNode root) {
// 越过叶子节点,返回深度为0
if (root == null) {
return 0;
}
// 根节点深度为1,如果只有左子树,深度为左子树+1;如果只有右子树,深度为右子树+1
// 如果既有左子树、右子树,深度为两者最大值+1
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
}
q55_II_平衡二叉树
题目
输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。
示例
3
/ \
9 20
/ \
15 7
返回:false
解答
- 高度差
public class Solution {
public boolean isBalanced(TreeNode root) {
return process(root) != -1;
}
/**
* 平衡二叉树:左右子树高度差必<=1
*/
private int process(TreeNode head) {
if (head == null) {
return 0;
}
// 后序遍历获取左右子树高度信息
int leftHeight = process(head.left);
if (leftHeight == -1) {
return -1;
}
int rightHeight = process(head.right);
if (rightHeight == -1) {
return -1;
}
// 左右子树高度差<=1,返回真实高度;左右子树高度差>1,返回-1
return Math.abs(leftHeight - rightHeight) <= 1 ? Math.max(leftHeight, rightHeight) + 1 : -1;
}
}
- 二叉树递归套路
public class Solution1 {
/**
* 树形递归,需要判断返回值类需要什么信息
* 平衡二叉树:需要平衡和高度两种信息
*/
class ReturnType {
boolean isBalanced;
int height;
ReturnType(boolean isBalanced, int height) {
this.isBalanced = isBalanced;
this.height = height;
}
}
public boolean isBalanced(TreeNode root) {
return process(root).isBalanced;
}
private ReturnType process(TreeNode head) {
// 单个节点,是平衡,深度为0
if (head == null) {
return new ReturnType(true, 0);
}
// 后序遍历,获取左右子树的情况
ReturnType leftType = process(head.left);
ReturnType rightType = process(head.right);
// 整体平衡:左子树平衡、右子树平衡、高度差<=1
boolean isBalanced = leftType.isBalanced && rightType.isBalanced && Math.abs(leftType.height - rightType.height) <= 1;
// 整体高度:左右子树最大高度+1
int height = Math.max(leftType.height, rightType.height) + 1;
return new ReturnType(isBalanced, height);
}
}
q56_I_数组中数字出现的次数I
题目
一个整型数组 nums
里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
示例
输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]
解答
public class Solution {
/**
* 一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。
* 要求时间复杂度是O(n),空间复杂度是O(1)。
*/
public int[] singleNumbers(int[] nums) {
int a = 0, b = 0;
// 0与任何数异或都为任何数本身
// 第一次遍历,a存那两个数字异或的结果,因为两数不相同,a≠0
for (int num : nums) {
a ^= num;
}
// 找到a中最右边的1,二次遍历划分数组
int rightOne = a & (~a + 1);
// 重置a为0,便于下面^操作
a = 0;
// 第二次遍历,利用不相同的二进制位,将原数组分为独立含数1和数2的两个部分
for (int num : nums) {
// 这里只能用!=0或者==0来判断属于哪一个阵营
// 因为二进制0和十进制0都是通用,000...0==0
if ((rightOne & num) == 0) {
b ^= num;
} else {
a ^= num;
}
}
return new int[]{a, b};
}
/**
* 先学习:一个数组中除一个数字外,其余数字出现了两次,找出这个数字
*/
public int oneNumbers(int[] nums) {
// 异或两大特性:
// 0与任何数异或都为任何数本身
// 任何一个数异或它自己都为0
int res = 0;
for (int num : nums) {
res ^= num;
}
return res;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] arr = {1, 2, 5, 2};
System.out.println(Arrays.toString(solution.singleNumbers(arr)));
}
}
q56_II_数组中数字出现的次数II
题目
在一个数组 nums
中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。
示例
输入:nums = [9,1,7,9,7,9,7]
输出:1
解答
public class Solution {
/**
* 在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。
*/
public int singleNumber(int[] nums) {
if (nums.length <= 0) {
return -1;
}
// 统计每一位数的二进制位==1的和
int[] bitSum = new int[32];
for (int num : nums) {
int bit = 1;
// 从低位开始遍历
for (int j = 31; j >= 0; j--) {
// !=0的,该位数+1
if ((num & bit) != 0) {
bitSum[j] += 1;
}
// =0就不用加,更新bit
bit = bit << 1;
}
}
int res = 0;
for (int i = 0; i < 32; i++) {
// bit从高位开始判断,res从低位0开始左移
res = res << 1;
res += bitSum[i] % 3;
}
return res;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] arr = {9, 1, 7, 9, 7, 9, 7};
System.out.println(solution.singleNumber(arr));
}
}
q57_I_和为s的两个数
题目
输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。
示例
输入:nums = [2,7,11,15], target = 9
输出:[2,7] 或者 [7,2]
解答
public class Solution {
/**
* 输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。
* 如果有多对数字的和等于s,则输出任意一对即可。
*/
public int[] twoSum(int[] nums, int target) {
// 因为数组有序,首尾确定两个数
int left = 0, right = nums.length - 1;
while (left < right) {
int sum = nums[left] + nums[right];
// 小了,就移动左指针
if (sum < target) {
left++;
} else if (sum > target) {
// 大了,就移动右指针
right--;
} else {
return new int[]{nums[left], nums[right]};
}
}
return new int[]{};
}
}
q57_II_和为s的连续正数序列
题目
输入一个正整数 target
,输出所有和为 target
的连续正整数序列(至少含有两个数)。
序列内的数字由小到大排列,不同序列按照首个数字从小到大排列
示例
输入:target = 15
输出:[[1,2,3,4,5],[4,5,6],[7,8]]
解答
public class Solution {
/**
* 输入一个正整数 `target` ,输出所有和为 `target` 的连续正整数序列(至少含有两个数)。
* 序列内的数字由小到大排列,不同序列按照首个数字从小到大排列.1 <= target <= 10^5
* 输入:target = 15
* 输出:[[1,2,3,4,5],[4,5,6],[7,8]]
*/
public int[][] findContinuousSequence(int target) {
int small = 1, big = 2;
// 由于至少要有2个数,small作为起点的上界是不到(target+1)/2
int mid = (target + 1) / 2;
int curSum = small + big;
List<int[]> res = new ArrayList<>();
while (small < mid) {
if (curSum == target) {
listAddData(small, big, res);
}
while (curSum > target && small < mid) {
// 在前一个和的基础上进行操作,减少不必要的计算
curSum -= small;
small++;
if (curSum == target) {
listAddData(small, big, res);
}
}
big++;
curSum += big;
}
// list转化为二维数组返回
return res.toArray(new int[0][]);
}
/**
* list中添加[small,big]的数组
*/
private void listAddData(int small, int big, List<int[]> list) {
int[] temp = new int[big - small + 1];
for (int i = small; i <= big; i++) {
temp[i - small] = i;
}
list.add(temp);
}
}
q58_I_反转单词顺序
题目
输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student. “,则输出"student. a am I”。
示例
输入: " hello world! "
输出: "world! hello"
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
解答
public class Solution {
/**
* 输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。
* 为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student. ",则输出"student. a am I"。
*/
public String reverseWords(String s) {
// 去掉首尾空格
String trim = s.trim();
// 双指针
// 每个非空单词的末尾指针
int last = trim.length() - 1;
// 从后往前遍历字符串
int index = last;
StringBuilder sb = new StringBuilder();
while (index >= 0) {
// 获得每个非空单词的起始位置前一个位置
while (index >= 0 && trim.charAt(index) != ' ') {
index--;
}
// 添加:将这个单词添加进结果字符串中,同时加上空格
sb.append(trim, index + 1, last + 1).append(" ");
// 跳过原字符中单词中间的空格
while (index >= 0 && trim.charAt(index) == ' ') {
index--;
}
// 更新每个单词的末尾指针
last = index;
}
// 返回结果去掉末尾的空格
return sb.toString().trim();
}
}
q58_II_左旋转字符串
题目
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
示例
输入: s = "abcdefg", k = 2
输出: "cdefgab"
解答
public class Solution {
/**
* 字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。
* 请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
*/
public String reverseLeftWords(String s, int n) {
int len = s.length();
StringBuilder sb = new StringBuilder();
// 原s:下标0,n-1,n,...,len-1
// 新sb:下标n,...,len-1,len,...,n+len-1
for (int i = n; i < n + len; i++) {
sb.append(s.charAt(i % len));
}
return sb.toString();
}
}
q59_I_滑动窗口最大值
题目
给定一个数组 nums
和滑动窗口的大小 k
,请找出所有滑动窗口里的最大值。
示例
输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
解答
public class Solution {
/**
* 给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。
* 输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
* 输出: [3,3,5,5,6,7]
*/
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length < k || k < 1) {
return new int[]{};
}
// 双端队列队头存区间最大值下标
Deque<Integer> queue = new LinkedList<>();
int[] res = new int[nums.length - k + 1];
int index = 0;
for (int i = 0; i < nums.length; i++) {
// 双端队列,队头保证存窗口内最大值下标
while (!queue.isEmpty() && nums[queue.peekLast()] <= nums[i]) {
queue.pollLast();
}
// 存下标
queue.addLast(i);
// 数字下标与队列头差值>=窗口长度,队头移出队列
if (i - queue.peekFirst() >= k) {
queue.pollFirst();
}
// 数字下标>=窗口长度下标,就要记录队头元素
if (i >= k - 1) {
res[index++] = nums[queue.peekFirst()];
}
}
return res;
}
public static void main(String[] args) {
int[] nums = {1, 3, -1, -3, 5, 3, 6, 7};
int k = 3;
Solution solution = new Solution();
int[] res = solution.maxSlidingWindow(nums, k);
System.out.println(Arrays.toString(res));
}
}
q59_II_队列最大值
题目
请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。
若队列为空,pop_front 和 max_value 需要返回 -1
示例
输入:
["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]
解答
public class MaxQueue {
/**
* 普通队列:正常的push、pop
*/
private Queue<Integer> queue;
/**
* 双端队列:队头保证是最大值
*/
private Deque<Integer> deque;
public MaxQueue() {
queue = new LinkedList<>();
deque = new LinkedList<>();
}
public int max_value() {
if (deque.isEmpty()) {
// 条件规定:max栈为空,返回-1
return -1;
}
return deque.peekFirst();
}
public void push_back(int value) {
// 待加元素>双端队列队尾元素,双端队列队尾就一直出队
while (!deque.isEmpty() && value > deque.peekLast()) {
deque.pollLast();
}
deque.offerLast(value);
queue.offer(value);
}
public int pop_front() {
if (queue.isEmpty()) {
return -1;
}
int value = queue.poll();
// 待出队元素,和最大值相同,双端队列队头出队
if (value == max_value()) {
deque.pollFirst();
}
return value;
}
public static void main(String[] args) {
LinkedList<Integer> queue = new LinkedList<>();
queue.addLast(1);
queue.addLast(2);
queue.addLast(3);
queue.addLast(4);
System.out.println(queue.peekFirst());
System.out.println(queue.peekLast());
}
}
q60_n个骰子的点数
题目
把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。
示例
输入: 2
输出: [0.02778,0.05556,0.08333,0.11111,0.13889,0.16667,0.13889,0.11111,0.08333,0.05556,0.02778]
解答
public class Solution {
/**
* 把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。
* 输入n,打印出s的所有可能的值出现的概率。
*/
public double[] dicesProbability(int n) {
// 初始化第1个骰子:点数和总数为6个,概率都为1.0/6.0
double[] dp = new double[6];
Arrays.fill(dp, 1.0 / 6.0);
// 计算第2个到第n个骰子的点数和概率
for (int i = 2; i <= n; i++) {
// 1个骰子:6种点数和总数;2个骰子:6+5种点数和总数;
// 点数和总数:5n+1
double[] next = new double[5 * i + 1];
// 正向遍历上一轮dp
for (int j = 0; j < dp.length; j++) {
// 每6个数,遍历这一轮的next数组
for (int k = 0; k < 6; k++) {
// 上一轮的dp[j]对它之后的每6个数都产生了dp[j]/6.0的影响
next[j + k] += dp[j] / 6.0;
}
}
// 上一轮的dp数组更新为这一轮的next数组
dp = next;
}
return dp;
}
}
q61_扑克牌中的顺子
题目
从若干副扑克牌中随机抽 5 张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。
示例
输入: [1,2,3,4,5]
输出: True
解答
public class Solution {
/**
* 从若干副扑克牌中随机抽 5 张牌,判断是不是一个顺子,即这5张牌是不是连续的。
* 2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。
* 输入: nums=[1,2,3,4,5],就是已经抽出来的5个数字啦
* 输出: True
*/
public boolean isStraight(int[] nums) {
// set判断是否有重复的数
Set<Integer> set = new HashSet<>();
// 假设扑克牌中大小王为0,A=1,范围[0,A,2...10,J,Q,K]
int min = 13, max = 0;
for (int num : nums) {
// 如果是大小王,这一轮循环跳过
if (num == 0) {
continue;
} else {
// 不是大小王,
// 若有重复数字,必不可能构成顺子
if (set.contains(num)) {
return false;
}
// 若没有重复数字,找到当前的max,min
// 每一轮必须满足判断顺子的条件:max-min<5
min = Math.min(min, num);
max = Math.max(max, num);
// 当前元素放进set中,给之后的遍历重复元素做铺垫
set.add(num);
}
}
// 返回整个数组中的max-min<5的布尔值
return max - min < 5;
}
}
q62_圆圈中最后剩下的数字
题目
0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。
例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。
示例
输入: n = 5, m = 3
输出: 3
解答
public class Solution {
/**
* 0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。
* 例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。
* 约瑟夫回环问题
*/
public int lastRemaining(int n, int m) {
if (n < 1 || m < 1) {
return -1;
}
// f(n,m)表示每次在n个数字0...n-1中删除第m个数字后剩下的数字
// 显然f(n,m)=f'(n-1,m)
// 得到n个数的序列中最后剩下的数字,只需要得到n-1个数字序列中最后剩下的数字和0
int last = 0;
for (int i = 2; i <= n; i++) {
last = (last + m) % i;
}
return last;
}
}
q63_股票的最大利润
题目
假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?
示例
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
解答
public class Solution {
/**
* 假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?
*/
public int maxProfit(int[] prices) {
if (prices.length <= 1) {
return 0;
}
// 记录买入价
int minPrice = prices[0];
int maxProfit = 0;
for (int i = 1; i < prices.length; i++) {
// 固定卖出价=prices[i],买入价minPrice越低,利润越大
minPrice = Math.min(minPrice, prices[i]);
maxProfit = Math.max(maxProfit, prices[i] - minPrice);
}
return maxProfit;
}
}
q64_求1加到n
题目
求 1+2+...+n
,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
示例
输入: n = 3
输出: 6
解答
public class Solution {
/**
* 求 `1+2+...+n` ,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
* 言外之意:只能使用>、<、=、&、^、|、+、-这几个符号
*/
public int sumNums(int n) {
// n>1时开始递归,不能用条件判断语句,就使用&&辅助完成判断
// boolean x =...,A&&B整体需要一个返回值接受,否则单独返回会报错
// A&&B,两边都需要布尔值,A的n>1比较容易想到,B中的>0利用1+...+n>0来凑布尔值
boolean x = (n > 1) && (n += sumNums(n - 1)) > 0;
// 返回n
return n;
}
/**
* 常见求和递归
*/
private int sumN(int n) {
// 当n=1时停止递归
// 不能用if,就用 n>1 &&
if (n == 1) {
return 1;
}
// n>1时,开始递归
// sum=n+fun(n-1)=n+n-1+fun(n-2)...
// 这个没有限制,可以使用,
// 但是为了保证&&两边都是布尔,取n += sumN(n - 1)>0必成立
n += sumN(n - 1);
return n;
}
}
q65_不用加减乘除做加法
题目
写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。
示例
输入: a = 1, b = 1
输出: 2
解答
public class Solution {
/**
* 写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。
*/
public int add(int a, int b) {
int sum;
while (b != 0) {
// 无进位相加=异或结果
sum = a ^ b;
// 进位信息=位与操作,再左移一位
b = (a & b) << 1;
a = sum;
}
// b==0跳出循环,返回a
return a;
}
public static void main(String[] args) {
int a = 1;
int b = 3;
Solution solution = new Solution();
System.out.println(solution.add(a, b));
}
}
q66_构建乘积数组
题目
给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B[i] 的值是数组 A 中除了下标 i 以外的元素的积, 即 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法
示例
输入: [1,2,3,4,5]
输出: [120,60,40,30,24]
解答
public class Solution {
/**
* 给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],
* 其中B[i] 的值是数组 A 中除了下标 i 以外的元素的积, 即B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。
* 不能使用除法
*/
public int[] constructArr(int[] a) {
if (a == null || a.length == 0) {
return new int[]{};
}
int len = a.length;
// a[i]中每个元素左边所有数的乘积
int[] left = new int[len];
// a[i]中每个元素右边所有数的乘积
int[] right = new int[len];
int[] res = new int[len];
// 初始化,两个dp数组左右两边的乘积为1
left[0] = 1;
right[len - 1] = 1;
for (int i = 1; i < len; i++) {
left[i] = left[i - 1] * a[i - 1];
}
for (int i = len - 2; i >= 0; i--) {
right[i] = right[i + 1] * a[i + 1];
}
for (int i = 0; i < len; i++) {
res[i] = left[i] * right[i];
}
return res;
}
}
第7章_两个面试案例
q67_字符串转换为整数
题目
写一个函数 StrToInt,实现把字符串转换成整数这个功能。不能使用 atoi 或者其他类似的库函数。
首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。
当我们寻找到的第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。
该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。
注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。
在任何情况下,若函数不能进行有效的转换时,请返回 0。
说明:
假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−231, 231 − 1]。如果数值超过这个范围,请返回 INT_MAX (231 − 1) 或 INT_MIN (−231) 。
示例
输入: " -42"
输出: -42
解释: 第一个非空白字符为 '-', 它是一个负号。
我们尽可能将负号与后面所有连续出现的数字组合起来,最后得到 -42 。
输入: "4193 with words"
输出: 4193
解释: 转换截止于数字 '3' ,因为它的下一个字符不为数字。
解答
public class Solution {
/**
* 写一个函数 StrToInt,实现把字符串转换成整数这个功能。不能使用库函数。
* 边界:""字符串、非法输入、正负号、整型溢出问题 -> 最后才 num=num*10+字符数字
*/
public int strToInt(String str) {
// 去掉首尾空格:原数组去首位空格后转换为字符数组
char[] c = str.trim().toCharArray();
if (c.length == 0) {
return 0;
}
int num = 0;
// num越界的两种情况:int型最大值21474836472147483647,
// 由于最后num=num*10+字符数,所以边界需要/10
int maxBoundary = Integer.MAX_VALUE / 10;
// sign:正负号标记,1正号,-1负号
int index = 1, sign = 1;
// 第一个部分有三种情况:+/-/数字
// -:遇到负号就sign成-1
if (c[0] == '-') {
sign = -1;
} else if (c[0] != '+') {
// 隐含条件:c[0]!= -和+,只能等于数字,初始化指针为0位置
index = 0;
}
for (int i = index; i < c.length; i++) {
// 力扣:遇到非数字部分,可以忽略,直接当前得到的整数
if (c[i] < '0' || c[i] > '9') {
break;
}
// 1.num=最大值/10 且 c[i]>'7',乘积后必越界
// 2.num>最大值/10 乘积后必越界
if (num > maxBoundary || num == maxBoundary && c[i] > '7') {
// 越界后,返回整型最值
return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
}
// 不越界,才拼接数字部分
num = num * 10 + (c[i] - '0');
}
// 返回符号*数字
return sign * num;
}
public static void main(String[] args) {
// ""字符串trim()后,长度为0
System.out.println("".trim().length());
}
}
q68_I_二叉搜索树的最近公共祖先
题目
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
示例
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出: 6
解释: 节点 2 和节点 8 的最近公共祖先是 6。
解答
public class Solution {
/**
* 给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
* BST:左小右大
*/
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 保证p,q左小右大
if (p.val > q.val) {
TreeNode temp = p;
p = q;
q = temp;
}
while (root != null) {
// root比最小的还小,说明root出现在了p,q的左边,那么公共祖先在root的右边
if (root.val < p.val) {
root = root.right;
} else if (root.val > q.val) {
// root比最大的还大,说明root出现在了p,q的右边,那么公共祖先在root的左边
root = root.left;
} else {
// root.val>=p.val||root.val<=q.val,root此时指向的就是最近公共祖先
break;
}
}
return root;
}
}
q68_II_二叉树的最近公共祖先
题目
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
示例
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。
解答
public class Solution {
/**
* 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
*/
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// base case:root越过叶子节点,返回null 或者root = p/q,返回root
if (root == null || root == p || root == q) {
return root;
}
// 普通二叉树找最近公共祖先,使用后序遍历,用left和right接收
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
// 根据base case:当left和right同时不为空,说明p,q在root异侧,root就是最近公共祖先
if (left != null && right != null) {
return root;
}
// 当left和right同时为空:说明root左右子树不包含p,q,无公共祖先
// 当left和right一个空,一个不空,最近公共祖先肯定在root非空的那个子树里
return left != null ? left : right;
}
}