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

循环链表与双向链表

一、循环链表(Circular Linked List)

1. 基本概念

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

2. 结构特点

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

3. 关键特性对比

特性 单链表 循环链表
尾节点指针 指向NULL 指向头节点/首节点
遍历终止条件 p != NULLp->next != NULL p->next != Head
空表判断 head == NULLhead->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. 空表判断:不同实现方式(带头节点/不带头节点)的判断条件不同

相关推荐
AI_56782 小时前
用Everything+Total Commander管理电脑文件
人工智能·学习
秦奈2 小时前
Unity复习学习随笔(11):二进制存储
学习
We་ct2 小时前
LeetCode 289. 生命游戏:题解+优化,从基础到原地最优
前端·算法·leetcode·矩阵·typescript
自己的九又四分之三站台2 小时前
9:MemNet记忆层使用,实现大模型对话上下文记忆
人工智能·算法·机器学习
Jack___Xue2 小时前
LangGraph学习笔记(六)---LangGraph ReAct应用
笔记·学习·react.js
LXS_3572 小时前
STL - 函数对象
开发语言·c++·算法
aini_lovee3 小时前
基于粒子群算法(PSO)优化BP神经网络权值与阈值的实现
神经网络·算法
老鼠只爱大米3 小时前
LeetCode经典算法面试题 #230:二叉搜索树中第K小的元素(递归法、迭代法、Morris等多种实现方案详细解析)
算法·leetcode·二叉搜索树·二叉树遍历·第k小的元素·morris遍历
星期五不见面3 小时前
嵌入式学习!(一)C++学习-leetcode(21)-26/1/29
学习·算法·leetcode
2501_941322033 小时前
通信设备零部件识别与检测基于改进YOLOv8-HAFB-2算法实现
算法·yolo