S4双向链表

2.3双向链表

2.3.1 双向链表定义

双向链表(Doubly Linked List)是一种更复杂的链式数据结构,它的每个节点都包含两个指针,分别指向直接前驱节点和直接后继节点。这种双向连接的特性使得链表可以双向遍历,解决了单链表只能单向遍历的限制。

核心 结构对比

特性对比 单向链表 双向链表
指针 数量 1个(next) 2个(prev + next)
遍历 方向 只能从头到尾 双向遍历(前向+后向)
前驱 访问 O(n)时间复杂度 O(1)时间复杂度
空间 开销 较小 较大(多一个指针)
操作 复杂度 相对简单 稍复杂(需维护两个指针)

2.3.2 双向链表的设计与实现

节点结构定义:

复制代码
typedef int ELEM_TYPE;

typedef struct ListNode {

ELEM_TYPE val;

ListNode* prior;

ListNode* next;

} ListNode;

typedef struct LinkList { //头结点

ListNode* head;

int cursize;

} LinkList;

代码 解读

  • 每个节点包含三个部分:数据域、前驱指针(prior)、后继指针(next)
  • Li nkList 结构体封装头指针和大小信息,便于管理
  • 循环 特性:尾节点的next指向头节点,头节点的prior指向尾节点
  • 带头 节点:头节点不存储有效数据,作为哨兵节点简化操作
  1. 初始化函数 ( In itList )

    void InitList(LinkList* plist) {

    assert(plist != NULL);

    ListNode* p = buynode();

    if (p == NULL) return;

    p->val = 0;

    p->prior = p; // 前驱指向自己

    p->next = p; // 后继指向自己

    plist->head = p;

    plist->cursize = 0;

    }

关键

  • 创建头节点并建立自环 结构: p- >prior = p; p->next = p;
  • 空的双向循环链表就是头节点自己指向自己
  • 这种设计使得插入和删除操作的边界条件处理统一
  1. 节点创建函数 ( bu ynode )

    ListNode* buynode(ELEM_TYPE val) {

    ListNode* p = (ListNode*)malloc(sizeof(ListNode));

    if (p == NULL) return NULL;

    p->val = val;

    p->prior = NULL;

    p->next = NULL;

    return p;

    }

  2. 按位置查找节点 ( Fi ndPos )

    ListNode* FindPos(const LinkList* plist, int pos) {

    assert(plist != NULL);

    if (pos < 0 || pos > plist->cursize || Is_Empty(plist)) {

    printf("位置不符或链表为空\n");

    return NULL;

    }

    ListNode* p = plist->head;

    while (pos--) {

    p = p->next;

    }

    return p;

    }

关键

  • 位置约定:pos=0返回头节点,pos=1返回第一个数据节点
  • 循环遍历直到找到目标位置
  • 时间复杂度O(n)
  1. 插入操作函数群

在指定节点后插入 ( In sertNext )

复制代码
bool InsertNext(LinkList* plist, ListNode* ptr, ELEM_TYPE val) {

assert(plist != NULL);

if (ptr == NULL) { return false; }

ListNode* p = buynode();

if (p == NULL) return false;

p->val = val;

// 关键指针操作

p->prior = ptr;

p->next = ptr->next;

ptr->next = p;

p->next->prior = p; // 原ptr->next节点的前驱指向新节点

// 如果插入在尾节点后,需要更新头节点的前驱指向

if (Is_Empty(plist) || (ptr == plist->head->prior)) {

plist->head->prior = p;

}

plist->cursize++;

return true;

}

图解 插入过程

复制代码
插入前: A <--> C

在A后插入B: A <--> B <--> C

步骤:

1. B->prior = A

2. B->next = A->next (即C)

3. A->next = B

4. C->prior = B (即B->next->prior = B)

在指定节点前插入 ( In sertPrev )

复制代码
bool InsertPrev(LinkList* plist, ListNode* ptr, ELEM_TYPE val) {

assert(plist != NULL);

ListNode* newNode = buynode();

newNode->val = val;

newNode->next = ptr;

newNode->prior = ptr->prior;

ptr->prior = newNode;

newNode->prior->next = newNode; // 原ptr->prior节点的后继指向新节点

plist->cursize++;

return true;

}

关键 :双向链表的优势体现,前插操作也是O(1)时间复杂度

头插法和尾插法

复制代码
// 头插法:在头节点后插入

bool Push_Front(LinkList* plist, ELEM_TYPE val) {

assert(plist != NULL);

return InsertNext(plist, plist->head, val);

}

// 尾插法:在尾节点后插入

bool Push_Back(LinkList* plist, ELEM_TYPE val) {

assert(plist != NULL);

ListNode* tail = plist->head->prior; // 直接获取尾节点

return InsertNext(plist, tail, val);

}

