前面关于数据结构的文章中介绍了数据结构中的单链表。并且实现了单链表增删查改各项功能。本文来介绍双向循环链表。
链表都是由一个个的结点构成的,所以,在介绍双向循环链表的整体结构之前,先来了解双向循环链表的结点的特点,其中,单链表的单个结点中包含了一个用于存储数据信息的数据域和用于存储下一个结点地址的指针域,即:
对双向循环链表的结点,虽然也是由数据域和指针域构成,但是,双向循环链表的指针域中,分别存储了上一个结点的地址和下一个结点的地址,为了后续方便表示,将存储上一个结点地址的指针命名为,存储下一个结点的指针命名为,双向循环链表单个结点的结构如下:
对于双向循环链表,从名字就可以知道它的两个特点,即:双向,循环。其整体结构如下:
为了方便解释,将下面给出的链表的第一个结点的指针变量命名为,将最后一个结点的指针变量命名为
每个结点的都保存了指向下一个结点的地址。但是中保存的地址是指向头结点。
每个结点的都保存了指向上一个结点的地址,但是中保存的地址指向尾结点。
双向循环链表的结点结构、以及结点与结点间的关系相对于单链表虽然较为复杂。但是双向循环链表在实现增删查改的各项功能时相比链表会较为简单。
与单链表相同,双向链表中的结点需要同时存储多种类型的数据。所以利用结构体来完成对于双向循环链表单个结点结构的表示:
typedef int ListDataType;
typedef struct ListNode
{
struct ListNode* prev;
ListDataType Data;
struct ListNode* next;
}LTNode;
单个结点的大体结构已经用结构体来进行表示,但是,为了灵活的增加新的结点,再定义一个函数来实现增加一个新的结点,并且根据需求对结点中的数据元素进行更改:
LTNode* BuyListNode(ListDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL)
{
perror("malloc");
exit(-1);
}
newnode->Data = x;
newnode->prev = NULL;
newnode->next = NULL;
return newnode;
}
上面给出的用于增加新结点的函数的返回类型是返回一个地址。对于次函数的返回值类型为什么返回地址,将在下面的内容中进行解释。
再创建完后,根据上面给出的双向循环的链表结构不难发现:链表的第一个结点并不存储整型元素,只是用于链接下一个结点和最后一个结点。所以,介于头结点与其他结点的不同,下面将给出一个初始化函数来完成对头结点的创建。同时,为了体现循环与双向这两个性质,头结点的两个指针都分别指向头结点自身,结构如下:
为了解释为什么的返回值返回一个地址,下面给出两种方法来定义初始化头结点函数:
方法1:
//用于初始化头结点:
LTNode* ListInit()
{
LTNode*phead = BuyListNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
方法2:
void ListInit(LTNode* phead)
{
phead = BuyListNode(-1);
phead->next = phead;
phead->prev = phead;
}
方法相对于方法,多了用于接受外部实际参数的形式参数。然而,方法这种创建方式是错误的。这是因为,在方法所编写的函数中,这一行改变了形式参数中的内容。在对于单链表尾插功能的实现中就多次提到,想要通过形参改变函数外部的内容,形式参数必须是二级指针。
而对于方法,虽然形式参数是一级指针,并且同样改变了形参的内容,但是最后的返回值返回了形式参数中存储的地址,只需要在外部定义一个指针用于接受函数的返回值即可。不需要再创建二级指针。
在打印链表中结点的内容时,需要遍历链表。但是双向循环链表在遍历时,循环的条件与单链表中检测当前结点中存储的地址是否为不同。双向循环链表的头结点不再打印的范围内。所以遍历的结束条件是检测当前结点是否为头结点,代码如下:
//用于打印链表各个结点的内容:
void LTPrint(LTNode* phead)
{
LTNode* cur = phead->next;
printf("phead<->");
while (cur != phead)
{
printf("%d<->", cur->Data);
cur = cur->next;
}
}
如上图所示,假设为链表尾结点的地址,是待插入的结点。当 进行尾插操作时:首先需要找到尾部结点。对于寻找尾部结点,就体现了双向循环链表的优点:不需要再像单链表中通过遍历来寻找尾部结点。而是就指向了尾部结点。
在找到了尾部结点后,尾插新的结点的过程可以分为两步:
1. 让与建立联系。
2.此时成为链表中新的尾部结点,需要更改与,来建立尾结点与头结点的关系,插入后效果如下:
代码如下:
//用于尾插结点:
void LTPushBack(LTNode* phead, ListDataType x)
{
assert(phead);
LTNode* tail = phead->prev;
LTNode* newnode = BuyListNode(x);
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
下面给出一个测试函数,来测试尾插功能:
void test1()
{
LTNode* plist = ListInit();
printf("测试尾插功能:");
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPrint(plist);
}
测试结果如下:
对于尾删功能的实现,也可以分成两步:
1.(为了后续方便说明,将倒数第二个结点命名为),让建立和头结点的联系,同时达到切断与联系的效果。
2. 结点
代码如下:
//用于尾删结点:
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(phead->prev != phead);
LTNode* tail = phead->prev;
LTNode* tailprev = tail->prev;
tailprev->next = phead;
phead->prev = tailprev;
free(tail);
}
测试函数如下:
void test1()
{
LTNode* plist = ListInit();
printf("测试尾插功能:");
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPrint(plist);
printf("\n");
printf("测试尾删功能:");
LTPopBack(plist);
LTPopBack(plist);
LTPrint(plist);
}
对于头插功能的实现,可以分下面的步骤来完成:
1.建立与的联系
2.建立与的联系
代码如下:
//用于实现头插:
void LTPushFront(LTNode* phead, ListDataType x)
{
assert(phead);
assert(phead->next != phead);
LTNode* newnode = BuyListNode(x);
LTNode* tail = phead->next;
newnode->next = tail;
tail->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
}
测试函数如下:
void test1()
{
LTNode* plist = ListInit();
printf("测试尾插功能:");
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPrint(plist);
printf("\n");
printf("测试尾删功能:");
LTPopBack(plist);
LTPopBack(plist);
LTPrint(plist);
printf("\n");
printf("用于测试头插:");
LTPushFront(plist, 10);
LTPushFront(plist, 20);
LTPrint(plist);
}
对于头删功能的实现,实际上就是反向执行头插的功能:
1. 将头指针与建立联系。
2.结点
代码如下:
//用于实现头删功能:
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* tail = phead->next;
LTNode* tailnext = tail->next;
phead->next = tailnext;
tailnext->prev = phead;
free(tail);
}
测试函数如下:
void test2()
{
LTNode* plist = ListInit();
printf("测试头删功能:");
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPopFront(plist);
LTPopFront(plist);
LTPrint(plist);
}
前面提到,因为头结点主要是用来链接最后一个结点以及后续的结点。所以,头结点在下面的函数中并不算在链表结点数量中,代码如下:
//用于记录链表结点数量:
int LTSize(LTNode* phead)
{
int size = 0;
LTNode* cur = phead->next;
while (cur != phead)
{
size++;
cur = cur->next;
}
return size;
}
本功能的实现较为简单,只需要输入一个用于查找的元素,再通过遍历链表,对结点中的元素进行对比即可。代码如下:
// 用于寻找符合条件的结点:
LTNode* LTFind(LTNode* phead, ListDataType x)
{
LTNode* cur = phead->next;
while (cur->Data != x)
{
cur = cur->next;
}
return cur;
}
例如,通过某个指定元素查找符合条件的结点的坐标,将这个坐标记作,并在这个坐标前面插入元素:
与前面头插,尾插的处理方式类似,都需要改变前后结点的链接,即:
对应代码如下:
//在pos位置前插入结点:
void LTInsert(LTNode* pos, ListDataType x)
{
assert(pos);
LTNode* posprev = pos->prev;
LTNode* newnode = BuyListNode(x);
newnode->next = pos;
pos->prev = newnode;
newnode->prev = posprev;
posprev->next = newnode;
}
测试函数如下:
void test3()
{
LTNode* plist = ListInit();
ListInit(plist);
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPushBack(plist, 5);
LTPushBack(plist, 6);
LTNode* pos = LTFind(plist, 3);
printf("\n");
printf("测试pos位置前插入:");
LTInsert(pos, 10);
LTInsert(pos, 20);
LTPrint(plist);
}
结果如下:
操作与上方头删,尾删类似,直接给出代码:
//在pos位置删除结点:
void LTErase(LTNode* pos)
{
assert(pos);
assert(pos->prev != pos);
LTNode* posprev = pos->prev;
LTNode* posnext = pos->next;
posprev->next = posnext;
posnext->prev = posprev;
}
测试函数如下:
void test2()
{
LTNode* plist = InitNode();
InitNode(plist);
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
LTPushBack(plist, 5);
LTPushBack(plist, 6);
LTNode* pos = LTFind(plist,3);
printf("\n");
printf("测试pos位置前插入:");
LTInsert(pos, 10);
LTInsert(pos, 20);
LTPrint(plist);
printf("\n");
printf("测试pos位置删除:");
LTErase(pos);
LTNode* pos1 = LTFind(plist, 4);
LTErase(pos1);
LTPrint(plist);
}