1. 双向链表的结构
对于单向链表和单向循环链表而言有一个共同的特点,就是链表的每个节点都只有一个指向后继节点的指针,通过这个指针我们就可以从前往后完成对链表的遍历。但是开弓没有回头箭,遍历到尾节点之后再想要回到头结点,是无能为力的。
1.1 双向链表的节点
为了克服链表单向性这一缺点,聪明的程序猿们便进行了改进,设计出了双向链表。双向链表(Doubly Linked List)是一种常见的数据结构,与单向链表不同的是,双向链表中的每个节点包含两个指针,一个指向前驱节点,一个指向后继节点。双向链表的这种特性使得我们可以从任意节点高效地向前或向后遍历。
假设双向链表的节点存储的是整形数据,那么该节点的定义如下:
// 定义一个节点,此为 C++ 语法
struct Node
{
int data = 0;
Node* next = nullptr;
Node* prev = nullptr;
};
双向链表的节点结构包含三个部分:
- 数据域(data):存储节点的值
- 前驱指针(prev):指向前一个节点
- 后继指针(next):指向后一个节点
在操作双向链表的时候通常会定义两个辅助指针:头节点指针和尾节点指针,头指针指向链表的第一个节点,尾指针指向链表的最后一个节点。
2. 双向链表的操作
2.1 链表的遍历
由于双向链表的节点中有前驱和后继指针,所以在遍历它的时候有两种方式:正向遍历和反向遍历。
正向遍历双向链表的过程(从头到尾):
- 从第一个数据节点开始,带头结点:从头结点的后继节点开始遍历
- 访问当前节点的数据,并通过next指针移动到下一个节点
- 重复步骤2,直到到达链表的末尾(即当前节点的next为nullptr)
反向遍历双向链表的过程(从尾到头):
- 从尾节点开始(前提条件:需要先找到尾节点)
- 访问当前节点的数据,并通过prev指针移动到上一个节点
- 重复步骤2,直到到达链表的头部,判定条件为:带头结点:当前节点为头结点
2.2 链表的插入
2.2.1 带头结点的插入
场景1:在头部和中间位置插入新节点
对于带头节点的链表而言,在头部插入节点就是把新的数据节点作为链表的第一个数据节点,它是头结点的后继节点。
在链表的头部和中间位置(pos)插入新节点需要分以下几步:
- 创建一个新的节点,并初始化,记作newNode
- 遍历链表找到pos-1位置的节点,记作preNode
- 将新节点的后继节点设置为pos位置的节点,也就是newNode->next = preNode->next
- 将新节点的前驱节点设置为pos-1位置的节点,也就是newNode->prev = preNode
- 重置pos位置节点的前驱,设置为newNode节点,也就是preNode->next->prev = newNode
- 重置preNode节点的后继,设置为newNode,即:preNode->next = newNode
温馨提示:第5、6步不能放到第3、4步之前处理。
下图是将新节点插入到链表的非第一个数据节点的位置:
下图是将新节点作为第一个数据节点插入到链表中:
场景2:在尾部插入新节点
在链表的尾部添加新节点就是让它作为原来的尾节点的后继,新节点的前驱是原来的尾节点。主要的操作步骤如下:
- 遍历链表并找到它的尾节点,记作tailNode
- 创建一个新的链表节点,记作newNode,并设置它的后继,有两种方式:
- newNode->next = nullptr
- newNode->next = tailNode->next
- 将newNode节点的前驱设置为tailNode,即:newNode->prev = tailNode
- 更新tailNode节点的后继节点,tailNode->next = newNode
2.3 链表的删除
2.3.1 带头结点的删除
场景1:删除头部和中间位置的节点
对于带头结点的链表而言,所谓的删除头部节点指的就是删除链表中的第一个数据节点。
删除头部和中间位置(pos)的节点的处理流程如下:
- 遍历链表,搜索链表的节点,找到要删除的节点的上一个节点(pos-1),记作preNode
- 通过preNode找到要删除的节点,记作delNode
- delNode = preNode->next
- 更新preNode节点的后继为pos+1位置的节点,即:preNode->next = delNode->next
- 更新pos+1位置节点的前驱为preNode,即:delNode->next->prev = prevNode
- 释放delNode节点指向的内存资源
下图为删除链表第一个数据节点的示意图:
下图为删除链表中间位置的数据节点的示意图:
场景2:删除尾部节点
删除链表尾部节点的处理流程如下:
- 遍历链表找到链表的倒数第二个(pos-1位置)节点,记作preNode
- 通过preNode找到要删除的节点,记作delNode
- delNode = preNode->next
- 让preNode节点的后继指针指向空,有两种处理方式
- preNode->next = nullptr
- preNode->next = delNode->next
- 释放delNode节点指向的内存资源