循环双链表的实现_2027考研408

循环双链表的实现——2027考研408数据结构全攻略

适用范围:2027年全国硕士研究生招生考试·计算机学科专业基础(408)
考纲章节:第二章 线性表 → 2.3 线性表的链式表示 → 循环双链表
难度等级:⭐⭐⭐⭐(进阶重点,综合应用型考点多)


目录

  1. 知识点概念
  2. 循环双链表的结构定义
  3. 循环双链表的实现(基本操作)
  4. 循环双链表的进阶操作
  5. 时间与空间复杂度分析
  6. 优缺点对比
  7. 四种链表横向对比
  8. 应用场景
  9. 高频考点与易错点
  10. 真题演练
  11. 总结速记

一、知识点概念

1.1 双向链表定义

双向链表(Doubly Linked List) 是在单链表的基础上,每个结点增加一个指向前驱结点的指针域 prior,使得链表可以双向遍历。

每个结点包含三部分:

  • prior:指向前驱结点的指针
  • data:数据域
  • next:指向后继结点的指针
←────── prior ──────→ ←────── prior ──────→
      [prior │ data │ next] ←→ [prior │ data │ next]
←─────── next ───────→ ←─────── next ───────→

1.2 循环双链表定义

循环双链表(Circular Doubly Linked List) 是双向链表的循环版本,其特点是:

  • 首尾相连:头结点的前驱指向尾结点,尾结点的后继指向头结点
  • 无 NULL 指针:遍历时以头结点作为终止标志,形成完整的环

循环双链表结构

        prior                    prior
          ↑                        ↑
          │    ┌──────────────────┘
          │    │
[头] ←── prior │ data │ next ──→ [a1] ←── prior │ data │ next ──→ ... ←── prior │ data │ next
          │    │                  ↑                   │
          │    └──────────────────┘                   │
          └───────────────────────────────────────────┘
                    next                              next

空表结构

[头] ── prior ──→ [头]
↑                  ↓
└────── next ─────┘
(头结点的前驱和后继都指向自己)

1.3 核心特征

  1. 双向遍历:既可以从头向尾遍历,也可以从尾向头遍历
  2. 首尾相连:尾结点的 next 指向头结点,头结点的 prior 指向尾结点
  3. 无 NULL 指针:以头结点作为双向循环的标志
  4. 插入删除更便捷:已知某结点时,其前驱和后继都可直接访问,插入/删除只需修改四个指针

1.4 循环双链表 vs 普通双链表

对比维度普通双链表循环双链表
首尾连接否,prior/ next 可能为 NULL是,形成环
头结点 priorNULL指向尾结点
尾结点 nextNULL指向头结点
空表 priorNULL指向自己
空表 nextNULL指向自己
遍历终止(从头到尾)p->next != NULLp->next != L
遍历终止(从尾到头)p->prior != NULLp->prior != L

二、循环双链表的结构定义

2.1 C语言结构体定义

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

typedef int ElemType;

/* 循环双链表结点定义 */
typedef struct DNode {
    ElemType data;               // 数据域
    struct DNode *prior;         // 前驱指针
    struct DNode *next;         // 后继指针
} DNode, *DLinkList;

⚠️ 注意:循环双链表的结点结构与普通双链表完全相同,区别在于初始状态和遍历终止条件。考试中默认使用带头结点的循环双链表。

2.2 空表的三种表示

// 循环双链表(带头结点)空表的三种等价判断
L->next == L;              // ① 后继指向自己
L->prior == L;             // ② 前驱指向自己
L->next == L && L->prior == L;  // ③ 两者同时成立

2.3 初始化状态

// 循环双链表初始化后的状态
[L] ──prior──→ [L]
↑               ↓
└─────next─────┘

三、循环双链表的实现(基本操作)

3.1 初始化(带头结点)

/* 初始化循环双链表(带头结点) */
bool InitList(DLinkList *L) {
    *L = (DLinkList)malloc(sizeof(DNode));
    if (*L == NULL) return false;
    
    (*L)->prior = *L;           // 前驱指向自己
    (*L)->next = *L;             // 后继指向自己(空表特征)
    return true;
}

/* 判空 */
bool ListEmpty(DLinkList L) {
    return L->next == L;          // 后继指向自己即为空表
}

3.2 按位查找

/* 按位查找:返回第i个结点的指针(1-based)
 * 从头结点出发向后遍历
 */
DNode* GetElem(DLinkList L, int i) {
    if (i < 1) return NULL;
    
    DNode *p = L;                  // p从头结点开始
    int j = 0;
    
    while (p != NULL && j < i) { // 找到第i个结点
        p = p->next;
        j++;
    }
    
    // 循环双链表中,由于是环,最终p会回到L,不会为NULL
    // 但j < i时可能p == L,此时i超过表长
    return (j == i) ? p : NULL;
}

