第三章 栈、队列和数组
3.1 栈
3.1.1 栈的基本概念
定义
栈是只允许在一端进行插入或删除操作的线性表,操作特性可以概括为后进先出。
栈的数学性质:n个不同元素进栈,出栈元素不同排列的个数为
基本操作
- void InitStack(&S):初始化一个空栈
- bool StackEmpty(S):判断一个栈是否为空
- bool Push(&S,x):进栈
- void Pop(&S,&x):出栈
- void GetTop(S,&x):读栈顶元素
- void DestoryStack(&S):销毁栈
3.1.2 栈的顺序存储结构
-
顺序栈的实现
结构体定义
#define MaxSize 50 #define ElemType int typedef struct { ElemType data[MaxSize]; int top; }SqStack;
顺序栈的基本运算
#include "SqStack.h" #include "iostream" using namespace std; //初始化一个空栈 void InitStack(SqStack &S){ S.top = -1; } //判断一个栈是否为空 bool StackEmpty(SqStack S){ return S.top == -1; } //进栈 bool Push(SqStack &S,int x){ if (S.top == MaxSize - 1) return false; S.data[++S.top] = x; return true; } //出栈 bool Pop(SqStack &S,int &x){ if (S.top == -1) return false; x = S.data[S.top--]; cout <<"弹出的元素为:"<< x << endl; return true; } //读栈顶元素 bool GetTop(SqStack S,int &x){ if (S.top == -1) return false; x = S.data[S.top]; cout <<"栈顶的元素为:"<< x << endl; return true; }
测试代码
void StackTest(){ SqStack S; InitStack(S); cout << "栈是否为空:" << StackEmpty(S) << endl; Push(S,1); Push(S,2); Push(S,3); int x ,y; Pop(S,x); Pop(S,y); int z; GetTop(S,z); }
运行结果
共享栈
两个栈共享同一片存储空间,这片存储空间不单独属于任何一个栈,某个栈需要的多一点,它就可能得到更多的存储空间;
两个栈的栈底在这片存储空间的两端,当元素入栈时,两个栈的栈顶指针相向而行。
栈空:top0 = -1;top1 = MaxSize;
栈满条件:top1 - top0 == 1;
3.1.3 栈的链式存储结构
采用链式存储的栈称为链栈,优点是不会出现栈满情况,和链表的头插法操作一样。
结构体实现
typedef struct{
ElemType data;
Struct LinkNode *next;
} *LiStack;
3.2 队列
3.2.1 队列的基本概念
定义
队列是一种操作受限的线性表,只允许在表的一端插入,在表的另一端删除。
基本操作
- InitQueue(&Q):初始化队列,构造一个空队列Q
- QueueEmpty(Q):判队列空,若队列Q为空返回true,否则返回false
- EnQueue(&Q,x):入队,若队列未满,将x加入,使之成为鑫的队尾
- DeQueue(&Q,&x):出队,若队列Q非空,删除队头元素,并用x返回
- GetHead(Q,&x):读队头元素,若队列Q非空,则将队头元素赋给x
3.2.2 队列的顺序存储结构
-
队列的顺序存储
结构体定义
#define MaxSize 50 #define ElemType int typedef struct { ElemType data[MaxSize]; int front,rear; }SqQueue;
基本操作实现
#include "SqQueue.h" #include "iostream" using namespace std; //初始化队列 void initQueue(SqQueue &Q){ Q.front = Q.rear = 0; } //判断队空 bool QueueEmpty(SqQueue Q){ return (Q.front == Q.rear && Q.front == 0); } //入队 bool EnQueue(SqQueue &Q,int x){ if (Q.rear == MaxSize - 1)return false; Q.data[Q.rear++] = x; cout << "元素" <<x<<"入队"<< endl; return true; } //出队 bool DeQueue(SqQueue &Q,int &x){ if (QueueEmpty(Q))return false; x = Q.data[Q.front++]; cout << "元素" <<x<<"出队"<< endl; return true; } bool GetHead(SqQueue Q,int &x){ if (QueueEmpty(Q))return false; x = Q.data[Q.front++]; cout << "队头元素:" <<x<< endl; return true; }
测试代码
void QueueTest(){ SqQueue Q; initQueue(Q); QueueEmpty(Q); EnQueue(Q,1); EnQueue(Q,2); EnQueue(Q,3); int x,y,z; DeQueue(Q,x); DeQueue(Q,y); GetHead(Q,z); }
存在的问题
顺序队列的假溢出
我们已经明白了队列这种基本数据结构,对于顺序队列而言,其存在已经足够解决大多时候的设计问题了,但是其依旧存在一些缺陷和不足,因为我们的入队和出队操作均是直接在其后面进行结点的链接和删除,这就造成其使用空间不断向出队的那一边偏移,产生假溢出。
-
循环队列
循环队列的出现解决了假溢出的问题。
初始时:Q.front == Q.rear = 0;
队首指针进1:Q.front = (Q.front + 1) % MaxSize;
队尾指针进1:Q.rear = (Q.rear + 1) % MaxSize;
队列长度:(Q.rear + MaxSize - Q.front)% MaxSize;
此时,判断队空和队满的条件都为Q.front = Q.rear,所以,需要牺牲一个存储单元来区分这两种情况
-
牺牲一个存储单元来区分队空和队满
队满条件:(Q.rear + 1) % MaxSize == Q.front;
队空条件:Q.front = Q.rear;
队列中元素个数:(Q.rear - Q.front + MaxSize)% MaxSize;
-
增设表示元素个数的数据成员
队空条件:Q.size == 0;
队满条件:Q.size == MaxSize
-
增设tag数据成员
-
-
循环队列的操作
//入队 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; cout << "元素" <<x<<"入队"<< endl; return true; } //出队 bool DeQueue(SqQueue &Q,int &x){ if (QueueEmpty(Q))return false; x = Q.data[Q.front]; Q.front = (Q.front + 1) % MaxSize; cout << "元素" <<x<<"出队"<< endl; return true; }
3.2.3 队列的链式存储结构
-
队列的链式存储
typedef struct LinkNode{//链式队列结点 Elemtype data; struct LinkNode *next; }LinkNode; typedef struct { LinkNode *front,*rear; }LinkQueue;
-
链式队列的基本操作
#include "LinkQueue.h" #include "cstdlib" //初始化 void InitQueue(LinkQueue &Q){ Q.front = Q.rear = (LinkNode *)malloc(sizeof(LinkNode)); Q.front->next = nullptr; } //判队空 bool IsEmpty(LinkQueue Q){ return Q.front == Q.rear; } //入队 void EnQueue(LinkQueue &Q,int x){ LinkNode *s = (LinkNode *)malloc(sizeof(LinkNode)); s->data = x; s->next = nullptr; Q.rear->next = s; Q.rear = s; } //出队 bool DeQueue(LinkQueue &Q,int &x){ if (IsEmpty(Q))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; }
3.2.4 双端队列
双端队列是允许两端都可以进行入队和出队操作的队列,又分为输出受限和输入受限的双端队列,主要考察输出序列的可能性。
3.3 栈和队列的应用
3.3.1 栈在括号匹配中的应用
问题描述
输入一字符串,检查字符串中 { }、[ ]、( ) 三种括号是否成对出现。不同括号间不能交叉出现且左右括号顺序不能颠倒,如 ) (、{ ( } )等。
基本思路
利用栈的特性,发现左括号就入栈,然后检索到右括号与栈顶的左括号比对,如果为同一种括号则栈顶括号出栈;如果不是同一种括号(交叉)或者栈为空(只有右括号)则匹配失败。
最后若栈空则说明括号匹配成功
代码实现
#define MaxSize 50
typedef struct {
char data[MaxSize];
int top;
}SqStack;
void StackTest();
bool match(char str[]);
//初始化一个空栈
void InitStack(SqStack &S){
S.top = -1;
}
//判断一个栈是否为空
bool StackEmpty(SqStack S){
return S.top == -1;
}
//入栈
bool Push(SqStack &S,char x){
if (S.top == MaxSize - 1) return false;
S.data[++S.top] = x;
return true;
}
//弹栈
bool Pop(SqStack &S,char &x){
if (S.top == -1) return false;
x = S.data[S.top--];
return true;
}
//括号匹配
bool match(char str[]){
SqStack S;
InitStack(S);
for (int i = 0;str[i] != '\0';i++){
if (str[i] == '{' || str[i] == '[' || str[i] == '(') Push(S,str[i]);//左括号就压栈
else {
char c;
Pop(S,c);
if ((c == '{' && str[i] == '}')||(c == '[' && str[i] == ']')||(c == '(' && str[i] == ')')){//右括号就弹栈
continue;
} else return false;
}
}
return true;
}
测试样例
char arr[] = "{[()]}";
char arr[] = "{}()[]]";
3.3.2 栈在表达式求值中的应用
表达式求值是程序设计语言编译中的一个最基本的问题。
中缀表达式:中缀表达式是一个通用的算术或逻辑公式表示方法。中缀表达式就是我们最常用的表达式形式,也是人最容易理解的表达式形式。
(a+b)c-d 34+5/6
后缀表达式:又叫逆波兰式,是计算机比较容易处理的表达式形式。
ab+cd- 3456/+
中缀表达式转后缀表达式
以a + b * c + ( d * e + f ) * g为例
-
基于堆栈的算法
-
从左到右扫描表达式,如果是操作数则直接输出。
-
如果扫描到的字符是一个操作符,分三种情况:
(1)如果堆栈是空的,直接将操作符存储到堆栈中(push it)
(2)如果该操作符的优先级大于堆栈出口的操作符,就直接将操作符存储到堆栈中(push it)
(3)如果该操作符的优先级低于堆栈出口的操作符,就将堆栈出口的操作符导出(pop it), 直到该操作符的优先级大于堆栈顶端的操作符。将扫描到的操作符导入到堆栈中(push)。
-
如果遇到的操作符是左括号"(”,就直接将该操作符输出到堆栈当中。该操作符只有在遇到右括号“)”的时候移除。这是一个特殊符号该特殊处理。
-
如果扫描到的操作符是右括号“)”,将堆栈中的操作符导出(pop)到output中输出,直到遇见左括号“(”。将堆栈中的左括号移出堆栈(pop )。继续扫描下一个字符。
-
如果输入的中缀表达式已经扫描完了,但是堆栈中仍然存在操作符的时候,我们应该讲堆栈中的操作符导出并输入到output 当中。
-
代码实现
//中缀表达式转后缀表达式
queue<char> InfixToSuffix(char arr[]) {
stack<char> stack;
queue<char> queue;
map<char, int> map;
map['+'] = 1;
map['-'] = 1;
map['*'] = 2;
map['/'] = 2;
map['('] = 0;
for (int i = 0; i < strlen(arr); ++i) {
if (arr[i] >= '0' && arr[i] <= '9') {//如果是操作数直接进入输出队列
queue.push(arr[i]);
} else {
if (stack.empty() || (map[arr[i]] > map[stack.top()]) && arr[i] != ')') {//如果该操作符优先级大于栈顶操作符优先级,压栈
stack.push(arr[i]);
} else if (map[arr[i]] <= map[stack.top()] && map[stack.top()] != 0 && arr[i] != ')' && arr[i] != '(') {//如果该操作符的优先级低于堆栈出口的操作符,就将堆栈出口的操作符导出, 直到该操作符的优先级大于堆栈顶端的操作符。将扫描到的操作符导入到堆栈中。
while (map[arr[i]] <= map[stack.top()] && stack.top() != '(') {
queue.push(stack.top());
stack.pop();
if (stack.empty())break;
}
stack.push(arr[i]);
} else if (arr[i] == '(') {//如果是左括号,直接压栈
stack.push(arr[i]);
} else if (arr[i] == ')') {//如果是右括号,循环弹栈直到遇到左括号
while (stack.top() != '(') {
queue.push(stack.top());
stack.pop();
}
stack.pop();//将左括号弹栈
} else exit(0);
}
}
while (!stack.empty()) {//如果遍历完后栈不为空,则将栈内所有元素弹出
queue.push(stack.top());
stack.pop();
}
return queue;
}
后缀表达式求值
基本思想:
建立一个操作数栈S。然后从左到右读表达式,如果读到操作数就将它压入栈S中,如果读到n元运算符(即需要参数个数为n的运算符)则取出由栈顶向下的n项操作数进行运算,再将运算的结果代替原栈顶的n项压入栈中。重复上面过程,如果后缀表达式读完且栈中只剩一个操作数,则该数就是运算结果;如果后缀表达式读完但是栈中操作数多于一个,则后缀表达式错误;如果栈中操作数只剩一个,但是后缀表达式还未读完且当前运算符为双元操作符,则后缀表达式同样错误。
测试样例:5 2 + 3 *
代码实现:
//后缀表达式求值
bool CalcSuffixExpression(char arr[],int &res){
stack<int> stack;
for (int i = 0; i < strlen(arr); ++i) {
if (arr[i] >= '0' && arr[i] <= '9'){
stack.push(arr[i] - '0');
} else{
int num_1,num_2,temp;
num_2 = stack.top();
stack.pop();
num_1 = stack.top();
stack.pop();
temp = calculate(num_1,num_2,arr[i]);
stack.push(temp);
}
}
res = stack.top();
return true;
}
3.3.3 栈在递归中的应用
很多问题都可以用递归的方式来解决,这类问题的特点是: 可以把原始问题转换为 属性相同、但规模较小的问题。
以斐波那契数列为例
KaTeX parse error: No such environment: equation at position 8: \begin{̲e̲q̲u̲a̲t̲i̲o̲n̲}̲ Fib(n)=\left\{…
int Fib(int n){
if(n == 0) return 0;
else if(n == 1) return 1;
else return Fib(n-1) + fib(n-2);
}
3.3.4 队列在层次遍历中的应用
在二叉树的层次遍历中,可以借助队列实现。
- 根节点入队
- 若队空(所有结点都已处理完毕),则结束遍历;否则重复3操作
- 队列中的第一个结点出队,并访问之。若其有左孩子,则将左孩子入队;若其有右孩子,则将右孩子入队,返回2。
具体代码实现放到树与二叉树章节。
3.3.5 队列在计算机系统中的应用
队列在计算机系统中的应用非常广泛,以下仅从两个方面来简述队列在计算机系统中的作用:第一个方面是解决主机与外部设备之间速度不匹配的问题,第二个方面是解决由多用户引起的资源竞争问题。
对于第一个方面,仅以主机和打印机之间速度不匹配的问题为例做简要说明。主机输出数据给打印机打印,输出数据的速度比打印数据的速度要快得多,由于速度不匹配,若直接把输出的数据送给打印机打印显然是不行的。解决的方法是设置一个打印数据缓冲区,主机把要打印输出的数据依次写入这个缓冲区,写满后就暂停输出,转去做其他的事情。打印机就从缓冲区中按照先进先出的原则依次取出数据并打印,打印完后再向主机发出请求。主机接到请求后再向缓冲区写入打印数据。这样做既保证了打印数据的正确,又使主机了效率。由此可见,打印数据缓冲区中所存储的数据就是一个队列。
对于第二个方面,CPU(即中央处理器,它包括运算器和控制器)资源的竞争就是一个典型的例子。在一个带有多终端的计算机系统上,有多个用户需要CPU各自运行自己的程序,它们分别通过各自的终端向操作系统提出占用CPU的请求。操作系统通常按照每个请求在时间上的先后顺序,把它们排成一个队列,每次把CPU分配给队首请求的用户使用。当相应的程序运行结束或用完规定的时间间隔后,令其出队,再把CPU分配给新的队首请求的用户使用。这样既能满足每个用户的请求,又使CPU能够正常运行。
3.4 数组和特殊矩阵
数组
数组是相同数据类型的元素按照一定顺序排列的集合。
一维数组的存储
一维数组的实质就是线性表,存储方法同顺序表。假设一维数组A = (A1, A2, A3, …, Ai,…, An),每个元素占L个存储单元,则元素A[i]的存储地址为
LOC(A[i]) = LOC(A[1]) + (i - 1)* L
二维数组的存储
二维数组可以有两种存储方式,行序主序和列序主序。
假设二维数组为A(m*n),每个元素占L个存储单元,则元素A[i][j]的存储地址如下。
按行存储
LOC(A[i][j]) = LOC(A[1][1]) + (n*(i - 1)+(j - 1))*L;
按列存储
LOC(A[i][j]) = LOC(A[1][1]) + (m*(j - 1)+(i - 1))*L;
三维数组的存储
假设三维数组A(rmn),每个元素占L个存储单元,则元素A[i][j][k]的存储地址为
LOC(A[i][j][k]) = LOC(A[1][1][1]) + ((i - 1)*m *n + (j - 1)*n + (k - 1)) * L
特殊矩阵
-
三角矩阵
三角矩阵分为上三角矩阵和下三角矩阵。
上三角矩阵:指矩阵的主对角线(不包括对角线)下方的元素均为0或常数c。上三角矩阵共有n(n+1)/2个元素。
上三角矩阵在一维数组中按行序为主序的存储地址为:
LOC(A[i][j] = LOC(A[1][1]) + 前i-1行非零元素 + 第i行中A[i][j]前非零元素 = LOC(A[1][1]) + (2n-i+2)*(i-1)/2 + (j - i)。
下三角矩阵在一维数组中按行序为主序的存储地址为:
LOC(A[i][j]) = LOC(A[1][1]) + 前i-1行非0元素 + 第i行中A[i][j]前非零元素 = LOC(A[1][1]) + i *(i - 1)/2 + (j - 1) 。 -
对角矩阵
对角矩阵是指矩阵中所有有效元素均集中在以主对角线为中心的带状区域中。 -
三对角矩阵
三对角矩阵是指三条对角线以外的元素均为零或者常数,且第一行和最后一行只有两个有效元素,其他行均有三个非零元素。
三对角矩阵,元素A[i][j]在一维数组中按行序为主序的存储地址为:
LOC(A[i][j]) = LOC(A[1][1]) + 3*(i - 1) - 1 + (j - i + 1) -
稀疏矩阵
矩阵中只有极少的非零元素,而且分布也不规律,如果非零元素个数只占矩阵元素总数的25%~30%或低于这个百分数时,这样的矩阵称为稀疏矩阵。
稀疏矩阵的压缩存储一般有两类:三元组顺序表(顺序结构)和十字链表(链式结构)。 -
三元组顺序表
以顺序存储结构来表示三元组表,成为系数矩阵的三元组顺序表。对于稀疏矩阵的非零元素来说,行号、列号以及元素值三项值可以唯一地确认该元素。三元组顺序表中的三元恰好反映了这三项值。(row, col, value)
在顺序表中,除了储存表示元素的三元组外,还应该存储稀疏矩阵的行数、列数、以及非零元素的个数。 -
十字链表
当矩阵的非零元素个数和位置在操作过程中的变化较大时,就不太适宜采用顺序存储结构来存储稀疏矩阵。此时,采用链式存储结构来表示则更为恰当。
在链表中,每个非零元素由一个结点来表示。结点结构如下:十字链表结点结构
在这个结点中,一共有5个域,其中row表示非零元素的行,col:非零元素的列,value:非零元素的值。down用来链接同一列中的下一个元素,up用来链接同一行中的下一个元素。