第三章 栈和队列

栈和队列有相同的逻辑结构,即都是线性结构

3.1. 栈

注:卡特兰数要记住

image-20250328151512344

本小节完整代码

栈的顺序存储
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#include<iostream>

using namespace std;

const int MAXSIZE = 10;

/*
1. * *InitStack(&S):初始化栈。构造一个空栈 S,分配内存空间。 * *
2. * *DestroyStack(&S):销毁栈。销毁并释放栈 S 所占用的内存空间。 * *
3. * *Push(&S, x):进栈。若栈 S 未满,则将 x 加入使其成为新的栈顶元素。 * *
4. * *Pop(&S, &x):出栈。若栈 S 非空,则弹出(删除)栈顶元素,并用 x 返回。 * *
5. * *GetTop(S, &x):读取栈顶元素。若栈 S 非空,则用 x 返回栈顶元素。 * *
6. * *StackEmpty(S):判空。断一个栈 S 是否为空,若 S 为空,则返回 true,否则返回 false。 * *
*/

//结构体定义
struct Stack
{
int data[MAXSIZE];
int top;
};

//初始化栈。构造一个空栈 S,分配内存空间。
void InitStack(Stack &s)
{
s.top = -1;
}

//判断一个栈 S 是否为空,若 S 为空,则返回 true,否则返回 false
bool Empty(Stack& s)
{
return s.top == -1;
}

//进栈。若栈 S 未满,则将 x 加入使其成为新的栈顶元素。
bool Push(Stack& s, int x)
{
if (s.top == MAXSIZE - 1)
return false;
s.top++;
s.data[s.top] = x;
return true;
}

//出栈。若栈 S 非空,则弹出(删除)栈顶元素,并用 x 返回
bool Pop(Stack& s, int& x)
{
if (s.top == -1)
return false;
x = s.data[s.top];
s.top--;
return true;
}

//读取栈顶元素。若栈 S 非空,则用 x 返回栈顶元素。
bool GetTop(Stack &s,int& x)
{
if (s.top == -1)
return false;
x = s.data[s.top];
return true;
}

//销毁栈。销毁并释放栈 S 所占用的内存空间。
void DestroyStack(Stack &s)
{
s.top = -1;
}

int main()
{
Stack s;
InitStack(s);
cout << Empty(s) << endl;
int y = 0;
for (int i = 1; i < 6; i++)
{
Push(s,i);
GetTop(s, y);
cout<<y<<" ";
}
cout << endl;
cout << Empty(s) << endl;
int x = 0;
for (int i = 1; i < 6; i++)
{
Pop(s, x);
cout << x << " ";
}
cout << endl;
cout << Empty(s) << endl;
return 0;
}
栈的链式存储
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#include<iostream>

using namespace std;

const int MAXSIZE = 10;

/*
1. * *InitStack(&S):初始化栈。构造一个空栈 S,分配内存空间。 * *
2. * *DestroyStack(&S):销毁栈。销毁并释放栈 S 所占用的内存空间。 * *
3. * *Push(&S, x):进栈。若栈 S 未满,则将 x 加入使其成为新的栈顶元素。 * *
4. * *Pop(&S, &x):出栈。若栈 S 非空,则弹出(删除)栈顶元素,并用 x 返回。 * *
5. * *GetTop(S, &x):读取栈顶元素。若栈 S 非空,则用 x 返回栈顶元素。 * *
6. * *StackEmpty(S):判空。断一个栈 S 是否为空,若 S 为空,则返回 true,否则返回 false。 * *
*/

//结构体定义
typedef struct Linknode {
int data; //数据域
Linknode* next; //指针域
}LNode, *Stack;

//初始化栈。构造一个空栈 S,分配内存空间。
bool InitStack(Stack& s)
{
s = (Stack)malloc(sizeof(Stack));
if (s == NULL)
return false;
s->next = NULL;
return true;
}

//判断一个栈 S 是否为空,若 S 为空,则返回 true,否则返回 false
bool Empty(Stack& s)
{
return s->next==NULL;
}

//进栈。若栈 S 未满,则将 x 加入使其成为新的栈顶元素。
bool Push(Stack& s, int x)
{
LNode* p = (LNode*)malloc(sizeof(LNode));
if (p == NULL)
return false;
p->next = s->next;
p->data = x;
s->next = p;
return true;
}

//出栈。若栈 S 非空,则弹出(删除)栈顶元素,并用 x 返回
bool Pop(Stack& s, int& x)
{
if (s->next == NULL)
return false;
LNode* p = s->next;
s->next = p->next;
x = p->data;
free(p);
return true;
}

//读取栈顶元素。若栈 S 非空,则用 x 返回栈顶元素。
bool GetTop(Stack& s, int& x)
{
if (s->next == NULL)
return false;
x = s->next->data;
return true;
}

//销毁栈。销毁并释放栈 S 所占用的内存空间。
void DestroyStack(Stack& s)
{
while (s != NULL)
{
LNode* p = s;
s = s->next;
free(p);
}
}

int main()
{
Stack s;
InitStack(s);
cout << Empty(s) << endl;
int y = 0;
for (int i = 1; i < 6; i++)
{
Push(s, i);
GetTop(s, y);
cout << y << " ";
}
cout << endl;
cout << Empty(s) << endl;
int x = 0;
for (int i = 1; i < 6; i++)
{
Pop(s, x);
cout << x << " ";
}
cout << endl;
cout << Empty(s) << endl;
return 0;
}

