数据结构与算法 第三章 栈和队列
3.1 栈的基本概念
栈是只允许在一端进行插入或删除操作的 线性表 。
栈顶:允许插入和删除的一端
栈底:不允许插入和删除的一端
基本操作
InitStack(&S):初始化栈
DestroyStack(&S):销毁栈
Push(&S,x):x进栈
Pop(&S,&x):栈顶元素出栈,并由x返回
GetTop(S,&x):读栈顶元素,用x返回
StackEmpty(S):判断栈是否空
3.2 栈的顺序存储实现
顺序栈的定义
#define MaxSize 10 // 定义栈中元素的最大个数
typedef struct{ElemType data[MaxSize]; // 静态数组存放栈中元素int top; // 栈顶指针
}SqStack;void testStack(){SqStack S; // 声明一个顺序栈(分配空间)
}
初始化操作
void InitStack(SqStack &S){S.top=-1; // 初始化栈顶指针
}// 判断栈空
bool StackEmpty(SqStack S){return (S.top==-1);
}
进栈操作
bool Push(SqStack &S,ElemType x){if(S.top==MaxSize-1) // 栈满,报错return false;S.top=S.top+1; // 指针先加1S.data[S.top]=x; // 新元素入栈return true;
}// 第4行与第5行的写法等价于:
S.data[++S.top]=x;
出栈操作
bool Pop(SqStack &S, ElemType &x){if (S.top==-1) // 栈空,报错return false;x=S.data[S.top]; // 栈顶元素先出栈S.top=S.top-1; // 指针再减1return true;
}// 第4行与第5行的写法等价于:
x=S.data[S.top--];
出栈时,栈顶指针只是减了1,所以数据还残留在内存中,但是逻辑上被删除了
读栈顶操作
bool GetTop(SqSatck S, ElemType &x){if (S.top==-1)return false;x=S.data[S.top];return true;
}
初始化时,也可以将栈顶指针设为0,此时相关操作会有细微差别,但是逻辑上的实现都一样。
共享栈
就是两个栈共享同一片存储空间,一个指针从上开始指,另一个栈指针从下开始指
#define MaxSize 10
typedef struct{ElemType data[MaxSize]; int top0; // 0号栈栈顶指针int top1; // 1号栈栈顶指针
}ShStack;// 初始化栈
void InitStack(ShStack &S){S.top1=-1;S.top2=MaxSize;
}
栈满的条件:top0+1==top1
3.3 栈的链式存储实现
typedef struct Linknode{ElemType data; // 数据域struct Linknode *next; // 指针域
} *LiStack; // 栈类型定义
3.4 队列的基本概念
队列:只允许在一端进行插入,在另一端删除的线性表
队头:允许删除的一端
队尾:允许插入的一端
#define MaxSize 10 // 定义队列中元素的最大个数
typedef struct{ElemType data[MaxSize]; // 用静态数组存放队列元素int front,rear; // 队头指针和队尾指针
}SqQueue;
基本操作
InitQueue(&Q):初始化队列
DestroyQueue(&Q):销毁队列
EnQueue(&Q,x):入队
DeQueue(&Q,x):出队
GetHead(Q, &x):读队头元素
QueueEmpty(Q):判队列空
3.5 队列的顺序实现
初始化操作
这里我们将front指向队头元素,rear指向队尾元素的后一个位置。
void InitQueue(SeQueue &Q){Q.rear=Q.front=0;
}
入队操作
bool EnQueue(SqQueue &Q,ELemType x){if(队列已满)return false;Q.data[Q.rear]=x;Q.rear=(Q.rear+1)%MaxSize; 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;return true;
}
队列中的元素个数=(rear+MaxSize-front)%MaxSize
判断队列空/满的三种方案
1
#define MaxSize 10 // 定义队列中元素的最大个数
typedef struct{ElemType data[MaxSize]; // 用静态数组存放队列元素int front,rear; // 队头指针和队尾指针
}SqQueue;void InitQueue(SeQueue &Q){Q.rear=Q.front=0;
}
初始化时头指针和尾指针都指向0,说明队空时头尾指针指向同一个
由于rear指针总是指向队尾元素的最后一个位置,说明队满时,rear再向后走一步就是front
bool QueueEmpty(SqQueue Q){return (Q.rear==Q.front);
}bool QueueFull(SqQueue Q){return ((Q.rear+1)%MaxSize==Q.front)
}
下面两种方案针对不浪费队满时rear指向的那片存储空间
2
使用size变量来记录队列大小
#define MaxSize 10 // 定义队列中元素的最大个数
typedef struct{ElemType data[MaxSize]; // 用静态数组存放队列元素int front,rear; // 队头指针和队尾指针int size;
}SqQueue;
bool QueueEmpty(SqQueue Q){return (Q.size==0);
}bool QueueFull(SqQueue Q){return (Q.size==MaxSize)
}
3
使用tag变量标记最近执行的操作是删除/插入
每次删除操作成功时,都令tag=0(只有删除操作,才可能导致队空)
每次插入操作成功时,都令tag=1(只有插入操作,才有可能导致队满)
#define MaxSize 10 // 定义队列中元素的最大个数
typedef struct{ElemType data[MaxSize]; // 用静态数组存放队列元素int front,rear; // 队头指针和队尾指针int tag; // 最近进行的是删除/插入
}SqQueue;void InitQueue(SeQueue &Q){Q.rear=Q.front=0;Q.tag=0;
}
bool QueueEmpty(SqQueue Q){return (Q.front==Q.rear&&Q.tag==0);
}bool QueueFull(SqQueue Q){return (Q.front==Q.rear&&Q.tag==1)
}
3.5 队列的链式实现
typedef struct LinkNode{ // 链式队列结点ElemType data;struct LinkNode *next;
}LinkNode;typedef struct{ // 链式队列LinkNode *front, *rear; // 队列的队头和队尾指针
}LinkQueue;
初始化(带头结点)
void InitQueue(LinkQueue &Q){// 初始时front、rear都指向头结点Q.front=Q.rear=(LinkNode*)malloc(sizeof(LinkNode));Q.front->next=NULL;
}
// 判断队列是否为空
bool IsEmpty(LinkQueue Q){return (Q.front==Q.rear);// return Q.front.next==NULL;// return Q.rear.next==NULL;
}
初始化(不带头结点)
void InitQueue(LinkQueue &Q){// 初始时front、rear都指向NULLQ.front=NULL;Q.rear=NULL;
}
// 判断队列是否为空
bool IsEmpty(LinkQueue Q){return (Q.front==NULL);
}
入队(带头结点)
void EnQueue(LinkQueue &Q, ElemType x){LinkNode *s=(LinkNode *)malloc(sizeof(LinkNode));s->data=x;s->next=NULL;Q.rear->next=s; // 新结点插入到rear之后Q.rear=s; // 修改表尾指针
}
入队(不带头结点)
void EnQueue(LinkQueue &L,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==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;
}
出队(不带头结点)
bool DeQueue(LinkQueue &Q,ElemType &x){if (Q.front==NULL)return false;LinkNode *p=Q.front;x=p->data;Q.front=p->next;if(Q.rear==p){Q.front=NULL;Q.rear=NULL;}free(p);return true;
}
3.6 双端队列
双端队列:只允许从两段插入、两端删除的线性表。
若只使用其中一端的插入、删除操作,则效果等同于栈
考点:判断输出序列合法性
3.7 栈在括号匹配中的应用
用栈实现括号匹配:
依次扫描所有字符,遇到左括号入栈,遇到右括号则弹出栈顶元素,检查是否匹配
匹配失败情况:
- 左括号单身
- 右括号单身
- 左右括号不匹配
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.8 栈在表达式求值中的应用
前缀表达式:运算符在两个操作数中间
后缀表达式:运算符在两个操作数后面
前缀表达式:运算符在两个操作数前面
中缀表达式转后缀表达式(手算)
- 确定中缀表达式中各个运算符的运算顺序
- 选择下一个运算符,按照
[左操作数,右操作数,运算符]
的方式组合成一个新的操作数 - 如果还有运算符没被处理,继续2
注意: 为了使结果唯一,采取左优先原则
后缀表达式的计算(机算)
用栈实现
- 从 左往右 扫描下一个元素,直到处理完所有元素
- 若扫描到操作数则压入栈,并回到1,否则执行3
- 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到1
注意: 先弹出的为右操作数,后一个弹出的为左操作数
最终栈存储的最后一个元素则是最终结果
中缀表达式转前缀表达式(手算)
- 确定中缀表达式中各个运算符的运算顺序
- 选择下一个运算符,按照
[运算符,左操作数,右操作数]
的方式组合成一个新的操作数 - 如果还有运算符没被处理,继续2
注意: 为了使结果唯一,采取右优先原则
前缀表达式的计算(机算)
用栈实现
- 从 右往左 扫描下一个元素,直到处理完所有元素
- 若扫描到操作数则压入栈,并回到1,否则执行3
- 若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到1
注意: 先弹出的为左操作数,后一个弹出的为右操作数
最终栈存储的最后一个元素则是最终结果
中缀表达式转后缀表达式(机算)
初始化一个栈,用于保存暂时还不能确定运算顺序的运算符
从左到右处理各个元素,直到末尾,可能遇到三种情况:
- 遇到操作数。直接假如后缀表达式
- 遇到界限符。遇到
"("
直接入栈;遇到")"
则依次弹出栈内运算符并加入后缀表达式,直到弹出"("
为止。 - 遇到运算符。依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到
"("
或栈空则停止。之后再把当前运算符入栈
中缀表达式的计算(用栈实现)
初始化一个栈,操作数栈和运算符栈
若扫描到操作数,压入操作数栈
若扫描到运算符或界限符,则按照”中缀转后缀“相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应计算,运算结果再压回操作数栈)
3.9 栈在递归中的应用
上面层层调用的函数有个特点:最后被调用的函数最先执行结束,符合栈这种结构
所以,函数调用时,需要用一个栈存储:
- 调用返回地址
- 实参
- 局部变量
函数调用背后的过程
栈在递归中的应用
适合用递归算法解决:可以把原始问题转换为属性相同,但规模较小的问题
缺点:效率低,可能包含很多重复计算
3.10 特殊矩阵的压缩存储
一维数组的存储结构
数组a[i]的存放地址 = LOC + i * sizeof(ElemType) (0<=i<10)
二维数组的存储结构
M行N列的二维数组b[M][N]中,
若按行优先存储,则b[i][j]的存储地址=LOC+(i*N+j)*sizeof(ElemType)
若按列优先存储,则b[i][j]的存储地址=LOC+(j*N+i)*sizeof(ElemType)
对称矩阵的压缩存储
若n阶方阵中任意一个元素ai,j都有ai,j=aj,i,则该矩阵为对称矩阵
压缩存储策略:只存储主对角线+下三角区(或主对角线+上三角区)
e.g:存储主对角线+下三角区,按行优先原则将各元素存储一维数组中:
三角矩阵的压缩存储
下三角矩阵压缩存储策略:按行优先原则将橙色区元素存储一维数组,并在最后一个位置存储常量c。
上三角矩阵存储策略:按行优先原则将绿色区元素存储一维数组中,并在最后一个位置存储常量c
三对角矩阵的压缩存储
三对角矩阵,又称带状矩阵:当|i-j|>1时,有ai,j=0(1<=i,j<=n)
压缩存储策略:按行优先(或列优先)原则,只存储带状部分