目录
前言
本篇文章主要为大家简单介绍一下链表中一个特别好的带头双向循环链表
一、带头双向循环链表好在哪里
顺序表:顺序表可以随机访问,但是对于插入和删除操作则移动的节点过多并且存储大小比较固定
链表:不能做到随机访问,但是在插入和删除时相对顺序表要简单很多,在使用中还可以动态开辟空间
他们各有各自的好处也有自己的缺点,但双向带头循环链表的优点足以掩盖它所有的缺点,下面让我们来看看吧
二、程序实现
1.构建与初始化
在正式写程序之前,为了保证后面代码的通用性我这边写了
typedef int LTDataType;
当我们使用的不是整型数据时,在这里将interesting改成其他类型就行
那么如果我们需要一个带头双向循环链表,我们先来看一下他的逻辑结构
如上图所示,在两个元素之间头指向尾,尾指向头
首先我们先创建一个结构体以表示这个整体,我们要一个存放数据的位置,一个指向头和一个指向尾的指针
typedef struct ListNode
{
LTDataType _data;
struct ListNode* next;
struct ListNode* prev;
}ListNode;
架子我们搭好了,我们再来想想它应该有一些什么功能
首先初始化是一定要的,我们需要给架子充实起来并且将它放在一个地方才行
然后插入删除、打印、销毁都是要的
那我们先写一下它的初始化
ListNode* ListCreate()
{
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
node->next = node;
node->prev = node;
return node;
}
我们创建了一个空间,然后他的头和尾都指向自身,我们在这里开辟了空间,但是当我们插入的时候是不是还是开辟一个空间内,这样是不是就造成了冗余了,这里我们可以选择用一个函数来完成开辟空间的操作,我们来改进一下上面的代码
ListNode* BuyListNode(LTDataType x)
{
ListNode* node = (ListNode*)malloc(sizeof(ListNode));
if (node)
{
perror("malloc fail");
exit(-1);
}
node->data = x;
node->next = NULL;
node->prev = NULL;
return node;
}
ListNode* ListCreate()
{
ListNode* phead = BuyListNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
2.打印
void ListPrint(ListNode* pHead)
{
assert(pHead);
ListNode* cur = pHead->next;
while (cur != pHead)
{
printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
将链表传入,不能为空,然后创建一个指针避免直接使用头指针。
2.尾插尾删
void ListPushBack(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListNode* newnode = BuyListNode(x);
ListNode* tail = pHead->prev;
tail->next = newnode;
newnode->prev = tail;
newnode->next = pHead;
pHead->prev = newnode;
}
我们首先创建一个空间存放x,然后创建一个指针等于头节点的上一个(当链表里面没有存储时头节点的上一个就是本身,否则就是最后一个元素)
void ListPopBack(ListNode* pHead)
{
assert(pHead);
assert(pHead->next);
ListNode* tail = pHead->prev;
ListNode* tailprev = tail->prev;
tailprev->next = pHead;
pHead->prev = tailprev;
free(tail);
}
首先我们判断一个头指针和头指针的下一个是否为空
但是这里可以看到我们创建了一个tail和tailprev看上去有一点多余,但是不创建tail在我们删除掉最后一个节点是,它还没有释放,可能会造成内存泄漏
2.头插头删
void ListPushFront(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListNode* newnode = BuyListNode(x);
newnode->next = pHead->next;
pHead->next->prev = newnode;
pHead->next = newnode;
newnode->prev = pHead;
}
void ListPopFront(ListNode* pHead)
{
assert(pHead);
assert(pHead->next);
ListNode* first = pHead->next;
ListNode* second = first->next;
free(first);
pHead->next = second;
second->prev = pHead;
}
当你弄懂了尾插和尾删,头插和头删就没啥难度了,就头插的时候先让要插入的那个数指向头指针后面那个数,其他地方逻辑相似
2.查找
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
assert(pHead);
ListNode* cur = pHead->next;
while (cur != pHead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
在带头双向循环链表里面找一个数,和链表原理一样遍历一遍就好了,无非就判断条件条件有所不同
2.指定位置插入和删除
void ListInsert(ListNode* pos, LTDataType x)
{
ListNode* prev = pos->prev;
ListNode* newnode = BuyListNode;
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
void ListErase(ListNode* pos)
{
assert(pos);
ListNode* next = pos->next;
ListNode* prev = pos->prev;
free(pos);
prev->next = next;
next->prev->prev;
}
在指定位置插入和删除,插入时我创建了两个指针分别在pos的前后,然后就是拆分重新指向
删除也即是基于前后一个指针重新指向然后释放空间
2.销毁
void ListDestory(ListNode* pHead)
{
assert(pHead);
ListNode* cur = pHead->next;
ListNode* next = cur->next;
while (cur != pHead)
{
free(cur);
cur = next;
next = cur->next;
}
free(pHead);
}
我这边是使用两个指正一个指向要销毁的,一个指向要销毁的下一个然后遍历
2.测试
void TestList1()
{
ListNode* phead = ListCreate();
ListPushBack(phead, 1);
ListPushBack(phead, 2);
ListPushBack(phead, 3);
ListPushBack(phead, 4);
ListPopBack(phead);
ListPrint(phead);
ListPushFront(phead, 1);
ListPushFront(phead, 2);
ListPushFront(phead, 3);
ListPushFront(phead, 4);
ListPopFront(phead);
ListPrint(phead);
ListNode* pos = ListFind(phead, 3);
ListInsert(pos , 6);
ListPrint(phead);
pos = ListFind(phead, 1);
ListErase(pos);
ListPrint(phead);
ListDestory(phead);
ListPrint(phead);
ListPrint(phead);
ListPrint(phead);
}
int main()
{
TestList1();
return 0;
}
总结
好了,到这里就要结束了,带头双向循环链表已经给大家介绍完毕,大家要是有更好的的想法也可以自行加入时它变得更加完美,要是有什么不懂的地方我建议多画画图,会好理解很多。
谢谢大家的阅览,欢迎文章存在问题欢迎大家提出以便博主及时改正
文章内容存在借鉴若有侵权请联系删除!!!