栈:后进先出(last-in, first-out, LIFO)
队列:先进先出(first-in, first-out, FIFO)
栈的实现有多种方法,比如静态数组、动态数组以及链表。
下面我们先简单介绍一下静态数组的实现方法:
我们可以用一个数组s[0…n-1]来实现一个至多有n个元素的栈,使用top来表示栈顶元素的下标,当top = -1时栈为空,当top = n-1时栈满。
int s[n];
int top = -1;
bool empty()
{
if (-1 == top)
return true;
return false;
}
void push(int x)
{
if (n-1 == top)
{
cout << "Error : Stack is full" <<endl;
return;
}
s[++top] = x;
}
void pop()
{
if (empty())
{
cout << "Error : Stack is empty" <<endl;
return;
}
--top;
}
int top()
{
if (empty())
{
cout << "Error : Stack is empty" <<endl;
return -1;
}
return s[top];
}
静态数组的特点是实现简单,而且占用内存小,不足之处是大小固定,不能动态扩充。
由于栈只有顶部元素才可以被访问,因此使用单链表来实现栈也是一个不错的选择,其优点是没有长度限制,缺点是实现稍微麻烦,易出错,而且存放指针需要额外的空间。
使用动态数组实现的stack可同时具备以上两者的优点,其大体实现与静态数组相同,不过数组的空间是动态分配的,因此,如果空间不够用,我们可以通过重新分配更大的内存空间来解决,为提高效率,我们并不是每次push一个就分配一个,而是设置一个capacity,扩充的时候可变为capacity*2的大小。
另外,为支持多种数据类型,可使用C++的模板机制来实现。
队列实现起来相对比栈复杂一点,因为栈的一端是固定的,元素的出和入都在另一端,而队列一边入一边出,导致首尾两端都是移动的。
鉴于这种移动性,我们可以将数组定义为环形数组,将首和尾连接起来。
我们使用一个数组q[0…n-1]来实现一个至多含n-1个元素的队列。队列具有属性head和tail,head指向队列的头,tail指向新元素将会被插入的位置。
于是队列的元素为q[head], q[head+1],…,q[tail-1],注意,在n-1和0的地方要进行卷绕。当head=tail时,队列为空,当head=tail+1时,队列是满的。
注意到,我们有一个位置是留空的,因为如果不留空,那么在队列满的时候,会有head=tail,这个条件跟队列是空的一样,使得我们无法分辨队列是空还是满,所以需要牺牲一点空间来换取编程的便利性。
下面只提供入队和出队的代码:
void push(int x)
{
if (head == tail+1)
{
cout << "Error : Stack is full" <<endl;
return;
}
q[tail++] = x;
if (tail == n)
tail = 0;
}
void pop()
{
if (head == tail)
{
cout << "Error : Stack is empty" <<endl;
return;
}
++head;
if (head == n)
head = 0;
}
int front()
{
if (head == tail)
{
cout << "Error : Stack is empty" <<endl;
return;
}
return q[head];
}
参见:
使用两个栈实现一个队列
使用两个队列实现一个栈
这里附上两个栈实现一个队列的代码:
template<typename T>
void CQueue<T>::appendTail(const T & element)
{
stack1.push(element);
}
template<typename T>
T CQueue<T>::deleteHead()
{
if (stack2.size() <= 0)
{
while (stack1.size() > 0)
{
T & data = stack1.top();
stack1.pop();
stack2.push(data);
}
}
if (stack2.size() == 0)
throw new exception("queue is empty");
T head = stack2.top();
stack2.pop();
return head;
}
输入两个整数序列,第一个表示栈的压入顺序,请判断第二个序列是否为该栈的弹出序列,假设压入栈的所以数字均不相等。
例如:1,2,3,4,5为压栈序列,则4,5,3,2,1为一个可能的弹出序列,而4,3,5,1,2则不是。
思路如下:
也就是说,每次第二个序列的当前元素跟栈顶元素比较,相等,则将栈顶弹出,第二个序列当前元素右移,表示匹配成功,否则继续将第一个序列入栈,直到找到匹配的。
下面实现:
bool isPopOrder(const int *pPush, const int *pPop, int length)
{
if (NULL != pPush && NULL != pPop && length > 0)
{
stack<int> s;
const int *pPushNext = pPush;
const int *pPopNext = pPop;
while (pPushNext - pPush < length)
{
s.push(*pPushNext);
++pPushNext;
while (!s.empty() && pPopNext - pPop < length && (*pPopNext) == s.top())
{
s.pop();
++pPopNext;
}
}
if (s.empty() && pPopNext - pPop == length)
return true;
}
return false;
}
定义栈的数据结构,在该类型中实现一个能够得到栈最小元素的min函数,且min、push、pop时间复杂度都是O(1)。
一开始的思路是:多一个成员变量来存放最小值。但是问题是,如果该最小值被弹出了,如何找到次小值?
所以一个成员变量是不够的,考虑栈特殊的数据结构,假设栈里有5个元素,那么最小值是5个元素中的最小值,当弹出栈顶元素之后,那么新的最小值应该是4个元素中的最小值……依此类推,于是我们可以每压进一个元素,就多记录一个值,该值表示从栈底到当前元素的最小值。当弹出一个元素时,也同时将该位的最小值元素去掉。
由于最小值的行为跟栈元素的行为类似,所以我们可以使用两个栈来实现这个数据结构:一个存放元素,一个存放从栈底到该元素的最小值。
比如对于序列3,4,2,1,5
辅助栈为:3,3,2,1,1
弹出5之后:
3,4,2,1
3,3,2,1
弹出1之后:
3,4,2
3,3,2
给定一个数组和滑动窗口的大小,找出所有滑动窗口里的最大值。
首先容易察觉到,其实滑动窗口类似一个队列,当窗口右移时,相当于队头元素出队,队尾添加一个新的元素。
在前面我讲了可以找到min的栈,又讲了两个栈实现队列,两者综合起来可以找到队列的min,同理也可以找max。
不过上面的实现会复杂一点,因为要综合两个题目的做法了。
另一种思路是,在队列(deque)中仅存放有可能成为最大值的数。比如滑动窗口为3,此时已经有3,当4进来后,3不可能为最大了,删掉,之后加入有2进来,由于4滑走之后,2可能成为最大,所以保留,再进来6,则4和2不可能是最大,删去……这是大体的思路,具体细节以及实现留待大家自行思考~
以上题目参考自:《剑指offer》
每天进步一点点,Come on!
(●’◡’●)
本人水平有限,如文章内容有错漏之处,敬请各位读者指出,谢谢!