学习笔记——循环链表与双向链表

循环链表与双向链表

一、循环链表(Circular Linked List)

1. 基本概念

循环链表是一种特殊的链表,它将单链表最后一个节点的next指针指向头节点 或第一个元素,使链表形成一个闭环。

2. 结构特点

复制代码
typedef struct Node {
    ElemType data;          // 数据域
    struct Node *next;      // 指针域
} Node, *LinkList;

3. 关键特性对比

特性 单链表 循环链表
尾节点指针 指向NULL 指向头节点/首节点
遍历终止条件 p != NULL 或 p->next != NULL p->next != Head
空表判断 head == NULL 或 head->next == NULL head->next == head(带头节点)
结构形态 线性结构 环形结构

4. 常见形式

  • 带头节点的循环链表(更常用):

    • 空表:head->next == head

    • 非空表:尾节点指向头节点

  • 不带头节点的循环链表:

    • 空表:head == NULL

    • 非空表:尾节点指向首节点

5. 循环链表操作示例

复制代码
// 初始化循环链表(带头节点)
Status InitList(LinkList *L) {
    *L = (LinkList)malloc(sizeof(Node));
    if (*L == NULL) return ERROR;
    (*L)->next = *L;  // 指向自身,形成环
    return OK;
}

// 判断链表是否为空(带头节点)
int ListEmpty(LinkList L) {
    return (L->next == L);
}

