双向带头循环链表的介绍与实现

双向带头循环链表是结构上最为复杂的一种链表,但是正是因为其结构复杂,也给这种链表带来了其他链表所没有的优势。今天我们就介绍一下双向带头循环链表并且完成其构建。

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

1.1单向不带头不循环链表

在解释双向带头循环链表之前,我们先来看看最淳朴的链表,以方便做对比。它的结构特点是:单向、不带头且没有循环。结构图如下:

 这种链表的特点是结构简单,便于理解。每个节点只有一个指针指向下一个节点,而不会保存上一个节点的地址。其次,它没有头节点,第一个节点就进行了对数据的保存。最后,链表没有循环结构,最后一个节点的后置指针保存的是空指针,以表示链表的结束。

介绍完了最简单的链表,我们还要了解双向链表、带头链表、循环链表。他们代表着三种不同形式的链表,搞清楚这三个链表之后,双向带头循环链表就容易理解了。

1.2双向链表

双向链表指每个节点除了有指向下一个节点的指针外,还增加了一个指针用于指向上一个节点,它的结构图如下:

相比基础的链表,双向链表在随机位置删除/增加节点时,不需要再进行遍历,而是可以通过前置指针找到要删除/增加节点的前后节点,以O(1)的时间复杂度完成任意位置节点的删除/增加节点 。

1.3带头节点链表

带头节点链表指链表的第一个节点不存储数据,而是单单用来作为链表的“头”。这个节点又称为哨兵位。它的结构图如下:

 

带上了头节点的链表,在对第一个节点进行操作时,会方便不少,若没有头节点,则在链表为空时要改变指向链表头部的指针,而有了头节点,只需要改变头节点的指针即可。 

1.4循环链表

循环链表的最后一个节点的后置指针并非保存空指针,而是保存了第一个节点的地址。它的结构图如下:

 对于单循环链表,它的优势在于可以以任意节点为起点,遍历链表中的所有节点,而循环的更多 优势则要在双向带头循环链表中体现。

1.5双向带头循环链表

 结合上面三种链表的特性,就可以得到双向带头循环链表了。它的结构图如下:

 虽然这种链表的结构看起来十分复杂,但是它也包含了上面三种链表的优势,这种优势在我们实现的时候就可以很好的体现出来了。

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

定义链表节点

typedef int ListDataType;
typedef struct ListNode
{
	struct ListNode* prev;//前置指针
	struct ListNode* next;//后置指针
	ListDataType data;//数据
}list;

因为是双向链表,除了后置指针next,我们还要加上前置指针prev以指向前一个节点。并且利用typedef对数据的类型进行定义,以便更改数据域保存的数据类型。

初始化头节点

list* list_init()
{
	list* head = (list*)malloc(sizeof(list));//开辟头节点
	if (head == NULL)//判断头节点是否开辟成功
	{
        printf("malloc fail");
		exit(-1);
	}
	head->next = head;//后置指针指向自己
	head->prev = head;//前置指针也指向自己
	return head;//返回初始化好的头节点
}

初始化函数使用malloc开辟一块动态空间以保存头节点。并且对头节点初始化,让其前置、后置指针都指向自己,这表示链表为空。最后返回头节点指针,我们在函数外部使用一个指针即可接收初始化好的头节点。下面是初始化好的链表的图示:

摧毁链表

void list_destroy(list* head)
{
	assert(head);//断言判断头节点是否存在
	while (head->next != head)//当链表只有头节点时退出循环
	{
		list_pop_back(head);//调用尾删函数
	}
	free(head);//释放头节点
}

析构函数在链表不止有头节点一个节点时,先进入循环进行尾删操作。由于链表是循环结构,头节点的后置指针指向自己时,表示链表只剩头节点了。此时退出循环,再释放掉头节点。但要注意函数外的指针就成了野指针,要对其置空才算完成了链表的摧毁。

尾插

void list_push_back(list* head,ListDataType x)
{
	assert(head);
	list* tail = head->prev;//找尾
	list* newnode = (list*)malloc(sizeof(list));//创建新节点
	if (newnode == NULL)
	{
		printf("malloc fail");
		exit(-1);
	}

	newnode->next = head;//连接节点
	head->prev = newnode;
	newnode->prev = tail;
	tail->next = newnode;

	newnode->data = x;//给新结点的数据域赋值
}

 由于链表是双向循环的,所以我们通过头指针的前置指针可以轻松的找到尾节点,新的尾节点与旧的尾节点和头节点相连,就可以完成尾插了。

尾删

