1.基本概念
1.1 什么是双向链表
双向链表(Doubly Linked List)是一种常见的链表数据结构。它与普通链表的区别在于,每个节点都有两个指针,一个指针指向前一个节点,一个指针指向后一个节点,因此可以从任意一个节点开始,双向遍历整个链表。
双向链表的节点通常由三部分组成:数据部分(存储节点的值)、前驱指针(指向前一个节点的指针)和后继指针(指向后一个节点的指针)。相比于单向链表,双向链表可以更方便地进行正反向的遍历以及节点的插入、删除操作。
双向链表的优点是在某些特定场景下可以更高效地操作链表,缺点是相比于单向链表,需要额外的空间来存储每个节点的前驱指针。
双向链表的特点:
1、双向链表中的每个节点都有两个指针,可以向前或向后遍历。
2、可以从任意节点开始向前或向后遍历整个链表。
3、插入和删除节点的操作相对容易,只需要调整节点的前驱和后继指针即可。
4、对于双向链表,可以更轻松地实现双向循环链表,即首尾节点相连。
双向链表的常见操作:
1、遍历链表:可以从头节点开始,按照后继指针依次访问每个节点。或者从尾节点开始,按照前驱指针依次访问每个节点。
2、插入节点:在给定位置之前或之后插入一个新节点,只需要调整前后节点的指针即可。
4、删除节点:删除给定位置上的节点,同样通过调整前后节点的指针来实现。
5、查找节点:可以通过遍历链表来查找特定值或者特定位置上的节点。
注意:在使用双向链表时需要注意指针的正确性和更新,以避免出现指针丢失和内存泄漏等问题。
1.2 双向链表与单向链表的区别
双向链表与单向链表的主要区别在于节点中指针的个数和指向方式:
1. 指针个数:双向链表每个节点有两个指针,分别指向前一个节点和后一个节点;而单向链表每个节点只有一个指针,指向下一个节点。
2. 遍历方式:双向链表可以从任意节点开始向前或向后遍历整个链表,而单向链表只能从头节点开始顺序遍历整个链表。
3. 插入和删除操作:双向链表的插入和删除操作相对方便,因为可以直接调整前驱和后继节点的指针;而单向链表在进行插入和删除时,需要更多的操作来调整节点之间的链接关系。
4. 空间占用:双向链表相较于单向链表需要额外的空间来存储每个节点的前驱指针。这在一些特定场景下可能会带来一定的开销。
5. 双向性:双向链表可以在节点内部通过前驱指针和后继指针来实现双向的遍历和操作。这使得在某些场景下,双向链表更加方便和高效,例如需要反向遍历链表或者需要在给定节点处进行插入和删除操作。
6. 双向循环性:双向链表可以通过连接首尾节点的前驱和后继指针来形成一个双向循环链表。这使得链表在某些场景下更具灵活性,例如需要循环遍历链表或者实现循环队列等。
7. 内存占用:相对于单向链表,每个节点额外存储一个前驱指针和一个后继指针,使得双向链表在存储上需要更多的内存空间。这是一个需要考虑的因素,尤其是在链表节点数量较大或内存有限的情况下。
8. 双向链表的前向遍历与后向遍历:由于双向链表具有前驱和后继指针,可以方便地进行正向和反向的遍历操作。而单向链表只能从头节点开始进行顺序遍历,无法直接进行反向遍历。
9. 删除操作的效率:相对于单向链表,在双向链表中删除给定节点的效率更高。由于双向链表可以直接通过前驱和后继指针找到目标节点的前后节点,所以删除操作只需要修改前后节点的指针即可,而单向链表需要遍历找到目标节点的前一个节点,才能进行删除操作。
10. 双向链表支持双向迭代器:由于双向链表具有双向性,可以轻松地实现双向迭代器。这使得在某些情况下,可以更加方便地进行双向迭代操作,如双向搜索算法或需要反向遍历的数据结构。
总之,双向链表相较于单向链表具有更强的遍历和操作能力,但在存储上需要额外的空间开销。根据具体需求和场景的不同,选择适合的链表类型是根据实际情况进行权衡和取舍。
我们实际中最常用的链表结构包括无头单向非循环链表和带头双向循环链表两种,无头单向非循环链表的结构比较简单,一般不会用来单独存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等。无头单向非循环链表详细讲解:无头单向非循环链表代码实现
带头双向循环链表的结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构都是带头双向循环链表。另外这个结构虽然结构比较复杂,但是使用代码实现以后会发现该结构会带来很多优势,实现反而简单了。
2.带头双向循环链表的实现
2.1 功能展示
typedef int ListDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
ListDataType data;
}ListNode;
2.2 定义链表结点
typedef struct ListNode
{
struct ListNode* next;//指向下一个结点
struct ListNode* prev;//指向上一个结点
ListDataType data;//结点中存储的数据
}ListNode;
2.3 创建双向链表的结点
//1、创建链表结点
ListNode* CreateListNode(ListDataType val)
{
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
if (node == NULL)
{
//判断结点是否创建成功
perror("malloc fail");
exit(-1);
}
node->prev = NULL;
node->next = NULL;
node->data = val;
return node;
}
2.4 初始化链表
初始化带头双向循环链表即使哨兵位结点(头节点)前后指针中存储的位置为pHead本身。
//2、链表初始化
ListNode* LinkListInite()
{
ListNode* pHead = CreateListNode(0);
pHead->next = pHead;
pHead->prev = pHead;
return pHead;
}
2.5 打印链表
//3、打印链表
void ListPrint(ListNode* pHead)
{
assert(pHead);
ListNode* cur = pHead->next;
while (cur != pHead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
2.6 链表的尾插
带头双向循环链表尾插即在head前插入节点,如图所示。
//4、链表尾插
//ListPushBack函数不会改变pHead指针,
//故传入指针本身,而不是传入指针的地址。
void ListPushBack(ListNode* pHead, ListDataType val)
{
assert(pHead);
//定义临时指针tail指向原双向链表的尾结点,将其地址备份,
//此时无需考虑指向改变的前后顺序问题,因为地址不会丢失,
//直接依次完成地址和前驱后继指针的指向改变即可。
ListNode* tail = pHead->prev;
ListNode* newNode = CreateListNode(val);
tail->next = newNode;
newNode->prev = tail;
newNode->next = pHead;
pHead->prev = newNode;
}
2.7 链表的尾删
链表的尾删即为删除head的前一个节点,当链表已经被删空,即只剩下头结点head时,再进行删除操作会出现错误,此时应该进行提醒并终止程序。
//5、链表尾删
void ListPopBack(ListNode* pHead)
{
assert(pHead);
//如果链表删成空的了,即只剩下头结点了,头结点中的前驱和后继指针均指向自己;
//此时继续删除链表结点,就断言报错,当报错时我们就知道链表已经为空了。
assert(pHead->next != pHead);
ListNode* tail = pHead->prev;
ListNode* tailPrev = tail->prev;
tailPrev->next = pHead;
pHead->prev = tailPrev;
free(tail);
//一般来说,指针tail在使用free函数释放后,需要置空;
//但是这里不置空影响也不大,因为这里tail是局部变量,
//声明周期在函数ListPopBack内,该函数结束,指针tail也随之销毁
//tail = NULL;
}
2.8 链表的头插
头部插入即为head后面插入一个节点,如下图所示。
//6、链表头插
void ListPushFront(ListNode* pHead, ListDataType val)
{
assert(pHead);
ListNode* newNode = CreateListNode(val);
ListNode* next = pHead->next;
pHead->next = newNode;
newNode->prev = pHead;
newNode->next = next;
next->prev = newNode;
}
2.9 链表头删
//7、链表头删
void ListPopFront(ListNode* pHead)
{
assert(pHead);
assert(pHead->next != pHead);
ListNode* next = pHead->next;
ListNode* nextNext = next->next;
pHead->next = nextNext;
nextNext->prev = pHead;
free(next);
}
2.10 在链表中查找某个结点
//8、查找某个结点
ListNode* ListNodeFind(ListNode* pHead, ListDataType val)
{
assert(pHead);
ListNode* cur = pHead->next;
while (cur!= pHead)
{
if (cur->data == val)
return cur;
cur = cur->next;
}
return NULL;
}
2.11 在pos位置之前插入一个结点
//9、在pos位置之前插入一个结点
void ListNodeInsert(ListNode* pos, ListDataType val)
{
assert(pos);
ListNode* newNode = CreateListNode(val);
ListNode* posPrev = pos->prev;
posPrev->next = newNode;
newNode->prev = posPrev;
newNode->next = pos;
pos->prev = newNode;
}
2.12 删除pos位置的结点
//10、删除pos位置的结点
void ListNodeErase(ListNode* pos)
{
assert(pos);
ListNode* posPrev = pos->prev;
ListNode* posNext = pos->next;
free(pos);
posPrev->next = posNext;
posNext->prev = posPrev;
}
2.13 清空链表
//11、清理掉所有结点,保留头结点,链表还可以使用
void ListClear(ListNode* pHead)
{
assert(pHead);
ListNode* cur = pHead->next;
while (cur != pHead)
{
ListNode* next = cur->next;
free(cur);
cur = next;
}
//头结点指向自己
pHead->next = pHead;
pHead->prev = pHead;
}
2.14 销毁链表
//12、删除链表--彻底删除,不保留头结点
void ListDestroy(ListNode* pHead)
{
assert(pHead);
ListClear(pHead);
free(pHead);
}
2.15 尾插、头插、尾删、头删的优化
由于函数ListNodeInsert()实现了在链表的任意位置插入结点,函数ListNodeErase()实现了在链表的任意位置删除结点,那就可以对尾插、头插、尾删、头删等函数进行复用,简化代码。
2.15.1 尾插
void ListPushBack(ListNode* pHead, ListDataType val)
{
assert(pHead);
ListNodeInsert(pHead, val);
}
2.15.2 头插
void ListPushFront(ListNode* pHead, ListDataType val)
{
assert(pHead);
ListNodeInsert(pHead->next, val);
}
2.15.3 尾删
void ListPopBack(ListNode* pHead)
{
assert(pHead)
ListNodeErase(pHead->prev);
}
2.15.4 头删
void ListPopFront(ListNode* pHead)
{
assert(pHead);
assert(pHead->next != pHead);
ListNodeErase(pHead->next);
}
完整代码:带头双向循环链表的实现