C语言实现栈和队列【数据结构/初阶】

简介: C语言实现栈和队列【数据结构/初阶】

1. 栈

1.1 概念

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

压栈:栈的插入操作称作进栈/压栈/入栈,

出栈:栈的删除操作称作出栈。

压栈和出栈都在栈顶。

1.2 结构

例如:进栈顺序为1、2、3、4,那么(在进栈时无出栈)出栈时的顺序为4、3、2、1

1.3 栈的实现

栈可以用数组和链表实现。回顾并在此比较两者的优缺点:

  1. 数组可以通过下标随机访问,但插入删除数据需要挪动数据,但在栈中,这个缺点不会体现出来,因为数据出栈只能在栈顶出,不能在中间出栈。相当于在两端插入或删除数据。
  2. 链表虽然插入或删除数据不用挪动数据,但这个优点在栈中也不会体现出来。链表的内存不是连续的,CPU高速缓存命中率较低。而且由于链表的结构相较于数组较复杂,每次增加一个节点都要开辟一次内存,(数组一般一次开辟容量的2倍),对性能消耗也是相对较大的。

栈的实现用数组,因为在栈底插入数据的代价较小。

结构:

1.3.1 结构体和数组元素类型定义

结构体成员包含数组,成员数量计数器top(栈顶),容量计数器capacity

typedef int STDataType;
typedef struct Stack
{
  STDataType* data;//注意是数组形式
  int top;
  int capacity;
};
typedef struct Stack Stack;//这是另一种规范的定义写法

1.3.2 栈的初始化与销毁

起初栈需要有空间存放数组元素,capacity不为零,由于还没有元素放入,top为0,要为数组开辟capacity个以结构为大小的空间

//初始化栈
void StackInit(Stack* pst)
{
  assert(pst);
  pst->data = (STDataType*)malloc(sizeof(STDataType) * 4);
  pst->top = 0;
  pst->capacity = 4;
}

为避免造成内存泄漏的问题,有内存的开辟就一定要有内存的销毁

  • 只要将计数器置零,数组地址置空即可,无需在处理数组中之前存放的元素
//销毁栈
void StackDestroy(Stack* pst)
{
  assert(pst);
  free(pst->data);
  pst->data = NULL;
  pst->top = 0;
  pst->capacity = 0;
}

1.3.3 判断栈是否为空

巧妙地将top == 0的结果作为返回值,为空则返回true,否则返回false

有些编译器不支持C99,所以bool可以先定义一下

typedef int bool;
bool StackEmpty(Stack* pst)
{
  assert(pst);
  return pst->top == 0;//top为0则为真,否则为假
}

1.3.4 压栈

压栈的操作就是对数组的尾插

  • 注意判断元素个数是否达到容量
  • 注意判断达到容量以后开辟内存是否成功
void StackPush(Stack* pst, STDataType x)
{
  assert(pst);
  //判断top是否达到容量,达到则扩容
  if (pst->top == pst->capacity)
  {
    STDataType* tmp = (Stack*)realloc(pst->data, sizeof(STDataType) * pst->capacity * 2);//扩容
    if (tmp == NULL)//增容失败
    {
      printf("realloc failed\n");
      exit(-1);//告诉系统这是异常终止程序
    }
    pst->data = tmp;
    pst->capacity = pst->capacity * 2;//更新容量
  }
  pst->data[pst->top] = x;//压入数据
  pst->top++;
}

1.3.5 出栈

出栈相当于对数组尾删,只需将top往后移一步即可,无需再处理删除的元素

  • 注意判断栈是否为空
//出栈(相当于尾删)
void StackPop(Stack* pst)
{
  assert(pst);
  assert(!StackEmpty(pst));
  pst->top--;
}

1.3.6 获取栈顶元素

由于top是从0开始的,所以top指向的是最新元素的下一个位置,要得到栈顶元素,下标要-1

  • 注意判断栈是否为空
//获取栈顶元素
STDataType StackTop(Stack* pst)
{
  assert(pst);
  assert(!StackEmpty(pst));
  return pst->data[pst->top - 1];//top指向第一个元素的下一位
}

1.3.7 获取栈的元素个数

top就是元素个数计数器,直接返回它即可

//获取栈的元素个数
int StackSize(Stack* pst)
{
  assert(pst);
  return pst->top;//top从0开始,其值等于元素个数
}

1.3.8 打印

遍历打印即可

//打印
void StackPrint(Stack* pst)
{
  assert(pst);
  for (int i = 0; i < pst->top; i++)
  {
    printf("%d ", pst->data[i]);
  }
}

2. 队列

2.1 概念

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

入队列:进行插入操作的一端称为队尾

出队列:进行删除操作的一端称为队头

2.2 结构

2.3 队列的实现

队列的结构决定了它的性质,只能在队尾入,在队头出。所以如果用数组实现队列,就正好体现了它的缺点,每次头删都要重新挪动一次数据。使用链表实现队列能避免这个问题。

使用单向链表实现队列,其实就是对链表的尾增和头删。

