关于栈和队列问题的总结

简介: 关于栈和队列问题的总结

关于栈和队列的总结

1.栈:

1.什么是栈

栈是一种对于数据进行管理的数据结构,对于数据,我们常见的操作就是删除和添加,而栈只有一个接口负责数据的管理,不论是删除还是添加都要通过这个口去处理,所以,栈就自然而然的满足先进后出的特点,先添加到栈中的数据就会被放到最底下,在删除数据的时候就会被最后删除,反之,最后添加到栈中的数据就会被放到最顶上,删除时也第一个删除。

2.栈顶:

栈顶是栈最顶层的元素,一般是最后一个添加的元素,同时在删除时也会满足第一个出栈,栈顶是栈很特殊的一个位置,对于栈的访问和销毁都有重要的作用:

3.代码实现:

我下面是对栈的一个程序构建,在这里,考虑到栈的删除和添加都是从一个口出入,那么我们就可以联想到尾删和尾插,对于只能尾删和尾插的数据结构,数组顺序表在这一方面很便捷,所以我这里使用数组顺序表去构建一个栈:

1.头文件test.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>//判断空的头文件
typedef int STDataType;
typedef struct Stack
{
  STDataType* a;//数组指针
  int top;//定义栈顶位置,就相当于顺序表里的元素个数状态量
  int capacity;//判断容量
}ST;
void STInit(ST* ps);//重定义栈
void STDestroy(ST* ps);//销毁数组栈
void STPush(ST* ps, STDataType x);//栈顶插
void STPop(ST* ps);//栈顶删
STDataType STTop(ST* ps);//获取栈顶元素
int STSize(ST* ps);//判断元素大小
bool STEmpty(ST* ps);//判断空

对于一个项目,我们一般构建一个头文件负责对函数和结构体等元素进行声明,这一点我在扫雷游戏专栏说过,这里不多赘述。

首先我们分析一下我们头文件的内容,既然是顺序表,我们就需要利用一个结构体去管理它,为了方便管理,我们首先创建一个数组指针,其实就是构建一个数组STDataType* a,方便数据进行操作,同时我说过栈顶元素很重要,所以我们同时也要时刻记录栈顶的位置int top,为了更方便管理数据,我们对容量也要时刻掌控,故创建一个int capacity管理容量。

既然是数据结构,我们自然对数据的操作方式无非增删查改 ,后面的函数也可以看出我们的实现目的,看注释即可,我不再过度强调。

2.函数文件list.c
#include"test.h"
void STInit(ST* ps)//重定义栈
{
  assert(ps);
  ps->a = NULL;
  ps->top = 0;
  ps->capacity = 0;
}
void STDestroy(ST* ps)//销毁数组栈
{
  assert(ps);
  free(ps->a);
  ps->a = NULL;
  ps->top = 0;
  ps->capacity = 0;
}
void STPush(ST* ps, STDataType x)//栈顶插
{
  assert(ps);
  if (ps->top == ps->capacity)
  {
    STDataType newCapacity=ps->capacity ==0 ? 4 :ps->capacity*2;//注意这里的思路,我们重新设置一个整型变量负责存放扩容的capacity,利用后续的capacity是否为0,三目操作符,是0就扩大为4,不是零就扩大原来的2倍
    STDataType* tmp = (STDataType*)realloc(ps->a,newCapacity*sizeof(STDataType));//这里直接使用realloc利用realloc的特性,当指示空间为空时,realloc的作用变成malloc。
    if (tmp == NULL)//注意这里要使用newCapacity,因为首先capacity本身就是不稳定的的赋值,你也不知道它赋值多少,所以我们用更稳定的newCapacity去处理
    {
      perror("realloc fail");
      exit(-1);
    }
    ps->a = tmp;//后续把新空间的指针重新赋给a
    ps->capacity =newCapacity;//重新赋给capacity即可
  }
  ps->a[ps->top] = x;
  ps->top++;
}
void STPop(ST* ps)//栈顶删
{
  assert(ps);
  assert(ps->top>0);//防止为空,进行断言
  --ps->top;
}
STDataType STTop(ST* ps)//获取栈顶元素
{
  assert(ps);
  assert(ps->top > 0);
  return ps->a[ps->top - 1];
}
int STSize(ST* ps)//判断元素大小size
{
  assert(ps);
  return ps->top;
}
bool STEmpty(ST* ps)//判断空
{
  assert(ps);
  return ps->top == 0;
}

