03. 栈和队列
3.1 栈
3.1.1 栈的基本概念
- 栈(Stack)是只允许在表尾进行插入或删除操作的线性表
- 【常考题型:给出进栈顺序,有哪些合法的出栈顺序?】
- n n n个不同元素进栈,出栈元素不同排列的个数为 1 n + 1 C 2 n n \frac{1}{n+1}C^n_{2n} n+11C2nn
3.1.2 栈的顺序存储实现
- 使用共享栈的好处:节省存储空间,降低发生上溢的可能
# define MaxSize 10
typedef struct{
ElemType data[MaxSize];
int top;
}SqStack;
// 初始化栈
bool InitStack(SqStack &S){
S.top = -1; // S.top = 0;
}
// 判空
bool StackEmpty(SqStack S){
if(S.top == -1) return true; // S.top == 0;
else return false;
}
// 新元素入栈
bool Push(SqStack &S,ElemType x){
if(S.top == MaxSize-1) // s.top == MaxSize
return false;
S.data[++S.top] = x; // S.data[S.top++] = x;
return true;
}
// 出栈
bool Pop(SqStack &S,ElemType &e){
if(S.top == -1)
return false;
x = S.data[S.top--]; // x = S.data[--S.top];
return true;
}
3.1.3 栈的链式存储实现
- 带头结点
// 头插法
bool LiStack_Head(LiStack &S){
// 初始化
S = (Linknode *)malloc(sizeof(Linknode));
if(S == NULL) return false;
S->next = NULL;
Linknode *p;
int x;
while(scanf("%d",x)){
p = (Linknode *)malloc(sizeof(Linknode));
p->data = x;
p->next = S->next;
S->next = p;
}
return true;
}
// push
bool PushLiStack(LiStack &S,int e){
Linknode *p = (Linknode *)malloc(sizeof(Linknode));
if(p == NULL) return false;
p->data = e;
p->next = S->next;
S->next = p;
return true;
}
// pop
bool PopLiStack(LiStack &S,int &e){
if(S->next == NULL)
return false;
Linknode *p = S->next;
e = p->data;
S->next = p->next;
free(p);
return true;
}
// empty
bool EmptyLiStack(LiStack S){
if(S->next == NULL) return true;
else retun false;
}
3.2 队列
3.2.1 队列的基本概念
- 队列(Queue)是只允许在一端进行插入,在另一端删除的线性表
3.2.2 队列的顺序实现
#define MaxSzie 10
typedef struct{
int data[MaxSize];
int Front,Rear;
}SqQueue;
// 初始化
void InitQueue(SqQueue &Q){
Q.Rear = Q.Front = 0; // ①
}
// 判空
bool QueueEmpty(SqQueue Q){
if(Q.Rear == Q.Front) return true;
else return false;
}
// 入队
bool EnQueue(SqQueue &Q,int x){
// 判满
if((Q.Rear + 1) % MaxSize == Q.Front) // 法一
return false;
Q.data[Q.Rear] = x;
Q.Rear = (Q.Rear + 1) % MaxSize;
return true;
}
// 出队
bool DeQueue(SqQueue &Q,int x){
if(Q.Rear == Q.Front) // 判空
return false;
x = Q.data[Q.Front];
Q.Front = (Q.Front + 1) % MaxSize;
return true;
}
-
判断队列已满 / 已空 / 元素个数
-
法一:牺牲一个存储单元
-
(Q.Rear + 1) % MaxSize == Q.Front // 满
-
Q.Rear == Q.Front // 空
-
(Q.Rear + MaxSize - Q.Front)%MaxSize // 队列元素个数
-
-
法二:size变量
-
Q.size == MaxSize // 满
-
Q.size == 0; // 空
-
size // 队列元素个数
-
-
法三:tag — 每次删除成功令tag=0,插入成功令tag=1
-
Q.Front == Q.Rear && tag == 1 // 满
-
Q.Front == Q.Rear && tag == 0 // 空
-
-
3.2.3 队列的链式实现
- 带头结点
// 初始化(带头结点)
void InitQueue(LinkQueue &Q){
// front 和 rear 都指向头结点
Q.Front = Q.Rear = (LinkNode *)malloc(sizeof(LinkNode));
Q.Front->next = NULL;
}
// 判空
bool IsEmpty(LinkQueue Q){
if(Q.Front == Q.Rear) return true;
else return false;
}
// 入队
void EnQueue(LinkQueue &Q,ElemType x){
LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
Q.Rear->next = s;
Q.Rear = s;
}
// 出队
bool DeQueue(LinkQueue &Q,ElemType &x){
if(Q.Front == Q.Rear)
return false;
LinkNode *p = Q.Front->next;
x = p->data;
Q.Front->next = p->next;
if(Q.Rear == p){ // 若是最后一个结点出队
Q.Rear = Q.Front;
}
free(p);
return true;
}
- 不带头结点
// 初始化
void InitQueue(LinkQueue &Q){
// front 和 rear 都指向NULL
Q.Front = NULL;
Q.Rear = NULL;
}
// 判空
bool IsEmpty(LinkQueue Q){
if(Q.Front == NULL) return true;
elsec return false;
}
// 入队
void EnQueue(LinkQueue &Q,ElemType x){
LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
if(Q.Front == NULL){
Q.Front = s;
Q.Rear = s;
}else{
Q.Rear->next = s;
Q.Rear = s;
}
}
// 出队
bool DeQueue(LinkQueue &Q,ElemType &x){
if(Q.Front == NULL)
return false;
LinkNode *p = Q.Front; // p指向此次出队的元素
x = p->data;
Q.Front = p->next;
if(Q.Rear == p){ // 若是最后一个结点出队
Q.Rear = NULL;
Q.Front = NULL;
}
free(p);
return true;
}
3.2.4 双端队列
给输入序列判断输出序列是否合法
- 输入受限的双端队列(只有一端可以输入):队列中填输入顺序,看受否存在对应的输出序列
- 输出受限的双端队列(只有一端可以输出):队列中填输出顺序,看是否存在正确的输入顺序
3.3 应用
3.3.1 栈 - 括号匹配
用栈实现括号匹配:依次扫描所有字符,遇到左括号入栈,遇到右括号则弹出栈顶元素检查是否匹配。
- 匹配失败情况:
- 左括号单身
- 右括号单身
- 左右括号不匹配
# define MaxSize 10
typedef struct{
ElemType data[MaxSize];
int top;
}SqStack;
// 初始化栈
void InitStack(SqStack &S);
// 判空
void StackEmpty(SqStack S);
// 入栈
bool Push(SqStack &S, char x);
// 出栈
bool Pop(SqStack &S,char &x);
// 括号匹配
bool bracketCheck(char str[],int length){
SqStack S;
InitStack(S);
for(int i=0;i<length;i++){
if(str[i] == '('||str[i] == '['||str[i] == '{'){
Push(S,str[i]);
}else{
if(StackEmpty(S))
return false;
char topElem;
Pop(S,topElem);
if(str[i] == ']' && topElem != '[')
return false;
if(str[i] == ')' && topElem != '(')
return false;
if(str[i] == '}' && topElem != '{')
return false;
}
}
return StackEmpty(S);
}
3.3.2 栈 - 表达式
-
中缀表达式转后缀表达式
-
手算
- 确定中缀表达式中各个运算符的运算顺序
- 选择下一个运算符,按照**「左操作数 右操作数 运算符」**的方式组合成一个新的操作数
- 如果还有运算符没被处理,就继续②
【“左优先”原则】:只要左边的运算符能先计算,就优先算左边的
-
机算(栈)
-
初始化一个栈,用于保存暂时还不能确定运算顺序的运算符
-
从左到右处理各个元素,直到末尾。可能遇到三种情况:
① 遇到操作数。直接加入后缀表达式。
② 遇到界限符。
-
遇到 “(” 直接入栈;
-
遇到 “)” 则依次弹出栈内运算符并加入后缀表达式,直到弹出 “(” 为止。
【注意】“(” 不加入后缀表达式。
③ 遇到运算符。依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到 “(” 或栈空则停止。之后再把当前运算符入栈。
-
-
按上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。
-
-
-
后缀表达式的计算:
-
手算
从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应运算,合体为一个操作数
-
机算(栈)
- 从左往右扫描下一个元素,直到处理完所有元素
- 若扫描到操作数则压入栈,并回到①;否则执行③
- 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到①
-
-
中缀表达式转前缀表达式
-
手算
- 确定中缀表达式中各个运算符的运算顺序
- 选择下一个运算符,按照**「运算符 左操作数 右操作数」**的方式组合成一个新的操作数
- 如果还有运算符没被处理,就继续②
【“右优先”原则】:只要右边的运算符能先计算,就优先算右边的
-
-
前缀表达式的计算:
- 机算
- 从右往左扫描下一个元素,直到处理完所有元素
- 若扫描到操作数则压入栈,并回到①;否则执行③
- 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到①
- 机算
-
中缀表达式的计算(机算)
初始化两个栈,操作数栈和运算符栈(中缀转后缀+后缀表达式求值)
① 若扫描到操作数,压入操作数栈
② 若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈)
3.3.3 栈在递归调用中的应用
-
函数调用的特点:最后被调用的函数最先执行结束(LIFO)
-
函数调用时,需要用一个“函数调用栈”存储:
- 调用返回地址
- 实参
- 局部变量
-
递归调用时,函数调用栈可称为**“递归工作栈”**
- 每进入一层递归,就将递归调用所需信息压入栈顶
- 每退出一层递归,就从栈顶弹出相应信息
-
缺点:效率低,太多层递归可能会导致栈溢出;可能包含很多重复计算
-
可以自定义栈将递归算法改造成非递归算法
3.3.4 队列的应用
- 树的层次遍历
- 图的广度优先遍历
- 在OS中的应用
- 先来先服务
3.4 特殊矩阵的压缩存储
题目未特别说明:矩阵下标默认从 1 开始,数组下标默认从 0 开始
1. 对称矩阵
-
压缩存储策略
- 只存储主对角线+下三角区(或主对角线+上三角区)
- 按行优先 / 列优先原则将各元素存入一维数组中
-
矩阵下标 a i j a_{ij} aij → 一维数组下标(行优先)
k = { i ( i − 1 ) 2 + j − 1 , i > = j ( 下三角区和主对角线元素 ) j ( j − 1 ) 2 + i − 1 , i < j ( 上三角区元素 a i j = a j i ) k= \begin{cases} \frac{i(i-1)}{2}+j-1,\ i>=j(下三角区和主对角线元素) \\ \frac{j(j-1)}{2}+i-1,\ i<j (上三角区元素a_{ij}=a_{ji})\\ \end{cases} k={2i(i−1)+j−1, i>=j(下三角区和主对角线元素)2j(j−1)+i−1, i<j(上三角区元素aij=aji)
2. 三角矩阵
下三角矩阵
-
压缩存储策略:按行优先 / 列优先原则将橙色区元素存入一维数组中。并在最后一个位置存储常量c
-
矩阵下标 a i j a_{ij} aij -> 一维数组下标(行优先)
k = { i ( i − 1 ) 2 + j − 1 , i > = j ( 下三角区和主对角线元素 ) n ( n + 1 ) 2 , i < j ( 上三角区元素 ) k= \begin{cases} \frac{i(i-1)}{2}+j-1,\ i>=j(下三角区和主对角线元素) \\ \frac{n(n+1)}{2},\ i<j (上三角区元素)\\ \end{cases} k={2i(i−1)+j−1, i>=j(下三角区和主对角线元素)2n(n+1), i<j(上三角区元素)
上三角矩阵
-
压缩存储策略:按行优先 / 列优先原则将绿色区元素存入一维数组中。并在最后一个位置存储常量c
-
矩阵下标 a i j a_{ij} aij -> 一维数组下标(行优先)
k = { ( i − 1 ) ( 2 n − i + 2 ) 2 + ( j − i ) , i < = j ( 下三角区和主对角线元素 ) n ( n + 1 ) 2 , i < j ( 上三角区元素 ) k= \begin{cases} \frac{(i-1)(2n-i+2)}{2}+(j-i),\ i<=j(下三角区和主对角线元素) \\ \frac{n(n+1)}{2},\ i<j (上三角区元素)\\ \end{cases} k={2(i−1)(2n−i+2)+(j−i), i<=j(下三角区和主对角线元素)2n(n+1), i<j(上三角区元素)
3. 三对角矩阵 / 带状矩阵
-
当 ∣ i − j ∣ > 1 |i-j|>1 ∣i−j∣>1 时,有 a i j = 0 ( 1 < = i , j < = n ) a_{ij}=0(1<=i,j<=n) aij=0(1<=i,j<=n)
-
压缩存储策略:按行优先(或列优先)原则,只存储带状部分
-
矩阵下标 a i j a_{ij} aij -> 一维数组下标(行优先)
- 前 i − 1 i-1 i−1 行共 3 ( i − 1 ) − 1 3(i-1)-1 3(i−1)−1 个元素
- a i j a_{ij} aij 是 i i i 行第 j − i + 2 j-i+2 j−i+2 个元素
-
a
i
j
a_{ij}
aij 是第
2
i
+
j
−
2
2i+j-2
2i+j−2 个元素
k = 2 i + j − 3 k = 2i+j-3 k=2i+j−3
-
一维数组下标-> 矩阵下标 a i j a_{ij} aij (行优先)
i = ⌊ ( k + 1 ) / 3 + 1 ⌋ j = k − 2 i + 3 i = \lfloor(k+1)/3+1\rfloor\\ j=k-2i+3 i=⌊(k+1)/3+1⌋j=k−2i+3
4. 稀疏矩阵
- 压缩存储策略一:顺序存储——三元组<行,列,值>
- 压缩存储策略二:链式存储——十字链表法