【数据结构】真正的链表!带头双向循环链表!

我们今天学习的是带头双向循环链表

这样设计是有它的道理的,比如你的尾插,是不是找尾很麻烦?这个链表只需要一步就可以,即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?

不要,因为你是一级指针,置空也改变不了实参,所以我们置空在主函数里面主动置空,或者传二级指针过来也可以,那这样有点割裂感

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值