【C】初阶数据结构6 -- 队列

类似于栈,队列也是一种特殊的数据结构,其底层的结构仍是顺序表和链表。


目录

[1 队列的概念与特点](#1 队列的概念与特点)

[1) 概念](#1) 概念)

[2) 特点](#2) 特点)

[2 队列的结构](#2 队列的结构)

[1) 逻辑结构](#1) 逻辑结构)

[2) 物理结构](#2) 物理结构)

[3 队列的实现](#3 队列的实现)

[1) 初始化队列和销毁队列](#1) 初始化队列和销毁队列)

[(1) 初始化队列](#(1) 初始化队列)

[(2) 销毁队列](#(2) 销毁队列)

[2) 入队和出队](#2) 入队和出队)

[(1) 入队](#(1) 入队)

[(2) 出队](#(2) 出队)

[3) 取队头与队尾元素](#3) 取队头与队尾元素)

[4) 判空和返回有效元素个数](#4) 判空和返回有效元素个数)

[4 代码](#4 代码)


cpp 复制代码
重点一  队列的特点

1 队列的概念与特点

1) 概念

队列是一种特殊的线性表。其只允许在一端进行插入数据,另一端进行删除数据。

入队:是向队列中插入数据的过程。插入数据的一端叫做队尾。

出队:是在队列中删除数据的过程。删除数据的一端叫做队头。

其实队列数据结构就像现实生活中排队时的队列,一个一个的数据就像排队的人,你要排队的时候就要从队尾排队,所以插入数据的一端就是队尾;同样的,如果你要离开队列,就要从队头离开,所以删除数据的一端叫做队头。

2) 特点

队列的特点与现实中的队列相同。先入队的肯定先出队,所以数据结构队列具有FIFO(first in first out)-- 先进先出的特点(正好与栈相反)。


cpp 复制代码
重点二  队列的结构

2 队列的结构

1) 逻辑结构

我们可以把队列想象成以下的结构:

2) 物理结构

队列的物理结构既可以使用链表,也可以使用数组(顺序表),这里选择使用链表来实现队列的结构(这里为了简化结构,使用单链表来实现)。

物理结构使用链表是基于其特点来选择的:队列符合先进先出的特点,如果选择数组的话,需要在尾部插入数据,在头部删除数据,虽然插入数据的时间复杂度为O(1),但是删除数据的时候,需要频繁挪动数据,时间复杂度为O(n),不是很方便,所以这里选择使用链表来作为其底层结构。

但是链表的尾插仍然是O(n),时间复杂度较高,所以这里会优化其结构,并不单单使用单链表。尾插的时间复杂度为O(n),究其原因是因为需要从首节点开始遍历整个链表找到尾节点,所以在实现队列的结构的结构时,不仅需要有节点的结构,还需要有两个指针来指向队列的头和尾,方便进行数据的插入和删除。

队列的结构如下:

cpp 复制代码
typedef int QDataType;
//定义队列结点的结构
typedef struct QueueNode
{
  QDataType data;//队列结点里的数据
  struct QueueNode* next;//指向下一个结点的指针
}QNode;
//定义队列的结构
typedef struct Queue
{
  QNode* phead;//对头,允许删除数据
  QNode* ptail;//队尾,允许插入数据
}Queue;

3 队列的实现

队列相关的操作主要有初始化队列、销毁队列、入队、出队、取队头元素、取队尾元素、判空以及有效元素的个数。

1) 初始化队列和销毁队列

(1) 初始化队列

初始化队列比较简单,只需要让队头和队尾指针指向 NULL 就可以了。

(2) 销毁队列

由于队列是以单链表作为底层结构的,所以其销毁就类似于单链表的销毁。这里定义一个 pcur 指针来指向队列的队头节点,然后循环判断 pcur 是否为 NULL(是否走到尾节点),在循环中保存 pcur 下一个节点的指针,然后释放 pcur 当前指向的节点,让 pcur 走到下一个节点,最后不要忘记让 phead 和 ptail (队头指针和队尾指针)都指向空,不然会变成野指针。

当然,也可以判断 pcur 是否等于 ptail (队尾指针),但是循环结束后,还需要释放队尾指针指向的节点。最后也不要忘记让 phead 和 ptail 都指向NULL。


2) 入队和出队

(1) 入队

入队是在队尾插入数据,所以就相当于单链表的尾插,之前单链表的尾插需要从头节点找到尾节点,但是在队列里面,尾插可以直接找到尾节点,就是 patil 节点,入队的过程如图所示:

这里需要判断一下特殊情况,就是队列为空(phead == NULL)的情况,此时 phead 与 ptail 都指向NULL,这样在改变 ptail 指向节点的 next 指针的时候,就会发生对 NULL 指针的解引用,会报错,所以为空时需要特殊处理一下:开辟一个新节点newnode,让 phead 和 ptail 都指向 newnode就可以了。

(2) 出队

出队就是单链表的头删,既然是删除数据,就需要在出队之前首先判断队列为不为空,出队的过程如图所示:

但是有一个特殊情况需要特殊处理一下,就是队列中只有一个节点的情况,虽然不会出现对 NULL 指针的解引用情况,但是此时队列为空了,但是 ptail 还指向已经释放了的节点,已经变成了野指针,所以需要把 ptail 也变为 NULL。


3) 取队头与队尾元素

这两个接口很简单,取队头元素只需要返回 phead 指向节点的 data 数据就可以了,取队尾元素只需要返回 ptail 指向节点的 data 数据。但是,在这之前需要先判断队列是否非空,如果是空队列,那么 phead 和 ptail 就是 NULL,访问 data 数据会发生对 NULL 指针的解引用的。


4) 判空和返回有效元素个数

