循环链表
简介
循环链表是链表的另一种重要变式,其尾节点的next指针不再指向NULL,而是重新指向头结点,形成一个环状结构。这种设计在某些场景下具有独特优势,例如需要循环遍历数据、实现约瑟夫环问题或构建环形缓冲区。
具体实现
头文件与结构体声明
循环链表是链表的一种变体,其核心特点是尾节点的next指针不再指向NULL,而是重新指向头结点,从而形成一个闭环。这种结构特别适合需要循环遍历的场景,比如操作系统的进程调度、约瑟夫环问题等。实现时,空链表的标志是头结点的next指向自身,插入和删除操作需要维护好节点间的链接关系,同时注意索引边界为1到size+1。遍历的终止条件是回到头结点而非NULL。循环链表的优势在于可以从任意节点开始遍历整个链表。
typedef int E; // 定义元素类型别名,便于统一修改数据类型
typedef struct LoopLinkedNode { // 定义循环链表节点结构体
E element; // 存储节点的数据元素
struct LoopLinkedNode *next; // 指向下一个节点的指针
} LoopLinkedList; // 为结构体取别名
typedef LoopLinkedList *LPLNode; // 定义节点指针类型别名,表示指向循环链表节点的指针
void InitLoopList(LPLNode Head); // 初始化循环链表
_Bool InsertLoopList(LPLNode Head, E element, int index); // 在指定位置插入元素
_Bool DeleteLoopList(LPLNode Head, int index); // 删除指定位置的元素
void PrintLoopList(LPLNode Head); // 打印整个循环链表
功能函数实现
初始化函数
初始化函数负责将头结点的next指针置为NULL,标志链表为空。函数首先进行防御性检查,如果传入的Head指针为NULL则报错返回。初始化完成后输出头结点地址便于调试。由于头结点本身不存储数据,只需要设置next指针即可。该函数没有返回值,因为头结点由调用者分配内存,函数只负责建立正确的链表结构。
void InitLinkList(Node Head){
if(Head == NULL){ // 检查传入的头指针是否为空
printf("[ERROR]:undefined LinkList\n "); // 输出错误信息
return; // 直接返回,不进行初始化
}
Head->next = NULL; // 将头结点的next指针设置为NULL,表示空链表
printf("[LOG]:This new LinkList 's Adress is %p\n",Head); // 打印头结点地址供调试
}
插入函数
插入函数首先进行参数校验,确保头指针有效且索引合法。然后通过循环移动指针定位到待插入位置的前驱节点,循环条件是--index,因此当index=1时不进入循环,此时前驱就是头结点。定位完成后,创建新节点并为其赋值。最关键的是指针更新步骤:先将新节点的next指向当前前驱节点的后继,再将前驱节点的next指向新节点,这样新节点就被成功链接到链表中。最后打印调试信息并返回成功标志。
_Bool insertLinkList(Node Head,E element,int index){
if(Head == NULL){ // 检查链表头指针是否有效
printf("[LOG]: undefined LinkList\n"); // 输出未定义错误
return 0; // 返回0表示失败
}
if(index < 1){ // 检查索引下界,位置从1开始
printf("[ERROR]:Wrong index\n"); // 输出索引错误
return 0; // 返回失败
}
while(--index){ // 循环找到插入位置的前驱节点
if(Head->next == NULL) return 0; // 如果提前到达末尾,索引无效
Head = Head->next; // 指针后移
}
Node node = malloc(sizeof(struct LinkNode)); // 为新节点分配内存
node->element = element; // 将数据存入新节点
node->next = Head->next; // 新节点的next指向原前驱的后继
Head->next = node; // 前驱节点的next指向新节点
printf("[LOG]:insert success\n[INFO]:Front Node address = %p,Target Node address = %p\n",Head,node);
return 1; // 返回1表示插入成功
}
删除函数
删除函数首先验证参数合法性。然后通过循环找到待删除节点的前驱节点,循环逻辑与插入类似。找到前驱后,用DeleteNode指针保存待删除节点的地址,然后执行核心的删除操作:将前驱节点的next指针直接指向待删除节点的后继节点,这就从逻辑上跳过了待删除节点。完成链表的指针调整后,需要调用free释放被删除节点的内存,防止内存泄漏。最后打印被删除节点的信息并返回成功标志。
_Bool deleteLinkList(Node Head,int index){
if(Head == NULL){ // 检查链表是否存在
printf("[ERROR]:undefined LinkList\n"); // 输出错误信息
return 0; // 返回失败
}
if(index < 1){ // 检查索引合法性
printf("[ERROR]:Wrong index\n"); // 输出索引错误
return 0; // 返回失败
}
Node DeleteNode; // 声明待删除节点的指针变量
while(--index){ // 循环找到待删除节点的前驱节点
if(Head->next == NULL){ // 如果链表提前结束
printf("[ERROR]:Wrong index\n"); // 输出索引超出范围
return 0; // 返回失败
}
Head = Head->next; // 指针后移
}
DeleteNode = Head->next; // 保存待删除节点的地址
Head->next = Head->next->next; // 前驱节点的next跳过待删除节点,指向其后继
printf("[LOG]:Delete success,Node Adress = %p,Node element = %d\n"
,DeleteNode,DeleteNode->element); // 打印被删除节点信息
free(DeleteNode); // 释放待删除节点的内存
return 1; // 返回成功
}
打印函数
打印函数用于遍历整个链表并输出每个节点的信息。首先进行空指针检查,确保链表头存在。然后进入循环,循环条件是当前节点的next指针不为空,即还有后继节点。每次循环先将head指针移动到下一个节点,然后打印该节点的element值和next指针地址。这种遍历方式会从第一个数据节点开始,逐个输出直到最后一个节点。由于最后一个节点的next为NULL,循环会在打印完最后一个节点后退出,因此输出格式中会在最后一个节点后留下一个“->”符号,打印完成后函数返回。
int GetLinkListSize(Node Head){
if(Head == NULL){ // 检查链表是否存在
printf("[ERROR]:undefined LinkList\n"); // 输出错误信息
return 0; // 返回0表示空链表
}
int Size = 0; // 初始化计数器
while(Head->next){ // 只要还有后继节点就继续计数
Head = Head->next; // 移动到下一个节点
Size++; // 计数器加1
}
return Size; // 返回链表中节点的个数
}
获取链表长度
该函数用于统计链表中实际存储数据的节点个数(头结点不计入)。首先进行空指针检查,如果头指针为空则返回0。然后定义Size变量作为计数器,初始值为0。进入循环,循环条件是当前节点的next指针不为空,因为头结点不存储数据,所以从第一个数据节点开始计数。每次循环移动指针到下一个节点并将Size加1,直到到达尾节点。循环结束后返回Size,即为链表中节点的总数。该函数不会修改链表本身,只进行只读的遍历操作。
int GetLinkListSize(Node Head){
if(Head == NULL){ // 检查链表是否存在
printf("[ERROR]:undefined LinkList\n"); // 输出错误信息
return 0; // 返回0表示空链表
}
int Size = 0; // 初始化计数器
while(Head->next){ // 只要还有后继节点就继续计数
Head = Head->next; // 移动到下一个节点
Size++; // 计数器加1
}
return Size; // 返回链表中节点的个数
}
按索引获取元素
该函数根据位置索引返回对应节点的元素值。首先检查链表是否存在,然后通过调用GetLinkListSize函数获取链表的总节点数,并判断传入的索引是否在1到size之间。索引验证通过后,进入while循环,循环条件为index--,即循环index次。每次循环将Head指针向后移动一个节点,初始时Head指向头结点,循环一次后指向第一个数据节点,循环两次后指向第二个数据节点,以此类推。循环结束后,Head正好指向第index个数据节点,直接返回该节点的element值即可。这种实现方式简洁高效,时间复杂度为O(n)。
Node GetPointByElement(Node Head,E element){
if(Head == NULL){ // 检查链表是否存在
printf("[ERROR]:undefined LinkList\n"); // 输出错误信息
return NULL; // 返回NULL表示查找失败
}
while(Head->next != NULL){ // 遍历链表直到末尾
Head = Head->next; // 移动到下一个节点
if(Head->element == element){ // 比较当前节点的元素是否与目标相等
printf("[LOG]:Find success\n[INFO]: Node Address = %p,Node element = %d\n",Head,Head->element);
return Head; // 找到目标,返回节点地址
}
}
printf("[LOG]:this element not in the LinkList\n"); // 遍历完未找到
return NULL; // 返回NULL表示查找失败
}
按元素查找节点
该函数根据给定的元素值查找第一个匹配的节点,并返回其指针。首先检查链表是否存在,如果头指针为空则直接返回NULL。然后进入while循环,循环条件是当前节点的next不为空,这意味着会遍历到最后一个数据节点。每次循环先将Head移动到下一个节点,然后检查该节点的element是否与目标元素相等。如果找到匹配的节点,打印调试信息并立即返回该节点的地址。如果循环结束后仍未找到匹配的元素,则打印未找到的信息并返回NULL。这种查找方式返回的是节点的指针,调用者可以直接通过指针访问节点甚至修改节点的值,为后续操作提供了便利。
Node GetPointByElement(Node Head,E element){
if(Head == NULL){ // 检查链表是否存在
printf("[ERROR]:undefined LinkList\n"); // 输出错误信息
return NULL; // 返回NULL表示查找失败
}
while(Head->next != NULL){ // 遍历链表直到末尾
Head = Head->next; // 移动到下一个节点
if(Head->element == element){ // 比较当前节点的元素是否与目标相等
printf("[LOG]:Find success\n[INFO]: Node Address = %p,Node element = %d\n",Head,Head->element);
return Head; // 找到目标,返回节点地址
}
}
printf("[LOG]:this element not in the LinkList\n"); // 遍历完未找到
return NULL; // 返回NULL表示查找失败
}
测试
测试用例
该测试代码共包含12个测试模块,全面覆盖了循环链表的各项功能:
- 初始化测试验证链表创建和初始状态是否正确
- 边界插入测试测试头部、中间、尾部插入以及无效索引的处理
- 边界删除测试测试删除头部、中间、尾部节点及无效索引处理
- 空链表操作测试验证空链表的特殊行为
- 单节点链表测试测试只有一个节点时的各种操作
- 大量插入压力测试测试100次头部插入和50次尾部插入的性能
- 大量删除压力测试测试头部删除50次和尾部删除30次
- 混合操作压力测试100次插入删除交替操作
- 极端边界测试测试负数索引和超大索引
- 循环特性验证遍历多圈验证循环链表特性
- 内存压力测试10轮创建销毁,验证无内存泄漏
- 最终状态输出汇总所有链表状态
`#include <stdio.h> #include <stdlib.h> #include "LoopLinkedList.h"
int main(){ printf("========== 循环链表全面测试 ==========\n\n");
// ========== 1. 初始化测试 ==========
printf("【测试1】初始化测试\n");
LPLNode head = (LPLNode)malloc(sizeof(LoopLinkedList));
InitLoopList(head);
PrintLoopList(head);
// ========== 2. 边界插入测试 ==========
printf("\n【测试2】边界插入测试\n");
printf("\n2.1 插入第一个元素(位置1):\n");
InsertLoopList(head, 100, 1);
PrintLoopList(head);
printf("\n2.2 插入到尾部(位置2):\n");
InsertLoopList(head, 200, 2);
PrintLoopList(head);
printf("\n2.3 插入到头部(位置1):\n");
InsertLoopList(head, 50, 1);
PrintLoopList(head);
printf("\n2.4 插入到中间(位置3):\n");
InsertLoopList(head, 150, 3);
PrintLoopList(head);
printf("\n2.5 测试无效位置0:\n");
InsertLoopList(head, 999, 0);
printf("\n2.6 测试超出范围位置10:\n");
InsertLoopList(head, 999, 10);
// ========== 3. 边界删除测试 ==========
printf("\n【测试3】边界删除测试\n");
printf("\n3.1 删除头部(位置1):\n");
DeleteLoopList(head, 1);
PrintLoopList(head);
printf("\n3.2 删除尾部(最后位置):\n");
DeleteLoopList(head, 3);
PrintLoopList(head);
printf("\n3.3 删除中间(位置2):\n");
DeleteLoopList(head, 2);
PrintLoopList(head);
printf("\n3.4 测试删除无效位置0:\n");
DeleteLoopList(head, 0);
printf("\n3.5 测试删除超出范围:\n");
DeleteLoopList(head, 10);
// ========== 4. 空链表操作测试 ==========
printf("\n【测试4】空链表操作测试\n");
LPLNode emptyHead = (LPLNode)malloc(sizeof(LoopLinkedList));
InitLoopList(emptyHead);
printf("\n4.1 空链表状态:\n");
PrintLoopList(emptyHead);
printf("\n4.2 向空链表插入元素10:\n");
InsertLoopList(emptyHead, 10, 1);
PrintLoopList(emptyHead);
printf("\n4.3 向空链表插入元素20(位置2):\n");
InsertLoopList(emptyHead, 20, 2);
PrintLoopList(emptyHead);
printf("\n4.4 从空链表删除(应失败):\n");
DeleteLoopList(emptyHead, 1);
// ========== 5. 单节点链表测试 ==========
printf("\n【测试5】单节点链表测试\n");
LPLNode singleHead = (LPLNode)malloc(sizeof(LoopLinkedList));
InitLoopList(singleHead);
InsertLoopList(singleHead, 777, 1);
printf("\n5.1 单节点链表:\n");
PrintLoopList(singleHead);
printf("\n5.2 在单节点后插入(位置2):\n");
InsertLoopList(singleHead, 888, 2);
PrintLoopList(singleHead);
printf("\n5.3 在单节点前插入(位置1):\n");
InsertLoopList(singleHead, 666, 1);
PrintLoopList(singleHead);
printf("\n5.4 删除中间节点(位置2):\n");
DeleteLoopList(singleHead, 2);
PrintLoopList(singleHead);
printf("\n5.5 删除头部(位置1):\n");
DeleteLoopList(singleHead, 1);
PrintLoopList(singleHead);
printf("\n5.6 删除唯一节点(位置1):\n");
DeleteLoopList(singleHead, 1);
PrintLoopList(singleHead);
// ========== 6. 大量插入压力测试 ==========
printf("\n【测试6】大量插入压力测试\n");
LPLNode stressHead = (LPLNode)malloc(sizeof(LoopLinkedList));
InitLoopList(stressHead);
printf("\n6.1 插入100个元素(位置1,头部插入):\n");
for (int i = 1; i <= 100; i++) {
InsertLoopList(stressHead, i, 1);
if (i % 20 == 0) {
printf(" 已插入 %d 个元素\n", i);
}
}
printf(" 最终链表大小(预期100): ");
PrintLoopList(stressHead);
printf("\n6.2 插入50个元素(尾部插入):\n");
for (int i = 101; i <= 150; i++) {
InsertLoopList(stressHead, i, i);
if (i % 20 == 0) {
printf(" 已插入 %d 个元素\n", i);
}
}
printf(" 最终链表大小(预期150): ");
PrintLoopList(stressHead);
// ========== 7. 大量删除压力测试 ==========
printf("\n【测试7】大量删除压力测试\n");
printf("\n7.1 删除50个元素(从头删除):\n");
for (int i = 1; i <= 50; i++) {
DeleteLoopList(stressHead, 1);
if (i % 10 == 0) {
printf(" 已删除 %d 个元素\n", i);
}
}
printf(" 剩余大小(预期100): ");
PrintLoopList(stressHead);
printf("\n7.2 删除30个元素(从尾删除):\n");
int size = 0;
LPLNode p = stressHead->next;
while (p != stressHead) { size++; p = p->next; }
for (int i = 1; i <= 30; i++) {
DeleteLoopList(stressHead, size);
size--;
if (i % 10 == 0) {
printf(" 已删除 %d 个元素\n", i);
}
}
printf(" 剩余大小(预期70): ");
PrintLoopList(stressHead);
// ========== 8. 混合操作压力测试 ==========
printf("\n【测试8】混合操作压力测试\n");
LPLNode mixHead = (LPLNode)malloc(sizeof(LoopLinkedList));
InitLoopList(mixHead);
printf("\n8.1 交替插入删除(100次操作):\n");
for (int i = 1; i <= 100; i++) {
if (i % 3 == 0) {
// 每3次操作删除一次
DeleteLoopList(mixHead, 1);
} else {
// 其他次数插入
InsertLoopList(mixHead, i, 1);
}
if (i % 20 == 0) {
printf(" 已完成 %d 次操作\n", i);
}
}
printf(" 最终结果: ");
PrintLoopList(mixHead);
// ========== 9. 极端边界测试 ==========
printf("\n【测试9】极端边界测试\n");
LPLNode extremeHead = (LPLNode)malloc(sizeof(LoopLinkedList));
InitLoopList(extremeHead);
printf("\n9.1 测试负数索引:\n");
InsertLoopList(extremeHead, 10, -1);
DeleteLoopList(extremeHead, -5);
printf("\n9.2 测试超大索引(1000000):\n");
InsertLoopList(extremeHead, 10, 1000000);
DeleteLoopList(extremeHead, 1000000);
printf("\n9.3 连续插入后删除所有节点:\n");
for (int i = 1; i <= 20; i++) {
InsertLoopList(extremeHead, i, i);
}
printf(" 插入20个节点后: ");
PrintLoopList(extremeHead);
for (int i = 1; i <= 20; i++) {
DeleteLoopList(extremeHead, 1);
}
printf(" 删除所有节点后: ");
PrintLoopList(extremeHead);
// ========== 10. 循环特性验证 ==========
printf("\n【测试10】循环特性验证\n");
LPLNode circleHead = (LPLNode)malloc(sizeof(LoopLinkedList));
InitLoopList(circleHead);
printf("\n10.1 创建链表: ");
InsertLoopList(circleHead, 10, 1);
InsertLoopList(circleHead, 20, 2);
InsertLoopList(circleHead, 30, 3);
InsertLoopList(circleHead, 40, 4);
PrintLoopList(circleHead);
printf("\n10.2 验证循环(遍历2圈):\n ");
LPLNode current = circleHead->next;
int count = 0;
while (current != circleHead && count < 10) {
printf("[%d]", current->element);
current = current->next;
if (current != circleHead) printf(" -> ");
count++;
}
printf(" -> (回到头结点,循环正常)\n");
// ========== 11. 内存压力测试 ==========
printf("\n【测试11】内存压力测试(频繁创建销毁)\n");
for (int i = 1; i <= 10; i++) {
LPLNode tempHead = (LPLNode)malloc(sizeof(LoopLinkedList));
InitLoopList(tempHead);
for (int j = 1; j <= 50; j++) {
InsertLoopList(tempHead, j, j);
}
for (int j = 1; j <= 50; j++) {
DeleteLoopList(tempHead, 1);
}
free(tempHead);
if (i % 5 == 0) {
printf(" 已完成 %d 轮内存测试\n", i);
}
}
printf(" 内存测试完成,无内存泄漏\n");
// ========== 12. 最终状态输出 ==========
printf("\n【测试12】最终状态\n");
printf("链表1(基础测试): ");
PrintLoopList(head);
printf("链表2(空链表): ");
PrintLoopList(emptyHead);
printf("链表3(压力测试): ");
PrintLoopList(stressHead);
printf("链表4(混合操作): ");
PrintLoopList(mixHead);
printf("链表5(极端测试): ");
PrintLoopList(extremeHead);
printf("链表6(循环验证): ");
PrintLoopList(circleHead);
// ========== 释放内存 ==========
printf("\n【清理】释放所有链表内存\n");
free(head);
free(emptyHead);
free(stressHead);
free(mixHead);
free(extremeHead);
free(circleHead);
printf("内存释放完成\n");
printf("\n========== 所有测试完成 ==========\n");
return 0;
}`