当前位置:   article > 正文

NzN的数据结构--栈的实现

NzN的数据结构--栈的实现

         在前面我们已经学习了哪些线性数据结构呢?大家一起来回顾一下:C语言学过的数组,数据结构中的线性表和顺序表和链表。那我们今天再来介绍数据结构里的两个线性结构--栈和队列。

目录

一、栈的概念及结构 

二、用数组实现栈

1. 栈的初始化和销毁

2. 从栈顶插入数据

3. 删除栈顶元素

4. 取栈顶元素

5. 计算栈中元素的数量

6. 判断栈是否为空

三、用链表实现栈

四、两种实现对比

1. 支持操作

2. 时间效率

3. 空间效率

五、栈的应用


一、栈的概念及结构 

        栈:一种特殊的线性表,只允许在固定一端插入和删除元素。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。

        压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶

        出栈:栈的删除操作叫做出栈,出数据也在栈顶

二、用数组实现栈

        栈的实现一般可以使用数组或链表实现,相对而言数组的结构实现更优一些

         使用数组实现栈时,我们可以将数组的尾部作为栈顶。入栈与出栈操作分别对应在数组尾部添加元素与删除元素。那我们先通过数组实现栈。

        先在头文件中定义需要实现的功能接口:

  1. //Stack.h
  2. #pragma once
  3. #include<stdio.h>
  4. #include <stdlib.h>
  5. #include<stdbool.h>
  6. #include<assert.h>
  7. typedef int STDataType;
  8. typedef struct Stack
  9. {
  10. STDataType* a;//数组实现
  11. int top;//栈顶
  12. int capacity;//栈的容量
  13. }ST;
  14. //栈的初始化和销毁
  15. void STInit(ST* ps);
  16. void STDestroy(ST* ps);
  17. //插入(只能从栈顶插入)
  18. void STPush(ST* ps, STDataType x);
  19. //栈中元素的删除
  20. void STPop(ST* ps);
  21. //取栈顶元素
  22. STDataType STTop(ST* ps);
  23. //计算栈中元素的数量
  24. int STSize(ST* ps);
  25. //判断栈是否为空
  26. bool STEmpty(ST* ps);

1. 栈的初始化和销毁

  1. void STInit(ST* ps)
  2. {
  3. assert(ps);
  4. ps->a = NULL;
  5. ps->top = 0;//top=0指向的是栈顶元素的下一个
  6. //我们也可以让top=-1,这样top就指向栈顶元素
  7. ps->capacity = 0;
  8. }
  9. void STDestroy(ST* ps)
  10. {
  11. assert(ps);
  12. free(ps->a);
  13. ps->a = NULL;
  14. ps->top = 0;
  15. ps->capacity = 0;
  16. }

2. 从栈顶插入数据

  1. void STPush(ST* ps, STDataType x)
  2. {
  3. assert(ps);
  4. //空间不够直接扩容
  5. if (ps->top == ps->capacity)
  6. {
  7. //一开始没有给容量多大,因此需要判断
  8. int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
  9. //ps->a为空的话,realloc相当于malloc
  10. STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));
  11. if (tmp == NULL)
  12. {
  13. perror("realloc");
  14. exit(1);
  15. }
  16. ps->a = tmp;
  17. ps->capacity = newcapacity;
  18. }
  19. ps->a[ps->top] = x;
  20. ps->top++;
  21. }

3. 删除栈顶元素

  1. void STPop(ST* ps)
  2. {
  3. assert(ps);
  4. assert(!STEmpty(ps));
  5. ps->top--;
  6. }

4. 取栈顶元素

  1. STDataType STTop(ST* ps)
  2. {
  3. assert(ps);
  4. assert(!STEmpty(ps));
  5. return ps->a[ps->top - 1];
  6. }

5. 计算栈中元素的数量

  1. int STSize(ST* ps)
  2. {
  3. assert(ps);
  4. return ps->top;
  5. }

6. 判断栈是否为空

  1. bool STEmpty(ST* ps)
  2. {
  3. assert(ps);
  4. return ps->top == 0;
  5. }

