栈和队列的实现和总结

目录

一.栈

1.定义

2.实现

 1.StackInit(栈的初始化)

  2.DestroyStack(摧毁栈)

3.StackEmpty(判断栈是否为空)

 4.DestroyStack(入栈)

5.StackPop(出栈)

6.StackTop(取栈顶元素)

 7.StackSize(栈长)

二.队列

 1.QueueInit(队列初始化)

  2.QueueDestroy(摧毁队列)

 3.QueuePush(入列)

 4.QueuePop(出列)

5.QueueFront(取队头元素)

6.QueueBack(取队尾元素)

7.QueueEmpty(判断队列是否为空)

8.QueueSize(队列长度)

三.用队列模拟实现栈

四.用栈模拟实现队列

五.循环队列


1.定义

栈是一种后进先出的结构,也就是我们俗称的Last in first out结构(LIFO)

它就像我们平时洗盘子,洗好的盘子会被放在最下面,那取盘子放进消毒柜里的时候,取的反而是

上面的盘子.

栈,是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作.
进行数据插入和删除操作的一端 称为栈顶,另一端称为栈底.

2.实现

我们前面已经学习过数组和链表两种存储数据的方式.

我们要采用哪种方式,去实现我们的栈呢?

链表的话,如果有一个top(栈顶指针),我们肯定设计栈顶为头指针,始终指向头结点,毕竟链表头

插和头删的效率才高.

数组的话,我们肯定就是采取动态开辟的方式,容量不够的时候,就直接扩容.

两者相较而言,其实都可以实现栈这种数据结构.

但相较于链表而言,数组实现在栈顶插入数据,显然要显得更加轻松和容易一些,同时,我们也知

道数组缓存利用率比链表更高,因此我们采用数组的方式去实现栈.

首先把改用的头文件包含一下

再把Stack(堆),里面的内容设计好.

可以发现,其实和我们的顺序表,栈几乎采取同样的设计,不过原本的size改变为栈顶top而已.

 接下来,我们来看下栈有哪些函数操作.

 1.StackInit(栈的初始化)

初始化可以采用先不动态申请一块空间,然后让我们的指针a指向它.这样在后续入栈的操作中,就

需要加入动态申请一块空间这一步操作.

这里采用另一种方式,也就是在初始化的时候,就把空间给开辟好,在入栈操作中,只要考虑扩容

问题即可.

在初始化时,我们还需要考虑一个问题,就是我们的top一开始设为多少.

如果设为0,则意味着没有元素的时候,top == 0;那top始终是指向栈顶元素的下一个位置.

 如果设为-1,则意味着没有元素的时候,top == -1;那top始终是指向栈顶元素的当前位置.

这里我们实现采用top == 0的方式,top == -1也是类似操作.

  2.DestroyStack(摧毁栈)

任务:1.s释放掉a指向的动态开辟的空间,把指针a置空

           2.容量和top相应调整为初始值

3.StackEmpty(判断栈是否为空)

 4.DestroyStack(入栈)

思考点:1.栈满扩容     2.top始终指向下一个位置,所以先赋值,再+1

5.StackPop(出栈)

 思考点:栈为空的时候,无法出栈.

 PS:不需要将原来栈顶元素改变,后续如果要进行入栈操作,会直接把原数据覆盖掉.

6.StackTop(取栈顶元素)

 7.StackSize(栈长)

由于我们的top始终指向最后一个元素的后一个元素,所以它和我们栈的元素个数实际是相等的.

如果top == -1,则需要返回top + 1;

二.队列

队列和栈不同,是一个先进先出的数据结构.俗称First in first out.(FIFO)

队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出
进行插入操作的一端称为队尾
出队列:进行删除操作的一端称为队头

它类似我们做核酸排队伍,先排队的,先做核酸. 

同样我们考虑用数组还是链表去实现队列.

这次考虑就显得简单很多,假设用数组实现,那无论是头部还是尾部,只要是出列,那都要移动元

素,这个时间复杂度是O(n),显然效率不高,而如果用链表实现,则没有这种情况.

因此我们采用链表的方式去实现队列.

按照惯例,我们先包含相应的头文件.

然后完成队列的结构设计,队列要是一个链表,那首先我们就需要构造队列中的每一个结点.

我们重命名为QNode.

 然后队列(结构体)包含队头,队尾指针,分别指向链表的头部和尾部.

