栈
栈是受限的序列,只能在栈顶插入和删除,栈底是盲端。栈属于序列的特例,所以可以基于顺序表和链表派生。
1. 顺序栈
空间管理:
#define MaxSize 50 // 定义栈中元素的最大个数
typedef struct {
ElemType data[MaxSize]; // 存放栈中元素
int top; // 栈顶元素
} SqStack;
静态操作:
bool Top(SqStack S, ElemType &e)
{
if (S.top==-1) return false; // 栈空,报错
e=S.data[S.top];
return true;
}
动态操作:
bool Push(SqStack &S, ElemType e)
{
if (S.top==MaxSize-1) return false; // 栈满,报错
S.data[++S.top]=e;
return true;
}
bool Pop(SqStack &S, ElemType &e)
{
if (S.top==-1) return false;
e=S.data[S.top--];
return true;
}
2. 链栈
空间管理:
#include "LinkList.h"
静态操作:
bool Top(LinkStack S, ElemType &e) // 取顶=获取首元素
{
if (S->next==NULL) return false; // 栈空,报错
e=S->next->data;
return true;
}
动态操作:
void Push(LinkStack &S, ElemType e) // 入栈=插入首元素
{
Insert(S,1,e); }
void Pop(LinkStack &S, ElemType &e) // 出栈=删除首元素
{
Delete(S,1,e); }
应用
1. 进制转换
问题描述:给定任一10进制非负整数 n n n,将其转换为 λ \lambda λ 进制表示形式?
算法思想: n n n 对 λ \lambda λ 反复取模和整除,每次取模结果保存栈中,最后输出栈内所有元素,即自低到高 λ \lambda λ 进制
void convert(stack<char> &S, int n, int base) {
\
char digit[] = "0123456789ABCDEF"; // 新进制符号
while (n > 0) {
S.push( digit[ n % base] ); // 余数入栈
n /= base;
}
} // 新进制下由高到低的各位数,自顶而下保存在栈中
2. 括号匹配
问题描述:如何判断表达式的括号是否匹配?
减而治之:消除一对紧邻的括号,不影响全局的匹配判断。
算法思想:顺序扫描表达式,栈记录已经扫描的部分,遇到 “(" 则进栈,遇到 “)” 则出栈,反复迭代消除紧邻括号。
bool paren(const char exp[], int lo, int high) {
stack<char> S; // 栈记录左括号
for (int i = lo; i < hi; i++) {
if ( ( exp[i] == '(' ) ) S.push( exp[i] ); // 遇左括号,则进栈左括号
else if ( !S.empty() ) S.pop(); // 遇右括号且栈非空,则出栈左括号
else return false; // 遇右括号且栈空,则不匹配
}
return S.empty(); // 匹配,则栈空
}
3. 栈混洗
栈混洗(stack permutation):栈 A = a 1 , a 2 , . . . a n , B = S = ∅ A = { a_1, a_2, ... a_n },B=S=∅ A=a1,a2,...an,B=S=∅,只允许栈 A A A 顶元素弹出入栈 S S S,栈 S S S 顶元素弹出入栈 B B B,最终栈 A A A 元素全转入栈 B B B,该过程称为栈混洗。
问题描述:判断栈混洗的数量
算法思想:每种栈混洗中第一个元素的位置是确定,该元素左右两侧元素的栈混洗排列是相互独立的,所以第一个元素在某个位置 k k k的栈混洗数量是 S P ( n − k ) ∗ S P ( k − 1 ) SP(n-k)*SP(k-1) SP(n−k)∗SP(k−1),而第一个元素的位置数量是 1 1 1 ~ n n n, 所以可以得到递推式 S P ( n ) = ∑ k = 1 n S P ( n − k ) ∗ S P ( k − 1 ) = ( 2 n ) ! ( n + 1 ) ! ⋅ n ! SP(n) = \sum_{k=1}^{n} SP(n-k)*SP(k-1)=\frac{(2n)!}{(n+1)!·n!} SP(n)=k=1∑nSP(n−k)∗SP(k−1)=(n+1)!⋅n!(2n)!
问题描述:甄别全排列中的非栈混洗
算法思想: [ p 1 , p 2 , . . . , p n ] [p_1, p_2, ..., p_n] [p1,p2,...,pn] 是 [ 1 , 2 , 3 , . . . , n ] [1, 2, 3, ..., n] [1,2,3,...,n] 的栈混洗,当且仅当任意 i < j i<j i<j 不含模式 [ . . . , j + 1 , . . . , i , . . . , j , . . . ] [..., j+1, ..., i, ..., j, ...] [...,j+1,...,i,...,j,...]
4. 中缀表达式
问题描述:给定语法正确的算术表达式,计算与之对应的数值?
- 策略一:括号匹配
减而治之:如果算术表达式允许添加括号,则按照需要的运算顺序添加括号,从而将运算符优先级转换为括号匹配的优先级计算表达式。
算法思想:设置一个操作数栈和符号栈,操作数栈存储左括号和数字,符号栈存储运算符号。
- 从左到右扫描算术表达式,遇到数字入操作数栈,遇到算术符号和左括号入符号栈
- 遇到右括号则操作数栈顶元素、次栈顶元素,符号栈栈顶元素和次栈顶元素出栈。
- 操作数的次栈顶元素作为操作数,栈顶元素作为被操作数,使用符号栈出栈的符号计算并将计算结果入操作栈,如此循环直至扫描完毕。
float calculate(float a, char op, float b) {
// 计算结果
switch(op) {
case '+': return a+b;
case '-': return a-b;
case '*': return a*b;
case '/': return a/b;
}
}
float evaluate(const char S[], int lo, int hi) {
stack<float> opnd; stack<char> optr; // 操作数栈、符号栈
float result = 0; // 运算结果
for (int i = lo; i < hi; i++) {
if ( S[i] == ' ' ) continue; // 忽略空格
if ( isdigit(S[i]) ) // 操作数入栈
opnd.push(S[i] - '0');
else if ( S[i] != ')') // 运算符和左括号入栈
optr.push(S[i]);
else if ( S[i] == ')') {
// 遇右括号,运算符和左括号出栈,操作数 a 和被操作数 b 出栈
char op = optr.top(); optr.pop(); optr.pop();
float b = opnd.top(); opnd.pop();
float a = opnd.top(); opnd.pop();
result = calculate(a, op, b);
opnd.push(result); // 结果入栈
}
}
return result;
}
- 策略二:后缀表达式
减而治之:如果算术表达式不允许添加括号,则优先级高的局部执行,并被代以其数值,运算符渐少,直至得到最终结果。
中缀转后缀:中缀表达式转换为后缀表达式,可以实现保证优先级高的局部执行。设置优先级表 M 用于判断运算符优先级,栈 op 保存运算符,后缀表达式靠前的运算符优先级高
- 从左到右扫描算术表达式,遇到数字即输出,遇到左括号即入栈
- 遇到右括号,意味着括号内运算优先级高,则一直输出栈内运算符并出栈,直至遇到左括号并把左括号出栈
- 遇到运算符,栈空则入栈;栈不空且运算符优先级小于等于栈顶元素,则输出栈顶元素并出栈,然后将运算符入栈
- 算术表达式扫描完毕,但栈非空则一直输出栈内元素并出栈
map<char,int> M = {
{
'+',1}, {
'-',1}, {
'*',2}, {
'/',2}
};
bool isOperator(char ch) {
if (ch == '+' || ch == '-' || ch == '*' || ch == '/')
return true;
return false;
}
void convert(const char exp[], int lo, int hi) {
stack<char> op;
for (int i = lo; i < hi; i++) {
if(isdigit(exp[i])) {
// 如果是数字
cout << exp[i];
} else if(exp[i] == '(') {
// (:左括号
op.push(exp[i]);
} else if(exp[i] == ')') {
// 如果是右括号,一直推出栈中操作符,直到遇到左括号(
while(op.top() != '(') {
cout << op.top();
op.pop();
}
op.pop(); // 把左括号(推出栈
} else if(isOperator(exp[i])) {
// 操作符
if(op.empty()) {
// 如果栈空,直接压入栈
op.push(exp[i]);
}
else {
// 比较栈op顶的操作符与ch的优先级
// 如果ch的优先级高,则直接压入栈
// 否则,推出栈中的操作符,直到操作符小于ch的优先级,或者遇到(,或者栈已空
while(!op.empty()) {
if(M[exp[i]] > M[op.top()]) break; // 优先级高于
cout << op.top(); op.pop();
} // while结束
op.push(exp[i]); // 防止不断的推出操作符,最后空栈了;或者ch优先级高了
}
}
}
while (!op.empty()) {
cout << op.top(); op.pop();
}
}
后缀表达式求值:后缀表达式按照优先级排列运算符,仍需要设置栈将局部执行的结果保存在原来位置。
- 从左到右扫描算术表达式,遇到操作数则入栈
- 遇到运算符则弹出栈顶元素和次栈顶元素,使用运算符计算并将结果入栈,最后栈顶元素是最终运算结果
float calculate(float a, char op, float b) {
// 计算结果
switch(op) {
case '+': return a+b;
case '-': return a-b;
case '*': return a*b;
case '/': return a/b;
}
}
float evaluate(const char exp[], int lo, int hi) {
stack<float> opnd;
for (int i = lo; i < hi; i++) {
if(isdigit(exp[i])) // 如果是数字
opnd.push(exp[i] - '0');
else {
// 操作符
float b = opnd.top(); opnd.pop();
float a = opnd.top(); opnd.pop();
float result = calculate(a, exp[i], b);
opnd.push(result); // 结果入栈
}
}
return opnd.top(); // 返回栈顶元素
}
队列
队列和栈一样也是受限的序列,只能在队尾插入和队头删除。队列属于序列的特例,所以可以基于向量和列表派生。
1. 循环队列
空间管理:
#define MaxSize 50 // 定义队列中元素的最大个数
typedef struct {
ElemType data[MaxSize]; // 存放队列元素
int front, rear; // 队头指针和队尾指针
};
动态操作:
bool EnQueue(SqQueue &Q, ElemType x)
{
if ((Q.rear+1)%MaxSize==Q.front) return false; // 队满
Q.data[Q.rear]=x;
Q.rear=(Q.rear+1)%MaxSize; // 队尾指针加1取模
return true;
}
bool DeQueue(SqQueue &Q, ElemType &x)
{
if (Q.rear==Q.front) return false; // 队空,报错
x=Q.data[Q.front];
Q.front=(Q.front+1)%MaxSize; // 队头指针加1取模
return true;
}
2. 链队列
空间管理:
#include "LinkList.h"
typedef struct {
// 链式队列
Position front, rear; // 队列的队头和队尾指针
}LinkQueue;
动态操作:
void EnQueue(LinkQueue &Q, ElemType x)
{
Position s =(Position)malloc(sizeof( struct Node ));
s->data=x; s->next=NULL; // 创建新结点,插入到链尾
Q.rear->next=s; Q.rear=x;
}
bool DeQueue(LinkQueue &Q, ElemType &x)
{
if (Q.front==Q.rear) return false; // 空队
Position=Q.front->next; x=p->data;
Q.front->next=p->next;
if (Q.rear==p) Q.rear=Q.front; // 队列只有一个结点,删除后变空
free(p);
return true;
}
应用
1. 资源循环分配
问题描述:一群客户共享同一资源时,如何兼顾公平和效率?(如多进程共享CPU)
算法思想:客户按照到达先后排成队列,队头客户出队,接受服务,再次入队,依次进行直至服务关闭。
RoundRobin {
// 循环分配器
Queuue Q( clients ); // 客户队列
while ( !ServiceClosed() ) {
e = Q.dequeue(); // 队头客户出队
serve( e ); // 接受服务
Q.enqueue( e ); // 再次入队
}
}
2. 双栈当队
问题描述:如何使用两个栈模拟一个队列?
算法思想:因为栈只能在栈顶进行操作,而队列在队尾插入队头删除,所以需要将栈 S 1 S1 S1 和栈 S 2 S2 S2 的栈顶分别作为队尾和队头。入队就是 S 1 S1 S1 入栈;出队时需要将栈 S 1 S1 S1 的所有元素出栈并入栈 S 2 S2 S2,再让栈 S 2 S2 S2 栈顶元素出栈;两栈均空则队空。
template <typename T>
class Queue : public Stack<T> {
// 双栈模拟队列
public:
void enqueue( T const & e) {
S1.push(e); } // 入队
T dequeue() {
// 出队
while ( !S1.empty() ) {
S2.push( S1.top() ); S1.pop();
}
S2.pop();
}
T &front() {
return S2.top(); } // 队首
private:
Stack<T> S1, S2;
};