详解循环链表

165 阅读12分钟

循环链表的定义及其特点;

定义

循环链表是一种特殊的链表数据结构,它与普通链表的区别在于尾节点的下一个指针指向头节点,形成一个环形结构。循环链表可以通过头节点访问到所有的节点,并且可以在常量时间内插入和删除节点。

循环链表的定义包含一个头节点和一个尾节点,头节点用于指示链表的起始位置,尾节点用于标识循环链表的结束位置。每个节点包含一个数据域和一个指针域,指针域指向下一个节点。

循环链表的优点是可以节省内存空间,因为不需要额外的指针来指向尾节点。同时,循环链表可以很方便地实现循环访问和遍历操作,例如循环队列和循环缓冲区等。

在循环链表中,插入和删除节点的操作与普通链表类似,只需要更新相应的指针即可。需要注意的是,在插入和删除节点时,需要确保循环链表的头节点和尾节点的指针正确更新,以保持循环链表的完整性。

image.png 注:图示是方便理解的例图,实际数量和结点并非一模一样

特点

  1. 循环性:循环链表的最后一个节点的指针指向链表的头节点,形成一个闭环。这使得循环链表可以无限循环地访问其中的节点,而无需遍历整个链表。
  2. 灵活性:由于循环链表可以在常量时间内插入和删除节点,因此它非常适用于需要频繁修改链表结构的场景。插入和删除节点只需要修改相邻节点的指针,而不需要遍历整个链表。
  3. 空间效率:循环链表不需要额外的指针来指向尾节点,相比于普通链表,可以节省一些内存空间。同时,循环链表可以通过头节点访问到所有的节点,避免了使用额外的指针来遍历链表。
  4. 循环访问:循环链表可以很方便地实现循环访问和遍历操作。通过头节点,可以轻松地遍历所有节点,而无需关心链表的长度。
  5. 应用广泛:循环链表在实际应用中有着广泛的用途。例如,循环队列、循环缓冲区和循环链表本身就是循环链表的应用。此外,循环链表还可以用于模拟循环的场景,如音乐播放器的播放列表等。

循环链表的运算

二级指针指在 对地址实现值传递 实现对地址的改变

C语言二级指针(指向指针的指针)

结构体

typedef int DataType;

/*简单链表的定义*/
typedef struct node
{
	DataType		data;	/*数据域*/
	struct node		*next;	/*指针域*/
}SingleLinkList, SingleLinkNode;

初始化

C 库函数 *void malloc(size_t size)  分配所需的内存空间,并返回一个指向它的指针。

C 库函数 – malloc() | 菜鸟教程 (runoob.com)

通过动态内存分配,可以创建一个空的循环链表。首先分配一个头节点,并将其指针域指向自身,形成一个空的循环链表。

/*1. 初始化*/
int init(SingleLinkList **Head)
{
	if(1)
	{
		/*申请内存*/
		(*Head) = (SingleLinkList*)malloc(sizeof(SingleLinkList));
		/*判断内存申请是否成功*/
		if(*Head == NULL)
		{
			printf("申请内存错误, 初始化失败![100001]\n");
			return 100001;
		}
		/*循环链表 next 指向组件 */
		(*Head)->next = *Head;

		return 0;
	}
	else
	{
		printf("该链表已经初始化!请删除后再执行此操作![100002]\n");
		return 100002;
	}
}

插入

在循环链表中插入一个新节点,可以在指定位置之前或之后插入。插入节点时,需要更新相邻节点的指针,使其指向新插入的节点。

image.png 注:图示是方便理解的例图,实际数量和结点并非一模一样

头插法

通过循环链表头插法,新插入的节点将成为新的头节点,而原来的节点则按照原来的顺序向后移动。

/*2. 插入元素,头插法*/
int insert_head(SingleLinkList **Head, DataType x)
{
	SingleLinkNode *newNode;

	if(0)
	{
		printf("链表未初始化![100003]\n");
		return 100003;
	}
	
	newNode = (SingleLinkNode*)malloc(sizeof(SingleLinkNode));
	if(!newNode)
	{
		printf("申请节点内存空间失败![100004]\n");
		return 100004;
	}
        
	newNode->data = x;
	newNode->next = (*Head)->next;

	(*Head)->next = newNode;

	return 0;
}

尾插法

通过循环链表尾插法,新插入的节点将成为新的尾节点,而原来的节点则按照原来的顺序向后移动。

