21. 合并两个有序链表
分析
二路归并算法
比较当前两个指针的值, 哪个比较小, 就接到答案的末尾, 然后让较小的指针后移, 最后再让答案的指针指向下一个位置.
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* mergeTwoLists(ListNode* l1, ListNode* l2) {
auto dummy = new ListNode(-1), tail = dummy;
while (l1 && l2){
if (l1->val < l2->val){
tail = tail->next = l1;
l1 = l1->next;
}else {
tail = tail->next = l2;
l2 = l2->next;
}
}
if (l1) tail->next = l1;
if (l2) tail->next = l2;
return dummy->next;
}
};
22. 括号生成
分析
合法的括号序列必须满足两个条件
- 任何前缀中
(
数量 >=)
数量 (
数量 =)
数量
因此dfs中需要保证 (
>)
, 注意是严格大于
code
class Solution {
public:
vector<string> res;
vector<string> generateParenthesis(int n) {
dfs(0, 0, "", n);
return res;
}
void dfs(int l, int r, string path, int n){
if (l == n && r == n){
res.push_back(path);
}else {
if (l < n) {
dfs(l + 1, r, path + '(', n);
}
if (l > r && r < n)
dfs(l, r + 1, path + ')', n);
}
}
};
23. 合并K个升序链表
分析
简单的优先队列操作
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:
struct cmp{
bool operator()(ListNode* a, ListNode* b){
return a->val > b->val;
}
};
ListNode* mergeKLists(vector<ListNode*>& lists) {
auto dummy = new ListNode(-1), tail = dummy;
priority_queue<ListNode*, vector<ListNode*>, cmp> heap;
for (auto list : lists){
if(list) heap.push(list);
}
while (heap.size()){
auto cur = heap.top(); heap.pop();
tail = tail->next = cur;
if (cur->next) heap.push(cur->next);
}
return dummy->next;
}
};
24. 两两交换链表中的节点
分析
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* swapPairs(ListNode* head) {
auto dummy = new ListNode(-1);
dummy->next = head;
for (auto p = dummy; p->next && p->next->next;){
auto a = p->next, b = a->next;
p->next = b; // 1
a->next = b->next; // 2
b->next = a; // 3
p = a; // 4
}
return dummy->next;
}
};
25. K 个一组翻转链表
分析
在循环中, 需要判断链表长度够不够k个, 如果不够, 直接break
够的话, 开始翻转:
- 先找到后面的节点c
- 让b->a
- a = b, b = c;
如下图(k个链表内部翻转):
一段完成后的扫尾工作:
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* reverseKGroup(ListNode* head, int k) {
ListNode* dummy = new ListNode(-1);
dummy->next = head;
for (auto p = dummy; p->next && p->next->next;){
auto q = p;
// 因为q是当前需要翻转的k个指针的前一个指针, 因此循环k次
for (int i = 0; i < k && q; i ++ ) q = q->next; // 注意这里要加上 &&q 条件, 否则容易循环到nullptr
if (!q) break;
auto a = p->next, b = a->next;
for (int i = 0; i < k - 1; i ++ ){
auto c = b->next; // 找到后面的节点c
b->next = a; // 翻转
a = b, b = c; // 找到下一次
}
auto c = p->next;
p->next = a, c->next = b; // 1. 2.
p = c;// 3.
}
return dummy->next;
}
};
26. 删除排序数组中的重复项
分析
同leetcode 80
保证[0, k - 1]已经是无重复的数, 然后考虑x(nums[i])
能往第k
个位置上放, 因为是有序的往后搜索的, 所以x
至多跟nums[k - 1]
上的数相同.
- 如果不相同, 那么直接放到第k个位置上, k ++
- 如果相同, 那么跳过当前x, 往nums后面的元素遍历,
for
循环会自动往后走
code
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int k = 0;
for (auto x : nums)
if (!k || nums[k - 1] != x)
nums[k ++] = x;
return k;
}
};
27. 移除元素
分析
遍历数组, [0, k - 1]表示没有= val
的元素
code
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int k = 0;
for (auto &x : nums)
if (x != val)
nums[k ++ ] = x;
return k;
}
};
28. 实现 strStr()
分析
KMP模板题
next[i] = j含义: 当前字符串以i结尾最大后缀 = 前缀的长度为j, 即p[i - j + 1 ~ i] = p[1 ~ j]
注意 next
数组是针对模版串p的next,因为需要对模版串计算 最大前缀和后缀相同,在文本中移动模版串。
code
class Solution {
public:
int strStr(string s, string p) {
if (p.empty()) return 0;
int n = s.size(), m = p.size();
s = ' ' + s, p = ' ' + p;
vector<int> ne(m + 1);
for (int i = 2, j = 0; i <= m; i ++ ){
while (j && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) j ++;
ne[i] = j;
}
for (int i = 1, j = 0; i <= n; i ++ ){
while (j && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) j ++ ;
if (j == m){ // 此时说明已经匹配成功m个字符
return i - m; // 那么在i中开头的字符为 i - m
}
}
return -1;
}
};
29. 两数相除
分析
一次一次减, 是可以做
但是如果
x
=
2
31
,
y
=
1
x = 2^{31}, y = 1
x=231,y=1, 就需要减
2
31
2^{31}
231次, 超时
需要想一些优化的做法.
倍增/ 二进制/ 快速幂思想
具体怎么将
2
1
,
2
4
,
2
5
2^1, 2^4, 2^5
21,24,25找出来呢
从大到小考虑
负数的话
code
class Solution {
public:
int divide(int x, int y) {
typedef long long LL;
vector<LL> exp;
bool is_minus = false;
if (x < 0 && y > 0 || x > 0 && y < 0) is_minus = true;
LL a = abs((LL)x), b = abs((LL)y);
for (LL i = b; i <= a; i = i + i) exp.push_back(i); // 从小到大计算 2^0b, 2^1b, ... 2^30b
LL res = 0;
for (int i = exp.size() - 1; i >= 0; i -- ){ // 倒序遍历找最大的2^k*b <= a , 2^k就是商
if (a >= exp[i]){
a -= exp[i];
res += 1ll << i; // 注意-2147483647 / -1, 移位会溢出, 所以用1ll << i
}
}
if (is_minus) res = -res;
if (res > INT_MAX || res < INT_MIN) res = INT_MAX;
return res;
}
};
30. 串联所有单词的子串
分析
先考虑下, 我们要枚举哪些情况
n : s.size()
m : words.size();
单词的个数
w : word[0].size()
每个单词的长度, 题目规定所有单词长度相等
因为我们要枚举所有m * w的子串, 大概要枚举
O
(
n
)
O(n)
O(n)个, 要枚举下起始位置, 起始位置可以是0, 1, 2, 3 …
可以把所有起始位置按照w的余数来分类.
第1类:
0, w, 2w, 3w, …
1, w + 1, 2w + 1, 3w + 1
…
w - 1, 2w - 1, 3w - 1, 4w - 1, …
即: 将所有起始位置分成w组, 分成w组有啥好处呢?
0 w 2w 3w 4w 5w 6w 7w
可以发现每一个单词的位置都是被限定好的, 每一个单词都会出现在区间里, 不会跨越kw
所有单词必然是会放到每个坑里, 不会横跨某个坑
对于第一类, 我们是要找, 连续m个窗口, 使得连续个m个子区间, 恰好可以对应到m个单词
我们可以将每个区间的单词, 看成一个整体, 问题转化为:
给了一堆连续的数, 让我们在这一堆连续的m个元素, 恰好是我们给定的m个元素
这就是经典的滑动窗口问题了
首先将m个单词存到hash表里,然后用长度是m的滑动窗口来维护所有长度是m的连续子段, 窗口里所有元素也存到hash表里, 每次窗口往后移动, 只会增加1个元素, 删除1个元素, O(1).
每次对于1个新的窗口, 当前维护的hash表的集合, 和一开始给的集合, 是不是对应的. 如果对应, 那就是一组解
怎么去判断两个集合相等?
cnt
表示当前窗口中有效的单词个数(同leetcode 3)
总的时间复杂度:
对于每一类, 有n / w个小区间, 遍历字符串, 每个w长度的字符串只会进hash表1次, 出hash表1次, 计算的次数是n / w, 单词往hash表插是O(w)的, O(n / w * w) , 然后一共有w组, O(n / w * w) * w = O(nw)
如果是字符串hash的做法, 每一类的做法会变成O(n / w), 总的时间复杂度会变成O(n)
联动题
以下题目中用的cnt
表示有效单词个数, 异曲同工.
LeetCode 3. 无重复字符的最长子串
LeetCode 76. 最小覆盖子串
code
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words) {
vector<int> res;
if (words.empty()) return res;
int n = s.size(), m = words.size(), w = words[0].size();
unordered_map<string, int> tot;
for (auto& word : words) tot[word] ++;
for (int i = 0; i < w; i ++ ){ // 分成w组, 每组以i(w的余数)为开头
unordered_map<string, int> wd; // 创建窗口的要放到每一组里, 相当于初始化窗口
int cnt = 0;// 每类w, 初始化cnt
for (int j = i; j + w <= n; j += w) { // 窗口右端点为j + w - 1 <= n - 1, 可以简写成j + w <= n, j表示当前窗口指针
if (j >= i + m * w){// 如果当前窗口指针j, 已经超过起点i出发, m个单词的位置(m * w), 表示左边界已经不在窗口内了
auto word = s.substr(j - m * w, w); // 超出左边界出窗口的单词
wd[word] --; // 窗口内该单词数量 -1
if (wd[word] < tot[word]) cnt --; // 如果减掉的是有效的单词
}
auto word = s.substr(j, w); // 找出当前需要加入到窗口的单词
wd[word] ++; // 窗口内单词次数 +1
if (wd[word] <= tot[word]) cnt ++; // 如果是有效的单词
if (cnt == m) res.push_back(j - (m - 1) * w); // 注意: 当前窗口是[..., j + w], 那么起点是 j - (m - 1)* w
}
}
return res;
}
};