这里我们来学习队列和栈的数据结构。
栈
什么是栈
栈(stack)是一个有序序列,只能在称为栈顶的一段进行数据的插入和删除的操作,最后一个插入的元素是第一个被删除的元素。所以栈也被称作“先进后出”表(LIFO)或“后进先出”表(FILO)。
对栈操作的方法有特殊的称呼。比如把数据插入栈,称为向栈压入(push)一个元素,而把数据从栈中删除,我们称为从栈弹出(pop)一个元素。从空栈中弹出元素会发生下溢(underflow),而向满栈压入元素会发生上溢(overflow)。我们把上溢和下溢统称为异常(exception)。
栈的抽象数据类型/ADT
为了简单起见,我们先把栈的内容数据类型设置为整形int。
操作
- Push(int data):将元素data压入栈中
- int Pop():从栈顶中弹出元素,并返回该元素的值。
- int Top():返回栈顶元素的值,但是不删除该元素
- int Size():返回栈中元素个数
- int IsEmptyStack():判断栈是否为空
- int IsFullStack():判断栈是否为满
异常
有时候栈的操作不能执行,比如向空栈中弹出元素或者向满栈中压入元素,此时Push()函数和Pop()函数无法正常执行,这时我们要求程序要抛出(throw)一个异常。
栈的应用
- 符号平衡
- 中缀表达式转后缀表达式
- 后缀表达式的计算
- 函数调用的实现
- 查找划分
- Web浏览器的访问历史
- 文本编辑器的撤销功能
- HTML和XML的标签匹配
栈的实现
栈的抽象数据类型有几种实现方法,下面我们介绍三种。
- 基于简单数组的实现
- 基于动态数组的实现
- 基于链表的实现
基于简单数组的实现
在数组中,我们从左向右的给数组添加元素,并使用一个辅助变量跟踪栈顶元素在数组中的下标。
数组实现的栈是会变满的,所以可能发生上溢异常。当数组为空时弹出元素也会发生下溢异常。
以下是代码的实现。
struct ArrayStack { int top; int capacity; int* array; }; struct ArrayStack* CreateStack() { struct ArrayStack* S = malloc(sizeof(struct ArrayStack)); if (!S) return NULL; S->capacity = 1; S->top - 1; S->array = malloc(S->capacity * sizeof(int)); if (!S->array) return NULL; return S; }
int IsEmptyStack(struct ArrayStack* S) { return(S->top == -1); }
int IsFullStack(struct ArrayStack* S) { return(S->top == S->capacity - 1); }
void Push(struct ArrayStack* S, int data) { if (IsFullStack(S)) printf("Stack overflow"); else S->array[++S->top] = data; }
int Pop(struct ArrayStack* S) { if (IsEmptyStack(S)) { printf("Stack is empty"); return 0; } else return(S->array[S->top--]); }
void DeleteStack(struct ArrayStack* S) { if (S) { if (S->array) { free(S->array); } free(S); } }
|
这个方法存在一个局限性,就是数组的大小需要事先指定,而且超出数组大小时压入元素会发生上溢。
最后总结一下使用静态数组实现栈的思路。我们使用一个下标变量top,用它来指向栈中最近插入元素的位置。为了插入一个元素,我们增加top元素的值,并把新元素放到top指定的位置上。为了弹出一个元素,我们先返回top上的元素,然后让top减1。
基于动态数组的实现
因为使用静态数组时栈的大小有限制,会发生上溢异常。为了动态的调整栈的大小,我们可以使用动态数组,当我们使用动态数组时,我们有两种思路。
首先是当每一次栈满时,我们就把栈的大小加1。但这样会造成巨大的时间开销。假如我们的数组初始长度为1,最后长度为n-1,当满栈时我们就要新建一个长度为n的数组,然后将n-1个元素复制过来。而这之前我们已经过(1+2+……+n-1)次复制操作,这样的算法时间复杂度为O(n2)。
另一种方法就是重复翻倍的思想。每当栈满时,我们就创建一个长度为当前数组的两倍的数组,再将元素复制过去。这样我们的算法时间复杂度就变成了O(n)。
注意,过多的翻倍操作可能会导致内存上溢。
基于链表的实现
利用链表也可以实现栈。Push操作通过在链表的首部插入元素来实现,而Pop操作则是删除链表的头部元素来实现。
以下是代码实现。
struct Stack { int data; struct Stack* next; };
struct Stack* CreateStack() { return NULL; }
void Push(struct Stack** top, int data) { struct Stack* temp; temp = malloc(sizeof(struct Stack));
if (!temp) return NULL;
temp->data = data; temp->next = *top;
*top = temp; }
int IsEmptyStack(struct Stack* top) { return (top == NULL); }
int Pop(struct Stack** top) { int data; struct Stack* temp;
if (IsEmptyStack(top)) return INT_MIN;
temp = *top; *top = (*top)->next; data = temp->data;
free(temp); return data; }
int Top(struct Stack* top) { if (IsEmptyStack(top)) return INT_MIN;
return (top->next)->data; }
void DeleteStack(struct Stack** top) { struct Stack* temp, * p; p = *top; while (p->next) { temp = p->next; p->next = temp->next; free(temp); } free(p); }
|
队列
什么是队列
队列(queue)是一个有序序列,只能在序列的一段(rear)插入元素,而在另一端(front)进行删除操作。最先被插入的元素也是最先被删除的元素。所以队列又被称为“先进先出”表(FIFO)或者“后进后出”表(LILO)
与栈类似,队列的插入和删除也有特定的称谓。队列在队尾进行插入操作我们称为入列(EnQueue)操作,删除队首元素,我们称之为出列(Dequeue)操作。
对空队列执行出列操作称为下溢(underflow)错误,对满队列执行入列操作称为上溢(overflow)错误。这些错误统称为异常(exception)。
队列的抽象数据类型/ADT
假设数据元素为整形。
操作
- EnQueue(int data):插入元素data到队列的队尾
- int DeQueue():删除队首元素,并返回该元素的值
- int Front():返回队首元素的值,但不删除首元素
- int QueueSize():返回队列的元素个数
- int IsEmptyQueue():判断队列是否为空
异常
和栈相同,对空队列执行DeQueue会抛出空队列异常,对满队列执行EnQueue会抛出满队列异常。
队列的应用
- 操作系统按照到达的顺序进行作业
- 模拟现实中的队列问题
- 异步数据传输
- 确定超市需要的收银员人数
- 算法的辅助数据结构
- 其他数据结构的组成部分
队列的实现
队列的实现与栈类似,有以下三种方法:
- 基于简单数组的实现
- 基于循环数组的实现
- 基于链表的实现
基于简单循环数组的实现
我们利用循环数组实现对队列ADT的简单实现,并用两个元素追踪队首和队尾元素。通常,用front指向队首元素,用rear指向队尾元素。
用来储存队列元素的数组可能会变满,此时EnQueue操作将抛出满队异常。同样的,对于空队列执行DeQueue操作将抛出空队异常。
初始化时,front和rear均指向-1,表示此时队列为空。
以下是实现的代码:
#define _CRT_SECURE_NO_WARNINGS #pragma warning(disable:6031) #include<stdio.h> #include<stdlib.h>
struct ArrayQueue { int front, rear; int capacity; int* array; };
struct ArrayQueue *Queue(int size) { struct ArrayQueue* Q = malloc(sizeof(struct ArrayQueue)); if (!Q) return NULL; Q->capacity = size; Q->front = Q->rear = -1; Q->array = malloc(Q->capacity * sizeof(int));
if (!Q->array) return NULL; return Q; }
int IsEmptyQueue(struct ArrayQueue* Q) { return(Q->front == -1); }
int IsFullQueue(struct ArrayQueue* Q) { return((Q->rear + 1) % Q->capacity == Q->front); }
int QueueSize(struct ArrayQueue* Q) { return (Q->capacity - Q->front + Q->rear + 1) % Q->capacity; }
void EnQueue(struct ArrayQueue* Q, int data) { if (IsFullQueue(Q)) { printf("Queue Overflow!"); } else { Q->rear = (Q->rear + 1) % Q->capacity; Q->array[Q->rear] = data; if (Q->front == -1) { Q->front = Q->rear; } } }
int DeQueue(struct ArrayQueue* Q) { int data = 0; if (IsEmptyQueue(Q)) { printf("Queue is empty!"); return 0; } else { data = Q->array[Q->front]; if (Q->front == Q->rear) { Q->front = Q->rear = -1; } else Q->front = (Q->front + 1) % Q->capacity; } return data; }
void DeleteQueue(struct ArrayQueue* Q) { if (Q) { if (Q->array) free(Q->array); free(Q); } }
|
局限性:队列的最大长度需要事先定义且不能更改,EnQueue满队时会产生满队异常。
基于动态循环数组的实现
动态循环数组的实现和简单循环数组大致相同,唯一的区别是加入了一个ResizeQueue函数用来调整预设循环数组的大小,代码如下:
#define _CRT_SECURE_NO_WARNINGS #pragma warning(disable:6031) #include<stdio.h> #include<stdlib.h>
struct ArrayQueue { int front, rear; int capacity; int* array; };
struct ArrayQueue* Queue(int size) { struct ArrayQueue* Q = malloc(sizeof(struct ArrayQueue)); if (!Q) return NULL; Q->capacity = size; Q->front = Q->rear = -1; Q->array = malloc(Q->capacity * sizeof(int));
if (Q->array) return NULL; return Q; }
int IsEmptyQueue(struct ArrayQueue* Q) { return(Q->front == -1); }
int IsFullQueue(struct ArrayQueue* Q) { return((Q->rear + 1) % Q->capacity == Q->front); }
int QueueSize(struct ArrayQueue* Q) { return (Q->capacity - Q->front + Q->rear + 1) % Q->capacity; }
void EnQueue(struct ArrayQueue* Q, int data) { if (IsFullQueue(Q)) { ResizeQueue(Q); } else { Q->rear = (Q->rear + 1) % Q->capacity; Q->array[Q->rear] = data; if (Q->front == -1) { Q->front = Q->rear; } } }
void ResizeQueue(struct ArrayQueue* Q) { int size = Q->capacity; Q->capacity = Q->capacity * 2; Q->array = realloc(Q->array, Q->capacity); if (!Q->array) { printf("Memory Error"); return; } if (Q->front > Q->rear) { for (int i = 0; i < Q->front; i++) { Q->array[i + size] = Q->array[i]; } Q->rear = Q->rear + size; } }
int DeQueue(struct ArrayQueue* Q) { int data = 0; if (IsEmptyQueue(Q)) { printf("Queue is empty!"); return 0; } else { data = Q->array[Q->front]; if (Q->front == Q->rear) { Q->front = Q->rear = -1; } else Q->front = (Q->front + 1) % Q->capacity; } return data; }
void DeleteQueue(struct ArrayQueue* Q) { if (Q) { if (Q->array) free(Q->array); free(Q); } }
|
基于链表的实现
实现队列的另一种途径是利用链表。EnQueue操作可以通过在链表表尾插入元素实现,而DeQueue则可以通过删除链表首元素实现。
代码实现如下:
#define _CRT_SECURE_NO_WARNINGS #pragma warning(disable:6031) #include<stdio.h> #include<stdlib.h> struct ListNode { int data; struct ListNode* next; };
struct Queue { struct ListNode* front; struct ListNode* rear; };
struct Queue* CreatQueue(void) { struct Queue* Q; struct ListNode* temp; Q = malloc(sizeof(struct Queue));
if (!Q) { return NULL; }
temp = malloc(sizeof(struct ListNode)); Q->front = Q->rear = NULL; return Q; }
int IsEmptyQueue(struct Queue* Q) { return(Q->front == NULL); }
void EnQueue(struct Queue* Q, int data) { struct ListNode* newNode; newNode = malloc(sizeof(struct ListNode)); if (!newNode) return NULL; newNode->data = data; newNode->next = NULL; if (Q->rear)Q->rear->next = newNode; Q->rear = newNode;
if (Q->front == NULL) Q->front = Q->rear; } int DeQueue(struct Queue* Q) { int data = 0; struct ListNode *temp; if (IsEmptyQueue(Q)) { printf("Queue is empty!"); return 0; } else { temp = Q->front; data = Q->front->data; Q->front = Q->front->next; free(temp); } return data; }
void DeleteQueue(struct Queue* Q) { struct ListNode* temp; while (Q->front) { temp = Q->front; Q = Q->front->next; free(temp); } free(Q); }
|
完结咯(๑′ᴗ‵๑)I Lᵒᵛᵉᵧₒᵤ❤