1.前言
数据无疑是当今信息时代的瑰宝。而作为程序猿,我们的职责之一就是管理和操作这些数据。要想在这片广袤的“数据山”中纵横驰骋,我们就必须有一辆性能强大的过山车——即高效的数据结构。今天,我们将继续探索这辆神奇的“过山车”,并尝试一起来回穿梭于数据的世界!
在前两篇博客中,我们学习了顺序表、单链表的基本概念及其应用。然而,顺序表和单链表的某些操作效率较低,例如插入和删除操作,这就导致了它们在某些场景下的表现不尽如人意。而双向链表(Doubly Linked List)正好弥补了这一缺陷,它的特殊结构使得插入和删除操作变得无比轻松。
本篇博客,我们将挖掘双向链表的奥秘。让这辆性能卓越的过山车带我们飞驰在“数据山”上吧!
2.双向链表的建立与优点
双向链表的特点和性质可是非常有趣的,它不仅能够像普通的链表一样顺着链表头一个个遍历节点,还能够在链表中从后往前遍历,并且插入和删除节点的操作也非常方便。
首先,我们来看看双向链表的定义。与普通链表一样,它也是由一系列节点组成的。每个节点中存储着数据元素,以及指向前驱节点和后继节点的指针。这个特点就决定了它可以双向遍历,而不像单向链表只能从一个方向遍历。
我们需要定义一个结构体,来存储链表节点的值和指向前后节点的指针。像这样:
typedef char ElemType;
typedef struct DuLNode {
ElemType data;
struct DuLNode* prior;
struct DuLNode* next;
}DuLinkList;
双向链表的插入和删除操作是其最大的特点和优点,这是因为每个节点中都保存了指向前驱和后继节点的指针,因此插入和删除节点时并不需要像普通链表那样需要遍历到要操作的节点位置。只需要修改前后节点的指针,就可以快速地完成这个操作。这使得双向链表在需要频繁插入或删除节点的场景中非常高效,例如缓存的实现。
此外,双向链表中的头结点和尾节点都有指针进行记录,从而方便双向链表的遍历。如果从头结点开始,那么与普通链表一样,只需要向一个方向遍历即可;如果从尾节点开始,那么可以通过从尾节点的前驱指针往前遍历,从而实现从另一个方向遍历链表。
这就是双向链表的性质和特点啦!它不仅可以双向遍历,而且插入、删除节点非常方便。对于需要频繁的插入、删除操作的使用场景,它可以优于普通链表。赶快上车,让双向链表带你去飞驰吧!
3. 双向链表的基本操作
头插法建表
void CreateListF_DuL(DuLinkList*& L, ElemType a[], int n) {
DuLinkList* s; // 定义一个指向新节点的指针
int i;
L = (DuLinkList*)malloc(sizeof(DuLinkList)); // 创建头结点
L->prior = L->next = NULL;
for (i = 0;i < n;i++) {
s = (DuLinkList*)malloc(sizeof(DuLinkList)); // 创建新节点
s->data = a[i];
s->next = L->next; // 将新节点的 next 指向原链表的第一个节点
if (L->next != NULL)
L->next->prior = s; // 如果已经有节点,将其前驱设置为新节点
L->next = s; // 将头结点的 next 指向新节点
s->prior = L; // 将新节点的前驱指向头结点
}
}
函数采用头插法建立链表,即:先采用数组中的第一个元素创建一个头结点,然后将数组中的其余元素按顺序插入到链表的头部(链表中头结点的下一个节点的位置)。
这段函数的重点难点有两个,分别是:
1. 双向链表的创建
双向链表需要考虑节点的前驱和后继,而头结点通常看作是链表的一部分,因此需要在创建头结点时将其前驱和后继指针都设置为 NULL。在创建新节点时,需要注意将其前驱指针指向头结点。
2. 双向链表的头插法
链表的头插法是一种比较常见的链表插入方式,一般不需要借助任何辅助变量即可完成操作。需要注意的是,在进行头插法操作时,需要先将要插入的节点的 next 指针指向原链表的第一个节点,即头结点的下一个节点。另外,如果原链表不为空,需要将原链表中第一个节点的前驱指针设置为新插入的节点。在进行头插法操作后,新节点就被插入到了原链表的头部位置。
尾插法建表
void CreateListR_DuL(DuLinkList*& L, ElemType a[], int n) {
DuLinkList* s, * r; // 定义两个指向新节点和尾节点的指针
int i;
L = (DuLinkList*)malloc(sizeof(DuLinkList)); // 创建头结点
L->prior = L->next = NULL;
r = L; // 将尾指针指向头结点
for (i = 0;i < n;i++) {
s = (DuLinkList*)malloc(sizeof(DuLinkList)); // 创建新节点
s->data = a[i];
r->next = s; // 将新节点插入到链表的尾部
s->prior = r;
r = s; // 更新尾指针
}
r->next = NULL; // 尾指针指向 NULL,表示链表结束
}
总体来说,该函数和上一个函数相似,都是用来创建一个带头结点的双向链表。不同之处在于,该函数采用尾插法来建立链表,即将新节点插入到链表的尾部。在进行尾插法操作时,需要在节点的 next 指针指向 NULL,表示链表的结束。退而求其次,头结点的 next 也指向 NULL,这就是一个无元素的双向链表。
在该函数中,需要定义两个指针 s 和 r,其中 s 是指向新节点的指针,r 是指向链表尾部节点的指针。在循环过程中,将新节点插入到尾节点之后,然后更新尾指针,最后让尾指针指向 NULL,表示链表的结束。
在i位置插入元素
bool ListInsert_DuL(DuLinkList& L, int i, ElemType e) {
DuLinkList p = L; // 定义指针p指向头节点L
int j = 0; // 计数器j初始化为0
while (j < i && p != NULL) { // 寻找第i个节点
j++;
p = p->next;
}
if (p == NULL) {
return false; // 位置i不合法,插入失败
} else {
DuLinkList s = (DuLinkList)malloc(sizeof(DuLNode)); // 创建新节点s
s->data = e; // 赋值新节点数据
s->prior = p->prior; // 将s的前驱指针指向p的前驱
s->next = p; // 将s的后继指针指向p
p->prior->next = s; // 将p的前驱指针指向s
p->prior = s; // 将p的前驱指针指向s
return true; // 插入成功
}
}
由于是双向链表,要注意前驱指针和后继指针的处理:
将s的前驱指针指向p的前驱,将s的后继指针指向p,将p的前驱指针指向s,最后将p的前驱指针指向s。
删除第i个元素
bool ListDelete_DuL(DuLinkList*& L, int i, ElemType& e) {
// j用于计数,p用于记录当前节点
int j = 0;
DuLinkList* p = L;
// 找到指定位置i的节点
while (j < i && p != NULL) {
j++;
p = p->next;
}
// 如果找不到指定位置i的节点,删除失败
if (p == NULL)
return false;
else {
e = p->data; // 拿到要删除节点的值
// 下面两句是将指定节点从双向链表中摘除
p->prior->next = p->next;
p->next->prior = p->prior;
free(p); // 释放指定节点的空间
return true; // 删除成功
}
}
双向链表的删除操作需要将指定节点的前驱节点的next指针指向当前节点的后继节点,同时将当前节点的后继节点的prior指针指向前驱节点,这样就将指定的节点从链表中摘除。在节点被摘除出链表后,需要释放其占用的空间,以避免内存泄露!
4.作者碎碎念
马上期末考,在我自己赶进度的同时也会赶进度更新博客的,毕竟我也要靠自己的博客来复习
(doge)。