单向循环链表(Circular Linked List)
一、基本概念
循环链表是一种特殊的链表,其末尾节点的后继指针指向头结点,形成一个闭环
循环链表的操作与普通链表基本一致,但需注意循环特性的处理。
二、代码实现
clList.h
/ #ifndef _CLLIST_H #define _CLLIST_H #include <stdio.h> #include <stdlib.h> #include <string.h> //定义节点数据的类型 typedef int DATA; //定义一个单向循环链表的节点 typedef struct node { DATA data; //节点数据域 struct node *next; //指针域,指向后继节点 }NODE; //函数原型的声明 /** * 创建链表 * @param head 待操作的链表 * @param data 待插入的数据 * @return 成功返回0,否则返回-1 */ extern int cllist_create(NODE **head, DATA data); /** * 在循环链表插入新节点(把新节点插入到头节点之前) * @param head 待操作的链表(默认是头节点) * @param data 待插入的数据 * @return 成功返回0,否则返回-1 */ extern int cllist_insert(NODE **head, DATA data); /** * 实现无头节点的头插法(插入在头节点之前) * @param head 待操作的链表(默认是头节点) * @param data 待插入的数据 * @return 成功返回0,否则返回-1 */ extern int cllist_insertAthead(NODE **head, DATA data); /** * 遍历链表数据 * @param head 待遍历的链表 * @return 成功返回0,否则返回-1 */ extern int cllist_showAll(const NODE *head); /** * 根据data返回查找对应的节点 * @param head 待操作的链表 * @param data 需要查找的节点数据 * @return 查找到的节点 */ extern NODE *cllist_find(const NODE *head, DATA data); /** * 根据newdata修改old对应的节点数据 * @param head 待操作的链表 * @param old 待修改的原数据 * @param newdata 待修改的新数据 * @return 成功返回0,否则返回-1 */ extern int cllist_update(const NODE *head, DATA old, DATA newdata); /** * 根据data删除对应的节点 * @param head 待操作的链表 * @param data 待删除的节点数据 * @return 成功返回0,否则返回-1 */ extern int cllist_delete(NODE **head, DATA data); /** * 销毁整个链表 * @param head 待销毁的链表 */ extern void cllist_destroy(NODE **head); #endif //_CLLIST_H
clList.c
#include "clList.h" //函数原型的声明 /** * 创建链表 * @param head 待操作的链表(head是指向头节点指针的地址) * @param data 待插入的数据 * @return 成功返回0,否则返回-1 */ int cllist_create(NODE **head, DATA data) { //如果链表存在,就无需创建 if(*head) { //打印错误信息到标准错误流(避免影响正常输出) fprintf(stderr, "链表已存在,无需创建!\n"); return -1; } //单向循环链表 //创建一个新节点 NODE *p = (NODE*)malloc(sizeof(NODE)); //校验新节点是否创建成功 if(!p) return -1; //初始化新节点 p->data = data; p->next = p; //循环特性:单个节点的next指向自身(即使单个节点也形成环) //设置头指针 *head = p; //将头指针设置为自己 } /** * 实现无头节点的头插法(插入在头节点之前) * @param head 待操作的链表(默认是头节点) * @param data 待插入的数据 * @return 成功返回0,否则返回-1 */ int cllist_insertAthead(NODE **head, DATA data) { //创建一个新节点 NODE *pNew = (NODE*)malloc(sizeof(NODE)); if(!pNew) { fprintf(stderr, "创建新节点失败!\n"); return -1; } //初始化新节点的数据域,指针域暂不赋值 pNew->data = data; NODE *p = *head; //创建一个遍历指针用来遍历链表寻找尾节点 //如果是空链表 if(!p) { pNew->next = pNew; *head = pNew; return 0; } //非空链表 pNew->next = *head; //新节点的next指向原头节点 //查找尾节点(原链表的最后一个节点) while(p->next != *head) //单向循环链表的尾节点next永远指向头节点(非NULL) { p = p->next; }//此时p是尾节点 p->next = pNew; *head = pNew; return 0; } /** * 在循环链表插入新节点(把新节点插入到头节点之前) * @param head 待操作的链表(默认是头节点) * @param data 待插入的数据 * @return 成功返回0,否则返回-1 */ int cllist_insert(NODE **head, DATA data) { //创建新节点 NODE *p = (NODE*)malloc(sizeof(NODE)); //校验节点是否创建成功 if(!p) return -1; //初始化新节点 p->data = data; p->next = p; //此时创建的新节点与链表还无关 //情景1:若待插入的链表是空链表 if(*head == NULL) { *head = p; //设置头指针,相当于创建了一个新链表 return 0; } //情景2:若待插入的链表是非空链表(在头节点和头节点的next之间插入,头节点不变) p->next = (*head)->next; //新节点指向原头节点的下一个节点 (*head)->next = p; //头节点指向新节点 } /** * 遍历链表数据 * @param head 待遍历的链表 * @return 成功返回0,否则返回-1 */ int cllist_showAll(const NODE *head) { //创建临时指针用于遍历(保持头指针不变) const NODE *p = head; //p指向当前遍历的节点 //空链表 if(!p) { fprintf(stderr, "空链表,没有数据!\n"); return -1; } //遍历列表 do //使用do—while保证至少执行依次(应对单节点循环链表) { printf("%d\t", p->data); p = p->next; }while(p != head); //循环终止条件:回到起始节点 printf("\n"); return 0; } /** * 根据data返回查找对应的节点 * @param head 待操作的链表 * @param data 需要查找的节点数据 * @return 查找到的节点 */ NODE *cllist_find(const NODE *head, DATA data) { //使用const指针避免意外修改节点 const NODE *p = head; //空链表 if(!head) return NULL; //遍历列表(确保至少执行一次) do { if(memcmp(&(p->data), &data, sizeof(DATA)) == 0) { return (NODE*)p; //强转,避免类型不匹配 } p = p->next; }while(p != head); return NULL; } /** * 根据newdata修改old对应的节点数据 * @param head 待操作的链表 * @param old 待修改的原数据 * @param newdata 待修改的新数据 * @return 成功返回0,否则返回-1 */ int cllist_update(const NODE *head, DATA old, DATA newdata) { NODE *p = cllist_find(head, old); if(!p) { fprintf(stderr, "数据未找到!\n"); return -1; } //找到旧数据的节点更新为新数据 p->data = newdata; return 0; } /** * 根据data删除对应的节点 * @param head 待操作的链表 * @param data 待删除的节点数据 * @return 成功返回0,否则返回-1 */ int cllist_delete(NODE **head, DATA data) { //尾随法(p为当前节点,q为前驱节点) NODE *p = *head, *q = NULL; //空链表 if(!*head) return -1; //遍历节点 while(p) { //找到要删除数据的位置 if(memcmp(&(p->data), &data, sizeof(DATA)) == 0) { //若要删除的节点是头节点 if(q == NULL) //指针还没有进行尾随,证明是头节点 { q = p->next; //获取头节点的下一个节点 //如果此头节点是唯一的头节点(p的next执行自身--头节点) if(p->next == *head) { //链表置空 *head = NULL; } else //链表中除了要删除的头节点还有其他节点 { //替换法(复制下一节点的数据并删除下一节点) p->data = q->data; //用头节点的后续节点数据替换头节点的数据,删除后续节点 p->next = q->next; free(q); } return 0; } //如果要删除的节点是非头节点 q->next = p->next; //前驱节点跳过当前节点 free(p); //释放当前节点 return 0; } q = p; p = p->next; //因为是循环链表,所以要想办法终结循环链表 if(p == *head) break; } return -1; } /** * 销毁整个单向循环链表 * @param head 待销毁的链表 */ void cllist_destroy(NODE **head) { //空链表 if(!*head) return; //在链表中找节点就用指针尾随法 NODE *p = *head, *q = NULL; //p指向当前节点,q用于保存待释放节点 while(p) { q = p; p = p->next; free(q); //解除循环链表 if(p == *head) break; } *head = NULL; //将头节点置空,不然会产生野指针 }
app.c
#include "clList.h" int main(int argc,char *argv[]) { NODE *head = NULL; //测试创建和插入 cllist_create(&head, 111); //创建头节点 cllist_insert(&head, 222); cllist_insert(&head, 333); cllist_insert(&head, 444); cllist_showAll(head); //111 444 333 222 cllist_insertAthead(&head, 222); cllist_insertAthead(&head, 333); cllist_insertAthead(&head, 444); cllist_showAll(head); //444 333 222 111 444 333 222 //测试更新 cllist_update(head, 333, 3333); cllist_showAll(head); //444 3333 222 111 444 333 222 //测试删除 cllist_delete(&head, 444); cllist_showAll(head); //3333 222 111 444 333 222 //销毁链表 cllist_destroy(&head); cllist_showAll(head); //空链表,没有数据 return 0; }
三、优缺点总结
-
优点
-
动态内存:无需预分配固定大小,适合数据量不确定的场景。
-
高效操作
-
头插/头删:O(1)时间复杂度
-
尾插:O(1)(维护尾指针时)或O(n)
-
中间插入:O(1)(定位后)
-
-
循环特性:适合周期性访问场景(如:轮询调度、循环缓冲区)
-
内存效率:按需分配,无扩容浪费
-
-
缺点:
-
随机访问 :必须遍历,时间复杂度O(n)
-
存储开销 :每个节点需额外存储指针
-
循环陷阱 :未正确处理终止条件会导致死循环
-
缓存不友好 :节点内存不连续,访问速度低于数组。
-
边界处理 :需特殊处理头尾节点的指针更新。
-