当前位置:   article > 正文

数据结构栈和队列

数据结构栈和队列

个人主页:星纭-CSDN博客

系列文章专栏:数据结构

踏上取经路,比抵达灵山更重要!一起努力一起进步!

目录

一.栈 

1.栈的介绍

2.初始化与销毁

3.入栈和出栈

4.栈顶元素,判空,求栈的元素个数

代码:

test.c

stack.h:

stack.c:

二.队列 

1.队列的介绍

2.初始化与销毁

3.插入与删除 

4.查看队头队尾元素

 5.判空与队列大小

代码:


一.栈 

1.栈的介绍

栈是一种线性数据结构,它只能在固定的一端进行插入和删除操作,这一端被称为栈顶,另一端则称为栈底。栈的特点是后进先出(Last In First Out,LIFO),即最后插入的元素最先被删除,而最先插入的元素最后被删除。

栈的基本操作有两个:入栈(push)和出栈(pop)。入栈操作将元素插入到栈顶,出栈操作将栈顶元素删除并返回。 

  1. 入栈:栈插入数据的操作也叫入栈/压栈/进栈,入数据在栈顶。
  2. 出栈:栈的删除操作也叫做出栈。出数据也在栈顶。

栈可以用来解决一些具有后效性的问题,例如表达式求值、括号匹配、函数调用等。它也可以用于实现一些常见的数据结构,例如递归函数、深度优先搜索等。 

栈可以使用数组或链表来实现。使用数组实现的栈称为顺序栈,使用链表实现的栈称为链式栈。无论使用哪种实现方式,栈的空间复杂度都为O(n),其中n为栈中元素的个数。 

栈还有一些常见的扩展操作,例如获取栈顶元素(top)判断栈是否为空(isEmpty)获取栈中元素个数(size)等。这些操作的时间复杂度都为O(1)。

对于顺序栈来说,又分两种情况:一种是定长的静态栈结构,一般情况下,实际中用的比较少,另一种是支持动态增长的栈。

以下的栈的实现,作者基于支持动态增长的顺序栈完成。

  1. typedef int STDatatype;
  2. typedef struct{
  3. STDatatype* a;
  4. int top;//栈顶
  5. int capacity;//栈的容量
  6. }Stack;

为了方便对存储的数据类型进行更改,我们对int重命名一下,如果以后要存储char类型的数据,只需要更改一下int为char就可以轻松完成。 

2.初始化与销毁

  1. //初始化
  2. void STInit(Stack* pst);
  3. //销毁
  4. void STDestory(Stack* pst);

 1.初始化

  1. void STInit(Stack* pst) {
  2. pst->a = NULL;
  3. pst->capacity = pst->top = 0;
  4. };

因为开始的时候,这个栈中并没有任何的元素,所以我们将其的a初始化为NULL,top和capacity为0.

其实对于top的初始化是有两种情况的:

如果top指向的是栈顶的元素:

 比如这样的TOP就是指向栈顶的元素8,此时存放了3个数据,Top的值等于2.

可以是我们开始初始化为0,此时的TOP指向下标为0的位置,可是此时的位置上并没有元素,这里的top指向的是栈顶元素的下一个位置,并不是栈顶元素。

所以如果要使top指向栈顶元素,初始化应该为-1,而不是0.

本文采用top指向栈顶元素的下一个位置。

2.销毁

销毁其实和初始化很类似。

因为a是我们使用malloc函数动态开辟的空间,所以我们需要free释放掉a。

  1. //初始化
  2. void STInit(Stack* pst) {
  3. assert(pst);
  4. pst->a = NULL;
  5. pst->capacity = pst->top = 0;
  6. };
  7. //销毁
  8. void STDestory(Stack* pst) {
  9. assert(pst);
  10. free(pst->a);
  11. pst->a = NULL;
  12. pst->capacity = pst->top = 0;
  13. };

为了防止pst->a时造成野指针,需要对pst进行断言一下,避免pst是NULL.

3.入栈和出栈

 1.入栈

在入栈之前,我们需要对栈判断空间是否足够。因为top指向的是栈顶元素的下一个位置,所以当满了的条件是top等于capacity,而对于top指向的是栈顶元素而言,判满的条件是capacity = top + 1.

可是对于这个条件来说,当容量为0的时候,这个条件也成立,所以这里需要使用三目操作符,更改容量,以免乘以2后还是0.用tmp接收开辟的空间是为了避免开辟失败,返回NULL,导致a变成了NULL,使数据丢失。realloc函数当第一个 参数的值为NULL的时候,它的功能和malloc函数一模一样。所以我们不需要单独判断a是否为NULL.

  1. void STPush(Stack * pst,STDatatype x){
  2. assert(pst);
  3. if (pst->top == pst->capacity) {
  4. int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
  5. STDatatype* tmp = (STDatatype*)realloc(pst->a,newcapacity * sizeof(STDatatype));
  6. if (tmp == NULL)
  7. {
  8. perror("malloc fail");
  9. exit(1);
  10. }
  11. pst->a = tmp;
  12. pst->capacity = newcapacity;
  13. }
  14. pst->a[pst->top++] = x;
  15. };

 注意:最后记得top+1,不要忘了。

