Long Luo's Life Notes

每一天都是奇迹

By Long Luo

Leetcode 209. 长度最小的子数组 题目如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
209. 长度最小的子数组

给定一个含有$n$个正整数的数组和一个正整数$\textit{target}$。

找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回$0$。

示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

示例 2:
输入:target = 4, nums = [1,4,4]
输出:1

示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0

提示:
1 <= target <= 10^9
1 <= nums.length <= 10^5
1 <= nums[i] <= 10^5

进阶:
如果你已经实现 $O(n)$ 时间复杂度的解法, 请尝试设计一个 $O(n\log n)$ 时间复杂度的解法。

方法一:暴力法

思路与算法:

很容易想到,使用 \(2\) 个循环,枚举 \(\textit{nums}\) 中的每个下标作为子数组的开始下标,对于每个子数组 \(i\),满足 \(i \le x \le j\),使得:

\[ sum=\sum_{x=i}^{j}\textit{nums}[x] \ge $\textit{target} \]

,并更新子数组的最小长度 \(j-i+1\)

代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public int minSubArrayLen_bf(int target, int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}

int len = nums.length;
int ans = Integer.MAX_VALUE;
for (int i = 0; i < len; i++) {
int sum = 0;
for (int j = i; j < len; j++) {
sum += nums[j];
if (sum >= target) {
ans = Math.min(ans, j - i + 1);
break;
}
}
}

return ans == Integer.MAX_VALUE ? 0 : ans;
}

复杂度分析:

  • 时间复杂度:\(O(N^2)\),其中 \(N\) 是数组 \(\textit{nums}\) 的长度。
  • 空间复杂度:\(O(1)\)

方法二:双指针 + 滑动窗口

思路与算法:

因为求连续子数组的和大于某个值,那么很容易想到使用滑动窗口,使用双指针 \(left\)\(right\) 表示一个窗口:

  1. \(right\) 向右移增大窗口,直到窗口内的数字和大于等于了 \(target\);
  2. 记录此时的长度,\(left\) 向右移动,开始减少长度,每减少一次,就更新最小长度,直到当前窗口内的数字和 \(< target\),回到第 \(1\) 步。

代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public int minSubArrayLen_sw(int target, int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}

int len = nums.length;
int ans = Integer.MAX_VALUE;
int sum = 0;
int left = 0;
int right = 0;
while (right < len) {
sum += nums[right];
while (sum >= target) {
ans = Math.min(ans, right - left + 1);
sum -= nums[left];
left++;
}

right++;
}

return ans == Integer.MAX_VALUE ? 0 : ans;
}

复杂度分析:

  • 时间复杂度:\(O(N)\),其中 \(N\) 是数组 \(\textit{nums}\) 的长度,指针 \(\textit{start}\)\(\textit{end}\) 最多各移动 \(n\) 次。
  • 空间复杂度:\(O(1)\)
阅读全文 »

By Long Luo

Leetcode 155. 最小栈 题目如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
155. 最小栈

设计一个支持push,pop,top操作,并能在常数时间内检索到最小元素的栈。

push(x) —— 将元素 x 推入栈中。
pop() —— 删除栈顶的元素。
top() —— 获取栈顶元素。
getMin() —— 检索栈中的最小元素。

示例:

输入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]

输出:
[null,null,null,null,-3,null,0,-2]

解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.

提示:
pop、top和getMin操作总是在 非空栈 上调用。

这道题有很多方法可以做,其实也就分为额外 \(O(n)\)\(O(1)\) 空间,如果 \(O(n)\) 的话,有非常多方法。如果只能使用一个栈的话,需要思考如何处理当前 \(\texttt{push()}\) 更小的数如何处理。

方法一:栈 + List

思路与算法:

最开始想到的方法就是用一个链表存储数据,这样排序之后获取最小值的就直接读取链表的第一个元素即可。

代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class MinStack {
Deque<Integer> stack;
List<Integer> numList;

public MinStack() {
stack = new ArrayDeque<>();
numList = new ArrayList<>();
}

// time: O(nlogn)
public void push(int val) {
stack.push(val);
numList.add(val);
Collections.sort(numList);
}

// time: O(n)
public void pop() {
int val = stack.pop();
for (int i = 0; i < numList.size(); i++) {
if (numList.get(i) == val) {
numList.remove(i);
break;
}
}

Collections.sort(numList);
}

// time: O(1)
public int top() {
return stack.peek();
}

// time: O(1)
public int getMin() {
return numList.get(0);
}
}

复杂度分析

  • 时间复杂度:
    • \(\texttt{push()}\): \(O(n \log n)\),需要进行排序。
    • \(\texttt{pop()}\): \(O(n \log n)\)
    • \(\texttt{top()}\): \(O(1)\)
    • \(\texttt{getMin()}\): \(O(1)\)
  • 空间复杂度:\(O(N)\),其中 \(N\) 为总操作数。

