带头循环双链表

     今天我们来了解一种很厉害的链表,它就是 带头的、双向的、循环的链表,这种链表算是一种比较难的链表,但它的结构是几乎完美的,我们慢慢的来了解它。

目录

1.什么是带头循环双链表?

2.链表的功能

1.基本功能

2)创建新节点

3)打印链表

 4)查找节点

2.增加元素 

1)头部增加

2)尾部增加

3)中间增加

3.删除元素

1)头部删除

2)尾部删除

3)中间删除

4.修改元素

5.销毁链表

测试

1)增加节点测试

2)删除节点测试: 

 3)  修改节点测试


1.什么是带头循环双链表?

字面意思,就是这种链表,它拥有头节点,它的每个节点都有两个指针,它的头和尾是连接起来的,这样就形成了这种链表。

来用图片,更直观的感受它的结构。

如图所示,phead是头节点,它不存数据元素,需要单独创建,这体现了带头。

其中的双向箭头就代表两个节点互相指向对方,这体现了体现双向。

然后,尾节点的后继指针,next需要指向我们的头节点phead,我们头节点的前驱指针prev需要指   向我们的尾节点,这体现了循环。

节点用结构体来实现,看代码:

typedef int DataType;
typedef struct QListNode
{
	DataType data;//数据元素
	struct QListNode *prev;//前驱指针
	struct QListNode *next;//后继指针
}QLN;//结构体类型的别名

   对这种链表有了基本了解之后,我们同样要研究它的增、删、查、改。


2.链表的功能

1.基本功能:1)创建头结点2)创建新节点3)打印链表4)查找节点
2.增加元素:1)头部增加  2)尾部增加  3)中间增加
3.删除元素:  1)头部删除     2)尾部删除   3)中间删除
4.修改元素:找到某个元素,把它修改掉        
5.销毁链表

1.基本功能

1)创建头结点

 链表是带头的,我们动态开辟一个头节点,把它的地址返回出去,在主函数中用一个头指针来管理。

看代码:

QLN *CreatePhead()//创建头结点
{//创建头结点,让它的前驱和后继暂时都指向自己
	QLN *newnode = new QLN;
	//QLN *newnode=(QLN*)malloc(sizeof(QLN));
	newnode->data = 0;//这个不用存数据元素
	newnode->next = newnode;
	newnode->prev = newnode;
	return newnode;
}

用new来申请一个节点,不熟悉的话就用malloc,然后把它初始化一下。

由于我们的头节点不用存储数据,所以随便给个值就好了。

然后最关键的是,我们把让它的前驱指针和后继指针,暂时都指向自己,即指针里面存的都是头节点自己的地址,这个处理非常巧妙,之后功能里面会体现它的好处。

这种状态也能作为链表为空的状态,我们能根据他进行判断。


2)创建新节点

创建新节点的操作用的很频繁,我们直接写成一个函数。

代码如下:

QLN *Buynewnode( DataType x)//创建新节点
{
	QLN *newnode = new QLN;
	//QLN *newnode=(QLN*)malloc(sizeof(QLN));
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

创建一个新节点,初始化一下,指针都暂时置为空,然后把它的地址返回出去。


3)打印链表

这个就遍历这个链表把数据打印出来就好了,关键点是,我们的链表是循环的,需要找好结束的条件,不要让它重复打印,导致死循环,这个条件就是,我们用来遍历的指针不指向phead!!

代码如下:

void QlistPrint(QLN *&phead)
{
	if (phead->next == phead)
	{
		cout << "链表为空,无法打印" << endl; 
		return;
	}
	QLN *cur = phead->next;
	while (cur!=phead)
	{
		cout << cur->data << "->";
		cur = cur->next;
	}
	cout << "phead" << endl;
}

从第一个节点开始,依次打印,当打印完最后一个节点的数据的时候,链表遍历完毕,这个后负责遍历的指针会回到我们的头节点处,这个时候,我们就要结束打印!!!打印完成后,我们顺便打印一个字符串"phead",体现一下我们是循环链表~~~

打印效果如下:


 4)查找节点

       在我们的双向链表里,查找节点,我们找那个就返回那个的地址就好了,只要我们保证输入的节点是正确的,总是能找到那个节点的。

      这个跟单链表,不一样,没那么麻烦,还得找前一个节点,因为在双链表里面,我们总能通过当前节点找到它的前一个节点和后一个节点。

在哪之前,还有一个辅助的函数,获取链表长度,就是遍历一遍,得到一个整数。

看代码:

int QListLen(QLN *&phead)
{
	if (phead->next == NULL)
		return 0;
	else
	{
		QLN *tail = phead->next;
		int temp = 0;
		while (tail != phead)
		{
			temp++;
			tail = tail->next;
		}
	return temp;
	}
	
}

