【学习笔记】双向带头循环链表(c语言)

本文详细介绍了双向带头循环链表的结构、分类及其特点,包括单向与双向、带头与不带头、循环与不循环的组合。重点讲解了如何实现双向带头循环链表的初始化、打印、插入、删除和销毁操作,以及其实战中的优势和使用案例。

1.链表

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。


2.链表分类

链表的结构多种多样有三个大分类:

1.单向、双向

2.带头、不带头

3.循环、不循环

而三组之间又可以互相组合,使得链表的结构多达8种,本篇的单链表主要是指单向不带头不循环链表,这也是本篇的重点。

2.1单向链表

单向链表:指链表的链接方向是单向的,对链表的访问要通过顺序读取从头部开始,只能通过前一个结点找到后一结点,而不能从后一结点找到前一结点。

2.2双向链表

双向链表:指链表的每个数据中结点都有两个指针,分别指向直接后继和直接前驱,它能从双向链表中的任意一个结点开始,并且可以方便地访问它的前驱结点和后继结点。

2.3不带头链表

不带头链表:指链表以一个结点结构的指针开头,我们存储的数据直接从该指针指向的位置存储,该链表实现相较于带头结点稍微复杂,是各oj题与面试题的常客。

2.4带头链表

带头链表:指链表的以一个不存任何数据的结点开始,我们称之为头结点,而我们存储的数据则从头节点的下一结点开始存储,这样就能有效地避免因链表为空带来的各种问题。

2.5不循环链表

不循环链表:指链表的最后一个结点指向空,使得当链表走到空时我们就可以知道链表已经走到了尽头。

2.6循环链表

循环链表:指链表的最后一个结点指向链表的头结点,从而在逻辑形态中就像一个完整的闭环。


3.双向带头循环链表

3.1双向带头循环链表概念

顾名思义,双向带头循环链表就是每个结点有两个指针(双向链表特征),链表最后一个结点指向链表的头结点(循环链表的特征)且拥有哨兵位的链表(带头链表特征)。实际上就是一个简单的组合,但这样的组合所带来的优点相比于单链表来说呈几何被增长。

3.2双向带头循环链表与单链表对比

注:这里的单链表指(不带头不循环单向链表)

1.单链表无法通过后一指针找到前一指针,而双向带头循环链表可以

2.单链表找到尾结点需要遍历链表复杂度是O(n),而双向带头循环链表的头结点的前一结点就是链表的尾结点复杂度是O(1)。

3.单链表的头插等操作会对链表指针进行频繁操作,因此我们需要传二级指针,而单链表因为有头结点的存在,所以不需要对创建的链表指针更改,传参只用传一级指针即可


4.双向带头循环链表实现

4.1双向带头循环链表结构

typedef int ListDataType;
typedef struct ListNode {								
	ListDataType val;
	struct ListNode* prev;
	struct ListNode* next;
}ListNode;

ListNode:链表头结点代称。

ListDataType:指链表中每个结点存储的数据类型,只需要改动typedef int SListDataType 中的int就能该变节点内存放的数据类型。

val:存放类型结构体变量名。

next:指向下一结点的指针。

prev:指向前一结点的指针。

4.2双向带头循环链表功能实现

文件:"List.c"

4.2.1双向带头循环链表初始化

ListNode* ListInit()					//双向链表初始化
{
	ListNode* head = (ListNode*)malloc(sizeof(ListNode));
	assert(head);
	head->next = head;                  //下一结点的指针指向自己
	head->prev = head;                  //前一结点的指针指向自己  
}

4.2.2双向带头循环链表打印

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

4.2.3双向带头循环链表尾插

void ListPushBack(ListNode* plist, ListDataType x)		//双向链表尾插
{
	assert(plist);
	ListNode* tail = plist->prev;
	ListNode* head = plist;
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));	//开辟新结点
	assert(newnode);
	newnode->val = x;
	tail->next = newnode;					//尾结点的next指向新结点
	newnode->prev = tail;					//新节点的prev指向尾结点
	newnode->next = head;					//新节点的next指向链表头
	head->prev = newnode;					//表头的prev指向新结点
}

