双向带头循环链表
链表实际上有八种组合出来的结构:
1.单向或者双向
2.带头或者不带头
3.循环或者非循环
今天来讲一下双向带头循环链表的一些相关知识点以及代码整理:
双向带头循环链表的特点以及实现
双向带头链表的最大的特点就是复杂,没错,它是所有链表中结构最为复杂的一种链表,但是使用起来确实最简单的。
结构体的创建以及链表的初始化
typedef int DLDatatype;
typedef struct DListNode
{
struct DListNode* prev;
struct DListNode* next;
DLDatatype data;
}DLTNode;
那么如何对链表进行初始化呢?
这里要注意一个问题,形参是实参的拷贝,如果函数无返回值,那么需要传递一个二级指针,如果有返回值,那么需要将返回值返回。
DLTNode* BuyDListNode(DLDatatype x)
{
DLTNode* node = (DLTNode*)malloc(sizeof(DLTNode));
if (node == NULL)
{
perror("malloc::BuyDListNode");
exit(-1);
}
node->data = x;
node->next = node->prev = NULL;
return node;
}
void DlistInit(DLTNode** phead)
{
*phead = BuyDListNode(-1);
(* phead)->next = *phead;
(* phead)->prev = *phead;
}
LTNode* BuyListNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL)
{
perror("malloc::newnode");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
//初始化链表
LTNode* ListInit(LTNode* phead)
{
phead = BuyListNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
链表的尾插
尾插不需要像单链表一样寻找尾结点,头结点的prev指针所指向的内容就是尾结点。同样,尾结点的next指向头指针。
//尾插
void DListPushBack(DLTNode** phead, DLDatatype x)
{
assert(phead);
DLTNode*newnode=BuyDListNode(x);
DLTNode* last = (* phead)->prev;
last->next = newnode;
newnode->next = *phead;
newnode->prev = last;
(*phead)->prev = newnode;
}
这里要注意一个问题,链表是循环的,一定注意头结点的prev指针所指向内容的修改。
链表的头插
和尾插相似,但是要注意这是带头的双向循环链表,phead是一个哨兵位的头结点,千万不要把新开辟的结点插入到phead之前。
头插需要再phead之后插入内容,那么就意味着需要创建一个指针来保存头插前phead->next所指向的内容。
//头插
void DListPushFront(DLTNode** phead,DLDatatype x)
{
assert(phead);
DLTNode*newnode=BuyDListNode(x);
DLTNode* node = (*phead)->next;
(*phead)->next = newnode;
newnode->next = node;
newnode->prev = *phead;
node->prev = newnode;
}
链表的尾删
链表的尾删除只需要修改被删除部分的prev和next指针所指向的内容,然后将删除元素free掉即可。
//尾删
void DListPopBack(DLTNode** phead)
{
assert(phead);
DLTNode* pop = (*phead)->prev;
//找到被pop的元素
DLTNode* node = pop->prev;
node->next = *phead;
(*phead)->prev = node;
free(pop);
}
链表的头删
链表头删要删除phead后的一个元素,直接将其pop,pop之前记录被删除元素所指向的下一个元素的地址。
//头删
void DListPopFront(DLTNode** phead)
{
assert(phead);
DLTNode* pop = (*phead)->next;
DLTNode* node = pop->next;
(*phead)->next = node;
node->prev = *phead;
free(pop);
}
判断是否为空链表
这里的Data是一个无效的数据,这个哨兵位的头结点在链表为空时next指针指向本身这个结点,prev指针也是指向这个结点。所以判断也很简单。
//判断链表是否为空
bool DListEmpty(DLTNode* phead)
{
assert(phead);
return phead->next == phead;
}
在pos位置之前插入x
如图所示,加入pos为D2所在的位置,那么在pos之前插入X,就要处理好D1与X,X与D2之间的关系。
//在pos位置之前插入X
void DListInsert(DLTNode** pos, DLDatatype x)
{
assert(pos);
DLTNode*newnode=BuyDListNode(x);
DLTNode* prev = (*pos)->prev;//找到pos位置之前的元素
prev->next = newnode;
newnode->prev = prev;
//先将prev和newnode两个结点连起来
newnode->next = *pos;
(*pos)->prev = newnode;
//将pos和newnode这两个结点连接起来
}
删除pos位置的结点
删除pos位置的结点,需要将pos位置之前和之后的结点连在一起。所以代码也很简单。
//删除pos位置的结点
void DListErase(DLTNode** pos)
{
assert(pos);
//先记录pos位置之前和之后的位置
DLTNode* nextnode = (*pos)->next;
DLTNode* prevnode = (*pos)->prev;
nextnode->prev = prevnode;
prevnode->next = nextnode;
free(pos);
}
求链表的长度
求链表的长度不能将链表的哨兵位的元素计算入内。
//求链表的长度
int DListSize(DLTNode** phead)
{
int size = 1;
DLTNode* node = (*phead)->next;
while (node->next != (*phead))
{
node = node->next;
size++;
}
return size;
}
链表的销毁
双向链表想要销毁,需要将每个结点逐一进行销毁。不可以直接对头结点进行销毁后就认为销毁完毕。这里注意一点,在销毁掉一个结点之前要先保存下一个结点
//链表的销毁
void DListDestroy(DLTNode** phead)
{
assert(phead);
DLTNode* node = (*phead);
while (node->next != (*phead))
{
DLTNode* next = node->next;
DListErase(&node);
node = next;
}
node = NULL;
}
链表和顺序表的对比
链表和顺序表各有其优缺点,针对其优缺点,有如下整理
顺序表的优点:
顺序表可以通过下标随机访问表中的元素,CPU高速缓存命中率高。
顺序表的缺点:
通过对顺序表的了解,头部或中间删除或插入效率低,扩容有一定的性能消耗,可能存在一定程度的空间浪费
这里就提一个问题:为什么顺序表的扩容一般都是以1.5或2倍进行扩容?
首先要明白顺序表的存储是在一块连续的空间,所以在添加元素的过程中,如果之前的空间存放不下,就会重新开辟一块新的空间,把数据复制到新开辟的内存中,并释放掉之前的内存。但是在复制的这个过程中,时间复杂度为O(n),所以一次扩容尽可能开辟得大一点,减少复制的过程。
链表的优点(以带头循环的双向链表为例子):
任意位置的插入删除的时间复杂度都是O(1),按需申请释放空间。
链表的缺点:
链表不支持下标的随机访问。
至此带头的双向循环链表就结束了,带头循环的双向链表是最为复杂的一种链表结构,有问题评论区见!