方法二:辅助栈

思路与算法:

方法一时间复杂度太高,其实排序也是不必要的,占用空间也不小。

因为栈是先进后出的,所以可以在每个元素 \(x\) 入栈时把当前栈的最小值 \(min\) 存储起来。

在这之后无论何时,如果栈顶元素是 \(x\),我们就可以直接返回存储的最小值 \(min\)

因此我们可以使用一个辅助栈,与元素栈同步插入与删除,用于存储与每个元素对应的最小值。

  • 当要入栈一个元素时,获取当前辅助栈的栈顶存储的最小值,与当前元素比较得出最小值,将这个最小值插入辅助栈中;

  • 当一个元素要出栈时,我们把辅助栈的栈顶元素也一并弹出;

  • 在任意一个时刻,栈内元素的最小值就存储在辅助栈的栈顶元素中。

阅读全文 »

By Long Luo

Leetcode 4. 寻找两个正序数组的中位数 ,这是一道很经典的题目,非常考察算法功底和逻辑思维能力,下面我们就通过几种解法来吃透这道题吧!

方法一:暴力法(合并数组)

思路与算法:

很直观的解法,先将两个数组按照元素大小合并为一个数组,然后根据新数组长度来决定返回值。

注意在合并数组时,要注意指针边界情况,不要越过数组边界。

代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;

if (m == 0) {
if (n % 2 == 0) {
return (nums2[n / 2 - 1] + nums2[n / 2]) / 2.0;
} else {
return nums2[n / 2];
}
} else if (n == 0) {
if (m % 2 == 0) {
return (nums1[m / 2 - 1] + nums1[m / 2]) / 2.0;
} else {
return nums1[m / 2];
}
}

int[] nums = new int[m + n];
int total = m + n;
for (int i = 0, j = 0, cnt = 0; cnt < total; ) {
if (i == m && j < n) {
nums[cnt++] = nums2[j++];
} else if (j == n && i < m) {
nums[cnt++] = nums1[i++];
} else if (i < m && nums1[i] <= nums2[j]) {
nums[cnt++] = nums1[i++];
} else if (i < m && nums1[i] > nums2[j]) {
nums[cnt++] = nums2[j++];
}
}

if (total % 2 == 0) {
return (nums[total / 2 - 1] + nums[total / 2]) / 2.0;
} else {
return nums[total / 2];
}
}

其实,在合并数组时,实际上只需要合并到 \(cnt=total/2\) 即可,优化之后如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public double findMedianSortedArrays_bf_opt(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;

if (m == 0) {
if (n % 2 == 0) {
return (nums2[n / 2 - 1] + nums2[n / 2]) / 2.0;
} else {
return nums2[n / 2];
}
} else if (n == 0) {
if (m % 2 == 0) {
return (nums1[m / 2 - 1] + nums1[m / 2]) / 2.0;
} else {
return nums1[m / 2];
}
}

int[] nums = new int[m + n];
int total = m + n;
for (int i = 0, j = 0, cnt = 0; cnt <= total / 2; ) {
if (i == m && j < n) {
nums[cnt++] = nums2[j++];
} else if (j == n && i < m) {
nums[cnt++] = nums1[i++];
} else if (i < m && nums1[i] <= nums2[j]) {
nums[cnt++] = nums1[i++];
} else if (i < m && nums1[i] > nums2[j]) {
nums[cnt++] = nums2[j++];
}
}

if (total % 2 == 0) {
return (nums[total / 2 - 1] + nums[total / 2]) / 2.0;
} else {
return nums[total / 2];
}
}

复杂度分析:

  • 时间复杂度:\(O(m+n)\),需要遍历 \(2\) 个数组。
  • 空间复杂度:\(O(m+n)\),开辟了一个新数组,长度为 \(m+n\)
阅读全文 »

By Long Luo

Leetcode 18. 四数之和 题解。

方法一:暴力枚举

思路与算法:

15. 三数之和 类似,我们先对数组进行排序,然后 \(4\) 层循环即可。

由于结果肯定会出现重复的数字,所以我们使用 \(\texttt{Set}\) 来去重,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public List<List<Integer>> fourSum(int[] nums, int target) {
if (nums == null || nums.length < 4) {
return new ArrayList<>();
}

Arrays.sort(nums);
int n = nums.length;
Set<List<Integer>> ans = new HashSet<>();
for (int first = 0; first < n - 3; first++) {
for (int second = first + 1; second < n - 2; second++) {
for (int third = second + 1; third < n - 1; third++) {
for (int fourth = third + 1; fourth < n; fourth++) {
if (nums[first] + nums[second] + nums[third] + nums[fourth] == target) {
ans.add(Arrays.asList(nums[first], nums[second], nums[third], nums[fourth]));
}
}
}
}
}

return new ArrayList<>(ans);
}