// 遍历循环链表
void TraverseList(LinkList L) {
    if (L->next == L) {
        printf("链表为空\n");
        return;
    }
    
    LinkList p = L->next;  // 从第一个节点开始
    while (p != L) {       // 回到头节点时结束
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

// 在循环链表中查找元素
Node* LocateElem(LinkList L, ElemType e) {
    if (L->next == L) return NULL;
    
    LinkList p = L->next;
    while (p != L) {
        if (p->data == e) return p;
        p = p->next;
    }
    return NULL;
}

6. 约瑟夫环问题(循环链表典型应用)

复制代码
// 约瑟夫环问题求解
void Josephus(int n, int m) {
    // 创建循环链表
    Node *head = (Node*)malloc(sizeof(Node));
    Node *prev = head;
    
    // 构建循环链表
    for (int i = 1; i <= n; i++) {
        Node *newNode = (Node*)malloc(sizeof(Node));
        newNode->data = i;
        prev->next = newNode;
        prev = newNode;
    }
    prev->next = head->next;  // 形成环
    
    // 开始约瑟夫环计数
    Node *current = head->next;
    Node *pre = prev;  // current的前驱
    
    printf("出列顺序:");
    while (current->next != current) {
        // 报数m-1次
        for (int count = 1; count < m; count++) {
            pre = current;
            current = current->next;
        }
        
        // 删除当前节点
        printf("%d ", current->data);
        pre->next = current->next;
        free(current);
        current = pre->next;
    }
    
    printf("%d\n", current->data);
    free(current);
    free(head);
}

二、双向链表(Double Linked List)

1. 基本结构

复制代码
typedef struct DulNode {
    ElemType data;           // 数据域
    struct DulNode *prior;   // 前驱指针
    struct DulNode *next;    // 后继指针
} DulNode, *DuLinkList;

2. 结构示意图

复制代码
         prior    data    next
NULL  ←  [Node]  →  [Node]  →  [Node]  → NULL
          ↑         ↑         ↑
         next      prior     prior

3. 双向链表操作

初始化
复制代码
// 初始化双向链表(带头节点)
Status InitDuList(DuLinkList *L) {
    *L = (DuLinkList)malloc(sizeof(DulNode));
    if (*L == NULL) return ERROR;
    
    (*L)->prior = NULL;
    (*L)->next = NULL;
    return OK;
}
插入操作
复制代码
// 在节点p之后插入新节点s
Status InsertAfterNode(DulNode *p, DulNode *s) {
    if (p == NULL || s == NULL) return ERROR;
    
    s->prior = p;
    s->next = p->next;
    
    if (p->next != NULL) {
        p->next->prior = s;
    }
    
    p->next = s;
    return OK;
}

// 在节点p之前插入新节点s
Status InsertBeforeNode(DulNode *p, DulNode *s) {
    if (p == NULL || s == NULL) return ERROR;
    
    s->next = p;
    s->prior = p->prior;
    
    if (p->prior != NULL) {
        p->prior->next = s;
    }
    
    p->prior = s;
    return OK;
}
删除操作
复制代码
// 删除节点p
Status DeleteNode(DulNode *p) {
    if (p == NULL) return ERROR;
    
    if (p->prior != NULL) {
        p->prior->next = p->next;
    }
    
    if (p->next != NULL) {
        p->next->prior = p->prior;
    }
    
    free(p);
    return OK;
}
遍历操作
复制代码
// 前向遍历(从尾到头)
void TraverseBackward(DuLinkList L) {
    if (L == NULL || L->next == NULL) {
        printf("链表为空\n");
        return;
    }
    
    // 找到尾节点
    DulNode *p = L->next;
    while (p->next != NULL) {
        p = p->next;
    }
    
    // 从后向前遍历
    while (p != L) {
        printf("%d ", p->data);
        p = p->prior;
    }
    printf("\n");
}

// 后向遍历(从头到尾)
void TraverseForward(DuLinkList L) {
    if (L == NULL || L->next == NULL) {
        printf("链表为空\n");
        return;
    }
    
    DulNode *p = L->next;
    while (p != NULL) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

4. 双向循环链表

双向链表也可以形成循环结构:

复制代码
// 初始化双向循环链表
Status InitDuCircularList(DuLinkList *L) {
    *L = (DuLinkList)malloc(sizeof(DulNode));
    if (*L == NULL) return ERROR;
    
    (*L)->prior = *L;  // 前驱指向自身
    (*L)->next = *L;   // 后继指向自身
    return OK;
}

// 判断双向循环链表是否为空
int DuCircularListEmpty(DuLinkList L) {
    return (L->next == L && L->prior == L);
}

三、对比总结

特性 单链表 循环链表 双向链表 双向循环链表
指针数量 1个(next) 1个(next) 2个(prior, next) 2个(prior, next)
空间复杂度 O(n) O(n) O(2n) O(2n)
遍历方向 单向 单向循环 双向 双向循环
查找效率 O(n) O(n) O(n)但可双向查找 O(n)但可双向查找
插入/删除 需知道前驱 需知道前驱 可直接操作,无需前驱 可直接操作,无需前驱
典型应用 一般线性存储 约瑟夫环、轮询调度 浏览器历史记录、文本编辑器 循环缓冲区、高级调度

四、实际应用场景

循环链表的应用:

  1. 操作系统:时间片轮转调度算法

  2. 游戏开发:玩家轮流回合

  3. 多媒体:循环播放列表

  4. 计算机网络:令牌环网络

双向链表的应用:

  1. 文本编辑器:撤销/重做功能

  2. 浏览器:前进/后退历史记录

  3. LRU缓存:最近最少使用算法

  4. 音乐播放器:上一曲/下一曲功能

五、编程注意事项

  1. 循环链表的终止条件:避免无限循环,确保有明确的结束条件

  2. 双向链表的指针更新:插入和删除时要同时更新prior和next指针

  3. 内存管理:及时释放删除的节点,防止内存泄漏

  4. 边界处理:特别注意头节点和尾节点的特殊情况

  5. 空表判断:不同实现方式(带头节点/不带头节点)的判断条件不同

相关推荐
机器人零零壹9 小时前
技术白皮书:iRobotCAM如何解决机器人激光切割应用中的核心难点,从CAD离线编程到双目视觉定位与轨迹自适应提升复杂空间曲线切割的精度、效率与柔性化水平
人工智能·算法
ZhengEnCi12 小时前
大模型的分类到底怎么分:从基础模型到推理模型、从通用到专用的完整图谱
算法
前进的李工13 小时前
Agent 学习笔记 05:系统提示词应该怎么写
笔记·学习·prompt·agent·个人开发·few-shot·ai应用开发
取名真是13 小时前
Python 算法知识点从基础到进阶
开发语言·python·算法
罗西的思考13 小时前
【轻量化复刻大模型 / KIMI K3】KDA 模块交互式演示
人工智能·算法·机器学习
正在走向自律14 小时前
【3/5】人工智能训练师 三级/高级工 考证学习方向:从“独立执行”到“设计、归因与全链条负责”
人工智能·学习·人工智能训练师·注方案设计·数据质控负责人·模型评测与调优辅助·ai训练项目交付
蒸蒸yyyyzwd15 小时前
后端学习笔记三个知识
笔记·八股
nm_zfyjt15 小时前
信息流广告投放优化实践:线索成本评估模型与转化链路重构(家装建材案例)
算法·重构·内蒙古朋友圈广告服务商·内蒙古朋友圈获客
CCIE-Yasuo16 小时前
网络安全-学习数据库-Mysql-2
数据库·学习·mysql
La Pulga16 小时前
【FreeRTOS源码学习笔记】任务切换
单片机·mcu·学习·嵌入式·freertos·实时操作系统