【数据结构】操作受限的线性表,栈的具体实现

简介: 【数据结构】操作受限的线性表,栈的具体实现

前言

  栈作为一种重要的线性结构,我们对栈的学习是必不可少的。本篇文章将会详细地介绍栈是如何具体实现的。

  文章末尾附带源码。


一、栈是什么

  在学习栈是如何实现的之前,我们需要直到栈是什么,它与普通的线性结构有什么区别。简而言之,栈就是操作受限的线性表,它限定仅在栈顶进行插入删除操作。因此,栈又称为后进先出的线性表。

  栈可以使用顺序表实现,也可以使用单链表实现。用顺序表实现相对更加简便一点,因为顺序表在尾部插入数据代价较小,所以本篇文章将会以顺序表的方式实现栈。

二、头文件的编写

  为了使代码可读性高,分工明确,我们将一些库函数头文件,函数定义等放在一个头文件里面。我们首先创建一个叫做 “Stack.h” 的头文件。

1.引入库函数头文件

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

2.定义栈结构体

// 栈的元素类型
typedef int STDataType;
// 栈的结构体
typedef struct Stack
{
  // 指向数组的指针
  STDataType* a;
  // 标识栈顶位置,其值为栈顶元素的下标+1
  int top;
  // 栈的容量
  int capacity;
}ST;

  同样的,我们将数据类型重命名为STDataType,方便以后改变。跟顺序表一样,我们定义一个指向数组的指针,数组内存放着数据,数据类型就是 STDataType 类型。因为栈只能在栈顶插入删除,所以我们需要一个标识栈顶位置的top。我们打算使用动态的分配空间的方式来给数组开辟空间,所以也需要一个表示栈的容量的capacity

  需要注意的是,top的含义不一样,后面函数的功能就会有变化。top的值可以是栈顶元素的下标,那么初始值就为-1,top的值也可以是栈顶元素的下标+1,那么初始值就为0,在这里,我们top的含义是指向栈顶元素的下一个。

  其实也可以跟顺序表一样,将top理解为数组里面的元素个数,其效果是完全相同的。

3.声明栈的功能函数

// 初始化
void STInit(ST* pst);
// 销毁
void STDestroy(ST* pst);
// 在栈顶插入
void STPush(ST* pst, STDataType x);
// 在栈顶弹出
void STPop(ST* pst);
// 查看栈顶元素
STDataType STTop(ST* pst);
// 判断栈是否为空
bool STEmpty(ST* pst);
// 查询栈的元素个数
int STSize(ST* pst);

  跟顺序表一样,无论栈里面是否有元素,我们创建的结构体都会存在,于是我们创建结构体时只需要创建结构体本身而不是跟单链表一样创建一个指向结构体的指针那样。但是我们操作栈的时候往往是需要改变栈结构体数据的,所以对于要改变结构体数据的函数,我们都需要传入一级指针,栈结构体的地址来间接改变结构体的内部数据。

三、主函数文件的编写

  为了方便测试,我们将测试函数与主函数放在一起,在主函数里面调用不同的测试函数。我们创建一个叫做 “test.c” 的源文件。

1.包含头文件

#include"Stack.h"

2.编写测试用例

void test()
{
    // 创建结构体
  ST s;
  // 初始化
  STInit(&s); 
  // 栈顶插入
  STPush(&s, 1);
  // 栈顶插入
  STPush(&s, 2);
  // 栈顶插入
  STPush(&s, 3);
  // 栈顶插入
  STPush(&s, 4);
  // 栈顶插入
  STPush(&s, 5);
  // 如果栈不为空
  while (!STEmpty(&s))
  {
      // 打印栈顶元素
    printf("%d ", STTop(&s));
    // 弹出栈顶元素
    STPop(&s);
  }
  printf("\n");
  // 销毁
  STDestroy(&s);
  return 0;
}

  以上测试用例为我随手编写,读者可根据自身情况去编写测试用例来验证结果。

3.主函数的编写

int main()
{
  test();
  return 0;
}

四、功能函数的编写

  同样的,我们将各个功能函数打包在一个文件里面。我们创建一个叫做 “Stack.c” 的源文件。

1.包含头文件

#include"Stack.h"

2.栈的初始化

void STInit(ST* pst)
{
  // pst是指向栈结构体的指针,不可能为空,为空说明传参错误
  assert(pst);
    
    // 还未开辟数组,令a指向NULL
  pst->a = NULL;
  // 栈顶元素下标+1,由于没有栈顶元素,故为0
  pst->top = 0;
  // 数组容量,初值为0
  pst->capacity = 0;
}

  在创建了栈结构体后,传参传入结构体的地址,这个地址是一定不为空的,所以可以在前面断言一下,以防传参错误。

3.栈的销毁

void STDestroy(ST* pst)
{
    // pst不可能为空
  assert(pst);
    
    // 释放数组空间
  free(pst->a);
  // 释放空间后的指针需要置空
  pst->a = NULL;
  // 空栈没有栈顶元素
  pst->top = 0;
  // 数组的容量也设置为0
  pst->capacity = 0;
}

  对数组进行销毁是比较方便的操作,无需遍历整个数组,直接释放整个数组空间就可以完成销毁所有元素的操作,但是只做到这是不够的,我们还需要修正参数为初始值。这样才算是完成了整个销毁操作。

4.入栈

void STPush(ST* pst, STDataType x)
{
    // pst为指向栈结构体的指针,不可能为空
  assert(pst);
    // 如果栈的元素个数与栈的容量相同
  if (pst->top == pst->capacity)
  {
      // 使用三目运算符设置新的容量
    int newCapacity = pst->capacity == 0 ? 4 : 2 * pst->capacity;
    // 用realloc函数开辟可以存储新容量的元素个数的空间
    STDataType* tmp = (STDataType*)realloc(pst->a, sizeof(STDataType) * newCapacity);
    // 开辟空间失败时
    if (tmp == NULL)
    {
        // 弹出反馈
      perror("realloc fail");
      // 终止程序
      exit(-1);
    }
    // 让指向数组的指针指向新的数组空间
    pst->a = tmp;
    // 把新的容量赋值给旧的容量
    pst->capacity = newCapacity;
  }
    // 在旧栈顶的下一个下标位置插入新元素
  pst->a[pst->top] = x;
  // 栈顶位置的下一个下标+1
  ++pst->top;
}

  前面我们有所过,虽然这个top的真正含义是标识栈顶元素的下一个下标,但是将top理解为这个栈或者说这个数组内的有效元素个数是完全没有问题的,毕竟我也觉得这样理解起来方便一点。在理清楚top的作用后,这个函数其实是非常非常简单的,在最后一个元素的下一个位置插入,最后修改一下相应的参数就好。唯一需要注意的就是扩容问题,插入操作难免会遇到存储上限的时候,合理的分情况扩容就可以很好的解决空间浪费或者空间不够用的问题。

5.出栈

void STPop(ST* pst)
{
    // pst不可能为空
  assert(pst);
  // 空栈不能删除
  assert(pst->top > 0);
    // 令top-1即可,相当于使最后一个数据无效
  --pst->top;
}

  出栈操作比较简单,令top-1即可,因为top的含义是标识栈顶元素的下一个下标,top-1后,原来的最后一个元素就成了无效数据,倒数第二个元素就是栈顶元素了。

6.返回栈顶元素内容

// 返回的是元素内容,数据类型为我们在头文件里定义的STDataType
STDataType STTop(ST* pst)
{
    // pst不可能不存在
  assert(pst);
  // 存在元素才能找栈顶元素
  assert(pst->top > 0);
    // 返回top-1下标的数组元素
  return pst->a[pst->top - 1];
}

  由于top是栈顶元素的下一个,所以top-1的下标就是栈顶元素的下标,通过数组返回即可。

7.判断栈是否为空

// 判断真假的问题,我们在这里使用布尔型的返回类型
bool STEmpty(ST* pst)
{
    // pst不应为空,为空说明传参错误
  assert(pst);
  // 栈为空返回true,不为空返回false
  return pst->top == 0;
}

  因为top也可以表示为栈的有效数据的个数,所以可以根据top是否为0来判断栈是否为空。

