前言
在数据结构的学习中,双链表是一种非常重要的线性数据结构。它通过每个节点包含两个指针,一个指向其前驱结点,另一个指向其后继结点,从而提供了双向的访问能力。这种特性使得双链表在某些场景下比单链表更加灵活和高效。本文将详细介绍如何基于带头结点的双链表来实现线性表List1,并演示一些基本操作,如初始化、判空、求表长、插入、删除、查找、修改、遍历以及销毁等。总体设计思路图见下:
一、双链表的定义
首先,我们定义了双链表的节点类型DLinkNode,每个节点由三部分组成:存储的数据data,指向前驱结点的指针prior,以及指向后继结点的指针next。
typedef int ElemType;
typedef struct DNode {
ElemType data;
struct DNode *prior; // 指向前驱结点
struct DNode *next; // 指向后继结点
} DLinkNode; // 声明双链表结点类型
二、双链表的基本操作
2.1 初始化(InitList)
初始化线性表时,我们需要创建一个头结点作为整个链表的起点。头结点不用于存储实际数据,而是方便对链表进行操作。该结点的头指针和尾指针均为空。即:
代码为:
void InitList(DLinkNode*& L) {
L = (DLinkNode*)malloc(sizeof(DLinkNode)); // 创建头结点
L->prior = L->next = NULL; // 头结点的前后指针均为空
}
2.2 判空(ListEmpty)
判断链表是否为空只需检查头结点的next指针是否为NULL。
bool ListEmpty(DLinkNode* L) {
return(L->next == NULL);
}
2.3 求表长(ListLength)
为了获取链表长度,我们需要从头到尾遍历整个链表,同时记录经过的结点个数。即:
代码为:
int ListLength(DLinkNode* L) {
DLinkNode* p = L;
int i = 0; // 计数器设置为0
while (p->next != NULL) { // 找尾结点p
i++; // i对应结点p的序号
p = p->next;
}
return(i); // 返回链表长度
}
2.4 插入(ListInsert)
插入元素需要找到插入位置的前一个结点,然后创建新结点并调整指针。即:
代码为:
bool ListInsert(DLinkNode*& L, int i, ElemType e) {
int j = 0;
DLinkNode* p = L, * s;
if (i <= 0) return false; // 检查i值合法性
while (j < i - 1 && p != NULL) { // 查找第i-1个结点p
j++;
p = p->next;
}
if (p == NULL) return false; // 未找到第i-1个结点
else { // 找到了第i-1个结点p
s = (DLinkNode*)malloc(sizeof(DLinkNode)); // 创建新结点s
s->data = e;
s->next = p->next; // 将结点s插入到结点p之后
if (p->next != NULL) p->next->prior = s;
s->prior = p;
p->next = s;
return true;
}
}
2.5 删除(ListDelete)
删除元素同样需要定位到待删除结点的前一个结点,然后调整指针以跳过该结点。即:
代码为:
bool ListDelete(DLinkNode*& L, int i, ElemType& e) //删除第i个元素
{
int j = 0;
DLinkNode* p = L, * q; //p指向头结点,j设置为0
if (i <= 0) return false; //i错误返回假
while (j < i - 1 && p != NULL) //查找第i-1个结点p
{
j++;
p = p->next;
}
if (p == NULL) //未找到第i-1个结点
return false;
else //找到第i-1个节p
{
q = p->next; //q指向第i个结点
if (q == NULL) //当不存在第i个结点时返回false
return false;
e = q->data;
p->next = q->next; //从双链表中删除结点q
if (p->next != NULL) //若p结点存在后继结点,修改其前驱指针
p->next->prior = p;
free(q); //释放q结点
return true;
}
}
下面操作中的步骤实际上在上面操作中均以实现,且代码注释很详细,故不再讲解。
2.6 查找 (GetElem && LocateElem)
bool GetElem(DLinkNode* L, int i, ElemType& e) //求线性表中第i个元素值
{
int j = 0;
DLinkNode* p = L;
if (i <= 0) return false; //i错误返回假
while (j < i && p != NULL) //查找第i个结点p
{
j++;
p = p->next;
}
if (p == NULL) //没有找到返回假
return false;
else //找到了提取值并返回真
{
e = p->data;
return true;
}
}
int LocateElem(DLinkNode* L, ElemType e) //查找第一个值域为e的元素序号
{
int i = 1;
DLinkNode* p = L->next;
while (p != NULL && p->data != e) //查找第一个值域为e的结点p
{
i++; //i对应结点p的序号
p = p->next;
}
if (p == NULL) //没有找到返回0
return(0);
else //找到了返回其序号
return(i);
}
2.7 遍历(DispList)
void DispList(DLinkNode* L) //输出线性表
{
DLinkNode* p = L->next;
while (p != NULL)
{
printf("%c ", p->data);
p = p->next;
}
printf("\n");
}
2.8 销毁 (DestroyList)
void DestroyList(DLinkNode*& L) //销毁线性表
{
DLinkNode* pre = L, * p = pre->next;
while (p != NULL)
{
free(pre);
pre = p; //pre、p同步后移一个结点
p = pre->next;
}
free(p);
}
三、编写主函数
最后汇总上述函数,测试功能,可编写如下主函数:
int main()
{
DLinkNode* L;
ElemType e;
printf("顺序表的基本运算如下:\n");
printf(" (1)初始化顺序表L\n");
InitList(L);
printf(" (2)依次插入a,b,c,d,e元素\n");
ListInsert(L, 1, 'a');
ListInsert(L, 2, 'b');
ListInsert(L, 3, 'c');
ListInsert(L, 4, 'd');
ListInsert(L, 5, 'e');
printf(" (3)输出顺序表L:");
DispList(L);
printf(" (4)顺序表L长度:%d\n", ListLength(L));
printf(" (5)顺序表L为%s\n", (ListEmpty(L) ? "空" : "非空"));
if (GetElem(L, 3, e) == false)
printf(" (6)顺序表L的第3个元素:查找失败\n");
else
printf(" (6)顺序表L的第3个元素:%c\n", e);
printf(" (7)元素h的位置:%d\n", LocateElem(L, 'h'));
printf(" (8)在第4个元素位置上插入f元素\n");
if (ListInsert(L, 4, 'f') == false)
printf(" 插入失败\n");
printf(" (9)输出顺序表L:");
DispList(L);
printf(" (10)删除L的第3个元素\n");
if(ListDelete(L, 3, e)==false)
printf(" 删除失败");
printf(" (11)输出顺序表L:");
DispList(L);
printf(" (12)释放顺序表L\n");
DestroyList(L);
return 1;
}
测试结果
正常的运行结果测试见下图:
在(6)中,当查找的元素序号不在链表中时,会输出相应信息,见下图:
在(7)中,如果查找的元素不在链表中,会输出对应的序号是0,见下图:
在(8)中,如果插入的序号不对,也会提示相应信息,即插入失败,见下图:
在(10)中,如果删除的序号不对,也会提示相应信息,即删除失败,见下图: