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;
- 空的双向循环链表就是头节点自己指向自己
- 这种设计使得插入和删除操作的边界条件处理统一
2. 节点创建函数 ( 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;}
3. 按位置查找节点 ( 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)
4. 插入操作函数群
在指定节点后插入 ( 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 = A2. B->next = A->next (即C)3. A->next = B4. 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 直接获得,无需遍历
5. 删除操作函数群
删除指定节点的后继节点 ( 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 直接找到倒数第二个节点
6. 查找函数
按值查找 ( 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)
7. 清空与销毁函数
清空链表 ( 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) | 双向循环链表的优势体现 |
核心优势
- 双向遍历能力:支持前向和后向遍历,灵活性极高
- 操作效率高:插入、删除、头尾操作都是O(1)时间复杂度
- 边界统一:循环结构使得头尾操作逻辑统一,代码简洁
- 空间利用率:相对于性能提升,额外的指针开销是可接受的
适用场景
- 需要频繁在链表两端进行插入删除的操作
- 需要双向遍历的应用程序(如浏览器历史记录)
- 实现双向队列(Deque)等高级数据结构
- 需要循环缓冲区的场景