函数分析:我们从简单的函数逻辑开始,判断空和取元素大小以及获取栈顶元素,我们在结构体中构建了top来管理栈顶,用capacity来管理元素个数,当capacity为0时就说明整个栈是空的,所以我们返回的就是ps->top这个表达式的真假,来判断是否为空,方便对于栈的遍历操作,其次是判断元素个数,由于我们创建了一个top,初始化函数中我们让top的初始值为0,加一个元素top相应加一,故top等于多少,则对应有几个元素,所以对于元素大小,我们直接返回top即可。

对于获取栈顶元素,我们知道,数组下标是从0开始的,也就是说,top作为我们可以掌控的量,它一旦作为下标的话,每一次是都要比栈顶元素多一位的,故我们返回的栈顶元素的下标应当是数组a[top-1],这个要注意,易错。

初始化栈的函数:我们创建了一个结构体就要对其初始化,初始化标准的逻辑就是动态开辟空间,让数组指针确确实实指向一个数组,同时对于动态增长的数组,我们方便对其管理的时候,要将元素个数变量和栈顶元素变量都设置为0(如果是固定长度的栈或者队列,我们只需要在最开始对其固定好长度即可,比如后面会提到的循环队列)。但在这里我使用了一个新方法,我在这里先将a设置为空,这个目的我后续会讲到,这里不多赘述。

销毁栈的函数:其原理与初始化函数差不多,我们首先要释放掉我们开辟的栈,也就是我们的数组a,对于free库函数,它是不会自动置空指针的,所以我们要手动置空,a=NULL。对于整型的成员,我们只需要将其设置为0表示空即可。

栈顶插:

void STPush(ST* ps, STDataType x)//栈顶插
{
  assert(ps);
  if (ps->top == ps->capacity)
  {
    STDataType newCapacity=ps->capacity ==0 ? 4 :ps->capacity*2;//注意这里的思路,我们重新设置一个整型变量负责存放扩容的capacity,利用后续的capacity是否为0,三目操作符,是0就扩大为4,不是零就扩大原来的2倍
    STDataType* tmp = (STDataType*)realloc(ps->a,newCapacity*sizeof(STDataType));//这里直接使用realloc利用realloc的特性,当指示空间为空时,realloc的作用变成malloc。
    if (tmp == NULL)//注意这里要使用newCapacity,因为首先capacity本身就是不稳定的的赋值,你也不知道它赋值多少,所以我们用更稳定的newCapacity去处理
    {
      perror("realloc fail");
      exit(-1);
    }
    ps->a = tmp;//后续把新空间的指针重新赋给a
    ps->capacity =newCapacity;//重新赋给capacity即可
  }
  ps->a[ps->top] = x;
  ps->top++;
}

这是整个数组栈的难点部分,我单独拿出来说:

由于我们开辟的是数组栈,不是链表栈,所以显而易见我们需要在需要的时候对其展开扩容,这一点和顺序表是相同的道理,但注意,常规的顺序表是在定义函数位置开辟出来的,而我在定义位置将a设为NULL,目的就是在这里进行扩容,首先动态内存开辟一段数组,但这个时候我们突然遇到一个问题,初始的数组是空的,而我使用realloc函数(注意,realloc函数在遇到本身为空的情况是作用等同于malloc),我里面的值都是0,0*任何数都是0,没法扩容,所以我们这里还需要一个NewCapacity函数来对其容量进行一个最开始的扩容解决无法扩容的问题,我的逻辑是:ps->capacity是空么?倘若是,那么我们令容量函数值变为4并开辟4个空间,倘若不是,我们就将其扩大两倍,利用三目操作符即可实现(这里提一下三目操作符,三目操作符的三个式子可以是表达式也可以是单独的量,比如这里,ps->capacity==0就是单独的表达式,不要将其拆开看,这个整体的三目操作符是要整体赋给newCapacity的),这样,我们就完成了扩容和元素状态量定义。(别忘了扩容后要进行空的判断),不要忘了把新开辟的指针和量要赋给结构体里面的成员,同时对于栈顶元素要插入我们的x,并更新我们的栈顶元素进行top量的更新。

