学习C/C++语言系列(1)双向循环带哨兵位链表

        虽然双向循环比单链表和循序表结构要复杂,但在我目前看来,它比单链表,顺序表都要好用,都更实用。所以我想在单链表和顺序表之前先讲解一下我学到的双向循环带哨兵位链表。

        首先来讲一讲什么是哨兵位。

        如图,哨兵位其实是一个在头节点之前的节点,这个节点并不存放有效值,只存放头节点的地址和尾节点的地址。

哨兵位

        为什么要有哨兵位呢?

        1.对于一个已经创建和初始化的链表来讲,可以没有数据,即头节点,尾节点等,但是不能没有哨兵位,因为哨兵位并不是帮我们存储数据的,只是来定位链表的。即使是一个空链表,它也会有哨兵位,所以我们可以用哨兵位是否为空来检测程序运行情况。

        2.对于链表来讲,一个很重要的地方就是头的位置,而哨兵位则可以起到定位头的作用,它的下一个节点就是头节点。

        3.在双向循环链表中,如果我们要遍历,那么很难去说明结束条件,而我们可以用哨兵位来作为结束条件,当当前节点为哨兵位时,遍历结束。

        哨兵位的好处还有很多,这里只是简单列举一下。

双向链表节点的结构

        那接下来就可以说一说双向链表的结构,从上图可以看到,每一个节点都有自己储存的值,上一个节点的位置,和下一个节点的位置,这三个信息。所以我们可以用这样的结构体来定义一个节点。

typedef int LTDataType;
typedef struct ListNode
{
	LTDataType _data;
	struct ListNode* _next;
	struct ListNode* _prev;
}ListNode;

        为了提高代码的灵活性,我们将 int 改名为 LTDateType ,这样如果要存放 char 类型的数据,只用更改一行代码即可。

双向链表的函数

        对于一个好的链表来讲,当然希望能有好的函数可用,那么双向链表要实现哪些函数的。我觉得应该有:创建一个双向链表,尾插一个节点,尾删一个节点,头插一个节点,头删一个节点,在特定处插入一个节点,在特定处删除一个节点,在链表中查找一个节点,删除链表。为了方便观察运行结果,还应该有一个打印整个链表的函数。

打印链表

        为了方便,我们可以最开始就先实现最简单的打印双向链表。按照之前讲的,我们不再以NULL为结束标志,而是以哨兵位为结束标志。实现代码如下。

// 双向链表打印
void ListPrint(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead ->next;
	while (cur != pHead)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

其中的 pHead 参数就是哨兵位。

创建一个节点

        考虑到之后的尾插,头插,创建一个双向链表都是需要申请一块动态内存的,我们可以将这个功能单独封装为一个函数。代码如下。

// 创建返回链表的头结点.
ListNode* ListCreate(LTDataType x)
{
	ListNode* tmp = (ListNode*)malloc(sizeof(ListNode));
	if (tmp == NULL)
	{
		printf("Malloc Fail In ListCreate!\n");
		exit(-1);
	}
	tmp->data = x;
	return tmp;
}

        这里返回的tmp是一个节点,但是这个节点只有存储值,没有前后节点的地址,方便使用。

初始化一个双向循环链表

        初始化一个双向循环链表,就是申请一个节点,用作哨兵位,我们需要使哨兵位的前后都指向自己,否则就不符合双向循环链表的特征。代码如下。

//初始化一个链表
ListNode* ListInit()
{
	ListNode* head = ListCreate(0);
	head->next = head;
	head->prev = head;
	return head;
}

尾插

        对于双向循环链表来讲,尾插是很简单的,因为哨兵位的前面就是链表的尾,这是哨兵位的好处。代码如下。

// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* tmp = ListCreate(x);
	tmp->next = pHead;
	(pHead->prev)->next = tmp;
	tmp->prev = pHead->prev;
	pHead ->prev = tmp;
}

        注意这里交换地址的顺序是不能乱的。读者可以测试一下在只有一个哨兵位的情况下,尾插是否管用。(是管用的)

尾删

        尾插一旦实现,尾删也就很简单了。但是要注意一下,如果只有一个哨兵位的话,就不需要进行操作了。如何判断是否只有一个哨兵位呢,答案是只要判断传进来的哨兵位的下一位是否是它本身就行了。代码如下。

        

// 双向链表尾删
void ListPopBack(ListNode* pHead)
{
	assert(pHead);
	if (pHead->next == pHead)
		return;
	ListNode* tmp = pHead->prev;
	(tmp->prev)->next = pHead;
	pHead->prev = tmp->prev;
	free(tmp);
	tmp = NULL;
}

头插和头删

        实现了尾插尾删后,头插头删也是一样的。代码如下。

// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* tmp = ListCreate(x);
	tmp->next = pHead->next;
	(pHead->next)->prev = tmp;
	pHead->next = tmp;
	tmp->prev = pHead;
}

// 双向链表头删
void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	if (pHead->next == pHead)
		return;
	ListNode* tmp = pHead->next;
	(tmp->next)->prev = pHead;
	pHead->next = tmp->next;
	free(tmp);
	tmp = NULL;
}

在链表中查找

        在链表中查找也很简单,只需要遍历就行了。链表也不是按大小排序的,所以只用遍历就ok了。代码如下。

// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* cur = pHead->next;
	while (cur!=pHead)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	return NULL;
}

在特定位置删除和插入

        这个其实和前面提到的插入和删除做法是一样的,只是换成了特定位置,但是位置是告诉你的,所以也不要去遍历链表。

// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
	//如果pos为哨兵位 依然可以用 相当于尾插
	assert(pos);
	ListNode* tmp = ListCreate(x);
	tmp->next = pos;
	tmp->prev = pos->prev;
	(pos->prev)->next = tmp;
	pos->prev = tmp;
}


// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
	assert(pos);
	//如果为哨兵位 则不进行删除
	if (pos->next == pos)
		return;
	(pos->next)->prev = pos->prev;
	(pos->prev)->next = pos->next;
	free(pos);
	pos = NULL;
}

删除链表

        删除链表,只需要一边删一边退即可。代码如下。

// 双向链表销毁
void ListDestory(ListNode* pHead)
{
	assert(pHead);
	ListNode* cur = pHead->next;
	ListNode* tmp = NULL;
	while (cur != pHead)
	{
		tmp = cur->next;
		free(cur);
		cur = tmp;
	}
	free(cur);
	cur = NULL;
}

最后

        大致的主要函数已经实现了,接下来可以愉快地使用双向链表了。

        半年前吧,我注册了CSDN的账号。这半年我经常浏览别人的博客,从中学到了许多,但是我很少写博客,写的博客也总是不够详尽,只有大体思路。我觉得想向他们学习的话,还是要创作一些优质的博客,而不是记一下流水账。所以我开启我的学习C/C++语言系列。一方面是自己再复习一次学过的知识,另一方面是希望自己的博客能帮助到像我一样在CSDN中寻找知识的人。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

roseisbule

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值