用c语言实现——一个带头节点的链队列,支持用户输入交互界面、初始化、入队、出队、查找、判空判满、显示队列、遍历计算长度等功能

一、知识介绍

带头节点的链队列是一种基于链表实现的队列结构,它在链表的头部添加了一个特殊的节点,称为头节点。头节点不存储实际的数据元素,主要作用是作为链表的起点,简化队列的操作和边界条件处理。

1.节点结构

链队列的每个节点由两部分组成:

  • 数据域(data):用于存储队列中的元素。

  • 指针域(next):用于指向下一个节点。

头节点是链队列的第一个节点,它不存储实际数据,但它的指针域指向队列的第一个实际数据节点。

2.队列结构

链队列通常包含两个指针:

  • 队头指针(front):指向头节点。

  • 队尾指针(rear):指向队列的最后一个节点。

3.初始化

在初始化链队列时,会创建一个头节点,并将队头指针和队尾指针都指向这个头节点。此时,队列为空。

4.入队操作

入队操作在队列的尾部添加一个新元素:

  1. 创建一个新节点。

  2. 将队尾节点的指针域指向新节点。

  3. 更新队尾指针指向新节点。

5.出队操作

出队操作从队列的头部移除一个元素:

  1. 检查队列是否为空。

  2. 如果队列不为空,获取队头节点的下一个节点的数据。

  3. 更新队头节点的指针域,使其指向下一个节点的下一个节点。

  4. 如果队列中只有一个元素,出队后需要将队尾指针也指向头节点。

6.判空

检查队头指针和队尾指针是否都指向头节点:

  • 如果是,则队列为空。

  • 否则,队列不为空。

7.判满

在动态内存分配的链队列中,队列通常不会满,除非系统内存耗尽。可以通过尝试分配新节点来间接判断队列是否已满。

8.显示队列和计算长度

通过遍历链表,从头节点的下一个节点开始,直到队尾节点,可以显示队列中的所有元素并计算队列的长度。

9.优势

  1. 简化操作:头节点的存在使得队列的操作(如入队、出队)更加统一,避免了对空队列或单节点队列的特殊处理。

  2. 边界条件处理方便:头节点作为固定的起点,使得处理队列为空或只有一个节点的情况更加简单。

  3. 增强可读性:代码逻辑更加清晰,易于理解和维护。

带头节点的链队列是链表的一种变体,通过引入头节点,提高了队列操作的效率和代码的健壮性。

二、思路大致

①定义结构体:有两种结构体👇

一个是节点结构体👉👉包含数据和指向下一个节点的指针;

一个是队列结构体👉👉包含front和rear。

②用户交互界面:

设计一个菜单,让用户选择不同的操作。用循环显示选项,输入数字选择操作。这可以用printf和scanf实现。

❗❗但要注意处理输入错误,比如用户输入的不是数字,或者选项不在范围内。这时候可能需要清空输入缓冲区,防止后续操作出错。

③初始化函数:

需要创建头节点,并让front和rear都指向它。

❗❗如果用户未初始化就进行入队或出队操作,程序应该提示错误。所以每个操作前都要检查队列是否已经初始化。

在程序中定义一个全局的LinkQueue变量,初始时front和rear都为NULL。初始化函数会创建头节点,并让front和rear指向它。之后的其他操作首先检查front是否为NULL,如果是,说明未初始化。这可能更简单。所以,假设有一个全局的LinkQueue queue变量,初始化的时候创建头节点,front和rear指向它。否则,front和rear都是NULL。那么,在入队或出队之前,检查front是否为NULL,如果是,提示用户先初始化队列。

④用户输入数据:

将数据添加到队尾。

❗❗这里要注意用户输入的数据是否有效,比如是否是整数。比如用scanf读取整数时,如果用户输入了字符,会导致错误。这时候需要处理输入错误的情况,可能需要清空输入缓冲区,并提示重新输入。

⑤出队操作:

需要判断队列是否为空,如果为空则提示不能出队,否则删除队头元素,并释放内存。

❗❗如果出队后队列为空,需要将rear指向front(即头节点),否则rear可能成为野指针。

⑥查找功能:

需要用户输入一个元素,然后遍历队列查找是否存在该元素。这里需要遍历头节点的下一个节点开始,直到rear指向的节点。

⑦判空判满,链队列理论上不会满,除非内存不足,所以判满可以返回false或者不处理。判空就是判断front == rear。

⑧显示队列需要从头节点的下一个节点开始,依次输出每个节点的数据,直到队尾。