3.1.1. 栈的基本概念

  • 只允许在一端(栈顶top)进行插入或删除操作的受限的线性表。
  • 后进先出(Last In First Out)LIFO。

img

  • 进栈顺序:a1 > a2 > a3 > a4 > a5
  • 出栈顺序:a5 > a4 > a3 > a2 > a1

3.1.2. 栈的基本操作

  1. InitStack(&S):初始化栈。构造一个空栈 S,分配内存空间。
  2. DestroyStack(&S):销毁栈。销毁并释放栈 S 所占用的内存空间。
  3. Push(&S, x):进栈。若栈 S 未满,则将 x 加入使其成为新的栈顶元素。
  4. Pop(&S, &x):出栈。若栈 S 非空,则弹出(删除)栈顶元素,并用 x 返回。
  5. GetTop(S, &x):读取栈顶元素。若栈 S 非空,则用 x 返回栈顶元素。
  6. StackEmpty(S):判空。断一个栈 S 是否为空,若 S 为空,则返回 true,否则返回 false。

3.1.3. 栈的顺序存储实现

img

【顺序栈的定义】

1
2
3
4
5
6
7
8
9
10
11
#define MaxSize 10              //定义栈中元素的最大个数

typedef struct{
ElemType data[MaxSize]; //静态数组存放栈中元素
int top; //栈顶元素
}SqStack;

void testStack(){
SqStack S; //声明一个顺序栈(分配空间)
//连续的存储空间大小为 MaxSize*sizeof(ElemType)
}

顺序栈的初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define MaxSize 10
typedef struct{
ElemType data[MaxSize];
int top;
}SqStack;

// 初始化栈
void InitStack(SqStack &S){
S.top = -1; //初始化栈顶指针
}

// 判断栈是否为空
bool StackEmpty(SqStack S){
if(S.top == -1)
return true;
else
return false;
}

【顺序栈的入栈出栈】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 新元素进栈
bool Push(SqStack &S, ElemType x){ // 判断栈是否已满
if(S.top == MaxSize - 1)
return false;
S.data[++S.top] = x;
return true;
}

// 出栈
bool Pop(SqStack &x, ElemType &x){ // 判断栈是否为空
if(S.top == -1)
return false;
x = S.data[S.top--];
return true;
}

【读取栈顶元素】

1
2
3
4
5
6
7
// 读栈顶元素
bool GetTop(SqStack S, ElemType &x){
if(S.top == -1)
return false;
x = S.data[S.top];
return true;
}
  • 进栈操作:栈不满时,栈顶指针先加1,再送值到栈顶元素。S.data[++S.top] = x
  • 出栈操作:栈非空时,先取栈顶元素值,再将栈顶指针减1。x = S.data[S.top–]
  • 栈空条件:S.top==-
  • 栈满条件:S.top==MaxSize-1
  • 栈长:S.top+1

【共享栈(两个栈共享同一片空间)】

  • 共享栈–特殊的顺序栈
  • 将栈底设计在共享空间的两端,栈顶向中间靠拢

top0=-1时0号栈为空

top1=maxsize时1号栈为空

top1-top0=1时,两个指针相遇栈满

好处:节省存储空间,防止发生上溢的可能

上溢是指存储器满了还让里面写,下溢是指存储器空还往外读

1
2
3
4
5
6
7
8
9
10
11
12
13
#define MaxSize 10         //定义栈中元素的最大个数

typedef struct{
ElemType data[MaxSize]; //静态数组存放栈中元素
int top0; //0号栈栈顶指针
int top1; //1号栈栈顶指针
}ShStack;

//初始化栈
void InitSqStack(ShStack &S){
S.top0 = -1; //初始化栈顶指针
S.top1 = MaxSize;
}

3.1.4. 栈的链式存储

【链栈的定义】

  • 定义:采用链式存储的栈称为链栈。
  • 优点:链栈的优点是便于多个栈共享存储空间和提高其效率,且不存在栈满上溢的情况。
  • 特点:进栈和出栈都只能在栈顶一端进行(链头作为栈顶)

链表的头部作为栈顶,意味着:

    1. 在实现数据”入栈”操作时,需要将数据从链表的头部插入;
    1. 在实现数据”出栈”操作时,需要删除链表头部的首元节点;

因此,链栈实际上就是一个只能采用头插法插入或删除数据的链表;
栈的链式存储结构可描述为:

【链栈的定义】

1
2
3
4
5
6
7
8
typedef struct Linknode{        
ElemType data; //数据域
Linknode *next; //指针域
}Linknode,*LiStack;

void testStack(){
LiStack L; //声明一个链栈
}

【链栈的初始化】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct Linknode{       
ElemType data;
Linknode *next;
}Linknode,*LiStack;

// 初始化栈
bool InitStack(LiStack &L){
L = (Linknode *)malloc(sizeof(Linknode));
if(L == NULL)
return false;
L->next = NULL;
return true;
}

// 判断栈是否为空
bool isEmpty(LiStack &L){
if(L->next == NULL)
return true;
else
return false;
}