栈顶删:

栈顶删是很简单的,如同顺序表一样,我们没必要删除元素,只需要操控top,令其自减一,不再控制旧的第一个元素即可,后续我们加新元素的时候,这个位置不管元素是什么,都会被覆盖,不会残留。

如此,我们便完成了栈的构建,现在创建一个主函数程序来运行一下:

3.主文件test.c
#include"test.h"
void test1()
{
  ST st;
  STInit(&st);
  STPush(&st, 1);
  STPush(&st, 2);
  STPush(&st, 3);
  STPush(&st, 4);
  STPush(&st, 5);
  while(!STEmpty(&st))//循环的条件是数组栈不为空
  {
    printf("%d ", STTop(&st));
    STPop(&st);
  }
  printf("\n");
  STDestroy(&st);
}//注意,数组栈的遍历的从栈顶往前进行的,所以每次遍历都要删除一个元素接着遍历,这就是栈的特点,先进后出,后进先出
int main()
{
  test1();
  return 0;
}

主函数的构建,我这里推荐一种方法,如同我上文所写,就是利用函数来实现,而且是不传参的函数,因为这样是很方便测试函数的功能,当你不使用的时候,只需要注释到函数的声明,就可以去调试其他函数的功能,对于复杂的大型工程进行测试时很有用。

让我们再一次强调一下栈的特点吧:只有一个接口,先进后出,后进先出。如果我们想要遍历整个栈,我们只能看一个删一个(利用我这个程序是如此,但假如你重新定义一个变量赋值给它top的值,然后利用它去自减似乎不用看一个删一个,例如你定义一个cur=top,你去自减cur访问,top的值是不变的,依旧指向栈顶位置,而且我们也没有删除元素,不过你需要在结构体内部创建,操作起来很麻烦,不推荐)我们循环的条件就是数组栈不为空,目的就是达到遍历的效果,在循环里,我们只需要每次都打印出当前的栈顶元素(利用构建的获取栈顶元素的函数),然后依次删除即可。最后,由于栈已经为空,我们也不进行操作,一定要销毁栈,这样是防止内存泄露。

如此,便是利用数组构建栈的全部,同样,链表也可以构建栈,但相对麻烦,根据实际情况可以去尝试。

补充细节:无论是栈抑或是下面的队列,我们都要对传入函数的指针判空,当对于删除或者添加的函数进行实现前,我们都要对栈或者队列内部的内容是否为空进行判断后再操作,否则对于整型控制的变量,我们很容易让其赋值为负数,这样后续的添加就不合理了,我这里使用assert暴力的方法进行检查,出问题会直接报错,比较直接,你也可以用较为温柔的方法,比如当其为空时直接将整型赋值为0,这样也可以避免出现负数的问题。

1.队列:

1.什么是队列:

队列,顾名思义,如同我们小时候排队,一般都是最矮的站在队尾或者队头,有时候是最高的,然后按照身高一次排列,假如我们按照从高到低的顺序排队从一个走廊里走进去,然后顺着一个方向走出来,我们会发现,最高的人是第一个进入走廊的,出来的时候也是最高的人先出来,而最矮的人是最后一个进入走廊的,但反方向出来的时候最矮的人却是第一个走出来的,这就是队列的大致特点。队列一共有头和尾两个接口,我们添加元素的时候只能从尾部去添加,而从队列中删除出元素的时候只能从队列的头部去出。

2.队列的头和尾:

队列的头可以理解为第一个进入队列的元素,而尾可以理解为最后一个进入队列的元素,头可以控制队列的删除,而尾可以控制队列的添加,这是两个很关键的队列数据结构的操控指针。

3.代码实现:

在明确队列特点的前提下,与栈不同,队列需要对头和尾部都进行操作,而且涉及到头删,这对于数组顺序表是不友好的,空间复杂度会很高,所以我们这里使用链表来实现,我这里使用的是无头不循环单向链表。