为了便于链表长度的计算,不需要逐一结点遍历,我们在结构体Queue中加入size变量,用于记录链表的长度.

队列一般有下面函数需要实现,下面我们将逐一介绍.

 1.QueueInit(队列初始化)

刚开始链表为空,则head 和 rear都指向NULL.

 

  2.QueueDestroy(摧毁队列)

Queue是堆区开辟的,不能free掉,真正需要释放的空间是我们的队列,也就是我们的链表.

但我们不能直接free(Queue->head),链表的释放,是需要逐个结点,逐一进行释放.

 3.QueuePush(入列)

入列实际就是构造链表.

那队列(链表)为空,和队列不为空,进行的操作显然不是相同的.

 4.QueuePop(出列)

和链表删除元素也是相同的,链表为空,则无法出列.

那除此之外,是否还有什么细节需要注意呢?

假设我们不断出列,然后我们不断移动头指针head,那删除最后一个元素后,就会出现

尾指针不为空,头指针为空的情况.

但我们一个队列为空,头指针和尾指针就必须为空.

这会造成什么影响呢?

单就对QueuePush(入列)来说,就可能出现尾指针解引用的情况,导致程序崩溃.

因此,只剩一个结点的时候,需要单独进行判断.直接释放结点就可以.

5.QueueFront(取队头元素)

判断队列不为空后,直接返回头指针指向的元素即可.

6.QueueBack(取队尾元素)

同理,判断队列不为空后,直接返回尾指针指向的元素即可.

7.QueueEmpty(判断队列是否为空)

队头和队尾指针都为空,而且都相等,即可认定队列为空.

8.QueueSize(队列长度)

因为我们在队列中,本身就加了size变量,因此直接返回size即可.

三.用队列模拟实现栈

队列是一个先进先出的结构,而栈是一个后进先出的结构.

所以队列先进的元素,如果要出栈,反而是最后出栈的.

那这个元素我们要对它进行什么操作呢? 我们注意到题目提供了两个队列,这就给我们提供了思

路.

假设有两个队列,我们设计为两个队列中任意一个队列为空,另一个不为空.

出栈的时候,只需要将不为空的队列中的元素,全部压入空队列中,只剩下最后一个元素,也就

是要出栈的元素.

 MyStack结构如下图所示

typedef struct {
   Queue q1;
   Queue q2;
} MyStack;


MyStack* myStackCreate() {
   MyStack * obj = (MyStack *)malloc(sizeof (MyStack));
   //分别对队列1和队列2初始化
   QueueInit(&obj->q1);
   QueueInit(&obj->q2);
   return obj;
}

void myStackPush(MyStack* obj, int x) {
    //先假设队列1不为空
    Queue* notempty = &obj->q1;
    //如果队列1为空,则往队列2里面插入元素
    if (QueueEmpty(&obj->q1))
    {
         notempty = &obj->q2;
    }
    //往不为空的队列里面插入元素
    QueuePush(notempty,x);
}

int myStackPop(MyStack* obj) {
    //假设队列1为空,队列2不为空
    Queue * emptyQ = &obj->q1;
    Queue * notemptyQ = &obj->q2;
    //如果q1不为空,则交换
    if (!QueueEmpty(&obj->q1))
    {
       emptyQ = &obj->q2;
       notemptyQ = &obj->q1;
    }
    //非空队列里的元素转移到空队列中,直到只剩下最后一个元素
    while (QueueSize(notemptyQ) > 1)
    {
        QueuePush(emptyQ,QueueFront(notemptyQ));
        QueuePop(notemptyQ);
    }
    int top = QueueFront(notemptyQ);
    QueuePop(notemptyQ);
    return top;
}

int myStackTop(MyStack* obj) {
    //假设队列1为空,队列2不为空
    Queue * emptyQ = &obj->q1;
    Queue * notemptyQ = &obj->q2;
    //如果q1不为空,则交换
    if (!QueueEmpty(&obj->q1))
    {
       emptyQ = &obj->q2;
       notemptyQ = &obj->q1;
    }
    return QueueBack(notemptyQ);
}

bool myStackEmpty(MyStack* obj) {
    //队列1和队列2都为空,则myStack为空
    return QueueEmpty(&obj->q1) && QueueEmpty(&obj->q2);
}

void myStackFree(MyStack* obj) {
   QueueDestroy(&obj->q1);
   QueueDestroy(&obj->q2);
   free(obj);
}

四.用栈模拟实现队列