/*2. 插入元素, 尾插法*/
int insert_tail(SingleLinkList **Head, DataType x)
{
	SingleLinkNode *newNode;
	SingleLinkNode *p;

	if(0)
	{
		printf("链表未初始化![100003]\n");
		return 100003;
	}
	
	newNode = (SingleLinkNode*)malloc(sizeof(SingleLinkNode));
	if(!newNode)
	{
		printf("申请节点内存空间失败![100004]\n");
		return 100004;
	}

	/* 新结点赋值 next 指向 Head */
	newNode->data = x;
	newNode->next = *Head;
	
	p = (*Head);

	// 遍历到最后一个结点 
	while(p->next != (*Head))
	{
		p = p->next;
	}
	
	// 把最后一个结点 next(Head) 指向 newNode 
	p->next = newNode;

	return 0;
}

位置i处插入元素x

循环链表在位置i处插入元素x的步骤包括创建新节点、定位到位置i的前一个节点、插入新节点并更新指针。通过这些操作,我们可以在循环链表的指定位置插入新元素。

/*2. 插入元素,在位置i处插入元素x */
int insert(SingleLinkList **Head, int i, DataType x)
{
	int j;
	SingleLinkNode *p;
	SingleLinkNode *newNode;

	
	/*对i进行判断,0<i<=length+1*/
	if(i<1 || i>length(*Head)+1)
	{
		printf("位置i不是链表有效位置![100005]\n");
		return 100005;
	}
	p = (*Head);
	j = 1;

	// 遍历到指向位置 
	while(j<i)
	{
		j++;
		p = p->next;
	}

	newNode = (SingleLinkNode*)malloc(sizeof(SingleLinkNode));
	/*此处省略检测newNode是否申请成功*/

	newNode->data = x;
	newNode->next = p->next;
	p->next = newNode;
	return 0;
}

删除

删除节点:在循环链表中删除指定位置的节点,需要更新相邻节点的指针,使其跳过被删除的节点。同时,需要释放被删除节点的内存空间。

image.png

注:图示是方便理解的例图,实际数量和结点并非一模一样

/*3. 删除元素, 删除值为x的元素*/
int delete(SingleLinkList **Head, DataType x)
{
	int i;
	int j;

	SingleLinkNode *p;
	SingleLinkNode *q; /*要删除的元素x*/

	i = find(*Head,x);
	if(!i)
	{
		printf("元素x【%d】不存在!100006\n", x);
		return 100006;
	}
	
	p = (*Head);
	j=1;

	while(j<i)
	{
		j++;
		p = p->next;
	}
	
	q = p->next;
	p->next = q->next;

	free(q); /*释放内存*/

	return 0;
}

遍历

链表长度

遍历循环链表:通过头节点,可以轻松地遍历循环链表中的所有节点,不断累加 len 得到长度。

/*4. 链表长度 */
int length(SingleLinkList *Head)
{
	int len=0;
	SingleLinkNode *p;

	p = Head->next;
	while(p!=Head)
	{
		len++;
		p = p->next;
	}
	return len;
}

输出链表

遍历循环链表:通过头节点,可以轻松地遍历循环链表中的所有节点。从头节点开始,依次访问每个节点,并根据需要进行相应的操作。

/*6.输出链表*/
void print(SingleLinkList *Head)
{
	SingleLinkNode *p;
	int i=0;

	p = Head->next;
	printf("%d == %d\n", p, Head->data);
	printf("%d == %d\n", p, Head);
	//  
	if(p==Head)
	{
		printf("链表为空!\n");
		return;
	}

	while(p!=Head)
	{
		printf("Node[%d]. = %d\n", ++i, p->data);
		p = p->next;
	}
}

判断链表是否存在该值

遍历循环链表:通过头节点,可以轻松地遍历循环链表中的所有节点。从头节点开始,依次访问每个节点,直到访问到存在的值,遍历完找不到则返回下标。

/*5. 查找值为x的元素,返回位置i */
int find(SingleLinkList *Head, DataType x)
{
	int i;
	SingleLinkNode *p;

	i = 1;
	p = Head->next;
        
	while( p!= Head && p->data != x)
	{
		i++;
		p = p->next;
	}

	if(p->next == Head)
	{
		return 0;
	}
	else
	{
		return i;
	}

}

循环链表的实现

完整代码

项目结构

    main.c
    SingleLinkList.c
    SingleLinkList.h
    welcome.h

项目文件

main.c

#include <stdio.h>
#include <string.h>
#include "SingleLinkList.h"
#include "welcome.h"


