题目目录
- Leetcode1.两数之和
- Leetcode2. 两数相加
- Leetcode3. 无重复字符的最长子串
- Leetcode4. 寻找两个正序数组的中位数
- Leetcode5. 最长回文子串
- Leetcode6. N 字形变换
- Leetcode7. 整数反转
- Leetcode8. 字符串转换整数 (atoi)
- Leetcode9. 回文数
- Leetcode10. 正则表达式匹配
- Leetcode11. 盛最多水的容器
- Leetcode12. 整数转罗马数字
- Leetcode13. 罗马数字转整数
- Leetcode14. 最长公共前缀
- Leetcode15. 三数之和
- Leetcode16. 最接近的三数之和
- Leetcode17. 电话号码的字母组合
- Leetcode18. 四数之和
- Leetcode19. 删除链表的倒数第 N 个结点
- Leetcode20. 有效的括号
Leetcode1.两数之和
思路: 因为要求的是两数之和,并且同一个元素在答案里不能重复出现,并且必定有解,所以易得出在把nums排序后
t
a
r
g
e
t
=
{
n
u
m
s
[
i
]
+
n
u
m
s
[
j
]
∣
n
u
m
s
[
i
]
≤
t
a
r
g
e
r
2
≤
n
u
m
s
[
j
]
,
i
≠
j
}
target =\{ nums[i] + nums[j] | nums[i] \le \frac{targer}{2} \le nums[j],i \neq j\}
target={nums[i]+nums[j]∣nums[i]≤2targer≤nums[j],i=j},那么我们只要定义两个指针
i
,
j
i,j
i,j,分别指向最中间的满足
n
u
m
s
[
i
]
≤
t
a
r
g
e
r
2
≤
n
u
m
s
[
j
]
,
i
≠
j
nums[i] \le \frac{targer}{2} \le nums[j],i \neq j
nums[i]≤2targer≤nums[j],i=j的位置,然后将指针向两边迁移
代码:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int l,r;
pair<int,int> pr[10005]; // first保存值,second保存在原数组中的序号
for(int i = 0;i < nums.size();i++) pr[i] = {nums[i],i};
sort(pr, pr + int(nums.size())); // 按数字排序
// 查找符合条件的l r,可以用二分写,但我比较懒(
for(int i = 0;i < nums.size();i++){
if(pr[i].first * 2 < target) l = i;
else if(pr[i].first * 2 == target){
l = i;
break;
}else break;
}
r = l + 1;
// 大了左推,小了右推
while(pr[l].first + pr[r].first != target){
if(pr[l].first + pr[r].first > target) --l;
else ++r;
}
vector<int> ans;
ans.clear();
ans.push_back(pr[l].second), ans.push_back(pr[r].second);
return ans;
}
};
Leetcode2. 两数相加
思路: 带指针的数据结构题,
d
f
s
dfs
dfs一下就行
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
int tmp = 0; // 记录进位情况
ListNode* dfs(ListNode* l1, ListNode* l2){
ListNode* ans = new ListNode(tmp);
// 处理当前位的数字
if(l1 != nullptr) ans->val += l1->val, l1 = l1->next;
if(l2 != nullptr) ans->val += l2->val, l2 = l2->next;
tmp = ans->val / 10, ans->val %= 10;
// 如果l1或者l2接下来还有数字那还得继续运行,把他们拼在当前答案后面
if(l1 != nullptr || l2 != nullptr) ans->next = dfs(l1, l2);
// 如果接下来都没数字了,那要注意进位情况
else if(tmp) ans->next = new ListNode(tmp);
return ans;
}
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
return dfs(l1, l2);
}
};
Leetcode3. 无重复字符的最长子串
思路: 滑动窗口,遍历字符串的时候下标为窗口右侧,定义一个
l
l
l为窗口左侧,题目要求的是找到不包含重复字符的连续子串,所以我们只需要保持窗口中字符串不重复。我使用了一个
m
a
p
<
c
h
a
r
,
i
n
t
>
map<char,int>
map<char,int>(用数组也行)用于存储对于一个字符而言是否存在于窗口中(0表示在窗口中不存在,1表示在窗口中有当前字符)。维护方法为:当发现当前遍历到的字符存在于窗口中,那么把窗口向右侧缩小,窗口左侧在缩小过程中遍历到的所有字符因为之前在窗口中唯一,所以缩小的时候他们就被移出窗口,该操作直到碰到与右窗口相同的字符为止。
代码:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int l = -1, ans = 0;
map<char, int> mp;
for(int i = 0;i < s.length();i++){
if(mp[s[i]] == 1){
// 移除左侧碰到的字母
while(s[++l] != s[i]) mp[s[l]] = 0;
}
mp[s[i]] = 1;
ans = max(ans, i - l);
}
return ans;
}
};
Leetcode4. 寻找两个正序数组的中位数
思路: 题目给出,两个数组均已排序,那么本题类似于归并排序合并两数组的过程,要考虑数组长度和为奇偶的情况。
代码:
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
// l为nums1下标, r为nums2下标, n为两个数组总大小
int l = -1, r = -1, n = int(nums1.size() + nums2.size());
double ans1 = 0, ans2 = 0;
// 分类讨论, 分奇偶
if(n % 2){
while((l + r + 2) * 2 < n){
// 两个数组都没用完
if(l + 1 < int(nums1.size()) && r + 1 < int(nums2.size())){
// 分类讨论, 哪个数组数字小
if(nums1[l + 1] < nums2[r + 1]) ans1 = nums1[++l];
else ans1 = nums2[++r];
}else if(l + 1 < int(nums1.size())) ans1 = nums1[++l]; // 用完了数组2
else ans1 = nums2[++r]; // 用完了数组1
}
// 便于计算答案
ans2 = ans1;
}else{
while((l + r + 2) * 2 <= n){
ans2 = ans1; // 用于记录小的那个中位数
// 两个数组都没用完
if(l + 1 < int(nums1.size()) && r + 1 < int(nums2.size())){
// 分类讨论, 哪个数组数字小
if(nums1[l + 1] < nums2[r + 1]) ans1 = nums1[++l];
else ans1 = nums2[++r];
}else if(l + 1 < int(nums1.size())) ans1 = nums1[++l]; // 用完了数组2
else ans1 = nums2[++r]; // 用完了数组1
}
}
return (ans1 + ans2) * 0.5;
}
};
Leetcode5. 最长回文子串
思路: 回文子串方法很多,数据规模是
1
e
3
1e3
1e3所以可以考虑
O
(
n
2
)
O(n^2)
O(n2)的代码,其中最好写的就是哈希,对于判断是否回文是
O
(
1
)
O(1)
O(1)的,查找回文串是
O
(
n
2
)
O(n^2)
O(n2)的。我们只需要对原串正着哈希一遍,反着也哈希一遍,对于某一段判断其是否为回文的方法就是判断这一段正着的哈希值与反着的哈希值是否相同。考虑到单哈希容易被卡的情况,这里采用了双哈希的方法(后来试了下单哈希,实际上我单哈希也没被卡)。
代码:
class Solution {
public:
// mod模数, HS哈希值, has[i]哈希值的i次方, pre前哈希, aft后哈希
const long long mod1 = 1000000007LL;
const long long mod2 = 100000007LL;
const long long HS1 = 13331LL;
const long long HS2 = 1331LL;
long long has1[1005] = {1LL}, has2[1005] = {1LL}, pre1[1005], pre2[1005], aft1[1005], aft2[1005];
// 比较HS1的情况下lr区间前后哈希值是否想通过
int comp1(int l, int r){
int tpre1 = ((pre1[r] - 1LL * pre1[l - 1] * has1[r - l + 1]) % mod1 + mod1) % mod1;
int tpre2 = ((aft1[l] - 1LL * aft1[r + 1] * has1[r - l + 1]) % mod1 + mod1) % mod1;
if(tpre1 == tpre2) return 1;
return 0;
}
// 比较HS2的情况下lr区间前后哈希值是否想通过
int comp2(int l, int r){
int tpre1 = ((pre2[r] - 1LL * pre2[l - 1] * has2[r - l + 1]) % mod2 + mod2) % mod2;
int tpre2 = ((aft2[l] - 1LL * aft2[r + 1] * has2[r - l + 1]) % mod2 + mod2) % mod2;
if(tpre1 == tpre2) return 1;
return 0;
}
string longestPalindrome(string s) {
s = " " + s; // 方便处理数组越界情况
// 预处理
for(int i = 1;i < s.length();i++){
has1[i] = has1[i - 1] * HS1 % mod1;
has2[i] = has2[i - 1] * HS2 % mod2;
pre1[i] = (pre1[i - 1] * HS1 + s[i]) % mod1;
pre2[i] = (pre2[i - 1] * HS2 + s[i]) % mod2;
aft1[s.length() - i] = (aft1[s.length() - i + 1] * HS1 + s[s.length() - i]) % mod1;
aft2[s.length() - i] = (aft2[s.length() - i + 1] * HS2 + s[s.length() - i]) % mod2;
}
string ans;
ans = ans + s[1]; // 答案至少为一个字母
// O(N^2)遍历比较
for(int i = 1;i < s.length();i++){
for(int j = i + 1;j < s.length();j++){
// 如果当前查找的区间长度小于等于答案, 那就没查找必要了
if(j - i + 1 <= ans.length()) continue;
if(comp1(i, j) && comp2(i, j)) ans = s.substr(i, j - i + 1);
}
}
return ans;
}
};
Leetcode6. N 字形变换
思路: 因为排列成N字形相当于把所有的字母分割为一个个形似于
V
V
V(竖+向右上的斜杠)的部分,那我们先将原字符串按长度为
(
n
−
1
)
∗
2
(n-1)*2
(n−1)∗2进行分割,对其每一位的行号进行处理,按顺序添加到每行答案序列,最后把每行答案按行号拼接
代码:
class Solution {
public:
string convert(string s, int numRows) {
// 存储每行从左到右答案
string ans[1005];
// 需要特判一下行数为1的时候的情况(直接输出)
if(numRows == 1) return s;
// V字形所包含的字符数量
int siz = (numRows - 1) * 2;
for(int i = 0;i < (s.length() + siz - 1) / siz;++i){
for(int j = 0;j < siz && i * siz + j < s.length();++j){
if(j < numRows) ans[j] = ans[j] + s[i * siz + j]; // 处于竖的情况
else ans[siz - j] = ans[siz - j] + s[i * siz + j]; // 处于斜的情况
}
}
string result = "";
for(int i = 0;i < numRows;++i) result = result + ans[i];
return result;
}
};
Leetcode7. 整数反转
思路: 第一眼直接转为
l
o
n
g
l
o
n
g
long long
longlong判结果是否小于
−
2147483648
-2147483648
−2147483648或大于
2147483647
2147483647
2147483647,然后发现假设环境不允许存储 64 位整数(有符号或无符号)。那就是用个
s
t
r
i
n
g
string
string存结果,注意判断正数上界是
2147483647
2147483647
2147483647而负数下界是
−
2147483648
-2147483648
−2147483648。如果当前数字长度小于上界长度,那么必然不会越界,不用考虑。如果数字长度相同,那么如果当前位置(假设下标为
l
l
l)的数字
s
[
l
]
s[l]
s[l]比上界对应位置数字
c
m
p
[
l
]
cmp[l]
cmp[l]小,那么从
l
l
l开始到结尾的数字必然满足
s
[
l
]
s
[
l
+
1
]
.
.
.
s
[
s
.
l
e
n
g
t
h
(
)
−
1
]
<
c
m
p
[
l
]
c
m
p
[
l
+
1
]
.
.
.
c
m
p
[
s
.
l
e
n
g
t
h
(
)
−
1
]
s[l]s[l+1]...s[s.length()-1] < cmp[l]cmp[l+1]...cmp[s.length()-1]
s[l]s[l+1]...s[s.length()−1]<cmp[l]cmp[l+1]...cmp[s.length()−1]。同理,如果当前位置的数字比上界对应位置数字大,那么从
l
l
l开始到结尾的数字必然满足
s
[
l
]
s
[
l
+
1
]
.
.
.
s
[
s
.
l
e
n
g
t
h
(
)
−
1
]
>
c
m
p
[
l
]
c
m
p
[
l
+
1
]
.
.
.
c
m
p
[
s
.
l
e
n
g
t
h
(
)
−
1
]
s[l]s[l+1]...s[s.length()-1] > cmp[l]cmp[l+1]...cmp[s.length()-1]
s[l]s[l+1]...s[s.length()−1]>cmp[l]cmp[l+1]...cmp[s.length()−1]。如果相等,那么大小情况取决于后方的大小情况。所以我们从后往前,维护一个大小标记
g
r
gr
gr用于判断到当前位置大小关系。
主要问题是不能用
l
o
n
g
l
o
n
g
long long
longlong,需要注意溢出的情况。
代码:
class Solution {
public:
int reverse(int x) {
string s = "", cmp = "2147483647";
int f = 0, ans = 0; // f为符号位, 0为正, 1为负 ans为结果数字串
if(x < 0) f = 1, cmp[cmp.length() - 1] = '8'; // 如果为负数, 那么数字下界为-2147483648
while(x) s = s + char(abs(x % 10) + '0'), x /= 10; // 注意加abs(), 否则x为负数时会出错
int gr = 0; // 用于判断s表示的数字是否大于cmp表示的数字的tag
if(s.length() == cmp.length()){
for(int i = s.length() - 1;i >= 0;i--){
if(s[i] < cmp[i]) gr = 0; // 小于必然小
else if(s[i] > cmp[i]) gr = 1; // 大于必然大
}
}
if(gr == 0) for(int i = 0;i < s.length();i++) ans = ans * 10 + (s[i] - '0');
if(f) ans = -ans;
return ans;
}
};
Leetcode8. 字符串转换整数 (atoi)
思路: 按照题意模拟即可
代码:
class Solution {
public:
int myAtoi(string s) {
int l = 0,f = 1;
// 去除前导空格
while(l < s.length() && s[l] == ' ') ++l;
// 处理符号问题
if(l < s.length() && s[l] == '-') f = -1, ++l;
else if(l < s.length() && s[l] == '+') ++l;
// 把答案存在ans, long long防溢出
long long ans = 0;
while(l < s.length() && s[l] >= '0' && s[l] <= '9'){
ans = ans * 10 + (s[l] - '0');
if(f == 1 && ans >= 2147483648){ // 正数溢出情况
ans = 2147483647;
break;
}else if(f == -1 && ans > 2147483648){ // 负数溢出形式
ans = 2147483648;
break;
}
++l;
}
return int(ans * f);
}
};
Leetcode9. 回文数
思路: 双指针,特判一下数字为负数的情况,负数必为
f
a
l
s
e
false
false。数字转为数组,双指针一个从前往后扫,一个从后往前扫。
代码:
class Solution {
public:
bool isPalindrome(int x) {
if(x < 0) return false; // 特判x为负数情况
if(x == 0) return true; // 特判x为0情况
int num[40];
int l = 0, r = -1;
while(x) num[++r] = abs(x % 10), x /= 10; // 创建相应数字数组(反)
while(l < r){
if(num[l] != num[r]) return false;
++l, --r;
}
return true;
}
};
Leetcode10. 正则表达式匹配
思路: 动态规划。对于字符串匹配而言,可以用动态规划的思想,比如当前原字符串
s
s
s匹配到的位置是
i
i
i,正则表达式字符串
p
p
p匹配到的位置是
j
j
j,即
s
.
s
u
b
s
t
r
(
0
,
i
+
1
)
∼
p
.
s
u
b
s
t
r
(
0
,
j
+
1
)
s.substr(0, i + 1) \sim p.substr(0, j + 1)
s.substr(0,i+1)∼p.substr(0,j+1),那么如果
s
[
i
+
1
]
=
=
p
[
j
+
1
]
s[i+1]==p[j+1]
s[i+1]==p[j+1]则
s
.
s
u
b
s
t
r
(
0
,
i
+
2
)
∼
p
.
s
u
b
s
t
r
(
0
,
j
+
2
)
s.substr(0, i + 2) \sim p.substr(0, j + 2)
s.substr(0,i+2)∼p.substr(0,j+2)以此类推。
本题有如下几种情况:
- 当前字符
s
[
i
]
=
=
p
[
j
]
s[i]==p[j]
s[i]==p[j]或者
p
[
j
]
=
=
p[j]==
p[j]==‘
.
.
.’
例如 { s = \{s= {s=‘ a a b aab aab’ , p = ,p= ,p=‘ a a aa aa’ , i = 1 , j = 1 } ,i=1,j=1\} ,i=1,j=1},此时的状态取决于 s = s= s=‘ a a a’ , p = ,p= ,p=' a a a’的状态。那么当前状态 d p [ i ] [ j ] dp[i][j] dp[i][j]取决于不包含 s [ i ] , p [ j ] s[i],p[j] s[i],p[j]的状态,即 d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] dp[i][j]=dp[i - 1][j - 1] dp[i][j]=dp[i−1][j−1]。 -
p
[
j
]
=
=
p[j]==
p[j]==‘
∗
*
∗’
2.1 如果 s [ i ] = = p [ j − 1 ] s[i]==p[j - 1] s[i]==p[j−1]或者 p [ j − 1 ] = = p[j - 1]== p[j−1]==‘ . . .’:
例1: { s = \{s= {s=‘ a a c c aacc aacc’ , p = ,p= ,p=‘ a a c c c ∗ aaccc^* aaccc∗’ , i = 3 , j = 5 } ,i=3,j=5\} ,i=3,j=5},此时匹配的字符串是 s = s= s=‘ a a c c aacc aacc’ , p = ,p= ,p=‘ a a c c aacc aacc’( p p p舍弃了 c ∗ c^* c∗,无效匹配)。那么当前状态 d p [ i ] [ j ] dp[i][j] dp[i][j]可以从不包含 p [ j ] p[j] p[j]的情况推导出来(直接忽略 p [ j ] p[j] p[j]),即 d p [ i ] [ j ] = d p [ i ] [ j − 2 ] dp[i][j]=dp[i][j - 2] dp[i][j]=dp[i][j−2]。
例2: { s = \{s= {s=‘ a a c aac aac’ , p = ,p= ,p=‘ a a c ∗ aac^* aac∗’ , i = 3 , j = 3 } ,i=3,j=3\} ,i=3,j=3},此时匹配的字符串是 s = s= s=‘ a a aa aa’ , p = ,p= ,p=‘ a a aa aa’( s s s舍弃了 c c c, p p p舍弃了 c ∗ c* c∗,匹配一次)。那么当前状态 d p [ i ] [ j ] dp[i][j] dp[i][j]可以从不包含 s [ i ] , p [ j − 1 ] p [ j ] s[i],p[j-1]p[j] s[i],p[j−1]p[j]的情况推导出来(直接忽略 s [ i ] p [ j − 1 ] p [ j ] s[i]p[j-1]p[j] s[i]p[j−1]p[j]),即 d p [ i ] [ j ] = d p [ i − 1 ] [ j − 2 ] dp[i][j]=dp[i-1][j - 2] dp[i][j]=dp[i−1][j−2]。
例3: { s = \{s= {s=‘ a a c c c c c aaccccc aaccccc’ , p = ,p= ,p=‘ a a c ∗ aac^* aac∗’ , i = 6 , j = 3 } ,i=6,j=3\} ,i=6,j=3},此时匹配的字符串是 s = s= s=‘ a a c c c c aacccc aacccc’ , p = ,p= ,p=‘ a a c ∗ aac* aac∗’( s s s舍弃了 c c c,匹配多次)。那么当前状态 d p [ i ] [ j ] dp[i][j] dp[i][j]可以从不包含 s [ i ] s[i] s[i]的情况推导出来(直接忽略 s [ j ] s[j] s[j]),即 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j]=dp[i-1][j] dp[i][j]=dp[i−1][j]。
2.2 如果 s [ i ] ≠ p [ j − 1 ] s[i] \ne p[j - 1] s[i]=p[j−1]
例子: { s = \{s= {s=‘ a a c aac aac’ , p = ,p= ,p=‘ a a c b ∗ aacb^* aacb∗’ , i = 2 , j = 4 } ,i=2,j=4\} ,i=2,j=4},此时匹配的字符串是 s = s= s=‘ a a c aac aac’ , p = ,p= ,p=‘ a a c aac aac’( p p p舍弃了 b ∗ b^* b∗,无效匹配)。那么当前状态 d p [ i ] [ j ] dp[i][j] dp[i][j]可以从不包含 p [ j ] p[j] p[j]的情况推导出来(直接忽略 p [ j − 1 ] p [ j ] p[j-1]p[j] p[j−1]p[j]),即 d p [ i ] [ j ] = d p [ i ] [ j − 2 ] dp[i][j]=dp[i][j - 2] dp[i][j]=dp[i][j−2]。
代码:
class Solution {
public:
bool isMatch(string s, string p) {
bool dp[30][30];
memset(dp, false, sizeof(dp)); // 记得清空数组
dp[0][0] = true;
for(int i = 1;i < p.length();i++) dp[0][i] = p[i - 1] == '*' ? dp[0][i - 2] : dp[0][i]; // 考虑例如a*匹配到空的情况
// 主体部分看上面的思路
for(int i = 0;i < s.length();i++){
for(int j = 0;j < p.length();j++){
if(s[i] == p[j] || p[j] == '.') dp[i + 1][j + 1] = dp[i][j];
if(p[j] == '*'){
dp[i + 1][j + 1] = dp[i + 1][j - 1];
if(s[i] == p[j - 1] || p[j - 1] == '.') dp[i + 1][j + 1] = dp[i + 1][j - 1] || dp[i][j - 1] || dp[i][j + 1];
}
}
}
return dp[int(s.length())][int(p.length())];
}
};
Leetcode11. 盛最多水的容器
思路: 首先要理解正确题意,题目是寻找左右两个垂线
L
l
,
L
r
L_l,Lr
Ll,Lr,使得他们的面积
(
r
−
l
)
∗
m
i
n
(
L
l
,
L
r
)
(r-l) *min(L_l,L_r)
(r−l)∗min(Ll,Lr)最大(刚开始看错题目了,以为是中间挡板也存在,思考怎么维护最近的比自己高的挡板)。
那么思路就是双指针,指针
l
,
r
l,r
l,r分别指向选中的左垂线
L
l
L_l
Ll右垂线
L
r
L_r
Lr。由于每次将长垂线向短垂线移动的时候,
m
i
n
(
L
l
,
L
r
)
min(L_l,L_r)
min(Ll,Lr)不变或者变小,
(
r
−
l
)
(r-l)
(r−l)变小,所以移动长垂线面积必然是一个单调递减。由于每次将短垂线向长垂线移动的时候,
m
i
n
(
L
l
,
L
r
)
min(L_l,L_r)
min(Ll,Lr)变小变大不变都有可能,
(
r
−
l
)
(r-l)
(r−l)变小,所以移动短垂线比移动长垂线更优。那么我们每次移动短垂线,维护一个最大面积,最后返回这个面积就行。
代码:
class Solution {
public:
int maxArea(vector<int>& height) {
int l = 0, r = height.size() - 1, ans = 0; // l,r指针
while(l < r){
ans = max(ans, (r - l) * (height[l] < height[r] ? height[l] : height[r])); // 维护答案
if(height[l] < height[r]) ++l; // 移动短垂线指针
else --r;
}
return ans;
}
};
Leetcode12. 整数转罗马数字
思路: 从大到小模拟即可
代码:
class Solution {
public:
string intToRoman(int num) {
string ans = "";
while(num >= 1000) num -= 1000, ans = ans + 'M';
if(num >= 900) num -= 900, ans = ans + "CM";
while(num >= 500) num -= 500, ans = ans + 'D';
if(num >= 400) num -= 400, ans = ans + "CD";
while(num >= 100) num -= 100, ans = ans + 'C';
if(num >= 90) num -= 90, ans = ans + "XC";
while(num >= 50) num -= 50, ans = ans + 'L';
if(num >= 40) num -= 40, ans = ans + "XL";
while(num >= 10) num -= 10, ans = ans + 'X';
if(num >= 9) num -= 9, ans = ans + "IX";
while(num >= 5) num -= 5, ans = ans + 'V';
if(num >= 4) num -= 4, ans = ans + "IV";
while(num >= 1) num -= 1, ans = ans + 'I';
return ans;
}
};
Leetcode13. 罗马数字转整数
思路: 按题意模拟即可
代码:
class Solution {
public:
int romanToInt(string s) {
int ans = 0;
for(int i = 0;i < s.length();i++){
if(s[i] == 'M') ans += 1000;
else if(s[i] == 'D') ans += 500;
else if(s[i] == 'C'){
if(i + 1 < s.length()){
if(s[i + 1] == 'M') ++i, ans += 900;
else if(s[i + 1] == 'D') ++i, ans += 400;
else ans += 100;
}else ans += 100;
}else if(s[i] == 'L') ans += 50;
else if(s[i] == 'X'){
if(i + 1 < s.length()){
if(s[i + 1] == 'C') ++i, ans += 90;
else if(s[i + 1] == 'L') ++i, ans += 40;
else ans += 10;
}else ans += 10;
}else if(s[i] == 'V') ans += 5;
else if(s[i] == 'I'){
if(i + 1 < s.length()){
if(s[i + 1] == 'X') ++i, ans += 9;
else if(s[i + 1] == 'V') ++i, ans += 4;
else ans += 1;
}else ans += 1;
}
}
return ans;
}
};
Leetcode14. 最长公共前缀
思路: 哈希判断字符串到当前位置是否相同。使用
l
o
n
g
l
o
n
g
long long
longlong避免溢出造成的问题。因为如果到当前哈希值相同那么就可以认为是相同的(不过好像直接判断同一个位置,当前字符串的字符与上一字符串的字符是否相同就好了,都用不到哈希。。。)。
代码:
class Solution {
public:
string longestCommonPrefix(vector<string>& strs) {
// 特判
if(int(strs.size()) == 0) return "";
if(int(strs.size()) == 1) return strs[0];
const int N = 205;
long long pre[N], has = 1331, mod = 1000000007;
for(int i = 0;i < strs.size();i++) pre[i] = 0;
string ans = "";
int end_flag = 1, same_flag = 1; // 结束的标记, 出现相同的标记
for(int i = 0;end_flag && same_flag;i++){
for(int j = 0;j < strs.size() && same_flag;j++){
if(strs[j].length() == 0) return "";
pre[j] = (pre[j] * has + strs[j][i]) % mod;
if(j && pre[j] != pre[j - 1]) same_flag = 0;
if(int(strs[j].length()) == i + 1) end_flag = 0;
}
if(same_flag) ans = ans + strs[0][i];
}
return ans;
}
};
Leetcode15. 三数之和
思路1: 题目要求的是
a
n
s
=
{
[
n
u
m
[
i
]
,
n
u
m
[
j
]
,
n
u
m
[
k
]
]
∣
n
u
m
[
i
]
+
n
u
m
[
j
]
=
n
u
m
[
k
]
,
k
<
i
<
j
}
ans=\{[num[i],num[j],num[k]]|num[i]+num[j]=num[k],k <i<j\}
ans={[num[i],num[j],num[k]]∣num[i]+num[j]=num[k],k<i<j},相当于问,对于
(
n
u
m
[
j
]
+
n
u
m
[
k
]
,
j
<
k
)
(num[j]+num[k],j < k)
(num[j]+num[k],j<k),找到是否存在
i
i
i,使得
i
i
i满足
(
n
u
m
[
i
]
=
n
u
m
[
j
]
+
n
u
m
[
k
]
,
i
<
j
)
(num[i]=num[j]+num[k],i<j)
(num[i]=num[j]+num[k],i<j)。为了加快速度没用
m
a
p
map
map映射,使用数组加上一个偏移值。
代码1:
class Solution {
public:
const int N = 400005;
const int add = 200000;
vector<vector<int>> threeSum(vector<int>& nums) {
int mp[N];
memset(mp, 0, sizeof(mp)); // 一定要记得memset, 这个地方我又错了...
set<vector<int>> ans; // 存储答案
vector<int> ins; // 存储暂时插入值
int tmp;
for(int j = 1;j < nums.size();j++){
mp[nums[j - 1] + add]++; // 统计前方num[i]的个数
for(int k = j + 1;k < nums.size();k++){
tmp = add - (nums[j] + nums[k]);
if(mp[tmp]){ // 如果满足条件则添加
ins = {tmp - add, nums[j], nums[k]};
sort(ins.begin(), ins.end()); // 用于去重
ans.insert(ins);
}
}
}
vector<vector<int>> result;
result.assign(ans.begin(), ans.end()); // set转vecor
return result;
}
};
思路2: 我自己写的方法主体复杂度是
O
(
N
2
)
O(N^2)
O(N2),但是使用set进行排序还有一个
l
o
g
(
n
)
log(n)
log(n)的时间复杂度,所以总的来说是
O
(
N
2
l
o
g
(
n
)
)
O(N^2log(n))
O(N2log(n))的时间复杂度,提交发现运行速度击败了
5.00
%
5.00\%
5.00%的用户于是学习了官方题解。
由于我们的目的仅仅是找到三个数相加等于
0
0
0,先后顺序并不重要,所以可以直接对原数组进行排序。对原数组进行排序后能发现,当我们循环遍历
(
n
u
m
[
i
]
,
n
u
m
[
j
]
)
(num[i],num[j])
(num[i],num[j])的时候,固定
i
i
i移动
j
j
j,
(
n
u
m
[
i
]
+
n
u
m
[
j
]
)
(num[i]+num[j])
(num[i]+num[j])必然是呈单调递增态势,那么
k
k
k必然是不断向左移动,那么相当于我们循环遍历
i
i
i,对于每一个
i
i
i,设置双指针
l
,
r
l,r
l,r,分别指向
i
+
1
i+1
i+1以及数组最后位置
n
u
m
s
.
s
i
z
e
(
)
−
1
nums.size()-1
nums.size()−1,每次先移动r后移动l,当
n
u
m
[
i
]
+
n
u
m
[
l
]
+
n
u
m
[
r
]
=
0
num[i]+num[l]+num[r]=0
num[i]+num[l]+num[r]=0的时候将其添加入答案。需要注意的是,每次移动(i,l,r),都要保证他们所在位置的值与上次的值不同,例如
n
u
m
[
i
]
≠
n
u
m
[
i
−
1
]
num[i] \ne num[i-1]
num[i]=num[i−1]。由于对于每个num[i]而言,在
n
u
m
[
l
]
num[l]
num[l]不会重复的时候,
n
u
m
[
r
]
num[r]
num[r]必然也不会重复,则答案不会重复,无需去重。
代码2:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(), nums.end());
vector<vector<int>> ans;
for(int i = 0;i < nums.size();i++){
if(i && nums[i - 1] == nums[i]) continue; // 维护num[i]唯一性
int l = i + 1, r = nums.size() - 1;
while(l < r){
if(l > i + 1) while(l < r && nums[l] == nums[l - 1]) ++l; // 维护num[l]唯一性
while(l < r && nums[i] + nums[l] + nums[r] > 0) --r; // 寻找答案
if(l < r && nums[i] + nums[l] + nums[r] == 0) ans.insert(ans.end(), {nums[i], nums[l], nums[r]});
++l;
}
}
return ans;
}
};
Leetcode16. 最接近的三数之和
思路: 和上一题差不多,只不过上一题要求的是三个数相加为
0
0
0,这道题可以看做是三个数相加为
t
a
r
g
e
t
target
target的近似值。那么还是双指针,将原数列排序(为什么能排序可以参考上一题的思路2),滚动第一维数组,对于每个第一维的
n
u
m
s
[
i
]
nums[i]
nums[i]而言,定义指针
l
,
r
l,r
l,r,如果三个数相加大于
t
a
r
g
e
t
target
target则将右指针左移,否则左指针右移。如果三个数之和与target的差值小于之前找到的数与
t
a
r
g
e
t
target
target的差值,则更新答案。时间复杂度
O
(
N
2
)
O(N^2)
O(N2)。
代码:
class Solution {
public:
int threeSumClosest(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
int ans = 1e8, tmp; // 初始化答案
for(int i = 0;i < nums.size() && ans - target;i++){ // 如果答案与target相同那就没继续遍历的必要性了
int l = i + 1,r = nums.size() - 1; // 定义双指针
while(l < r){
tmp = nums[i] + nums[l] + nums[r];
if(abs(tmp - target) < abs(ans - target)) ans = tmp; // 更新答案
if(tmp - target > 0) --r; // 大了左移
else if(tmp - target < 0) ++l; // 小了右移
else break;
}
}
return ans;
}
};
Leetcode17. 电话号码的字母组合
思路: 要求对于某个串数字的所有可能字母组合。对于某一串字母而言,当前可能的组合是去掉首数字后剩下的数字串的字母组合与首数字的字母组合的组合,例如:数字串
234
234
234的字母组合是数字串
2
2
2的组合
[
a
,
b
,
c
]
[a,b,c]
[a,b,c]与数字串
34
34
34的字母组合
[
d
g
,
d
h
,
d
i
,
e
g
,
e
h
,
e
i
,
f
g
,
f
h
,
f
i
]
[dg,dh,di,eg,eh,ei,fg,fh,fi]
[dg,dh,di,eg,eh,ei,fg,fh,fi]的叉乘:
[
a
d
g
,
a
d
h
,
a
d
i
,
a
e
g
,
a
e
h
,
a
e
i
,
a
f
g
,
a
f
h
,
a
f
i
,
b
d
g
,
b
d
h
,
b
d
i
,
b
e
g
,
b
e
h
,
b
e
i
,
b
f
g
,
b
f
h
,
b
f
i
,
c
d
g
,
c
d
h
,
c
d
i
,
c
e
g
,
c
e
h
,
c
e
i
,
c
f
g
,
c
f
h
,
c
f
i
]
[adg,adh,adi,aeg,aeh,aei,afg,afh,afi,bdg,bdh,bdi,beg,beh,bei,bfg,bfh,bfi,cdg,cdh,cdi,ceg,ceh,cei,cfg,cfh,cfi]
[adg,adh,adi,aeg,aeh,aei,afg,afh,afi,bdg,bdh,bdi,beg,beh,bei,bfg,bfh,bfi,cdg,cdh,cdi,ceg,ceh,cei,cfg,cfh,cfi]。
本题数字串长度最大是
4
4
4,甚至可以直接四个
f
o
r
for
for判断长度是否越界就行。。。
我用的递归,长串的结果由短串变化而得(不过我是从后往前写的,写完之后发现从前往后只要
f
o
r
for
for就行了,不需要递归)。
代码1(从后往前的递归):
class Solution {
public:
string s[10] = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
vector<string> letterCombinations(string digits) {
vector<string> ans, tmp; // 记录答案, 记录删除首数字后的答案
ans.clear(), tmp.clear();
if(digits.length() == 0) return ans; // 空串返回空值
int num = digits[0] - '0'; // 存储当前数字
if(digits.length() == 1){ // 如果数字串长度为1需要特判
for(int i = 0;i < s[num].length();i++) ans.push_back(s[num].substr(i, 1));
}else{
tmp = letterCombinations(digits.substr(1, digits.length() - 1)); // 获得删除首字母后的答案
for(int i = 0;i < s[num].length();i++){
for(int j = 0;j < tmp.size();j++) ans.push_back(s[num].substr(i, 1) + tmp[j]); // 将首字母的组合与删除首字母后的答案进行组合
}
}
return ans;
}
};
代码2(从前往后for):
class Solution {
public:
string s[10] = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
vector<string> letterCombinations(string digits) {
vector<string> ans, tmp;
if(digits.length() == 0) return ans; // 空串返回空值
for(int i = 0;i < digits.length();i++){
int num = digits[i] - '0'; // 存储当前数字
tmp = ans; // 将digits.substr(0,i)的答案转移到tmp备用
ans.clear();
if(tmp.size() == 0){ // 如果被转移的答案长度为0, 那么说明是第一个数字, 特判这种情况(答案直接为当前数字所在的字母组合)
for(int j = 0;j < s[num].length();j++) ans.push_back(s[num].substr(j, 1));
}else{
for(int k = 0;k < tmp.size();k++){
for(int j = 0;j < s[num].length();j++) ans.push_back(tmp[k] + s[num].substr(j, 1)); // 组合答案
}
}
}
return ans;
}
};
Leetcode18. 四数之和
思路: 和前面的两道三数之和很像,只不过多了一个维度。三数和是固定一个维度,另外两个维度用双指针维护,将
O
(
N
2
)
O(N^2)
O(N2)优化为
O
(
N
)
O(N)
O(N),相当于去掉了一重循环。那么本题四数和按照三数和的思路,可以先排序,然后固定其中两个维度,将另外两个维度用双指针维护,时间复杂度为
O
(
N
3
)
O(N^3)
O(N3),由于数据规模为
2
e
2
2e2
2e2,所以这个复杂度是可以被接受的。
在遍历固定的两个维度以及双指针指针移动的时候,要注意去重,方法是不要在同一个指针两次遍历相同的数字。
在四数和相加的时候要注意处理溢出的情况。
代码:
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
sort(nums.begin(), nums.end()); // 排序
vector<vector<int>> ans; // 记录答案
for(int i = 0;i < nums.size();i++){
if(i && nums[i] == nums[i - 1]) continue; // 维护第一维数字不重复
for(int j = nums.size() - 1;j > i;j--){
if(j != (nums.size() - 1) && nums[j] == nums[j + 1]) continue; // 维护第二维数字不重复
int l = i + 1,r = j - 1; // l为左指针 r为右指针
while(l < r){
// 维护左指针数字不重复
if(l != i + 1 && nums[l] == nums[l - 1]){
++l;
continue;
}
// 维护右指针数字不重复
if(r != j - 1 && nums[r] == nums[r + 1]){
--r;
continue;
}
long long tmp = 0LL + nums[l] + nums[r] + nums[i] + nums[j]; // 计算和, 用long long处理溢出的情况
if(tmp < target) ++l; // 小了右移
else if(tmp > target) --r; // 大了左移
else{
ans.push_back({nums[i], nums[l], nums[r], nums[j]});
++l, --r;
}
}
}
}
return ans;
}
};
Leetcode19. 删除链表的倒数第 N 个结点
思路: dfs,回溯的时候如果发现当前节点是倒数第N个节点,那么将其删除
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
int tmp = 0; // 统计层数
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* result = nullptr; // 答案变量, 初始化为nullptr
if(head->next != nullptr) result = removeNthFromEnd(head->next, n); // 遇到一个ListNode*, 把这个ListNode*除首数字以外的所有数字扔去dfs(相当于不断删掉第一个数字)
++tmp; // 统计当前层数
if(tmp == n) return result; // 层数正确删除(返回后面的序列相当于不管当前数字, 相当于删除当前数字)
head->next = result; // 层数不正确, 将当前数字添加到后面的序列的首项
return head;
}
};
Leetcode20. 有效的括号
思路:
有如下几条性质:
- 如果出现了一个右括号,那么在他之前一定需要有一个左括号存在。
- 一个左括号只能匹配一个右括号。
- 只有两个括号之间是个有效括号串,那么这两个括号才可能形成一个有效括号串(例如 ‘[(){}]’ 中间的 ‘(){}’ 是个有效括号串,那么当 ‘[’ 与 ‘]’ 匹配,‘[(){}]’ 才是个有效括号串)。那么当一串括号串有效的时候,我们可以将其删除(匹配了就不影响之后的匹配操作)。
- 一个括号只能匹配与他同种类的括号(例如 ‘[’ 仅能匹配 ‘]’ )。
那我们可以用栈来维护这个括号串,如果是左括号,直接入栈即可(因为左括号不用考虑左侧其他括号对他的匹配情况)。如果是右括号,当栈为空的时候,必定无法匹配;当栈顶为其他类型括号的时候,必定无法匹配(比如栈中从底到顶为 ‘[(’ ,那么当匹配到 ‘]’ 的时候,栈顶为 ‘(’ ,即使前面有能与之匹配的括号,最终也会形成 ‘[(]’ 这个不合法括号串)。如果遍历结束,发现栈不为空,必定不是有效括号串(例如 ‘[((()){}’ 最后会留下’[(’ 在栈中)。
代码:
class Solution {
public:
bool isValid(string s) {
stack<char> st; // 栈维护
while(!st.empty()) st.pop();
for(int i = 0;i < s.length();i++){
if(s[i] == '(' || s[i] == '[' || s[i] == '{') st.push(s[i]); // 左括号直接入栈
else if(st.empty()) return false; // 右括号想入栈, 发现栈为空
// 括号类型不匹配
else if(s[i] == ')' && st.top() != '(') return false;
else if(s[i] == ']' && st.top() != '[') return false;
else if(s[i] == '}' && st.top() != '{') return false;
else st.pop(); // 匹配到了合法括号, 那么将这两个匹配到的括号删除
}
if(!st.empty()) return false; // 最终栈不为空则不合法
return true;
}
};