判空很简单,只需要判断 phead 是否为 NULL 就可以了。

返回有效元素个数也很简单,但是这里注意不能用 ptail - phead 来计算元素个数,因为其地址并不是连续的。这里应该用一个计数器 count 来记录元素个数,然后用一个指针变量 pcur 来遍历队列,遍历完后返回 count 即可。

当然,如果这样写其时间复杂度就是 O(n),如果想要时间复杂度变为 O(1) 的话,可以在队列之中添加一个 size 变量:

cpp 复制代码
typedef struct Queue
{
  QNode* phead;//队头指针
  QNode* ptail;//队尾指针
  size_t size;//有效元素个数
}Queue;

这样设计结构的话,只需要返回 size 就可以了。但是这样写不要忘记在入队时 ++size ,在出队时 --size。


4 代码

Queue.h文件:

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

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

typedef struct Queue
{
  QNode* phead;
  QNode* ptail;
}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文件:

cpp 复制代码
#include"Queue.h"

//初始化队列
void QueueInit(Queue* pq)
{
  assert(pq);
  pq->phead = NULL;
  pq->ptail = NULL;
}

//销毁队列
//1
//void QueueDestroy(Queue* pq)
//{
// assert(pq);
//  //相当于单链表的销毁
//  QNode* pcur = pq->phead;
//  while (pcur != pq->ptail)
//  {
//    QNode* next = pcur->next;
//    free(pcur);
//    pcur = next;
//  }
//  //不要忘记释放队尾指针指向节点
//  free(pq->ptail);
//  pq->phead = pq->ptail = NULL;
//}

//2
void QueueDestroy(Queue* pq)
{
  assert(pq);
  //相当于单链表的销毁
  QNode* pcur = pq->phead;
  while (pcur)
  {
    QNode* next = pcur->next;
    free(pcur);
    pcur = next;
  }
  pq->phead = pq->ptail = NULL;
}

