1. 两数之和
分析
返回下标
可以暴力枚举下, 两重循环, 如果和 = target, 就返回 O(n^2)
双指针的话, 需要先排序, 排序O(nlogn)
想下有没有O(n)的算法
每次枚举第2个数(s[i]), 去考虑 第2个数前面有没有数 + s[i] = target,
其实就是问前面是否存在一个数 = target - s[i]
map找一个数是log(n), 底层是平衡树, 无序的unordered_map 找数的话是O(1), 底层是hash表
从前往后扫描, 每扫描一个数, 就将该数放到hash表中, 当我们扫描到
s
i
s_i
si的时候, 已经将
s
i
s_i
si的所有数放到hash表中, 当访问到
s
i
s_i
si的时候, 看一下hash表中是否有数 = target - s[i]
每个数最多会往hash表中插入1次, 每个数会查询hash表1次, hash表插入和查询操作都是O(1), 所以总共是O(n)
code
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> hash;// 1.当前数的值 2.当前数的下标
for (int i = 0; i < nums.size(); i ++ ){
int another = target - nums[i]; // 找当前数的前面是否存在解
// 由于当前数前面的数, 都放到hash表里了, 直接用hash.count()查询
if (hash.count(another)) return {hash[another], i}; // 如果存在, 则直接返回答案
hash[nums[i]] = i; // 否则, 将当前数插入到hash表 key = 当前数的值, value = 下标
}
return {};
}
};
2. 两数相加
分析
模拟加法, 高精度加法模板类似
code
/**
* 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:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
auto dummy = new ListNode(-1), tail = dummy;
int t = 0;
while (l1 || l2 || t) {
if (l1) t += l1->val, l1 = l1->next;
if (l2) t += l2->val, l2 = l2->next;
tail = tail->next = new ListNode(t % 10);
t /= 10;
}
return dummy->next;
}
};
3. 无重复字符的最长子串
分析
考虑该题的时候, 需要思考怎么能将所有情况都枚举到, 所有子串大概n^2 / 2种(两个端点, 端点有序, 即左端点可以[1,k]选k种, 由端点[k + 1, n]中选, n - k种)
需要在n^2/ 2种方案种, 找到无重复的子串
以i
(表示子串的右端点) 为分类依据, 将子串分成n类, 那么问题就变成固定了i
, 找最靠左的j
, 使得[j, i]不包含重复字符
如果暴力枚举j
话, 总的时间复杂度外层要套i
的循环, O(n^2)
考虑下单调性
证明
所以当后指针i
往后移动时候, 前指针j
至多也是原地不动, 不可能往前移动.
这样的话, 每次i
往后移动到i'
的时候, 新前指针j'
可以从上一个j
的位置, 继续往后枚举就可以了, 这样相当于不会走回头路, 因此每个指针最多只会走n次, 加在一块的话,就是O(n)的算法
然后用hash表来维护[j, i]每个字符出现的次数
i
向后移动一格的时候, 变成i + 1
, 就将移动后的当前字母加入到hash表中
然后看下当前hash表中是否有重复元素, 如果有重复元素的话, 那么必然是hash[s[i + 1]]
(程序中应该是hash[s[i]]), 然后将j
一直往后移动, 直到移动到直到i +1
为止, 那么就只剩i + 1
一个元素了, 就满足要求了
双指针, i
为后指针, j
为前指针
当hash[i] > 1
说明s[i]
元素在前面重复了, 前指针j
指针需要后移, 找到不重复的位置
联动题
LeetCode 76. 最小覆盖子串
LeetCode 30. 串联所有单词的子串
code
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_map<char, int> hash;
int res = 0;
for (int i = 0, j = 0; i < s.size(); i ++ ){
hash[s[i]] ++;// 将当前后指针i 对应的字符记录到hash表中
while (hash[s[i]] > 1) hash[s[j ++ ]] --; // i为后指针, 当后指针遇到的字符串 > 1, 那么表示当前段末尾字符重复, 前指针后移
res = max(res, i - j + 1);
}
return res;
}
};
4. 寻找两个正序数组的中位数
分析
有简单的做法, 两个数组合并到一起sort, 然后取中位数O(m+nlog(m + n))
1.递归的方式O(log(n + m))
2.二分log(min(m,n)) ❌(非常不推荐, 边界十分复杂)
求一下两个数组的第k
的元素, 让k = (m + n) / 2, 那么就得到答案了
为了解决该问题, 尽可能的分解成子问题来解决
先看下两个数组k/2
位置元素
分两种情况来看
-
A [ k 2 ] < B [ k 2 ] A[\frac{k}{2}] < B[\frac{k}{2}] A[2k]<B[2k]
此时B[1, k/2]中严格< A [ k 2 ] A[\frac{k}{2}] A[2k]的元素个数至多为 k 2 − 1 \frac{k}{2} -1 2k−1个, 因为最后一个数 A [ k 2 ] < B [ k 2 ] A[\frac{k}{2}] < B[\frac{k}{2}] A[2k]<B[2k], 所以两个数组就[1, k/2]这段合在一起严格< A [ k 2 ] A[\frac{k}{2}] A[2k]的元素个数<k个.
并且因为 A [ k 2 ] < B [ k 2 ] A[\frac{k}{2}] < B[\frac{k}{2}] A[2k]<B[2k], 所以A[1, k/2]中所有元素不能存在第k个数(至少有个 B [ k 2 ] B[\frac{k}{2}] B[2k]卡着呢), 因此可以直接删除 A [ 1 , k / 2 ] A[1, k/2] A[1,k/2]中所有数(删掉k/2个元素), 来缩小搜索范围.
-
A [ k 2 ] > B [ k 2 ] A[\frac{k}{2}] > B[\frac{k}{2}] A[2k]>B[2k]
同理, 第k个数一定不在 B [ 1 , k / 2 ] B[1, k/2] B[1,k/2]区间内(至少有个 A [ k / 2 ] A[k/2] A[k/2]卡着), 因此可以删除. -
A [ k 2 ] = B [ k 2 ] A[\frac{k}{2}] = B[\frac{k}{2}] A[2k]=B[2k]
这种情况的话, 最好了, 因为直接就找到答案了, 答案就是 A [ k / 2 ] A[k/2] A[k/2]或者 B [ k / 2 ] B[k/2] B[k/2], 因此两段随便删除哪一段均可以
因此, 可以发现每次比较, 一定可以从中删除
k
/
2
k/2
k/2个元素. 假如删除的是
B
[
1
,
k
/
2
]
B[1, k/2]
B[1,k/2]区间中的所有元素, 那么问题就转化为在红色区间, 找第
k
−
k
/
2
k - k/2
k−k/2个元素
所以我们发现, 每次k/2, 当k==1的时候, 比较下两个数组的开头元素较小者即可.
k每次/2, 最多只会递归
log
k
\log k
logk次,
k
=
(
m
+
n
)
/
2
k = (m + n) / 2
k=(m+n)/2
code
代码k是从1开始, 因此计算中位数的时候, 两数组总和为偶数, 则找
t
o
t
/
2
tot / 2
tot/2 和
t
o
t
/
2
+
1
tot / 2 + 1
tot/2+1; 奇数就找
t
o
t
/
2
+
1
tot/ 2 + 1
tot/2+1
并且为了方便, 假设第1个数组长度比较小
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int tot = nums1.size() + nums2.size();
if (tot % 2 == 0){
int left = find(nums1, 0, nums2, 0, tot / 2); // k从1开始, 所以tot按照[1, n]的方式计算的
int right = find(nums1, 0, nums2, 0, tot / 2 + 1);
return (left + right) / 2.0;
}else {
return find(nums1, 0, nums2, 0, tot / 2 + 1);
}
}
int find(vector<int> &nums1, int i, vector<int> &nums2, int j, int k){
if (nums1.size() - i > nums2.size() - j) return find(nums2, j, nums1, i, k);
if (k == 1) {
if (nums1.size() == i) return nums2[j];
else return min(nums1[i], nums2[j]);
}
if (nums1.size() == i) return nums2[j + k - 1];
int si = min((int)nums1.size(), i + k / 2), sj = j + k - k / 2;// si 和 sj表示图中A[k / 2], B[k / 2]的后一个数,因为数组下标从0开始计算
if (nums1[si - 1] > nums2[sj - 1]) // A[k / 2] > B[k / 2] 可以删除B[1, k / 2]
return find(nums1, i, nums2, sj, k - (sj - j));
else
return find(nums1, si, nums2, j, k - (si - i));
}
};
5. 最长回文子串
分析
回文串分为两种
- 长度为奇数, 只需要左右两边对称即可, 中间一个无所谓
- 长度为偶数, 两两配对, 分别相等
先去枚举下回文串的中心点, 用两个指针, 同时从中间往两边走, 直到走到两个字符不一样/某一个走出边界为止, 这样就找到了以这个点为中心的最长的回文串了, 因为再继续走就更不一样/出界了
走的时候, 比较对应的字符, 如果一样就继续往两边走, 不一样就停止, 停下来的时候左指针位置L
, 右指针位置R
, 说明[L + 1, R - 1], 是以i为中心的最长的回文串, 长度就为R - 1 - (L + 1) + 1 = R - L - 1
长度为奇数的话, 将L = i - 1, R = i + 1, 即两指针在i的左右两边开始走, 无视中间的i
长度为偶数的话, L = i, R = i + 1
i的含义: 回文串的中心
枚举中心i
O(n), 左右两个指针最多O(n), O(n^2)
联动
AcWing139. 回文子串(二分+ hash)
code
class Solution {
public:
string longestPalindrome(string s) {
string res;
for (int i = 0; i < s.size(); i ++ ) {
int l = i - 1, r = i + 1;
while (l >= 0 && r < s.size() && s[l] == s[r]) l --, r ++;
if (res.size() < r - l - 1) res = s.substr(l + 1, r - l - 1);
l = i, r = i + 1;
while (l >= 0 && r < s.size() && s[l] == s[r]) l --, r ++;
if (res.size() < r - l - 1) res = s.substr(l + 1, r - l - 1);
}
return res;
}
};
6. Z 字形变换
分析
可以发现第1行是等差数列, 公差是6, 怎么来的呢?
假如行数 = n,
除了第1个数之外, 第1列有 n - 1个数, 然后斜着的, 除了第1列的最后一个数, 有 n - 1个数,
然后看中间的数, 将中间的数分成两种类型在竖列上的数和在斜列上的数 也是公差为2n - 2的等差数列
所以中间是两个等差数列混在一起, 先写第1个等差数列一项, 再写第2个等差数列的一项, 交替
每个等差数列的起点都是0, 1, 2, 3一直到 n - 1, 然后斜线上的等差数列的起点需要考虑, 比如5, 4, 可以发现斜线上的数 + 第1组等差数列的起点 = 2n - 2, 因此第2组等差数列的起点 = 2n - 2 - i
code
class Solution {
public:
string convert(string s, int n) {
if (n == 1) return s; // 边界情况, 只有1行, 直接返回
string res;
for (int i = 0; i < n; i ++ ){
if (i == 0 || i == n - 1){
for (int j = i; j < s.size(); j += 2 * n - 2)
res += s[j];
}else {
for (int j = i, k = 2 * n - 2 - i; j < s.size() || k < s.size(); j += 2 * n -2, k += 2 * n - 2){
if (j < s.size()) res += s[j];
if (k < s.size()) res += s[k];
}
}
}
return res;
}
};
7. 整数反转
分析
int
写法的话, 代码溢出只有两种情况, r是正数的话是一种情况, r是负数的话, 是另外一种情况
r > 0, 溢出的话, 意味着
x
>
0
x > 0
x>0,
10
r
+
x
%
10
10r + x \% 10
10r+x%10 正的方向溢出, 即
10
∗
r
+
x
%
10
>
m
a
x
10 * r + x \% 10 > max
10∗r+x%10>max ⬅️➡️
10
∗
r
>
m
a
x
−
x
%
10
10 * r > max - x \% 10
10∗r>max−x%10 ⬅️➡️
r
>
(
m
a
x
−
(
x
%
10
)
)
/
10
r > (max - (x \% 10)) / 10
r>(max−(x%10))/10 (max - x % 10 ) 不会溢出
即判断下这个式子, 如果成立, 那么溢出, 返回0
r < 0, 溢出的话意味着,
x
<
10
x < 10
x<10,
10
r
+
x
%
10
10r + x \% 10
10r+x%10负的方向溢出
10
r
+
x
%
10
<
m
i
n
10r + x \% 10 < min
10r+x%10<min
10
r
<
m
i
n
−
(
x
%
10
)
10r < min - (x \%10)
10r<min−(x%10)
r
<
(
m
i
n
−
(
x
%
10
)
)
/
10
r < (min - (x \%10) )/ 10
r<(min−(x%10))/10
code(long long写法)
class Solution {
public:
int reverse(int x) {
long long r = 0;
while (x){
r = r * 10 + x % 10;
x /= 10;
}
if (r > INT_MAX) return 0;
if (r < INT_MIN) return 0;
return r;
}
};
code(int写法)
class Solution {
public:
int reverse(int x) {
int r = 0;
while (x){
if (x > 0 && r > (INT_MAX - x % 10) / 10) return 0;
if (x < 0 && r < (INT_MIN - x % 10) / 10) return 0;
r = r * 10 + x % 10;
x /= 10;
}
if (r > INT_MAX) return 0;
if (r < INT_MIN) return 0;
return r;
}
};
8. 字符串转换整数 (atoi)
分析
- 先删空格
- 第1个字符可能是
'+'
/'-'
int x = s[k] - '0'
int 溢出的话, 主要会在res = res * 10 + x;
-
如果is_minus = 1, 那么
res > (MAX - x) / 10
就会上溢出 -
如果is_minus = -1, 那么表示
res = res * 10 + x
, *is_minus
后会下溢出, 注意: 是*-1后溢出,
转化为-res * 10 - x < MIN
,
-
因为在计算
res = res * 10 + x;
的时候, 都是先计算正数的结果, 然后再最后判断正负号, 那么会有一个问题
如果res = res * 10 + s[k] - '0';
是INT_MIN的话, 因为INT_MIN比INT_MAX多1, [-2147483648, 2147483647],
因此正数存不下来, 所以要特判
-res * 10 - x == INT_MIN
表示溢出的第3种情况
code(long long)
class Solution {
public:
int myAtoi(string s) {
int k = 0;
while (s[k] == ' ') k ++;
if (k == s.size()) return 0;
int is_minus = 1;
if (s[k] == '-') is_minus = -1, k ++;
else if (s[k] == '+') k ++;
long long res = 0;
while (k < s.size() && s[k] >= '0' && s[k] <= '9'){ // 只能用while, 用for遍历的话, "words and 987" 不好判断
res = res * 10 + s[k] - '0';
if (res > INT_MAX) break; // 此时不能直接返回INT_MAX, 因为还没计算符号问题, 等到外面结算
k ++;
}
if (is_minus) res *= is_minus;
if (res > INT_MAX) return INT_MAX;
if (res < INT_MIN) return INT_MIN;
return res;
}
};
code(int)
class Solution {
public:
int myAtoi(string s) {
int k = 0;
while (s[k] == ' ') k ++;
if (k == s.size()) return 0;
int is_minus = 1;
if (s[k] == '-') is_minus = -1, k ++;
else if (s[k] == '+') k ++;
int res = 0;
while (k < s.size() && s[k] >= '0' && s[k] <= '9'){// 只能用while, 用for遍历的话, "words and 987" 不好判断
int x = s[k] - '0';
if (is_minus > 0 && res > (INT_MAX - x)/ 10) return INT_MAX; // 第1种情况溢出
if (is_minus < 0 && -res < (INT_MIN + x)/ 10) return INT_MIN; // 第2种情况溢出
if (-res * 10 - x == INT_MIN) return INT_MIN; // 第3种情况溢出, 因为负数边界绝对值比正数边界多1
res = res * 10 + x;
k ++;
if (res > INT_MAX) break;
}
if (is_minus) res *= is_minus;
if (res > INT_MAX) return INT_MAX;
if (res < INT_MIN) return INT_MIN;
return res;
}
};
9. 回文数
分析
直接除
code
class Solution {
public:
bool isPalindrome(int x) {
if (x < 0) return false;
int p = 0;
int a = x;
while (a){
p = 1ll * p * 10 + a % 10;
a /= 10;
}
return p == x;
}
};
10. 正则表达式匹配
分析
code
class Solution {
public:
bool isMatch(string s, string p) {
int n = s.size(), m = p.size();
s = ' ' + s, p = ' ' + p;
vector<vector<int>> f(n + 1, vector<int>(m + 1));
f[0][0] = true;
for (int i = 0; i <= n; i ++ )
for (int j = 1; j <= m; j ++ ){
// 第2个串需要考虑, 如果s串为空, 那么可能和p串匹配, 因为p可能有*, 但是j = 0开始的话, i = 0, 已经初始化了
// i > 0, f[i][0] 一定不匹配, 非空的串 可能匹配空串, 所以j = 0开始没有意义
if (j + 1 <= m && p[j + 1] == '*') continue; // 比如a*, 当前位置是a, 那么a不能单独用, 要联合*, 因此当前状态跳过, 到下一个状态*会计算a的情况
if (i && p[j] != '*') f[i][j] = (s[i] == p[j] || p[j] == '.') && f[i - 1][j - 1];
else if (p[j] == '*') f[i][j] = f[i][j - 2] || i && f[i - 1][j] && (s[i] == p[j - 1] || p[j - 1] == '.');// 注意这里p[j] = ‘*’, 所以s[i] 得要与 p[j - 1]匹配
}
return f[n][m];
}
};