带头双向循环链表
这种结构没有缺陷,除了在定义结点是有一点点麻烦,但操作上却简单很多
带头使得情况简单,不再需要考虑第一个结点是否改变;双向循环使得操作简单,找尾变得十分简单
带了头结点之后,就意味着不用再使用二级指针。因为链表的第一个结点不可能会再被改变,永远只会是头结点;而且插入时考虑链表是否为空、删除时考虑链表中是否只有一个结点都不需要了,因为链表的第一个结点始终是头结点
相关操作
结点的定义
typedef int DSLDataType;
typedef struct DSLNode
{
struct DSLNode* prev;
struct DSLNode* next;
DSLDataType data;
}DSLNode;
prev和next
两个指针实现了双向
新建结点
DSLNode* BuyDSLNode(DSLDataType x)
{
DSLNode* newnode = (DSLNode*)malloc(sizeof(DSLNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->prev = NULL;
newnode->next = NULL;
newnode->data = x;
return newnode;
}
初始化链表
这里的初始化需要注意,最初的链表并不是没有结点的,而是有一个哨兵位的头结点
DSLNode* InitDSL()
{
DSLNode* phead = BuyDSLNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
由于这里涉及第一个结点的改变,可以使用二级指针。但我们为了保持和下面函数的形参一致,选择用返回值的方式来改变一个结点
这里BuyDSLNode(-1)
中的值可以随意写,反正也不会用到
注意,尽管链表中只有一个哨兵位的头结点,依然要满足双向循环的要求
打印链表
这里的关键在于循环的结束条件。cur从phead->next的开始走,当cur==phead时结束
void PrintDSL(DSLNode* phead)
{
assert(phead);
DSLNode* cur = phead->next;
while (cur != phead)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
这里断言的原因:按道理来说,带头双向循环链表是不可能为空的,最起码还有个头结点。但就怕有人传参传错了,传了个空链表过来
这之后函数中凡是出现
assert(phead)
都是因为上述原因
尾插
void DSLPushBack(DSLNode* phead, DSLDataType x)
{
assert(phead);
//DSLNode* newnode = BuyDSLNode(x);
//
进行尾插
找原本的尾
//DSLNode* tail = phead->prev;
新结点与原本的尾相连
//tail->next = newnode;
//newnode->prev = tail;
新结点与头结点相连
//phead->prev = newnode;
//newnode->next = phead;
//DSLInert是插在pos之前,所以这里要传phead
DSLInsert(phead, x);
}
尾删
void DSLPopBack(DSLNode* phead)
{
assert(phead);
assert(phead->next != NULL);//防止链表中只有一个哨兵位头结点
/*DSLNode* tail = phead->prev;
DSLNode* tailprev = tail->prev;
tailprev->next = phead;
phead->prev = tailprev;
free(tail);*/
DSLErase(phead->prev);
}
头插
void DSLPushFront(DSLNode* phead, DSLDataType x)
{
assert(phead);
//DSLNode* newnode = BuyDSLNode(x);
两种写法.个人更倾向于第二种
第一种,不额外定义指针,直接改变链接关系。但这种写法要先改与后面的链接关系
///*newnode->next = phead->next;
//phead->next->prev = newnode;
//phead->next = newnode;
//newnode->prev = phead;*/
第二种,保存原本的第一个结点。这种写法不需要考虑链接关系更改先后
//DSLNode* first = phead->next;
//newnode->next = first;
//first->prev = newnode;
//newnode->prev = phead;
//phead->next = newnode;
DSLInsert(phead->next, x);
}
头删
void DSLPopFront(DSLNode* phead)
{
assert(phead);
assert(phead->next != NULL);
/*DSLNode* first = phead->next;
DSLNode* second = first->next;
phead->next = second;
second->prev = phead;
free(first);*/
DSLErase(phead->next);
}
查找
DSLNode* DSLFind(DSLNode* phead, DSLDataType x)
{
assert(phead);
DSLNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
在pos之前插入
void DSLInsert(DSLNode* pos, DSLDataType x)
{
assert(pos);
DSLNode* newnode = BuyDSLNode(x);
DSLNode* posprev = pos->prev;
posprev->next = newnode;
newnode->prev = posprev;
newnode->next = pos;
pos->prev = newnode;
}
当pos指向第一个结点时可以实现头插
当pos指向phead时可以实现尾插
删除pos
void DSLErase(DSLNode* pos)
{
assert(pos);
DSLNode* posprev = pos->prev;
DSLNode* posnext = pos->next;
free(pos);
posprev->next = posnext;
posnext->prev = posprev;
}
判断链表是否为空
bool DSLEmpty(DSLNode* phead)
{
assert(phead);
return phead == phead->next;
}
若phead->next指向自己即为空
返回链表长度
size_t DSLSize(DSLNode* phead)
{
DSLNode* cur = phead->next;
size_t size = 0;
while (cur != phead)
{
size++;
cur = cur->next;
}
return size;
}
注意有些代码会将链表长度保存在phead的data中。这样的写法其实是工程经验不足的表现。如果现在的数据类型不再是int,而是char,且链表的长度超过了127,那么就会出现问题了。所以这样写是不好的
销毁链表
void DSLDestroy(DSLNode* phead)
{
DSLNode* cur = phead->next;
while (cur != phead)
{
DSLNode* curnext = cur->next;
free(cur);
cur = curnext;
}
free(phead);
}
为保持整体代码形参一致,这里就不传二级指针了。只不过这样就要在外部将phead=NULL