栈和队列

逻辑结构

栈是只允许在一端进行插入和删除操作的线性表

特点是先进后出

image-20220507151516634

n个不同元素进栈,出栈元素不同排列的个数为$\frac{1}{n+1}C^n_{2n}$(卡特兰数)

基本操作

创建,元素进栈,出栈,获取栈顶元素

存储结构

顺序栈

定义

1
2
3
4
typedef struct {
int data[MaxSize]; //栈的最大值
int top; //栈顶指针
}SqStack;

初始化

1
2
3
4
void InitStack(SqStack  *S)
{
S->top=0;
}

进栈

1
2
3
4
void Push(SqStack* S, int x) {
S->data[S->top] = x;
S->top++; //栈顶指针永远指向下一个元素进来的位置
}

出栈

1
2
3
4
void Pop(SqStack* S,int x) {
S->top--;
x = S->data[S->top];
}

读取栈顶元素

1
2
3
4
5
void gettop(SqStack* S,int x) {
int t=S->top-1
x = S->data[t];
return x;
}

链式栈

头插法创建链表就是链式栈


队列

逻辑结构

队列是只允许在一端进行插入,在另一端进行删除的线性表

特点是先进先出

image-20220507155526596


基本操作

创建,初始化,入队,出队,读队头元素


存储结构

顺序队列

定义

1
2
3
4
typedef struct {
int data[MaxSize]; //静态数组存放队列元素
int front, rear; //队头元素和队尾元素
}SqQueue;

初始化

1
2
3
4
5
void Init(SqQueue* q)
{
q->front = 0; //队头和队尾指针为0
q->rear = 0;
}

入队

(以下是队尾指针指向队尾元素的后一个位置,如果队尾指针指向队尾元素,就要先让队尾指针+1,再进行插入操作)

1
2
3
4
void  Insert(SqQueue *q,int x) {
q->data[q->rear] = x; //插入到队尾
q->rear = q->rear + 1; //队尾指针加1
}

出队

1
2
3
4
5
6
7
8
9
//删除一个队头元素,并用x返回
void DeQueue(SqQueue* q,int *x) {
if (((q->rear) + 1) % MaxSize == q->front) { // 需要牺牲一个空间来判断是否满队列
// 不能用rear=front判断,因为判空就是根据指针相同
printf("full");
}
x = q->data[q->front];
q->front = (q->front + 1) % MaxSize; // MOD元素让队列变成了循环队列
}

队满:

(q->rear) + 1) % MaxSize == q->front

image-20220507161556684

队空:

q->rear == q->front

image-20220507161801286

为了区别队空和队满时的状态,除了牺牲一个空间以外,也可以用标志位tag进行判断


链式队列

image-20210323153544209

定义

1
2
3
4
5
6
7
8
typedef struct LinkNode{
ElemType data ;
struct LinkNode *next;
}LinkNode;

typedef struct {
LinkNode *front,*rear;
}LinkQueue

初始化

1
2
3
4
5
void InitQueue(LinkQueue &Q){
//初始化时front和rear都指向头结点
Q.front=Q.rear=(LinkNode*)malloc(sizeof(LinkNode));
Q.front->next=null;
}

入队

1
2
3
4
5
6
7
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;
}

出队

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool DeQueue (LinkQueue &Q,ElemType &x){
if(Q.front==Q.rear){ // 如果front=rear则队空
return false;
}
LinKNode *p=Q.front->next;
x=p->data;
Q.front->next=p->next;
if(Q.rear==p) //如果rear=p,那么队空了
{
Q.rear=Q.front;
}
return true;

}

链式一般不会队满,所以不需要考虑


双端队列

允许从双端插入,双端删除的队列


栈和队列的应用

栈和队列相对来说应用较少,栈一般用来解决成对元素消去的问题

括号匹配问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
char pairs(char a) {
if (a == '}') return '{';
if (a == ']') return '[';
if (a == ')') return '(';
return 0;
}

bool isValid(char* s) {
int n = strlen(s);
if (n % 2 == 1) {
return false;
}
int stk[n + 1], top = 0;
for (int i = 0; i < n; i++) {
char ch = pairs(s[i]);
if (ch) {
if (top == 0 || stk[top - 1] != ch) {
return false;
}
top--;
} else {
stk[top++] = s[i];
}
}
return top == 0;
}

中缀转为后缀表达式

算数表达式由操作数(数字),运算符(+ - × ÷),界限符(括号)组成

运算符在两个操作数后面就称为后缀表达式

手算:

  1. 先确定中缀表达式中各个运算符的运算顺序,

  2. 选择下一个运算符,按照[左操作数 右操作数 运算符]的方式组成一个新的操作数

