【数据结构】链队列的C语言实现

简介: 【数据结构】链队列的C语言实现

队列

1.队列的概念

队列 和栈一样,是一个 特殊的线性表

队列只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表。进行 插入操作 的一端称为 队尾,进行 删除操作 的一端称为队头

队列中的元素遵守 先进先出(First In First Out) 的原则。就和排队一样,队列是绝对公平的,先来的先到队头,不存在插队行为,只能后面排队,前面离开。

d434d995a1314f2ba21eea504cec72ff.png

2.队列的结构

队列的结构可以用 数组链表 来实现。哪个结构更好?

数组:

数组左边为队头右边为队尾:队尾(右)插入数据 为 顺序表尾插 很方便,但是 队头(左)删除数据 需要挪动数据,很麻烦。

数组左边为队尾右边为队头:队头(右)删除数据 为尾删,队尾(左)插入数据 需要挪动数据,也很麻烦。

所以 数组结构 并不适合队列。


链表:

结构选择:单/双 循环/非循环 带头/不带头

带不带头?可要可不要,带头就是方便尾插,少一层判断,没什么影响。

双向吗?没必要找前一个元素,队列只需要队头队尾出入数据。

循环吗?价值不大。双向循环可以找尾,但是没有必要。


双向链表:

可以使用双向链表,但是没必要,小题大做了,使用单链表就可以。

3.队列的实现

3.1结构设计

上面确定了用 单链表实现,所以就一定要有结构体表示 节点

typedef struct QueueNode
{
  QDataType data;
  struct QueueNode* next;
}QNode;

由于链表的尾插比较麻烦,而队列的入数据为尾插。所以定义队列的结构体时,可以定义两个指针 headtail 分别对应 队头队尾 ,tail 的引入就是方便尾插,再在给定一个 sz 实时记录队列的 大小

typedef struct Queue
{
  QNode* head;
  QNode* tail;
  int sz;
}Queue;

大小

typedef struct Queue
{
  QNode* head;
  QNode* tail;
  int sz;
}Queue;

3.2接口总览

void QueueInit(Queue* pq); // 初始化
void QueueDestroy(Queue* pq); // 销毁
void QueuePush(Queue* pq, QDataType x); // 入队列
void QueuePop(Queue* pq); // 出队列
QDataType QueueFront(Queue* pq); // 取队头元素
QDataType QueueBack(Queue* pq); // 取队尾元素
bool QueueEmpty(Queue* pq); // 判空
int QueueSize(Queue* pq); // 计算队列大小

3.3初始化

队列初始化,就只需要结构中指针初始化为 NULL,并将 sz 初始化为0。

void QueueInit(Queue* pq)
{
  assert(pq);
  pq->head = pq->tail = NULL;
  pq->sz = 0;
}

这里是通过结构体的地址来找到结构体中的两个指针,通过结构体来改变指针的。

3.4销毁

我们实现的队列结构为 链式 的,所以本质为一条 单链表

那么销毁时,就需要迭代销毁链表的节点。

void QueueDestroy(Queue* pq)
{
  assert(pq);
  QNode* cur = pq->head;
  while (cur)
  {
    QNode* next = cur->next;
    free(cur);
    cur = next;
  }
  pq->head = pq->tail = NULL;
  pq->sz = 0;
}

3.5入队列

对于单链表的尾插,需要创建节点,找尾,然后链接。


但是我们设计队列结构时,给定了一个 tail 作为队尾,这时插入就比较方便了。但是需要注意一下 第一次尾插 的情况。


在 入队列 之后,记得调整 sz。


而且队列只会从队尾入数据,所以创建节点的一步,并没有必要封装一个接口专门调用,直接在函数中创建即可。