8.查询栈的有效元素个数

// 返回有效数据的个数而不是数据内容,所以返回值为int
int STSize(ST* pst)
{
    // pst不会为空
  assert(pst);
    // 返回top的值
  return pst->top;
}

  同样,top既然可以作为栈有效元素的个数,那么直接将top返回即可。

五、代码整合及结果演示

1.代码整合

若在整合后出现某些函数不安全的错误,请在头文件里面加上下面这行代码。

#define _CRT_SECURE_NO_WARNINGS 1

1.头文件 Stack.h 部分

#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* pst);
// 销毁
void STDestroy(ST* pst);
// 在栈顶插入
void STPush(ST* pst, STDataType x);
// 在栈顶弹出
void STPop(ST* pst);
// 查看栈顶元素
STDataType STTop(ST* pst);
// 判断栈是否为空
bool STEmpty(ST* pst);
// 查询栈的元素个数
int STSize(ST* pst);

2.源文件 Stack.c 部分

#include"Stack.h"
void STInit(ST* pst)
{
  assert(pst);
  pst->a = NULL;
  pst->top = 0;
  pst->capacity = 0;
}
void STDestroy(ST* pst)
{
  assert(pst);
  free(pst->a);
  pst->a = NULL;
  pst->top = 0;
  pst->capacity = 0;
}
void STPush(ST* pst, STDataType x)
{
  assert(pst);
  if (pst->top == pst->capacity)
  {
    int newCapacity = pst->capacity == 0 ? 4 : 2 * pst->capacity;
    STDataType* tmp = (STDataType*)realloc(pst->a, sizeof(STDataType) * newCapacity);
    if (tmp == NULL)
    {
      perror("realloc fail");
      exit(-1);
    }
    pst->a = tmp;
    pst->capacity = newCapacity;
  }
  pst->a[pst->top] = x;
  ++pst->top;
}
void STPop(ST* pst)
{
  assert(pst);
  // 空栈不能删除
  assert(pst->top > 0);
  --pst->top;
}
STDataType STTop(ST* pst)
{
  assert(pst);
  // 存在元素才能找栈顶元素
  assert(pst->top > 0);
  return pst->a[pst->top - 1];
}
bool STEmpty(ST* pst)
{
  assert(pst);
  // 栈为空返回true,不为空返回false
  return pst->top == 0;
}
int STSize(ST* pst)
{
  assert(pst);
  return pst->top;
}

3.源文件 test.c 部分

#include"Stack.h"
void test()
{
  ST s;
  STInit(&s); 
  STPush(&s, 1);
  STPush(&s, 2);
  STPush(&s, 3);
  STPush(&s, 4);
  STPush(&s, 5);
  while (!STEmpty(&s))
  {
    printf("%d ", STTop(&s));
    STPop(&s);
  }
  printf("\n");
  STDestroy(&s);
  return 0;
}
int main()
{
  test();
  return 0;
}

2.结果演示


总结

  栈是特殊的线性表,但他比普通的线性表要简单很多,只要将普通的线性表了解透彻,那么学习栈将会非常轻松。所以说,打好基础也是很重要的,前面学的好,会帮助理解后面的内容,这是一个很好的良性循环。

  栈的内容不是很多,也比较简单,所以篇幅不算很长,但还是难免出现一些瑕疵,如果发现错误,欢迎指正。

