栈(stack)
定义
一种后进先出(LIFO)的线性表,在表的一端插入和删除。
进栈:最先插入的元素放在栈的底部。
出栈:最后插入的元素最先出栈。
*栈顶元素先出栈,栈底元素最后出栈。
基本操作
1、InitStack (&S)
操作结果:构造一个空栈 S 。
2、DestroyStack (&S)
初始条件:栈 S 已存在 。
操作结果:栈 S 被销毁 。
3、StackEmpty (S)
初始条件:栈 S 已存在。
操作结果:若栈 S 为空栈,则返回 TRUE,否则 FALSE 。
4、StackLength (S)
初始条件:栈 S 已存在 。
操作结果:返回 S 的元素个数,即栈的长度 。
5、GetTop (S, &e)
查看栈顶元素,不同于 Pop ;
初始条件:栈 S 已存在且非空 。
操作结果:用 e 返回 S 的栈顶元素 。
6、ClearStack (&S)
初始条件:栈 S 已存在 。
操作结果:将 S 清为空栈 。
7、Push (&S, e)
初始条件:栈 S 已存在 。
操作结果:插入元素 e 为新的栈顶元素 。
8、Pop(&S, &e)
初始条件:栈 S 已存在且非空 。
操作结果:删除 S 的栈顶元素,并用 e 返回其值 。
注意 GetTop 和 Pop 的区别! !
判断出栈顺序类题目的做题技巧:
先看头节点,看其后是否有比它更小的数,若有两个以上,只要这几个比它小的数是按照逆序排列就是合理的。
例:
1)4 3 5 6 1 2(错!!应该是 2 1 才可以)
2)1 3 5 4 2 6(正确)
栈的表示与实现
顺序栈
指栈的顺序结构,一组地址连续的存储单元依次存放自栈底到栈顶的数据元素。
1、栈底指针 base,总是指向栈底。
2、栈顶指针 top:栈空时,top 等于 base;栈非空时,总是指向栈顶元素 + 1 的位置。
1)插入一个栈顶元素,指针 top 增 1;
2)删除一个栈顶元素,指针 top 减 1;
栈的顺序表示:
#define MAXSIZE 100 // 存储空间初始分配量
typedef int ElemType;
typedef int Position;
typedef struct {
ElemType *base; // 栈构造前和销毁后,base的值为NULL
Position top; // 栈顶指针
int size; // 当前已分配的存储空间,以元素为单位
} SqStack;
当然也有另一种堆栈方式,我有详细分析其实现方式以及优势,博文链接我放在这了:
链栈
栈的一种链式存储结构,链栈的存储空间是动态分配的,不需要预先确定最大容量。
1、插入和删除(进栈 / 出栈)只能在表头位置(栈顶)进行 。
2、链栈中的结点是动态产生的,可不考虑上溢问题 。
3、无需头结点,栈顶指针就是链表(即链栈)头指针。
栈的链式表示:
typedef struct node{
LElemType data;
struct node *link;
}StackNode, *LinkStack;
栈的应用
1、数制转换
#include <iostream>
#include <stack>
using namespace std;
void convert(int num, int base) // 余数放入栈
{
stack<int> s;
while (num)
{
int x = num % base;
s.push(x);
num /= base;
}
if (s.empty())
{
s.push(0);
}
while (!s.empty())
{
int digit = s.top();
s.pop();
if (digit < 10)
{
cout << digit;
}
else
{
cout << char(digit - 10 + 'A');
}
}
cout << endl;
}
int main()
{
int num, base;
cin >> num >> base;
convert(num, base);
return 0;
}
时间复杂度:O(log num)
空间复杂度:O(log num)
栈在算法中的作用:暂存程序运行中的状态 。
2、括号匹配的检验
#include <iostream>
#include <stack>
#include <string>
using namespace std;
bool isOpeningBracket(char c) {
return c == '(' || c == '[' || c == '{';
}
bool isMatchingPair(char opening, char closing) {
return (opening == '(' && closing == ')') ||
(opening == '[' && closing == ']') ||
(opening == '{' && closing == '}');
}
int main() {
string expression;
getline(cin, expression);
stack<char> bracketStack;
for (char c : expression) {
if (isOpeningBracket(c)) {
bracketStack.push(c);
} else if (c == ')' || c == ']' || c == '}') {
if (bracketStack.empty()) {
cout << "no" << endl;
return 0;
}
char top = bracketStack.top();
if (isMatchingPair(top, c)) {
bracketStack.pop();
} else {
cout << top << endl;
cout << "no" << endl;
return 0;
}
}
}
if (bracketStack.empty()) {
cout << "yes" << endl;
} else {
cout << bracketStack.top() << endl;
cout << "no" << endl;
}
return 0;
}
3、表达式求值
表达式的三种表示方法
设 Exp = S1 + OP + S2 (这里 S1 和 S2 是操作数,OP 是运算符 ):
1)前缀表示法:OP + S1 + S2 ,运算符在操作数之前。例如 2 + 3 ,前缀表示为 + 2 3 。
2)中缀表示法:S1 + OP + S2 ,这是最常用的表示形式,运算符在两个操作数中间,如 2 + 3 。
3)后缀表示法:S1 + S2 + OP ,运算符在操作数之后。对于 2 + 3 ,后缀表示为 2 3 + 。
结论:
1)操作数相对次序不变:无论前缀、中缀还是后缀表示法,操作数(如 a、b 等)在表达式中的相对先后顺序是一样的 ,只是运算符位置和组合方式改变。
2)运算符相对次序不同:三种表示法中,运算符的排列顺序差异明显,如前缀式中运算符在操作数前,后缀式中在操作数后,中缀式在操作数中间。
3)中缀式运算次序问题:中缀式去掉括号后,像有多种优先级运算符时(如加减乘除混合 ),无法明确运算先后,必须依赖括号辅助确定运算顺序。
4)前缀式运算规则:比如前缀式 + × a b × - c / d e f 里,× a b 就是连续两个操作数 a、b 和之前紧邻的运算符 × 构成最小表达式,先进行 a 乘 b 的运算。
5)后缀式运算规则:以后缀式 a b × c d e / - f × + 为例,运算符出现顺序就是计算顺序,如先算 a b × ,再算 c d e / - 等,每个运算符和之前紧邻两个操作数构成最小表达式。
中缀表达式转后缀表达式的方法:
1)当扫描到一个运算符,若它的优先级比栈顶运算符高,就将其压入栈。
2)若扫描到的运算符优先级比栈顶运算符低,则将栈顶运算符出栈,写入后缀表达式。
4、栈走迷宫
求解思想:回溯法
1)从入口出发,按某一方向向未走过的前方探索。
2)若能走通,则到达新点,否则试探下一方向。
3)若所有的方向均没有通路,则沿原路返回前一点,换下一个方向再继续试探(沿原路返回实际上是出栈) 。
4)直到所有可能的通路都探索到,或找到一条通路,或无路可走又返回到入口点。
5、函数调用过程
调用前,系统完成:
1)将实参,返回地址等传递给被调用函数。
2)为被调用函数的局部变量分配存储区。
3)将控制转移到被调用函数的入口。
调用后,系统完成:
1)保存被调用函数的计算结果。
2)释放被调用函数的数据区。
3)依照被调用函数保存的返回地址将控制转移到调用函数。
6、递归与栈
递归:
1)自我调用。
2)必须有结束条件。
7、在一个数组中实现两个堆栈:
队列(queue)
定义
一种先进先出(FIFO)的线性表。在表的一端插入,在另一端删除。
基本操作
1、InitQueue(&Q)
功能:构造一个空队列 Q 。通过调用这个函数,创建一个没有元素的队列,为后续在队列上进行插入(入队 )、删除(出队 )等操作做准备。这里的 &Q 表示传递队列 Q 的地址,以便函数内部能对其进行初始化操作。
2、DestroyQueue(&Q)
功能:销毁队列 Q ,使队列不再存在。调用该函数后,队列占用的内存等资源被释放,不能再对其进行与队列相关的操作。
3、QueueEmpty(Q)
功能:若 Q 为空队列,则返回 TRUE,否则返回 FALSE。
4、QueueLength(Q)
功能:返回 Q 的元素个数,即队列的长度。
5、GetHead(Q, &e)
功能:用 e 返回 Q 的队头元素。
队列的表现与实现
顺序队列
队列的顺序表示:
#define MAXQSIZE 100 // 最大队列长度
typedef struct
{
QElemType *base; //初始化动态分配存储空间
int front, rear ; //队首、队尾
}SqQueue;
SqQueue Q;
1)rear 指向队尾元素; front 指向队头元素 。初始化 front=rear=0 。
2)空队列条件:Q.front==Q.rear 。
3)队列满:Q.rear-Q.front=m 。
4)入队:Q.base [rear++]=x 。
5)出队:x=Q.base [front++] 。
!!但是这种表现方式存在假溢出的问题:
设数组维数为 m,则:
- 当 rear-front==m 时,再有元素入队发生溢出(真溢出)
- 当 rear 指向队尾,但队列前端仍有空位置时,再有元素入队发生溢出(假溢出)
解决方式是使用循环队列:
将数组首尾相接(即:base [0] 连在 base [m-1] 之后) 。
出入队运用模运算:
- 入队:Q.rear=(Q.rear+1)% m
- 出队:Q.front=(Q.front+1)% m
解决方案 :
法一:另外设一个标志以区别队空、队满 。
法二:少用一个元素空间:
- 队空:Q.rear = Q.front
- 队满:(Q.rear+1) % m = Q.front
队列的循环顺序存储可以解决空间浪费的问题。
链队列
队列的链式表示:
typedef int QElemType;
typedef struct QNode { // 结点类型
QElemType data;
struct QNode *next;
} QNode, *QueuePtr;
typedef struct { // 链队列类型
QueuePtr front; // 队头指针
QueuePtr rear; // 队尾指针
} LinkQueue;
在链队列中删除元素(出队 )时,一般情况下只需修改头指针。
但当删除队头元素后,若此时队列变为空(即原队头元素是队列中唯一元素 ),此时不仅要修改头指针 -- 将头指针指向头节点 ,还要修改尾指针也指向头节点 ,以头尾指针一致来表示空队列状态。
队列的应用
1、舞伴问题
我之前写的关于这个问题的博文链接放这里了:
2、扑克牌排序
这道题的链接在此(里面我写的很详细):
拓展(STL--stack/queue的使用方法)
1、stack
stack 模板类的定义在<stack>头文件中。
stack 模板类需要两个模板参数,一个是元素类型,一个容器类型,但只有元素类型是必要的,在不指定容器类型时,默认的容器类型为 deque。
定义 stack 对象的方法:
1)stack<int> s1;
2)stack<string> s2;
stack 的基本操作有:
- 入栈:s1.push (x)。
- 出栈:s1.pop ()。 !!出栈操作只是删除栈顶元素,并不返回该元素的值。
- 访问栈顶:s1.top ()。
- 判断栈是否为空:s1.empty (),当栈空时,返回 true。
- 访问栈中的元素个数:s1.size ()。
2、queue
queue 模板类的定义在<queue>头文件中。
与 stack 模板类很相似,queue 模板类也需要两个模板参数,一个是元素类型,一个容器类型,元素类型是必要的,容器类型是可选的,默认为 deque 类型。
定义 queue 对象的方法:
1)queue<int> q1;
2)queue<double> q2;
queue 的基本操作有:
- 入队:q1.push (x), 将 x 接到队列的末端。
- 出队:q1.pop (),弹出队列的第一个元素。!!出队操作只是弹出队列的第一个元素,并不返回该元素的值。
- 访问队首元素:q1.front (),访问最早被压入队列的元素。
- 访问队尾元素:q1.back (),访问最后被压入队列的元素。
- 判断队列是否为空:q1.empty (),当队列空时,返回 true。
- 访问队列中的元素个数:q1.size ()。