2.出栈

对于数组而言,出栈的时候,并不需要把这个数据真的移除掉,只需要top--即可,因为这样这个元素实际上就不在这个栈中了,我们再入栈的时候,就会覆盖掉这个元素。

  1. void STPop(Stack* pst) {
  2. assert(pst);
  3. assert(pst->top>0);
  4. pst->top--;
  5. };

出栈要注意,栈空了就不能出栈了。否则top就变成负数了。

4.栈顶元素,判空,求栈的元素个数

  1. //栈顶元素
  2. STDatatype STTop(Stack* pst) {
  3. assert(pst);
  4. assert(pst->top>0);
  5. return pst->a[pst->top - 1];
  6. };
  7. //判空
  8. bool IsEmpty(Stack* pst) {
  9. assert(pst);
  10. return pst->top == 0;
  11. }
  12. //求元素个数
  13. int STSize(Stack* pst) {
  14. assert(pst);
  15. return pst->top;
  16. };

这三个函数非常简单:栈定元素需要保证这个栈不能为空,也就是top要大于0,否则不对。

判空,当栈空的时候top就0,所以只需要判断top等不等于0即可。

求元素个数:因为top指向栈顶元素的下一个位置,又因为数组下标从0开始,所以top就代表元素的个数。

代码:

test.c

  1. #define _CRT_SECURE_NO_WARNINGS
  2. #include"stack.h"
  3. int main()
  4. {
  5. Stack s;
  6. STInit(&s);
  7. STPush(&s,1);
  8. printf("%d->", STTop(&s));
  9. STPush(&s,2);
  10. printf("%d->", STTop(&s));
  11. STPush(&s,3);
  12. printf("%d->", STTop(&s));
  13. STPush(&s,4);
  14. printf("%d->", STTop(&s));
  15. STPush(&s,5);
  16. printf("%d->", STTop(&s));
  17. STPush(&s,6);
  18. printf("%d->", STTop(&s));
  19. printf("\n栈总共有%d个元素\n",STSize(&s));
  20. //打印整个栈的元素
  21. while (!IsEmpty(&s)) {
  22. printf("%d->",STTop(&s));
  23. STPop(&s);
  24. }
  25. printf("\n栈总共有%d个元素\n", STSize(&s));
  26. STDestory(&s);
  27. return 0;
  28. }

输出结果: 

stack.h:

  1. #pragma once
  2. #include<stdio.h>
  3. #include<stdlib.h>
  4. #include<assert.h>
  5. #include<stdbool.h>
  6. //#include<>
  7. typedef int STDatatype;
  8. typedef struct{
  9. STDatatype* a;
  10. int top;//栈顶
  11. int capacity;//栈的容量
  12. }Stack;
  13. //初始化
  14. void STInit(Stack* pst);
  15. //销毁
  16. void STDestory(Stack* pst);
  17. //入栈
  18. void STPush(Stack* pst, STDatatype x);
  19. //出栈
  20. void STPop(Stack* pst);
  21. //栈顶元素
  22. STDatatype STTop(Stack* pst);
  23. //判空
  24. bool IsEmpty(Stack* pst);
  25. //求元素个数
  26. int STSize(Stack* pst);

stack.c:

  1. #define _CRT_SECURE_NO_WARNINGS
  2. #include"stack.h"
  3. //初始化
  4. void STInit(Stack* pst) {
  5. assert(pst);
  6. pst->a = NULL;
  7. pst->capacity = pst->top = 0;
  8. };
  9. //销毁
  10. void STDestory(Stack* pst) {
  11. assert(pst);
  12. free(pst->a);
  13. pst->a = NULL;
  14. pst->capacity = pst->top = 0;
  15. };
  16. //入栈
  17. void STPush(Stack * pst,STDatatype x){
  18. assert(pst);
  19. if (pst->top == pst->capacity) {
  20. int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
  21. STDatatype* tmp = (STDatatype*)realloc(pst->a,newcapacity * sizeof(STDatatype));
  22. if (tmp == NULL)
  23. {
  24. perror("malloc fail");
  25. exit(1);
  26. }
  27. pst->a = tmp;
  28. pst->capacity = newcapacity;
  29. }
  30. pst->a[pst->top++] = x;
  31. };
  32. //出栈
  33. void STPop(Stack* pst) {
  34. assert(pst);
  35. assert(pst->top>0);
  36. pst->top--;
  37. };
  38. //栈顶元素
  39. STDatatype STTop(Stack* pst) {
  40. assert(pst);
  41. assert(pst->top>0);
  42. return pst->a[pst->top - 1];
  43. };
  44. //判空
  45. bool IsEmpty(Stack* pst) {
  46. assert(pst);
  47. return pst->top == 0;
  48. }
  49. //求元素个数
  50. int STSize(Stack* pst) {
  51. assert(pst);
  52. return pst->top;
  53. };