目录
相关文章
|
2月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
168 77
|
21天前
|
算法 调度 C++
STL——栈和队列和优先队列
通过以上对栈、队列和优先队列的详细解释和示例,希望能帮助读者更好地理解和应用这些重要的数据结构。
27 11
|
1月前
|
DataX
☀☀☀☀☀☀☀有关栈和队列应用的oj题讲解☼☼☼☼☼☼☼
### 简介 本文介绍了三种数据结构的实现方法:用两个队列实现栈、用两个栈实现队列以及设计循环队列。具体思路如下: 1. **用两个队列实现栈**: - 插入元素时,选择非空队列进行插入。 - 移除栈顶元素时,将非空队列中的元素依次转移到另一个队列,直到只剩下一个元素,然后弹出该元素。 - 判空条件为两个队列均为空。 2. **用两个栈实现队列**: - 插入元素时,选择非空栈进行插入。 - 移除队首元素时,将非空栈中的元素依次转移到另一个栈,再将这些元素重新放回原栈以保持顺序。 - 判空条件为两个栈均为空。
|
2月前
|
存储 C++ 索引
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
【数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】初始化队列、销毁队列、判断队列是否为空、进队列、出队列等。本关任务:编写一个程序实现环形队列的基本运算。(6)出队列序列:yzopq2*(5)依次进队列元素:opq2*(6)出队列序列:bcdef。(2)依次进队列元素:abc。(5)依次进队列元素:def。(2)依次进队列元素:xyz。开始你的任务吧,祝你成功!(4)出队一个元素a。(4)出队一个元素x。
67 13
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
|
2月前
|
存储 C语言 C++
【C++数据结构——栈与队列】链栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现链栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储整数,最大
57 9
|
2月前
|
C++
【C++数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】
【数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】(1)遇到左括号:进栈Push()(2)遇到右括号:若栈顶元素为左括号,则出栈Pop();否则返回false。(3)当遍历表达式结束,且栈为空时,则返回true,否则返回false。本关任务:编写一个程序利用栈判断左、右圆括号是否配对。为了完成本关任务,你需要掌握:栈对括号的处理。(1)遇到左括号:进栈Push()开始你的任务吧,祝你成功!测试输入:(()))
50 7
|
2月前
|
存储 算法 测试技术
【C++数据结构——线性表】求集合的并、交和差运算(头歌实践教学平台习题)【合集】
本任务要求编写程序求两个集合的并集、交集和差集。主要内容包括: 1. **单链表表示集合**:使用单链表存储集合元素,确保元素唯一且无序。 2. **求并集**:遍历两个集合,将所有不同元素加入新链表。 3. **求交集**:遍历集合A,检查元素是否在集合B中存在,若存在则加入结果链表。 4. **求差集**:遍历集合A,检查元素是否不在集合B中,若满足条件则加入结果链表。 通过C++代码实现上述操作,并提供测试用例验证结果。测试输入为两个集合的元素,输出为有序集合A、B,以及它们的并集、交集和差集。 示例测试输入: ``` a c e f a b d e h i ``` 预期输出:
74 7
|
2月前
|
机器学习/深度学习 存储 C++
【C++数据结构——线性表】单链表的基本运算(头歌实践教学平台习题)【合集】
本内容介绍了单链表的基本运算任务,涵盖线性表的基本概念、初始化、销毁、判定是否为空表、求长度、输出、求元素值、按元素值查找、插入和删除数据元素等操作。通过C++代码示例详细解释了顺序表和链表的实现方法,并提供了测试说明、通 - **任务描述**:实现单链表的基本运算。 - **相关知识**:包括线性表的概念、初始化、销毁、判断空表、求长度、输出、求元素值、查找、插入和删除等操作。 - **测试说明**:平台会对你编写的代码进行测试,提供测试输入和预期输出。 - **通关代码**:给出了完整的C++代码实现。 - **测试结果**:展示了测试通过后的预期输出结果。 开始你的任务吧,祝你成功!
118 5
|
2月前
|
机器学习/深度学习 存储 C++
【C++数据结构——线性表】顺序表的基本运算(头歌实践教学平台习题)【合集】
本文档介绍了线性表的基本运算任务,涵盖顺序表和链表的初始化、销毁、判定是否为空、求长度、输出、查找元素、插入和删除元素等内容。通过C++代码示例详细展示了每一步骤的具体实现方法,并提供了测试说明和通关代码。 主要内容包括: - **任务描述**:实现顺序表的基本运算。 - **相关知识**:介绍线性表的基本概念及操作,如初始化、销毁、判定是否为空表等。 - **具体操作**:详述顺序表和链表的初始化、求长度、输出、查找、插入和删除元素的方法,并附有代码示例。 - **测试说明**:提供测试输入和预期输出,确保代码正确性。 - **通关代码**:给出完整的C++代码实现,帮助完成任务。 文档
85 5
|
4月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
115 5