Array
练题步骤
- 5-10分钟:读题和思考
- 有思路:自己开始做和写代码;
- 不然,马上看题解!
- 第一遍:默写背诵、熟练
- 第二遍:然后开始自己写(闭卷)
移动零
283. 移动零:给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
题解思路
用两个指针i
和j
,只要nums[i]!=0
,我们就交换nums[i]
和nums[j]
class Solution {
public void moveZeroes(int[] nums) {
// 增强稳健性
if (nums == null || nums.length == 0)
return;
//insertPos就是图示中的b
int insertPos = 0;
// i就是图示中的a
for (int i = 0; i < nums.length; i++) {
if (nums[i] != 0) {
int temp = nums[j];
nums[insertPos++] = nums[i];
nums[i] = temp;
}
}
}
}
盛最多水的容器
11. 盛最多水的容器:给你 n 个非负整数 a1,a2,…,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
说明:你不能倾斜容器,且 n 的值至少为 2。
图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
解题思路一:枚举
- 枚举:left bar x, right bar y,(x-y) *height_diff
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)
// 解法一:枚举法
int maxArea = 0;
// 双层循环的写法
for (int i = 0; i < height.length; i++) {
for (int j = i + 1; j < height.length; j++) {
int area = Math.min(height[i], height[j]) * (j - i);
maxArea = Math.max(maxArea, area);
}
}
return maxArea;
解题思路二:双指针算法
- 关键字∶左右两边
- 模式识别:需要移动左右两头的问题可以考虑双指针
- 相同情况下两边距离越远越好区域受限于较短边
class Solution {
public int maxArea(int[] height) {
if (height.length == 0 || height == null) {
return 0;
}
int left = 0;
int right = height.length - 1;
int maxArea = 0;
while (left < right) {
int tempArea = Math.min(height[left], height[right]) * (right - left);
maxArea = Math.max(maxArea, tempArea);
if (height[left] < height[right]) {
++left;
} else {
--right;
}
}
return maxArea;
}
}
精简写法
- 左右边界i、j,向中间收敛:左右夹逼
class Solution {
public int maxArea(int[] height) {
public int maxArea ( int[] height){
int maxArea = 0;
for (int i = 0, j = height.length - 1; i < j; ) {
// 取出小的那个短边
int minHeight = height[i] < height[j] ? height[i++] : height[j--];
// 因为这里的i,j在前面被移动过一次,宽度变小,所以要加1;
// int area = minHeight * (j - i + 1)
maxArea = Math.max(maxArea,minHeight * (j - i + 1));
}
return maxArea;
}
}
}
爬楼梯
70. 爬楼梯:假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?**注意:**给定 n 是一个正整数。
解题思路一:斐波那契数列
如果观察数学规律,可知本题是斐波那契数列,那么用斐波那契数列的公式即可解决问题,公式如下:根据递推方程
f
(
n
)
=
f
(
n
−
1
)
+
f
(
n
−
2
)
f(n) = f(n - 1) + f(n - 2)
f(n)=f(n−1)+f(n−2),我们可以写出这样的特征方程:
x
2
=
x
+
1
x^2=x+1
x2=x+1
求得
x
1
=
1
+
5
2
x_1=\frac{1+\sqrt{5}}{2}
x1=21+5,
x
2
=
1
−
5
2
x_2=\frac{1-\sqrt{5}}{2}
x2=21−5,设通解为
f
(
n
)
=
c
1
x
1
n
+
c
2
x
2
n
f(n)=c_1x_{1}^{n} +c_2x_{2}^{n}
f(n)=c1x1n+c2x2n,代入初始条件f(1)=1,f(2)=2,得
c
1
=
1
5
c_1=\frac{1}{\sqrt5}
c1=51,
c
2
=
−
1
5
c_2=-\frac{1}{\sqrt5}
c2=−51,得到了这个递推数列的通项公式:
F
n
=
1
5
[
(
1
+
5
2
)
n
−
(
1
−
5
2
)
n
]
F_n=\frac{1}{\sqrt5}[(\frac{1+\sqrt5}{2})^n−(\frac{1-\sqrt5}{2})^n]
Fn=51[(21+5)n−(21−5)n]
- 时间复杂度:O(logn)
class Solution {
public int climbStairs(int n) {
if(n<=0) return 0;
double sqrt_5=Math.sqrt(5);
//斐波那契表示为:f(1)=1,f(2) = 1, f(3) = 2....
//而爬楼梯f(1) = 1, f(2) = 2,f(3) = 3
//与斐波那契的n表示差1,所以需要将斐波那契数列的n+1
double fib_n = Math.pow ( (1+sqrt_5)/2 , n+1)+Math.pow((1-sqrt_5)/2 , n+1);
return (int)(fib_n/sqrt_5);
}
}
解题思路二:动态规划
- 找最近重复子问题
本问题其实常规解法可以分成多个子问题,爬第n阶楼梯的方法数量,等于 2 部分之和
-
爬上 n-1阶楼梯的方法数量。因为再爬1阶就能到第n阶
-
爬上 n-2 阶楼梯的方法数量,因为再爬2阶就能到第n阶
所以得到公式 dp[n] = dp[n-1] + dp[n-2]
- 初始化 dp[0]=1、dp[1]=1和dp[2]=2;
时间复杂度:O(n)
class Solution {
public int climbStairs(int n) {
if (n == 1) return 1;
if (n == 2) return 2;
// dp的下标范围是[0,n]
int[] dp = new int[n + 1];
//0阶台阶,没有方式
dp[0] = 0;
//1阶台阶,只有一种方式(1)
dp[1] = 1;
//2阶台阶,只有两种种方式
dp[2] = 2;
//要遍历到第n个台阶,所以指针其实是从[0,n]
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
三数之和
15. 三数之和:给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。注意:答案中不可以包含重复的三元组。
解题思路一:暴力求解法
- 时间复杂度是 O ( n 3 ) O(n^3) O(n3)
大体思路如下,实际上的运行结构并不正确
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
//暴力求解法
Arrays.sort(nums);
//结果集使用了set集合,避免了返回值重复
Set<List<Integer>> res = new LinkedHashSet<>();
for (int i = 0; i < nums.length - 2; i++) {
for (int j = i + 1; j < nums.length - 1; j++) {
for (int k = j + 1; k < nums.length; k++) {
if (nums[i] + nums[k] + nums[j] == 0) {
res.add(Arrays.asList(nums[i], nums[j], nums[k]));
}
}
}
}
return res;
}
}
解题思路二:双指针法思路
- 关键字:不可以包含重复
- 模式识别:利用排序避免重复答案-降低复杂度变成twoSum
- 利用双指针找到所有解
双指针法铺垫: 先将给定 nums
排序,复杂度为
O
(
N
l
o
g
N
)
O(NlogN)
O(NlogN)
具体过程:固定 3 个指针中最左(最小)数字的指针 k,双指针 i,j 分设在数组索引 (k, len(nums))两端,通过双指针交替向中间移动,记录对于每个固定指针 k 的所有满足 nums[k] + nums[i] + nums[j] == 0 的 i,j 组合:
- 特判,对于数组长度 n,如果数组为null或者数组长度小于 3,返回[ ]。
- 当 nums[k] > 0 时直接break跳出:因为 nums[j] >= nums[i] >= nums[k] > 0,即 3 个数字都大于 0 ,在此固定指针 k 之后不可能再找到结果了。
- 当 k > 0 且 nums[k] == nums[k - 1]时即跳过此元素nums[k]:因为已经将 nums[k - 1] 的所有组合加入到结果中,本次双指针搜索只会得到重复组合。
- i,j 分设在数组索引 (k, len(nums)) 两端,当i < j时循环计算s = nums[k] + nums[i] + nums[j],并按照以下规则执行双指针移动:
- 当s < 0时,i += 1并跳过所有重复的nums[i];
- 当s > 0时,j -= 1并跳过所有重复的nums[j];
- 当s == 0时,记录组合[k, i, j]至res,执行i += 1和j -= 1并跳过所有重复的nums[i]和nums[j],防止记录到重复组合。
复杂度分析:
- 时间复杂度 O ( N 2 ) O(N^2) O(N2):其中固定指针k循环复杂度 O(N),双指针 i,j 复杂度 O(N))。
- 空间复杂度 O(1):指针使用常数大小的额外空间。
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> res = new LinkedList<>();
for (int k = 0; k < nums.length - 2; k++) {
// 如果当前数字大于0,则三数之和一定大于0,所以结束循环
if(nums[k] > 0) break;
// 当k>0且 nums[k] == nums[k - 1]时即跳过此元素nums[k]
// k > 0是为了排除初始情况
if (k > 0 && nums[k] == nums[k - 1]) {
continue;
}
int i = k + 1;
int j = nums.length - 1;
while (i < j) {
int sum = nums[k] + nums[i] + nums[j];
if (sum < 0) {
//当s < 0时,i += 1并跳过所有重复的nums[i];
while (i < j && nums[i] == nums[++i]) ;
} else if (sum > 0) {
//当s > 0时,j -= 1并跳过所有重复的nums[j];
while (i < j && nums[j] == nums[--j]) ;
} else {
//当s == 0时,记录组合[k, i, j]至res,执行i += 1和j -= 1并跳过所有重复的nums[i]和nums[j],防止记录到重复组合。
res.add(Arrays.asList(nums[k], nums[i], nums[j]));
while (i < j && nums[i] == nums[++i]) ;
while (i < j && nums[j] == nums[--j]) ;
}
}
}
return res;
}
}
第三课 数组、链表、跳表-Array
练题步骤
- 5-10分钟:读题和思考
- 有思路:自己开始做和写代码;
- 不然,马上看题解!
- 第一遍:默写背诵、熟练
- 第二遍:然后开始自己写(闭卷)
移动零
283. 移动零:给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
题解思路
用两个指针i
和j
,只要nums[i]!=0
,我们就交换nums[i]
和nums[j]
class Solution {
public void moveZeroes(int[] nums) {
// 增强稳健性
if (nums == null || nums.length == 0)
return;
//insertPos就是图示中的b
int insertPos = 0;
// i就是图示中的a
for (int i = 0; i < nums.length; i++) {
if (nums[i] != 0) {
int temp = nums[j];
nums[insertPos++] = nums[i];
nums[i] = temp;
}
}
}
}
盛最多水的容器
11. 盛最多水的容器:给你 n 个非负整数 a1,a2,…,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
说明:你不能倾斜容器,且 n 的值至少为 2。
图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
解题思路一:枚举
- 枚举:left bar x, right bar y,(x-y) *height_diff
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)
// 解法一:枚举法
int maxArea = 0;
// 双层循环的写法
for (int i = 0; i < height.length; i++) {
for (int j = i + 1; j < height.length; j++) {
int area = Math.min(height[i], height[j]) * (j - i);
maxArea = Math.max(maxArea, area);
}
}
return maxArea;
解题思路二:双指针算法
- 关键字∶左右两边
- 模式识别:需要移动左右两头的问题可以考虑双指针
- 相同情况下两边距离越远越好区域受限于较短边
class Solution {
public int maxArea(int[] height) {
if (height.length == 0 || height == null) {
return 0;
}
int left = 0;
int right = height.length - 1;
int maxArea = 0;
while (left < right) {
int tempArea = Math.min(height[left], height[right]) * (right - left);
maxArea = Math.max(maxArea, tempArea);
if (height[left] < height[right]) {
++left;
} else {
--right;
}
}
return maxArea;
}
}
精简写法
- 左右边界i、j,向中间收敛:左右夹逼
class Solution {
public int maxArea(int[] height) {
public int maxArea ( int[] height){
int maxArea = 0;
for (int i = 0, j = height.length - 1; i < j; ) {
// 取出小的那个短边
int minHeight = height[i] < height[j] ? height[i++] : height[j--];
// 因为这里的i,j在前面被移动过一次,宽度变小,所以要加1;
// int area = minHeight * (j - i + 1)
maxArea = Math.max(maxArea,minHeight * (j - i + 1));
}
return maxArea;
}
}
}
爬楼梯
70. 爬楼梯:假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?**注意:**给定 n 是一个正整数。
解题思路一:斐波那契数列
如果观察数学规律,可知本题是斐波那契数列,那么用斐波那契数列的公式即可解决问题,公式如下:根据递推方程
f
(
n
)
=
f
(
n
−
1
)
+
f
(
n
−
2
)
f(n) = f(n - 1) + f(n - 2)
f(n)=f(n−1)+f(n−2),我们可以写出这样的特征方程:
x
2
=
x
+
1
x^2=x+1
x2=x+1
求得
x
1
=
1
+
5
2
x_1=\frac{1+\sqrt{5}}{2}
x1=21+5,
x
2
=
1
−
5
2
x_2=\frac{1-\sqrt{5}}{2}
x2=21−5,设通解为
f
(
n
)
=
c
1
x
1
n
+
c
2
x
2
n
f(n)=c_1x_{1}^{n} +c_2x_{2}^{n}
f(n)=c1x1n+c2x2n,代入初始条件f(1)=1,f(2)=2,得
c
1
=
1
5
c_1=\frac{1}{\sqrt5}
c1=51,
c
2
=
−
1
5
c_2=-\frac{1}{\sqrt5}
c2=−51,得到了这个递推数列的通项公式:
F
n
=
1
5
[
(
1
+
5
2
)
n
−
(
1
−
5
2
)
n
]
F_n=\frac{1}{\sqrt5}[(\frac{1+\sqrt5}{2})^n−(\frac{1-\sqrt5}{2})^n]
Fn=51[(21+5)n−(21−5)n]
- 时间复杂度:O(logn)
class Solution {
public int climbStairs(int n) {
if(n<=0) return 0;
double sqrt_5=Math.sqrt(5);
//斐波那契表示为:f(1)=1,f(2) = 1, f(3) = 2....
//而爬楼梯f(1) = 1, f(2) = 2,f(3) = 3
//与斐波那契的n表示差1,所以需要将斐波那契数列的n+1
double fib_n = Math.pow ( (1+sqrt_5)/2 , n+1)+Math.pow((1-sqrt_5)/2 , n+1);
return (int)(fib_n/sqrt_5);
}
}
解题思路二:动态规划
- 找最近重复子问题
本问题其实常规解法可以分成多个子问题,爬第n阶楼梯的方法数量,等于 2 部分之和
-
爬上 n-1阶楼梯的方法数量。因为再爬1阶就能到第n阶
-
爬上 n-2 阶楼梯的方法数量,因为再爬2阶就能到第n阶
所以得到公式 dp[n] = dp[n-1] + dp[n-2]
- 初始化 dp[0]=1、dp[1]=1和dp[2]=2;
时间复杂度:O(n)
class Solution {
public int climbStairs(int n) {
if (n == 1) return 1;
if (n == 2) return 2;
// dp的下标范围是[0,n]
int[] dp = new int[n + 1];
//0阶台阶,没有方式
dp[0] = 0;
//1阶台阶,只有一种方式(1)
dp[1] = 1;
//2阶台阶,只有两种种方式
dp[2] = 2;
//要遍历到第n个台阶,所以指针其实是从[0,n]
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
三数之和
15. 三数之和:给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。注意:答案中不可以包含重复的三元组。
解题思路一:暴力求解法
- 时间复杂度是 O ( n 3 ) O(n^3) O(n3)
大体思路如下,实际上的运行结构并不正确
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
//暴力求解法
Arrays.sort(nums);
//结果集使用了set集合,避免了返回值重复
Set<List<Integer>> res = new LinkedHashSet<>();
for (int i = 0; i < nums.length - 2; i++) {
for (int j = i + 1; j < nums.length - 1; j++) {
for (int k = j + 1; k < nums.length; k++) {
if (nums[i] + nums[k] + nums[j] == 0) {
res.add(Arrays.asList(nums[i], nums[j], nums[k]));
}
}
}
}
return res;
}
}
解题思路二:双指针法思路
- 关键字:不可以包含重复
- 模式识别:利用排序避免重复答案-降低复杂度变成twoSum
- 利用双指针找到所有解
双指针法铺垫: 先将给定 nums
排序,复杂度为
O
(
N
l
o
g
N
)
O(NlogN)
O(NlogN)
具体过程:固定 3 个指针中最左(最小)数字的指针 k,双指针 i,j 分设在数组索引 (k, len(nums))两端,通过双指针交替向中间移动,记录对于每个固定指针 k 的所有满足 nums[k] + nums[i] + nums[j] == 0 的 i,j 组合:
- 特判,对于数组长度 n,如果数组为null或者数组长度小于 3,返回[ ]。
- 当 nums[k] > 0 时直接break跳出:因为 nums[j] >= nums[i] >= nums[k] > 0,即 3 个数字都大于 0 ,在此固定指针 k 之后不可能再找到结果了。
- 当 k > 0 且 nums[k] == nums[k - 1]时即跳过此元素nums[k]:因为已经将 nums[k - 1] 的所有组合加入到结果中,本次双指针搜索只会得到重复组合。
- i,j 分设在数组索引 (k, len(nums)) 两端,当i < j时循环计算s = nums[k] + nums[i] + nums[j],并按照以下规则执行双指针移动:
- 当s < 0时,i += 1并跳过所有重复的nums[i];
- 当s > 0时,j -= 1并跳过所有重复的nums[j];
- 当s == 0时,记录组合[k, i, j]至res,执行i += 1和j -= 1并跳过所有重复的nums[i]和nums[j],防止记录到重复组合。
复杂度分析:
- 时间复杂度 O ( N 2 ) O(N^2) O(N2):其中固定指针k循环复杂度 O(N),双指针 i,j 复杂度 O(N))。
- 空间复杂度 O(1):指针使用常数大小的额外空间。
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> res = new LinkedList<>();
for (int k = 0; k < nums.length - 2; k++) {
// 如果当前数字大于0,则三数之和一定大于0,所以结束循环
if(nums[k] > 0) break;
// 当k>0且 nums[k] == nums[k - 1]时即跳过此元素nums[k]
// k > 0是为了排除初始情况
if (k > 0 && nums[k] == nums[k - 1]) {
continue;
}
int i = k + 1;
int j = nums.length - 1;
while (i < j) {
int sum = nums[k] + nums[i] + nums[j];
if (sum < 0) {
//当s < 0时,i += 1并跳过所有重复的nums[i];
while (i < j && nums[i] == nums[++i]) ;
} else if (sum > 0) {
//当s > 0时,j -= 1并跳过所有重复的nums[j];
while (i < j && nums[j] == nums[--j]) ;
} else {
//当s == 0时,记录组合[k, i, j]至res,执行i += 1和j -= 1并跳过所有重复的nums[i]和nums[j],防止记录到重复组合。
res.add(Arrays.asList(nums[k], nums[i], nums[j]));
while (i < j && nums[i] == nums[++i]) ;
while (i < j && nums[j] == nums[--j]) ;
}
}
}
return res;
}
}