4.2.4双向带头循环链表尾删

void ListPopBack(ListNode* plist)			//双向链表尾删
{
	assert(plist);
	ListNode* tail = plist->prev;
	assert(plist != tail);
	ListNode* tailprev = tail->prev;
	ListNode* head = plist;
	tailprev->next = plist;					//尾部前一结点的next指向链表头
	head->prev = tailprev;					//表头的prev指向尾部前一结点
	free(tail);
}

4.2.5双向带头循环链表头插

void ListPushFront(ListNode* plist, ListDataType x)			//双向链表头插
{
	assert(plist);
	ListNode* head = plist;
	ListNode* headnext = plist->next;
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	assert(newnode);
	newnode->val = x;
	head->next = newnode;					//链表头的next指向新节点
	newnode->prev = head;					//新节点的prev指向链表头
	newnode->next = headnext;				//新节点的next指向原来链表头的next
	headnext->prev = newnode;				//原来链表头的prev指向新节点
}

4.2.6双向带头循环链表头删

void ListPopFront(ListNode* plist)			//双向链表头删
{
	assert(plist);
	ListNode* head = plist;
	ListNode* headnext = plist->next;
	assert(plist != headnext);
	ListNode* newnext = headnext->next;
	newnext->prev = head;					//链表头的下下结点的prev指向链表头
	head->next = newnext;					//链表头的next指向链表头的下下结点
	free(headnext);
}

4.2.7双向带头循环链表查找

ListNode* ListFind(ListNode* plist, ListDataType x)			//双向链表查找
{
	assert(plist);
	ListNode* head = plist;
	ListNode* cur = head->next;
	while (cur != head)						//遍历链表查找val
	{
		if (cur->val == x)
		{
			return cur;						//找到返回所在位置的指针
		}
		cur = cur->next;
	}
	return NULL;							//找不到返回NULL
}

4.2.8双向带头循环链表按位插入

void ListInsert(ListNode* pos, ListDataType x)				//双向链表位插
{
	assert(pos);
	ListNode* posnext = pos->next;
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	assert(newnode);
	newnode->val = x;			
	pos->next = newnode;					//插入位置的next指向新结点
	newnode->prev = pos;					//新结点的prev指向插入位置
	newnode->next = posnext;				//新结点的next指向插入位置结点的下一结点
	posnext->prev = newnode;				//插入位置结点的下一结点的prev指向新结点
}

4.2.9双向带头循环链表按位删除

void ListErase(ListNode* pos)				//双向链表位删
{
	assert(pos);
	ListNode* posprev = pos->prev;
	assert(pos != posprev);
	ListNode* posnext = pos->next;
	posprev->next = posnext;				//删除位置的前一结点的next指向删除位置的后一结点
	posnext->prev = posprev;				//删除位置的后一结点的prev指向删除位置的前一结点
	free(pos);
}

4.2.10双向带头循环链表销毁

void ListDestroy(ListNode* plist)				//双向链表销毁
{
	assert(plist);
	ListNode* head = plist;
	ListNode* cur = head->next;
	while (cur != head)							//遍历销毁每个存储结点
	{
		ListNode* next = cur->next;
		free(cur);
		cur = next;
	}	
	free(head);									//销毁哨兵位结点		
}

4.3双向带头循环链表头文件

文件:"List.h"

#define _CRT_SECURE_NO_WARNINGS 1

#pragma once											//防止重定义

#include<stdio.h>										//头文件
#include<malloc.h>
#include<assert.h>

typedef int ListDataType;								//双向链表存储数据类型

typedef struct ListNode {								//双向不带头循环链表结构
	ListDataType val;
	struct ListNode* prev;
	struct ListNode* next;
}ListNode;

ListNode* ListInit();									//初始
void ListPrint(ListNode* plist);						//打印
void ListPushBack(ListNode* plist, ListDataType x);		//尾插
void ListPopBack(ListNode* plist);						//尾删
void ListPushFront(ListNode* plist, ListDataType x);	//头插
void ListPopFront(ListNode* plist);						//头删
ListNode* ListFind(ListNode* plist, ListDataType x);	//查找
void ListInsert(ListNode* pos, ListDataType x);			//位插
void ListErase(ListNode* pos);							//位删
void ListDestroy(ListNode* plist);						//销毁