我们可以在每次循环中增加判断,防止出现重复四元组,使用 \(\texttt{List}\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public List<List<Integer>> fourSum(int[] nums, int target) {
if (nums == null || nums.length < 4) {
return new ArrayList<>();
}

Arrays.sort(nums);
int len = nums.length;
List<List<Integer>> ans = new ArrayList<>();
for (int i = 0; i < len - 3; i++) {
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}

if ((long)nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) {
break;
}

if ((long)nums[i] + nums[len - 3] + nums[len - 2] + nums[len - 1] < target ) {
continue;
}

for (int j = i + 1; j < len - 2; j++) {
if (j > i + 1 && nums[j] == nums[j - 1]) {
continue;
}

if ((long)nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target) {
break;
}

if ((long)nums[i] + nums[j] + nums[len - 2] + nums[len - 1] < target) {
continue;
}

for (int k = j + 1; k < len - 1; k++) {
if (k > j + 1 && nums[k] == nums[k - 1]) {
continue;
}

if ((long)nums[i] + nums[j] + nums[k] + nums[k + 1] > target) {
break;
}

if ((long)nums[i] + nums[j] + nums[k] + nums[len - 1] < target) {
continue;
}

for (int l = k + 1; l < len; l++) {
if (l > k + 1 && nums[l] == nums[l - 1]) {
continue;
}

if (nums[i] + nums[j] + nums[k] + nums[l] == target) {
ans.add(Arrays.asList(nums[i], nums[j], nums[k], nums[l]));
}
}
}
}
}

return ans;
}

复杂度分析:

  • 时间复杂度:\(O(n^4)\),其中 \(n\) 是数组 \(\textit{nums}\) 的长度。
  • 空间复杂度:\(O(logn)\), 空间复杂度主要取决于排序额外使用的空间 \(O(logn)\)
阅读全文 »

By Long Luo

Leetcode 15. 三数之和 题解。

方法一:暴力遍历

思路与算法:

首先想到的是暴力枚举\(3\)层循环,但结果肯定会出现重复的数字,我们可以使用HashSet来去除重复元素。

值得注意的是需要先进行排序,如果不排序的话,会返回重复的答案,因为虽然List元素都相同,但顺序不同,还是重复的三元组。

代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public List<List<Integer>> threeSum(int[] nums) {
if (nums == null || nums.length < 3) {
return new ArrayList<>();
}

Set<List<Integer>> ans = new HashSet<>();
Arrays.sort(nums);
int length = nums.length;
for (int i = 0; i < length - 2; i++) {
for (int j = i + 1; j < length - 1; j++) {
for (int k = j + 1; k < length; k++) {
if (nums[i] + nums[j] + nums[k] == 0) {
ans.add(Arrays.asList(nums[i], nums[j], nums[k]));
}
}
}
}

return new ArrayList<>(ans);
}
}

上述操作会超时,而且使用了Set进行去重,可不可以不用Set呢?

如何才能保证不包含重复的三元组?

保持三重循环的大框架不变,只需要保证:

  • 第二重循环枚举到的元素不小于当前第一重循环枚举到的元素;
  • 第三重循环枚举到的元素不小于当前第二重循环枚举到的元素。

也就是说,我们枚举的三元组 \((a, b, c)\) 满足 \(a \leq b \leq c\) ,保证了只有 \((a, b, c)\) 这个顺序会被枚举到,而\((b, a, c)\)\((c, b, a)\) 等等这些不会,这样就减少了重复。

要实现这一点,先将数组中的元素从小到大进行排序,在每次循环中如果判断,防止出现重复的答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public List<List<Integer>> threeSum(int[] nums) {
if (nums == null || nums.length < 3) {
return new ArrayList<>();
}

List<List<Integer>> ans = new ArrayList<>();
Arrays.sort(nums);
int length = nums.length;
for (int i = 0; i < length - 2; i++) {
if (nums[i] > 0) {
break;
}

if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}

for (int j = i + 1; j < length - 1; j++) {
if (j > i + 1 && nums[j] == nums[j - 1]) {
continue;
}

for (int k = j + 1; k < length; k++) {
if (k > j + 1 && nums[k] == nums[k - 1]) {
continue;
}

if (nums[i] + nums[j] + nums[k] == 0) {
ans.add(Arrays.asList(nums[i], nums[j], nums[k]));
}
}
}
}

return ans;
}

复杂度分析

  • 时间复杂度: \(O(N^3)\) ,其中 \(N\) 是数组 \(\textit{nums}\) 的长度。
  • 空间复杂度: \(O(\log N)\),额外的排序的空间复杂度为\(O(\log N)\)
阅读全文 »
0%