1.头文件test.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int QDataType;
typedef struct QueueNode
{
  struct QueueNode* next;
  QDataType data;
}QNode;
typedef struct Queue//对于头指针和尾指针,我们可以建立一个结构体指针的结构体,这样既不用哨兵位,也不用二级指针,同时也避免的函数只能传回一个值的问题,而且在这个结构体内部,我们还能统计队列的实时状态量
{
  QNode* head;
  QNode* tail;
  int size;
}Que;
void QueueInit(Que* pq);//重定义
void QueueDestroy(Que* pq);//销毁队列
void QueuePush(Que*pq, QDataType x);//尾插                对于队列来说,其特性:头出尾进,所以我们如果传入头和尾指针可以更好的操控整个队列,同时省去了遍历的时间复杂度。
void QueuePop(Que* pq);//头删
QDataType QueueBack(Que* pq);//寻找队尾元素
QDataType QueueFront(Que* pq);//寻找队头元素
bool QueueEmpty(Que* pq);//判断空
int QueueSize(Que* pq);//记录队列的状态量

首先,我们常规创建结构体节点负责管理数据和指向下一个指针的next,考虑到队列的特点,我们需要对头和尾进行管理,我这里想到,倘若直接在结构体内部管理头和尾,我们的操作不是普遍的,(其实也可以,不过我使用起来会很乱)。我这里是又建立了一个结构体,它负责控制头和尾,同时统计元素的个数,故我创建了head,tail,size。这样两个结构体就创建好了,结构体是一个很方便去管理和控制数据的结构,对于需要在一个函数内部同时控制多个变量的情况,结构体很好用。

函数的功能如同我上文所写。