有了上面的实现经验,可能有人很激动,就直接想着同样让一个栈为空,另一个栈不为空.

然后每次需要pop出元素的时候,就相应的将非空栈中的元素,全部压入空栈中,再弹出top元素位

置的元素即可.

但是入列(队列)操作,是不改变元素顺序的;入栈(栈)操作,是会将元素顺序改变,将元素倒过来.

因此假如我们push 1 2 3 4,再pop一下(以此获得1),会有下面的情况.

此时我们再进行pop操作,并不需要再将元素压入空栈中.(直接pop操作,把2弹出即可)

但是又出现一个新的问题,假如此时进行的是Push操作呢?

我们可以将原来的元素压入空栈中,然后再将新元素压入栈中.

然后又pop操作,又要将它从非空栈压入空栈中,显然这样的效率很低,而且,有时候需要压入空

栈,有时候又不需要压入空栈,显然思路不太正确. 

我们可以采取这样的思路.

规定一个栈A,专门用来出栈,Pop出元素.

另一个栈B,专门用来入栈,Push进入元素.

假如要出栈,则从栈APop出元素;假如栈A没有元素,则把栈B中的元素全部压入栈A中.

它的核心思路,其实也是利用了入栈A,再出栈到另一个栈B,栈B就相当于一个队列,不过这个

队列只有出列的功能,还需要栈A暂时存储元素,来实现入列的功能.

typedef struct {
  ST popst;//出栈
  ST pushst;//入栈
} MyQueue;


MyQueue* myQueueCreate() {
    MyQueue* obj = (MyQueue*)malloc(sizeof (MyQueue));
    StackInit(&obj->popst);
    StackInit(&obj->pushst);
    return obj;
}

void myQueuePush(MyQueue* obj, int x) {
    StackPush(&obj->pushst,x);
}

int myQueuePop(MyQueue* obj) {
    //出栈为空,倒数据
    if (StackEmpty(&obj->popst))
    {
        while (!StackEmpty(&obj->pushst))
        {
            StackPush(&obj->popst,StackTop(&obj->pushst));
            StackPop(&obj->pushst);
        }
    }
    int top = StackTop(&obj->popst);
    StackPop(&obj->popst);
    return top;
}

int myQueuePeek(MyQueue* obj) {
    //出栈为空,倒数据
    if (StackEmpty(&obj->popst))
    {
        while (!StackEmpty(&obj->pushst))
        {
            StackPush(&obj->popst,StackTop(&obj->pushst));
            StackPop(&obj->pushst);
        }
    }

    return StackTop(&obj->popst);
}

bool myQueueEmpty(MyQueue* obj) {
    return StackEmpty(&obj->popst) && StackEmpty(&obj->pushst);
}

void myQueueFree(MyQueue* obj) {
    DestroyStack(&obj->popst);
    DestroyStack(&obj->pushst);
    free(obj);
}

五.循环队列

假设当前为队列分配的空间为6,那当队列Queue->rear == 5的时候,就不不能再插入元素了.否

则,数组就会越界.(若用顺序栈实现).

但假如此时前面的位置依旧为空呢(实际可用的空间并未被占满)?那空间会被浪费.

一个解决方案,除了我们上面提到的链队列外,就是我们接下来要介绍的——循环队列.

循环队列和我们队列的设计是一样的,也是有一个front指针指向头,rear指针始终指向尾部的下一个

位置,当两者相等的时候,队列为空,当然,因为是数组,所以front和rear直接设为int类型,代表下

标即可.

那循环列表有什么需要考虑的吗?

首先第一点需要考虑的就是,假如我们要存k个元素,我们能够直接开k个空间存元素吗?

显然是不能的.

拿队列分配的空间为6来说

front和rear的插值总共有6种可能,0,1,2,3,4,5

而总共的状态有7种,列表为空,有一个元素,两个元素,三个元素,四个元素...,六个元素

这显然用6种可能,是不可能表示7种状态的.

因此通常有两种方式解决,一种是另设一个标志位以区别队列是空还是满.

另一个就是我们呢今天实现的方式,约定以“队列头指针在队列尾指针的下一个位置,作为队列满的状态.”

按上面图片来说,此时队列就是满的状态,大小为k + 1的空间,最多能存k个元素.

第二点要考虑的就是循环,顾名思义,循环,也就是rear在超过队列长度后,要回到数组下标为0

的位置.

 那现在k = 5,代表总共能存5个元素.

