栈
一、概念
- 栈(stack)又名堆栈,它是一种运算受限的线性表:
-
- 限定仅在表尾进行插入和删除操作的线性表。允许插入和删除的一端为栈顶,另一端是栈底。
-
- 向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素。
-
- 从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
- 举个例子,向 AK-47 的弹夹那样,最后压(存)入的子弹最先被打出去,最开始压入的子弹,最后才能被射出。
- 再举一个例子,一个空盘子,每烙好一张饼都放在盘子最上面(这是 push 压入栈中),烙完后,盘子里堆了一叠饼,最下面的是最先烙好的。最上面的是刚烙好的,每一次吃只能从上面一张张拿,吃完一张拿下一张饼(这是 pop 出栈),直到盘子为空。
- 栈的性质:栈是一种 LIFO(Last In First Out) 的线性表,也就是数据元素遵循后进先出的原则。
- 栈的抽象数据类型:
ADT 栈(Stack)
Data
具有线性表一样的性质。
Operation
top:获取栈顶元素
count:栈的长度
isEmpty:栈内元素是否为空
push(data):元素进栈
pop():元素出栈
removeAll():清空栈内元素
EndADT
- 栈的分类(存储结构):
① 栈的顺序存储结构:单栈、共享栈;
② 栈的链式存储结构; - 共享栈:两个顺序栈共享存储空间,栈1的栈顶在下标0处,栈2的栈顶在下标n-1处。
- 栈的顺序结构和链式结构区别:
① 顺序结构需要预先估算存储空间大小,适合数据元素变化不大的情况;
② 链式结构不需要预先估算存储空间,适合数据元素变化较大的情况; - 栈和线性表的不同之处在于,栈只有进栈和出栈操作,并且遵循后进先出的规则,也就是说数据元素顺序进栈,逆序出栈。栈可以实现回退操作,递归和四则运算等。
二、栈的操作
- s.push(item) // 在栈顶压入新元素
- s.pop() // 删除栈顶元素但不返回其值
- s.empty() // 如果栈为空返回true,否则返回false
- s.size() // 返回栈中元素的个数
- s.top() // 返回栈顶的元素,但不删除该元素
三、顺序存储栈
- 概念
-
- 顺序存储栈即物理结构是顺序存储,先开辟一块内存空间(和顺序存储链表一样有没有),每 push 一个新元素,栈顶标记 top+1。
-
- 直到开辟的空间被存满,每 pop 一个栈顶元素,top-1,也就是下一个元素变成栈顶元素。
- 定义数据结构:
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define OK 1
#define MAXSIZE 20 /* 存储空间初始分配量 */
typedef int Status;/* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int ElemType;/* ElemType类型根据实际情况而定,这里假设为int */
/* 顺序栈结构 */
typedef struct {
ElemType data[MAXSIZE];
int top; /* 用于栈顶指针 */
}Stack;
- 初始化:
Status InitStack(Stack *S) {
S->top = -1;
return OK;
}
- 清空:
Status ClearStack(Stack *S) {
S->top = -1;
return OK;
}
- 获取栈顶元素:
Status GetTop(Stack S, ElemType *e) {
if (S.top == -1) return ERROR;
*e = S.data[S.top];
return OK;
}
- 获取栈长度:
int StackLength(Stack S) {
return S.top+1;
}
- Push:
Status PushStack(Stack *S, ElemType e) {
if (S->top == MAXSIZE -1) return ERROR;
S->top++;
S->data[S->top] = e;
return OK;
}
- Pop:
Status PopStack(Stack *S, ElemType *e) {
if (S->top == -1) {
return ERROR;
}
*e = S->data[S->top];
S->top--;
return OK;
}
- 输出测试:
int main(int argc, const char * argv[]) {
// 创建栈
Stack S;
InitStack(&S);
for (int i = 0; i < 10; i++) {
PushStack(&S, i);
}
StackPrint(S);
// 出栈
ElemType e;
PopStack(&S, &e);
printf("出栈:%d",e);
StackPrint(S);
// 获取栈顶元素
GetTop(S, &e);
printf("栈顶:%d\n",e);
// 输出长度
printf("栈长度:%d\n",StackLength(S));
return 0;
}
栈
0 1 2 3 4 5 6 7 8 9
出栈:9
栈
0 1 2 3 4 5 6 7 8
栈顶:8
栈长度:9
四、链式存储栈
- 概念
-
- 链式存储栈以链表的形式,新入栈的节点,next 指向原来的栈顶节点,插在链表的最前端,成为新的栈顶(和链表的头插法像不像?)。
-
- 用 top 标记栈顶节点,而不是上面顺序存储的位置,每一次入栈新节点,top 指向新栈顶节点,count 也随之 +1。出栈时,top 指向栈顶节点的 next 节点,count-1。
- 定义:
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define OK 1
typedef int Status;/* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int ElemType;/* ElemType类型根据实际情况而定,这里假设为int */
/* 链栈的每一个节点,和单链表很像有没有 */
typedef struct StackNode {
ElemType data;
struct StackNode *next;
}StackNode;
typedef struct StackNode * StackNodePtr;
/* 栈结构 */
typedef struct {
StackNodePtr top;
int count;
}LinkStack;
- 初始化:
Status InitStack(LinkStack *S) {
S->top = NULL;
S->count = 0;
return OK;
}
- 清空:
Status ClearStack(LinkStack *S) {
if (S->top == NULL) return ERROR;
StackNodePtr p;
while (S->count != 0) {
p = S->top;
S->top = S->top->next;
free(p);
S->count--;
}
return OK;
}
- 获取栈顶元素:
Status GetTop(LinkStack S, ElemType *e) {
if (S.top == NULL) return ERROR;
// if (S->count == 0) return ERROR; // 也可以
*e = S.top->data;
return OK;
}
- 获取栈长度:
int StackLength(LinkStack S) {
return S.count;
}
- Push:
Status PushStack(LinkStack *S, ElemType e)
{
// 创建新元素
StackNodePtr p = (StackNodePtr)malloc(sizeof(StackNode));
if (p == NULL) return ERROR;
p->data = e;
p->next = S->top;
S->top = p;
S->count++;
return OK;
}
- Pop:
Status PopStack(LinkStack *S, ElemType *e) {
if (S->top == NULL) return ERROR;
// if (S->count == 0) return ERROR; // 也可以
/* 将栈顶指针指向新的栈顶 */
StackNodePtr temp = S->top;
S->top = S->top->next;
*e = temp->data;
free(temp);
S->count--;
return OK;
}
- 输出测试:
nt main(int argc, const char * argv[]) {
// 创建栈
LinkStack S;
InitStack(&S);
for (int i = 0; i < 10; i++) {
PushStack(&S, i);
}
StackPrint(S);
ElemType e;
PopStack(&S, &e);
printf("出栈:%d\n",e);
StackPrint(S);
// 获取栈顶元素
GetTop(S, &e);
printf("栈顶:%d\n",e);
// 输出长度
printf("栈长度:%d\n",StackLength(S));
return 0;
}
9 8 7 6 5 4 3 2 1 0
出栈:9
8 7 6 5 4 3 2 1 0
栈顶:8
栈长度:9
队列
一、概念
- 只允许在一段进行插入操作,而在另一端进行删除操作的线性表;
- 性质:先进先出;
- 队列的抽象数据类型:
ADT 队列(Queue)
Data
具有线性表一样的性质。
Operation
front:队列第一个数据
count:队列的长度
isEmpty():队列是否为空
enQueue(data):数据进队列
deQueue():数据出队列
removeAll():清空队列内的所有数据
EndADT
- 顺序队列:就是使用数组来模拟队列,但是数组的插入和删除需要移动数据,比较繁琐。
- 循环顺序队列:在顺序队列的基础上改造,使队列的队头和队尾可以在数组中循环变化,在数据插入和删除就不需要频繁移动数据了。但是顺序队列,都需要提前申请存储空间,还有溢出的可能。
- 链式队列:为了解决顺序队列的不足,引用链式队列。不需要提前申请空间,只不过会引入频繁申请和释放的操作。
- 队列有什么作用?在开发过程中,接触最多的应该是全局并发队列。为什么要用队列实现呢?在线程的优先级一样的情况下,不应该先申请系统资源的先被满足吗?这和在银行排队取钱是一个道理。
二、队列的操作
- push(item)
- q.pop()
- q.front()
- q.back()
- q.size()
- q.empty()
三、链队列
- 链式队列的表示:
- 是不是似曾相识的结构?链栈,再看看链栈的表示:
- 区别:栈,新的元素添加在栈顶,而且栈顶先出;队列,队尾进,队首出。
- 二者相反,所以,链队列的操作简单来说就是:
-
- 进入队列:Q.rear 尾节点后追加新节点,将 Q.rear 指向新节点,新节点成队尾;
-
- 出队列:Q.front 指向的首元节点出队列,Q.front 指向首元节点的下一个节点。
- 先定义数据结构:
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define OK 1
typedef int Status;/* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int ElemType;/* ElemType类型根据实际情况而定,这里假设为int */
typedef struct QueueNode {
ElemType data;
struct QueueNode *next;
} QueueNode, *QueueNodePtr;
typedef struct {
QueueNodePtr front;
QueueNodePtr rear;
} LinkQueue;
- 初始化:先创建一个头节点,让 Q.front 和 Q.rear 指向头节点,头节点的 next 为 NULL:
// 初始化Status
InitQueue(LinkQueue *Q) {
// 初始队列为空,只有头节点,不是有效数据的节点
*Q->front = *Q->rear = (QueueNodePtr)malloc(sizeof(QueueNode));
if (*Q->front == NULL) return ERROR;
// 头节点的后面为空
*Q->front->next = NULL;
return OK;
}
- 判断队列为空:当队列为空时,恰入上图初始化的状态,只剩一个头节点,此时 Q.front == Q.rear;
// 判断是否为空Status
QueueEmpty(LinkQueue Q) {
if (Q.front == Q.rear) return TRUE;
return FALSE;
}
- 进入队列:
-
- 进入队列的操作,是将新元素,追加到 rear 指向的队尾之后,rear->next = 新元素,再将 rear 指向新元素,此时,新元素成为队尾。因为不受存储空间限制(内存占满另说),所以不需要一开始就判断是否队满,也没有队满的操作。
-
- 创建新节点 p;
-
- 追加到队尾;
-
- 队列的 rear 指向 p,标记成新队尾。
Status EnQueue(LinkQueue *Q, ElemType e) {
QueueNodePtr p = (QueueNodePtr)malloc(sizeof(QueueNode));
if (p == NULL) return ERROR;
p->data = e;
p->next = NULL;
// 追加到队尾
*Q->rear->next = p;
// 标记成队尾
*Q->rear = p;
return OK;
}
- 出队列:出队列操作,是将首元节点从链队删除。
-
- 判断队列是否为空;
-
- 找到队首节点 head(此时已拿到节点,将该节点的 data 回调出去),等待删除;
-
- 更改标记 head 后面的节点 s 为首元节点,即 head->next;
-
- 判断是否是最后一个节点,是的话,rear 也指向头节点;
-
- 释放原首节点 head。
Status DeQueue(LinkQueue *Q, ElemType *e) {
if (QueueEmpty(*Q)) return ERROR;
QueueNodePtr head;
// 找到要删除的节点
head = *Q->front->next;
// 回调到函数外
*e = head->data;
// 更改头节点
*Q->front->next = head->next;
// 如果删到了队尾最后一个元素
if (*Q->rear == head)
*Q->rear = *Q->front;
// 删除临时指针指向的头节点
free(head);
return OK;
}
- 清空:仅清空头节点以外的全部节点,有头节点在,清空完,还能继续 EnQueue() 操作,又回到初始化后的状态;
// 清空队列
Status ClearQueue(LinkQueue *Q) {
if (*Q->front->next == NULL) return ERROR;
QueueNodePtr temp, p; // 首元节点
*Q->rear = *Q->front;
p = *Q->front->next;
*Q->front->next = NULL;
// 此时只有temp指向首元节点
while (p) {
temp = p;
p = p->next;
free(temp);
}
return OK;
}
- 销毁:销毁操作,和清空不一样,清空仅仅删除除头节点以外的所有节点,清空后还可以再入队。但是销毁已经彻底不使用,需要连头节点也一并 free 掉,所以代码中是从 front 开始,而非 front->next,已 free 所有的节点;
// 销毁队列
Status DestoryQueue(LinkQueue *Q) {
if (*Q->front->next == NULL) return ERROR;
/*
说明,头节点也是个malloc开辟的,也需要释放
*/
while (*Q->front) {
*Q->rear = *Q->front->next;
free(*Q->front);
*Q->front = *Q->rear;
}
return OK;
}
- 获取队首:不更改队列,不破坏队列现有结构,仅查找,所以首元节的数据直接读取 Q.front->next->data;
Status GetHead(LinkQueue Q, ElemType *e) {
if (QueueEmpty(*Q)) return ERROR;
*e = Q.front->next->data;
return OK;
}