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)等高级数据结构
  • 需要循环缓冲区的场景
相关推荐
Java技术实践3 小时前
JPA 用 List 入参在 @Query中报错 unexpected AST node: {vector}
数据结构·windows·list
2401_841495643 小时前
【数据结构】最长的最短路径的求解
java·数据结构·c++·python·算法·最短路径·图搜索
泡沫冰@3 小时前
数据结构(5)
数据结构
第七序章3 小时前
【C + +】红黑树:全面剖析与深度学习
c语言·开发语言·数据结构·c++·人工智能
violet-lz4 小时前
数据结构四大简单排序算法详解:直接插入排序、选择排序、基数排序和冒泡排序
数据结构·算法·排序算法
小白.cpp5 小时前
list链表容器
数据结构·链表·list
仰泳的熊猫5 小时前
LeetCode:207. 课程表
数据结构·c++·算法·leetcode
liu****5 小时前
19.map和set的封装
开发语言·数据结构·c++·算法
拾光Ծ6 小时前
【C++高阶数据结构】红黑树
数据结构·算法