【数据结构】一种令人愉悦的链表——带头双向循环链表(代码实现)

带头双向循环链表

  • 1.带头双向循环链表介绍
  • 2.代码实现带头双向循环链表
    • 2.1创建结构体
    • 2.2链表创建
    • 2.3链表初始化(哨兵位)
    • 2.4查找链表中某一元素的位置
    • 2.4任意位置插入数据(在pos位置之前)
    • 2.5任意位置删除数据
    • 2.6 打印链表
  • 3.总结

1.带头双向循环链表介绍

【数据结构】一种令人愉悦的链表——带头双向循环链表(代码实现)_第1张图片

所谓的带头双向循环链表就是 带有哨兵位且该链表是双向循环的。
如果实现双向循环?
就是 head -> pre = d3,d3 -> next = head;
是的,这种数据结构就是一个结点中存有pre(上一个结点的地址),存有next(下一个结点的地址),data(该结点的数据)

带头双向循环链表比起单链表使用起来实在是再舒服不过了,因为它本身带有哨兵位,而且每个结点都存有上一个和下一个结点的地址,大大简化了代码量。
下面让我们感受一下,这种数据结构实现起来多方便吧

2.代码实现带头双向循环链表

2.1创建结构体

typedef int LTDataType;//typedef 一下数据类型,想用其他类型时直接在这里改就好了
typedef struct ListNode
{
	LTDataType data;  //data是我们要存的数据
	struct ListNode* pre; //pre存储的是上一个结点的地址
	struct ListNode* next;//next存储的是下一个结点的地址
}LSTNode;  //typedef一下这个结构体,一下LSTNode指的都是这个结构体类型

2.2链表创建

LSTNode* BuyNode(LTDataType x) //创建一个新的结点 存的数据是 x 
{
	//用malloc动态开辟一个新的结点
	LSTNode* newnode = (LSTNode*)malloc(sizeof(LSTNode));
	if (newnode == NULL) //可能会开辟失败,养成好习惯判断一下
	{
		perror("malloc fail");
		exit(-1); //判断失败就退出程序
	}
	newnode->data = x;  //要存储的数据是 x
	newnode->next = NULL; //先把存储的上一个结点和下一个结点置为NULL
	newnode->pre = NULL;

	return newnode; //返回这个创建好的结点,在函数外面用该结构体类型接收这个结点
}

2.3链表初始化(哨兵位)

LSTNode* Init()  //链表的哨兵位初始化
{
	LSTNode* phead = BuyNode(-1); //初始化数据为 -1
	phead->pre = phead;   // 哨兵位先自己指向自己
	phead->next = phead;  // 哨兵位先自己指向自己
	return phead;  // 返回这个哨兵位
}

在函数外面创建结点的方式

LSTNode* n1 = Init(); // n1 是我们的哨兵位

2.4查找链表中某一元素的位置

【数据结构】一种令人愉悦的链表——带头双向循环链表(代码实现)_第2张图片

LSTNode* LSTFind(LSTNode* phead, LTDataType x)//传入哨兵位,传入要查找的数 x 
{
	assert(phead);//因为我们的哨兵位不能为空,为了防止人为传入空,所以断言一下
	LSTNode* cur = phead -> next;
//我们的哨兵位的数据不计入我们链表的数据,哨兵位的数据可有可无,所以从哨兵位下一个位置开始找
	while (cur != phead) 
// 由于我们的链表是循环的,到了最末尾以后,再走一步就会回到开头哨兵位位置,所以当等于哨兵位时代表链表遍历完毕
	{
		if (cur->data == x) 
		{
			return cur; //找到了就返回这个结点
		}
		cur = cur->next; //否则就继续走
	}
	return NULL;//最后找不到就返回空,表示未找到
}

2.4任意位置插入数据(在pos位置之前)

//这里指的是在pos位置之前插入
//为什么要在pos位置之前插入呢?
//先看代码 
void LSTInsert(LSTNode* pos,LTDataType x)
{
	assert(pos);// pos位置不能空,断言一下
	LSTNode* newnode = BuyNode(x); // 创建要插入的结点newnode
	LSTNode* prev = pos->pre;//先用一个结点存储pos位置的前一个结点
	prev->next = newnode;//pos位置前一个结点(prev)的下一位(prev->next)指向我们要插入的结点newnode
	newnode->pre = prev;//newnode的pre指向pos的前一个结点(之前存储过的)
	newnode->next = pos;//newnode的next指向我们的pos
	pos->pre = newnode;//pos的pre再指向我们的newnode
}

思考一下,如果我们直接先 pos->pre = newnode 会怎么样?
答案是: 这样的话 pos -> pre 就会先变成我们新创建的结点newnode
之前pos -> pre 就找不到了,那我们就不能在 pos 位置之前插入数据了

现在我们来回答一下为什么要在pos位置之前插入,原因是为了方便头插和尾插

头插就是 LSTInsert(phead -> next,x);在哨兵位后面插入数据
尾插就是 LSTInsert(phead,x);在哨兵位之前插入数据就是尾插

2.5任意位置删除数据

void LSTErase(LSTNode* pos) // 删除pos位置
{
	assert(pos);
	LSTNode* pos_pre = pos->pre; // 把pos的pre存起来
	LSTNode* pos_next = pos->next;//把pos的next存起来
	free(pos);//free掉pos
	pos_pre->next = pos_next;//之前存的pos的pre的next指向之前存的pos的next
	pos_next->pre = pos_pre;//之前存的pos的next的pre指向pos的pre
}
那么这里的头删就是
LSTErase(phead -> next);
尾删就是
LSTErase(phead->pre);

2.6 打印链表

void LSTPrint(LSTNode* phead)
{
	assert(phead);  // 哨兵位不能为空
	LSTNode* cur = phead -> next; // 从哨兵位下一位开始打印
	while (cur != phead)  //当循环到哨兵位时结束打印
	{
		printf("%d ", cur->data); //打印数据
		cur = cur->next; // cur 移动
	}

}

3.总结

带头双向循环链表的尾删和尾插都非常方便,原因就是我们带了个哨兵位并且链表头和尾都是连起来的,这样我们要尾插和尾删可以利用哨兵位来帮助我们更好地实现。

你可能感兴趣的:(数据结构,链表,数据结构)