【入栈出栈】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 新元素入栈
bool pushStack(LiStack &L,ElemType x){
Linknode *s = (Linknode *)malloc(sizeof(Linknode));
if(s == NULL)
return false;
s->data = x;
// 头插法
s->next = L->next;
L->next = s;
return true;
}

// 出栈
bool popStack(LiStack &L, int &x){
// 栈空不能出栈
if(L->next == NULL)
return false;
Linknode *s = L->next;
x = s->data;
L->next = s->next;
free(s);
return true;
}

3.2. 队列

本小节完整代码

顺序存储队列–牺牲一个存储空间

rear指向要填入元素的位置,而不是最后一个元素

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#include<iostream>

using namespace std;

const int MAXSIZE = 10;

/*
1. InitQueue(&Q):初始化队列,构造一个空队列Q。
2. QueueEmpty(Q):判队列空,若队列Q为空返回true,否则返回false。
3. EnQueue(&Qx):入队,若队列Q未满,则将x加入使之成为新的队尾。
4. DeQueue(&Q & x):出队,若队列Q非空,则删除队头元素,并用x返回。
5. GetHead(Q & x):读队头元素,若队列Q非空则用x返回队头元素。
6. ClearQueue(&Q):销毁队列,并释放队列Q占用的内存空间。
*/

struct queue
{
int data[MAXSIZE];
int front, rear;//头指针和尾指针
};

//初始化队列,构造一个空队列Q。
void InitQueue(queue& q)
{
q.front = 0;
q.rear = 0;
}

//判队列空,若队列Q为空返回true,否则返回false。
bool QueueEmpty(queue& q)
{
return q.front == q.rear;
}

//入队,若队列Q未满,则将x加入使之成为新的队尾。
//最多只可以存储MAXSIZE-1个元素
bool EnQueue(queue& 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;
}

//出队,若队列Q非空,则删除队头元素,并用x返回。
bool DeQueue(queue& q, int &x)
{
//判断是否非空
if (q.rear == q.front)
return false;
x = q.data[q.front];
q.front = ( q.front + 1) % MAXSIZE;
return true;
}

//读队头元素,若队列Q非空则用x返回队头元素。
bool GetHead(queue& q, int& x)
{
//判断是否非空
if (q.rear == q.front)
return false;
x = q.data[q.front];
return true;
}

//销毁队列,并释放队列Q占用的内存空间。
void ClearQueue(queue& q)
{
q.front = 0;
q.rear = 0;
}

int main()
{
queue q;
InitQueue(q);
cout << QueueEmpty(q) << endl;
for (int i = 1; i < 6; i++)
EnQueue(q, i);
cout << QueueEmpty(q) << endl;
int x = 0;
for (int i = 1; i < 6; i++)
{
GetHead(q, x);
cout << x << " ";
DeQueue(q, x);
}
cout << endl;
cout << QueueEmpty(q) << endl;
ClearQueue(q);
return 0;
}

顺序存储队列–加入size变量

rear指向要填入元素的位置,而不是最后一个元素

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#include<iostream>

using namespace std;

const int MAXSIZE = 10;

/*
1. InitQueue(&Q):初始化队列,构造一个空队列Q。
2. QueueEmpty(Q):判队列空,若队列Q为空返回true,否则返回false。
3. EnQueue(&Qx):入队,若队列Q未满,则将x加入使之成为新的队尾。
4. DeQueue(&Q & x):出队,若队列Q非空,则删除队头元素,并用x返回。
5. GetHead(Q & x):读队头元素,若队列Q非空则用x返回队头元素。
6. ClearQueue(&Q):销毁队列,并释放队列Q占用的内存空间。
*/

struct queue
{
int data[MAXSIZE];
int front, rear;//头指针和尾指针
int size;
};

//初始化队列,构造一个空队列Q。
void InitQueue(queue& q)
{
q.front = 0;
q.rear = 0;
q.size = 0;
}

//判队列空,若队列Q为空返回true,否则返回false。
bool QueueEmpty(queue& q)
{
return q.size == 0;
}

//入队,若队列Q未满,则将x加入使之成为新的队尾。
//最多只可以存储MAXSIZE-1个元素
bool EnQueue(queue& q,int x)
{
//队列已经满了
if (q.size==MAXSIZE)
return false;
q.data[q.rear] = x;
q.rear=(q.rear+1)%MAXSIZE;
q.size++;
return true;
}

//出队,若队列Q非空,则删除队头元素,并用x返回。
bool DeQueue(queue& q, int &x)
{
//判断是否非空
if (q.size == 0)
return false;
x = q.data[q.front];
q.front = ( q.front + 1) % MAXSIZE;
q.size--;
return true;
}

//读队头元素,若队列Q非空则用x返回队头元素。
bool GetHead(queue& q, int& x)
{
//判断是否非空
if (q.size == 0)
return false;
x = q.data[q.front];
return true;
}

//销毁队列,并释放队列Q占用的内存空间。
void ClearQueue(queue& q)
{
q.front = 0;
q.rear = 0;
q.size = 0;
}

int main()
{
queue q;
InitQueue(q);
cout << QueueEmpty(q) << endl;
for (int i = 1; i < 11; i++)
EnQueue(q, i);
cout << QueueEmpty(q) << endl;
int x = 0;
for (int i = 1; i < 11; i++)
{
GetHead(q, x);
cout << x << " ";
DeQueue(q, x);
}
cout << endl;
cout << QueueEmpty(q) << endl;
ClearQueue(q);
return 0;
}