结构:

2.3.1 链表和队列结构定义

typedef int QDataType;
//用链表表示队列
typedef struct QueueNode
{
  struct QueueNode* next;
  QDataType data;
};
typedef struct QueueNode QNode;
//队列结构
typedef struct Queue
{
  QNode* head;
  QNode* tail;
};

2.3.2 队列的初始化与销毁

初始化时,没有节点,head和tail指向NULL

//初始化
void QueueInit(Queue* pq)
{
  //0个元素,头尾指针置空
  pq->head = NULL;
  pq->tail = NULL;
}

将每个节点销毁,两指针重新指向NULL

void QueueDestroy(Queue* pq)
{
  QNode* cur = pq->head;
  while (cur)//将每个节点都销毁 
  {
    QNode* next = cur->next;
    free(cur);
    cur = next;//迭代
  }
  //头尾指针置空
  pq->head = NULL;
  pq->tail = NULL;
}

2.3.3 判断队列是否为空

思路同栈

如果编译器支持C99,那么可以引用头文件

#include <stdbool.h>
//判断队列是否为空
bool QueueEmpty(Queue* pq)
{
  assert(pq);
  return pq->head == NULL;
}

2.3.4 入队

相当于给单链表尾插

  • 注意判断开辟内存是否成功
  • 注意判断链表为空
//入队(尾插)
void QueuePush(Queue* pq, QDataType x)
{
  assert(pq);
  //新增节点
  QNode* newnode = (QNode*)malloc(sizeof(QNode));
  if (newnode == NULL)
  {
    printf("malloc  failed\n");
    exit(-1);
  }
  newnode->data = x;
  newnode->next = NULL;
  if (pq->tail == NULL)//没有元素
  {
    pq->head = newnode;
    pq->tail = newnode;
  }
  else
  {
    pq->tail->next = newnode;//链接旧尾和新节点
    pq->tail = newnode;//更新尾
  }
}

2.3.5 出队

相当于给单链表头删

  • 注意判断只有链表为空和只有一个节点的情况
//出队(头删)
void QueuePop(Queue* pq)
{
  assert(pq);
  assert(pq->head);//判断链表为空
  if (pq->head->next == NULL)//只有一个节点
  {
    free(pq->head);
    pq->head = NULL;
    pq->tail = NULL;
  }
  else
  {
    QNode* next = pq->head->next;//保存头的下一个节点
    free(pq->head);
    pq->head = next;
  }
}

2.3.6 获取队头和队尾的元素

思路同栈

//获取队头的元素
QDataType QueueFront(Queue* pq)
{
  assert(pq);
  assert(!QueueEmpty(pq));
  return pq->head->data;
}
//获取队尾的元素
QDataType QueueBack(Queue* pq)
{
  assert(pq);
  assert(!QueueEmpty(pq));
  return pq->tail->data;
}

2.3.7 获取队列中元素的个数

迭代计数即可

//获取队列中元素的个数
int QueueSize(Queue* pq)
{
  int size = 0;
  QNode* cur = pq->head;
  while (cur)
  {
    size++;
    cur = cur->next;
  }
  return size;
}

*3. 循环队列

循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。

循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。

来源:力扣(LeetCode)

循环队列用数组和链表皆可实现。但实现过程需要注意以下问题。

下面将循环队列形象地用环形表示:

head和tail起始都指向第一个元素,tail随着新元素的增加而往下走,直到元素个数达到队列容量。

问题在于:队列全空和队列全满的条件是相同的:head和tail指向同一个位置。

如何解决?

将一个位置空出来,不存储有效数据。

这样就能区分满和空两种情况了。

  • 队列全满:tail的下一个是head
  • 队列全空:tail和head相等(包括删除的情况)

3.1 设计循环队列

)]

思路:下面通过数组实现循环队列。思路同上,只不过在下标的处理、条件判断有所不同

3.1.1 队列结构的声明

typedef struct {
    int* a;//数组
    int k;//队列容量
    int head;
    int tail;
} MyCircularQueue;

3.1.2 队列初始化与销毁

要为队列和队列中的数组开辟空间,注意数组开辟的空间要比给定的k多一个。

MyCircularQueue* myCircularQueueCreate(int k) {
    MyCircularQueue* obj = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
    obj->a = (int*)malloc(sizeof(int) * (k+1));//要空出一个
    obj->k = k;
    obj->head = 0;
    obj->tail = 0;
    return obj;
}

销毁内存只需free掉之前开辟的内存即可,注意顺序

void myCircularQueueFree(MyCircularQueue* obj) {
    free(obj->a);//注意顺序
    free(obj);
}

3.1.3 判断队列是否为空

bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
    return obj->head == obj->tail;
}

3.1.4 判断队列是否已满

bool myCircularQueueIsFull(MyCircularQueue* obj) {
    int tailNext = obj->tail+1;
    if(tailNext == obj->k+1)
    {
        tailNext = 0;
    }
    return obj->head == tailNext;
}

