初级数据结构(四)——队列

   文中代码源文件已上传:数据结构源码

<-上一篇 初级数据结构(三)——栈        |        初级数据结构(五)——树和二叉树的概念 下一篇->

        本篇是属于上一篇的补充篇,因为队列和栈的属性特别类似,很多细节部分可以查看上一篇或者初级据结构的第二篇。

1、队列特性

         之前已知,栈结构特性为 LIFO ,队列则是与之相反的先入先出,后入后出,也称为 FIFO ( Fist In Fist Out )。如下图:

         因此,队列与栈的区别只在于弹出顺序,其余完全一致。但是,基于队列的特性,如果选用顺序表实现,则需要不断腾挪数据以填充弹出的头部位置,因此这里最好选用链表来实现以减小计算机资源的开销。

2、文件结构

        仍然是三个文件分模块实现:

        queue.h :用于创建项目的结构体类型以及声明函数;

        queue.c :用于创建队列各种操作功能的函数;

        main.c :仅创建 main 函数,用作测试。

3、队列构建

3.1、代码

        queue.h 中代码如下:

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

#define DATAPRT "%d"
typedef int DATATYPE;

typedef struct QueueNode
{
	DATATYPE data;
	struct QueueNode* next;
}QueueNode;

typedef struct QueueInfo
{
	int size;
	QueueNode* head;
	QueueNode* tail;
}QueueInfo;

//函数定义---------------------------------

//队列初始化
extern void QueueInit(QueueInfo*);
//数据入队
extern void QueuePush(QueueInfo*, DATATYPE);
//数据出队
extern void QueuePop(QueueInfo*);
//销毁队列
extern void QueueDestroy(QueueInfo*);
//获取队列数据
extern DATATYPE QueueGetData(QueueInfo*);
//打印队列数据
extern void QueuePrint(QueueInfo*);

        queue.c 中代码:

#include "queue.h"

//队列初始化
void QueueInit(QueueInfo* info)
{
	//参数有效性验证
	if (!info)
	{
		fprintf(stderr, "Queue Pointer NULL\n");
		return;
	}
	//初始化
	info->size = 0;
	info->head = NULL;
	info->tail = NULL;
}

//数据入队
void QueuePush(QueueInfo* info, DATATYPE data)
{
	//参数有效性验证
	if (!info)
	{
		fprintf(stderr, "Queue Pointer NULL\n");
		return;
	}
	//创建节点
	QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
	//创建成功验证
	if (!newNode)
	{
		fprintf(stderr, "Malloc Fail\n");
		return;
	}
	//新节点初始化
	newNode->data = data;
	newNode->next = NULL;
	//链接
	if (!info->size)
	{
		info->head = newNode;
		info->tail = newNode;
	}
	else
	{
		info->tail->next = newNode;
		info->tail = newNode;
	}
	info->size++;
}

//数据出队
void QueuePop(QueueInfo* info)
{
	//参数有效性验证
	if (!info)
	{
		fprintf(stderr, "Queue Pointer NULL\n");
		return;
	}
	//空表判断
	if (!info->size)
	{
		fprintf(stderr, "Queue Is Empty\n");
	}
	QueueNode* headNode = info->head->next;
	free(info->head);
	info->head = headNode;
	info->size--;
	if (!info->size)
	{
		info->tail = NULL;
	}
}

//销毁队列
void QueueDestroy(QueueInfo* info)
{
	//参数有效性验证
	if (!info)
	{
		fprintf(stderr, "Queue Pointer NULL\n");
		return;
	}
	//逐级释放节点
	if (info->size > 0)
	{
		while (info->head != info->tail)
		{
			QueueNode* currentNode = info->head;
			info->head = info->head->next;
			free(currentNode);
		}
		free(info->head);
	}
	QueueInit(info);
}

//获取队列数据
DATATYPE QueueGetData(QueueInfo* info)
{
	//参数有效性验证
	assert(info);
	assert(info->head);
	DATATYPE data = info->head->data;
	QueuePop(info);
	return data;
}

//打印队列数据
void QueuePrint(QueueInfo* info)
{
	//参数有效性验证
	if (!info)
	{
		fprintf(stderr, "Queue Pointer NULL\n");
		return;
	}
	//空队列打印NULL
	if (info->size == 0)
	{
		printf("NULL ");
		return;
	}
	printf(DATAPRT" ", QueueGetData(info));
}

        main.c 中的测试用例:

#include "queue.h"

