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

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

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

相关推荐
天天扭码16 分钟前
一分钟解决 | 高频面试算法题——滑动窗口最大值(单调队列)
前端·算法·面试
tan77º38 分钟前
【算法】BFS-解决FloodFill问题
算法·leetcode·宽度优先
知识烤冷面39 分钟前
【力扣刷题实战】找到字符串中所有字母异位词
数据结构·算法·leetcode
XiaoyaoCarter1 小时前
每日两道leetcode
c++·算法·leetcode·职场和发展·贪心算法
LIU_Skill1 小时前
SystemV-消息队列与责任链模式
linux·数据结构·c++·责任链模式
青瓦梦滋1 小时前
【算法】双指针8道速通(C++)
数据结构·算法·leetcode
程序员-King.1 小时前
day48—双指针-通过删除字母匹配到字典最长单词(LeetCode-524)
算法·leetcode·双指针
.格子衫.1 小时前
011数论——算法备赛
算法
Conan х1 小时前
进阶篇 第 6 篇:时间序列遇见机器学习与深度学习
人工智能·python·深度学习·算法·机器学习·数据分析
Y1nhl1 小时前
搜广推校招面经七十九
人工智能·算法·机器学习·推荐算法·搜索算法·lgb