写一些自己LeetCode的刷题过程及总结02
##leetcode上有2000+的题,不可能都刷完,我的刷题顺序是先分类型,然后再分难度,不断提升,当然过程中也参考了其他大神们的一些建议,光刷题收获不大,最重要的还是不断归纳总结,没事刷两道,坚持写总结其实也挺有意思的。##
##还在不断更新总结!##
##本文仅用来记录自己平时的学习收获##
##有朝一日我也能写出漂亮的代码!##
一、栈与队列
1.1 栈
栈提供push和pop等接口,所有元素必须满足先进后出原则,所以栈不提供迭代器,不像set或map可用迭代器来遍历所有元素。
栈是以底层容器完成所有的工作,对外提供统一的接口,底层容器是可以插拔的(也就是说我们可以决定使用哪种容器来实现栈的功能)。所以在STL中,栈通常不被归为容器,而被归为容器适配器。
1.2 队列
队列必须满足先进先出的原则,其它与栈相同,也不提供迭代器,不允许有遍历行为,队列的底层也可以有不同的实现,因此队列也不被归为容器,而被归为容器适配器。
1.3 leetcode部分栈与队列题目及代码
232.用栈实现队列
面试题03.02.栈的最小值
225.用队列实现栈
20.有效的括号
1047.删除字符串中的所有相邻重复项
150.逆波兰表达式
239.滑动窗口最大值
347.前k个高频元素(优先队列--小顶堆)
1046.最后一块石头的重量(优先队列-大顶堆)
739.每日温度
752.打开转盘锁
1、232.用栈实现队列
//考察对栈基本操作的掌握程度
class MyQueue {
public:
stack<int> stIn;
stack<int> stOut;
/** Initialize your data structure here. **/
MyQueue() {
}
/** 将元素 x 推到队列的末尾 **/
void push(int x) {
stIn.push(x);
}
/** 从队列的开头移除并返回元素 **/
int pop() {
//当stOut栈为空时将stIn栈中的全部元素都放入stOut中
if (stOut.empty()) {
while (!stIn.empty()) {
int temp = stIn.top();
stOut.push(temp);
stIn.pop();
}
}
int ret = stOut.top();
stOut.pop();
return ret;
}
/** 返回队列开头的元素 **/
int peek() {
//直接调用pop()
int ret = this->pop();
stOut.push(ret);
return ret;
}
/** 如果队列为空,返回 true ;否则,返回 false **/
bool empty() {
return stIn.size() == 0 && stOut.size() == 0;
}
};
2、面试题03.02.栈的最小值
//添加一个辅助栈就行
class MinStack {
public:
stack<int> st;
stack<int> minSt;//辅助栈
/** initialize your data structure here. */
MinStack() {
minSt.push(INT_MAX);
}
void push(int x) {
st.push(x);
minSt.push(min(minSt.top(), x));
}
void pop() {
st.pop();
minSt.pop();
}
int top() {
return st.top();
}
int getMin() {
return minSt.top();
}
};
3、225.用队列实现栈
//考察对队列基本操作的掌握程度
class MyStack {
public:
queue<int> q1;
queue<int> q2;
/** Initialize your data structure here. */
MyStack() {
}
/** 将元素 x 压入栈顶。 */
void push(int x) {
q1.push(x);
}
/** 移除并返回栈顶元素。 */
int pop() {
int size = q1.size() - 1;
//将q1导入q2,但是留下q1的最后一个元素
while (size--) {
q2.push(q1.front());
q1.pop();
}
int ret = q1.front();
q1.pop();
//将q2再导回q1,并将q2清空
q1 = q2;
while (!q2.empty()) {
q2.pop();
}
return ret;
}
/** 返回栈顶元素。 */
int top() {
return q1.back();
}
/** 如果栈是空的,返回 true ;否则,返回 false */
bool empty() {
return q1.size() == 0;
}
};
4、20.有效的括号
//稍微有一点技巧:
//在匹配的过程中,如果是左括号就让对应的右括号入栈,
//这样在匹配右括号时只用判断是否与栈顶元素相同就可以了
class Solution {
public:
bool isValid(string s) {
stack<char> st;
for (char ch : s) {
if (ch == '(') st.push(')');
else if (ch == '[') st.push(']');
else if (ch == '{') st.push('}');
//如果在匹配过程中栈已经为空,或者和栈顶元素不同,返回false
else if (st.empty() || ch != st.top()) return false;
//与栈顶元素相同,出栈
else st.pop();
}
//遍历完后,如果栈为空返回true,否则返回false
return st.empty();
}
};
5、1047.删除字符串中的所有相邻重复项
//这道题肯定想到用栈去解决,但是要注意,如果用stack,最后需要将字符串反转后再返回
//所以这样不如干脆用字符串来模拟栈操作,效率更高
class Solution {
public:
string removeDuplicates(string s) {
//用字符串来模拟栈
string stk = "";
for (int i = 0; i < s.size(); ++i) {
if (i == 0 || s[i] != stk.back()) stk.push_back(s[i]);
else stk.pop_back();
}
return stk;
}
};
6、150.逆波兰表达式
//逆波兰表达式是一种后缀表达式,不清楚的去查下
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> stk;
for (string ch : tokens) {
if (ch == "+" || ch == "-" || ch == "*" || ch == "/") {
int num2 = stk.top();
stk.pop();
int num1 = stk.top();
stk.pop();
if (ch == "+") stk.push(num1 + num2);
else if (ch == "-") stk.push(num1 - num2);
else if (ch == "*") stk.push(num1 * num2);
else if (ch == "/") stk.push(num1 / num2);
} else {
stk.push(stoi(ch));
}
}
return stk.top();
}
};
7、239.滑动窗口最大值
//看到一个大佬用单调队列解的,太巧妙了!
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MyQueue que;
vector<int> ret;
//先让前k个数入队
for (int i = 0; i < k; ++i) {
que.push(nums[i]);
}
ret.push_back(que.front());
for (int i = k; i < nums.size(); ++i) {
//移除滑动窗口的第一个元素
que.pop(nums[i - k]);
//向滑动窗口中加入下一个元素
que.push(nums[i]);
//记录当前滑动窗口中的最大值
ret.push_back(que.front());
}
return ret;
}
private:
class MyQueue {
public:
//用deque来实现单调队列
deque<int> que;
//每次弹出的时候,比较是否等于对头的出口元素,等于才弹出
void pop(int val) {
if (!que.empty() && que.front() == val) {
que.pop_front();
}
}
//如果push的值大于入口元素,先从队列后端弹出,直到push的值小与等于入口的值
//这样就保证了队列中的值是从大到小的
void push(int val) {
while (!que.empty() && val > que.back()) {
que.pop_back();
}
que.push_back(val);
}
//对头的值就是最大值
int front() {
return que.front();
}
};
};
8、347.前k个高频元素
//这道题目主要涉及到如下三块内容:
// a.要统计元素出现频率
// b.对频率排序
// c.找出前K个高频元素
//首先统计元素出现的频率,这一类的问题可以使用map来进行统计。
//然后是对频率进行排序,这里我们可以使用一种 容器适配器就是优先级队列。
//优先队列就是一个披着队列外衣的堆,优先级队列内部元素是自动依照元素的权值排列。
//priority_queue默认利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)。
//堆是一颗完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。
class Solution {
public:
//小顶堆
class MyComparison {
public:
bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
return lhs.second > rhs.second;
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> map;
for (int num : nums) ++map[num];
//定义小顶堆对频率排序,大小为k
priority_queue<pair<int, int>, vector<pair<int, int>>, MyComparison> priQue;
//用固定大小为k的小顶堆,扫描所有频率的数值
for (unordered_map<int, int>::iterator it = map.begin(); it != map.end(); ++it) {
priQue.push(*it);
//如果堆的大小超过了k,弹出,保证堆的大小一直为k
if (priQue.size() > k) {
priQue.pop();
}
}
//找出前k个高频元素,因为小顶堆先弹出的是最小的,所以倒序输出到数组
vector<int> ret(k);
for (int i = k - 1; i >= 0; --i) {
ret[i] = priQue.top().first;
priQue.pop();
}
return ret;
}
};
//最开始想到的解法是这样的,只用一个哈希表,感觉有点蠢了
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> m;
vector<int> vec;
for (int num : nums) {
++m[num];
}
int maxTimes = 0;
for (auto it = m.begin(); it != m.end(); ++it) {
if (it->second > maxTimes) maxTimes = it->second;
}
while (k > 0) {
for (auto it = m.begin(); it != m.end(); ++it) {
if (it->second == maxTimes) {
vec.push_back(it->first);
--k;
}
}
--maxTimes;
}
return vec;
}
};
最后再说明一下C++中的priority_queue.
priority_queue<Type, Container, Functional>
Type 就是数据类型,Container 就是容器类型(Container必须是用数组实现的容器,比如vector,deque等等,但不能用 list。STL里面默认用的是vector),Functional 就是比较的方式,当需要用自定义的数据类型时才需要传入这三个参数,使用基本数据类型时,只需要传入数据类型,默认是大顶堆。
//升序队列
priority_queue<int,vector<int>,greater<int>> q;
//降序队列
priority_queue<int,vector<int>,less<int>>q;
//greater和less是std实现的两个仿函数(就是使一个类的使用看上去像一个函数。
//其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了)
9、1046.最后一块石头的重量
//这道题也是用优先队列来解,也就是大顶堆
class Solution {
public:
int lastStoneWeight(vector<int>& stones) {
priority_queue<int> q;
for (int stone : stones) {
q.push(stone);
}
while (q.size() > 1) {
int stone1 = q.top();
q.pop();
int stone2 = q.top();
q.pop();
if (stone1 != stone2) q.push(stone1 - stone2);
}
if (q.empty()) return 0;
return q.top();
}
};
10、739.每日温度
class Solution {
public:
//通过一个单调栈(在这里是递减栈:栈里的元素是递减的)来完成
//具体操作如下:
//1、遍历整个数组,如果栈不空,且当前数字大于栈顶元素,那么如果直接入栈的话就不是 递减栈 ,
//所以需要取出栈顶元素,由于当前数字大于栈顶元素的数字,而且一定是第一个大于栈顶元素的数,
//直接求出下标差就是二者的距离。
//2、继续看新的栈顶元素,直到当前数字小于等于栈顶元素停止,然后将数字入栈,
//这样就可以一直保持递减栈,且每个数字和第一个大于它的数的距离也可以算出来。
vector<int> dailyTemperatures(vector<int>& temperatures) {
if (temperatures.size() == 0) return temperatures;
stack<int> st;
vector<int> vec(temperatures.size(), 0);
st.push(0);
for (int i = 1; i < temperatures.size(); ++i) {
if (temperatures[i] <= temperatures[st.top()]) {
st.push(i);
} else {
while (!st.empty() && temperatures[st.top()] < temperatures[i]) {
vec[st.top()] = i - st.top();
st.pop();
}
st.push(i);
}
}
return vec;
}
};
1.4 利用队列进行层序遍历
752.打开转盘锁
994.腐烂的橘子
815.公交路线
690.员工的重要性
1、752.打开转盘锁
//用队列进行BFS
class Solution {
public:
int openLock(vector<string>& deadends, string target) {
unordered_set<string> set(deadends.begin(), deadends.end());
if (set.count("0000") == 1) return -1;
queue<string> que;
que.push("0000");
int count = 0;
while (!que.empty()) {
int len = que.size();
for (int i = 0; i < len; ++i) {
string str = que.front();
que.pop();
if (str == target) return count;
// 处理str周围的八个相邻结点
for (int j = 0; j < 4; ++j) {
//+1 、 -1
for (int k = -1; k < 2; k += 2) {
string cur = str;
cur[j] = (str[j] - '0' + 10 + k) % 10 + '0';
if (set.count(cur) != 1) {
set.insert(cur);
que.push(cur);
}
}
}
}
// 本层队列中元素处理完成,到达下一转动步数,步数加1
++count;
}
return -1;
}
};
2、994.腐烂的橘子
class Solution {
public:
int orangesRotting(vector<vector<int>>& grid) {
queue<pair<int, int>> que;
int freshNum = 0;
for (int i = 0; i < grid.size(); ++i) {
for (int j = 0; j < grid[0].size(); ++j) {
if (grid[i][j] == 1) ++freshNum;
else if (grid[i][j] == 2) que.push({i, j});
}
}
int minCount = 0;
vector<pair<int, int>> dict = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
while (!que.empty()) {
int len = que.size();
bool flag = false;
for (int i = 0; i < len; ++i) {
auto temp = que.front();
que.pop();
for (int j = 0; j < 4; ++j) {
int x = temp.first + dict[j].first;
int y = temp.second + dict[j].second;
if (x >=0 && x < grid.size() && y >= 0 && y < grid[0].size() && grid[x][y] == 1) {
grid[x][y] = 2;
--freshNum;
flag = true;
que.push({x, y});
}
}
}
if (flag) ++minCount;
}
if (freshNum != 0) return -1;
else return minCount;
}
};
3、815.公交路线
class Solution {
public:
int numBusesToDestination(vector<vector<int>>& routes, int source, int target) {
if (source == target) return 0;
// 标记站点出现的线路
unordered_map<int, vector<int>> mp;
// 标记访问的线路
vector<bool> visited(routes.size(), false);
// 记录站点出现的线路
for (int i = 0; i < routes.size(); ++i) {
for (int j = 0; j < routes[i].size(); ++j) {
mp[routes[i][j]].push_back(i);
}
}
queue<int> que;
que.push(source);
int step = 0;
while (!que.empty()) {
++step;
int len = que.size();
for (int i = 0; i < len; ++i) {
int temp = que.front();
que.pop();
for (int m : mp[temp]) {
if (!visited[m]) {
for (int n = 0; n < routes[m].size(); ++n) {
if (routes[m][n] == target) return step;
que.push(routes[m][n]);
}
visited[m] = true;
}
}
}
}
return -1;
}
};
4、690.员工的重要性
class Solution {
public:
int getImportance(vector<Employee *> employees, int id) {
unordered_map<int, Employee *> mp;
for (auto &employee : employees) {
mp[employee->id] = employee;
}
int total = 0;
queue<int> que;
que.push(id);
while (!que.empty()) {
int curId = que.front();
que.pop();
Employee *employee = mp[curId];
total += employee->importance;
for (int subId : employee->subordinates) {
que.push(subId);
}
}
return total;
}
};
1.5 栈与队列总结
1、栈:其实函数的递归就是通过栈来实现的,函数调用时入栈,结束调用时出栈,所以也可以用栈来模拟函数的递归。括号的匹配和后缀表达式等问题都是栈的经典题目。
2、队列:个人感觉队列最经典的题目还是二叉树的层序遍历。上面介绍的两道题滑动窗口的最大值和前k个高频元素这两道题感觉还是比较绕,第一下可能不会想到是队列(想我比较菜确实也没想到…)
(一)滑动窗口的最大值问题,主要思想是队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。C++中没有直接支持单调队列,需要我们自己来一个单调队列。针对这一问题所设计的单调队列它的push和pop操作需要保持一定原则,这一点在代码中已经说明了。(但这两种操作仅适用于本题目,不同的应用场景单调队列也需要不同的写法)
(二)前k个高频元素问题,引出了优先级队列。优先队列就是一个披着队列外衣的堆,优先级队列内部元素是自动依照元素的权值排列。priority_queue默认利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)。堆是一颗完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。