⑨计算长度则是遍历队列,统计节点数量。

⑩代码的健壮性。

❗❗在初始化时:如果内存分配失败,要处理错误。

❗❗在入队时:如果新节点分配失败,也要提示错误。

❗❗每个操作前:都要检查队列是否已经初始化。

❗❗对于用户输入的选项:需要处理非法输入,比如输入字符而不是数字,这时候需要清空输入缓冲区,并提示重新输入。

代码的大致流程可能是:

  1. 初始化队列变量为未初始化状态(front和rear为NULL)。

  2. 显示菜单,用户选择操作。

  3. 根据用户选择调用对应的函数,每个函数首先检查队列是否已经初始化(比如,除了初始化操作外,其他操作需要队列已初始化)。

❗❗ 初始化队列时,如果用户多次调用初始化,是否会导致内存泄漏?

比如,如果队列已经初始化过,再次调用InitQueue会创建新的头节点,而原来的头节点没有被释放。

因此,在InitQueue函数中,应该先检查队列是否已经初始化,如果是,则先销毁原来的队列,或者提示用户先销毁。或者,在初始化函数中,如果队列已经初始化,就提示错误。或者,在InitQueue函数中,先释放之前的头节点,如果存在的话。这可能比较复杂。所以,在用户选择初始化队列时,如果队列已经初始化,应该提示用户需要先销毁队列。或者,在初始化函数内部处理这种情况

三、分段解释

1.节点结构体定义

QNode 定义

typedef struct QNode:定义了一个名为 QNode 的结构体类型,用于表示队列中的节点。

int data;:每个节点包含一个 int 类型的成员变量 data,用于存储队列中的数据元素。

struct QNode *next;:每个节点还包含一个指向 QNode 类型的指针 next,用于指向下一个节点,从而形成链表结构。

} QNode;:结束结构体定义,并将其命名为 QNode

2.队列结构体定义

LinkQueue 定义

typedef struct:定义了一个匿名结构体类型,并立即使用 typedef 为其创建一个别名 LinkQueue

QNode* front;LinkQueue 结构体包含一个指向 QNode 的指针 front,用于指向队列的头部节点(头节点)。

QNode* rear;LinkQueue 结构体还包含一个指向 QNode 的指针 rear,用于指向队列的尾部节点。

} LinkQueue;:结束结构体定义,并将其别名为 LinkQueue

3.判空逻辑

return q->front == q->rear;:这是函数的实现部分,它比较链队列的队头指针 front 和队尾指针 rear 是否相等。

在带头节点的链队列中,初始化时 frontrear 都指向头节点。当队列为空时,frontrear 仍然指向同一个节点(头节点),因此它们相等。

当队列不为空时,rear 指向最后一个实际数据节点,因此 frontrear 不相等。

4. 初始化链队列

a.检查队列是否已经初始化

检查 q->front 是否不为 NULL。如果不为 NULL,说明队列可能已经初始化过,或者包含一些未清理的节点。

b.清理现有节点

如果队列已经初始化过(即 q->front 不为 NULL),则需要清理现有的所有节点以防止内存泄漏。

使用一个临时指针 p 遍历链表,逐一释放每个节点的内存。

最后,将 q->frontq->rear 都设置为 NULL,表示队列现在为空。

c.创建头节点

使用 malloc 分配一个新节点的内存空间,这个节点将作为头节点。

d.检查内存分配是否成功

如果内存分配失败,输出错误信息并返回,不继续执行后续操作。

e.初始化头节点

将头节点的 next 指针设置为 NULL,表示初始时队列为空。

q->frontq->rear 都指向这个头节点。

最后输出成功信息

5. 入队操作

a.创建新节点

使用 malloc 分配一个新节点的内存空间。

b.检查内存分配是否成功

如果内存分配失败,输出错误信息并返回 false

c.初始化新节点

将新节点的数据域设置为传入的 data 值。

将新节点的指针域设置为 NULL

d.将新节点添加到队列尾部

更新当前队尾节点的指针域,使其指向新节点。

更新队列的队尾指针 rear,使其指向新节点。

最后返回成功状态。

6. 出队操作

a.函数定义

bool Dequeue(LinkQueue* q, int *data):定义了一个返回类型为 bool 的函数 Dequeue,它接受一个指向 LinkQueue 类型的指针 q 和一个指向整型的指针 data。这个函数用于从链队列 q 的头部移除一个元素,并将该元素的值存储到 data 所指向的内存位置。

b.检查队列是否为空

