c语言实现数据结构中的带头双向循环链表

目录标题

  • 一.单向链表的不足
  • 二.带头双向链表的准备
  • 三.带头双向链表的初始化
  • 四.带头双向链表的尾插
  • 五.带头双向链表的打印
  • 六.带头双向链表头插
  • 七.判断链表是否为空
  • 八.带头双向链表尾删
  • 九.带头双向链表头删
  • 十.带头双向链表的长度
  • 十一.带头双向链表的查找
  • 十二.带头双向链表任意位置的插入
  • 十三.带头双向链表任意位置的删除
  • 十四.带头双向链表的销毁

一.单向链表的不足

我们上一篇文章中就讲解了一下单链表的简单的实现,但是在实现的过程中大家似乎也能感觉到一个问题就是我们单向链表好像也没有比顺序表方便了多少,我们单链表在在实现尾插尾删的时候也得挨个的找到尾部,我们在中间插入或者删除的时候也得挨个的找到你传过来的那个位置,而且单链表的一些函数在实现的过程中有时候还要分情况来讨论是传一级指针还是传二级指针,而且我们单链表不停的使用malloc来开辟空间的话,还会导致我们整体的效率下降,所以整体来看的话我们的单链表好像也没有占到什么便宜吧,所以我们这里就得对单链表进行一下简单让其变成带头双向循环链表。

二.带头双向链表的准备

我们首先来看看这个链表的图是怎么来表示的:
c语言实现数据结构中的带头双向循环链表_第1张图片
我们可以看到这个链表中的相邻的元素之间是用两个指针来连接的,这就是我们这里双向的体现,然后在我们这个链表的头部有一个头节点,这个节点就是起到一个指引的作用,因为我们之前的没带头的单向链表在实现的过程中会不停的改变我们外部的头指针,所以我们这里就加一个头节点,该节点指向我们链表的第一个元素,这样我们在头插的时候就不用改变我们外部的头指针,只要我们指针指向这个头节点我们就可以通过这个头节点来找到其他的元素,我们这里的头节点也有两个节点,一个节点指向后面的第一个元素,但是我们头节点前面是没有元素的,但是这个指针又不想简单的空着,所以我们这里就让这个节点指向我们的最后一个元素,同样的道理我们的最后一个元素也有两个节点,有一个指针指向倒数第二个元素,但是我们最后一个元素的后面已经没有元素了,所以我们就让这个指针指向头节点,这样的话,我们就可以很快的通过头节点找到链表吧的尾部,通过最后一个元素找到链表的头节点,那么这里就可以体现我们这里的循环的功能,那么我们这里就介绍了一下带头双向链表的一些特点,那么我们这里显然每个元素里面有很多的变量,那么我们这里就可以创建一个结构体来储存这些变脸,那么我们这里的代码就如下:

typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* prev;
	struct ListNode* next;
	LTDataType x;
}LTNode;

三.带头双向链表的初始化

首先为我们在main函数里面创建一个指针变量plist,将其值初始化为空,然后我们就执行链表的初始化,那么我们这里的初始化肯定是会改变这个变量的值的所以我们这里的函数需要的参数是一个二级指针,但是我们这个链表中的其他函数是只用一级指针的,所以我们这里就对其做出一点修改,将其改成只需要一级指针,那么我们这里是怎么来做的呢?我们只用在函数的内部返回一下我们新开辟出来的地址就可以了,然后在函数外部来进行一下接收,这样我们就可以做到即传递的是一级指针但是也可以改变外部的指针变量的值的特点,那么接下来我们再来看看这个函数的内部是如何来实现的,首先我们想一下我们这里的初始化要干嘛,首先我们的这个链表要有一个头节点,所以我们这里的初始化干的事情就是先创建出来一个头节点,然后我们这个头节点中有两个指针,但是此时我们的链表中是没有其他的元素的,所以我们这里就只能将这两个指针指向自己本省,最后我们再将节点的地址作为函数的返回值,那么我们这里的函数的具体的实现就如下:

LTNode* ListInit(LTNode* phead)
{
	LTNode* guard = (LTNode*)malloc(sizeof(LTNode));
	if (guard == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	guard->next = guard;
	guard->prev = guard;
	return guard;
}

四.带头双向链表的尾插

因为我们这里的链表是带头的,所以我们这里不管有没有元素,我们这里都不需要改变外部头指针的值,所以我们这个函数需要的是一个一级指针,同样的道理通过之前的初始化我们知道这里的链表不管有没有其他的元素,那一定是有一个头节点的,而且我们的外部的头指针还指向着这个头节点,所以我们这里传过来的一级指针是一定不能为空的,那么我们就得对其进行一下断言,防止传过来的值为空,接下来我们就实现尾插,首先我们得创建一个节点,因为用到这个功能的地方还很多,所以我们这里就将其合并成为一个函数那么代码如下:

LTNode* BuyListNode(LTDataType num)
{
	LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
	if (phead == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	phead->next = phead->prev = NULL;
	phead->x = num;
	return phead;
}

然后再通过头节点的prv指针来找到最后一个元素的地址,再将最后一个元素中的next的值改成newnode的地址,再将newnode中的prev指向之前的最后一个值,这样我们的newnode就成为了当前的最后一个元素,因为我们这里是循环的所以我们还要将头节点的prev指向newnode和newnode的next指向头节点,这样我们的尾插就完成了,那么我们的代码就如下:

void ListPushBack(LTNode* phead, LTDataType x)
{
	LTNode* cur = BuyListNode(x);
	phead->prev->next = cur;
	cur->prev = phead->prev;
	phead->prev = cur;
	cur->next = phead;
}

但是我们这样写的话可读性就优点低,我们可以再创建一个指针变量来专门的指向我们的最后一个元素,那么这样的话我们的代码的可读性就会大大的提高:

void ListPushBack(LTNode* phead, LTDataType x)
{
	LTNode* newnode = BuyListNode(x);
	LTNode* at_last = phead->prev;
	at_last->next = newnode;
	newnode->prev = at_last;
	phead->prev = newnode;
	newnode->next=phead;
}

那么这样的话我们的可读性就提高了许多,那么我们这里再来测试一下我们这里写的代码:

void ListPrint(LTNode* phead)
{
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d->", cur->x);
		cur = cur->next;
	}
	printf("null");
}

我们来看看这个代码运行的结果为:
c语言实现数据结构中的带头双向循环链表_第2张图片
那么我们这里运行的结果跟我们预测的是一模一样的所以我们这里的函数实现是正确的。那么我们接着往下看。

五.带头双向链表的打印

我们将数据插入进去了,但是看不到那是不是就特别的尴尬啊,所以我们这里就来实现一下这个函数,首先我们得知道一件事就是我们这里的链表与之前的不带头的单向链表还是有那么点区别的就是,我们这里的尾部事没有空指针的,我们之前在不带头的单链表中实现这个函数的原理事链表的末尾指向的是一个空指针,所以我们以此作为结束的根据,但是我们这里就不能这么做,因为我们这里是循环的,那我们该怎么做呢?我们这里将打印完之后我吗的指针就又回到了起点,嗯?起点,既然我们这里没有空指针来作为结束的标志,那我们这里能不能把回到开头来作为结束的标志呢?答案是可以的,因为我们的头节点里面并没有数据,所以我们一开始就创建一个指针变量cur将其值赋值为第一个元素的地址,而不是头节点,然后我们就创建一个循环,在该循环里面打印数据,并且修改cur的值,让其指向下一个元素,当cur的值等于头节点的话我们的循环就结束了,这样的话我们的打印也就跟着结束了,那么我们的代码就如下:

void ListPrint(LTNode* phead)
{
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d->", cur->x);
		cur = cur->next;
	}
	printf("null");
}

六.带头双向链表头插

既然我们这里有了尾插,那么我们这里也就会有头插,那我们头插就不用去管循环之类的事情,因为我们这里的头插插的是头节点的后面,并不会影响循环,所以我们这里还是想创建一个节点newnode,然后再创建一个指针变量scend将其值赋值为第二个元素的地址,那么我们这里的插入的逻辑就是将newnode的next指向scend,再将scend的prev指向newnode,那么这样我们的newnode就和当前第一个元素连接了起来,那么我们剩下要做的就是将newnode和头节点连接起来,那么这里的逻辑就是一样的,就不多说了直接看代码:

void ListPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = BuyListNode(x);
	LTNode* scend = phead->next;
	newnode->next = scend;
	scend->prev = newnode;
	phead->next = newnode;
	newnode->prev = phead;
}

那么我们这里再来测试一下这个函数的实现是否是正确的:

#include"List.h"
int main()
{
	LTNode* plist = NULL;
	plist = ListInit(plist);
	ListPushBack(plist, 10);
	ListPushBack(plist, 20);
	ListPushBack(plist, 30);
	ListPushBack(plist, 40);
	ListPushFront(plist, 50);
	ListPushFront(plist, 60);
	ListPushFront(plist, 70);
	ListPrint(plist);
	return 0;
}

我们来看看这个代码的运行结果:
c语言实现数据结构中的带头双向循环链表_第3张图片
那么这里我们的函数实现就是正确的。

七.判断链表是否为空

我们来想一下我们链表为空的时候有什么样的特点,是不是就只有一个头节点啊,那么我们这里的头节点中prev指针是不是就是指向自己的啊,那么这个就可以作为我们判断链表是否为空的一个一句,如果为空的话我们就返回0,如果不为空的话我们就返回其他的值,那么我们这里就可以直接返回一个表达式:phead->next!=phead当我们相等的时候我们这个表达式的返回值就是0,那么我们的函数实现就是这样:

bool Listempty(LTNode* phead)
{
	return phead->next != phead;
}

八.带头双向链表尾删

实现我们这个函数首先做的第一步就是得判断一下我们的链表是否为空,当为空的时候我们就不能再进行删除了,所以这里就得用到我们上面实现的判断链表是否为空的这个函数,好判断完之后我们就来实现尾插,这里尾插就得先找到倒数第二个和倒数第一个元素的地址,将其记录下来,将倒数第一个元素进行释放,然后再把倒数第二个元素和我们的头节点来构成一个循环,那么这里的逻辑就很简单我们直接来看看代码:

void ListPopBack(LTNode* phead)
{
	assert(phead);
	assert(Listempty);
	LTNode* tail = phead->prev;//倒数第一
	LTNode* prev = tail->prev;//倒数第二
	free(tail);
	tail = NULL;
	prev->next = phead;
	phead->prev = prev;
}

然后我们再来测试一下这个代码:

#include"List.h"
int main()
{
	LTNode* plist = NULL;
	plist = ListInit(plist);
	ListPushBack(plist, 10);
	ListPushBack(plist, 20);
	ListPushBack(plist, 30);
	ListPushBack(plist, 40);
	ListPushFront(plist, 50);
	ListPushFront(plist, 60);
	ListPushFront(plist, 70);
	ListPopBack(plist);
	ListPopBack(plist);
	ListPrint(plist);
	return 0;
}

我们来看看这个代码的运行结果:
c语言实现数据结构中的带头双向循环链表_第4张图片
我们可以看到当有元素的时候我们的删除函数是正确的,然后我们再测试一下当我们的元素个数为0的时候再来执行删除会不会报错:

#include"List.h"
int main()
{
	LTNode* plist = NULL;
	plist = ListInit(plist);
	ListPushFront(plist, 70);
	ListPopBack(plist);
	ListPopBack(plist);
	ListPrint(plist);
	return 0;
}

那当我们运行起来之后我们就可以看到我们这里报出来了错误:

c语言实现数据结构中的带头双向循环链表_第5张图片
那么说明我们这里确实是有预警的作用。

九.带头双向链表头删

我们这里的头删和尾删是一样的,得先判断一下我们的链表是否为空,然后再将这里的第一个元素进行删除,因为第一个元素是和第二个元素连在一起的,所以我们这里就创建一个指针记录一下第二个元素的位置,然后我们就先将第一个元素释放掉,然后再将头节点和第二个元素连接起来,那么我们这里的代码就是这样:

void ListPopFront(LTNode* phead)
{
	assert(phead);
	assert(Listempty(phead));
	LTNode* scend = phead->next->next;
	free(phead->next);
	phead->next = scend;
	scend->prev = phead;
}

我们这里可以来测试一下这个函数的实现是否是正确的:

#include"List.h"
int main()
{
	LTNode* plist = NULL;
	plist = ListInit(plist);
	ListPushFront(plist, 70);
	ListPushFront(plist, 60);
	ListPushFront(plist, 50);
	ListPushFront(plist, 40);
	ListPushFront(plist, 30);
	ListPopFront(plist);
	ListPopFront(plist);
	ListPrint(plist);
	return 0;
}

我们来看一下运行的结果:
c语言实现数据结构中的带头双向循环链表_第6张图片
那么我们这里的代码实现就是真确的,但是这里有些小伙伴们可能就会优点疑惑啊,我们这里要是只有一个元素,那你这还是真确的吗?你确定这里不需要分开来讨论吗?那么这里大家可以想象一下,当我们只有一个元素的时候,我们这里的scend会指向的是谁?phead->next->next,因为我们这里的循环是连续的,所以我们这里的scend指的就是我们的phead对吧,如果是phead的话我们下面的操作是不是就是将phead的两个指针都指向自己啊,所以我们这里只有一个元素的话,也不会出现问题。

十.带头双向链表的长度

在有些书籍上面喜欢把头节点中的存放数据的地方用来存放我们链表的长度,那这么做是否是对的呢?我只能说有些情况是对的,我们的链表的长度永远都是整型,但是我们链表中的元素存放的数据一定是整型吗?不一定吧,他可能是其他的类型,比如说指针类型,浮点型。结构体类型等等,那如果是这些类型的话,你再用头节点来顺便求长度的话是不是就会出错啊,所以我们这里就不建议大家采用这样的方法,那么我们这里在求链表的长度的时候就最好采用循环遍历的方式,跟我们之前实现链表的打印是差不多的,那么我们的代码就如下:

int ListSize(LTNode* phead)
{
	LTNode* cur = phead->next;
	int size = 0;
	while (cur != phead)
	{
		size++;
		cur = cur->next;
	}
	return size;
}

我们来测试一下这个函数的正确确性:

#include"List.h"
int main()
{
	LTNode* plist = NULL;
	plist = ListInit(plist);
	ListPushFront(plist, 70);
	ListPushFront(plist, 60);
	ListPushFront(plist, 50);
	ListPushFront(plist, 40);
	ListPushFront(plist, 30);
	ListPopFront(plist);
	ListPopFront(plist);
	int size = ListSize(plist);
	ListPrint(plist);
	printf(" %d", size);
	return 0;
}

我们来看看运行的结果:
c语言实现数据结构中的带头双向循环链表_第7张图片
这里的元素个数确实是3那么我们这里的函数实现就是正确的。

十一.带头双向链表的查找

我们这里查找的思路也和之前打印的思路是一样的,通过循环遍历来一个一个的查找元素,如果找到了我们就返回这个元素的地址,如果没有找到就返回一个空指针,那么我们这里的代码就如下:

LTNode* ListFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->x == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

那么我们这里就可以来测试一下:

#include"List.h"
int main()
{
	LTNode* plist = NULL;
	plist = ListInit(plist);
	ListPushFront(plist, 70);
	ListPushFront(plist, 60);
	ListPushFront(plist, 50);
	ListPushFront(plist, 40);
	ListPushFront(plist, 30);
	LTNode*cur=ListFind(plist, 60);
	printf("%d", cur->x);
	return 0;
}

那么我们这里运行起来就可以看到这里的代码运行正确:
c语言实现数据结构中的带头双向循环链表_第8张图片

十二.带头双向链表任意位置的插入

我们这里默认的插入是前插,我们先创建一个节点newnode,再找到这个位置的前一个元素的地址,然后我们创建一个指针变量prev来记录这个地址,接着我们将这个数据插入到这个链表里面,那么这里我们就让prev的next指向newnode,再让newnode的prev指向prev,这样我们就让前面的元素和后面的元素连接了起来,然后我们再让newnode和pos位置的元素连接起来,那么这里操作也与上面的类似,那么我们这里就不多说了我们直接看代码:

void ListInsert( LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newnode = BuyListNode(x);
	LTNode* prev = pos->prev;
	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
}

那么这里大家要注意的一点就是当我们传过来的地址是头部的话,那我们这里插入的是哪里呢?这里大家仔细地推导一下就可以发现这里插入的地方就是在我们链表的尾部,因为头节点的前面是链表的尾部,那么这里大家在使用的时候得注意一下。我们来测试一下看看是否是对的:

#include"List.h"
int main()
{
	LTNode* plist = NULL;
	plist = ListInit(plist);
	ListPushFront(plist, 70);
	ListPushFront(plist, 60);
	ListPushFront(plist, 50);
	ListPushFront(plist, 40);
	ListPushFront(plist, 30);
	LTNode*cur=ListFind(plist, 60);
	ListInsert(cur, 10 * cur->x);
	ListPrint(plist);

	return 0;

我们运行一下:
c语言实现数据结构中的带头双向循环链表_第9张图片

十三.带头双向链表任意位置的删除

那么我们这里的任意位置的删除的思路也十分的类似就是先找到指定位置前的元素的地址,再找到该位置之后的元素的地址,然后再释放掉该位置的元素,再将前一个元素和后一个元素连接起来,那么这就非常的简单了哈,大家看看代码就应该能够明白:

void ListPop( LTNode* pos)
{
	assert(pos);
	LTNode* prev = pos->prev;
	LTNode* next = pos->next;
	free(pos);
	prev->next = next;
	next->prev = next;
}

我们来测试一下:

#include"List.h"
int main()
{
	LTNode* plist = NULL;
	plist = ListInit(plist);
	ListPushFront(plist, 70);
	ListPushFront(plist, 60);
	ListPushFront(plist, 50);
	ListPushFront(plist, 40);
	ListPushFront(plist, 30);
	LTNode*cur=ListFind(plist, 60);
	ListPop(cur);
	ListPrint(plist);
	return 0;
}

那么我们的运行结果就如下:
c语言实现数据结构中的带头双向循环链表_第10张图片

十四.带头双向链表的销毁

我们对链表的操作结束之后我们就得将其进行销毁,那么我们这里的销毁就是边遍历边销毁,这里的思路也和我们打印的时候的思路是一样的,但是我们在遍历完了还得将我们的头节点进行释放,那么我们的代码如下:

LTNode* ListDestory(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
}

那么我们这个函数是不会改变外部指针的,所以这里大家使用这个函数的时候要自己来改变这个指针的值,将其置为空。

点击此处获取代码

你可能感兴趣的:(C语言实现数据结构,链表,数据结构,c语言)