【数据结构与算法】之栈与队列的应用和操作

本文介绍了栈和队列的基本概念、操作和存储结构。栈是一种后进先出(LIFO)的数据结构,常用于递归、回退等操作。顺序存储栈和链式存储栈各有优缺点,链式存储栈更适合数据变化大的情况。队列则是一种先进先出(FIFO)的数据结构,常见于任务调度。链队列通过在队尾插入元素和队首删除元素实现队列操作。
摘要由CSDN通过智能技术生成

一、概念

  • 栈(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
出栈:90 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;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

╰つ栺尖篴夢ゞ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值