int main()
{
	QueueInfo info;
	QueueInit(NULL);
	QueueInit(&info);
	QueuePush(&info, 1);
	QueuePush(&info, 2);
	QueuePush(&info, 3);
	QueuePush(&info, 4);
	QueuePush(&info, 5);
	QueuePush(&info, 6);
	QueuePrint(&info);
	QueuePrint(&info);
	QueuePrint(&info);
	QueuePrint(&info);
	QueuePrint(&info);
	QueuePrint(&info);
	QueuePrint(&info);
	QueuePush(&info, 1);
	QueuePush(&info, 2);
	QueuePush(&info, 3);
	QueuePush(&info, 4);
	QueuePush(&info, 5);
	QueuePush(&info, 6);
	QueuePop(&info);
	QueuePrint(&info);
	QueuePrint(&info);
	QueuePop(&info);
	QueuePrint(&info);
	QueuePush(&info, 3);
	QueuePrint(&info);
	QueuePrint(&info);
	QueuePrint(&info);
	QueuePrint(&info);
	QueueDestroy(&info);
	return 0;
}

        以上便是全部代码,可自行测试和参考。构建过程中有一些需要注意的点,接下来作阐述。

3.2、构建要点

        1、与常规单链表不同,队列的构建最好以两种不同的结构体进行操作,除了节点信息的结构体之外,需要另外起一个结构体替代单链表的头节点指针。在此结构体内,储存的除了链表头节点指针之外,由于队列的特性,插入数据都是在尾部进行,因此另行创建一个成员变量用于保存尾节点指针。除此之外,为了方便判断队列中的节点个数(常规只判断是否为空队列),最好定义一个成员变量用于储存队列中节点个数。

        2、对于空队列和非空队列的数据入队需要分开讨论。空队列的数据入队需要同时操作头节点及尾节点指针同时指向第一个入队的元素。至于非空队列,需要操作尾节点指针,虽然不再操作头节点指针,但需要另行操作尾节点内部的 next 指针。

        3、数据出队时,如果操作对象时空队列则结束操作,可根据个人喜好自行选择用 assert 警告或者其他方式判断是否为空队列。

        4、销毁队列与单链表的销毁操作一致,直接参考即可。

4、用顺序表构建队列

        虽然不推荐用顺序表,但不代表顺序表不可行。但是在用顺序表构建队列时最好人为地设置阈值以控制什么时候将数组的数据移动至数组前端。

4.1、代码

        文件结构仍与链表实现队列的文件结构一致。代码如下:

        queue.h :

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

#define DATAPRT "%d"

typedef int DATATYPE;

typedef struct Queue
{
	//数据记录
	size_t size;
	int head;
	int tail;
	size_t capacity;
	//数据
	DATATYPE data[0];
}Queue;

//函数声明------------------------------------------
//初始化创建队列
extern Queue* QueueCreate(void);
//数据入队
extern void QueuePush(Queue*, DATATYPE);
//数据出队
extern void QueuePop(Queue*);
//销毁队列
extern void QueueDestroy(Queue**);
//获取队列数据
extern DATATYPE QueueGetData(Queue*);
//打印队列数据
extern void QueuePrint(Queue*);

        queue.c :

#include "queue.h"

//初始化创建队列
Queue* QueueCreate(void)
{
	//创建队列
	Queue* queue = (Queue*)malloc(sizeof(Queue) + 4 * sizeof(DATATYPE));
	//队列创建结果检查
	if (!queue)
	{
		fprintf(stderr, "Malloc Fail");
		return;
	}
	//队列数据初始化
	queue->size = 0;
	queue->head = -1;
	queue->tail = -1;
	queue->capacity = 4;
	return queue;
}

//数据入队
void QueuePush(Queue* queue, DATATYPE data)
{
	//参数有效性检查
	if (!queue)
	{
		fprintf(stderr, "Queue Address NULL\n");
		return;
	}
	queue->tail++;
	if (queue->tail + 1 >= queue->capacity)
	{
		//队列扩容
		size_t tempSize = (sizeof(Queue) + sizeof(DATATYPE) * queue->capacity * 2);
		Queue* tempQueue = (Queue*)realloc(queue, tempSize);
		//队列扩容结果检查
		if (!tempQueue)
		{
			fprintf(stderr, "Realloc Fail\n");
			return;
		}
		queue = tempQueue;
		queue->capacity *= 2;
	}
	//数据入队
	queue->data[queue->tail] = data;
	//空队列判断
	if (0 == queue->size)
	{
		queue->head = queue->tail;
	}
	queue->size++;
}

//数据出队
void QueuePop(Queue* queue)
{
	//参数有效性检查
	if (!queue)
	{
		fprintf(stderr, "Queue Address NULL\n");
		return;
	}
	//空队列检查
	if (!queue->size)
	{
		fprintf(stderr, "Queue Is Empty\n");
		return;
	}
	queue->head++;
	queue->size--;
	//弹出后空队列检查
	if (!queue->size)
	{
		queue->head = -1;
		queue->tail = -1;
	}
	//空间回收
	if ((queue->size < queue->capacity / 2) && queue->capacity > 4)
	{
		//数据迁移
		for (int i = queue->head, j = 0; i <= queue->tail; i++, j++)
		{
			queue->data[j] = queue->data[i];
		}
		//空间收缩
		size_t tempSize = (sizeof(Queue) + sizeof(DATATYPE) * queue->capacity / 2);
		Queue* tempQueue = (Queue*)realloc(queue, tempSize);
		//队列空间收缩结果检查
		if (!tempQueue)
		{
			fprintf(stderr, "Realloc Fail");
			return;
		}
		//记录迁移
		tempQueue->capacity = queue->capacity / 2;
		tempQueue->head = 0;
		tempQueue->tail = queue->size - 1;
		queue = tempQueue;
	}
}

//销毁队列
void QueueDestroy(Queue** queue)
{
	//参数有效性检查
	if (!(*queue) || !queue)
	{
		fprintf(stderr, "Queue Address NULL\n");
		return;
	}
	free(*queue);
	*queue = NULL;
}

//获取队列数据
DATATYPE QueueGetData(Queue* queue)
{
	//参数有效性检查
	assert(queue);
	//空队列检查检查
	assert(queue->size);
	DATATYPE data = queue->data[queue->head];
	QueuePop(queue);
	return data;
}


//打印队列数据
void QueuePrint(Queue* queue)
{
	//参数有效性检查
	assert(queue);
	//空队列检查检查
	if (!queue->size)
	{
		printf("NULL ");
		return;
	}
	printf(DATAPRT" ", QueueGetData(queue));
}

        main.c 的测试用例:

#include "queue.h"

int main()
{
	Queue* queue = QueueCreate();
	QueuePush(NULL, 1);
	QueuePush(queue, 1);
	QueuePush(queue, 2);
	QueuePush(queue, 3);
	QueuePush(queue, 4);
	QueuePush(queue, 5);
	QueuePush(queue, 6);
	QueuePush(queue, 7);
	QueuePush(queue, 8);
	QueuePush(queue, 9);
	QueuePop(NULL);
	QueuePrint(queue);
	QueuePrint(queue);
	QueuePush(queue, 70);
	QueuePush(queue, 72);
	QueuePrint(queue);
	QueuePrint(queue);
	QueuePrint(queue);
	QueuePop(queue);
	QueuePop(queue);
	QueuePop(queue);
	QueuePop(queue);
	QueuePrint(queue);
	QueuePrint(queue);
	QueuePrint(queue);
	QueuePrint(queue);
	QueuePush(queue, 1);
	QueuePush(queue, 2);
	QueuePush(queue, 3);
	QueuePush(queue, 4);
	QueuePush(queue, 5);
	QueuePush(queue, 6);
	QueuePush(queue, 7);
	QueuePush(queue, 8);
	QueuePush(queue, 9);
	QueuePrint(queue);
	QueuePrint(queue);
	QueueDestroy(&queue);
	QueuePrint(queue);

	return 0;
}

4.2、构建要点

        这里采用的是柔性数组顺序表,当然也可以用结构体包含数组指针的顺序表构建。

        1、对于空队列的初始化,头数据的下标与尾数据的下标应该设置为 -1 ,这点与栈的顶部数据下标设置完全一致。

        2、通过头部元素的数组下标及尾部元素的数组下标来进行队列有效部分内容的控制,出队则头部数组下标 +1 ,入队则数组尾部下标 +1。

        3、数据出队必须进行空队列检查。此外,出队之后对队列中剩余有效元素进行判断,最佳方案是,如果剩余数据元素个数小于开辟空间的一半,则从头部至尾部元素依次腾挪到数组 0 下标位置开始的空间。腾挪完毕后再对过大的空间进行回收。

        4、此外,数据腾挪的时候使用 realloc 有个坑。由于 realloc 的原地扩容和异地扩容是不可控的,如果 realloc 是原地扩容,对临时队列的操作也同样会影响到真实的队列。如上述代码的出队函数,如果用 realloc 创建 tempQueue ,有可能 tempQueue 与 queue 指向同一块空间,所以切不可先对空间进行回收后才进行数据腾挪。

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
青葱年少的头像青葱年少普通用户
上一篇 2023年12月19日
下一篇 2023年12月19日

相关推荐