/* 按位查找(循环双链表优化版)
 * 利用循环双链表特性:已知表长时可直接判断越界
 */
DNode* GetElem_Opt(DLinkList L, int i, int len) {
    if (i < 1 || i > len) return NULL;
    
    DNode *p = L->next;            // p指向第一个数据结点
    for (int j = 1; j < i; j++)
        p = p->next;
    return p;
}

3.3 按值查找

/* 按值查找:返回第一个值为e的结点的位序,不存在返回0 */
int LocateElem(DLinkList L, ElemType e) {
    DNode *p = L->next;
    int i = 1;
    
    while (p != L) {              // 未回到头结点则继续
        if (p->data == e)
            return i;
        p = p->next;
        i++;
    }
    return 0;
}

3.4 插入操作

循环双链表的插入操作是其最大优势之一——已知某结点时,前插和后插都只需 O(1) 时间,且不需要遍历找前驱。

3.4.1 在p结点之后插入s(后插)
/* 在p结点之后插入元素e
 * 核心:只需修改4个指针
 */
bool InsertNextDNode(DNode *p, ElemType e) {
    if (p == NULL) return false;
    
    DNode *s = (DNode *)malloc(sizeof(DNode));
    if (s == NULL) return false;
    s->data = e;
    
    s->next = p->next;            // ① s的后继指向p的后继
    s->prior = p;                 // ② s的前驱指向p
    p->next->prior = s;           // ③ p的后继的前驱指向s
    p->next = s;                  // ④ p的后继指向s
    
    return true;
}

后插图解

插入前:     p ──next──→ [a1] ←─prior── L
                  ↑
                  │
插入e=99:

① s->next = p->next  →  s指向[a1]
② s->prior = p       →  s指向p
③ p->next->prior = s →  [a1]指向s
④ p->next = s        →  p指向s

插入后:     p ──next──→ [s:99] ──next──→ [a1] ←─prior── L
           ↑________________________________↑
                     prior
3.4.2 在p结点之前插入s(前插)

循环双链表的前插操作同样高效,无需像单链表那样"偷天换日"。

/* 在p结点之前插入元素e
 * 直接利用prior指针找到前驱,然后插入
 */
bool InsertPriorDNode(DNode *p, ElemType e) {
    if (p == NULL) return false;
    
    DNode *s = (DNode *)malloc(sizeof(DNode));
    if (s == NULL) return false;
    s->data = e;
    
    s->prior = p->prior;          // ① s的前驱指向p的前驱
    s->next = p;                  // ② s的后继指向p
    p->prior->next = s;           // ③ p的前驱的后继指向s
    p->prior = s;                 // ④ p的前驱指向s
    
    return true;
}

📌 考研重点:循环双链表的前插和后插都只需 O(1) 时间,这是区别于所有单链表变体的核心优势!

3.4.3 按位序插入
/* 在第i个位置插入元素e(1-based) */
bool ListInsert(DLinkList *L, int i, ElemType e) {
    if (i < 1) return false;
    
    DNode *p = GetElem(*L, i - 1); // 找到第i-1个结点
    if (p == NULL) return false;
    
    return InsertNextDNode(p, e);
}

/* 头插法(高效) */
bool ListInsert_Head(DLinkList *L, ElemType e) {
    return InsertNextDNode(*L, e);  // 在头结点之后插入,即头插
}

/* 尾插法(高效) */
bool ListInsert_Tail(DLinkList *L, ElemType e) {
    return InsertPriorDNode(*L, e); // 在头结点之前插入,即尾插
}

3.5 删除操作

3.5.1 删除p的后继结点
/* 删除p的后继结点(p本身不删)
 * 循环双链表中,p的后继一定存在(不为NULL)
 */
bool DeleteNextDNode(DNode *p, ElemType *e) {
    if (p == NULL) return false;
    if (p->next == p) return false; // 空表,无法删除(只有一个头结点)
    
    DNode *q = p->next;            // q指向待删除结点
    *e = q->data;
    
    p->next = q->next;             // ① p的后继指向q的后继
    q->next->prior = p;           // ② q的后继的前驱指向p
    free(q);
    
    return true;
}

删除后继图解

删除前:  p ──next──→ [q] ──next──→ [r]
                  ↑
删除q:
① p->next = q->next  →  p指向r
② r->prior = p       →  r指向p

删除后:  p ──next──→ [r]
3.5.2 删除p结点本身
/* 删除指定结点p(假设p不是头结点) */
bool DeleteDNode(DNode *p, ElemType *e) {
    if (p == NULL) return false;
    
    *e = p->data;
    p->prior->next = p->next;     // ① p的前驱的后继指向p的后继
    p->next->prior = p->prior;    // ② p的后继的前驱指向p的前驱
    free(p);
    
    return true;
}