调用 IsEmpty 函数检查队列是否为空。如果队列为空,输出错误信息并返回 false

c.获取队头元素

队头元素是头节点的下一个节点,将这个节点存储在临时指针 temp 中。

头节点不是首元素!!!

d.保存数据

将队头元素的数据保存到 data 指针所指向的内存位置。

e.更新队头指针

更新头节点的指针域,使其指向队头元素的下一个节点,从而移除原来的队头元素。

比如旧有队列为 head 、1、2、3、4。经过出队操作后,此步骤即把2设为1出列后的队头元素。

f.处理队尾指针

如果队列中只有一个元素(即队尾指针指向当前要移除的队头元素),移除该元素后,需要将队尾指针重新指向头节点。

然后释放内存,并且返回成功状态。

7.销毁操作

a.检查队列是否为空

检查队列的队头指针 q->front 是否为 NULL。如果为 NULL,说明队列已经为空或未初始化,直接返回,无需执行后续操作。

b.遍历并释放节点内存

使用指针 p 从队头节点开始遍历链表。

在每次迭代中,将当前节点 p 存储到临时指针 temp 中。

p 移动到下一个节点 p->next

释放 temp 指针所指向的当前节点的内存。

c.将队列指针置为空

将队列的队头指针 front 和队尾指针 rear 都设置为 NULL,表示队列已经销毁。

最后输出销毁信息

8. 遍历操作

a.检查队列是否为空

调用 IsEmpty 函数检查队列是否为空。如果队列为空,输出提示信息并返回。

b.初始化遍历指针

将指针 p 初始化为头节点的下一个节点,即队列的第一个实际数据节点。

c.打印队列元素

输出队列元素的提示信息。

使用 while 循环遍历队列中的每个节点,直到 pNULL(即队列末尾)。

在每次迭代中,打印当前节点的数据,并将 p 移动到下一个节点。

9.查找操作

a.函数定义

int FindElement(LinkQueue* q, int value):定义了一个返回类型为 int 的函数 FindElement,它接受一个指向 LinkQueue 类型的指针 q 和一个整型参数 value。这个函数用于在链队列 q 中查找值为 value 的元素的位置。

b.检查队列为空是否

调用 IsEmpty 函数检查队列是否为空。如果队列为空,返回 -1 表示找不到元素。

c.初始化遍历指针和位置计数器

将指针 p 初始化为头节点的下一个节点,即队列的第一个实际数据节点。

初始化位置计数器 pos1,表示从第一个位置开始计数。

d.遍历队列查找元素

使用 while 循环遍历队列中的每个节点,直到 pNULL(即队列末尾)。

在每次迭代中,检查当前节点的数据是否等于要查找的 value

如果找到匹配的值,返回当前的位置计数器 pos

如果未找到,将 p 移动到下一个节点,并递增位置计数器 pos

10. 长度计算

a.初始化计数器和指针

初始化计数器 len0,用于记录队列中元素的数量。

将指针 p 初始化为头节点的下一个节点,即队列的第一个实际数据节点。

b.遍历队列并计数

使用 while 循环遍历队列中的每个节点,直到 pNULL(即队列末尾)。

在每次迭代中,递增计数器 len,并将 p 移动到下一个节点。

最后返回len,即队列中元素的数量。

11.主函数main()逻辑

主循环

  • while (1):创建一个无限循环,使程序持续运行直到用户选择退出。

  • menu(); :调用 menu 函数显示操作菜单(假设 menu 函数已定义)。

  • 输入处理

    • 使用 scanf_s 读取用户输入的选择。

    • 如果输入失败(即输入的不是有效整数),输出错误信息,并使用 while (getchar() != '\n'); 清空输入缓冲区,然后跳过本次循环重新显示菜单。

操作分支

根据用户的选择,执行不同的操作

0.退出程序

1.初始化程序

2.入队操作

3.出队操作

4.判断队列是否为空

5.显示队列元素

6.查找元素

7.获取队列长度

8.销毁队列

default:默认情况