顺序存储队列–加入tag变量
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#include<iostream>

using namespace std;

const int MAXSIZE = 10;

/*
1. InitQueue(&Q):初始化队列,构造一个空队列Q。
2. QueueEmpty(Q):判队列空,若队列Q为空返回true,否则返回false。
3. EnQueue(&Qx):入队,若队列Q未满,则将x加入使之成为新的队尾。
4. DeQueue(&Q & x):出队,若队列Q非空,则删除队头元素,并用x返回。
5. GetHead(Q & x):读队头元素,若队列Q非空则用x返回队头元素。
6. ClearQueue(&Q):销毁队列,并释放队列Q占用的内存空间。
*/

struct queue
{
int data[MAXSIZE];
int front, rear;//头指针和尾指针
int tag;
};

//初始化队列,构造一个空队列Q。
void InitQueue(queue& q)
{
q.front = 0;
q.rear = 0;
q.tag = 0;
}

//判队列空,若队列Q为空返回true,否则返回false。
bool QueueEmpty(queue& q)
{
return q.front == q.rear && q.tag == 0;
}

//入队,若队列Q未满,则将x加入使之成为新的队尾。
//最多只可以存储MAXSIZE-1个元素
bool EnQueue(queue& q, int x)
{
if (q.rear == q.front && q.tag == 1)
return false;
q.data[q.rear] = x;
q.rear = (q.rear + 1) % MAXSIZE;
q.tag = 1;
return true;
}

//出队,若队列Q非空,则删除队头元素,并用x返回。
bool DeQueue(queue& q, int& x)
{
//判断是否非空
if (q.rear == q.front && q.tag == 0)
return false;
x = q.data[q.front];
q.front = (q.front + 1) % MAXSIZE;
q.tag = 0;
return true;
}

//读队头元素,若队列Q非空则用x返回队头元素。
bool GetHead(queue& q, int& x)
{
//判断是否非空
if (q.rear == q.front)
return false;
x = q.data[q.front];
return true;
}

//销毁队列,并释放队列Q占用的内存空间。
void ClearQueue(queue& q)
{
q.front = 0;
q.rear = 0;
q.tag = 0;
}

int main()
{
queue q;
InitQueue(q);
cout << QueueEmpty(q) << endl;
for (int i = 1; i < 6; i++)
EnQueue(q, i);
cout << QueueEmpty(q) << endl;
int x = 0;
for (int i = 1; i < 6; i++)
{
GetHead(q, x);
cout << x << " ";
DeQueue(q, x);
}
cout << endl;
cout << QueueEmpty(q) << endl;
ClearQueue(q);
return 0;
}

链式存储队列
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#include<iostream>

using namespace std;

const int MAXSIZE = 10;

/*
1. InitQueue(&Q):初始化队列,构造一个空队列Q。
2. QueueEmpty(Q):判队列空,若队列Q为空返回true,否则返回false。
3. EnQueue(&Qx):入队,若队列Q未满,则将x加入使之成为新的队尾。
4. DeQueue(&Q & x):出队,若队列Q非空,则删除队头元素,并用x返回。
5. GetHead(Q & x):读队头元素,若队列Q非空则用x返回队头元素。
6. ClearQueue(&Q):销毁队列,并释放队列Q占用的内存空间。
*/

struct LNode
{
int data;
LNode* next;
};

struct queue
{
LNode* front, * rear;//头指针和尾指针
};


//初始化队列,构造一个空队列Q。
void InitQueue(queue& q)
{
q.front = NULL;
q.rear = NULL;
}

//判队列空,若队列Q为空返回true,否则返回false。
bool QueueEmpty(queue& q)
{
return q.front == NULL;
}

//入队,若队列Q未满,则将x加入使之成为新的队尾。
//最多只可以存储MAXSIZE-1个元素
bool EnQueue(queue& q, int x)
{
LNode* p = (LNode*)malloc(sizeof(LNode));
p->data = x;
p->next = NULL;
if (q.front == NULL)
{
q.front = p;
q.rear = p;
}
else
{
q.rear->next = p;
q.rear = p;
}
return true;
}

//出队,若队列Q非空,则删除队头元素,并用x返回。
bool DeQueue(queue& q, int& x)
{
//空队列
if (q.front == NULL)
return false;
LNode* p = q.front;
x = p->data;
q.front = q.front->next;
if (p == q.rear)
{
q.front = NULL;
q.rear = NULL;
}
free(p);
return true;
}

//读队头元素,若队列Q非空则用x返回队头元素。
bool GetHead(queue& q, int& x)
{
//判断是否非空
if (NULL == q.front)
return false;
x = q.front->data;
return true;
}

//销毁队列,并释放队列Q占用的内存空间。
void ClearQueue(queue& q)
{
while (q.front != NULL)
{
LNode* p = q.front;
q.front = q.front->next;
free(p);
}
}

int main()
{
queue q;
InitQueue(q);
cout << QueueEmpty(q) << endl;
for (int i = 1; i < 6; i++)
EnQueue(q, i);
cout << QueueEmpty(q) << endl;
int x = 0;
for (int i = 1; i < 6; i++)
{
GetHead(q, x);
cout << x << " ";
DeQueue(q, x);
}
cout << endl;
cout << QueueEmpty(q) << endl;
ClearQueue(q);
return 0;
}