int main(int argc, char* argv[])
{
	SingleLinkList *Head;
	DataType x;
	int i,m,n,cmd;

	for(i=0;i<strlen(welcome);i++)
	{
		printf("%c",welcome[i]);
		for(m=0;m<1000;m++)
			for(n=0;n<1000;n++)
			{
				;
			}
	}

	printf("-----------简单链表演示程序----------\n");
	do
	{
		printf("1. 初始化链表表\n");
		printf("2. 插入元素(头插法)\n");
		printf("3. 插入元素(尾插法)\n");
		printf("4. 插入元素(在位置i插入)\n");
		printf("5. 查找元素x\n");
		printf("6. 求链表长度\n");
		printf("7. 输出链表\n");
		printf("8. 删除元素\n");
		printf("10. 帮助\n"); 
		printf("0. 退出\n");
		printf("请输入您要进行的操作(1~6,0退出):");
		scanf("%d", &cmd);
		switch(cmd)
		{
		case 1:
			if(!init(&Head))
			{
				printf("链表已初始化!\n");
			}
			break;
		case 2:
			printf("请输入插入元素x:x=");
			scanf("%d",&x);
			if(!insert_head(&Head,x))
			{
				printf("元素(%d)已插入\n", x);
			}
			break;
		case 3:
			printf("请输入插入元素x:x=");
			scanf("%d",&x);
			if(!insert_tail(&Head,x))
			{
				printf("元素(%d)已插入\n", x);
			}
			break;
		case 4:
			printf("请输入插入元素位置i和元素x(i,x):");
			scanf("%d,%d", &i, &x);
			if(!insert(&Head, i, x))
			{
				printf("已在位置(%d)插入元素(%d)!\n",i, x);
			}
			break;
		case 5:
			printf("请输入要查找的元素x:");
			scanf("%d", &x);
			if(i = find(Head,x))
			{
				printf("元素%d存在,在链表位置%d.\n", x, i);
			}
			else
			{
				printf("在链表中未找到元素x。\n");
			}
			break;
		case 6:
			printf("链表的长度为:%d\n", length(Head));
			break;
		case 7:
			print(Head);
			break;
		case 8:
			printf("请输入要删除的元素x:");
			scanf("%d", &x);
			if(!delete(&Head, x))
			{
				printf("元素x【%d】已删除!\n", x);
			}
			break;
		case 10:
			printf(" 本程序为链表的演示程序,有XXX设计开发,程序完成了。。。。功能!。。。\n");
			break;
			
		}
	}while(cmd != 0);

	return 0;
}

SingleLinkList.c

/*
	SingleLinkList.c
	
*/
#include "SingleLinkList.h"
#include <stdlib.h>
#include <stdio.h>

/*1. 初始化*/
int init(SingleLinkList **Head)
{
	if(1)
	{
		/*申请内存*/
		(*Head) = (SingleLinkList*)malloc(sizeof(SingleLinkList));
		/*判断内存申请是否成功*/
		if(*Head == NULL)
		{
			printf("申请内存错误, 初始化失败![100001]\n");
			return 100001;
		}
		(*Head)->next = *Head;
                
		return 0;
	}
	else
	{
		printf("该链表已经初始化!请删除后再执行此操作![100002]\n");
		return 100002;
	}
}


/*2. 插入元素,头插法*/
int insert_head(SingleLinkList **Head, DataType x)
{
	SingleLinkNode *newNode;

	if(0)
	{
		printf("链表未初始化![100003]\n");
		return 100003;
	}
	
	newNode = (SingleLinkNode*)malloc(sizeof(SingleLinkNode));
	if(!newNode)
	{
		printf("申请节点内存空间失败![100004]\n");
		return 100004;
	}
	newNode->data = x;
	newNode->next = (*Head)->next;

	(*Head)->next = newNode;

	return 0;
}


/*2. 插入元素, 尾插法*/
int insert_tail(SingleLinkList **Head, DataType x)
{
	SingleLinkNode *newNode;
	SingleLinkNode *p;

	if(0)
	{
		printf("链表未初始化![100003]\n");
		return 100003;
	}
	
	newNode = (SingleLinkNode*)malloc(sizeof(SingleLinkNode));
	if(!newNode)
	{
		printf("申请节点内存空间失败![100004]\n");
		return 100004;
	}

	newNode->data = x;
	newNode->next = *Head;

	p = (*Head);

	while(p->next != (*Head))
	{
		p = p->next;
	}

	p->next = newNode;

	return 0;
}


/*2. 插入元素,在位置i处插入元素x */
int insert(SingleLinkList **Head, int i, DataType x)
{
	int j;
	SingleLinkNode *p;
	SingleLinkNode *newNode;

	
	/*对i进行判断,0<i<=length+1*/
	if(i<1 || i>length(*Head)+1)
	{
		printf("位置i不是链表有效位置![100005]\n");
		return 100005;
	}
	p = (*Head);
	j = 1;

	while(j<i)
	{
		j++;
		p = p->next;
	}

	newNode = (SingleLinkNode*)malloc(sizeof(SingleLinkNode));
	/*此处省略检测newNode是否申请成功*/

	newNode->data = x;
	newNode->next = p->next;
	p->next = newNode;
	return 0;
}


