单向循环链表(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)
-
存储开销 :每个节点需额外存储指针
-
循环陷阱 :未正确处理终止条件会导致死循环
-
缓存不友好 :节点内存不连续,访问速度低于数组。
-
边界处理 :需特殊处理头尾节点的指针更新。
-