再来看,查找函数:

QLN *FindNode(QLN *&phead, DataType pos)//按位置,查找某个节点
{
	if (pos<=0 || pos>QListLen(phead))
	{
		cout << "输入的节点不正确,请重新输入!!" << endl;
		return NULL;
	}
	else
	{
		int num = 1;
		QLN *cur = phead->next;
		while (num <= pos - 1)//这找节点,肯定能找到
		{
			cur = cur->next;
			num++;
		}
		return cur;
	}

上边传参用到了,引用的操作,这里简单介绍一下。

如果写成引用传递,相当于对我们的实参又起了一个名字,实参原本的名字和新起的名字,都能对实参的内容进行修改,不需要再为形参开辟空间了。

而传地址,这里每次调用就要创建一个指针,再把形参的值传过来(这里我们的实参也是个指针,所以传值就好了)。

看图:

而在主函数里:

QLN *pphead = CreatePhead();

我们传的就是pphead的值,即头节点的地址!!!!

这里只做简单介绍,上边的函数中把&去掉是可以的,形参就是指针了。


2.增加元素 

1)头部增加

通过头节点找到,第一个节点,然后在它们之间再插入一个节点。

上代码:

void QlistPushFront(QLN *&phead, DataType x)//头插
{
	QLN *newnode = Buynewnode(x);
		QLN *next = phead->next;//直接先记录一下,下一个节点,然后随便连
		phead->next = newnode;
		newnode->prev = phead;//即使链表为空,这种方法也能解决,这就是这种链表的特点
		newnode->next = next;
		next->prev = newnode;
		/*newnode->next = phead->next;
		phead->next->prev = newnode;//这种写法顺序必须固定,不然会找不到后边的节点
		phead->next = newnode;
		newnode->prev = phead;*/
}

分析:

1.记录旧的头节点,把新节点放上,连接头节点和旧的新节点

这个连接的过程是四步,如果我们不记录旧的头节点,我们的代码必须这样写

       newnode->next = phead->next;
        phead->next->prev = newnode;//这种写法顺序必须固定,不然会找不到后边的节点
        phead->next = newnode;
        newnode->prev = phead;

首先得把旧的头节点,连接在新节点的后边,不能直接修改phead->next。

但记录一下旧的头节点,就随便连接了。

2.这种写法能处理特殊情况的头插,比如当我们的链表为空的时候,我们来分析一下:

      QLN *newnode = Buynewnode(x);
       QLN *next = phead->next;

这个时候,phead->next得到的是头节点的地址(这个刚刚前边说的特殊处理,把空链表的头节点的两个指针都指向自己的作用在这里体现了。)
        phead->next = newnode;头节点来指向新节点。
        newnode->prev = phead;新节点的前驱指针指向头节点。
        newnode->next = next;新节点的后继指针指向phead!!!
        next->prev = newnode;phead的前驱指针指向新节点!!!

这一点要自己仔细分析一下,才能理解,理解过后,你会发现,这种链表的特点就体现出来了!!!


2)尾部增加

这个功能有一点很关键,就是不用像单链表那样找尾了!!!

我们的尾就在头节点的“前边”!!!它的前驱指针就指向了我们的尾节点!!!

所以我们认为尾节点在头节点的前边!!!

再来看看这个图!!!

上代码:

void QlistPushBack(QLN *&phead, DataType x)//尾插
{
	//头节点的prev就是尾
		QLN* newnode = Buynewnode(x);
		QLN *tail = phead->prev;//即使链表为空,也能插入
		tail->next = newnode;
		newnode->prev = tail;
		newnode->next = phead;
		phead->prev = newnode;
}

这里还是要注意,链表即使为空,这个操作也能实现。

再想,链表为空的尾插,不就是头插吗,所以过程跟上边介绍的一样!!!


3)中间增加

找到节点,增加元素即可。

看代码:

void QlistInsert(QLN *&phead, DataType pos, DataType x)//插入节点
{
	QLN *temp = NULL;
	if (pos == 0)//尾插
	{
		temp = phead;
	}
	else
	{
		temp = FindNode(phead, pos);
	}
	if (temp)
	{
		QLN *newnode = Buynewnode(x);
		QLN *prev = temp->prev;
		prev->next = newnode;
		newnode->prev = prev;
		newnode->next = temp;
		temp->prev = newnode;
	}
	else
	{
		cout << "插入失败!!!" << endl;
	}
}

我们先通过查找函数,找到对应的节点,如果没找到(代表FindNode的返回值为空, 其实也就是输入的节点不合理),我们同时打印插入失败