4.4双向带头循环链表测试

文件:"List.h"

#include"List.h"

void test2()
{
	ListNode* list;
	list = ListInit();				//双向链表初始化

	ListInsert(list, 1);			//双向链表插入
	ListInsert(list, 2);
	ListInsert(list, 3);
	ListInsert(list, 4);
	ListPrint(list);				

	ListNode* pos = ListFind(list, 2);		//双向链表查找和删除
	ListErase(pos);
	ListPrint(list);

	pos = ListFind(list, 1);
	ListErase(pos);
	ListPrint(list);
	
	pos = ListFind(list, 4);
	ListErase(pos);
	ListPrint(list);

	pos = ListFind(list, 3);
	ListErase(pos);
	ListPrint(list);

	//ListErase(list);			//过量删除测试
	ListDestroy(list);			//双向链表销毁
}

void test1()
{
	ListNode* list;
	list = ListInit();			//双向链表初始化

	ListPushBack(list, 0);		//双向链表尾插
	ListPushBack(list, 1);
	ListPushBack(list, 2);
	ListPushBack(list, 3);
	ListPrint(list);

	ListPopBack(list);			//双向链表尾删
	ListPrint(list);
	ListPopBack(list);
	ListPrint(list);
	ListPopBack(list);
	ListPrint(list);
	ListPopBack(list);
	ListPrint(list);
	//ListPopBack(list);		//过量删除报错

	ListPushFront(list, 1);		//双向链表头插
	ListPushFront(list, 2);
	ListPushFront(list, 3);
	ListPushFront(list, 4);
	ListPrint(list);

	ListPopFront(list);			//双向链表头删
	ListPrint(list);
	ListPopFront(list);
	ListPrint(list);
	ListPopFront(list);
	ListPrint(list);
	ListPopFront(list);
	ListPrint(list);
	//ListPopFront(list);		//过量删除报错

	ListDestroy(list);			//双向链表销毁
}

int main()
{
	test1();
	test2();
	return 0;
}

✨写在最后

⏱笔记时间:2021_10_3

🌐代码:Gitee:朱雯睿 (zhu-wenrui) - Gitee.com

               Github:Zero0Tw0 · GitHub

💻代码平台:Visual Studio2019

