栈的定义
栈的概念
- 栈是线性表的一种;
- 允许进行插入和删除操作的一端被称为栈顶,相对地,把另一端称为栈底;
- 这是一种后进先出(LIFO, Last In First Out)或先进后出(FILO, First In Last Out)结构,最先(晚)到达栈的结点将最晚(先)被删除。
栈的基本操作
- 创建一个栈
create()
:创建一个空的栈; - 进栈
push(x)
:将x插入栈中,使之成为栈顶元素; - 出栈
pop()
:删除栈顶元素并返回栈顶元素值; - 读栈顶元素
top()
:返回栈顶元素值但不删除栈顶元素; - 判栈空
isEmpty()
:若栈为空,返回true,否则返回false。
栈的抽象类
template <class elemType>
class stack {
public:
virtual bool isEmpty() const = 0;
virtual void push(const elemType &x) = 0;
virtual elemType pop() = 0;
virtual elemType top() const = 0;
// 虚析构函数防止内存泄漏
virtual ~stack() {}
};
顺序栈
顺序栈的概念
顺序栈指栈的顺序实现。
- 用连续的空间存储栈中的结点,即数组。
- 下标为0的一端为栈底。
- 进栈和出栈总是在栈顶一端进行,不会引起类似顺序表中的大量数据的移动。
顺序栈类定义
template <class elemType>
class seqStack: public stack<elemType>{
private:
elemType *elem;
int top_p;
int maxSize;
void doubleSpace();
public:
seqStack(int initSize = 10);
// 析构函数
~seqStack() { delete [] elem; }
// 判断栈是否为空,若top_p的值为-1,返回true,否则返回false。
bool isEmpty() const { return top_p == -1; }
void push(const elemType &x) ;
// 出栈:返回栈顶元素,并把元素数量减1
elemType pop() { return elem[top_p--]; }
// top:与pop类似,只是不需要将top_p减1
elemType top() const { return elem[top_p]; }
};
顺序栈成员函数实现
- 构造函数:按照用户估计的栈的规模申请一个动态数组。
template <class elemType>
seqStack<elemType>::seqStack(int initSize) {
elem = new elemType[initSize];
maxSize = initSize ;
top_p = -1;
}
push(x)
:将top_p
加1
,将x
放入top_p
指出的位置中。但要注意数组满的情况。
template <class elemType>
void seqStack<elemType>::push(const elemType &x) {
if (top_p == maxSize - 1)
doubleSpace();
elem[++top_p] = x;
}
顺序栈性能分析
- 除了进栈操作以外,所有运算实现的时间复杂度都是
O(1)
。 - 进栈运算在最坏情况下的时间复杂度是
O(N)
。(需要doubleSpace
) - 均摊分析法:最坏情况在
N
次进栈操作中至多出现一次。如果把扩展数组规模所需的时间均摊到每个插入操作,每个插入只多了一个拷贝操作,因此从平均的意义上讲,插入运算还是常量的时间复杂度
练习
验证栈序列
给出两个序列 pushed
和 poped
两个序列,其取值从 1 到n (1 ≤ n ≤ 10^5)。已知入栈序列是1 2 3 … n,如果出栈序列有可能是 poped,则输出Yes,否则输出No。每个测试点有多组数据。
输入描述:
第一行一个整数q,询问次数。
接下来q个询问,对于每个询问:
第一行一个整数n表示序列长度;
第二行n个整数表示出栈序列。
输出描述:
对于每个询问输出单独一行答案。如可能,则输出Yes;反之,则输出No。
示例 1:
输入:
2
5
5 4 3 2 1
4
2 4 1 3
输出:
Yes
No
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define N 100005
using namespace std;
int a[N], b[N], s[N], top, n;
int main() {
int T;
scanf("%d", &T);
for (int i = 0; i <100000; i++)
a[i] = i + 1;
while (T--) {
memset(b, 0, sizeof(b));
memset(s, 0, sizeof(s));
scanf("%d", &n);
for (int i = 0; i < n; ++i) scanf("%d", &b[i]);
int j = 0;
top = 0;
for (int i = 0; i < n; ++i) {
while (j < n && (!top || s[top - 1] != b[i]))
s[top++] = a[j++];
if (s[top - 1] == b[i])
--top;
}
if (!top) printf("Yes\n");
else printf("No\n");
}
return 0;
}
链接栈
链接栈的概念
链接栈指的是用链接实现的栈。
- 由于栈的操作都是在栈顶进行的,因此用单链表就足够了
- 链接栈不需要头结点,因为对栈来讲只需要考虑栈顶元素的插入删除。从栈的基本运算的实现方便性考虑,可将单链表的头指针指向栈顶。
链接栈类定义
template <class elemType>
class linkStack: public stack<elemType> {
private:
struct node {
elemType data;
node *next;
node(const elemType &x, node *N = NULL) {
data = x;
next = N;
}
node():next(NULL) {}
~node() {}
};
node *top_p;
public:
// 构造函数:将top_p设为空指针
linkStack() { top_p = NULL; }
~linkStack();
// isEmpty():判top_p是否为空指针
bool isEmpty() const { return top_p == NULL; }
// push(x):在表头执行单链表的插入。
void push(const elemType &x) { op_p = new node(x, top_p); }
elemType pop();
// top():返回top_p指向的结点的值。
elemType top() const { return top_p->data; }
};
链接栈成员函数实现
- 析构函数:注意需要释放空间。
template <class elemType>
linkStack<elemType>::~linkStack() {
node *tmp;
while (top_p != NULL) {
tmp = top_p;
top_p = top_p ->next;
delete tmp;
}
}
pop()
:出栈操作,在表头执行单链表的删除。
template <class elemType>
elemType linkStack<elemType>::pop() {
node *tmp = top_p;
elemType x = tmp->data;
top_p = top_p->next;
delete tmp;
return x;
}
链接栈性能分析
由于所有的操作都是对栈顶的操作,与栈中的元素个数无关。所以,所有运算的时间复杂度都是O(1)
。
栈的应用
栈的一个主要应用就是把递归函数转换成非递归函数。
递归函数调用过程
- 递归是一种特殊的函数调用,是在一个函数中又调用了函数本身。
- 递归程序的本质是函数调用,而函数调用是要花费额外的时间和空间。
- 在系统内部,函数调用是用栈来实现。
递归消除方法
如果程序员可以自己控制程序调用的栈,就可以消除递归调用。
方法如下:
定义一个存放子问题的栈
把整个问题放入栈中
While (栈非空)
执行解决问题的过程,分解出的小问题进栈
举例:快速排序的非递归实现
void quicksort(int a[], int low, int high) {
int mid;
if (low >= high) return;
mid = divide(a, low, high);
quicksort( a, low, mid-1);
quicksort( a, mid+1, high);
}
-
快排非递归实现的思想:
- 设置一个栈,记录要做的工作,即要排序的数据栈。
- 栈里面的每个元素为一个node,有left和right两个成员,表示对下标从left到right这一部分数据进行排序。
- 操作步骤:
先将整个数组进栈 重复下列工作,直到栈空: 从栈中弹出一个元素,即一个排序区间。 将排序区间分成两半。 检查每一半,如果多于两个元素,则进栈。
-
快排非递归实现的过程:
-
左边表示要排序的数组,右边表示我们的栈里面的内容。
-
-
快排非递归实现的代码:
// 每个node表示要对从left到right这一部分数据进行排序
struct node {
int left;
int right;
};
void quicksort( int a[], int size) {
seqStack <node> st;
int mid, start, finish;
node s;
if (size <= 1) return;
// 排序整个数组
s.left = 0;
s.right = size - 1;
st.push(s);
while (!st.isEmpty()) {
s = st.pop();
start = s.left;
finish = s.right;
mid = divide(a, start, finish);
if (mid - start > 1) {
s.left = start;
s.right = mid - 1;
st.push(s);
}
if (finish - mid > 1) {
s.left = mid + 1;
s.right = finish;
st.push(s);
}
}
}
练习
表达式括号匹配
假设一个表达式有英文字母(小写)、运算符+ — * /
和左右小(圆)括号构成,以@作为表达式的结束符。请编写一个程序检查表达式中的左右圆括号是否匹配,若匹配,则返回YES;否则返回NO。表达式长度小于255,左圆括号少于20个。
输入描述:
一行,输入的表达式。
输出描述:
一行,YES或NO
示例 1:
输入:
2*(x+y)/(1-x)@
输出:
YES
代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
char s[1010];
int main() {
int a = 0;
scanf("%s", s);
int len = strlen(s);
for (int i = 0; i < len; i++) {
if (s[i] =='(') {
a++;
}
if (s[i] == ')') {
a--;
if (a < 0) {
printf("NO");
return 0;
}
}
}
if (a == 0)
printf("YES");
else
printf("NO");
return 0;
}
for (int i = 0; i < len; i++) {
if (s[i] =='(') {
a++;
}
if (s[i] == ')') {
a--;
if (a < 0) {
printf("NO");
return 0;
}
}
}
if (a == 0)
printf("YES");
else
printf("NO");
return 0;
}