找到了,我们这里的插入还是向该节点的前边插入节点,同样的不能直接把新节点放在该节点的前面,即temp->prev=newnode,会导致前面的节点找不到,我们还是先保存一下前一个节点,在把它们依次连接(还是连接四次)。

同样也的,由于我们是向某个节点的前边插入节点,所以我们的尾插很特殊,需要单独处理,

因为我们的尾部在头节点前面,而且我们的首节点的逻辑位置我们认为是 1,那么我们就把头节点的逻辑位置认为是0当pos等于0,且仅当在调用QlistInsert时我们把temp的值给成phead,这样我们就能用它来实现尾插了。


3.删除元素

删除元素,无非就是把对应的节点所申请的空间释放掉,再把剩余的节点连接起来,需要注意的是,当链表为空的时候,我们无须释放任何节点也不要不小心把头节点删除了。

1)头部删除

找到第一个节点(头节点的next指向的)把它释放掉,再连接新的头节点。

看代码:

void QlistPopFront(QLN *&phead)//头删
{
	//删的话只删有效的节点,所以链表为空,不能把头节点删了。
	if (phead->next == phead)
	{
		cout << "链表为空,无法删除!" << endl;
		return;
	}
	else
	{
		QLN *first = phead->next;//这样就满足了所有情况,其中只有一个节点的情况,也能很好解决
		QLN *next = phead->next->next;
		phead->next = next;
		next->prev = phead;
        first->next=first->prev=NULL;
		delete first;//free(first)
	}
}

代码中,first存的是要删除节点的地址,next存的是原来第二个节点,即新的头节点

我们先让头节点和新的首节点连接,在释放之前,我们把待删除的节点里面的指针先置为空,然后再释放掉旧的首节点

这个还是比较好理解的,我们来看一下一种特殊情况,假如我们的链表只有一个节点,把它释放掉,我们的链表能不能回到头节点的前驱和后驱指针都指向自己的状态

答案是可以的!!!我们来分析一下:(只有一个节点)

这个时候,first存的还是要删除的节点,next存的是什么??

是我们的phead!!!是头节点!!太棒了!!!

那么接下来,phead->next=next,即头节点的后继指针还指向自己,next->prev=phead,即头节点的p前驱指针还指向自己!!!

这里再次体现了,这种链表的特点!!!


2)尾部删除

找尾部,连接剩余节点,删除尾部。

看代码:

void QlistPopBack(QLN *&phead)//尾删
{
	if (phead->next == phead)//链表为空不删
	{
		cout << "链表为空,无法删除" << endl;
		return;
	}
	else
	{
		QLN *tail = phead->prev;
		QLN *prev = tail->prev;//这个是新的尾
		prev->next = phead;
		phead->prev = prev;
        tail->next=tail->prev=NULL;
		delete tail;
	}
}

同样的,链表为空我们不删,然后找尾。

头节点的前边就是尾部,记录一下它,然后找到新的尾节点,即旧尾节点的前一个节点,也记录一下它。

新尾节点的后继指针要指向头节点,头节点的前驱指针要指向新的尾节点,最后删除旧的尾节点,在这之前,先把它里面的指针置为空,功能就实现了。


3)中间删除

找到节点,连接剩余节点,删除该节点。

看代码:

void QlistDelete(QLN *&phead, int pos)//删除节点
{
	QLN *temp = FindNode(phead, pos);
	if (temp)
	{
		QLN *prev = temp->prev;
		QLN *next = temp->next;
		prev->next = next;
		next->prev = prev;
        temp->next=temp->prev=NULL;
		delete temp;
	}
	else
	{
		cout << "删除失败!!" << endl;
	}
}

找到节点后,我们需要先把该节点前面的节点后边的节点连接起来,先把待删除节点里面的指针置为空,再把待删除的节点释放掉就好了。


总的来说,增加节点和删除节点的功能,其实只用一个QlistInsert和QlistDelete就能实现对应的增加和删除的所有功能,所以,如果想快速写出一个这样的链表,增减和删除功能只写这两个函数就够了!!!


4.修改元素

看到这里,链表的功能基本已经都实现了~~~

修改元素的话,那就找到它,改掉即可。

看代码:

void QlistModify(QLN *&phead, int pos, DataType X)
{
	QLN *temp = FindNode(phead, pos);
	if (temp)
	{
		temp->data = X;
		return;
	}
	else
	{
		cout << "修改失败!!!" << endl;
	}
}

这个相对是比较简单的,就是使用FindNode函数找到节点,把他的数据域元素修改掉就好了。


5.销毁链表

这个与单链表的销毁是类似的

直接看代码:

void Qlistdestory(QLN *&phead)//销毁链表
{
	QLN *cur = phead->next;
	QLN *temp = NULL;
	while (cur != phead)
	{
		temp = cur->next;
		cur->next = cur->prev = NULL;
		delete cur;
		cur = temp;
	}
phead->next=phead->prev=NULL;
	delete phead;
	phead = cur =temp= NULL;
}

我们采用的是在释放当前节点前,把它的下一个节点的地址先记录一下,同时把当前节点里的指针都置为空(防止出现野指针,因为即使这个节点被释放,它里面的指针变量还是存在的,它存的还有其它节点的地址,节点被释放后,这块区域我们就不能使用了,所以指针最好置为空)。

循环结束的条件就是cur等于phead也就是说这个时候我们的链表已经遍历完毕了,退出循环就好了。

到最后,把我们的头节点也释放掉(因为它也是动态开辟的),再把它里面的指针置为空,再把临时创建指针都置为空。


以上,我们带头循环双链表的功能就介绍完了!!!

整体来说,这种链表是有难度的,

这个难度体现在它的结构上,我们是根据它的结构去写它的增删查改。

所以,了解他,我们必须明白它的结构是怎么样的,什么是带头,什么双向,什么是循环。

只有这样,我们才能去实现它的增删查改,去感受它的优点!!!

然后我们来搞测试!!!


测试

老规矩,做测试!!!

1)增加节点测试

void test()
{
	cout << "插入节点测试:" << endl << endl;
	QLN *phead = CreatePhead();//创建头节点
	for (int i = 0; i < 5; i++)
	{
		QlistPushBack(phead, i + 1);
	}
	cout << "尾插一些数据后,链表值为:" << endl;
	QlistPrint(phead);
	cout << endl;
	QlistPushFront(phead, 100);//头插一个100
	cout << "头插一个100后,链表值为:" << endl;
	QlistPrint(phead);
	cout << endl;
	QlistInsert(phead, 1, 300);
	cout << "用QlistInsert头插一个300后,链表值为" << endl;
	QlistPrint(phead);
	cout << endl;
	QlistInsert(phead, 0, 500);
	cout << "用QlistInsert尾插一个500后,链表值为" << endl;
	QlistPrint(phead);
	cout << endl;
	
}

运行结果:


2)删除节点测试: 

void test02()
{
	cout << "删除节点测试:" << endl << endl;
	QLN *phead = CreatePhead();//创建头节点
	for (int i = 0; i < 5; i++)
	{
		QlistPushBack(phead, i + 1);
	}
	cout << "尾插一些数据后,链表值为:" << endl;
	QlistPrint(phead);
	cout << endl;
	QlistPopFront(phead);
	cout << "头删 1 后,链表值为:" << endl;
	QlistPrint(phead);
	cout << endl;
	QlistPopBack(phead);
	cout << "尾删 5 后,链表的值为:" << endl;
	QlistPrint(phead);
	cout << endl;
	QlistDelete(phead, 1);
	cout << "用QlistDelete头删 2 后,链表的值为:" << endl;
	QlistPrint(phead);
	cout << endl;
	QlistDelete(phead, QListLen(phead));
	cout << "用QlistDelete尾删 4 后,链表的值为:" << endl;
	QlistPrint(phead);
	cout << endl;
}

运行结果:


3)修改节点测试

void test03()
{
	cout << "修改节点元素测试:" << endl << endl;
	QLN *phead = CreatePhead();//创建头节点
	for (int i = 0; i < 5; i++)
	{
		QlistPushBack(phead, i + 1);
	}
	cout << "尾插一些数据后,链表值为:" << endl;
	QlistPrint(phead);
	cout << endl;
	for (int j = 1; j <=QListLen(phead); j++)
	{
		QlistModify(phead, j, j * 3);
	}
	cout << "用Modify将各节点的元素修改后,链表的值为:"<<endl;
	QlistPrint(phead);
	cout << "销毁链表测试:" << endl;
	Qlistdestory(phead);
}

运行结果:

由于链表的销毁状况我们无法从运行框中看出来,所以我们用调试窗口看看:

这是执行QlistDestory之前,phead里面的数据:

这是执行QlistDestory之后,phead里面的数据:

明显,空间都被释放了,无法读取了。

通过这个我们发现phead释放成功了,那么同理其它节点也释放掉了。


以上,就是对带头循环双链表的全部介绍啦~~~

期待家人的点赞,关注和收藏哦~~~

需要源代码,可以去博主的仓库自取哦~~

链接在这:

琦琦的日常代码: 平时做的程序 - Gitee.com


觉得代码写的有问题,欢迎指正哦~~~

我们下期再见~~~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值