机算:

  1. 遇到左括号直接入栈,遇到右括号将栈中左括号之后入栈的运算符全部弹栈输出,同时左括号出栈但是不输出

  2. 遇到乘号和除号直接入栈,直到遇到优先级比它更的运算符,依次弹栈。

  3. 遇到加号和减号,如果此时栈空,则直接入栈,否则,将栈中优先级高的运算符依次弹栈(注意:加号和减号属于同一个优先级,所以也依次弹栈)直到栈空或则遇到左括号为止,停止弹栈。(因为左括号要匹配右括号时才弹出)。

  4. 获取完后,将栈中剩余的运算符号依次弹栈输出

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/*中缀转后缀函数*/
void Change(SqStack *S,Elemtype str[])
{
int i=0;
Elemtype e;
InitStack(S); //初始化运算符栈
while(str[i]!='\0') //字符串未到末尾
{
while(isdigit(str[i]))
{/*过滤数字字符,直接输出,直到下一位不是数字字符打印空格跳出循环 */
printf("%c",str[i++]);
if(!isdigit(str[i]))
{
printf(" ");
}
}
/*加减运算符优先级最低,如果栈顶元素为空则直接入栈,否则将栈中存储的运算符
全部弹栈,如果遇到左括号则停止,将弹出的左括号从新压栈,因为左括号要和右括号
匹配时弹出,这个后面单独讨论。弹出后将优先级低的运算符压入栈中*/
if(str[i]=='+'||str[i]=='-')
{
if(!StackLength(S))
{
PushStack(S,str[i]);
}
else
{
do
{
PopStack(S,&e);
if(e=='(')
{
PushStack(S,e);
}
else
{
printf("%c ",e);
}
}while( StackLength(S) && e != '(' );

PushStack(S,str[i]);
}
}
/*当遇到右括号是,把括号里剩余的运算符弹出,直到匹配到左括号为止
左括号只弹出不打印(右括号也不压栈)*/
else if(str[i]==')')
{
PopStack(S,&e);
while(e!='(')
{
printf("%c ",e);
PopStack(S,&e);
}
}
/*乘、除、左括号都是优先级高的,直接压栈*/
else if(str[i]=='*'||str[i]=='/'||str[i]=='(')
{
PushStack(S,str[i]);
}

else if(str[i]=='\0')
{
break;
}

else
{
printf("\n输入格式错误!\n");
return ;
}
i++;
}
/*最后把栈中剩余的运算符依次弹栈打印*/
while(StackLength(S))
{
PopStack(S,&e);
printf("%c ",e);
}
}

后缀表达式求值

  1. 从左往右扫描下一个元素,直到处理完所有元素
  2. 若扫描到操作数则压入栈
  3. 若扫描到运算符,则弹出两个栈顶元素,执行相应运算
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
bool isNumber(char* token) {
return strlen(token) > 1 || ('0' <= token[0] && token[0] <= '9');
}

int evalRPN(char** tokens, int tokensSize) {
int n = tokensSize;
int stk[n], top = 0; //操作数栈
for (int i = 0; i < n; i++) {
char* token = tokens[i];
if (isNumber(token)) {
stk[top++] = atoi(token);
} else {
int num2 = stk[--top];
int num1 = stk[--top];
switch (token[0]) {
case '+':
stk[top++] = num1 + num2;
break;
case '-':
stk[top++] = num1 - num2;
break;
case '*':
stk[top++] = num1 * num2;
break;
case '/':
stk[top++] = num1 / num2;
break;
}
}
}
return stk[top - 1];
}

特殊矩阵的压缩

对称矩阵

若n阶方阵中任意一个元素都有$a_{i,j}=a_{j,i}$,则改矩阵为对称矩阵

image-20220507193320066

可以按行优先原则,只存储主对角线和下三角的元素,用一个大小为$\frac{n(n+1)}{2}$的数组来存储

$a_{i,j}$对应的就是数组中第$\frac{i(i-1)}{2}+j$个元素,下标在此基础上-1

(不需要背公式,主要是理解存储顺序以及下标需要-1


三角矩阵

除了主对角区和上(下)三角区,其余的元素都相同

image-20220507194132179

相比与对称矩阵,三角矩阵需要空间为$\frac{n(n+1)}{2}+1$,多一个空间用于最后存放c这个常量


三对角矩阵

当$|i-j|>1$时,有$a_{i,j=0}$

image-20220507195404275

需要大小为3n-2的数组,前i-1行共3(i-1)-1个元素,$a_{i,j}$是第i行第j-i+2个元素

所以$a_{i,j}$是第2i+j-2个元素


稀疏矩阵

非零元素远远少于矩阵元素的个数

image-20220507200903464

使用三元组,即ijv值的方法存储

image-20220507202249532

假设矩阵中有X个非零元素,除了需要X个三元组之外,还需要一个三元组记录当前矩阵的行数,列数,非零元素个数

也可以使用十字链表