发现labuladong算法秘籍真的不错,已经跟着刷了一段时间题了,打算总结一下,先从第一篇学剑开始吧!加油!具体可以参考原网站
原论文为Java版本,我这里选择用C++实现,同时记录自己的一些思考
1.1 数组+链表
田忌赛马背后的算法决策:
例题:870. 优势洗牌
解题思路:
- 如果当前最快的马可以比过对方最快的马,则用该马来比赛
- 误区:如果第二快的马也可以比过对方,是否可以选择这一匹来比?可以但没必要,因为这样的话我最快的一匹还是比对方第二最快的马快
- 如果当前最快的马比不过对方最快的马,则用最烂的一匹来比
- 如果当前最快的马可以比过对方最快的马,则用该马来比赛
实现trike:
- 因为nums2不能改变顺序,但又需要他从大到小排列(方便后面比较),因此选用priority_queue来重新存储nums2,但这里需要把下标i以及数值num一起存储,并且用num来比大小,因此需要重载operator<运算符
- nums1也要排序,这里选用升序排序,left为最小值,right为最大值
- 双指针来收缩nums1的两端,如果nums2当前最大值大于nums1[right],则用left对应的马,反之则用right对应的马
代码:
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
50class Solution {
public:
vector<int> advantageCount(vector<int>& nums1, vector<int>& nums2)
{
// 定义结构体:用来自定义优先队列比较规则
struct id_nums2
{
int i;
int num;
id_nums2(int j, int x)
{
i = j;
num = x;
}
bool operator<(const id_nums2& a) const
{
return num < a.num; // 大顶堆
}
};
// 对nums2数组建立优先队列
priority_queue<id_nums2> nums2pq;
for(int i=0; i<nums2.size(); ++i)
{
nums2pq.push(id_nums2(i, nums2[i]));
}
// 双指针
int left = 0, right = nums1.size()-1;
vector<int> res(nums1.size(), 0);
sort(nums1.begin(), nums1.end()); // 从小到大排序
while(left<=right)
{
int i = nums2pq.top().i;
int num = nums2pq.top().num;
nums2pq.pop();
// nums1能打过
if(nums1[right]>num)
{
res[i] = nums1[right];
right--;
}
else
{
res[i] = nums1[left];
left++;
}
}
return res;
}
};复杂度分析:前期排序为O(nlogn),后续仅遍历一次队列,时间复杂度为O(nlogn),因为共n个元素,调整最大堆的复杂度为O(logn),其他操作为O(1),因此时间复杂度为O(nlogn)。空间复杂度为O(n)因为需要新开辟优先队列和res
链表操作的递归思维一览:用递归不用迭代
例题:206. 反转链表
解题思路:
- reverse函数:输入一个节点head,将以head为起点的链表反转,并返回反转后的头节点
解题tricks:
- 不要跳进递归,而是要根据刚才的函数定义,来弄清楚这段代码会产生什么结果
代码:
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/**
* 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* reverse(ListNode* head)
{
if(head == NULL || head->next == NULL)
{
return head;
}
ListNode* last = reverse(head->next);
head->next->next = head;
head->next = NULL;
return last;
}
ListNode* reverseList(ListNode* head)
{
return reverse(head);
}
};复杂度分析:只遍历一遍链表,因此时间复杂度为O(n),空间复杂度为O(n),因为递归的底层实现是栈,而栈的大小是n(压入所有的节点)
例题:92. 反转链表 II
解题思路:
- 先尝试解决反转前N个node
- 定义reverseN(head, N)函数,输入节点head,以及要反转的节点个数N,返回前N个节点反转,后面节点顺序不变的链表的链表头
解题tricks:
- 定义successor,用来指向要反转的最后一个node的下一个node,好把第一个node指向successor上。
- 在left不等于1时,反复调用原函数,使得head向后移动的同时,缩小left和right,当left为1时再调用reversN函数,最后将head->next指向reverseN的返回值(链表头)
代码:
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/**
* 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 *successor = NULL;
ListNode* reverseN(ListNode* head, int m) // 反转前M个节点,并返回反转后链表的头节点
{
if(m == 1)
{
successor = head->next;
return head;
}
ListNode* last = reverseN(head->next, m-1);
head->next->next = head;
head->next = successor;
return last;
}
ListNode* reverseBetween(ListNode* head, int left, int right)
{
if(left == 1) return reverseN(head, right); // 从left节点开始反转
head->next = reverseBetween(head->next, left-1, right-1);
return head;
}
};复杂度分析:只遍历一遍,时间复杂度为O(n),空间复杂度为O(n)
1.3 队列&栈
用栈解决“括号问题”
例题1:20. 有效的括号
解题思路:
- 如果仅有一种括号,只用记录左括号的数量即可
- 但多种括号时,无法通过计数来完成,比如"([)]"是不合法的,此时需要借助栈(后进先出)来完成
- 当输入为左括号时,压入栈中;当输入为右括号时,比较栈顶和右括号是否匹配
- 匹配时,pop出栈顶元素
- 不匹配或栈为空时,return false
解题tricks:
遍历字符串:
1
2
3
4
5
6
7
8
9
10方法一:
for(int i=0; i<s.size(); ++i)
{
s[i];
}
方法二:auto类型
for(auto c:s)
{
c;
}
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class Solution {
public:
bool isValid(string s)
{
stack<char> left;
for(int i=0; i<s.size(); ++i)
{
if(s[i]=='(' || s[i]=='[' || s[i]=='{') left.push(s[i]);
else
{
if(left.empty()) return false;
if((s[i]==')' && left.top()=='(')
|| (s[i]==']' && left.top()=='[')
|| (s[i]=='}' && left.top()=='{'))
left.pop();
else return false;
}
}
return left.empty();
}
};复杂度分析:时间复杂度为O(N),空间复杂度为O(N)
解题思路:
- 思路一:用栈来解决,碰到不匹配时说明左括号不够用,当最后结束时栈没空,证明左括号太多了
- 思路二:不用栈,用两个变量记录左括号的数量以及res即可,思路同上
解题tricks:最后要记得加上栈中残留的左括号
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class Solution {
public:
int minAddToMakeValid(string s)
{
stack<char> left;
int res = 0;
for(char c:s)
{
if(c=='(') left.push(c);
else
{
if(left.empty()) res++;
else
{
left.pop();
}
}
}
res += left.size();
return res;
}
};复杂度分析:时间空间复杂度均为O(N),但空间复杂度可以降到O(1)
1541. 平衡括号字符串的最少插入次数 1个左括号和2个右括号匹配
解题思路:
- 不用栈:left记录当前有多少个左括号,以及res目前添加了多少个括号了
- 如果当前为左括号,left++
- 如果为右括号
- 判断当前左括号是否为空,空的话则在当前位置左侧添加一个左括号
- 不为空时,判断当前位置的下一个位置是否为右括号
- 如果是,则不用添加任何操作,把left--,然后i++(跳过下一个位置)
- 如果不是,则需要在后侧加入一个右括号,即res++,同时减去匹配的左括号left--
- 最后剩下的左括号需要用2倍的右括号来匹配
- 用栈:也可以完成
- 不用栈:left记录当前有多少个左括号,以及res目前添加了多少个括号了
解题tricks:
代码:
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
30class Solution {
public:
int minInsertions(string s)
{
int left=0, res=0;
for(int i=0; i<s.size(); ++i)
{
if(s[i] == '(') left++;
else
{
if(left==0)
{
res++;
left++;
}
if(s[i+1]==')')
{
left--;
i++;
}
else if(s[i+1]=='(' || i==s.size()-1)
{
res++;
left--;
}
}
}
return res + 2*left;
}
};复杂度分析:时间复杂度为O(N),空间为O(1)
单调栈结构解决“下一个更大数问题”
单调栈解决的就是The Next Greater Number问题,适用范围还是比较窄的,但是遇到这类问题就要用这个方法
模板:
1
2
3
4
5
6
7
8
9
10
11stack<int> s;
// 从后向前遍历数组
for(int i=nums.size()-1; i>=0; --i) // 当有循环数组出现时,可以遍历两遍,用%符号
{
while(!s.empty() && nums[i]>=s.top()) // 如果当前值小于栈顶值,则压入栈,否则pop栈顶元素
{
s.pop();
}
res[i] = s.empty()? -1:s.top(); // 这里res存储nums的值,其实有些题目也可以存储下标
s.push(nums[i]);
}
解题思路:
- 建立单调栈,先把nums2中的数组从后向前遍历,同时将数值压入栈中,使栈呈现由大到小的排列,此时栈顶即为下一个更大的元素
解题tricks:
- 用哈希表来存储<数值,下一个更大元素值>,这样遍历nums1时就可以直接找到对应的答案了
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2)
{
unordered_map<int, int> hm;
vector<int> res(nums1.size(), -1);
stack<int> s;
for(int i=nums2.size()-1; i>=0; --i)
{
while(!s.empty() && s.top()<=nums2[i])
{
s.pop();
}
hm[nums2[i]] = s.empty() ? -1:s.top();
s.push(nums2[i]);
}
for(int i=0; i<nums1.size(); ++i)
{
res[i] = hm[nums1[i]];
}
return res;
}
};复杂度分析:因为每个元素只会被push和pop一次,因此时间复杂度为O(n),空间复杂度因为是栈结构,且返回值也是数值,因此为O(n)
解题思路:
- 沿用单调栈的思路,找到下一个更大的数值,但这里设计循环数组,一般这个时候可以复制一份到原数组后面,然后就可以了
解题tricks:没必要复制一份,可以遍历2n次,然后i用%n的值就可以达到相同的目的了
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class Solution {
public:
vector<int> nextGreaterElements(vector<int>& nums)
{
int n = nums.size();
vector<int> res(n);
stack<int> s;
for(int i= 2*n-1; i>=0; --i) // 假设复制了一份
{
while(!s.empty() && s.top()<=nums[i%n]) // 用%来求相应的坐标
{
s.pop();
}
if(!s.empty()) res[i%n] = s.top();
else res[i%n] = -1;
s.push(nums[i%n]);
}
return res;
}
};复杂度分析:时间空间也都是O(n)
解题思路:同上
解题tricks:栈中存储下一个更大元素的下标,这样方便计算距离,同时用下标也可以找到当日的温度
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures)
{
vector<int> res(temperatures.size(), 0);
stack<int> s; // 存放next greater number的下标
for(int i=temperatures.size()-1; i>=0; --i)
{
while(!s.empty() && temperatures[s.top()]<=temperatures[i])
{
s.pop();
}
if(!s.empty()) res[i] = s.top() - i;
s.push(i);
}
return res;
}
};复杂度分析:时间空间复杂度是O(n)
单调队列解决“滑动窗口最大值”
单调队列的实现方式使用STL中的deque类,中文是双向队列,意思是两边都可以进出,在进出的过程中保证队列是单调递减(增)的就可以
解题思路:
- 思路一:用优先队列,滑动窗口内的值和下标作为pair压入最大堆中,first为值,因为优先队列先对pair的首个元素排序。for循环在外侧遍历所有的窗口右端点,while内循环来判断最大值是否在滑动窗口内部,不在的话需要都pop出去。
- 思路二:用单调队列,解题参考
- 遍历给定数组中的元素,如果队列不为空且当前考察元素大于等于队尾元素,则将队尾元素移除。直到,队列为空或当前考察元素小于新的队尾元素
- 当队首元素的下标小于滑动窗口左侧边界left时,表示队首元素已经不再滑动窗口内,因此将其从队首移除
- 由于数组下标从0开始,因此当窗口右边界right+1大于等于窗口大小k时,意味着窗口形成。此时,队首元素就是该窗口内的最大值
解题tricks:
- 优先队列priority_queue中数据类型为pair时,先对first比大小,在对second比大小
- 双端队列deque可以从两端进行push和pop,关键字分别是back和front
代码:
思路一:
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
26class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k)
{
// 解法一:最大堆
priority_queue<pair<int, int>> pq;
vector<int> res;
for(int i=0; i<nums.size(); ++i)
{
if(i < k-1)
{
pq.push(pair(nums[i], i));
}
else
{
pq.push(pair(nums[i], i));
while(pq.top().second < i-k+1)
{
pq.pop();
}
res.emplace_back(pq.top().first);
}
}
return res;
}
};思路二:可以通过存pair来做,也可以通过存下标来完成
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
55class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k)
{
// 解法二:双向队列来实现单调队列
deque<pair<int, int>> dq; // 存储值和下标
vector<int> res;
for(int r=0; r<nums.size(); ++r)
{
while(!dq.empty() && dq.back().first <= nums[r])
{
dq.pop_back();
}
dq.emplace_back(pair(nums[r], r));
int l = r-k+1;
while(!dq.empty() && dq.front().second < l)
{
dq.pop_front();
}
if(r+1>=k)
{
res.emplace_back(dq.front().first);
}
}
return res;
}
};
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k)
{
// 解法二:双向队列来实现单调队列
deque<int> dq; // 只存储下标
vector<int> res;
for(int r=0; r<nums.size(); ++r)
{
while(!dq.empty() && nums[dq.back()] <= nums[r])
{
dq.pop_back();
}
dq.emplace_back(r);
int l = r-k+1;
while(!dq.empty() && dq.front() < l)
{
dq.pop_front();
}
if(r+1>=k)
{
res.emplace_back(nums[dq.front()]);
}
}
return res;
}
};
复杂度分析:
- 思路一的时间复杂度为O(nlogn),因为优先队列的调整是logn的,而遍历数组是n
- 思路二的时间复杂度为O(n),因为从元素的角度出发,每个元素只被push和pop了一次
“数组去重问题”
解题思路:
- 可以将题目拆分成3个问题
- 去重
- 字符前后顺序不变
- 字典序最小
- 可以尝试用栈+标记数组来解决前两个问题,即将
babc
变为bac
- 再尝试修改上面的思路,在插入元素时用while和栈顶元素比大小,并将大的元素且后面还会出现的元素pop出来(后方是否还会出现需要用一个count数组在一开始遍历一下整个字符串,记录每个字符出现的次数)
- 可以将题目拆分成3个问题
解题triks
- 方是否还会出现需要用一个count数组在一开始遍历一下整个字符串,记录每个字符出现的次数【在循环外多遍历一遍数组不会增加复杂度,因为遍历只有O(N)复杂度】
代码
解决前两个问题:
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
class Solution {
public:
string removeDuplicateLetters(string s)
{
stack<char> S;
vector<bool> inStack(26, false);
for(int i=0; i<s.size(); ++i)
{
if(inStack[s[i]-'a'])
{
continue;
}
S.push(s[i]);
inStack[s[i]-'a'] = true;
}
string res;
while(!S.empty())
{
res.push_back(S.top());
S.pop();
}
reverse(res.begin(), res.end());
return res;
}
};解决第三个问题
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
class Solution {
public:
string removeDuplicateLetters(string s)
{
stack<char> S;
vector<int> ccount(26, 0); // 统计当前位置的后面元素的多少
vector<char> inStack(26, false);
for(char c : s)
{
ccount[c-'a']++;
}
for(int i=0; i<s.size(); ++i)
{
// 无论后面如何操作,当前的字符都--
ccount[s[i]-'a']--;
// 如果当前字符已经在里面了,那就没必要把一些元素挤出来,再push它进去了。例如abca,当第二个a来时,直接跳过即可,否则就变成了a
if(inStack[s[i]-'a']) continue;
// 当前值需要被插入
// 栈不为空,栈顶元素大于当前值,且栈顶元素在后面还会出现
while(!S.empty() && S.top() > s[i] && ccount[S.top()-'a'] > 0)
{
inStack[S.top()-'a'] = false;
S.pop();
}
// 把该pop的pop走后,再把当前元素push进来
S.push(s[i]);
inStack[s[i]-'a'] = true;
}
string res;
while(!S.empty())
{
res.push_back(S.top());
S.pop();
}
reverse(res.begin(), res.end());
return res;
}
};
复杂度分析:总共遍历了一次字符串,虽然内部有个while循环,但是所有元素只会被push和pop一次到栈内,因此时间复杂度是O(N)