数据结构(三)-单向循环链表

单向循环链表(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;
}
​

三、优缺点总结

  • 优点

    1. 动态内存:无需预分配固定大小,适合数据量不确定的场景。

    2. 高效操作

      • 头插/头删:O(1)时间复杂度

      • 尾插:O(1)(维护尾指针时)或O(n)

      • 中间插入:O(1)(定位后)

    3. 循环特性:适合周期性访问场景(如:轮询调度、循环缓冲区)

    4. 内存效率:按需分配,无扩容浪费

  • 缺点:

    1. 随机访问 :必须遍历,时间复杂度O(n)

    2. 存储开销 :每个节点需额外存储指针

    3. 循环陷阱 :未正确处理终止条件会导致死循环

    4. 缓存不友好 :节点内存不连续,访问速度低于数组。

    5. 边界处理 :需特殊处理头尾节点的指针更新。

相关推荐
半青年8 小时前
数据结构之哈希表的原理和应用:从理论到实践的全面解析
java·c语言·数据结构·c++·python·哈希算法
不是仙人的闲人10 小时前
算法之回溯法
开发语言·数据结构·c++·算法
小德乐乐10 小时前
计算机软考中级 知识点记忆——排序算法 冒泡排序-插入排序- 归并排序等 各种排序算法知识点整理
数据结构·算法·排序算法
小豪GO!12 小时前
数据结构-八大排序
数据结构·算法·排序算法
大龄门外汉12 小时前
数据结构之二叉树
c语言·数据结构·笔记·改行学it
坚持就完事了13 小时前
python链表
开发语言·python·链表
好易学数据结构13 小时前
可视化图解算法:按之字形顺序打印二叉树( Z字形、锯齿形遍历)
数据结构·算法·leetcode·面试·二叉树·力扣·笔试·遍历·二叉树遍历·牛客网·层序遍历·z·z字形遍历·锯齿形遍历
时光の尘14 小时前
FreeRTOS菜鸟入门(六)·移植FreeRTOS到STM32
c语言·数据结构·stm32·单片机·嵌入式硬件·嵌入式
大学生亨亨14 小时前
蓝桥杯之递归二
java·数据结构·笔记·算法