二.队列 

1.队列的介绍

 队列是一种常见的数据结构,它遵循先进先出(FIFO)的原则,只允许在一端(队尾)进行插入数据,在另一端(队头)进行删除数据。队列有两个主要操作:入队(enqueue)和出队(dequeue)。入队将元素添加到队列的末尾,出队将队列的第一个元素删除并返回。

 队列与排队类似,有素质的人,不会插队,会从排到队尾,完成事情后,从队头离开。

所以我们,是不能访问中间元素的。

队列通常用于存储需要按照顺序处理的数据。例如,在计算机科学中,可以使用队列来实现广度优先搜索算法(BFS)。

在实现队列时,可以使用数组或链表来存储队列中的元素。数组实现的队列被称为顺序队列,链表实现的队列被称为链式队列。

队列也可以数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。

本文,作者采用链式队列。

队列的时间复杂度如下:

  • 入队:O(1)
  • 出队:O(1)
  • 查看队首元素:O(1)

队列的空间复杂度为O(n),其中n是队列中的元素数量。

  1. typedef int QDatatype;
  2. typedef struct QueueNode {
  3. struct QueueNode* next;
  4. QDatatype val;
  5. }QNode;
  6. typedef struct Queue {
  7. QNode* phead;
  8. QNode* ptail;
  9. int size;//节点个数
  10. }Queue;

这里采用两个指针分别指向队头与队尾,为了方便,把两个指针与size存放在一个结构体中,这样传参更加的方便。

2.初始化与销毁

1.初始化

最开始时,队列中是没有任何元素的。

  1. //初始化
  2. void QueueInit(Queue* pq) {
  3. assert(pq);
  4. pq->phead = pq->ptail = NULL;
  5. pq->size = 0;
  6. };

注意观察此处的参数,如果不放在一个结构体 中,那么传参的时候,就需要传递三个指针,大大地增加了使用者的操作难度。开始要断言一下,以免pq为NULL。

2.销毁

  1. //销毁
  2. void QueueDestory(Queue* pq) {
  3. assert(pq);
  4. QNode* cur = pq->phead;
  5. while (cur) {
  6. QNode* next = cur->next;
  7. free(cur);
  8. cur = next;
  9. }
  10. pq->phead = pq->ptail = NULL;
  11. pq->size = 0;
  12. };

队列的销毁与单链表的销毁很相似,这里不过多介绍。

3.插入与删除 

1.插入

插入数据,先malloc动态申请内存来存放这个数据。

然后考虑当这个队列没有元素的时候,phead和ptail指针均指向NULL,这个新插入的节点,即是头节点又是尾节点,所以需要分开讨论。 

  1. //队尾插入数据
  2. void QueuePush(Queue* pq, QDatatype x)
  3. {
  4. QNode* newnode = (QNode*)malloc(sizeof(QNode));
  5. if (newnode == NULL)
  6. {
  7. perror("malloc fail");
  8. exit(1);
  9. }
  10. newnode->val = x;
  11. newnode->next = NULL;
  12. if (pq->phead == NULL) {
  13. pq->phead = pq->ptail = newnode;
  14. pq->size++;
  15. }
  16. else {
  17. pq->ptail->next = newnode;
  18. pq->ptail = newnode;
  19. pq->size++;
  20. }
  21. }

当队列为空的时候,就直接将phead与ptail指针指向newnode既可。如果队列不为空就尾插。

2.删除

删除数据,需要释放第一个节点即可,然后让phead指针指向下一个节点。

可是这样写有一个问题,如果此时队列只有一个节点,如果只改变phead的话,ptail仍然指向原来的节点,这样的就会造成野指针了,所以仍然分开讨论。

  1. //队头删除数据
  2. void QueuePop(Queue* pq)
  3. {
  4. assert(pq);
  5. assert(pq->phead);
  6. //一个节点
  7. if (pq->phead == pq->ptail) {
  8. free(pq->phead);
  9. pq->phead = pq->ptail = NULL;
  10. }//多个节点
  11. else {
  12. QNode* del = pq->phead;
  13. pq->phead = pq->phead->next;
  14. free(del);
  15. del = NULL;
  16. }
  17. --pq->size;
  18. }