双端队列

3.2.1. 队列的基本概念

image-20250328135341766

  • 只允许在表的一端(队尾)插入,表的另一端(队头)进行删除操作的受限的线性表。
  • 特点:先进先出(先入队的元素先出队)、FIFO(First In First Out)。

img

3.2.2. 队列的基本操作

  1. InitQueue(&Q):初始化队列,构造一个空队列Q。
  2. QueueEmpty(Q):判队列空,若队列Q为空返回true,否则返回false。
  3. EnQueue(&Qx):入队,若队列Q未满,则将x加入使之成为新的队尾。
  4. DeQueue(&Q&x):出队,若队列Q非空,则删除队头元素,并用x返回。
  5. GetHead(Q&x):读队头元素,若队列Q非空则用x返回队头元素。
  6. ClearQueue(&Q):销毁队列,并释放队列Q占用的内存空间。

3.2.3. 队列的顺序存储实现

img

  • 队头指针:指向队头元素
  • 队尾指针:指向队尾元素的下一个位置

【顺序队列的定义】

1
2
3
4
5
6
7
8
9
10
#define MaxSize 10;     //定义队列中元素的最大个数

typedef struct{
ElemType data[MaxSize]; //用静态数组存放队列元素
int front, rear; //队头指针和队尾指针
}SqQueue;

void test{
SqQueue Q; //声明一个队列
}

顺序队列的初始化】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#define MaxSize 10;

typedef struct{
ElemType data[MaxSize];
int front, rear;
}SqQueue;

// 初始化队列
void InitQueue(SqQueue &Q){
// 初始化时,队头、队尾指针指向0
// 队尾指针指向的是即将插入数据的数组下标
// 队头指针指向的是队头元素的数组下标
Q.rear = Q.front = 0;
}

// 判断队列是否为空
bool QueueEmpty(SqQueue Q){
if(Q.rear == Q.front)
return true;
else
return false;
}

【入队出队(循环队列)】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 新元素入队
bool EnQueue(SqQueue &Q, ElemType 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, ElemType &x){
// 如果队列为空直接返回
if(Q.rear == Q.front)
return false;
x = Q.data[Q.front];
Q.front = (Q.front+1)%MaxSize;
return true;
}

获得队头元素】

1
2
3
4
5
6
7
// 获取队头元素并存入x
bool GetHead(SqQueue &Q, ElemType &x){
if(Q.rear == Q.front)
return false;
x = Q.data[Q.front];
return true;
}
  • 循环队列不能使用Q.rear == Q.front作为判空的条件,因为当队列已满时也符合该条件,会与判空发生冲突!

**解决方法一:**牺牲一个单元来区分队空和队满,即将(Q.rear+1)%MaxSize == Q.front作为判断队列是否已满的条件。(主流方法)
**解决方法二:**设置 size 变量记录队列长度。

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
#define MaxSize 10; 

typedef struct{
ElemType data[MaxSize];
int front, rear;
int size;
}SqQueue;

// 初始化队列
void InitQueue(SqQueue &Q){
Q.rear = Q.front = 0;
Q.size = 0;
}

// 判断队列是否为空
bool QueueEmpty(SqQueue 0){
if(Q.size == 0)
return true;
else
return false;
}

// 新元素入队
bool EnQueue(SqQueue &Q, ElemType x){
if(Q.size == MaxSize)
return false;
Q.size++;
Q.data[Q.rear] = x;
Q.rear = (Q.rear+1)%MaxSize;
return true;
}

// 出队
bool DeQueue(SqQueue &Q, ElemType &x){
if(Q.size == 0)
return false;
Q.size--;
x = Q.data[Q.front];
Q.front = (Q.front+1)%MaxSize;
return true;
}

解决方法三:设置 tag 变量记录队列最近的操作。(tag=0:最近进行的是删除操作;tag=1 :最近进行的是插入操作)

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
#define MaxSize 10;   

typedef struct{
ElemType data[MaxSize];
int front, rear;
int tag;
}SqQueue;

// 初始化队列
void InitQueue(SqQueue &Q){
Q.rear = Q.front = 0;
Q.tag = 0;
}

// 判断队列是否为空,只有tag==0即出队的时候才可能为空
bool QueueEmpty(SqQueue 0){
if(Q.front == Q.rear && Q.tag == 0)
return true;
else
return false;
}

// 新元素入队
bool EnQueue(SqQueue &Q, ElemType x){
if(Q.rear == Q.front && tag == 1)
return false;
Q.data[Q.rear] = x;
Q.rear = (Q.rear+1)%MaxSize;
Q.tag = 1;
return true;
}

// 出队
bool DeQueue(SqQueue &Q, ElemType &x){
if(Q.rear == Q.front && tag == 0)
return false;
x = Q.data[Q.front];
Q.front = (Q.front+1)%MaxSize;
Q.tag = 0;
return true;
}

3.2.4. 队列的链式存储实现

image-20250328150704010

【链队列的定义】

1
2
3
4
5
6
7
8
9
10
11
// 链式队列结点
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}

// 链式队列
typedef struct{
// 头指针和尾指针
LinkNode *front, *rear;
}LinkQueue;

链队列的初始化(带头结点)】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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){
if(Q.front == Q.rear)
return true;
else
return false;
}