//入队
void QueuePush(Queue* pq, QDataType x)
{
  assert(pq);
  //相当于单链表的尾插
  if (pq->phead == NULL)
  {
    QNode* newnode = (QNode*)malloc(sizeof(QNode));
    if (newnode == NULL)
    {
      perror("malloc fail!\n");
      exit(1);
    }
    newnode->data = x;
    newnode->next = NULL;
    pq->phead = pq->ptail = newnode;
  }
  else
  {
    //开辟一个新结点
    QNode* newnode = (QNode*)malloc(sizeof(QNode));
    //可以再判断newnode有效性,这里就不判断了
    newnode->data = x;
    newnode->next = NULL;
    pq->ptail->next = newnode;
    pq->ptail = pq->ptail->next;
  }
}
//队列判空
bool QueueEmpty(Queue* pq)
{
  assert(pq);
  //只需判断pq->phead指针为不为空
  return pq->phead == NULL;
}

//出队
void QueuePop(Queue* pq)
{
  //出队首先要判断队列为不为空
  assert(!QueueEmpty(pq));
  //相当于单链表的头删
  //只有一个结点
  if (pq->phead->next == NULL)
  {
    free(pq->phead);
    pq->phead = pq->ptail = NULL;
  }
  else
  {
    //多个结点
    QNode* next = pq->phead->next;
    free(pq->phead);
    pq->phead = next;
  }

}

//取队头元素
QDataType QueueFront(Queue* pq)
{
  //判断队列为不为空
  assert(!QueueEmpty(pq));
  //返回队头指针所指向结点的数据
  return pq->phead->data;
}

//取队尾元素
QDataType QueueBack(Queue* pq)
{
  //判断队列为不为空
  assert(!QueueEmpty(pq));
  //返回队尾指针所指向结点的数据
  return pq->ptail->data;
}

//队列有效元素个数
int QueueSize(Queue* pq)
{
  assert(pq);
  //创建一个计数器变量
  int count = 0;
  QNode* pcur = pq->phead;
  while (pcur)
  {
    pcur = pcur->next;
    count++;
  }
  return count;
}

test.c文件:

cpp 复制代码
#include"Queue.h"

void Test5()
{
    Queue q;
    QueueInit(&q);
    //测试入队
    QueuePush(&q, 1);
    QueuePush(&q, 2);
    QueuePush(&q, 3);
    QueuePush(&q, 4);
    //测试取对头元素与取队尾元素
    /*QDataType ret1 = QueueFront(&q);
    printf("%d\n", ret1);
    QDataType ret2 = QueueBack(&q);
    printf("%d\n", ret2);*/
    //测试出队
    QueuePop(&q);
    QueuePop(&q);
    //QueuePop(&q);
    //QueuePop(&q);
    //QueuePop(&q);
    QDataType ret1 = QueueFront(&q);
    printf("%d\n", ret1);
    QDataType ret2 = QueueBack(&q);
    printf("%d\n", ret2);
    int size = QueueSize(&q);
    printf("%d\n", size);
    QueueDestroy(&q);
}

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

可以看到,队列基本上就是单链表的复现,只不过增加了一个队头指针和队尾指针,相信在理解了单链表的基础上,队列也就很简单了。

相关推荐
Dizzy.51726 分钟前
数据结构(查找)
数据结构·学习·算法
软件开发技术局1 小时前
撕碎QT面具(8):对控件采用自动增加函数(转到槽)的方式,发现函数不能被调用的解决方案
开发语言·qt
周杰伦fans2 小时前
C#中修饰符
开发语言·c#
yngsqq2 小时前
c# —— StringBuilder 类
java·开发语言
赔罪3 小时前
Python 高级特性-切片
开发语言·python
Jared_devin3 小时前
数据结构——模拟栈例题B3619
数据结构
sushang~4 小时前
leetcode21.合并两个有序链表
数据结构·链表
子豪-中国机器人4 小时前
2月17日c语言框架
c语言·开发语言
夏天的阳光吖4 小时前
C++蓝桥杯基础篇(四)
开发语言·c++·蓝桥杯
oioihoii4 小时前
C++17 中的 std::to_chars 和 std::from_chars:高效且安全的字符串转换工具
开发语言·c++