<think>嗯,用户问的是如何在循环链表中删除头节点。首先,我得回忆一下循环链表的结构。循环链表分为单向和双向两种,不过用户没有特别说明,可能需要都考虑一下。根据引用3和4的内容,单循环链表双向循环链表的结构不同,删除操作也会有差异。 先考虑单循环链表的情况。单循环链表的尾节点指向头节点,形成一个环。删除头节点的话,需要找到头节点的前一个节点,也就是尾节点,然后调整指针。比如原来的结构是尾节点->头节点->第二个节点...->尾节点。删除头节点后,尾节点应该指向第二个节点,成为新的头节点。这时候需要处理指针的重新连接,还要释放原来的头节点内存。不过单链表中找前驱节点可能需要遍历整个链表,直到找到指向头节点的节点,也就是尾节点。这一步可能效率不高,但循环链表的结构允许从任意节点出发遍历,所以可能需要从头节点开始,一直找到尾节点。 然后看引用2中的代码,有删除节点的例子。比如删除特定数据的节点,或者删除指定节点。不过单循环链表的头节点删除可能需要调整尾节点的next指针。比如,假设链表不为空,找到尾节点后,将尾节点的next指向原来的头节点的下一个节点,然后释放原头节点。同时,如果链表只有一个节点,删除后链表就为空了,这时候需要处理特殊情况。 再考虑双向循环链表的情况,比如引用4提到的带头节点的双向循环链表。这种情况下,每个节点都有前驱和后继指针,删除头节点可能更简单,因为可以直接通过前驱指针找到尾节点。例如,头节点的前驱是尾节点,头节点的后继是第二个节点。删除头节点时,需要将尾节点的next指向第二个节点,第二个节点的prev指向尾节点。如果存在哨兵节点(头节点不存储数据),可能需要不同的处理方式,但用户的问题可能是指普通双向循环链表。 用户的问题可能没有区分单双向,所以需要分别回答两种情况。另外,要处理链表为空的情况,避免空指针操作。例如,在删除之前检查链表是否为空,或者只有一个节点的情况,这时候删除后链表变为空。 然后看引用中的代码,比如引用2中的删除节点函数,比如删除以数据为条件的节点,或者以节点指针删除。比如,在单链表中删除节点需要找到前驱节点,然后调整指针。循环链表的情况类似,但需要考虑循环的连接。 总结步骤的话,对于单循环链表: 1. 检查链表是否为空,如果为空则无法删除。 2. 如果链表只有一个节点,删除后链表为空。 3. 否则,找到尾节点(即头节点的前驱)。 4. 将尾节点的next指向头节点的下一个节点。 5. 释放原头节点的内存。 6. 新的头节点变为原头节点的下一个节点。 对于双向循环链表: 1. 检查链表是否为空。 2. 如果只有一个节点,处理后链表为空。 3. 否则,原头节点的前驱(尾节点)的next指向原头节点的下一个节点。 4. 原头节点的下一个节点的prev指向尾节点。 5. 释放原头节点。 6. 新的头节点是原头节点的下一个节点。 需要给出具体的代码示例,比如用C语言或Java,但用户引用了Java和C的示例,可能需要选择一种语言。不过用户的问题没有指定语言,所以可能需要用伪代码或者说明步骤。不过引用中的例子有C和Java,可能更倾向于C语言的结构。 另外,注意边界条件,比如删除后链表为空的情况,避免内存泄漏或者指针错误。例如,在单循环链表中,当只有一个节点时,删除后需要将链表置空。 可能还需要考虑是否有哨兵节点。比如引用4提到的带头节点的双向循环链表,这种情况下头节点可能不存储数据,删除操作可能不同。但用户的问题可能是指普通的循环链表,即头节点是第一个数据节点。 最后,根据用户提供的引用,特别是引用2和3中的代码示例,可以提取相关的删除逻辑,调整到循环链表的情况。例如,在单循环链表中,找到尾节点可能需要遍历,直到某个节点的next指向头节点。 总之,步骤大致是找到前驱节点,调整指针,释放内存,处理特殊情况。需要分单双链表两种情况说明,并给出代码示例。</think>在循环链表中删除头节点需要根据链表类型(单向/双向)采取不同操作,关键步骤包括定位前驱节点、调整指针关系和内存管理。以下分两种情况说明: --- ### 一、单向循环链表删除头节点 **核心步骤**: 1. 定位尾节点(即头节点的前驱) 2. 将尾节点的`next`指向原头节点的下一个节点 3. 释放原头节点内存 **C语言实现**: ```c Node* deleteHead(Node* head) { if (head == NULL) return NULL; // 空链表处理 Node* tail = head; while (tail->next != head) { // 定位尾节点 tail = tail->next; } if (tail == head) { // 只有一个节点的情况 free(head); return NULL; } tail->next = head->next; // 调整尾节点指针 Node* newHead = head->next; free(head); return newHead; // 返回新头节点 } ``` --- ### 二、双向循环链表删除头节点 **核心步骤**: 1. 直接通过前驱指针定位尾节点 2. 调整尾节点和第二个节点的指针 3. 释放原头节点内存 **C语言实现**: ```c void deleteHead(DListNode** head) { if (*head == NULL) return; DListNode* tail = (*head)->prev; // 尾节点 DListNode* second = (*head)->next; // 第二个节点 if (tail == *head) { // 单个节点情况 free(*head); *head = NULL; return; } tail->next = second; // 尾节点连接第二个节点 second->prev = tail; free(*head); *head = second; // 更新头指针 } ``` --- ### 关键问题处理 1. **空链表检测**:操作前必须检查链表是否为空 2. **单节点处理**:删除后需将链表置空(如`return NULL`) 3. **时间复杂度**: - 单向循环链表:$O(n)$(需遍历定位尾节点) - 双向循环链表:$O(1)$(直接通过前驱指针访问) ---
评论 10
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值