入队出队(带头结点)】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 新元素入队
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;
// 如果p是最后一个结点,则将队头指针也指向NULL
if(Q.rear == p)
Q.rear = Q.front;
free(p);
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
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
typedef struct LinkNode{   
ElemType data;
struct LinkNode *next;
}LinkNode;

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

// 初始化队列
void InitQueue(LinkQueue &Q){
// 不带头结点的链队列初始化,头指针和尾指针都指向NULL
Q.front = NULL;
Q.rear = NULL;
}

// 判断队列是否为空
bool IsEmpty(LinkQueue Q){
if(Q.front == NULL)
return true;
else
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 *s = Q.front;
x = s->data;
if(Q.front == Q.rear){
Q.front = Q.rear = NULL;
}else{
Q.front = Q.front->next;
}
free(s);
return true;
}

3.2.5. 双端队列

image-20250328150630486

双端队列定义

  • 双端队列是允许从两端插入、两端删除的线性表。
  • 如果只使用其中一端的插入、删除操作,则等同于栈。
  • 输入受限的双端队列:允许一端插入,两端删除的线性表。
  • 输出受限的双端队列:允许两端插入,一端删除的线性表。

img

**双端队列考点:**判断输出序列的合法化

  • 例:数据元素输入序列为 1,2,3,4,判断 4! = 24 个输出序列的合法性
    输入受限的双端队列:只有 4213 和 4231 不合法
    输出受限的双端队列:只有 4132 和 4231 不合法

在把双端队列当做栈的情况

image-20250328150441627

3.3. 栈与队列的应用

3.3.1 栈在括号匹配中的应用

  • 用栈实现括号匹配:
    1. 最后出现的左括号最先被匹配 (栈的特性——LIFO)。
    2. 遇到左括号就入栈。
    3. 遇到右括号,就“消耗”一个左括号(出栈)。
  • 匹配失败情况:
    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
33
34
35
36
37
38
#define MaxSize 10 
typedef struct{
char data[MaxSize];
int top;
}SqStack;

void InitStack(SqStack &S);
bool StackEmpty(SqStack &S);
bool Push(SqStack &S, char x);
bool Pop(SqStack &S, char &x);

// 判断长度为length的字符串str中的括号是否匹配
bool bracketCheck(char str[], int length){
SqStack S;
InitStack(S);
// 遍历str
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;
// 用topElem接收栈顶元素
Pop(S, topElem);
// 括号不匹配
if(str[i] == ')' && topElem != '(' )
return false;
if(str[i] == ']' && topElem != '[' )
return false;
if(str[i] == '}' && topElem != '{' )
return false; }
}
// 扫描完毕若栈空则说明字符串str中括号匹配
return StackEmpty(S);
}
leetcode练习题目

20. 有效的括号 - 力扣(LeetCode)

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
class Solution {
public:
bool isValid(string s) {
stack<char> st;
for(auto c:s)
{
if(c=='('||c=='{'||c=='[')
st.push(c);
else
{
if(st.empty())
return false;
char temp=st.top();
st.pop();
if(c=='}'&&temp!='{')
return false;
else if(c==')'&&temp!='(')
return false;
else if(c==']'&&temp!='[')
return false;
}

}
return st.empty();
}
};
//下面的做法也可以,基本思想是遇到左括号输入右括号,然后如果相等就弹出,不相等就是false

/*
class Solution {
public:
bool isValid(string s) {
stack<char> st;
for(auto c:s)
{
if(c=='(')
st.push(')');
else if(c=='[')
st.push(']');
else if(c=='{')
st.push('}');
else if(!st.empty()&&c==st.top())
st.pop();
else
return false;
}
return st.empty();
}
};*/

3.3.2. 栈在表达式求值中的应用

image-20250331185326655

  • 中缀表达式:中缀表达式是一种通用的算术或逻辑公式表示方法,运算符以中缀形式处于操作数的中间。对于计算机来说中缀表达式是很复杂的,因此计算表达式的值时,通常需要先将中缀表达式转换为前缀或后缀表达式,然后再进行求值。
  • 前缀表达式(波兰表达式):前缀表达式的运算符位于两个操作数之前。
  • 后缀表达式(逆波兰表达式):后缀表达式的运算符位于两个操作数之后。

中缀表达式转后缀表达式-手算
步骤1: 确定中缀表达式中各个运算符的运算顺序
步骤2: 选择下一个运算符,按照[左操作数 右操作数 运算符]的方式组合成一个新的操作数
步骤3: 如果还有运算符没被处理,继续步骤2

“左优先”原则: 只要左边的运算符能先计算,就优先算左边的 (保证运算顺序唯一);

1
2
3
中缀:A + B - C * D / E + F
① ④ ② ③ ⑤
后缀:A B + C D * E / - F +

后缀表达式的计算—手算:
从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应的运算,合体为一个操作数

后缀表达式的计算—机算
用栈实现后缀表达式的计算(栈用来存放当前暂时不能确定运算次序的操作数)
步骤1: 从左往后扫描下一个元素,直到处理完所有元素;
步骤2: 若扫描到操作数,则压入栈,并回到步骤1;否则执行步骤3;
步骤3: 若扫描到运算符,则弹出两个栈顶元素,执行相应的运算,运算结果压回栈顶,回到步骤1;