void QueuePush(Queue* pq, QDataType x)
{
  assert(pq);
  QNode* newnode = (QNode*)malloc(sizeof(QNode));
  if (newnode == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  else
  {
    newnode->data = x;
    newnode->next = NULL;
    //不置空newnode->next是随机值,会出问题
  }
  // 尾插
  if (pq->head == NULL)
  {
    assert(pq->tail == NULL);
    //头为空,尾却没为空,警告
    pq->head = pq->tail = newnode;
  }
  else
  {
    pq->tail->next = newnode;
    pq->tail = pq->tail->next;
  }
  pq->sz++; 
}

3.6 出队列

首先明确,队列为空不能出队列,出队列是从 队头 出数据。

其次,需要考虑删除时会不会有什么特殊情况。

一般删除时,可以记录 head 的下一个节点,然后释放 head ,再重新为 head 赋值。


但是,当 只有一个节点 呢?此刻 head == tail,它们的地址相同,如果此时仅仅释放了 head,最后head走到 NULL,但是tail 此刻指向被释放的节点,且没有置空,此刻风险就产生了,tail就变成野指针了。

1c35d2946ff1483dad45efe972dc96fd.png之后一旦我 入队列 时,tail != NULL,那么必定就会走到 else 部分,对 tail 进行访问,此刻程序就会奔溃,所以需要处理一下,将 tail 也置空

同样的,出队列 成功后 sz 需要发生调整。

void QueuePop(Queue* pq)
{
  assert(pq);
  assert(!QueueEmpty(pq));
  // 一个节点时删除的特殊情况
  // 需要将头尾都变为空
  if (pq->head->next == NULL)
  {
    free(pq->head);
    pq->head = pq->tail = NULL;
  }
  else
  {
    QNode* newhead = pq->head->next;
    free(pq->head);
    pq->head = newhead;
  }
  pq->sz--;
}

3.7取对头数据

队列非空,取 head 出数据

QDataType QueueFront(Queue* pq)
{
  assert(pq);
  assert(!QueueEmpty(pq));
  return pq->head->data;
}

3.8 取队尾数据

队列非空,取 tail 处数据。

QDataType QueueBack(Queue* pq)
{
  assert(pq);
  assert(!QueueEmpty(pq));
  return pq->tail->data;
}

3.9 判空

当 head 和 tail 都为空指针时,说明队列中无元素。

bool QueueEmpty(Queue* pq)
{
  assert(pq);
  return pq->head == NULL && pq->tail == NULL;
}

3.10 计算队列大小

从这个接口处,就可以看出设计结构时,设计的还是很精妙的,因为有 sz 直接返回即可。

int QueueSize(Queue* pq)
{
  assert(pq);
  return pq->sz;
}

试想一下,如果没有这个 sz,我们如何计算队列大小?是不是又得遍历链表了?这样接口的时间复杂度就为O(N),而其他接口几乎都是O(1)的复杂度,是不是不太理想?所以结构设计时加上一个 sz 的效果是极好的!

下面贴上没有 sz 时的代码:

int QueueSize(Queue* pq)
{
  assert(pq);
  QNode* cur = pq->head;
  int size = 0;
  while (cur)
  {
    cur = cur->next;
    ++size;
  }
  return size;
}

4. 完整代码

Queue.h

#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
typedef int QDataType;
typedef struct QueueNode
{
  QDataType data;
  struct QueueNode* next;
}QNode;
typedef struct Queue
{
  QNode* head;
  QNode* tail;
  int sz;
}Queue;
void QueueInit(Queue* pq); // 初始化
void QueueDestroy(Queue* pq); // 销毁
void QueuePush(Queue* pq, QDataType x); // 入队列
void QueuePop(Queue* pq); // 出队列
QDataType QueueFront(Queue* pq); // 取队头元素
QDataType QueueBack(Queue* pq); // 取队尾元素
bool QueueEmpty(Queue* pq); // 判空
int QueueSize(Queue* pq); // 计算队列大小

Queue.c

#include "Queue.h"
void QueueInit(Queue* pq)
{
  assert(pq);
  pq->head = pq->tail = NULL;
  pq->sz = 0;
}
void QueueDestroy(Queue* pq)
{
  assert(pq);
  QNode* cur = pq->head;
  while (cur)
  {
    QNode* next = cur->next;
    free(cur);
    cur = next;
  }
  pq->head = pq->tail = NULL;
  pq->sz = 0;
}
void QueuePush(Queue* pq, QDataType x)
{
  assert(pq);
  QNode* newnode = (QNode*)malloc(sizeof(QNode));
  if (newnode == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  else
  {
    newnode->data = x;
    newnode->next = NULL;
    //不置空newnode->next是随机值,会出问题
  }
  // 尾插
  if (pq->head == NULL)
  {
    assert(pq->tail == NULL);
    //头为空,尾却没为空,警告
    pq->head = pq->tail = newnode;
  }
  else
  {
    pq->tail->next = newnode;
    pq->tail = pq->tail->next;
  }
  pq->sz++;
}
void QueuePop(Queue* pq)
{
  assert(pq);
  assert(!QueueEmpty(pq));
  // 一个节点时删除的特殊情况
  // 需要将头尾都变为空
  if (pq->head->next == NULL)
  {
    free(pq->head);
    pq->head = pq->tail = NULL;
  }
  else
  {
    QNode* newhead = pq->head->next;
    free(pq->head);
    pq->head = newhead;
  }
  pq->sz--;
}
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;
}
bool QueueEmpty(Queue* pq)
{
  assert(pq);
  //return pq->size==0;
  return pq->head == NULL && pq->tail == NULL;
}
int QueueSize(Queue* pq)
{
  assert(pq);
  return pq->sz;
}

test.c

#include "Queue.h"
int main()
{
  Queue q;
  QueueInit(&q);
  QueuePush(&q,1);
  QueuePush(&q,2);
  QueuePush(&q,3);
  QueuePush(&q,4);
  while (!QueueEmpty(&q))
  {
    printf("%d ", QueueFront(&q));
    QueuePop(&q);
  }
  printf("\n");
  QueueDestroy(&q);
  system("pause");
  return 0;
}

7.总结:

今天我们认识并学习了队列的相关概念、结构与接口实现,并且针对每个常用的功能接口进行了实现。总体来说,链队列的结构相比于之前的数据结构是比较简单的,之后将介绍和讲解栈与队列的相关OJ题。希望我的文章和讲解能对大家的学习提供一些帮助。

当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~

c3ad96b16d2e46119dd2b9357f295e3f.jpg

相关文章
|
14天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
90 9
|
28天前
|
存储 算法 C语言
通义灵码在考研C语言和数据结构中的应用实践 1-5
通义灵码在考研C语言和数据结构中的应用实践,体验通义灵码的强大思路。《趣学C语言和数据结构100例》精选了五个经典问题及其解决方案,包括求最大公约数和最小公倍数、统计字符类型、求特殊数列和、计算阶乘和双阶乘、以及求斐波那契数列的前20项和。通过这些实例,帮助读者掌握C语言的基本语法和常用算法,提升编程能力。
|
13天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
56 16
|
13天前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
62 8
|
16天前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
44 4
|
17天前
|
存储 C语言
【数据结构】顺序表(c语言实现)(附源码)
本文介绍了线性表和顺序表的基本概念及其实现。线性表是一种有限序列,常见的线性表有顺序表、链表、栈、队列等。顺序表是一种基于连续内存地址存储数据的数据结构,其底层逻辑是数组。文章详细讲解了静态顺序表和动态顺序表的区别,并重点介绍了动态顺序表的实现,包括初始化、销毁、打印、增删查改等操作。最后,文章总结了顺序表的时间复杂度和局限性,并预告了后续关于链表的内容。
49 3
|
17天前
|
存储 算法 C语言
C语言数据结构(2)
【10月更文挑战第21天】
|
28天前
|
存储 算法 C语言
【趣学C语言和数据结构100例】
《趣学C语言和数据结构100例》精选5个编程问题,涵盖求最大公约数与最小公倍数、字符统计、特殊序列求和及阶乘计算等,通过实例讲解C语言基础与算法思维,适合初学者实践学习。
|
1月前
|
存储 C语言
探索C语言数据结构:利用顺序表完成通讯录的实现
本文介绍了如何使用C语言中的顺序表数据结构实现一个简单的通讯录,包括初始化、添加、删除、查找和保存联系人信息的操作,以及自定义结构体用于存储联系人详细信息。
19 2
|
16天前
|
C语言
【数据结构】双向带头循环链表(c语言)(附源码)
本文介绍了双向带头循环链表的概念和实现。双向带头循环链表具有三个关键点:双向、带头和循环。与单链表相比,它的头插、尾插、头删、尾删等操作的时间复杂度均为O(1),提高了运行效率。文章详细讲解了链表的结构定义、方法声明和实现,包括创建新节点、初始化、打印、判断是否为空、插入和删除节点等操作。最后提供了完整的代码示例。
37 0