文章目录
3. 栈、队列和数组
3.1 栈
3.1.1 栈的基本概念
-
栈的定义:栈(Stack)是只允许在一端进行插入或删除操作的线性表
-
栈的基本操作:
InitStack(&S)
:初始化栈。构造一个空栈 S,分配内存空间。DestroyStack(&S)
:销毁栈。销毁并释放栈 S 所占用的内存空间。Push(&S,x)
:进栈,若栈S未满,则将x加入使之成为新栈顶。Pop(&S,&x)
:出栈,若栈S非空,则弹出栈顶元素,并用x返回。GetTop(S, &x)
:读栈顶元素。若栈 S 非空,则用 x 返回栈顶元素其他常用操作:
StackEmpty(S)
:判断一个栈 S 是否为空。若S为空,则返回true,否则返回false -
卡特兰数
3.1.2 栈的顺序存储实现
-
顺序栈
#include <stdio.h> #define true 1 #define false 0 #define MAXSIZE 10 typedef int bool; typedef int ElemType; typedef struct { ElemType data[MAXSIZE]; // 数据域 int top; // 栈顶指针 } SqStack; /*初始化栈*/ bool InitStack(SqStack* S) { S->top= -1; // 让栈顶指针指向 -1,表示当前为空栈 return true; } /*判断栈是否为空*/ bool isEmpty(SqStack S) { if (S.top == -1) return true; // 栈空 return false; // 非空 } /*进栈操作*/ bool Push(SqStack* S, ElemType e) { if (S->top == MAXSIZE - 1) return false; // 栈已满 S->top++; // 栈顶指针加一 S->data[S->top] = e; // 将值赋值给新的栈顶元素 return true; } /*出栈操作*/ bool Pop(SqStack* S, ElemType* x) { if (isEmpty(*S)) return false; // 栈空,无出栈元素 *x = S->data[S->top]; S->top--; // 栈顶指针减一 return true; } /*获取栈顶元素*/ bool GetTop(SqStack S, ElemType* x) { if (isEmpty(S)) return false; // 栈空 *x = S.data[S.top]; return true; } int main() { SqStack S; // 定义一个栈 InitStack(&S); // 初始化一个栈 Push(&S, 666); Push(&S, 777); // 判断栈是否为空 isEmpty(S) ? printf("The Stack is NULL\n") : printf("The Stack not is NULL\n"); ElemType x; Pop(&S, &x) ? printf("The pop element is %d\n", x) : printf("Pop failure\n"); GetTop(S, &x) ? printf("Get top element is %d\n", x) : printf("The Stack is NULL\n"); return 0; }
-
共享栈
#include <stdio.h> #define true 1 #define false 0 #define MAXSIZE 10 typedef int bool; typedef int ElemType; typedef struct { ElemType data[MAXSIZE]; int top0; // 0号栈栈顶指针 int top1; // 1号栈栈顶指针 } ShStack; /*初始化栈*/ bool InitStack(ShStack* S) { S->top0 = -1; // 0号栈栈顶指针指向 -1 S->top1 = MAXSIZE; // 1号栈栈顶指针指向 MaxSize } /*判断栈是否为空*/ bool isEmpty(ShStack S) { if (S.top0 == -1 && S.top1 == MAXSIZE) return true; return false; } /*入栈 c=0代表操作0号栈,c=1代表操作1号栈*/ bool Push(ShStack* S, ElemType e, int c) { if (c == 0)// 操作0号栈 { if (S->top0 + 1 == S->top1) return false; // 栈满 S->data[++S->top0] = e; // 将top0加一,并填入新元素 return true; } else if (c == 1) // 操作1号栈 { if (S->top1 - 1 == S->top0) return false; // 栈满 S->data[--S->top1] = e; // 将top1减一,并填入新元素 return true; } return false; // c参数输入不合法 } /*弹栈 c=0代表操作0号栈,c=1代表操作1号栈*/ bool Pop(ShStack* S, ElemType* x, int c) { if (c == 0)// 操作0号栈 { if (S->top0 == -1) return false; // 栈空 *x = S->data[S->top0--]; // 出栈,栈顶指针减一 return true; } else if (c == 1) // 操作1号栈 { if (S->top1 == MAXSIZE) return false; // 栈空 *x = S->data[S->top1++]; // 出栈,栈顶指针加一 return true; } return false; // c参数输入不合法 } int main() { ShStack S; InitStack(&S); // 初始化栈 isEmpty(S); // 判断栈是否为空 Push(&S, 985, 0); // 0号栈入栈 Push(&S, 211, 1); // 1号栈入栈 ElemType x; Pop(&S, &x, 0); // 0号栈弹栈 Pop(&S, &x, 1); // 1号栈弹栈 return 0; }
3.1.3 栈的链式存储实现
-
不带头结点
#include <stdio.h> #include <stdlib.h> #define true 1 #define false 0 typedef int bool; typedef int ElemType; typedef struct LinkNode { // 定义链栈的结构体 ElemType data; // 数据域 struct LinkNode* next; // 指针域 }LinkNode,*LiStack; /*初始化链栈*/ bool InitStack(LiStack* S) { *S = NULL; return true; } /*判空*/ bool isEmpty(LiStack S) { return S == NULL; } /*入栈*/ bool Push(LiStack* S, ElemType e) { if (isEmpty(*S)) // 当前链栈为空 { *S = (LinkNode*)malloc(sizeof(LinkNode)); if (*S == NULL) // 内存不足,分配失败 return false; (*S)->data = e; (*S)->next = NULL; return true; } // 链栈不为空时 LinkNode* p = (LinkNode*)malloc(sizeof(LinkNode)); if (p == NULL) return false; p->data = e; p->next = *S; // 让新节点指向原来的第一个节点 *S = p; // 让头指针指向新节点 return true; } /*弹栈*/ bool Pop(LiStack* S, ElemType* x) { if (isEmpty(*S)) // 链栈为空 return false; if ((*S)->next == NULL) // 链栈中只有一个节点 { *x = (*S)->data; // 把仅有的一个节点的数据赋值给x free(*S); // 把仅有的那个节点空间释放掉 *S = NULL; return true; } // 链栈中不只有一个节点 LinkNode* p = (*S)->next; *x = (*S)->data; free(*S); // 释放第一个节点 *S = p; // 让栈指针指向原来位序为2的节点 return true; } /*获取栈顶元素*/ bool GetTop(LiStack S, ElemType* x) { if (isEmpty(S)) return false; *x = S->data; } int main() { LiStack S; InitStack(&S); isEmpty(S); Push(&S, 666); Push(&S, 777); ElemType x; Pop(&S, &x); GetTop(S, &x); Pop(&S, &x); Pop(&S, &x); return 0; }
-
带头结点
#include <stdio.h> #include <stdlib.h> #define true 1 #define false 0 typedef int bool; typedef int ElemType; typedef struct LinkNode { // 定义链栈的结构体 ElemType data; // 数据域 struct LinkNode* next; // 指针域 }LinkNode, * LiStack; /*初始化链栈*/ bool InitStack(LiStack* S) { *S = (LinkNode*)malloc(sizeof(LinkNode)); if (*S == NULL) return false; (*S)->next = NULL; } /*判空*/ bool isEmpty(LiStack S) { return S->next == NULL; } /*入栈*/ bool Push(LiStack S, ElemType e) { LinkNode* p = (LinkNode*)malloc(sizeof(LinkNode)); if (p == NULL) return false; p->data = e; p->next = S->next; // 让新节点指向头节点的下一个节点 S->next = p; // 让头节点指向 新节点 return true; } /*弹栈*/ bool Pop(LiStack S, ElemType* x) { if (isEmpty(S)) return false; LinkNode* p = S->next; // 让p指向即将被删除的节点 *x = p->data; S->next = p->next; // 断链。让头节点的next指向被删除节点的下一个节点 free(p); // 释放被删除节点的空间 return true; } bool Pop(LiStack* S, ElemType* x); /*获取栈顶元素*/ bool GetTop(LiStack S, ElemType* x) { if (isEmpty(S)) return false; *x = S->next->data; return true; } int main() { LiStack S; InitStack(&S); Push(S, 666); Push(S, 777); isEmpty(S); ElemType x; GetTop(S, &x); Pop(S, &x); Pop(S, &x); Pop(S, &x); return 0; }
3.2 队列
3.2.1 队列的基本概念
-
队列的定义:队列(Queue)是只允许在一端进行插入,在另一端删除的线性表
-
队列的基本操作:
InitQueue(&Q)
:初始化队列,构造一个空队列Q。DestroyQueue(&Q)
:销毁队列。销毁并释放队列Q所占用的内存空间。EnQueue(&Q,x)
:入队,若队列Q未满,将x加入,使之成为新的队尾。DeQueue(&Q,&x)
:出队,若队列Q非空,删除队头元素,并用x返回。GetHead(Q,&x)
:读队头元素,若队列Q非空,则将队头元素赋值给x。其他常用操作:
QueueEmpty(Q)
:判队列空,若队列Q为空返回true,否则返回false。
3.2.2 队列的顺序实现(循环队列)
-
顺序实现的队列我们只讨论循环队列,因为不能循环得队列没意义
-
循环队列(初始化Q.front=0; Q.rear=0)
#include <stdio.h> #include <stdlib.h> #define true 1 #define false 0 #define MAXSIZE 10 typedef int bool; typedef int ElemType; typedef struct { ElemType data[MAXSIZE]; int front, rear; // 队头指针和队尾指针(头出尾进) } SqQueue; /*初始化队列*/ void InitQueue(SqQueue* Q) { // 初始时对头指针和队尾指针都指向 0 (*Q).front = (*Q).rear = 0; } /*判断队列是否为空*/ bool isEmpty(SqQueue Q) { return Q.front == Q.rear; } /*入队*/ bool EnQueue(SqQueue* Q, ElemType e) { if (((*Q).rear + 1) % MAXSIZE == (*Q).front) return false; // 队满 (*Q).data[(*Q).rear] = e; // 新元素插入队尾 (*Q).rear = ((*Q).rear + 1) % MAXSIZE; // 队尾指针加 1 取模 return true; } /*出队*/ bool DeQueue(SqQueue* Q, ElemType* x) { if (isEmpty(*Q)) return false; // 队列为空 *x = (*Q).data[(*Q).front]; (*Q).front = ((*Q).front + 1) % MAXSIZE; // 队头指针后移 return true; } /*获取队头元素值*/ bool GetHead(SqQueue Q, ElemType* x) { if (isEmpty(Q)) return false; *x = Q.data[Q.front]; return true; } int main() { SqQueue Q; InitQueue(&Q); EnQueue(&Q, 666); ElemType x; GetHead(Q, &x); DeQueue(&Q, &x); DeQueue(&Q, &x); return 0; }
-
判断队列已满/已空
初始化时
rear=front=0
- 队空的条件:
Q.front == Q.rear
- 队满的条件:牺牲一个存储位置:
(Q.rear + 1) % MAXSIZE == Q.front
在结构体添加一个size属性记录当前队列的长度
- 队空条件:
Q.size == 0
- 队满的条件:
Q.size == MAXSIZE
在结构体添加一个tag属性每次删除操作成功时,都令tag=0;每次插入操作成功时,都令tag=1;
- 队空条件:
Q.front == Q.rear && tag == 0
- 队满的条件:
Q.front == Q.rear && tag == 1
- 队空的条件:
-
还有其他出题方法,比如出题人初始时front仍然指向0不变,但是rear要求实时指向当前队列中的元素,那么我们最好将rear的初始值定为 MAXSIZE - 1。这样的话判队空/满的条件也就都发生变化了,所以学东西一定要灵活,重在理解。
🧡💚💛3.2.3 队列的链式实现
-
带头结点
#include <stdio.h> #include <stdlib.h> #define true 1 #define false 0 typedef int bool; typedef int ElemType; // ★★★★★ 之前从来没有定义过两个结构体 typedef struct LinkNode { // 链式队列节点 ElemType data; struct LinkNode *next; }LinkNode; typedef struct { // 链式队列 LinkNode *front, *rear; // 队列的队头指针和队尾指针 }QueueNode, *LinkQueue; /*初始化队列*/ bool InitQueue(LinkQueue* Q) { // ★★★★★ 之前写过的数据结构没这样写过 *Q = (LinkQueue)malloc(sizeof(QueueNode)); // 先给队列Q申请空间 if (*Q == NULL) return false; // 申请头节点 LinkNode *s = (LinkNode*)malloc(sizeof(LinkNode)); if (s == NULL) return false; s->next = NULL; (*Q)->front = (*Q)->rear = s; // Q的头指针和尾指针都指向头节点 } /*判断队列是否为空*/ bool isEmpty(LinkQueue Q) { return Q->front == Q->rear; } /*入队*/ bool EnQueue(LinkQueue Q, ElemType e) { LinkNode* s = (LinkNode*)malloc(sizeof(LinkNode)); if (s == NULL) return false; s->data = e; s->next = NULL; Q->rear->next = s; // 当前队尾节点指向新插入的节点 Q->rear = s; // 保证队列的rear指针一直指向队尾元素 return true; } /*出队*/ bool DeQueue(LinkQueue Q, ElemType* x) { if (isEmpty(Q)) return false; // 队列为空 LinkNode* s = Q->front->next; // 让s指向队列链表中的除头节点的第一个节点 *x = s->data; Q->front->next = s->next; // 让头节点指向出队节点的下一个节点 if (s == Q->rear) // 当前出队的是队中的最后一个节点(★★★★★) Q->rear = Q->front; // 修改 rear 的指针 return true; } int main() { LinkQueue Q; LinkNode* L; InitQueue(&Q); free(Q); EnQueue(Q, 111); EnQueue(Q, 222); ElemType x; DeQueue(Q, &x); DeQueue(Q, &x); DeQueue(Q, &x); return 0; }
-
不带头结点
#include <stdio.h> #include <stdlib.h> #define true 1 #define false 0 typedef int bool; typedef int ElemType; typedef struct LinkNode { // 链式队列的节点 ElemType data; struct LinkNode* next; } LinkNode; typedef struct { // 链式队列 LinkNode *front, *rear; }QueueNode, *LinkQueue; /*初始化队列*/ bool InitQueue(LinkQueue* Q) { *Q = (QueueNode*)malloc(sizeof(QueueNode)); if (*Q == NULL) return false; (*Q)->front = (*Q)->rear = NULL; // 头指针和尾指针都指向NULL return true; } /*判空*/ bool isEmpty(LinkQueue Q) { return Q->front == NULL; } /*入队*/ bool EnQueue(LinkQueue Q, ElemType e) { LinkNode* s = (LinkNode*)malloc(sizeof(LinkNode)); if (s == NULL) return false; s->data = e; s->next = NULL; if (Q->front == NULL) // 在空队列中插入第一个元素 { Q->front = Q->rear = s; // 修改队头队尾指针 return true; } Q->rear->next = s; // 将新节点插入到 rear 节点之后 Q->rear = s; // 修改 rear指针 return true; } /*出队*/ bool DeQueue(LinkQueue Q, ElemType* x) { if (Q->front == NULL) return false; LinkNode* s = Q->front; *x = s->data; if (Q->front == Q->rear) // 此次是最后一个节点出队 Q->front = Q->rear = NULL; // front 和 rear都指向NULL Q->front = s->next; // 修改front 指针 free(s); return true; } int main() { LinkQueue Q; InitQueue(&Q); EnQueue(Q, 111); EnQueue(Q, 222); ElemType x; DeQueue(Q, &x); DeQueue(Q, &x); DeQueue(Q, &x); return 0; }
3.2.4 双端队列
3.3 栈和队列的应用
3.3.1 栈在括号匹配中的应用
-
流程图
-
处理流程:
- 遇到左括号 ([{ :进栈
- 遇到右括号 )]} :弹栈 & 判断是否匹配
- 遇到非括号:数据输入不合法,匹配失败(){}[]
-
三种失败情况:
- 在处理过程中括号不匹配,失败。如:(){]
- 在处理过程中,遇到右括号,需要弹栈,但是栈空,失败。如:())
- 括号处理完之后,栈不为空,说明括号元素有多余,失败。如:{()
/*括号是否匹配*/
bool bracketCheck(char str[], int length)
{
// 注意:length接收的长度是实际 字符串长度 + 1,因为有'\0'
SqStack S;
InitStack(&S); // 初始化一个栈
for (int i = 0; i < length - 1; i++)
{
if (str[i] == '(' || str[i] == '[' || str[i] == '{') { // 扫描到左括号
Push(&S, str[i]); // 扫描到左括号元素入栈
} else if(str[i] == ')' || str[i] == ']' || str[i] == '}') { // 扫描到右括号
if (isEmpty(S)) { // 栈为空
return false; // 匹配失败
} else { // 栈不为空
ElemType topElem;
Pop(&S, &topElem); // 栈顶元素出栈
if (str[i] == ')' && topElem != '(')
return false;
if (str[i] == ']' && topElem != '[')
return false;
if (str[i] == '}' && topElem != '{')
return false;
}
} else{
return false; // 表示输入数据不合法
}
}
return isEmpty(S); // 若此时栈为空则说明完全匹配,且无剩余,表示匹配成功;
}
int main()
{
char str[] = "{()";
bool flag = bracketCheck(str, sizeof(str) / sizeof(char));
flag ? printf("匹配成功\n") : printf("匹配失败\n");
return 0;
}
3.3.2 栈在表达式求值中的应用
-
中缀转后缀(手算)
(1)确定中缀表达式各个运算符的运算顺序(采用“左优先原则”)
(2)选择下一个运算符,按照<左操作数 右操作数 运算符>的方式组合成一个新的操作数
(3)如果还有运算符没被处理,就继续(2)eg: A+B*(C-D)-E/F
运算次序:3 2 1 5 4
ABCD-*+EF/-
123 45eg: A+B-CD/E+F
运算次序: 1 4 2 3 5
AB+CDE/-F+
1 2 34 5
- 后缀表达式的运算(手算)
从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应运算,合体为一个操作数
- 后缀表达式的运算(机算)【这也正是后缀转中缀的算法】
用栈实现后缀表达式的计算:
(1)从左往右扫描,直到处理完所有元素
(2)若扫描到操作数则压栈,并且返回(1),否则执行(3)
(3)若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到(1)
———若表达式合法,则最后栈中只会留下一个元素,就是最终结果
注意:先出栈的是“右操作数”
-
中缀转前缀(手算)
(1)确定中缀表达式各个运算符的运算顺序(采用“右优先原则”)
(2)选择下一个运算符,按照<运算符 左操作数 右操作数>的方式组合成一个新的操作数
(3)如果还有运算符没被处理,就继续(2)A+B*(C-D)–E/F
5 3 2 4 1
+A-*B-CD/EF
5 43 2 1
+A-*B-CD/EF
- 前缀表达式的计算(手算)【这也正是前缀转中缀的算法】
用栈实现前缀表达式的计算:
(1)从右往左扫描下一个元素,直到处理完所有元素
(2)若扫描到操作数则压入栈,并回到(1);否则执行(3)
(3)若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到(1)
注意:先取出的是左操作数
- 中缀表达式转后缀表达式(机算)
初始化一个栈,用于暂时保存还不能确定运算顺序的操作符。
从左至右处理各个元素,直到末尾。可能遇到三种情况:
(1)遇到操作数。直接加入后缀表达式。
(2)遇到界限符。遇到’(‘直接入栈;遇到’)‘则依次弹出栈内运算符并加入后缀表达式,直到
弹出’(‘为止。注意’(‘不加入后缀表达式。
(3)遇到运算符。依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,
若碰到’('或栈空则停止。之后再把当前运算符入栈。
按上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。
- 中缀表达式的计算(机算)
初始化两个栈,操作数栈和运算符栈
若扫描到操作数,压入操作数栈
若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈(期间也会弹出
运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,
运算结果再压回操作数栈)
3.3.3 栈在递归中的应用
-
函数调用的特点:最后被调用的函数最先执行结束(LIFO)
-
函数调用时,需要用一个“函数调用栈” 存储:
① 调用返回地址
② 实参
③ 局部变量
- 递归调用时,函数调用栈可称为“递归工作栈”每进入一层递归,就将递归调用所需信息压入栈顶每退出一层递归,就从栈顶弹出相应信息。
- 缺点:效率低,太多层递归可能会导致栈溢出;可能包含很多重复计算
3.3.4 队列的应用
- 树的层序遍历
- 图的广度优先遍历
- 队列在操作系统中的应用
3.4 数组和特殊矩阵
####3.4.1 对称矩阵
特点:对方阵中的任意一个元素都有ai,j = aj,i
压缩:只存储主对角线+下三角区(或主对角线+上三角区)
3.4.2 三角矩阵
特点:上三角区全为常量(下三角矩阵);或下三角区全为常量(上三角矩阵)
压缩:按行/列优先规则依次存储非常量区域,并在最后一个元素存储常量c
####3.4.3 三对角矩阵
特点:当|i - j| > 1时,有ai,j = 0(1<=j, j<=n)
压缩:按行/列优先规则依次存储带状区域
####3.4.4 稀疏矩阵
特点:非零元素远小于零元素个数
压缩:只存储非零元素
顺序存储:顺序存储三元组<行、列、值>
链式存储:十字链表法