第二个断言是为了防止队列为空仍然删除数据。

4.查看队头队尾元素

  1. // 取队头和队尾的数据
  2. QDatatype QueueFront(Queue* pq) {
  3. assert(pq);
  4. assert(pq->size);
  5. return pq->phead->val;
  6. };
  7. QDatatype QueueBack(Queue* pq) {
  8. assert(pq);
  9. assert(pq->size);
  10. return pq->ptail->val;
  11. };

 在查看之前,首先要确定这个队列中有元素,否则不能查看

 5.判空与队列大小

  1. int QueueSize(Queue* pq) {
  2. assert(pq);
  3. return pq->size;
  4. };
  5. bool QueueEmpty(Queue* pq) {
  6. assert(pq);
  7. return pq->size == 0;
  8. };

判空与队列大小很简单这里就不过多讲解。

代码:

Queue.h:

  1. #pragma once
  2. #include<stdio.h>
  3. #include<stdlib.h>
  4. #include<assert.h>
  5. #include<stdbool.h>
  6. typedef int QDatatype;
  7. typedef struct QueueNode {
  8. struct QueueNode* next;
  9. QDatatype val;
  10. }QNode;
  11. typedef struct Queue {
  12. QNode* phead;
  13. QNode* ptail;
  14. int size;
  15. }Queue;
  16. //初始化与销毁
  17. void QueueInit(Queue* pq);
  18. void QueueDestory(Queue* pq);
  19. //队尾插入数据/队头删除数据
  20. void QueuePush(Queue* pq,QDatatype x);
  21. void QueuePop(Queue* pq);
  22. // 取队头和队尾的数据
  23. QDatatype QueueFront(Queue* pq);
  24. QDatatype QueueBack(Queue* pq);
  25. //判空与队列大小
  26. int QueueSize(Queue* pq);
  27. bool QueueEmpty(Queue* pq);

Queue.c:

  1. #define _CRT_SECURE_NO_WARNINGS
  2. #include"queue.h"
  3. //初始化
  4. void QueueInit(Queue* pq) {
  5. assert(pq);
  6. pq->phead = pq->ptail = NULL;
  7. pq->size = 0;
  8. };
  9. //销毁
  10. void QueueDestory(Queue* pq) {
  11. assert(pq);
  12. QNode* cur = pq->phead;
  13. while (cur) {
  14. QNode* next = cur->next;
  15. free(cur);
  16. cur = next;
  17. }
  18. pq->phead = pq->ptail = NULL;
  19. pq->size = 0;
  20. };
  21. //队尾插入数据
  22. void QueuePush(Queue* pq, QDatatype x)
  23. {
  24. QNode* newnode = (QNode*)malloc(sizeof(QNode));
  25. if (newnode == NULL)
  26. {
  27. perror("malloc fail");
  28. exit(1);
  29. }
  30. newnode->val = x;
  31. newnode->next = NULL;
  32. if (pq->phead == NULL) {
  33. pq->phead = pq->ptail = newnode;
  34. pq->size++;
  35. }
  36. else {
  37. pq->ptail->next = newnode;
  38. pq->ptail = newnode;
  39. pq->size++;
  40. }
  41. }
  42. //队头删除数据
  43. void QueuePop(Queue* pq)
  44. {
  45. assert(pq);
  46. assert(pq->phead);
  47. //一个节点
  48. if (pq->phead == pq->ptail) {
  49. free(pq->phead);
  50. pq->phead = pq->ptail = NULL;
  51. }//多个节点
  52. else {
  53. QNode* del = pq->phead;
  54. pq->phead = pq->phead->next;
  55. free(del);
  56. del = NULL;
  57. }
  58. --pq->size;
  59. }
  60. // 取队头和队尾的数据
  61. QDatatype QueueFront(Queue* pq) {
  62. assert(pq);
  63. assert(pq->size);
  64. return pq->phead->val;
  65. };
  66. QDatatype QueueBack(Queue* pq) {
  67. assert(pq);
  68. assert(pq->size);
  69. return pq->ptail->val;
  70. };
  71. int QueueSize(Queue* pq) {
  72. assert(pq);
  73. return pq->size;
  74. };
  75. bool QueueEmpty(Queue* pq) {
  76. assert(pq);
  77. return pq->size == 0;
  78. };

另外扩展了解一下,实际中我们有时还会使用一种队列叫循环队列。如操作系统课程讲解生产者消费者模型时可以就会使用循环队列。环形队列可以使用数组实现,也可以使用循环链表完成。

循环队列与双端队列,下一篇文章讲解。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/637377
推荐阅读
相关标签
  

闽ICP备14008679号