1 双向链表
1.1 概念与结构
双向链表(Doubly Linked List)是一种链式数据结构,它由一系列节点组成,每个节点包含三部分:数据部分(存储元素)、指向前一个节点的指针(prev)和指向下一个节点的指针(next)。与单向链表不同,双向链表的节点不仅可以通过下一个节点(next)进行访问,还可以通过前一个节点(prev)进行访问,因此它支持从两个方向进行遍历。
带头链表里面的头结点(head),实际为“哨兵位”,哨兵位结点实际不储存任何元素,只是站在这里“放哨”。
结点点对应的结构体代码:
struct Node {
int data; // 数据部分
struct Node* prev; // 指向前一个节点的指针
struct Node* next; // 指向下一个节点的指针
};
这种结构使得在任意节点的插入、删除以及遍历操作都非常高效。希望这个简要的图示和说明可以帮助你理解双向链表的基本概念!
1.2 性质特点
-
双向链接:每个节点不仅可以访问到下一个节点,还可以访问到前一个节点。这样,可以在两个方向上遍历链表。
-
动态大小:与数组不同,双向链表的大小不固定,可以根据需要动态添加或删除节点,内存使用更加灵活。
-
插入和删除操作:在双向链表中,插入和删除节点的操作通常比数组更高效,因为不会涉及到大规模的数据移动。只需修改几个指针即可完成操作。
-
存储开销:由于双向链表的每个节点需要额外存储一个指向前一个节点的指针,因此其存储开销比单向链表大。
-
访问效率:虽然双向链表支持双向遍历,但由于访问特定元素仍需从头或尾开始遍历,因此随机访问的效率较低,通常是O(n)时间复杂度。
-
适合多种应用场景:双向链表非常适合需要频繁插入和删除的应用场景,比如实现队列、栈、LRU缓存等。
-
需要更多的内存管理:由于每个节点都需要维护两个指针,管理和操作双向链表时需要更小心内存的分配与释放,以避免内存泄漏和悬挂指针。
总结来说,双向链表既具有灵活的动态特性,又能支持双向遍历,但在空间效率和随机访问方面有所折中。
1.3 双向链表的遍历
双链表是循环结构,要想遍历双链表,需要找到双链表的特殊条件:哨兵位。
双向链表为空的情况下只有一个哨兵位,如果连哨兵位都没有的话,这不是双链表而是单链表。
while(pcur != head)
{
printf("%d", pcur->data);
pcur = pcur->next;
}
2 双向链表的实现
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
//定义双向链表的结构
typedef int LTDataType;
typedef struct ListNode {
LTDataType data;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
void LTPrint(LTNode* phead);
//双向链表的初始化 plist &plist
//void LTInit(LTNode** pphead);//传地址:形参的改变影响实参
LTNode* LTInit();
//为了保持接口一致性,建议统一参数,都传一级:手动将实参置为NULL
void LTDesTroy(LTNode* phead);
//传二级:未保持接口一致性
//void LTDesTroy(LTNode** pphead);
//尾插
//phead结点不会发生改变,参数传一级
//phead结点发生改变,参数传二级
void LTPushBack(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//尾删
void LTPopBack(LTNode* phead);
//头删
void LTPopFront(LTNode* phead);
LTNode* LTFind(LTNode* phead, LTDataType x);
//在pos位置之后插⼊数据
void LTInsert(LTNode* pos, LTDataType x);
//删除pos位置的结点
void LTErase(LTNode* pos);
bool LTEmpty(LTNode* phead);
2.1 尾插
在实现尾插法时,需要根据链表是否为空的情况进行不同的处理:
判断链表是否为空
- 链表为空:如果
tail == NULL
,说明链表中没有元素。这时新节点既是头节点也是尾节点,因此将head
和tail
都指向该节点。 - 链表非空:如果链表不为空,说明链表已有元素。这时我们将新节点的
prev
指针指向当前尾节点,当前尾节点的next
指针指向新节点,然后将tail
更新为新节点。
链表为空:
链表非空:
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = buyNode(x);
newnode->prev = phead->prev;
newnode->next = phead;
phead->prev->next = newnode;
phead->prev = newnode;
}
2.2 头插
头插法的核心思想是:每次都将新节点插入到链表的头部,使得新节点成为链表的头节点。
判断链表是否为空,若为空则新节点成为头节点且尾节点;若非空,则新节点成为新的头节点,更新链表的头部指针和相关节点的指针。
判断链表是否为空
- 链表为空:如果
head == NULL
,说明链表没有任何节点。此时新节点既是头节点也是尾节点,因此我们将head
和tail
都指向该节点。 - 链表非空:如果链表非空,则将新节点插入到链表的头部。我们需要做以下操作:
- 新节点的
next
指向当前的头节点。 - 当前头节点的
prev
指向新节点。 - 将
head
更新为新节点。
- 新节点的
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = buyNode(x);
newnode->prev = phead;
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
}
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
2.3 尾删
尾删操作实现
尾删操作是指删除链表的尾节点,并更新相应的指针。对于双向链表,尾删的操作分为几种情况:
- 链表为空:如果
head == NULL
,说明链表为空,无法删除节点。则先断言该节点不为空。 - 链表只有一个节点:如果
head == tail
,说明链表中只有一个节点,删除该节点后,head
和tail
都应指向NULL
。 - 链表有多个节点:如果链表中有多个节点,我们需要将尾节点的前一个节点更新为新的尾节点,并且更新尾指针。
删除尾节点的步骤
- 如果
list->tail
是链表的最后一个节点,则需要先判断链表是否为空。 - 如果链表非空,则需要更新
tail
指针,指向倒数第二个节点。 - 更新尾节点的
next
指针为NULL
,并释放原尾节点。
//尾删
void LTPopBack(LTNode* phead)
{
assert(!LTEmpty(phead));
LTNode* del = phead->prev;
//phead del->prev(d2) del(d3)
del->prev->next = phead;
phead->prev = del->prev;
free(del);
del = NULL;
}
2.4 头删
头删操作实现
头删操作是指删除链表的第一个节点,并更新相应的指针。对于双向链表,头删操作分为以下几种情况:
- 链表为空:如果
head == NULL
,说明链表为空,无法删除节点。 - 链表只有一个节点:如果
head == tail
,说明链表中只有一个节点,删除该节点后,head
和tail
都应指向NULL
。 - 链表有多个节点:如果链表中有多个节点,我们需要将头节点的
next
节点的prev
指针更新为NULL
,并且更新head
指针。
删除头节点的步骤
- 如果链表为空(
head == NULL
),直接返回。 - 如果链表只有一个节点(
head == tail
),删除该节点后,head
和tail
都应设置为NULL
。 - 如果链表有多个节点,将头节点的
next
节点的prev
指针设置为NULL
,并将head
指针指向下一个节点。
//头删
void LTPopFront(LTNode* phead)
{
assert(!LTEmpty(phead));
LTNode* del = phead->next;
//phead del del->next
del->next->prev = phead;
phead->next = del->next;
free(del);
del = NULL;
}
2.5 在指定位置之后插入数据
在指定位置后插入数据时,首先我们需要找到目标位置的节点,然后将新节点插入到该节点之后。插入的步骤如下:
- 插入的位置是链表的头部:在这种情况下,插入节点需要成为新的头节点,更新头节点的指针。
- 插入的位置是链表的尾部:如果目标节点是尾节点,新的节点将成为新的尾节点,更新尾指针。
- 插入的位置是中间某个节点:需要调整目标节点的
next
和prev
指针来插入新节点。
插入操作步骤
- 找到目标节点:从链表的头部开始,找到指定位置的节点。
- 创建新节点:分配内存并填充数据。
- 调整指针:更新目标节点的
next
和新节点的prev
指针,最后将新节点的next
指向目标节点的下一个节点(如果有)。
//在pos位置之后插⼊数据
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = buyNode(x);
//pos newnode pos->next
newnode->prev = pos;
newnode->next = pos->next;
pos->next->prev = newnode;
pos->next = newnode;
}
2.6 删除指定位置的结点
删除操作的核心是:
- 遍历链表找到目标位置的节点。
- 调整相邻节点的指针,将目标节点从链表中移除。
- 处理三种特殊情况:
- 删除链表的头节点。
- 删除链表的尾节点。
- 删除中间节点。
删除操作的步骤
-
目标节点是头节点:
- 更新头指针,使其指向下一个节点。
- 如果链表只包含一个节点,删除后需要将尾指针也设置为
NULL
。
-
目标节点是尾节点:
- 更新尾指针,使其指向前一个节点。
-
目标节点是中间节点:
- 更新前一个节点的
next
指针,指向目标节点的next
。 - 更新下一个节点的
prev
指针,指向目标节点的prev
。
- 更新前一个节点的
//删除pos位置的结点
void LTErase(LTNode* pos)
{
assert(pos);
//pos->prev pos pos->next
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}
2.7 销毁
销毁双向链表的核心操作是释放每个节点占用的内存。我们从链表的头节点开始,逐个释放节点。每次访问一个节点时,我们保存下一个节点的指针,释放当前节点,然后移动到下一个节点。销毁过程中需要特别注意:
- 遍历链表时需要小心处理指针。
- 最后,链表的头指针和尾指针需要设置为
NULL
,表示链表已被销毁。
void LTDesTroy(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
free(phead);
phead = NULL;
}
最后附上完整代码:
#include"List.h"
LTNode* buyNode(LTDataType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{
perror("malloc fail!");
exit(1);
}
node->data = x;
node->next = node->prev = node;
return node;
}
//双向链表的初始化
//void LTInit(LTNode** pphead)
//{
// *pphead = buyNode(-1);
//}
LTNode* LTInit()
{
LTNode* phead = buyNode(-1);
return phead;
}
void LTDesTroy(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
free(phead);
phead = NULL;
}
void LTPrint(LTNode* phead)
{
LTNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d -> ", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = buyNode(x);
//newnode phead->prev(尾结点) phead
newnode->prev = phead->prev;
newnode->next = phead;
phead->prev->next = newnode;
phead->prev = newnode;
}
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = buyNode(x);
//newnode phead phead->next
newnode->prev = phead;
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
}
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
//尾删
void LTPopBack(LTNode* phead)
{
assert(!LTEmpty(phead));
LTNode* del = phead->prev;
//phead del->prev(d2) del(d3)
del->prev->next = phead;
phead->prev = del->prev;
free(del);
del = NULL;
}
//头删
void LTPopFront(LTNode* phead)
{
assert(!LTEmpty(phead));
LTNode* del = phead->next;
//phead del del->next
del->next->prev = phead;
phead->next = del->next;
free(del);
del = NULL;
}
LTNode* LTFind(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
//在pos位置之后插⼊数据
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = buyNode(x);
//pos newnode pos->next
newnode->prev = pos;
newnode->next = pos->next;
pos->next->prev = newnode;
pos->next = newnode;
}
//删除pos位置的结点
void LTErase(LTNode* pos)
{
assert(pos);
//pos->prev pos pos->next
pos->next->prev = pos->prev;
pos->prev->next = pos->next;
free(pos);
pos = NULL;
}