细节处理:对下标的模处理可以转换为让它重新指向第一个元素的位置。

CPU中只有加法器,直接将它移到数组首端比模运算效率更高。

3.1.5 入队

  • 注意判断队列是否已满
  • 注意判断tail是否已经指向了最后一个空出来的位置
  • 注意计数器的迭代
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
    if(myCircularQueueIsFull(obj))//满了
    {
        return false;
    }
    else
    {
        obj->a[obj->tail] = value;
        obj->tail++;
        if(obj->tail == obj->k+1)
        {
            obj->tail = 0;
        }
        return true;
    }
}

3.1.6 出队

  • 注意判断队列是否为空
  • 注意判断tail是否已经指向了最后一个空出来的位置
  • 注意计数器的迭代
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
    if(myCircularQueueIsEmpty(obj))//空了
    {
        return false;
    }
    else
    {
        obj->head++;
        if(obj->head == obj->k+1)//tail已经指向了最后一个空出来的位置
        {
            obj->head = 0;
        }
        return true;
    }
}

3.1.7 取首

  • 注意判断队列是否为空
int myCircularQueueFront(MyCircularQueue* obj) {
    if(myCircularQueueIsEmpty(obj))
    {
        return -1;
    }
    else
    {
        return obj->a[obj->head];
    }
}

3.1.8 取尾

  • 注意判断队列是否为空
  • 注意判断tail是否已经指向了最后一个空出来的位置(最新的元素是否是最后一个第k个)
int myCircularQueueRear(MyCircularQueue* obj) {
    if(myCircularQueueIsEmpty(obj))
    {
        return -1;
    }
    else
    {
        int tailPrev = obj->tail - 1;//取最新的有效数据(tail指向它的下一个位置)
        if(tailPrev == -1)//说明最新的元素在第k个
        {
            tailPrev = obj->k;
        }
        return obj->a[tailPrev];
    }
}

日志

4/22/2022
4/22/2022
//修正了内存开辟部分变量的类型
目录
相关文章
|
4天前
|
定位技术 C语言
c语言及数据结构实现简单贪吃蛇小游戏
c语言及数据结构实现简单贪吃蛇小游戏
|
4天前
|
DataX
☀☀☀☀☀☀☀有关栈和队列应用的oj题讲解☼☼☼☼☼☼☼
### 简介 本文介绍了三种数据结构的实现方法:用两个队列实现栈、用两个栈实现队列以及设计循环队列。具体思路如下: 1. **用两个队列实现栈**: - 插入元素时,选择非空队列进行插入。 - 移除栈顶元素时,将非空队列中的元素依次转移到另一个队列,直到只剩下一个元素,然后弹出该元素。 - 判空条件为两个队列均为空。 2. **用两个栈实现队列**: - 插入元素时,选择非空栈进行插入。 - 移除队首元素时,将非空栈中的元素依次转移到另一个栈,再将这些元素重新放回原栈以保持顺序。 - 判空条件为两个栈均为空。
|
22天前
|
搜索推荐 C语言
数据结构(C语言)之对归并排序的介绍与理解
归并排序是一种基于分治策略的排序算法,通过递归将数组不断分割为子数组,直到每个子数组仅剩一个元素,再逐步合并这些有序的子数组以得到最终的有序数组。递归版本中,每次分割区间为[left, mid]和[mid+1, right],确保每两个区间内数据有序后进行合并。非递归版本则通过逐步增加gap值(初始为1),先对单个元素排序,再逐步扩大到更大的区间进行合并,直至整个数组有序。归并排序的时间复杂度为O(n*logn),空间复杂度为O(n),且具有稳定性,适用于普通排序及大文件排序场景。
|
1月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
142 77
|
1月前
|
C++
【C++数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】
【数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】(1)遇到左括号:进栈Push()(2)遇到右括号:若栈顶元素为左括号,则出栈Pop();否则返回false。(3)当遍历表达式结束,且栈为空时,则返回true,否则返回false。本关任务:编写一个程序利用栈判断左、右圆括号是否配对。为了完成本关任务,你需要掌握:栈对括号的处理。(1)遇到左括号:进栈Push()开始你的任务吧,祝你成功!测试输入:(()))
38 7
|
1月前
|
存储 C++ 索引
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
【数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】初始化队列、销毁队列、判断队列是否为空、进队列、出队列等。本关任务:编写一个程序实现环形队列的基本运算。(6)出队列序列:yzopq2*(5)依次进队列元素:opq2*(6)出队列序列:bcdef。(2)依次进队列元素:abc。(5)依次进队列元素:def。(2)依次进队列元素:xyz。开始你的任务吧,祝你成功!(4)出队一个元素a。(4)出队一个元素x。
44 13
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
|
1月前
|
存储 C语言 C++
【C++数据结构——栈与队列】链栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现链栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储整数,最大
46 9
|
3月前
|
算法 数据处理 C语言
C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合
本文深入解析了C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合,旨在帮助读者掌握这一高效的数据处理方法。
94 1
|
3月前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
99 1

热门文章

最新文章