⚠️ 注意:循环双链表中删除任意已知结点(除头结点外)的时间复杂度都是 O(1)

3.5.3 按位序删除
/* 删除第i个位置的元素,并用e返回其值 */
bool ListDelete(DLinkList *L, int i, ElemType *e) {
    if (i < 1) return false;
    
    DNode *p = GetElem(*L, i - 1); // 找到第i-1个结点
    if (p == NULL || p->next == *L) return false; // p->next是头结点则越界
    
    return DeleteNextDNode(p, e);
}

3.6 求表长

/* 求循环双链表的表长 */
int ListLength(DLinkList L) {
    int len = 0;
    DNode *p = L->next;           // p指向第一个数据结点
    
    while (p != L) {              // 未回到头结点则计数
        len++;
        p = p->next;
    }
    return len;
}

3.7 整表创建

3.7.1 尾插法创建
/* 尾插法创建循环双链表 */
DLinkList List_TailInsert(DLinkList *L) {
    InitList(L);
    
    DNode *r = *L;                  // r指向头结点(也是尾结点)
    ElemType x;
    DNode *s;
    
    printf("请输入元素(输入9999结束): ");
    scanf("%d", &x);
    
    while (x != 9999) {
        s = (DNode *)malloc(sizeof(DNode));
        s->data = x;
        
        // 尾插法:在尾结点之前插入(头结点之前即表尾)
        s->next = r;
        s->prior = r->prior;
        r->prior->next = s;
        r->prior = s;
        
        r = s;                      // r移动到新的尾结点
        scanf("%d", &x);
    }
    
    return *L;
}
3.7.2 头插法创建
/* 头插法创建循环双链表 */
DLinkList List_HeadInsert(DLinkList *L) {
    InitList(L);
    
    ElemType x;
    DNode *s;
    
    printf("请输入元素(输入9999结束): ");
    scanf("%d", &x);
    
    while (x != 9999) {
        s = (DNode *)malloc(sizeof(DNode));
        s->data = x;
        
        // 头插法:在头结点之后插入
        s->next = (*L)->next;
        s->prior = *L;
        (*L)->next->prior = s;
        (*L)->next = s;
        
        scanf("%d", &x);
    }
    
    return *L;
}

3.8 完整示例代码

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

typedef int ElemType;

typedef struct DNode {
    ElemType data;
    struct DNode *prior;
    struct DNode *next;
} DNode, *DLinkList;

/* 初始化 */
bool InitList(DLinkList *L) {
    *L = (DLinkList)malloc(sizeof(DNode));
    if (*L == NULL) return false;
    (*L)->prior = *L;
    (*L)->next = *L;
    return true;
}

/* 判空 */
bool ListEmpty(DLinkList L) {
    return L->next == L;
}

/* 按位查找 */
DNode* GetElem(DLinkList L, int i) {
    if (i < 1) return NULL;
    DNode *p = L;
    int j = 0;
    while (p != NULL && j < i) {
        p = p->next;
        j++;
    }
    return (j == i) ? p : NULL;
}

/* 按值查找 */
int LocateElem(DLinkList L, ElemType e) {
    DNode *p = L->next;
    int i = 1;
    while (p != L) {
        if (p->data == e) return i;
        p = p->next;
        i++;
    }
    return 0;
}

/* 后插 */
bool InsertNextDNode(DNode *p, ElemType e) {
    if (p == NULL) return false;
    DNode *s = (DNode *)malloc(sizeof(DNode));
    if (s == NULL) return false;
    s->data = e;
    s->next = p->next;
    s->prior = p;
    p->next->prior = s;
    p->next = s;
    return true;
}

/* 前插 */
bool InsertPriorDNode(DNode *p, ElemType e) {
    if (p == NULL) return false;
    DNode *s = (DNode *)malloc(sizeof(DNode));
    if (s == NULL) return false;
    s->data = e;
    s->prior = p->prior;
    s->next = p;
    p->prior->next = s;
    p->prior = s;
    return true;
}

/* 按位序插入 */
bool ListInsert(DLinkList *L, int i, ElemType e) {
    if (i < 1) return false;
    DNode *p = GetElem(*L, i - 1);
    return p != NULL && InsertNextDNode(p, e);
}

/* 删除后继 */
bool DeleteNextDNode(DNode *p, ElemType *e) {
    if (p == NULL || p->next == p) return false;
    DNode *q = p->next;
    *e = q->data;
    p->next = q->next;
    q->next->prior = p;
    free(q);
    return true;
}

/* 删除指定结点 */
bool DeleteDNode(DNode *p, ElemType *e) {
    if (p == NULL) return false;
    *e = p->data;
    p->prior->next = p->next;
    p->next->prior = p->prior;
    free(p);
    return true;
}