void list_pop_back(list* head)
{
	assert(head&&head->next!=head);//断言保证链表中除了头节点至少有一个节点
	list* backprev = head->prev->prev;//保存倒数第二个节点
	free(head->prev);//释放尾节点

	backprev->next = head;//连接节点
	head->prev = backprev;
}

与删除有关的函数内的断言除了确保头结点的有效性,也要保证链表中至少有一个节点存在,否则可能将头节点释放,产生严重错误。断言后通过前置指针找到并保存倒数第二个节点,再让倒数第二个节点与头节点相连就可以完成尾删了。

头插

void list_push_front(list* head,ListDataType x)
{
	assert(head);
	list* next = head->next;//保存头节点的下一个节点
	list* newnode = (list*)malloc(sizeof(list));
	if (newnode == NULL)
	{
		printf("malloc fail");
		exit(-1);
	}

	head->next = newnode;//连接节点
	newnode->prev = head;
	next->prev = newnode;
	newnode->next = next;

	newnode->data = x;//赋值
}

头插先保存头节点的下一个节点,再创建新节点,让新节点连接上头节点与next节点,就完成了头插。

头删

void list_pop_front(list* head)
{
	assert(head && head->next != head);//断言
	list* next = head->next;//保存要释放的节点

	head->next->next->prev = head;//连接节点
    head->next = head->next->next;
	

	free(next);//释放
}

头删也要通过断言确保链表中至少有一个保存数据的节点,否则可能删除头节点。然后先保存要删除的节点,连接节点就可以完成头删了。

打印链表

void list_print(list* head)
{
	assert(head);
	list* cur = head->next;//从头结点的下一个位置开始
	while (cur != head)//当指针指向头节点时结束循环
	{
		printf("%d ", cur->data);//此处以int型数据为例输出
		cur = cur->next;//指针指向下一个节点
	}
	printf("\n");
}

 由于有头节点,所以输出时遍历链表的节点应该从头结点的下一个节点开始遍历。又因为是循环结构,当cur指向头结点时,表示链表完成了一次遍历,此时退出循环。 

查找

list* list_find(list* head, ListDataType x)
{
	assert(head);
	list* cur = head->next;//保存头节点的下一个节点
	while (cur != head)//当遍历一遍还没有找到,退出循环
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;//cur指向下一个节点
	}
	return NULL;//没有找到则返回空
}

查找同样要通过断言确保从头结点的下一个节点开始,遍历链表,若找到就返回节点的指针,否则当指针与head相等,表示链表遍历了一遍还没有找到,退出循环并返回NULL。

在指定节点前插入一个新节点

void list_insert(list * pos, ListDataType x)
{
	assert(pos);
	list* prev = pos->prev;//保存前一个节点,以便连接

	list* newnode = (list*)malloc(sizeof(list));//创建新节点
	if (newnode == NULL)
	{
		printf("malloc fail");
		exit(-1);
	}

	prev->next = newnode;//连接节点
	newnode->prev = prev;
	pos->prev = newnode;
	newnode->next = pos;

	newnode->data = x;//赋值
}

 首先保存指定位置的前一个节点,以便于新节点的连接,然后创造新结点并连接节点即可。

删除指定节点

void list_erase(list* pos)
{
	assert(pos&&pos->next!=pos);//断言

	pos->prev->next = pos->next;//连接前后节点
	pos->next->prev = pos->prev;

	free(pos);//释放
}

连接要删除的节点前后的节点再释放要删除的节点的空间即可完成删除。

3.总结

通过构造双向带头循环链表我们可以发现,虽然这种链表的结构复杂:

1.带有头节点。

2.每个节点都包含前置指针和后置指针。

3.最后一个节点的后置节点指向了头节点,头节点的前置指针指向了最后一个节点,形成双向循环。

通过上面的实现阶段可以看出,虽然这种链表的结构十分复杂,但是极大程度的降低了实现的难度:因为有了循环结构,尾插的时候不需要找尾,可以通过头指针的前置指针直接找到尾节点,进行插入。有了双向结构,在任意节点的前面插入节点时,也不需要通过遍历链表寻找前一个指针,而是可以直接通过前置指针找到前一个节点,完成插入。有了头节点,也不需要在插入时额外判断链表是否为空。

并且这种结构在使用时也有极大的优势:省去了许多遍历(例如找尾,找前一个节点),让这种链表在操作时的时间复杂度大大降低,相比最简单的链表在效率上有了极大的提升。

不过这种结构也有小小的缺点,因为前置指针的存在,每创建一个节点,就多花费了一个指针的空间,不过这也无法否定双向带头循环链表是十分实用的。

 

 

 

 

 

  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值