栈与队列理论基础
栈先进后出,而队列则是先进先出:
C
对于直接用C语言来说,两者都是通过malloc一个一维数组直接手动模拟,区别是stack可以直接用一个stkTop来控制入栈出栈;而queue则需要head和tail两个值的变化实现入队出队。
C++
对于C++而言,就需要搞清楚用的是哪一个STL(C++标准库),通过对代码随想录的解读,现在用的比较多的应该是SGI STL。
栈提供push 和 pop 等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素。
栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。
所以STL中栈往往不被归类为容器,而被归类为container adapter(容器适配器)。
常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的底层结构。
deque是一个双向队列,只要封住一端,只开通另一端就可以实现栈的逻辑了。
队列中先进先出的数据结构,同样不允许有遍历行为,不提供迭代器, SGI STL中队列一样是以deque为缺省情况下的底部结构。
所以STL 队列也不被归类为容器,而被归类为container adapter( 容器适配器)。
也正因如此,也可以用诸如vector或者list来进行初始化。
用栈实现队列
例题232(简单)用栈实现队列
注意要点:
- 通过两个栈,一个用来压入元素,另一个用来在出队时作为缓冲,来将最后一个出栈元素作为第一个出队元素。
下面贴出代码:
CPP版本
class MyQueue {
public:
stack<int> stIn;
stack<int> stOut;
MyQueue() {
}
void push(int x) {
stIn.push(x);
}
int pop() {
//stOut空才从stIn导入数据
if (stOut.empty())
{
while (!stIn.empty())
{
stOut.push(stIn.top());
stIn.pop();
}
}
int result = stOut.top();
stOut.pop();
return result;
}
int peek() {
int res = this->pop(); //通过this直接调用这个类中的实现函数
stOut.push(res); //在pop中弹出,要再次压入栈
return res;
}
bool empty() {
return stIn.empty() && stOut.empty();
}
};
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue* obj = new MyQueue();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->peek();
* bool param_4 = obj->empty();
*/
C版本
typedef struct {
int stackInTop, stackOutTop;
int stackIn[101], stackOut[101];
} MyQueue;
MyQueue* myQueueCreate() {
MyQueue* queue = (MyQueue* )malloc(sizeof(MyQueue));
queue->stackInTop = 0;
queue->stackOutTop = 0;
return queue;
}
void myQueuePush(MyQueue* obj, int x) {
obj->stackIn[(obj->stackInTop)++] = x;
}
int myQueuePop(MyQueue* obj) {
//优化:复制栈顶指针,减少对内存的访问次数
int stackInTop = obj->stackInTop;
int stackOutTop = obj->stackOutTop;
//若输出栈为空
if(!stackOutTop)
{
//将第一个栈中元素复制到第二个栈中
while(stackInTop)
{
//因为此时stackInTop的位置是即将插入的位置,但其实是没有元素在这个位置上的,所以应该用--stackInTop
obj->stackOut[stackOutTop++] = obj->stackIn[--stackInTop];
}
}
//第二个栈出栈
int top = obj->stackOut[--stackOutTop];
//将输出栈中元素放回输入栈中
while(stackOutTop)
{
obj->stackIn[stackInTop++] = obj->stackOut[--stackOutTop];
}
//更新栈顶指针
obj->stackInTop = stackInTop;
obj->stackOutTop = stackOutTop;
//返回队列中第一个元素
return top;
}
int myQueuePeek(MyQueue* obj) {
return obj->stackIn[0];
}
bool myQueueEmpty(MyQueue* obj) {
return !obj->stackInTop && !obj->stackOutTop;
}
void myQueueFree(MyQueue* obj) {
obj->stackInTop = 0;
obj->stackOutTop = 0;
}
/**
1. Your MyQueue struct will be instantiated and called as such:
2. MyQueue* obj = myQueueCreate();
3. myQueuePush(obj, x);
4. int param_2 = myQueuePop(obj);
5. int param_3 = myQueuePeek(obj);
6. bool param_4 = myQueueEmpty(obj);
7. myQueueFree(obj);
*/
从这道题就可以看出,用C++的话,有各种STL自带的比如push、pop、empty等会简便很多!!!!
stack有支持push、top、pop,可以直接使用来做到入栈、查栈顶以及出栈!
用队列实现栈
例题225(简单)用队列实现栈
注意要点:
- 通过两个队列,一个控制入栈元素,另一个作为出栈缓冲,把除队尾元素外都复制入队,最后在出栈完成时,重新拷贝回到入栈的队列。
下面贴出代码:
CPP版本
class MyStack {
public:
queue<int> que1;
queue<int> que2;
MyStack() {
}
void push(int x) {
que1.push(x);
}
int pop() {
int size = que1.size();
int result = que1.back();
size--;
while (size--)
{
que2.push(que1.front());
que1.pop();
}
//此时只剩下队尾元素,即所求栈顶
que1.pop();
que1 = que2;
while (!que2.empty()) {que2.pop();}
return result;
}
int top() {
return que1.back();
}
bool empty() {
return que1.empty();
}
};
/**
* Your MyStack object will be instantiated and called as such:
* MyStack* obj = new MyStack();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->top();
* bool param_4 = obj->empty();
*/
C版本
typedef struct {
int queue1[100], queue2[100];
int head1, head2;
int tail1, tail2;
} MyStack;
MyStack* myStackCreate() {
MyStack* stack = (MyStack* )malloc(sizeof(MyStack));
stack->head1 = 0, stack->head2 = 0;
stack->tail1 = 0, stack->tail2 = 0;
return stack;
}
void myStackPush(MyStack* obj, int x) {
obj->queue1[(obj->tail1)++] = x;
}
int myStackPop(MyStack* obj) {
int head1 = obj->head1, head2 = obj->head2;
int tail1 = obj->tail1, tail2 = obj->tail2;
//除了最后一个元素,全部复制进入queue2
while ((tail1 - head1) > 1)
{
obj->queue2[tail2++] = obj->queue1[head1++];
}
//queue1最后一个元素出队
int top = obj->queue1[head1++];
//queue2全部再次进入queue1
while (tail2 - head2)
{
obj->queue1[tail1++] = obj->queue2[head2++];
}
obj->head1 = head1, obj->tail1 = tail1;
obj->head2 = head2, obj->tail2 = tail2;
return top;
}
int myStackTop(MyStack* obj) {
return obj->queue1[obj->tail1 - 1];
}
bool myStackEmpty(MyStack* obj) {
return obj->tail1 == obj->head1;
}
void myStackFree(MyStack* obj) {
obj->head1 = 0, obj->tail1 = 0;
obj->head2 = 0, obj->tail2 = 0;
}
/**
1. Your MyStack struct will be instantiated and called as such:
2. MyStack* obj = myStackCreate();
3. myStackPush(obj, x);
4. int param_2 = myStackPop(obj);
5. int param_3 = myStackTop(obj);
6. bool param_4 = myStackEmpty(obj);
7. myStackFree(obj);
*/
再次可以看出,用C++有了STL之后,操作有多简单!
C++中,queue支持push、pop、front、back、empty,可以入队、出队、直接查询队列头元素和尾元素、判断是否是空队列!
有效的括号
例题20(简单)有效的括号
注意要点:
- 遇到这种有顺序的匹配,就可以考虑用栈来实现;
- 遇到左括号,就入栈右括号;
- 如果是匹配的右括号,就可以出栈;
- 如果遇到入栈是右括号但栈已经空,或者左右不匹配就false;
- 退出循环后栈非空则证明并不匹配。
下面贴出代码:
CPP版本
class Solution {
public:
bool isValid(string s) {
if (s.size() % 2) {return 0;}
stack<char> stk;
for (char str : s)
{
if (str == '(') {stk.push(')');}
else if (str == '[') {stk.push(']');}
else if (str == '{') {stk.push('}');}
else if (stk.empty() || stk.top() != str) {return 0;}
else {stk.pop();}
}
return stk.empty();
}
};
C版本
//辅助函数:判断栈顶元素与输入的括号是否为一对。若不是,则返回False
int notMatch(char par, char* stack, int stackTop) {
switch(par)
{
case ']': return stack[stackTop - 1] != '[';
case ')': return stack[stackTop - 1] != '(';
case '}': return stack[stackTop - 1] != '{';
}
return 0;
}
bool isValid(char * s){
int len = strlen(s);
char* stack = (char* )malloc(sizeof(char) * len);
int stkTop = 0;
for (int i = 0; i < len; i++)
{
if (s[i] == '(' || s[i] == '[' || s[i] == '{') {stack[stkTop++] = s[i];}
else
{
if (!stkTop || notMatch(s[i], stack, stkTop)) {return 0;}
stkTop--;
}
}
return !stkTop;
}
删除字符串中的所有相邻重复项
例题1047(简单)删除字符串中的所有相邻重复项
注意要点:
- 删除后相邻也要删除,所以很适合用栈来实现;
- 需要注意入栈后,出栈会导致字符串反向,所以要么倒序遍历,要么出栈后reverse。
下面贴出代码:
CPP版本
class Solution {
public:
string removeDuplicates(string s) {
stack<char> stk;
for (int i = s.size() - 1; i >= 0; i--)
{
if (!stk.empty() && stk.top() == s[i]) {stk.pop();}
else {stk.push(s[i]);}
}
string str = "";
while (!stk.empty())
{
str += stk.top();
stk.pop();
}
return str;
}
};
C版本
char * removeDuplicates(char * s){
int len = strlen(s) + 1;
char* stk = (char* )malloc(sizeof(char) * len);
int stkTop = 0;
for (int i = 0; i < len - 1; i++)
{
if (stkTop && stk[stkTop - 1] == s[i]) {stkTop--;}
else {stk[stkTop++] = s[i];}
}
stk[stkTop] = '\0';
return stk;
}
逆波兰表达式求值
例题150(中等)逆波兰表达式求值
注意要点:
- 只需要记住用栈来实现,碰到计算符号,就取出栈顶的两个元素相应计算。
下面贴出代码:
CPP版本
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<long long> stk;
for (int i = 0; i < tokens.size(); i++)
{
if (tokens[i]=="+"||tokens[i]=="-"||tokens[i]=="*"||tokens[i]=="/")
{
long long num2 = stk.top();
stk.pop();
long long num1 = stk.top();
stk.pop();
if (tokens[i] == "+") {stk.push(num1 + num2);}
if (tokens[i] == "-") {stk.push(num1 - num2);}
if (tokens[i] == "*") {stk.push(num1 * num2);}
if (tokens[i] == "/") {stk.push(num1 / num2);}
}
else {stk.push(stoll(tokens[i]));}
}
return stk.top();
}
};
C版本
bool isSign(char* s)
{
if (strlen(s) == 1 &&(s[0] == '+' || s[0] == '-' || s[0] == '*' || s[0] == '/'))
{
return true;
}
else return false;
}
int evalRPN(char ** tokens, int tokensSize){
int* stack = (int* )malloc(sizeof(int) * tokensSize);
int top = 0;
for (int i = 0; i < tokensSize; i++)
{
char* str = tokens[i];
if (!isSign(str))
{
stack[top++] = atoi(str);
}
else
{
int num2 = stack[--top];
int num1 = stack[--top];
switch(str[0])
{
case '+':
stack[top++] = num1 + num2;
break;
case '-':
stack[top++] = num1 - num2;
break;
case '*':
stack[top++] = num1 * num2;
break;
case '/':
stack[top++] = num1 / num2;
break;
}
}
}
return stack[top - 1];
}
滑动窗口最大值
例题239(困难)滑动窗口最大值
注意要点:
- 通过自行创建辅助队列来进行判断:只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的,也就是一个单调队列;
- pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作;
- push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止。
下面贴出代码:
CPP版本
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MyQueue que;
vector<int> ret;
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<int> que;
// 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,相等则弹出。
// 同时pop之前判断队列当前是否为空。
void pop(int value)
{
if (!que.empty() && value == que.front()) {que.pop_front();}
}
// 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
// 这样就保持了队列里的数值是单调从大到小的了。
void push(int value)
{
while (!que.empty() && value > que.back()) {que.pop_back();}
que.push_back(value);
}
// 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
int front() {return que.front();}
};
};
C版本
/**
1. Note: The returned array must be malloced, assume caller calls free().
*/
int* maxSlidingWindow(int* nums, int numsSize, int k, int* returnSize){
int* queue = (int* )malloc(sizeof(int) * numsSize);
int left = 0, right = 0;
for (int i = 0; i < k; i++)
{
while (left < right && nums[i] > nums[queue[right - 1]]) {right--;}
queue[right++] = i;
}
*returnSize = 0;
int* ans = (int* )malloc(sizeof(int) * (numsSize - k + 1));
ans[(*returnSize)++] = nums[queue[left]];
for (int i = k; i < numsSize; i++)
{
while (left < right && nums[i] > nums[queue[right - 1]]) {right--;}
queue[right++] = i;
while (queue[left] <= i - k) {left++;}
ans[(*returnSize)++] = nums[queue[left]];
}
return ans;
}
对于底层实现deque这个双向队列而言,可以从两端front和back以及pop和push,只要变成push_back和pop_front就可以了。
前k个高频元素
例题347(中等)前k个高频元素
注意要点:
- 对频率进行排序,这里我们可以使用一种容器适配器就是优先级队列,其本质就是一个堆,堆就是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值,父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆;
- 用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素;
- 缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)
下面贴出代码:
CPP版本
class Solution {
public:
class Mycomparison
{
public:
//通过排序变成小顶堆
bool operator() (const pair<int, int>& l, const pair<int, int>& r)
{
return l.second > r.second;
}
};
vector<int> topKFrequent(vector<int>& nums, int k) {
//map统计频率
unordered_map<int, int> map;
for (int i = 0; i < nums.size(); i++) {map[nums[i]]++;}
//定义一个大小为k的小顶堆
priority_queue<pair<int, int>, vector<pair<int, int>>, Mycomparison> pri_que;
for (unordered_map<int, int>::iterator it = map.begin(); it != map.end(); it++)
{
pri_que.push(*it);
if (pri_que.size() > k) {pri_que.pop();}
}
//反向输出小顶堆,则得到从大到小的前k个
vector<int> ans(k);
for (int i = k - 1; i >= 0; i--)
{
ans[i] = pri_que.top().first;
pri_que.pop();
}
return ans;
}
};
C版本过于复杂,就没有花精力去写了……
总结
- 从本章开始,可以明显感受到有STL的C++操作起来简单很多,各种插入和弹出元素都有STL已经实现,只需要关注逻辑代码;C则需要自行维护一个数组,通过栈和队列的定义自行操作指针来实现模拟出入栈和出入队列;
- 难点在于最后两题,一个自行实现单调队列,一个则借助了优先级队列的概念;
- C++中,queue和stack都支持push、pop、top;如果从底层出发,则可以直接使用deque,两头都可以操作,只要用形如x_back和x_front即可,同时top则替换成front和back;
- 栈和队列在二叉树的遍历之中非常常用,是基础的数据结构;
- 本章练习题主要关注239和347,需要经常看看,不然就忘了……