/*3. 删除元素, 删除值为x的元素*/
int delete(SingleLinkList **Head, DataType x)
{
	int i;
	int j;

	SingleLinkNode *p;
	SingleLinkNode *q; /*要删除的元素x*/

	i = find(*Head,x);
	if(!i)
	{
		printf("元素x【%d】不存在!100006\n", x);
		return 100006;
	}
	
	p = (*Head);
	j=1;

	while(j<i)
	{
		j++;
		p = p->next;
	}
	
	q = p->next;
	p->next = q->next;

	free(q); /*释放内存*/

	return 0;
}


/*5. 查找值为x的元素,返回位置i */
int find(SingleLinkList *Head, DataType x)
{
	int i;
	SingleLinkNode *p;

	i = 1;
	p = Head->next;
        
	while( p!= Head && p->data != x)
	{
		i++;
		p = p->next;
	}

	if(p->next == Head)
	{
		return 0;
	}
	else
	{
		return i;
	}

}

/*4. 链表长度*/
int length(SingleLinkList *Head)
{
	int len=0;
	SingleLinkNode *p;

	p = Head->next;
        
	while(p!=Head)
	{
		len++;
		p = p->next;
	}
	return len;
}

/*6.输出链表*/
void print(SingleLinkList *Head)
{
	SingleLinkNode *p;
	int i=0;

	p = Head->next;
        
	if(p==Head)
	{
		printf("链表为空!\n");
		return;
	}
        
	while(p!=Head)
	{
		printf("Node[%d]. = %d\n", ++i, p->data);
		p = p->next;
	}
}

SingleLinkList.h

/*
	SingleLinkList.h
*/

typedef int DataType;

/*简单链表的定义*/
typedef struct node
{
	DataType		data;	/*数据域*/
	struct node		*next;	/*指针域*/
}SingleLinkList, SingleLinkNode;


/*1. 初始化*/
int init(SingleLinkList **Head);

/*2. 插入元素,头插法*/
int insert_head(SingleLinkList **Head, DataType x);

/*2. 插入元素, 尾插法*/
int insert_tail(SingleLinkList **Head, DataType x);

/*2. 插入元素,在位置i处插入元素x */
int insert(SingleLinkList **Head, int i, DataType x);

/*3. 删除元素, 删除值为x的元素*/
int delete(SingleLinkList **Head, DataType x);

/*5. 查找值为x的元素,返回位置i */
int find(SingleLinkList *Head, DataType x);

/*6. 求链表的长度 */
int length(SingleLinkList *Head);

/*7.输出链表*/
void print(SingleLinkList *Head);

welcome.h

char welcome[] = "\n\
           /\ \n\
          ( *)======/\==== \n\
           )(      /  \ \n\
__________/  )    /    \ \n\
\___         /   / \"\"   \ \n\
  \____    _/   / (**)   \ \n\
     / \__/    (----------) \n\
    /____|__//_ ( 送给您- ) \n\
         |      ( 亲爱的 ) \n\
         |       (      )\n\
         |        (____)\n\
        _|__\n\
         \\    ☆新年 . 快乐☆\n\n";

运行结果

QQ截图20231014093817.png

QQ截图20231014094058.png

Snipaste_2023-10-14_09-41-46.png

小结

循环链表是一种特殊的链表,它的最后一个节点指向第一个节点,形成一个循环的结构。相比于普通链表,循环链表可以更方便地进行循环操作,常用于环形队列、约瑟夫问题等场景。

下面是循环链表的一些应用:

  1. 应用:

    • 环形队列:使用循环链表实现的队列,可以循环利用空间,实现高效的入队和出队操作。
    • 约瑟夫问题:使用循环链表可以模拟约瑟夫问题,即在固定规则下,依次淘汰指定位置的节点,直到只剩下一个节点为止。

循环链表是一种特殊的链表,最后一个节点指向第一个节点,形成一个循环结构。它具有插入和删除操作高效、遍历方便等特点,常用于环形队列、约瑟夫问题等场景。使用循环链表可以简化问题的处理,并提高算法的效率。

Josephu问题 - 城北有个混子 - 博客园 (cnblogs.com)

参考文献

ora.ai

C语言循环链表创建,遍历,插入,删除,查找_循环链表的遍历-CSDN博客

详解顺序表 - 掘金 (juejin.cn)