/* 按位序删除 */
bool ListDelete(DLinkList *L, int i, ElemType *e) {
    if (i < 1) return false;
    DNode *p = GetElem(*L, i - 1);
    if (p == NULL || p->next == *L) return false;
    return DeleteNextDNode(p, e);
}

/* 表长 */
int ListLength(DLinkList L) {
    int len = 0;
    DNode *p = L->next;
    while (p != L) {
        len++;
        p = p->next;
    }
    return len;
}

/* 尾插法创建 */
DLinkList List_TailInsert(DLinkList *L) {
    InitList(L);
    DNode *r = *L;
    ElemType x;
    printf("输入元素(9999结束): ");
    scanf("%d", &x);
    while (x != 9999) {
        DNode *s = (DNode *)malloc(sizeof(DNode));
        s->data = x;
        s->next = r;
        s->prior = r->prior;
        r->prior->next = s;
        r->prior = s;
        r = s;
        scanf("%d", &x);
    }
    return *L;
}

/* 打印(正序) */
void PrintList(DLinkList L) {
    printf("循环双链表(正序,长度=%d): ", ListLength(L));
    DNode *p = L->next;
    while (p != L) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

/* 打印(逆序) */
void PrintList_Reverse(DLinkList L) {
    printf("循环双链表(逆序): ");
    DNode *p = L->prior;           // 从尾结点开始
    while (p != L) {
        printf("%d ", p->data);
        p = p->prior;
    }
    printf("\n");
}

/* 主函数测试 */
int main() {
    DLinkList L;
    ElemType e;

    // 尾插法创建: 10 20 30 40 50
    List_TailInsert(&L);
    PrintList(L);                   // 正序: 10 20 30 40 50
    PrintList_Reverse(L);           // 逆序: 50 40 30 20 10

    // 在第3位插入25
    ListInsert(&L, 3, 25);
    PrintList(L);                   // 正序: 10 20 25 30 40 50

    // 在尾插(直接在头结点之前插入)
    InsertPriorDNode(L, 5);
    PrintList(L);                   // 正序: 5 10 20 25 30 40 50

    // 删除第2位
    ListDelete(&L, 2, &e);
    printf("删除的元素: %d\n", e);  // 输出: 10
    PrintList(L);                   // 正序: 5 20 25 30 40 50

    // 查找
    printf("第3个元素: %d\n", GetElem(L, 3)->data);       // 输出: 25
    printf("元素30的位序: %d\n", LocateElem(L, 30));    // 输出: 4

    free(L);
    return 0;
}

四、循环双链表的进阶操作

4.1 正序与逆序遍历

/* 正序遍历(从头到尾) */
void Traverse_Forward(DLinkList L) {
    DNode *p = L->next;
    printf("正序: ");
    while (p != L) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

/* 逆序遍历(从尾到头)——循环双链表独有! */
void Traverse_Backward(DLinkList L) {
    DNode *p = L->prior;           // 从尾结点开始
    printf("逆序: ");
    while (p != L) {
        printf("%d ", p->data);
        p = p->prior;
    }
    printf("\n");
}

📌 考研重点:循环双链表是唯一能 O(1) 时间逆序遍历的链表结构!普通单链表无法逆序遍历,普通双链表逆序遍历时间复杂度是 O(n)(因为不知道尾结点在哪)。

4.2 合并两个循环双链表

/* 合并两个循环双链表La和Lb,结果放在La中 */
DLinkList MergeList(DLinkList *La, DLinkList *Lb) {
    if (*La == NULL || *Lb == NULL) return NULL;
    
    // 找到La的尾结点和首结点
    DNode *La_tail = (*La)->prior; // La的尾结点
    DNode *La_head = *La;          // La的头结点
    DNode *Lb_tail = (*Lb)->prior; // Lb的尾结点
    DNode *Lb_head = (*Lb)->next;  // Lb的首结点
    
    // 断开两个环,重新连接
    La_tail->next = Lb_head;       // La尾连Lb首
    Lb_head->prior = La_tail;
    
    Lb_tail->next = La_head;       // Lb尾连La头
    La_head->prior = Lb_tail;
    
    free(*Lb);                      // 释放Lb的头结点
    return *La;
}

4.3 循环双链表的逆置

/* 逆置循环双链表
 * 思路:依次将每个结点的前驱和后继交换
 */
void Reverse(DLinkList *L) {
    if (*L == NULL || (*L)->next == *L) return; // 空表
    
    DNode *p = *L;
    DNode *q;
    
    // 依次交换每个结点(包括头结点)的前驱和后继
    do {
        q = p->next;               // 保存后继
        // 交换prior和next
        p->next = p->prior;
        p->prior = q;
        p = q;                     // 移动到下一个结点
    } while (p != *L);
    
    // 此时链表方向已反转
}

4.4 删除所有值为x的结点

/* 删除循环双链表中所有值为x的结点 */
void DeleteX(DLinkList *L, ElemType x) {
    if (*L == NULL || (*L)->next == *L) return; // 空表
    
    DNode *p = (*L)->next;
    
    while (p != *L) {
        if (p->data == x) {
            DNode *q = p;
            p->prior->next = p->next;
            p->next->prior = p->prior;
            p = p->next;            // 移动到下一个结点
            free(q);
        } else {
            p = p->next;
        }
    }
}

4.5 C++ STL list 实现(了解)

#include <iostream>
#include <list>
using namespace std;

int main() {
    list<int> L;                    // C++ STL list是双向链表
    
    // 插入
    L.push_back(10);               // 尾插
    L.push_front(5);               // 头插
    L.insert(++L.begin(), 15);     // 在第二个位置插入
    
    // 删除
    L.pop_back();                 // 尾删
    L.pop_front();                // 头删
    L.remove(10);                 // 删除所有值为10的结点
    
    // 遍历(支持双向迭代器)
    for (auto it = L.begin(); it != L.end(); ++it)
        cout << *it << " ";
    cout << endl;
    
    // 逆序遍历
    for (auto it = L.rbegin(); it != L.rend(); ++it)
        cout << *it << " ";
    cout << endl;
    
    return 0;
}

五、时间与空间复杂度分析

5.1 时间复杂度

操作最好情况最坏情况平均情况说明
按位查找 GetElemO(1)O(n)O(n)需遍历
按值查找 LocateElemO(1)O(n)O(n)目标在起始位置最好
后插(已知位置)O(1)O(1)O(1)只需4个指针修改
前插(已知位置)O(1)O(1)O(1)只需4个指针修改
删除后继(已知位置)O(1)O(1)O(1)只需3个指针修改
删除指定结点O(1)O(1)O(1)只需3个指针修改
头插法建表O(1)/次O(1)/次O(1)/次每次O(1),共n次
尾插法建表O(1)/次O(1)/次O(1)/次每次O(1),共n次
正序遍历O(n)O(n)O(n)遍历所有结点
逆序遍历O(1)O(n)从尾到头O(1),但找尾需O(n)

5.2 空间复杂度

类型空间复杂度说明
循环双链表O(n)n个结点需2n个指针
各操作辅助空间O(1)仅需几个临时指针

📌 考研重点:循环双链表是所有链表结构中插入删除效率最高的!所有已知结点的插入删除操作都是 O(1),且能双向遍历。


六、优缺点对比

6.1 循环双链表优点

  1. 双向遍历:既可以从头到尾,也可以从尾到头遍历
  2. 插入删除极致高效:已知结点时,前插/后插/删除都是 O(1)
  3. 无需找前驱:双向指针使得前驱操作直接可得
  4. 尾操作高效:尾插、尾删无需遍历,O(1) 时间
  5. 无 NULL 指针:内存利用率高,边界处理统一

6.2 循环双链表缺点

  1. 存储密度更低:每个结点需要两个指针域(prior + next),比单链表多一倍
  2. 实现复杂度高:指针操作多,4指针修改容易出错
  3. 容易死循环:遍历终止条件错误会导致死循环
  4. 空间开销大:指针域占用的内存比单链表多一倍

七、四种链表横向对比

7.1 完整对比表

对比维度顺序表普通单链表循环单链表循环双链表
物理存储连续离散离散离散
指针个数/结点0112
存储密度最高较高较高较低
按位查找O(1)O(n)O(n)O(n)
按值查找O(n)O(n)O(n)O(n)
前插(已知位置)O(n)O(n)①O(n)②O(1)
后插(已知位置)O(n)O(1)O(1)O(1)
删除后继(已知位置)O(n)O(1)O(1)O(1)
删除自己(已知位置)O(n)③O(n)③O(1)
正向遍历O(n)O(n)O(n)O(n)
反向遍历O(n)不可不可O(n)
尾插法建表O(1)O(n²)O(n)O(n)
约瑟夫环不适合不适合适合最适合
实现难度简单较低较高最高

① 普通单链表前插需要先找到前驱:O(n)
② 循环单链表前插可以用"偷天换日":O(1)
③ 单链表删除自己需要先找到前驱:O(n)

7.2 选择建议

场景推荐结构
频繁随机访问(查找)顺序表
频繁在任意位置插入删除循环双链表
只需头插/头删单链表 / 循环单链表
约瑟夫环等环形问题循环单链表 / 循环双链表
需要双向遍历循环双链表
追求实现简单顺序表 / 普通单链表
追求存储密度顺序表 / 普通单链表

八、应用场景

8.1 适合使用循环双链表的场景

  • 需要双向遍历:如文本编辑器(光标移动)、浏览器前进后退
  • 频繁在任意位置插入删除:如LRU缓存淘汰算法
  • 约瑟夫环问题:最合适的结构
  • 操作系统调度:进程的双向就绪/阻塞队列
  • 文件系统:目录树的双向链接
  • 音乐播放器:播放列表的上一首/下一首切换

8.2 实际应用举例

应用使用原因
浏览器前进/后退双向链表天然支持前进后退
LRU缓存淘汰双向链表O(1)移动到表尾(最新访问)
Linux内核链表内核中的经典数据结构
文本编辑器撤销/重做双向链表维护操作历史
约瑟夫环循环双链表效率最高
音乐播放器播放列表支持上一首/下一首快速切换

九、高频考点与易错点

9.1 高频考点

考点1:四种指针修改 ⭐⭐⭐⭐

循环双链表插入/删除时,指针修改的数量和顺序是高频考点:

// 在p之后插入s(后插):修改4个指针
s->next = p->next;        // ① s的后继 = p的后继
s->prior = p;             // ② s的前驱 = p
p->next->prior = s;       // ③ p的后继的前驱 = s
p->next = s;              // ④ p的后继 = s

// 在p之前插入s(前插):修改4个指针
s->prior = p->prior;      // ① s的前驱 = p的前驱
s->next = p;               // ② s的后继 = p
p->prior->next = s;        // ③ p的前驱的后继 = s
p->prior = s;              // ④ p的前驱 = s

// 删除p的后继q:修改3个指针
p->next = q->next;        // ① p的后继 = q的后继
q->next->prior = p;       // ② q的后继的前驱 = p
free(q);                   // ③ 释放q

// 删除p本身:修改2个指针
p->prior->next = p->next; // ① p的前驱的后继 = p的后继
p->next->prior = p->prior; // ② p的后继的前驱 = p的前驱
free(p);
考点2:空表判断 ⭐⭐⭐
// 循环双链表(带头结点)空表的三种等价判断
L->next == L;              // ✅ 后继指向自己
L->prior == L;             // ✅ 前驱指向自己
L->next == L && L->prior == L; // ✅ 两者同时成立
考点3:循环遍历终止条件 ⭐⭐⭐
遍历方向循环双链表终止条件
从头到尾p != L
从尾到头p != L
单链表(对比)p != NULL
循环单链表(对比)p != L
考点4:插入删除 O(1) 特性 ⭐⭐⭐⭐

循环双链表是唯一一种所有基本操作(除遍历查找外)都是 O(1) 的链表结构:

  • 已知某结点 → 前插:O(1)
  • 已知某结点 → 后插:O(1)
  • 已知某结点 → 删除该结点:O(1)
  • 已知某结点 → 删除其后继:O(1)
考点5:后插 vs 前插的选择 ⭐⭐
// 后插:已知位置 p,在 p 之后插入
InsertNextDNode(p, e);   // s->next = p->next; s->prior = p; ...

// 前插:已知位置 p,在 p 之前插入
InsertPriorDNode(p, e);  // s->prior = p->prior; s->next = p; ...

// 考研注意:若要在表尾插入,只能用前插(尾结点无后继!)
// 因为 p->next == L(头结点),前插会插到尾结点之前
考点6:循环双链表 vs 循环单链表 ⭐⭐
对比维度循环单链表循环双链表
指针数/结点12
删除尾结点O(n)(需遍历找前驱)O(1)(直接通过 prior)
反向遍历❌ 不支持O(1) 找尾,O(n) 遍历
前插O(1)(偷天换日)O(1)(直接用 prior)
存储密度较高较低
实现难度较高最高

9.2 易错点汇总

易错点1:前插/后插时指针顺序写错 ⭐⭐⭐
// ❌ 错误:先修改 p->next,再访问 p->next->prior(此时 p->next 已改变!)
s->next = p->next;      // ① p->next 已指向 s
p->next->prior = s;      // ② ❌ p->next 已是 s,无法正确链接!

// ✅ 正确顺序:先保存后继,再依次链接
s->next = p->next;      // ① s 的后继 = p 的后继
s->prior = p;            // ② s 的前驱 = p
p->next->prior = s;     // ③ p 的后继的前驱 = s(此时 p->next 还没变)
p->next = s;             // ④ p 的后继 = s
易错点2:删除时忘记处理前驱链接 ⭐⭐⭐
// ❌ 错误:只修改 next,忘记修改 prior
p->next = q->next;      // ① 改 next
free(q);                  // ② 但 q->next 的 prior 还是指向 q!
// 结果:q->next->prior 仍指向已释放的 q!

// ✅ 正确:两个指针都要修改
p->next = q->next;       // ① p 的后继 = q 的后继
q->next->prior = p;     // ② q 的后继的前驱 = p
free(q);                  // ③ 释放 q
易错点3:删除尾结点时未处理循环关系 ⭐⭐
// 循环双链表中删除尾结点(p 为尾结点)
p->prior->next = p->next; // p->prior 为头结点(L)
// p->next 为头结点(L)
// 所以头结点的 next 应指向 p->next(即 L),无需额外处理
// 只需:p->prior->next = p->next(这一步已将 L 的 next 指向 L)
// 再:p->next->prior = p->prior(头结点的 prior 指向 p 的前驱,即 L)
// 最后 free(p)

// ✅ 循环双链表删除任意已知结点(除头结点外)都是 O(1)
易错点4:混淆普通双链表和循环双链表的判空 ⭐⭐
// 普通双链表(带头结点)空表判断
L->next == NULL;           // ✅ 头结点 next 为 NULL
L->prior == NULL;          // ✅ 头结点 prior 为 NULL

// 循环双链表(带头结点)空表判断
L->next == L;              // ✅ 头结点 next 指向自己
L->prior == L;             // ✅ 头结点 prior 指向自己

// ❌ 常见错误:循环双链表判空写成 NULL
if (L->next == NULL)      // ❌ 这是普通双链表的写法!
易错点5:头插/尾插记混 ⭐⭐
// 头插(头结点之后插入):在 L 之后插入
s->next = L->next;        // ① s 的后继 = 原来的第一个结点
s->prior = L;             // ② s 的前驱 = 头结点
L->next->prior = s;       // ③ 原第一个结点的前驱 = s
L->next = s;              // ④ 头结点的后继 = s

// 尾插(头结点之前插入,即表尾):在 L->prior 之后插入
// 等价于:在尾结点之后插入(尾插法)
// 即在 L->prior 之后插入,可直接用前插
InsertPriorDNode(L, e);    // ✅ 在头结点之前插入,即尾插
// 等价于:
s->next = L;               // s 的后继 = 头结点
s->prior = L->prior;      // s 的前驱 = 原尾结点
L->prior->next = s;      // 原尾结点的后继 = s
L->prior = s;             // 头结点的前驱 = s
易错点6:逆置时忘记头结点 ⭐⭐
// ❌ 错误:逆置时漏掉头结点的指针交换
// 只交换数据结点,忘记头结点的 prior 也要更新

// ✅ 正确:交换所有结点(包括头结点)的前驱和后继
void Reverse_Correct(DLinkList *L) {
    DNode *p = *L;
    DNode *q;
    do {
        q = p->next;
        p->next = p->prior;
        p->prior = q;
        p = q;
    } while (p != *L);
    // 头结点也被正确交换了!
}

9.3 大题常见考法

  1. 选择题:四种指针修改的数量和顺序
  2. 选择题:循环双链表 vs 其他链表的复杂度对比
  3. 选择题:空表判断、遍历终止条件
  4. 选择题:给定结点指针,判断前插/后插/删除的时间复杂度
  5. 算法设计:删除循环双链表中所有值为 x 的结点
  6. 算法设计:合并两个循环双链表
  7. 填空题:写出前插/后插/删除操作的指针修改步骤

十、真题演练

题目1(选择题)⭐⭐⭐

题目:在循环双链表中,在指针 p 所指向的结点之后插入指针 s 所指向的结点,需要修改的指针数量为( )。

A. 2  B. 3  C. 4  D. 5

答案:C

解析:后插需要修改 4 个指针:

  1. s->next = p->next
  2. s->prior = p
  3. p->next->prior = s
  4. p->next = s

题目2(选择题)⭐⭐⭐

题目:在循环双链表中,删除指针 p 所指向的结点(假设 p 不是头结点),需要修改的指针数量为( )。

A. 1  B. 2  C. 3  D. 4

答案:B

解析:删除指定结点只需要修改 2 个指针:

  1. p->prior->next = p->next
  2. p->next->prior = p->prior

题目3(选择题)⭐⭐

题目:以下叙述中,正确的是( )。

A. 循环双链表不支持随机访问
B. 循环双链表的按位查找时间复杂度为 O(1)
C. 循环双链表的删除操作时间复杂度一定为 O(1)
D. 循环双链表不需要判满

答案:A

解析

  • A ✅:链表不支持随机访问,只能顺序访问
  • B ❌:按位查找需遍历,时间复杂度为 O(n)
  • C ❌:删除操作本身 O(1),但找到待删结点需要 O(n)
  • D ✅:链表无需判满,但这不是循环双链表独有的特性

题目4(算法设计)⭐⭐⭐

题目:编写算法,在循环双链表中删除所有值为 x 的结点,并分析时间复杂度。

/* 删除循环双链表中所有值为 x 的结点 */
void DeleteX(DLinkList *L, ElemType x) {
    if (*L == NULL || (*L)->next == *L) return; // 空表

    DNode *p = (*L)->next;

    while (p != *L) {              // 遍历到头结点为止
        if (p->data == x) {
            DNode *q = p;
            p = p->next;            // 先保存下一个结点

            q->prior->next = q->next;  // 前驱的后继 = q 的后继
            q->next->prior = q->prior; // q 的后继的前驱 = q 的前驱
            free(q);                  // 释放 q
            // 注意:p 已在上面保存,不需要重新找
        } else {
            p = p->next;
        }
    }
}

复杂度分析:时间 O(n),空间 O(1)。


题目5(算法设计)⭐⭐⭐

题目:编写算法,合并两个循环双链表 La 和 Lb,结果放在 La 中,并分析时间复杂度。

/* 合并两个循环双链表 La 和 Lb,结果放在 La 中 */
DLinkList MergeList(DLinkList *La, DLinkList *Lb) {
    if (*La == NULL) return *Lb;
    if (*Lb == NULL) return *La;

    // 保存首尾指针
    DNode *La_head = *La;
    DNode *La_tail = (*La)->prior;  // La 尾结点
    DNode *Lb_head = (*Lb)->next;   // Lb 首结点
    DNode *Lb_tail = (*Lb)->prior;  // Lb 尾结点

    // 断开两个环,重新连接
    La_tail->next = Lb_head;         // La 尾连 Lb 首
    Lb_head->prior = La_tail;

    Lb_tail->next = La_head;         // Lb 尾连 La 头
    La_head->prior = Lb_tail;

    free(*Lb);                        // 释放 Lb 的头结点
    return *La;
}

复杂度分析:时间 O(1),空间 O(1)。这是循环双链表合并的巨大优势——无需遍历找尾结点!


十一、总结速记

🔑 核心概念速记

【循环双链表的本质】
  = 双向链表 + 首尾相连
  = prior 和 next 都形成环
  = 无 NULL 指针

【四种指针操作速记】
  后插(p后插入s):4步  s→后继  s→p  后继→s  p→s
  前插(p前插入s):4步  s→前驱  s→p  前驱→s  p→s
  删除后继(p的后继q):3步  p→后继  后继→p  free(q)
  删除自己:2步  前驱→后继  后继→前驱

【空表判断(三选一)】
  L->next == L    ✅
  L->prior == L   ✅
  L->next == L && L->prior == L  ✅

🔑 核心优势速记

循环双链表是所有链表结构中综合性能最强的:
  ✅ 按位查找:O(n)
  ✅ 前插:O(1)(已知结点)
  ✅ 后插:O(1)(已知结点)
  ✅ 删除:O(1)(已知结点)
  ✅ 正向遍历:O(n)
  ✅ 反向遍历:O(n)(可从尾结点直接开始)
  ✅ 合并两表:O(1)(无需遍历找尾)

🔑 循环双链表 vs 循环单链表一句话

循环单链表:只能单向遍历,删除尾结点需 O(n) 找前驱,但存储密度高
循环双链表:双向遍历,删除任意结点 O(1),但存储密度低(多一倍指针)

🔑 考试答题模板

写循环双链表算法时,必须包含:

  1. ✅ 后插:先保存后继,再依次修改 4 个指针
  2. ✅ 前插:先保存前驱,再依次修改 4 个指针
  3. ✅ 删除:同时修改前驱和后继的链接,共 2 步
  4. ✅ 遍历:p != L(不能用 p != NULL
  5. ✅ 空表:L->next == L(不能用 == NULL

🔑 知识体系框架

循环双链表(Circular Doubly Linked List)
│
├── 结构特征
│   ├── prior → 指向前驱结点
│   ├── next  → 指向后继结点
│   ├── 头结点的 prior → 尾结点(形成逆向环)
│   └── 尾结点的 next  → 头结点(形成正向环)
│
├── 关键判断
│   ├── 空表:L->next == L(prior 也可)
│   ├── 遍历(正向):p != L
│   ├── 遍历(逆向):p != L
│   └── 尾结点:p->next == L
│
├── 核心优势
│   ├── 已知结点前插/后插:O(1)
│   ├── 已知结点删除自己:O(1)
│   ├── 双向遍历
│   └── 合并两表:O(1)
│
├── 指针操作
│   ├── 后插:4 个指针
│   ├── 前插:4 个指针
│   ├── 删除后继:3 个指针
│   └── 删除自己:2 个指针
│
└── 应用
    ├── 约瑟夫环
    ├── LRU 缓存淘汰算法
    ├── 浏览器前进/后退
    ├── 音乐播放器上一首/下一首
    └── 操作系统进程调度

📚 参考资料

  • 王道考研《数据结构》第2章 线性表
  • 严蔚敏《数据结构(C语言版)》第2章
  • 2027年408考试大纲·数据结构部分

本文涵盖2027年408考研数据结构循环双链表全部考点,建议结合真题反复练习代码实现。
如有疑问,欢迎在评论区交流!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值