中缀表达式转后缀表达式(机算)
初始化一个栈,用于保存暂时还不能确定运算顺序的运算符从左到右处理各个元素,直到末尾。可能遇到三种情况:
1.遇到操作数:直接加入后缀表达式。
2.遇到界限符:遇到“(”直接入栈;遇到“)”则依次弹出栈内运算符并加入后缀表达式,直到 弹出“(”为止。注意:“(”不加入后缀表达式。
3.遇到运算符:依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式, 若碰到“(” 或栈空则停止。之后再把当前运算符入栈。

image-20250402104553751

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#define MaxSize 40 
typedef struct{
char data[MaxSize];
int top;
}SqStack;

typedef struct{
char data[MaxSize];
int front,rear;
}SqQueue;

void InitStack(SqStack &S);
bool StackEmpty(SqStack S);
bool Push(SqStack &S, char x);
bool Pop(SqStack &S, char &x);
void InitQueue(SqQueue &Q);
bool EnQueue(LQueue &Q, char x);
bool DeQueue(LQueue &Q, char &x);
bool QueueEmpty(SqQueue Q);

// 判断元素ch是否入栈
int JudgeEnStack(SqStack &S, char ch){
char tp = S.data[S->top];
// 如果ch是a~z则返回-1
if(ch >= 'a' && ch <= 'z')
return -1;
// 如果ch是+、-、*、/且栈顶元素优先级大于等于ch则返回0
else if(ch == '+' && (tp == '+' || tp == '-' || tp == '*' || tp == '/'))
return 0;
else if(ch == '-' && (tp == '+' || tp == '-' || tp == '*' || tp == '/'))
return 0;
else if(ch == '*' && (tp == '*' || tp == '/'))
return 0;
else if(ch == '/' && (tp == '*' || tp == '/'))
return 0;
// 如果ch是右括号则返回2
else if(ch == ')')
return 2;
// 其他情况ch入栈,返回1
else return 1;
}

// 中缀表达式转后缀表达式
int main(int argc, char const *argv[]) {
SqStack S;
SqQueue Q;
InitStack(S);
InitQueue(Q);
char ch;
printf("请输入表达式,以“#”结束:");
scanf("%c", &ch);
while (ch != '#'){
// 当栈为空时
if(StackEmpty(&S)){
// 如果输入的是数即a~z,直接入队
if(ch >= 'a' && ch <= 'z')
EnQueue(Q, ch);
// 如果输入的是运算符,直接入栈
else
Puch(S, ch);
}else{
// 当栈非空时,判断ch是否需要入栈
int n = JudgeEnStack(S, ch);
// 当输入是数字时直接入队
if(n == -1){
EnQueue(Q, ch);
}else if(n == 0){
// 当输入是运算符且运算符优先级不高于栈顶元素时
while (1){
// 取栈顶元素入队
char tp;
Pop(S, tp);
EnQueue(Q, tp);
// 再次判断是否需要入栈
n = JudgeEnStack(S, ch);
// 当栈头优先级低于输入运算符或者栈头为‘)’时,入栈并跳出循环
if(n != 0){
EnStack(S, ch);
break;
}
}
}else if(n == 2){
// 当出现‘)’时 将()中间的运算符全部出栈入队
while(1){
char tp;
Pop(S, tp);
if(tp == '(')
break;
else
EnQueue(Q, tp);
}
}else{
// 当运算符优先级高于栈顶元素或出现‘(’时直接入栈
Push(S, ch);
}
}
scanf("%c", &ch);
}
// 将最后栈中剩余的运算符出栈入队
while (!StackEmpty(S)){
char tp;
Pop(S, tp);
EnQueue(Q, tp);
}
// 输出队中元素
while (!QueueEmpety(Q)){
printf("%c ", DeQueue(Q));
}
return 0;
}

用栈实现中缀表达式的计算:
1.初始化两个栈,操作数栈和运算符栈;
2.若扫描到操作数,压入操作数栈;
3.若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈)

image-20250331202409726

leetcode题目练习

150. 逆波兰表达式求值 - 力扣(LeetCode)

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
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
for(auto c:tokens)
{
if(c=="+")
{
int num1=st.top();
st.pop();
int num2=st.top();
st.pop();
st.push(num1+num2);
}
else if(c=="-")
{
int num1=st.top();
st.pop();
int num2=st.top();
st.pop();
st.push(num2-num1);
}
else if(c=="*")
{
int num1=st.top();
st.pop();
int num2=st.top();
st.pop();
st.push(num2*num1);
}
else if(c=="/")
{
int num1=st.top();
st.pop();
int num2=st.top();
st.pop();
st.push(num2/num1);
}
else
st.push(stoi(c));
}
return st.top();
}
};

3.3.3. 栈在递归中的应用

函数调用的特点:最后被调用的函数最先执行结束(LIFO)

函数调用时,需要用一个栈存储:

  • 调用返回地址
  • 实参
  • 局部变量

递归调用时,函数调用栈称为 “递归工作栈”:

  • 每进入一层递归,就将递归调用所需信息压入栈顶;
  • 每退出一层递归,就从栈顶弹出相应信息;

缺点:太多层递归可能回导致栈溢出;适合用“递归”算法解决:可以把原始问题转换为属性相同,但规模较小的问题

消除递归的方法:

1.用栈模拟

2.对于单向递归(线性递归)和尾递归,用迭代也可以消除

单向递归(线性递归)

定义与特点

单向递归是指递归函数中存在多个递归调用分支,但每次递归的方向是单一的,即递归调用后仍需进行其他操作(如计算、合并结果等)。这种递归通常需要保留每一层的中间状态,导致内存消耗较高。

可能会有大量的冗余计算,通过迭代法或动态规划优化

举例:斐波那契数列计算

二、尾递归

定义与特点

尾递归是递归的特殊形式,其递归调用是函数的最后一步操作,且返回值直接传递,无需后续处理。这种结构允许编译器将其优化为循环,消除栈空间增长

通俗的说就是递归函数的return 返回的是递归函数调用

无冗余操作:递归调用后无需回溯,直接传递累积结果

举例:阶乘的计算

3.3.4. 队列的应用

  1. 队列应用:树的层次遍历
  2. 队列应用:图的广度优先遍历
  3. 队列应用:操作系统中多个进程争抢着使用有限的系统资源时,先来先服务算法(First Come First Service)是是一种常用策略。

3.4. 特殊矩阵的压缩存储

img

image-20250331211015347

3.4.1 数组的存储

一维数组的存储:各数组元素大小相同,且物理上连续存放。设起始地址为LOC,则数组元素a[i]的存放地址 = LOC + i * sizeof(ElemType) (0≤i<10)(i从0开始的,如果从1开始的话就得变成i-1)

LOC + (i -1)* sizeof(ElemType) (0≤i<10)

img

维数组的存储

下面的i,j均从0开始

1
2
3
4
5
6
7
8
9
1.M行N列的二维数组b[M][N]b[M][N]中
设起始地址为LOC
若按行优先存储
则b[i][j]b[i][j]的存储地址 = LOC+(i∗N+j)∗sizeof(ElemType)LOC+(i∗N+j)∗sizeof(ElemType)

2. M行N列的二维数组b[M][N]b[M][N]中
设起始地址为 LOC
若按列优先存储
则b[i][j]b[i][j]的存储地址 = LOC+(i∗N+j)∗sizeof(ElemType)LOC+(i∗N+j)∗sizeof(ElemType)

img

img

image-20250331204232184

3.4.2 对称矩阵的压缩存储

​ 对称矩阵的压缩存储:若n阶方阵中任意一个元素a_{i,j},都有a_{i,j}=a_{j,i}则该矩阵为对称矩阵,对于对称矩阵,只需存储主对角线+下三角区。若按照行优先原则将各元素存入一维数组中,即a_{i,j}存入到数组B[k]中,那么数组B[k]共有\frac{n(n-1)}{2}+1个元素。对于k,有:

image-20250407124619162

img

image-20250331204511497

下三角区的元素存在一个一维数组里面可以节省快一半的空间,可以建立一个映射函数,用矩阵的行号和列号直接对应访问一维数组,如下图所示(i>=j的情况)

image-20250331204940858

由于B数组下标从0开始,所以减1才可以

如果题目中的数组是从1开始而不是0的话就不需要减1

如果访问的是上三角区域的话,利用a(i,j)=a(j,i)的性质,转换为访问下三角区域即可,表现在公式中就是i,j的位置互换

image-20250331205246424

3.4.3 三角矩阵的压缩存储

  1. 下三角矩阵:除了主对角线和下三角区,其余的元素都相同。

  2. 上三角矩阵:除了主对角线和上三角区,其余的元素都相同。

  3. 压缩存储策略:按行优先原则将主对角线+下三角区存入一维数组中,并在最后一个位置存储常量。

    a_{i,j}存入到数组B[k]中,那么数组B[k]共有\frac{n(n-1)}{2}+1个元素。对于k,有:

image-20250407124651069

image-20250331205753349

下三角矩阵

如果按照列优先的话

那么第j列的的元素个数就是n-(j-1),可以看不是橙色的部分,减去的就是这一部分的数量

那么第j-1列就是 n-(j-1-1) = n-j+2

一共有j-1列,前j-1列的数量如下计算

那和就是

n+n-1+n-2+....+n-j+2

首项n,末项n-j+2,项数j-1,上述式子就是

(j-1)*(n+n-j+2)/2=(j-1)*(n-j+2)/2

再算第j列

(j-i)

在一维数组中就是

(j-1)*(n-j+2)/2+(j-i)

如果一维数组下标从1开始就是

(j-1)*(n-j+2)/2+(j-i+1)

image-20250331205832577

上三角区域

上三角按照行优先等价于下三角按照列优先,只是i和j互换个位置即可

3.4.4 三对角矩阵的压缩存储

三对角矩阵,又称带状矩阵: 当|i-j|>1时,a(i,j)=0

第一行和最后一行两个,剩下的都是三个,那就是一共存储3n-2个元素

对于三对角矩阵,按行优先原则,只存储带状部分,即a(i,j)存入到数组B[k]中,那么k=2i+j-3

image-20250331210200646image-20250331210608277

最后由k=2i+j-3推出j的值即可

3.4.5 稀疏矩阵的压缩存储

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

压缩存储策略:

  • 顺序存储:三元组 <行,列,值>

    缺点:访问某个元素的话只能按照顺序访问

img

  • 链式存储:十字链表法

image-20250331210954261