三、用链表实现栈

        使用链表实现栈时,我们可以将链表的头节点视为栈顶,尾节点视为栈底。

        对于入栈操作,我们只需将元素插入链表头部,这种节点插入方法被称为“头插法”。而对于出栈操作,只需将头节点从链表中删除即可。

         实现逻辑与数组相似,因此在这里直接给出实现代码:

  1. //基于链表实现的栈
  2. typedef struct {
  3. ListNode *top;//将头节点作为栈顶
  4. int size;//栈的长度
  5. } LinkedListStack;
  6. //栈的初始化
  7. LinkedListStack *newLinkedListStack()
  8. {
  9. LinkedListStack *s = malloc(sizeof(LinkedListStack));
  10. s->top = NULL;
  11. s->size = 0;
  12. return s;
  13. }
  14. //栈的销毁
  15. void delLinkedListStack(LinkedListStack *s)
  16. {
  17. while (s->top)
  18. {
  19. ListNode *n = s->top->next;
  20. free(s->top);
  21. s->top = n;
  22. }
  23. free(s);
  24. }
  25. //计算栈中元素的数量
  26. int size(LinkedListStack *s)
  27. {
  28. return s->size;
  29. }
  30. //判断栈是否为空
  31. bool isEmpty(LinkedListStack *s)
  32. {
  33. return size(s) == 0;
  34. }
  35. //从栈顶插入数据
  36. void push(LinkedListStack *s, int num)
  37. {
  38. ListNode *node = (ListNode *)malloc(sizeof(ListNode));
  39. node->next = s->top;//更新新加节点指针域
  40. node->val = num;//更新新加节点数据域
  41. s->top = node;//更新栈顶
  42. s->size++;//更新栈大小
  43. }
  44. //取栈顶元素
  45. int peek(LinkedListStack *s) {
  46. if (s->size == 0)
  47. {
  48. printf("栈为空\n");
  49. return -1;
  50. }
  51. return s->top->val;
  52. }
  53. //删除栈顶元素
  54. int pop(LinkedListStack *s)
  55. {
  56. int val = peek(s);
  57. ListNode *tmp = s->top;
  58. s->top = s->top->next;
  59. //释放内存
  60. free(tmp);
  61. s->size--;
  62. return val;
  63. }

四、两种实现对比

1. 支持操作

        两种实现都支持栈定义中的各项操作。数组实现额外支持随机访问,但这已超出了栈的定义范畴,因此一般不会用到。

2. 时间效率

在基于数组的实现中,入栈和出栈操作都在预先分配好的连续内存中进行,具有很好的缓存本地性,因此效率较高。然而,如果入栈时超出数组容量,会触发扩容机制,导致该次入栈操作的时间复杂度变为O(N) 。

在基于链表的实现中,链表的扩容非常灵活,不存在上述数组扩容时效率降低的问题。但是,入栈操作需要初始化节点对象并修改指针,因此效率相对较低。不过,如果入栈元素本身就是节点对象,那么可以省去初始化步骤,从而提高效率。

        综上所述,当入栈与出栈操作的元素是基本数据类型时,例如int或double我们可以得出以下结论。

  • 基于数组实现的栈在扩容时效率会降低,但由于扩容是低频操作,因此平均效率更高。
  • 基于链表实现的栈可以提供更加稳定的效率表现。

3. 空间效率

        在初始化列表时,系统会为列表分配“初始容量”,该容量可能超出实际需求;并且,扩容机制通常是按照特定倍率(例如 2 倍)进行扩容的,扩容后的容量也可能超出实际需求。因此,基于数组实现的栈可能造成一定的空间浪费

        然而,由于链表节点需要额外存储指针,因此链表节点占用的空间相对较大

        综上,我们不能简单地确定哪种实现更加节省内存,需要针对具体情况进行分析。

五、栈的应用

  • 浏览器中的后退与前进、软件中的撤销与反撤销。每当我们打开新的网页,浏览器就会对上一个网页执行入栈,这样我们就可以通过后退操作回到上一个网页。后退操作实际上是在执行出栈。如果要同时支持后退和前进,那么需要两个栈来配合实现。
  • 程序内存管理。每次调用函数时,系统都会在栈顶添加一个栈帧,用于记录函数的上下文信息。在递归函数中,向下递推阶段会不断执行入栈操作,而向上回溯阶段则会不断执行出栈操作。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/527821
推荐阅读
相关标签
  

闽ICP备14008679号