我们今天学习的是带头双向循环链表
这样设计是有它的道理的,比如你的尾插,是不是找尾很麻烦?这个链表只需要一步就可以,即head的前一个就是尾,而且不用考虑前后指针是不是为空,即不用担心空指针的问题。
看完就会说,这才是真正的链表!
定义链表
typedef struct sl
{
struct sl* prev;
struct sl* next;
int data;
}sl;
定义链表很简单,就是定义一个结构体,里面有prev指针和next指针,
注意,为了方便我们将struct sl 起别名为sl
打印链表
我们在打印链表的时候,哨兵位是不需要打印的,即我们的遍历应该是从phead下一个开始的,那什么时候结束呢?
当cur = phead的时候就结束了,想一想为什么
void print(sl* phead)
{
assert(phead);
sl* cur = phead->next;
while (cur != phead)
{
printf("%d->", cur->data);
cur = cur->next;
}
}
牛逼的地方在于,当我们链表为空的时候,只有哨兵位
是不打印东西的
创建新结点
当我们要尾插,头插的时候就要创建结点了,为了可读性和方便我们用一个函数来表示
sl* creatsl(int x)
{
sl* phead = (sl*)malloc(sizeof(sl));
if (!phead)
{
perror("malloc");
return NULL;
}
phead->prev = NULL;
phead->next = NULL;
phead->data = x;
return phead;
}
关于结点的next和prev指针要不要指向自己,其实都可以,反正我们在我们要改的
初始化链表
初始化其实就是初始化我们的哨兵位
sl* init()
{
sl* phead = (sl*)malloc(sizeof(sl));
if (!phead)
{
perror("malloc");
return NULL;
}
phead->prev = phead;
phead->next = phead;
return phead;
}
注意: 这个时候我们的next和prev指针是指向自己的,这不是创建结点,是初始化哨兵位
那怎么使用我们的初始化?让主函数创建指针的时候就指向哨兵位
sl* list = init();
这样就指向哨兵位了
尾插
万事大吉,现在就可以开始写我们的链表了
尾插熟悉不过了吧,重要的就是找到尾
void pushback(sl* phead, int x)
{
assert(phead);
sl* newnode = creatsl(x);
sl* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
双向带头循环链表的牛逼之处就是在于当你只有哨兵位的时候,代码也适用
头插
上面代码是错误的,在头插的时候,我们要注意,如果先将phead->next指向newnode的话,我们就找不到d1了,你以为连接到了d1,实际上phead->next已经改成newnode本身了。为了解决,我们有二种方法,建议使用第二种方法
1:先连接后面的,即newnode->next先指向d1,再去动prev
void pushback(sl* phead, int x) { assert(phead); sl* newnode = creatsl(x); newnode->next = phead->next; phead->next->prev = newnode; phead->next = newnode; newnode->prev = phead; }
2:创建变量标记d1,这个时候可以随便顺序连接了
void pushback(sl* phead, int x) { assert(phead); sl* newnode = creatsl(x); sl* next = phead->next; phead->next = newnode; newnode->prev = newnode; next->prev = newnode; newnode->next = next; }
当链表为空的时候,即只有哨兵位的时候,头插也是适用的
尾删
尾删也是很方便的,找到尾就可以操作,当然找到尾后,尾的前一个也就可以顺势而求
我们只需要free(tail),重新改变tailPrev的连接
void popback(sl* phead)
{
assert(phead);
assert(phead != phead->next);
sl* tail = phead->prev;
sl* tailprev = tail->prev;
free(tail);
prev->next = phead;
phead->prev = tailprev;
}
值得注意的是,当链表为空的时候要不要继续删?
不要,我们真实的链表是在哨兵位后面的,为空的时候,不是把哨兵位删除了吗?
这个时候我们就可以加一个断言assert(phead != phead->next);
注意: 我们要断言2个及其以上的时候推荐分开写,这样程序报错的时候就会精准告诉你是哪一个断言出问题了,而不是用&&连接
头删
与尾删相似,这里推荐的方法还是先创建变量标记上下一个结点,这样就可以随意删除结点了
我们只需要free掉tail,改变结点之间的连接就可以了
void popfront(sl* phead)
{
assert(phead);
assert(phead != phead->next);
sl* tail = phead->next;
sl* next = tail->next;
free(tail);
phead->next = next;
next->prev = phead;
}
同样的这样的代码在只剩一个的时候也适用
查找结点
查找结点就是你输入一个data值,可以找到这个结点的地址,方便你在这个结点增删改
同样的代码逻辑很简单,就直接写了
sl* findsl(sl* phead, int x)
{
assert(phead);
sl* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
随机插入
我们的随机插入,需要和你的查找结点联动,即查找到目标结点,地址位pos,并且是默认插入在pos之前
怎么联动?
在主函数里,用find函数查找到你要的结点位置,再作为实参传给随机插入函数
sl* pos = findsl(list,2);
if (pos)
{
insert(pos, 4);
}
同样的,如果不想太关注顺序就定义变量,存储前一个位置
void insert(sl* pos, int x)
{
assert(pos);
sl* newnode = creatsl(x);
sl* prev = pos->prev;
newnode->next = pos;
newnode->prev = prev;
prev->next = newnode;
pos->prev = newnode;
}
是不是很简单,没有想到链表有一天也可以这么方便吧
做到了随机插入,我们就可以改变尾插代码了,进行复用
void pushback(sl* phead, int x)
{
assert(phead);
/*sl* newnode = creatsl(x);
sl* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;*/
insert(phead, x);
}
注意: 随机插入是插入pos之前的结点,我们复用尾插的时候,就要传哨兵位过去,哨兵位之前就是尾嘛
同样的,头插也可以复用
void pushfront(sl* phead, int x)
{
assert(phead);
/*sl* newnode = creatsl(x);
sl* next = phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = next;
next->prev = newnode;*/
insert(phead->next, x);
}
随机删除
我们标记好前一个后一个,直接free掉pos,不是分分钟搞定?
void erase(sl* pos)
{
assert(pos);
sl* prev = pos->prev;
sl* next = pos->next;
free(pos);
prev->next = next;
next->prev = prev;
}
同样的我们也可以复用头删尾删
void popfront(sl* phead)
{
assert(phead);
assert(phead != phead->next);
/*sl* tail = phead->next;
sl* next = tail->next;
free(tail);
phead->next = next;
next->prev = phead;*/
erase(phead->next);
}
void popback(sl* phead)
{
assert(phead);
assert(phead != phead->next);
/*sl* tail = phead->prev;
sl* prev = tail->prev;
free(tail);
prev->next = phead;
phead->prev = prev;*/
erase(phead->prev);
}
注意: 我们的随机删除,有一个缺陷,可能会删掉哨兵位,但是我们可以将哨兵位的data设置为一些离谱的值,让使用者发现不了我们的哨兵位,自己知道这个缺陷就行,当然我们实参传过来phead也可以,对比一下,仁者见仁智者见智
清理链表
我们清理链表的逻辑很简单,就是遍历一边链表,释放每一个结点,当然遍历前要将下一个的结点存储起来
void destroy(sl* phead)
{
sl* cur = phead->next;
while (cur != phead)
{
sl* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
注意:我们真正创建的指针就只有哨兵位,后面在函数里面创建的指针出作用域就销毁了
这个时候我们要不要置空phead?
不要,因为你是一级指针,置空也改变不了实参,所以我们置空在主函数里面主动置空,或者传二级指针过来也可以,那这样有点割裂感