如何实现带头双向循环链表
链表
链表有双向的、单向的,带头的、不带头的,循环的、非循环的,这次介绍的双向链表是带头循环双向列表,虽然该链表结构复杂,但是实现起来并不复杂。
该链表的第一个节点是不存储有效数据的头节点。具体是如何实现带头循环双向链表的,将在定义链表结构体部分详细讲述。
定义链表的结构体
定义数据类型
首先我们要确定链表中的数据类型,可以选int,long, double, char等类型,具体的类型可以根据我们的具体情况来选取,下面的代码是将int作为链表存储的数据类型。
typedef int LTDateType;
定义链表的结构体
我们在结构体中定义了两个结构体指针,其中结构体指针prev表示该节点的前一个节点的地址,结构体指针next表示该节点的后一个节点的地址,因此我们可以通过访问该节点找到它的上一个节点和下一个节点,由此实现链表的双向。
我们将链表的头节点的prev指向尾节点,将链表的尾结点的next指向头节点,由此实现链表的循环。
typedef struct ListNode
{
LTDateType data;
struct ListNode* prev;
struct ListNode* next;
}LTNode;
初始化链表
以下操作是创建了一个由不包含有效数据的头节点构成的链表,由于链表是循环且双向的,所以此时头节点的prev指向自己,next指向自己。最后返回新创建的头指针。
LTNode* ListInit()
{
LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
phead->prev = phead;
phead->next = phead;
return phead;
}
链表的打印函数
我们之所以先写链表的打印函数,是为了更好的观察其它关于链表函数实现结果。
为什么要使用assert()函数?
提示:assert()函数中条件为真什么事也不发生,程序继续执行,如果条件为假,则会终止程序运行并报错。
如果我们传入的phead为空时,该函数在下面使用phead的时候将会对phead进行解引用,这将会导致程序运行错误,使用assert()函数可以帮我们快速地找到问题所在。
由于链表是循环的,控制打印的停止将是一个问题。
这里我们通过循环迭代更新cur指向链表的下一个节点,当cur == phead 时就应该停止打印。
提示:不打印phead节点的值是因为phead中不存储有效数据。
void ListPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur!= phead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
创建一个新的节点
使用malloc()函数给新的节点分配一个空间,并将新的节点的数据值初始化我们想要的值,并且应该将新节点中的next和prev指针初始化为NULL,要养成好习惯,否则,以后可能会出现野指针的问题。
LTNode* BuyListNode(LTDateType x)
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
node->data = x;
node->next = NULL;
node->prev = NULL;
return node;
}
链表的尾插
先创建一个新的节点。
再创建一个结构体指针记录当前的尾节点,这样做的好处是,逻辑清晰,且后续链接节点的时候不用考虑顺序问题。
尾结点的查找,由于该链表是循环的,所以头节点的prev所指向的节点就是尾节点。
接下来就是链接部分,第一步是链接当前的尾节点和新的节点,第二步是链接新的节点和头节点,这两步之间没有顺序。
void ListPushBack(LTNode* phead, LTDateType x)
{
assert(phead);
LTNode* NewNode = BuyListNode(x);
LTNode* tail = phead->prev;
tail->next = NewNode;
NewNode->prev = tail;
phead->prev = NewNode;
NewNode->next = phead;
}
链表的尾删
带头的链表一般不删头节点,所以第二个assert()函数所起到的作用就是防止尾删删掉头节点,当头节点的prev指向的是头节点,说明链表只有一个节点,且这个节点是头节点,我们不应该删掉,assert()函数将起到作用。
多定义一个指向尾节点的指针tail可以让逻辑更清晰,且不用考虑顺序问题。
注意:尾节点要最后释放,否则,将会出现野指针问题。
void ListPopBack(LTNode* phead)
{
assert(phead);
assert(phead->prev != phead);
LTNode* tail = phead->prev;
phead->prev = tail->prev;
tail->prev->next = phead;
free(tail);
}
链表的头插
同样是先要获得一个新的节点,然后定义一个结构指针指向头结点的下一个节点,使逻辑清晰,链接过程中不用考虑顺序问题。
void ListPushFront(LTNode* phead, LTDateType x)
{
assert(phead);
LTNode* NewNode = BuyListNode(x);
LTNode* PheadNext= phead->next;
NewNode->next = PheadNext;
PheadNext->prev = NewNode;
NewNode->prev = phead;
phead->next = NewNode;
}
链表的头删
同样是为了防止传过来的头节点是NULL,和防止删掉头节点,我们使用了两次assert()函数。
我们同样新创建一个指向头节点下一个节点的指针,使之后不用考虑顺序问题
然后把头节点的下一个节点的下一个节点链接到头节点上,最后释放原来的头节点的下一个节点。
void ListPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* PheadNext = phead->next;
phead->next = PheadNext->next;
PheadNext->next->prev = phead;
free(PheadNext);
}
链表查找
整体思路是:如果查找到x的话,返回x的地址;如果在链表中查找完一遍查找不到x,则返回NULL。
先创建一个新的指针cur指向头节点的下一个节点,while循环的条件是cur不指向头节点。当循环结束时即当cur指向头节点时,说明链表已经查找完一遍了,且在链表中未查找到有数据值为x的节点,此时返回NULL。
LTNode* ListFind(LTNode* phead, LTDateType x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
return cur;
else
cur = cur->next;
}
return NULL;
}
链表任意位置的插入
这里链表的插入说的是在pos位置前(由于链表是双向的,所以查找pos的前一个位置非常方便)插入一个新的节点,这里先创建一个新的结构体,再创建一个结构体指针指向pos的前一个节点,之后将新节点链接上pos位置处的节点,将指向pos的前一个节点的指针链接上新节点链表的插入就完成了。
void ListInsert(LTNode* pos, LTDateType x)
{
assert(pos);
LTNode* NewNode = BuyListNode(x);
LTNode* PosPrev = pos->prev;
NewNode->prev = PosPrev;
PosPrev->next = NewNode;
NewNode->next = pos;
pos->prev = NewNode;
}
链表任意位置的删除
在这里,我们新创建两个结构体指针分别指向pos节点的前一个节点,pos节点的后一个节点,我们只要将新创建的节点链接上,即可在链表中删除pos节点,但一定不要忘了释放pos位置处结构体。
void ListErase(LTNode* pos)
{
assert(pos);
LTNode* PosNext = pos->next;
LTNode* PosPrev = pos->prev;
PosPrev->next = PosNext;
PosNext->prev = PosPrev;
free(pos);
}
链表的销毁
销毁原链表的时候,要记得把主函数里面的头指针置为NULL,否则,可能会出现野指针的现象。
将主函数里面的头指针置为NULL有三种方法,一种方法是通过传头指针的地址,在函数中修改头指针为NULL;第二种方法就是通过函数返回值将头指针置为NULL;第三种方法就是在调用完销毁函数后在主函数中将头指针置为NULL.
我这里是采用的第三种方法。
void ListDestroy(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{
LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
测试
注意要包含所需要的头文件。
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
尾插尾删测试
int main()
{
LTNode* list = ListInit();
ListPrint(list);
ListPushBack(list, 1);
ListPushBack(list, 2);
ListPushBack(list, 3);
ListPushBack(list, 4);
ListPrint(list);
ListPopBack(list);
ListPopBack(list);
ListPopBack(list);
ListPrint(list);
list = NULL;
return 0
}
测试结果
头插头删测试
int main()
{
LTNode* list = ListInit();
ListPrint(list);
ListPushFront(list, 1);
ListPushFront(list, 2);
ListPushFront(list, 3);
ListPushFront(list, 4);
ListPrint(list);
ListPopFront(list);
ListPrint(list);
list = NULL;
return 0;
}
测试结果
查找、插入、删除测试
int main()
{
LTNode* list = ListInit();
ListPrint(list);
ListPushFront(list, 1);
ListPushFront(list, 2);
ListPushFront(list, 3);
ListPushFront(list, 4);
ListPrint(list);
ListPopFront(list);
ListPrint(list);
printf("%p\n",ListFind(list, 1));
ListInsert(ListFind(list, 1),5);
ListPrint(list);
ListErase(ListFind(list, 5));
ListPrint(list);
list = NULL;
return 0;
}
测试结果
总结
由于带头双向循环列表特殊的结构,让我们可以快速的找到尾节点,和指定位置的前一个节点,虽然结构复杂,但是能带来性能上很大的提升。
文章中如有错误,欢迎请大佬们指出,我会立即更改。