12. 健壮性代码的方式

  • scanf_s 函数的返回值

    • scanf_s 函数的返回值是指成功读取和转换的输入项的数量。

    • 在尝试读取一个整数时,如果成功读取一个整数,返回值是 1

    • 如果输入失败(比如用户输入了非数字字符),返回值会是 0EOF(表示输入失败或遇到文件结束)。

  • 为什么要检查返回值

    • 如果用户输入的不是一个有效的整数(例如输入了字母 "abc"),scanf_s 会失败,并返回一个非 1 的值。

    • 如果不检查返回值,程序会继续执行,但此时变量 data 中的值可能是未定义的,导致后续操作出现意外行为,比如:

      • 使用未定义的值进行计算或操作。

      • 引发逻辑错误或程序崩溃。

  • 防止程序异常

    • 通过检查 scanf_s 的返回值,可以及时发现输入错误。

    • 如果输入无效,程序可以提示用户重新输入,而不是继续使用无效的数据进行操作。

    • 这种做法增强了程序的健壮性和用户体验,避免因输入错误导致程序异常终止。

  1. 输入缓冲区

    • 当用户通过键盘输入数据时,数据会先进入输入缓冲区。

    • 按下回车键后,输入的数据(包括回车键对应的换行符 \n)会被存入缓冲区。

  2. 清空缓冲区的必要性

    • 如果用户输入了错误的数据类型或格式,缓冲区中可能会残留未处理的字符。

    • 这些残留的字符可能会影响后续的输入操作,导致程序行为异常。

  3. 代码作用

    • getchar() 逐个读取输入缓冲区中的字符。

    • while (getchar() != '\n'); 会不断读取并丢弃字符,直到遇到换行符 \n 为止。

    • 这样可以确保输入缓冲区被清空,避免残留字符干扰后续输入。

以及使用入队、出队、查找等操作之前,都有相应的检查是否初始化的逻辑代码,已经在上述代码解析中给出。

四、完整代码

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

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


typedef struct
{
	QNode* front;
	QNode* rear;
}LinkQueue;



bool IsEmpty(LinkQueue* q)
{
	return q->front == q->rear;
}



void InitQueue(LinkQueue* q)
{
	if (q->front != NULL)
	{
		QNode* p = q->front;
		while (p)
		{
			QNode* temp = p;
			p = p->next;
			free(temp);
		}
		q->front = q->rear = NULL;
	}

	QNode* head = (QNode*)malloc(sizeof(QNode));
	if (!head)
	{
		printf("初始化失败,内存不足!\n");
		return;
	}
	head->next = NULL;
	q->front = head;
	q->rear = head;
	printf("队列初始化成功!\n");
}




bool EnQueue(LinkQueue* q, int data)
{
	QNode* newNode = (QNode*)malloc(sizeof(QNode));
	if (!newNode)
	{
		printf("内存分配失败,无法入队!\n");
		return false;
	}

	newNode->data = data;
	newNode->next = NULL;
	q->rear->next = newNode;
	q->rear = newNode;
	return true;
}



bool Dequeue(LinkQueue* q, int *data)
{
	if (IsEmpty(q))
	{
		printf("队列为空,无法出队!\n");
		return false;
	}

	QNode* temp = q->front->next;
	*data = temp->data;
	q->front->next = temp->next;
	if (q->rear == temp)
	{
		q->rear = q->front;
	}
	free(temp);
	return true;
}



void DestroyQueue(LinkQueue* q)
{
	if (q->front == NULL)
	{
		return;
	}
	QNode* p = q->front;
	while (p)
	{
		QNode* temp = p;
		p = p->next;
		free(temp);
	}
	q->front = q->rear = NULL;
	printf("队列已销毁!\n");

}




void DisplayQueue(LinkQueue* q)
{
	if (IsEmpty(q))
	{
		printf("队列为空!\n");
		return;
	}

	QNode* p = q->front->next;
	printf("队列元素:>");
	while (p)
	{
		printf("%d ", p->data);
		p = p->next;
	}
	printf("\n");

}




// 查找元素位置(返回1-based位置,找不到返回-1)
int FindElement(LinkQueue* q, int value)
{
	if (IsEmpty(q))
	{
		return -1;
	}
	QNode* p = q->front->next;
	int pos = 1;
	while (p)
	{
		if (p->data == value)
		{
			return pos;
		}
		p = p->next;
		pos++;
	}
	return -1;
}



int QueueLength(LinkQueue* q)
{
	int len = 0;
	QNode* p = q->front->next;
	while (p)
	{
		len++;
		p = p->next;
	}
	return len;
}




void menu()
{
	printf("===================\n");
	printf("链队列操作菜单:>\n");
	printf("1. 初始化队列\n");
	printf("2. 入队\n");
	printf("3. 出队\n");
	printf("4. 判空\n");
	printf("5. 显示队列\n");
	printf("6. 查找元素\n");
	printf("7. 计算队列长度\n");
	printf("8. 销毁队列\n");
	printf("0. 退出\n");
	printf("请输入选项:>");
}

