Skip to content

Commit 8901ecc

Browse files
✨ 001-040
1 parent ae262bc commit 8901ecc

File tree

40 files changed

+5206
-0
lines changed

40 files changed

+5206
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
### 题目描述
2+
3+
这是 LeetCode 上的 **[1. 两数之和](https://leetcode-cn.com/problems/two-sum/solution/po-su-jie-fa-ha-xi-biao-jie-fa-by-ac_oie-yf7o/)** ,难度为 **简单**
4+
5+
Tag : 「哈希表」、「模拟」
6+
7+
8+
9+
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出「和为目标值」的那「两个」整数,并返回它们的数组下标。
10+
11+
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
12+
13+
你可以按任意顺序返回答案。
14+
15+
示例 1:
16+
17+
```
18+
输入:nums = [2,7,11,15], target = 9
19+
输出:[0,1]
20+
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
21+
```
22+
23+
示例 2:
24+
25+
```
26+
输入:nums = [3,2,4], target = 6
27+
输出:[1,2]
28+
```
29+
30+
31+
示例 3:
32+
33+
```
34+
输入:nums = [3,3], target = 6
35+
输出:[0,1]
36+
```
37+
38+
39+
提示:
40+
41+
* 2 <= nums.length <= $10^3$
42+
* -$10^9$ <= nums[i] <= $10^9$
43+
* -$10^9$ <= target <= $10^9$
44+
* 只会存在一个有效答案
45+
46+
---
47+
48+
### 朴素解法
49+
50+
由于我们每次要从数组中找两个数。
51+
52+
因此一个很简单的思路是:使用两重循环枚举下标 i 和 j ,分别代表要找的两个数。
53+
54+
然后判断 $nums[i] + nums[j] == target$ 是否成立。
55+
56+
另外为了防止得到重复的解,我们需要在第一层循环中让 i 从 0 开始,到 n - 2 结束(确保能取到下一位数作为 j );在第二层循环中让 j 从 i + 1 开始,到 n - 1 结束。
57+
58+
```Java []
59+
class Solution {
60+
    public int[] twoSum(int[] nums, int t) {
61+
        int n = nums.length;
62+
        for (int i = 0; i < n - 1; i++) {
63+
            for (int j = i + 1; j < n; j++) {
64+
                if (t == nums[i] + nums[j]) return new int[]{i,j};
65+
            }
66+
        }
67+
        return new int[]{};
68+
    }
69+
}
70+
```
71+
* 时间复杂度:两重循环,以复杂度是 $O(n^2)$
72+
* 空间复杂度:$O(1)$
73+
74+
---
75+
76+
### 哈希表解法
77+
78+
首先,任何优化的思路都不外乎「减少重复」。
79+
80+
从朴素解法中可以知道,逻辑上我们是先定下来一个数,然后从数组中往后找另外一个值是否存在。但其实我们在找第二个数的过程中是重复扫描了数组多次。
81+
82+
举个例子,对于 `nums = [2,3,8,4], target = 9` 的样例,我们先确定下来第一个数是 `2`,然后从后扫描到最后一个数,检查是否有 `7`。发现没有,再决策第一个数为 `3` 的情况,这时候我们应该利用前一次扫描的结果来帮助我们快速判断是否存在 `6`,而不是再重新进行一次扫描。
83+
84+
这是直观的优化思路,不难想到可以使用哈希表进行存储。以数值为 key,数值的下标为 value。
85+
86+
当动手将想法转化为代码时,会发现如果先敲定第一个数,将后面的数加入哈希表,再进行下一位的遍历的时候,还需要将该数值从哈希表中进行删除。
87+
88+
```Java []
89+
class Solution {
90+
    public int[] twoSum(int[] nums, int t) {
91+
        Map<Integer, Integer> map = new HashMap<>();
92+
        for (int i = 0; i < nums.length; i++) map.put(nums[i], i);
93+
        for (int i = 0; i < nums.length; i++) {
94+
            int a = nums[i], b = t - a;
95+
            if (map.get(a) == i) map.remove(a);
96+
            if (map.containsKey(b)) return new int[]{i, map.get(b)};
97+
        }
98+
        return new int[]{};
99+
    }
100+
}
101+
```
102+
最坏情况下,每个数会对应一次哈希表的插入和删除。该解法本质是在循环过程中敲定第一个数,在哈希表中找该数后面是否存在第二个数。
103+
104+
这时候不妨将思路转换过来,遍历过程中敲定第二个数,使用哈希表在第二个数的前面去找第一个数。
105+
106+
具体的做法是,边遍历边存入哈希表,遍历过程中使用的下标 i 用作敲定第二个数,再从现有的哈希表中去找另外一个目标数(由于下标 i 前面的数都被加入哈希表了,即在下标 i 前面去找第一个数)。
107+
108+
```Java []
109+
class Solution {
110+
    public int[] twoSum(int[] nums, int t) {
111+
        Map<Integer, Integer> map = new HashMap<>();
112+
        for (int i = 0; i < nums.length; i++) {
113+
            int a = nums[i], b = t - a;
114+
            if (map.containsKey(b)) return new int[]{map.get(b), i};
115+
            map.put(a, i);
116+
        }
117+
        return new int[]{};
118+
    }
119+
}
120+
```
121+
从 LeetCode 上的执行时间来看,第一种哈希表做法是 4ms,而第二种哈希表的做法是 0ms(不足 1ms 的意思),快在了我们减少了哈希表的插入和删除操作。
122+
123+
但这只是常数级别上的优化,LeetCode 上的执行时间建议只作普通参考,还是要从算法的时空复杂度来分析快慢。
124+
125+
* 时间复杂度:第一种哈希表的做法会扫描数组两次,复杂度是 $O(n)$(忽略常数);第二种做法只会对数组进行一遍扫描,复杂度是 $O(n)$
126+
* 空间复杂度:两种做法都使用了哈希表进行记录,复杂度是 $O(n)$
127+
128+
---
129+
130+
### 其他
131+
132+
可以看到,我将原题目的入参 `target` 替换成了 `t`,但并不影响正确性,目的是为了提高编码速度。如果你也经常参与 LeetCode 周赛,就会发现这是一个常用的小技巧。
133+
134+
这个技巧,我希望你在第一题就掌握。
135+
136+
---
137+
138+
### 最后
139+
140+
这是我们「刷穿 LeetCode」系列文章的第 `No.1` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先将所有不带锁的题目刷完。
141+
142+
在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。
143+
144+
为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode。
145+
146+
在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。
147+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
### 题目描述
2+
3+
这是 LeetCode 上的 **[10. 正则表达式匹配](https://leetcode-cn.com/problems/regular-expression-matching/solution/shua-chuan-lc-dong-tai-gui-hua-jie-fa-by-zn9w/)** ,难度为 **困难**
4+
5+
Tag : 「动态规划」
6+
7+
8+
9+
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。
10+
11+
* '.' 匹配任意单个字符
12+
* '*' 匹配零个或多个前面的那一个元素
13+
14+
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
15+
16+
示例 1:
17+
```
18+
输入:s = "aa" p = "a"
19+
输出:false
20+
解释:"a" 无法匹配 "aa" 整个字符串。
21+
```
22+
示例 2:
23+
```
24+
输入:s = "aa" p = "a*"
25+
输出:true
26+
解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
27+
```
28+
示例 3:
29+
```
30+
输入:s = "ab" p = ".*"
31+
输出:true
32+
解释:".*" 表示可匹配零个或多个('*')任意字符('.')。
33+
```
34+
示例 4:
35+
```
36+
输入:s = "aab" p = "c*a*b"
37+
输出:true
38+
解释:因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。
39+
```
40+
示例 5:
41+
```
42+
输入:s = "mississippi" p = "mis*is*p*."
43+
输出:false
44+
```
45+
46+
提示:
47+
* 0 <= s.length <= 20
48+
* 0 <= p.length <= 30
49+
* s 可能为空,且只包含从 a-z 的小写字母。
50+
* p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *
51+
* 保证每次出现字符 * 时,前面都匹配到有效的字符
52+
53+
---
54+
### 动态规划
55+
56+
整理一下题意,对于字符串 `p` 而言,有三种字符:
57+
58+
* 普通字符:需要和 `s` 中同一位置的字符完全匹配
59+
* `'.'`:能够匹配 `s` 中同一位置的任意字符
60+
* `'*'`:不能够单独使用 `'*'`,必须和前一个字符同时搭配使用,数据保证了 `'*'` 能够找到前面一个字符。能够匹配 `s` 中同一位置字符任意次。
61+
62+
所以本题关键是分析当出现 `a*` 这种字符时,是匹配 0 个 a、还是 1 个 a、还是 2 个 a ...
63+
64+
本题可以使用动态规划进行求解:
65+
66+
* 状态定义:`f(i,j)` 代表考虑 `s` 中以 `i` 为结尾的子串和 `p` 中的 `j` 为结尾的子串是否匹配。即最终我们要求的结果为 `f[n][m]`
67+
68+
* 状态转移:也就是我们要考虑 `f(i,j)` 如何求得,前面说到了 `p` 有三种字符,所以这里的状态转移也要分三种情况讨论:
69+
70+
1. `p[j]` 为普通字符:匹配的条件是前面的字符匹配,同时 `s` 中的第 `i` 个字符和 `p` 中的第 `j` 位相同。 即 `f(i,j) = f(i - 1, j - 1) && s[i] == p[j]`
71+
2. `p[j]``'.'`:匹配的条件是前面的字符匹配, `s` 中的第 `i` 个字符可以是任意字符。即 `f(i,j) = f(i - 1, j - 1) && p[j] == '.'`
72+
3. `p[j]``'*'`:读得 `p[j - 1]` 的字符,例如为字符 a。 然后根据 `a*` 实际匹配 `s``a` 的个数是 0 个、1 个、2 个 ...
73+
74+
3.1. 当匹配为 0 个:`f(i,j) = f(i, j - 2)`
75+
76+
3.2. 当匹配为 1 个:`f(i,j) = f(i - 1, j - 2) && (s[i] == p[j - 1] || p[j - 1] == '.')`
77+
78+
3.3. 当匹配为 2 个:`f(i,j) = f(i - 2, j - 2) && ((s[i] == p[j - 1] && s[i - 1] == p[j - 1]) || p[j] == '.')`
79+
80+
...
81+
82+
83+
**我们知道,通过「枚举」来确定 `*` 到底匹配多少个 `a` 这样的做法,算法复杂度是很高的。**
84+
85+
**我们需要挖掘一些「性质」来简化这个过程。**
86+
87+
![640.png](https://pic.leetcode-cn.com/1611397993-lmpHIZ-640.png)
88+
89+
代码:
90+
```Java []
91+
class Solution {
92+
public boolean isMatch(String ss, String pp) {
93+
// 技巧:往原字符头部插入空格,这样得到 char 数组是从 1 开始,而且可以使得 f[0][0] = true,可以将 true 这个结果滚动下去
94+
int n = ss.length(), m = pp.length();
95+
ss = " " + ss;
96+
pp = " " + pp;
97+
char[] s = ss.toCharArray();
98+
char[] p = pp.toCharArray();
99+
// f(i,j) 代表考虑 s 中的 1~i 字符和 p 中的 1~j 字符 是否匹配
100+
boolean[][] f = new boolean[n + 1][m + 1];
101+
f[0][0] = true;
102+
for (int i = 0; i <= n; i++) {
103+
for (int j = 1; j <= m; j++) {
104+
// 如果下一个字符是 '*',则代表当前字符不能被单独使用,跳过
105+
if (j + 1 <= m && p[j + 1] == '*') continue;
106+
107+
// 对应了 p[j] 为普通字符和 '.' 的两种情况
108+
if (i - 1 >= 0 && p[j] != '*') {
109+
f[i][j] = f[i - 1][j - 1] && (s[i] == p[j] || p[j] == '.');
110+
}
111+
112+
// 对应了 p[j] 为 '*' 的情况
113+
else if (p[j] == '*') {
114+
f[i][j] = (j - 2 >= 0 && f[i][j - 2]) || (i - 1 >= 0 && f[i - 1][j] && (s[i] == p[j - 1] || p[j - 1] == '.'));
115+
}
116+
}
117+
}
118+
return f[n][m];
119+
}
120+
}
121+
```
122+
* 时间复杂度:n 表示 s 的长度,m 表示 p 的长度,总共 $n * m$ 个状态。复杂度为 $O(n * m)$
123+
* 空间复杂度:使用了二维数组记录结果。复杂度为 $O(n * m)$
124+
125+
**动态规划本质上是枚举(不重复的暴力枚举),因此其复杂度很好分析,有多少个状态就要被计算多少次,复杂度就为多少。**
126+
127+
128+
---
129+
### 最后
130+
131+
这是我们「刷穿 LeetCode」系列文章的第 `No.10` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先将所有不带锁的题目刷完。
132+
133+
在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。
134+
135+
为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode。
136+
137+
在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。
138+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
### 题目描述
2+
3+
这是 LeetCode 上的 **[2. 两数相加](https://leetcode-cn.com/problems/add-two-numbers/solution/po-su-jie-fa-shao-bing-ji-qiao-by-ac_oie-etln/)** ,难度为 **中等**
4+
5+
Tag : 「递归」、「链表」、「数学」、「模拟」
6+
7+
8+
9+
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
10+
11+
请你将两个数相加,并以相同形式返回一个表示和的链表。
12+
13+
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
14+
15+
示例 1:
16+
17+
![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2021/01/02/addtwonumber1.jpg)
18+
19+
```
20+
输入:l1 = [2,4,3], l2 = [5,6,4]
21+
输出:[7,0,8]
22+
解释:342 + 465 = 807.
23+
```
24+
示例 2:
25+
```
26+
输入:l1 = [0], l2 = [0]
27+
输出:[0]
28+
```
29+
示例 3:
30+
```
31+
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
32+
输出:[8,9,9,9,0,0,0,1]
33+
```
34+
35+
提示:
36+
37+
* 每个链表中的节点数在范围 [1, 100]
38+
* 0 <= Node.val <= 9
39+
* 题目数据保证列表表示的数字不含前导零
40+
41+
42+
---
43+
44+
### 朴素解法(哨兵技巧)
45+
46+
这是道模拟题,模拟人工竖式做加法的过程:
47+
48+
从最低位至最高位,逐位相加,如果和大于等于 10,则保留个位数字,同时向前一位进 1 如果最高位有进位,则需在最前面补 1。
49+
50+
做有关链表的题目,有个常用技巧:添加一个虚拟头结点(哨兵),帮助简化边界情况的判断。
51+
52+
代码:
53+
```Java []
54+
class Solution {
55+
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
56+
ListNode dummy = new ListNode(0);
57+
ListNode tmp = dummy;
58+
int t = 0;
59+
while (l1 != null || l2 != null) {
60+
int a = l1 != null ? l1.val : 0;
61+
int b = l2 != null ? l2.val : 0;
62+
t = a + b + t;
63+
tmp.next = new ListNode(t % 10);
64+
t /= 10;
65+
tmp = tmp.next;
66+
if (l1 != null) l1 = l1.next;
67+
if (l2 != null) l2 = l2.next;
68+
}
69+
if (t > 0) tmp.next = new ListNode(t);
70+
return dummy.next;
71+
}
72+
}
73+
```
74+
* 时间复杂度:$m$ 和 $n$ 分别代表两条链表的长度,则遍历到的最远位置为 $max(m,n)$,复杂度为 $O(max(m,n))$
75+
* 空间复杂度:创建了 $max(m,n) + 1$ 个节点(含哨兵),复杂度为 $O(max(m,n))$(忽略常数)
76+
77+
**注意:事实上还有可能创建 $max(m + n) + 2$ 个节点,包含哨兵和最后一位的进位。但复杂度仍为 $O(max(m,n))$。**
78+
79+
---
80+
81+
### 最后
82+
83+
这是我们「刷穿 LeetCode」系列文章的第 `No.2` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先将所有不带锁的题目刷完。
84+
85+
在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。
86+
87+
为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode。
88+
89+
在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

0 commit comments

Comments
 (0)