带头双向链表

目录

链表介绍

增删查改

尾插

头插

尾删

头删

查找

随意位置的前方插入

任意位置删除

销毁


大家应该都应该学过单链表的实现了,在实现的过程中是否会觉得单链表进行尾删,尾插这种操作的时候太过繁琐了,又要遍历,判空又要分很多种情况。今天我介绍一个新的链表结构——带头双向链表。介绍完了之后你就会发现,实现起来真的是云泥之别。(单从实现上看)。

链表介绍

顾名思义,相比于单链表,带头双向链表就是本身自带头指针,链表的结构比单链表多一个用来指向前一个结点的地址的指针,如图:

 所以我们定义结构体的时候,要多定义一个指针:

typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}ListNode;

这双向链表虽然结构看起来稍微复杂一点,但是实现起来会非常简单。

直接初始化一下这个链表:

ListNode* BuyLTNode(LTDataType x)
{
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	if (newnode == NULL)
	{
		perror("maaloc fail");
		return NULL;
	}

	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}
ListNode* ListCreate()
{
	ListNode* phead = BuyLTNode(-1);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

很简单,用malloc申请空间,将结构体里的指针都赋空值,数据传进来是啥就是啥,在这里头指针就无所谓值多少了,因为我们不会用到这个值,然后这个结构巧妙的地方的就是没有值的时候,头指针的prev和next都是指向自己的。所以就这样初始化。

增删查改

尾插

从这里开始你就会慢慢感受到这个结构的妙处,先看代码:

void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	ListNode* tail = pHead->prev;
	ListNode* newnode = BuyLTNode(x);

	tail->next = newnode;
	newnode->next = pHead;
	newnode->prev = tail;
	pHead->prev = newnode;

}

插入无非两种情况嘛,一个是原本链表中有数据,一个是链表中没有数据,这个大家都清楚,在我们实现单链表的时候,是不是还得分情况判空,而看代码,我没有进行任何这样的操作,这是为什么。

 让我们来看看这两种情况,第一种通过head->prev找到尾结点,标记为tail,然后更改一下头尾指向就好了,再看第二种,还是通过prev去找尾结点,发现尾结点还是自己,这个时候继续进行更改操作,发现其实都是一样的,这个时候tail就是head,完全不影响。所以就用一套代码就行了。

头插

void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	ListNode* tail = pHead->next;
	ListNode* newnode = BuyLTNode(x);

	tail->prev = newnode;
	newnode->prev = pHead;
	newnode->next = tail;
	pHead->next = newnode;
}

还是一样,不用多加判断是否为空,同样是一套代码完成。

 还是两种情况,第一种非空,找到head的下一个结点,标记起来,然后改变这个标记结点的头指针和head的尾指针改下,指向新结点。第二种情况是为空,这个时候head的next是head的,这个时候头插改边标记的结点的prev其实就是head的prev,next也还是head的next,所以你会发现还是都还是用一套代码就ok了。

尾删

void ListPopBack(ListNode* pHead)
{
	assert(pHead);

	ListNode* tail = pHead->prev;
	ListNode* prev = tail->prev;

	prev->next = pHead;
	pHead->prev = prev;
	free(tail);

}

这里的小细节就是我采用了两个指针,一个是尾结点,一个是尾结点的前一个结点,这个时候,虽然定义的有点多,但是你就不用再担心因为操作顺序问题,导致链接断开,找不到前一个结点的问题了。

头删

bool LTEmpty(ListNode* pHead)
{
	assert(pHead);

	return pHead->next == pHead;
}
void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	assert(!LTEmpty(pHead));

	ListNode* tail = pHead->next;
	ListNode* next = tail->next;

	pHead->next = next;
	next->prev = pHead;
	free(tail);
}

这里面利用到布尔值去分辨是否为空链表,这里可以看到LTEmpty里面返回的是phead的next是否是本身,是的话返回真,而我函数里面是!LTEmpty(pHead),也就是说为空的话,程序就会进行提醒了。然后就是老样子创建两个指针,方便更改指向。

查找

ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	assert(!LTEmpty(pHead));

	ListNode* cur = pHead->next;
	while (cur != pHead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

 查找就像单链表那样,一个个挨着找,找到就返回那个结点的地址。

随意位置的前方插入

改配合着查使用,先使用查找找到需要改的值,然后进行更改,是插入这个结点的前面。

void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);

	ListNode* prev = pos->prev;
	ListNode* newnode = BuyLTNode(x);

	prev->next = newnode;
	newnode->next = pos;
	pos->prev = newnode;
	newnode->prev = prev;
}

还是老样子,这里的逻辑就跟插入没什么区别。所以其实头插尾插,都可以使用这个函数去代替,如果用户需要头插,尾插,我们也不用专门去写这个代码,直接函数套用,头插就是把原本

head->next给传进去,我插入到这个的前面,就是头插了,而尾插就是把头结点给传进去,头结点的前一个就是尾结点,直接完成插入。

任意位置删除

void ListErase(ListNode* pos)
{
	assert(pos);

	ListNode* prev = pos->prev;
	ListNode* next = pos->next;

	prev->next = next;
	next->prev = prev;
	free(pos);

}

同样的配合查找使用,这个也可以去进行替代头删尾删,而且直接把头尾结点传进去就行。

销毁

void ListDestory(ListNode* pHead)
{
	ListNode* cur = pHead->next;
	while (cur != pHead)
	{
		ListNode* next = cur->next;
		free(next);
		cur = cur->next;
	}
	printf("\n");

	free(pHead);
}

最后进行销毁,一个指针进行遍历,遍历的同时定义的新指针,让新指针完成,释放空间这个操作,不要影响到遍历。

带头双向链表就是这样了,你是否也觉得十分的方便。最后你如果有什么疑问或者我哪里说错了,欢迎指出。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值