int main()
{
	LinkQueue queue = { NULL,NULL };
	int choice = 0;
	int data = 0;

	while (1)
	{
		menu();
		if (scanf_s("%d", &choice) != 1)
		{
			printf("输入错误,请重新输入!\n");
			while (getchar() != '\n');
			continue;
		}

		switch (choice)
		{
		case 0:
			DestroyQueue(&queue);
			printf("程序已退出!\n");
			return 0;

		case 1:
			InitQueue(&queue);
			break;

		case 2:
		{
			if (queue.front == NULL)
			{
				printf("队列未初始化,请先初始化!\n");
				break;
			}
			printf("请输入要入队的整数:");
			if (scanf_s("%d", &data) != 1)
			{
				printf("输入错误,请重新输入!\n");
				while (getchar() != '\n');
				break;
			}
			if (EnQueue(&queue, data))
			{
				printf("元素 %d 入队成功!\n", data);
			}
			break;
		}

		case 3:
		{
			if (queue.front == NULL)
			{
				printf("队列未初始化,请先初始化!\n");
				break;
			}
			int dequeuedData = 0;
			if (Dequeue(&queue, &dequeuedData))
			{
				printf("出队元素:%d\n", dequeuedData);
			}
			break;
		}
		case 4:
		{
			if (queue.front == NULL)
			{
				printf("队列未初始化!\n");
			}
			else
			{
				printf("队列%s空\n", IsEmpty(&queue) ? "为" : "不");
			}
			break;
		}
			

		case 5:
		{
			if (queue.front == NULL)
			{
				printf("队列未初始化!\n");
			}
			else
			{
				DisplayQueue(&queue);
			}
			break;
		}

		case 6:
		{
			if (queue.front == NULL)
			{
				printf("队列未初始化!\n");
				break;
			}
			printf("请输入要查找的整数:");
			int value = 0;
			if (scanf_s("%d", &value) != 1)
			{
				printf("输入错误,请重新输入!\n");
				while (getchar() != '\n');
				break;
			}
			int pos = FindElement(&queue, value);
			if (pos != -1)
			{
				printf("元素 %d 存在于队列中,位置为第%d个!\n", value, pos);
			}
			else
			{
				printf("元素 %d 不存在于队列中!\n", value);
			}
			break;
		}

		case 7:
		{
			if (queue.front == NULL)
			{
				printf("队列未初始化!\n");
			}
			else
			{
				printf("队列长度:%d\n", QueueLength(&queue));
			}
			break;
		}
		case 8:
		{
			if (queue.front == NULL)
			{
				printf("队列未初始化!\n");
			}
			else {
				DestroyQueue(&queue);
			}
			break;
		}

		default:
			printf("无效选项,请重新输入!\n");
			break;
		}
	}
	return 0;
}

以上是基于VS2022编译器,用C语言编写的------用c语言实现------一个带头节点的链队列,支持用户输入交互界面、初始化、入队、出队、查找、判空判满、显示队列、遍历计算长度等功能,注意代码的健壮性,包括边界、是否为空指针、以及防止用户输入错误数据,未初始化就进行出列、入队操作等

❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀

参考课程:哔哩哔哩------王道考研数据结构相关视频。

相关推荐
武大打工仔15 分钟前
用 Java 复现哲学家就餐问题
算法
要做朋鱼燕16 分钟前
【数据结构】用堆解决TOPK问题
数据结构·算法
黑客影儿1 小时前
Java技术总监的成长之路(技术干货分享)
java·jvm·后端·程序人生·spring·tomcat·maven
秋难降1 小时前
LRU缓存算法(最近最少使用算法)——工业界缓存淘汰策略的 “默认选择”
数据结构·python·算法
XH华3 小时前
C语言第九章字符函数和字符串函数
c语言·开发语言
CoovallyAIHub3 小时前
线性复杂度破局!Swin Transformer 移位窗口颠覆高分辨率视觉建模
深度学习·算法·计算机视觉
点云SLAM3 小时前
Eigen中Dense 模块简要介绍和实战应用示例(最小二乘拟合直线、协方差矩阵计算和稀疏求解等)
线性代数·算法·机器学习·矩阵·机器人/slam·密集矩阵与向量·eigen库
Jayyih3 小时前
嵌入式系统学习Day19(数据结构)
数据结构·学习
renhongxia13 小时前
大模型微调RAG、LORA、强化学习
人工智能·深度学习·算法·语言模型