关键

  • 头插法和尾插法的时间复杂度都是O( 1)
  • 双向循环链表的尾节点可以通过 he ad->prior 直接获得,无需遍历
  1. 删除操作函数群

删除指定节点的后继节点 ( De lNext )

复制代码
bool DelNext(LinkList* plist, ListNode* ptr) {

assert(plist != NULL);

if (plist->cursize <= 0) return false;

if (plist == NULL || ptr == NULL) return false;

ListNode* p = ptr->next;

ptr->next = p->next;

p->next->prior = ptr;

// 如果删除的是尾节点,需要更新头节点的前驱指向

if (p->next == plist->head) {

plist->head->prior = ptr;

}

free(p);

p = NULL;

plist->cursize--;

return true;

}

图解 删除过程

复制代码
删除前: A <--> B <--> C

删除B: A <--> C

步骤:

1. A->next = B->next (即C)

2. C->prior = B->prior (即A)

3. free(B)

头删法和尾删法

复制代码
// 头删法:删除头节点后的第一个节点

bool Pop_Front(LinkList* plist) {

return DelNext(plist, plist->head);

}

// 尾删法:删除尾节点

bool Pop_Back(LinkList* plist) {

assert(plist != NULL);

// 删除尾节点等价于删除尾节点的前驱节点的后继

return DelNext(plist, plist->head->prior->prior);

}

关键

  • 头删法和尾删法的时间复杂度都是O( 1)
  • 尾删法通过 he ad->prior->prior 直接找到倒数第二个节点
  1. 查找函数

按值查找 ( Fi ndValue )

复制代码
ListNode* FindValue(const LinkList* plist, ELEM_TYPE val) {

assert(plist != NULL);

ListNode* p = plist->head->next;

// 循环遍历,遇到头节点说明遍历完成

while (p != plist->head) {

if (p->val == val) return p;

p = p->next;

}

return NULL;

}

关键

  • 遍历的终止条件是 p != plist->head (不是NULL)
  • 时间复杂度O(n)
  1. 清空与销毁函数

清空链表 ( Cl earList )

复制代码
void ClearList(LinkList* plist) {

assert(plist != NULL);

ListNode* p = plist->head->next;

while (p != plist->head) {

ListNode* n = p;

p = p->next;

free(n);

n = NULL;

}

// 恢复头节点的自环状态

plist->head->prior = plist->head;

plist->head->next = plist->head;

plist->cursize = 0;

}

销毁链表 ( De stroyList )

复制代码
void DestroyList(LinkList* plist) {

assert(plist != NULL);

ListNode* p = plist->head->next;

// 先释放所有数据节点

while (p != plist->head) {

ListNode* n = p;

p = p->next;

free(n);

n = NULL;

}

// 再释放头节点

free(plist->head);

plist->head = NULL;

plist->cursize = 0;

}

2.3.3 双向循环链表的优势总结

操作 时间复杂度 关键要点
初始化 O(1) 创建头节点并建立自环
插入操作 O(1) 需要维护两个方向的指针
删除操作 O(1) 需要维护两个方向的指针
按值查找 O(n) 需要遍历整个链表
头尾操作 O(1) 双向循环链表的优势体现

核心 优势

  1. 双向 遍历能力:支持前向和后向遍历,灵活性极高
  1. 操作 效率高:插入、删除、头尾操作都是O(1)时间复杂度
  1. 边界 统一:循环结构使得头尾操作逻辑统一,代码简洁
  1. 空间 利用率:相对于性能提升,额外的指针开销是可接受的

适用 场景

  • 需要频繁在链表两端进行插入删除的操作
  • 需要双向遍历的应用程序(如浏览器历史记录)
  • 实现双向队列(Deque)等高级数据结构
  • 需要循环缓冲区的场景
相关推荐
多多*8 小时前
分布式系统中的CAP理论和BASE理论
java·数据结构·算法·log4j·maven
小白程序员成长日记8 小时前
2025.11.10 力扣每日一题
数据结构·算法·leetcode
dragoooon3410 小时前
[优选算法专题六.模拟 ——NO.40~41 外观数列、数青蛙]
数据结构·算法·leetcode
.小小陈.11 小时前
数据结构5:二叉树
数据结构
Laity______11 小时前
指针(2)
c语言·开发语言·数据结构·算法
是苏浙11 小时前
零基础入门C语言之C语言实现数据结构之顺序表经典算法
c语言·开发语言·数据结构·算法
太理摆烂哥13 小时前
数据结构之红黑树
数据结构
hnjzsyjyj13 小时前
洛谷 B4241:[海淀区小学组 2025] 统计数对 ← STL map
数据结构·stl map
泡沫冰@14 小时前
数据结构(18)
数据结构
苏纪云16 小时前
数据结构期中复习
数据结构·算法