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

循环链表与双向链表

一、循环链表(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. 空表判断:不同实现方式(带头节点/不带头节点)的判断条件不同

相关推荐
蒲小英8 分钟前
算法-二分查找
算法
-Thinker9 分钟前
贪心算法解决找零钱问题
算法·贪心算法
浩浩的科研笔记15 分钟前
投论文常用技术笔记-使用visio导出贴合图像尺寸大小的PDF(无白边、无黑框)
笔记·pdf·论文笔记
sin_hielo18 分钟前
leetcode 2054(排序 + 单调栈,通用做法是 DP)
数据结构·算法·leetcode
晨晖219 分钟前
直接插入排序
c语言·数据结构·c++·算法
SJLoveIT24 分钟前
神经网络反向传播推导笔记 (整理版)
人工智能·笔记·神经网络
love530love31 分钟前
【笔记】华硕 ROG MAXIMUS Z890 HERO 主板 BIOS 更新完整操作实录
运维·人工智能·windows·笔记·单片机·嵌入式硬件·bios
HUST33 分钟前
C 语言 第七讲:数组和函数实践:扫雷游戏
c语言·开发语言·数据结构·vscode·算法·游戏·c#
玖剹36 分钟前
字符串相关题目
c语言·c++·算法·leetcode
llz_11237 分钟前
图(邻接表)-(DFS/BFS)-Dijkstra
算法·深度优先·dijkstra·宽度优先