rear此时下标为6,如何让它回到0呢?

这里需要我们采用以前提到过的取模操作

rear %= (k + 1)

就能让rear重新回到下标为0的位置,真正实现循环.

完整代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>

typedef struct {
    int* a;
    int front; //循环队列头部
    int rear;  //循环队列尾部
    int k;     //记录队列总共可存元素个数
} MyCircularQueue;


MyCircularQueue* myCircularQueueCreate(int k) {
    MyCircularQueue* q = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
    if (q == NULL)
    {
        perror("malloc fail.\n");
        exit(-1);
    }
    q->a = (int*)malloc(sizeof(int) * (k + 1));
    q->front = q->rear = 0;
    q->k = k;
    return q;
}

bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
    return obj->front == obj->rear;
}

bool myCircularQueueIsFull(MyCircularQueue* obj) {
    return ((obj->rear + 1) % (obj->k + 1)) == (obj->front);
}

bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
    assert(obj);
    //如果循环队列为满,则无法入列
    if (myCircularQueueIsFull(obj))
    {
        return false;
    }
    else
    {
        obj->a[obj->rear++] = value;
        //如果超出数组范围,及时将它调整回相应的大小
        obj->rear %= (obj->k + 1);
        return true;
    }
}

bool myCircularQueueDeQueue(MyCircularQueue* obj) {
    assert(obj);
    //如果循环队列为空,则出列失败
    if (myCircularQueueIsEmpty(obj))
        return false;
    else
    {
        obj->front++;
     //如果超出数组范围,及时将它调整回相应的大小
        obj->front %= (obj->k + 1);
        return true;
    }
}

int myCircularQueueFront(MyCircularQueue* obj) {
    assert(obj);
    //如果循环队列为空,则出列失败
    if (myCircularQueueIsEmpty(obj))
        return -1;
    else
        return obj->a[obj->front];
}

int myCircularQueueRear(MyCircularQueue* obj) {
    assert(obj);
    //如果循环队列为空,则取最后元素失败
    if (myCircularQueueIsEmpty(obj))
        return -1;
    else
        return obj->a[(obj->rear + obj->k) % (obj->k + 1)];
}

void myCircularQueueFree(MyCircularQueue* obj) {
    free(obj->a);
    free(obj);
}

 最后再单独谈一下取循环队列尾部元素(myCircularQueueRear)的操作.

一般来说,取队尾元素,只要取下标为rear - 1的元素即可.(像下图的1一样)

但是可能会出现rear小于front的情况(像下图的2一样),这个时候,为了取到真实下标k

(rear - 1k + 1)%(k + 1) 

也就是我们代码里面的

(rear + k)% (k + 1) 

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
栈和队列都是常用的数据结构,用于存储和操作一系列的元素。下面是关于C语言中栈和队列的概要设计: 1. 栈的概要设计: - 栈是一种后进先出(LIFO)的数据结构,类似于我们日常生活中的堆栈,只能从顶部进行插入和删除操作。 - 声明栈结构体,其中包含一个数组用于存储元素,以及一个整数用于记录栈顶的位置。 - 设计栈的初始化函数,用于初始化栈的大小和栈顶的位置。 - 实现栈的push函数,即将一个元素放入栈中,需要更新栈顶的位置。 - 实现栈的pop函数,即从栈中弹出一个元素,也需要更新栈顶的位置。 - 可以选择添加其他辅助函数,如栈是否为空、栈是否已满等等。 2. 队列的概要设计: - 队列是一种先进先出(FIFO)的数据结构,类似于我们日常生活中排队的场景,只能从队列尾部插入元素,从队列头部删除元素。 - 声明队列结构体,其中包含一个数组用于存储元素,以及两个整数用于记录队列头部和尾部的位置。 - 设计队列的初始化函数,用于初始化队列的大小和头尾指针的位置。 - 实现队列的enqueue函数,即将一个元素插入到队列中的尾部,同时更新尾部指针。 - 实现队列的dequeue函数,即删除队列头部的元素,同时更新头部指针。 - 可以选择添加其他辅助函数,如队列是否为空、队列是否已满等等。 总结栈和队列是常用的数据结构,用于在程序中实现对元素的存储和操作。栈是一种后进先出的数据结构,而队列则是一种先进先出的数据结构。在C语言中,可以通过结构体和数组的组合来实现栈和队列的概要设计,包括初始化函数、插入/删除操作函数以及一些辅助函数。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值