2.函数文件math.c
#include"test.h"
void QueueInit(Que* pq)//重定义
{
  assert(pq);
  pq->head = pq->tail = NULL;
  pq->size = 0;
}
void QueueDestroy(Que* pq)//销毁队列
{
  assert(pq);
  QNode* cur = pq->head;
  while (cur)
  {
    QNode* next = cur->next;
    free(cur);
    cur = next;
  }
  pq->head = pq->tail = NULL;
  pq->size = 0;
}
void QueuePush(Que* pq, QDataType x)//尾插 
{
  assert(pq);
  QNode* newnode = (QNode*)malloc(sizeof(QNode));
  if (newnode == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  newnode->data = x;
  newnode->next = NULL;//对新节点进行处理
  if (pq->tail == NULL)
  {
    pq->head = pq->tail = newnode;
  }
  else
  {
    pq->tail->next = newnode;
    pq->tail = newnode;
  }
        pq->size++;
}
void QueuePop(Que* pq)//头删
{
  assert(pq);
  assert(!QueueEmpty(pq));
  if (pq->head->next == NULL)
  {
    free(pq->head);
    pq->head = pq->tail = NULL;
  }
  else
  {
    QNode* next = pq->head->next;
    free(pq->head);
    pq->head = next;
  }
        pq->size--;
}
QDataType QueueBack(Que* pq)//寻找队尾元素
{
  assert(pq);
  assert(!QueueEmpty(pq));
  return pq->tail->data;
}
QDataType QueueFront(Que* pq)//寻找队头元素
{
  assert(pq);
  assert(!QueueEmpty(pq));
  return pq->head->data;
}
bool QueueEmpty(Que* pq)//判断空
{
  assert(pq);
  return pq->head == NULL;
}
int QueueSize(Que* pq)//记录队列的状态量
{
  assert(pq);
  return pq->size;
}

对于队列,我们要控制其头和尾,在尾插和头删的位置对数据的头和尾进行处理,所以,我们要传入的数据是我们定义的 Que结构体。

重定义函数:相同的操作,对指针置空,对整型变量处理为0或者其他要求的值。

尾插函数:首先要开辟一个节点,对这个节点进行数据存储和指针置空处理。然后我们要思考如何定义头和尾呢?这里分两种情况,首先自然而然,我们的头和尾指针开始时是指向一个位置的,我们放入一个数据,尾指针向后走一步,头指针不变,不断循环,最后尾指针就会指向尾部,而头指针指向头部,所以我们只需要考虑两个情况即可,即队列为空时,我们尾插怎么处理,以及队列不为空时,我们尾插怎么处理指针。首先是为空的情况,我们插入一个数据后,倘若tail->next==NULL,证明数据为空,那么我们就让头和尾同时指向开辟的新节点,倘若!=NULL,证明数据不为空,则尾部指针向后移动,首先让pq->tail->next指向newnode,将newnode链接起来,然后对newnode也要向后移动一位,我们用pa->tail=newnode处理即可,别忘了size++.

头删函数:首先要检验队列是否为空,后续我们写出判断空函数的时候,直接利用assert去判断一下即可,对于头删,我们也要分情况,我们先考虑常规情况,如果对于一个4节点的链表,我们要头删使其变为3节点,那我我们的思路就是首先定义一个next,令其存储头指针下一个节点的地址,然后free掉当前的head,手动将head赋值为next,这样就处理干净了常规的头,不过倘若是只有一个节点呢,我们只处理了头,这个时候尾也需要处理,我们就释放掉这个节点,同时将head和tail都置空,最后别忘了size–。

寻找队头和队尾元素:这是很简单的,因为我们首先记录了头节点和尾节点,只需要返回对应的data即可。

判空:同样是利用bool变量,我只需要返回头指针是不是为NULL的真假即可。

记录节点实时状态量:返回size即可。

3.主文件test.c
#include"test.h"
void test1()
{
  Que q;
  QueueInit(&q);
  QueuePush(&q, 1);
  QueuePush(&q, 2);
  QueuePush(&q, 3);
  QueuePush(&q, 4);
  QueuePush(&q, 5);
  while (!QueueEmpty(&q))//循环进行的条件就是一直不是空的,空了直接停止循环
  {
    printf("%d ", QueueFront(&q));
    QueuePop(&q);
  }
  QueueDestroy(&q);//队列的遍历方式和栈基本相同,都是从尾部依次遍历,但遍历一遍后就删除,队列对于数据的出和入的逻辑很清晰
}
int main()
{
  test1();
  return 0;
}

主文件没太多需要强调的,主要是队列的访问方式和栈差不多,都是遍历一个删一个,只不过从尾删变成了头删而已。

以上就是队列的全部内容,需要注意的也是需要对其判空处理,以及对指针的判空处理。

3.总结:

总的来说,队列的实用性要远大于栈,比如医院摇号系统的逻辑就类似队列,方便实时管理,但栈也有其一定的作用,我后续会讲解如何利用栈实现队列以及如何利用队列实现栈,那时我们对栈和队列会有更深的理解。

目录
相关文章
|
1月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
218 9
|
1月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
37 1
|
29天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
55 5
|
1月前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
|
1月前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。
|
1月前
|
存储
系统调用处理程序在内核栈中保存了哪些上下文信息?
【10月更文挑战第29天】系统调用处理程序在内核栈中保存的这些上下文信息对于保证系统调用的正确执行和用户程序的正常恢复至关重要。通过准确地保存和恢复这些信息,操作系统能够实现用户模式和内核模式之间的无缝切换,为用户程序提供稳定、可靠的系统服务。
52 4
|
1月前
|
算法 安全 NoSQL
2024重生之回溯数据结构与算法系列学习之栈和队列精题汇总(10)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第3章之IKUN和I原达人之数据结构与算法系列学习栈与队列精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
1月前
|
算法
数据结构之购物车系统(链表和栈)
本文介绍了基于链表和栈的购物车系统的设计与实现。该系统通过命令行界面提供商品管理、购物车查看、结算等功能,支持用户便捷地管理购物清单。核心代码定义了商品、购物车商品节点和购物车的数据结构,并实现了添加、删除商品、查看购物车内容及结算等操作。算法分析显示,系统在处理小规模购物车时表现良好,但在大规模购物车操作下可能存在性能瓶颈。
50 0
|
2月前
数据结构(栈与列队)
数据结构(栈与列队)
23 1
|
2月前
|
算法 程序员 索引
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
栈的基本概念、应用场景以及如何使用数组和单链表模拟栈,并展示了如何利用栈和中缀表达式实现一个综合计算器。
48 1
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器