单循环链表(不带表头)的C语言实现(详细)

目录

  • 前言
    • 尾指针
      • 尾指针的定义
      • 尾指针的作用
    • 包含相关声明的头文件
    • 何时需要2级指针
  • 单循环链表的定义
  • 单循环链表的基本通用操作
    • 1.初始化单循环链表
    • 2.单循环链表是否为空
    • 3.创建一个新结点
    • 4.查找指定位置上的元素
    • 5.在指定位置插入元素
    • 6.在头部插入元素
    • 7.在尾部插入元素
    • 8.删除指定位置上的元素
    • 9.在头部删除元素
    • 10.在尾部删除元素
    • 11.修改指定位置上元素的值
    • 12.计算单循环链表长度
    • 13.清空单循环链表
  • 非通用操作(只适用于整型单循环链表)
    • 1.查找和指定值相同的第一个结点
    • 2.返回和指定值相同的第一个结点的序号
    • 3.删除和指定值相同的第一个结点



前言


尾指针


尾指针的定义

尾指针是一个指向表尾的指针,可以用它来找到整个链表。

尾指针的作用

为什么不像单链表一样使用头指针呢?因为单循环链表和单链表一样,是单向的。找到后继结点很方便,需要O(1)的复杂度,找到前驱结点却很难,需要O(n)的复杂度。
尾指针指向的是尾结点,它的后继结点就是第一个数据结点(本文的链表没有头结点,如果有头结点,那么尾结点的后继结点是头结点)。所以,尾指针可以做到头指针的所有功能,而且尾指针还可以很方便找到表尾,和表尾相关的操作(比如合并2个单循环链表)用尾指针就很方便。

包含相关声明的头文件


将单循环链表包含的相关头文件,函数声明,结构体定义,宏定义等放到一个叫circular_linked_list.h的头文件中,是很好的管理思路。
想要使用单链表,只需要包含头文件circular_linked_list.h。

#include "circular_linked_list.h"

下面是完整的声明

何时需要2级指针

在单链表中,有头结点时,插入,删除等相关操作,都不需要进行特殊处理。因为处理表头时,不需要直接改变头指针,只需要改变头结点。
但是,在单循环链表中,不管是插入还是删除,做表尾的相关操作时,都需要改变尾指针的指向,所以就需要2级指针。
简单总结:只要尾结点改变了,都需要2级指针



单循环链表的定义


单循环链表是在单链表的基础上做修改,有2个要点:
1.让表尾的next指针域不要指向NULL,而是指向头结点。
2.不要定义头指针,而是定义尾指针。尾指针指向表尾(尾结点)。
因为能找到表尾就能找到表头。头指针的作用,尾指针可以完美替代,而且当需要找表尾时,尾指针只需要O(1)的时间复杂度,而头指针需要O(n)。

和单链表一样,单循环链表的定义不是定义整个链表,而是定义单个结点(用结构体实现)。
创建单循环链表时,只需要将这些节点连接起来即可。

typedef int ElemType;				// 可以创建任何数据类型的链表,只需要修改这一行代码

struct Node;						// 先定义结点的结构体,但是不创建模板
typedef struct Node* PtrToNode;		// 指向Node结构体的指针有2个应用场景,下面2行代码分开定义
typedef PtrToNode CircList;			// 定义循环链表尾指针
typedef PtrToNode Position;			// 定义循环链表结点地址
// 循环链表结点定义
typedef struct Node {
	ElemType element;
	Position next;
}Node;

定义结点的代码大同小异,我只是用typedef把struct Node*改成了Position,这样命名更贴切一些。
这种命名方式是借鉴了经典书籍《数据结构与算法分析——C语言描述》里的代码。



单循环链表的基本通用操作


1.初始化单循环链表

由于我写的是没头结点的链表,所以初始化只需要设置尾指针为NULL。

void Init(CircList* ppRear)
{
	*ppRear = NULL;		
}

2.单循环链表是否为空

没头结点的链表判断是否为空表,是检测rear是否为NULL。

bool IsEmpty(const CircList rear)
{
	return rear == NULL;
}

3.创建一个新结点

因为插入的相关操作都需要创建一个新结点,所以可以写成一个函数实现复用

Position CreateNode(const ElemType elem)
{
	Position newNode = (Position)malloc(sizeof(Node));
	// 异常处理
	if (newNode == NULL)
	{
		puts("The creation of new node failed!");
		exit(EXIT_FAILURE);
	}
	
	newNode->element = elem;
	newNode->next = NULL;			// 因为新结点暂时没连接链表,所以next暂时设置为NULL
	return newNode;
}

4.查找指定位置上的元素

查询操作的异常处理是:
1.空表异常处理
2.pos范围越界异常处理

查询比较值得注意的特殊情况是:
1.查询的位置是尾结点,程序虽然会从if(cur == rear) break; 这里跳出,但是判定越界的if语句是,if (count < pos),如果查询的是表尾,count会等于pos,并不会被判断为越界。所以,程序可以通过查询尾结点的情况。
2.pos为0,此时count 和pos一开始都为0,程序不会进入循环,会直接返回cur,cur的初始值为rear。所以会返回尾指针,也和预计吻合。

/*
 * 查找循环链表指定位置元素的地址,pos的范围:[0,length],pos为0则返回尾指针
 * 如果指定位置超出范围,就打印提示信息,并终止程序
 * 空表不能查询,需要做异常处理
 */
Position GetElem(const CircList rear, int pos)
{
	// 左边的非法范围异常处理
	if (pos < 0)
	{
		puts("The position is out of range!");
		exit(EXIT_FAILURE);
	}
	
	// 空表的异常处理
	if (IsEmpty(rear))
	{
		puts("The circular linked list is empty!");
		exit(EXIT_FAILURE);
	}
	
	// 查询操作核心代码
	int count = 0;
	Position cur = rear;		// 保存指定位置元素的地址,初始时指向尾结点
	while (count < pos)
	{
		cur = cur->next;
		count++;
		if (cur == rear)
			break;
	}
	
	// 右边的非法范围异常处理
	if (count < pos)
	{
		puts("The position is out of range!");
		exit(EXIT_FAILURE);
	}
	return cur;
}

5.在指定位置插入元素

异常情况有1个:pos越界异常。
特殊情况有1个:空表的插入,需要让新结点的next域指向它自己。空表的插入只有pos=1合法,pos>1需要单独做范围越界处理。
正常插入操作分为3步:
1.创建新结点。
2.找到被插入位置的前驱结点。
3.让新结点连接上链表。
如果是对表尾插入,需要移动尾指针到下一位。

/*
 * 在指定位置上插入元素(也就是把新结点插入到指定位置结点的前面)
 * 空表的插入需要做特殊处理,因为这是没头结点的版本
 * pos的合法范围是[1,length+1],GetElem函数会处理越界异常
 * 如果在表尾插入,需要移动尾结点
 */
void InsertElem(CircList* ppRear, int pos, ElemType elem)
{
	Position newNode = CreateNode(elem);
	// 空表的插入需要特殊化处理
	if (IsEmpty(*ppRear))
	{
		// 如果pos为1,则可以将新结点插入空表,否则就是越界
		if (pos == 1)
		{
			*ppRear = newNode;
			newNode->next = newNode;
			return;
		}
		else
		{
			puts("The position is out of range!");
			exit(EXIT_FAILURE);
		}
	}
	// GetElem函数会做指定位置范围越界的检测
	Position prec = GetElem(*ppRear, pos - 1);
	/*
	 * 如果在表尾后插入元素(pos=length+1),需要移动尾结点
	 * 而prec==尾指针包含两种情况,表头插入或者表尾插入
	 * 表头插入时,pos为1,所以需要剔除这种情况
	 */
	if (prec == *ppRear && pos != 1)
		*ppRear = (*ppRear)->next;		//在表尾插入,需要让尾指针往后移动一位
	// 插入操作核心代码
	newNode->next = prec->next;
	prec->next = newNode;
}

6.在头部插入元素

头插其实非常简单,就是在表尾后插入一个新结点。
绝大多数情况,尾指针是不用改变的。
但是对空表进行头插时,需要改变尾指针(所以需要传入二级指针的参数)。

/*
 * 在表头前插入结点,等价于在尾结点后插入元素,因为循环链表的表尾的后继结点就是表头
 * 当表为空时,需要做特殊处理
 */
void PushFront(CircList* ppRear, ElemType elem)
{
	Position newNode = CreateNode(elem);
	// 空表的头插需要特殊处理
	if (IsEmpty(*ppRear))
	{
		newNode->next = newNode;
		*ppRear = newNode;
		return;
	}
	// 头插核心操作
	newNode->next = (*ppRear)->next;
	(*ppRear)->next = newNode;
}

7.在尾部插入元素

头插和尾插的位置其实是一样的,都是在原来尾结点的下一位插入新结点
区别只有一点,尾插需要移动尾指针到下一位。这样,新结点就变成表尾了。

/*
 * 在表尾插入结点,尾指针一定要往后移动一位
 * 当表为空时,需要做特殊处理
 */
void PushBack(CircList* ppRear, ElemType elem)
{
	Position newNode = CreateNode(elem);
	// 空表的头插需要特殊处理
	if (IsEmpty(*ppRear))
	{
		newNode->next = newNode;
		*ppRear = newNode;
		return;
	}
	// 尾插核心操作
	newNode->next = (*ppRear)->next;
	(*ppRear)->next = newNode;
	*ppRear = (*ppRear)->next;
}

8.删除指定位置上的元素

删除指定位置的元素是实现较为复杂的操作。
有2个异常情况:
1.空表删除异常。
2.查找位置越界异常。
有2个特殊情况:
1.如果原链表只有1个元素,需要做清空链表的操作。
2.如果删除元素是尾结点,需要移动尾指针到前一位。
基本的删除操作分为2步:
1.找到被删除结点的前驱结点。
2.执行删除的核心操作(让被删除结点的前驱直接连接后继,并且释放被删除结点的空间)。
有的应用场景,可能会使用被删除结点的值,所以我将这个值作为返回值返回。

// 删除单循环链表指定位置上的元素,并返回该元素的值,位置范围为:[1,length]
ElemType DeleteElem(CircList* ppRear, int pos)
{
	// 空表的异常处理
	if (IsEmpty(*ppRear))
	{
		puts("The circular linked list is empty!");
		exit(EXIT_FAILURE);
	}

	// 左边的非法范围异常处理
	if (pos <= 0)
	{
		puts("The position is out of range!");
		exit(EXIT_FAILURE);
	}

	// 找被删除结点的前驱结点
	Position prec = *ppRear;
	int count = 0;
	while (count < pos - 1)
	{
		prec = prec->next;
		count++;
		// 如果找到尾结点了,说明要找的前驱结点越界了
		if (prec == *ppRear)
		{
			puts("The position is out of range!");
			exit(EXIT_FAILURE);
		}
	}

	Position cur = prec->next;
	ElemType deleteElem = cur->element;
	// 如果要删除结点是尾结点
	// 分为循环链表有1个元素和多个元素的情况
	if (cur == *ppRear)
	{
		if (pos == 1)
		{
			Clear(ppRear);
			return deleteElem;
		}
		else
			*ppRear = prec;
	}

	// 删除核心操作
	prec->next = cur->next;
	free(cur);
	return deleteElem;
}

9.在头部删除元素

头删操作非常简单。
异常处理只有一个,空表异常处理。
特殊情况是只有一个元素的链表的头删。
核心删除操作很简单,就是让尾结点指向头结点的后继结点,并且释放头结点。

// 删除第一个数据结点,并且返回被删除结点的数据域
ElemType PopFront(CircList* ppRear)
{
	// 空表的异常处理
	if (IsEmpty(*ppRear))
	{
		puts("The circular linked list is empty!");
		exit(EXIT_FAILURE);
	}
	
	Position cur = (*ppRear)->next;
	ElemType deleteElem = cur->element;
	
	// 对于只有一个元素的链表,需要做特殊处理
	if (cur == *ppRear)
	{
		Clear(ppRear);
		return deleteElem;
	}
	
	// 头删的核心操作
	(*ppRear)->next = cur->next;
	free(cur);
	return deleteElem;
}

10.在尾部删除元素

尾删相对于头删要复杂一点点。
有一个异常情况,空表异常。
有一个特殊情况,原链表只有一个元素。
删除核心操作分为这几步:
1.找尾结点的前驱结点。
2.直接连接尾结点的前驱和后继结点。
3.释放尾结点的空间。
4.让尾指针往前移动一位。
5.返回被删除元素的值,有的场景可能会用。
加粗部分是相比头删多出来的2步。

// 删除循环单链表的尾结点,并返回被删除结点的数据域
ElemType PopBack(CircList* ppRear)
{
	// 空表的异常处理
	if (IsEmpty(*ppRear))
	{
		puts("The circular linked list is empty!");
		exit(EXIT_FAILURE);
	}
	
	ElemType deleteElem = (*ppRear)->element;
	// 如果原链表只有一个元素
	if ((*ppRear)->next == *ppRear)
	{
		Clear(ppRear);
		return deleteElem;
	}
	
	// 寻找尾结点的前驱结点
	Position prec = (*ppRear)->next;
	while (prec->next != *ppRear)
		prec = prec->next;
		
	// 尾删核心操作
	prec->next = (*ppRear)->next;
	free(*ppRear);
	*ppRear = prec;
	return deleteElem;
}

11.修改指定位置上元素的值

修改指定位置元素的值的函数很容易。
唯一需要注意的是,我设计的GetElem函数不会判定pos=0为异常。
所以,当pos为0时,需要做异常处理。

// 修改指定位置上元素的值,pos的合法范围为:[1,length]
void ModifyElem(CircList rear, int pos, ElemType elem)
{
	// 后面的GetElem函数不会做pos=0的非法范围检查,需要单独处理
	if (pos == 0)
	{
		puts("The position is out of range!");
		exit(EXIT_FAILURE);
	}
	Position cur = GetElem(rear, pos);
	cur->element = elem;
}

12.计算单循环链表长度

特殊情况是空表,length为0。
计算长度就是用一个计数变量,每循环一次自增1。

// 计算链表长度,如果是空表就返回0
int GetLength(const CircList rear)
{
	// 如果是空表,返回0
	if (IsEmpty(rear))
		return 0;
	int length = 0;
	Position cur = rear;
	// 每移动一次,length+1,如果移动到表尾,返回length
	do
	{
		cur = cur->next;
		length++;
	}
	while (cur != rear);
	return length;
}

13.清空单循环链表

清空链表很简单,注意需要succ指针保存当前被释放结点的后继节点。
免得释放当前结点后,丢失链表。

void Clear(CircList* ppRear)
{
	// 空表异常处理
	if (IsEmpty(ppRear))
	{
		puts("The circular linked list is empty!");
		exit(EXIT_FAILURE);
	}
	
	Position cur = (*ppRear)->next;	// 从第一个结点开始释放
	while (cur != *ppRear)			// 在循环内释放除尾结点之外的其他结点
	{
		Position succ = cur->next;	// 要保存当前要释放结点的后继结点,保证能找到链表的后续部分
		free(cur);
		cur = succ;
	}
	free(cur);						// 最后释放尾结点
	*ppRear = NULL;					// 让尾指针指向NULL
}

非通用操作(只适用于整型单循环链表)


1.查找和指定值相同的第一个结点

LocateElem函数看起来很简单其实有一个细节。
循环有可能会遍历整个链表(除非提前找到值),但是循环链表遍历的循环判断条件是有细节的。
1.必须从第一个数据结点开始找起,不能从尾结点找起,因为函数是返回和指定值相同的第一个元素的地址
2.循环链表遍历一遍的标志是再次到相同结点。所以,一定要先做值是否相同的判断,然后立刻将cur移动到下一位,最后做while循环的条件判断。这样程序在跳出循环时,必定已经遍历了一遍循环链表。

// 返回循环单链表内和指定值相同的第一个元素的地址,如果没有找到就返回NULL
Position LocateElem(const CircList rear, ElemType elem)
{
	// 空表的异常处理
	if (IsEmpty(rear))
	{
		puts("The circular linked list is empty!");
		exit(EXIT_FAILURE);
	}

	Position cur = rear->next;
	// 从第一个数据结点开始检查,找到就直接返回该节点
	// 如果再次找到第一个数据结点,说明链表没有和指定值相同的结点,就跳出循环,返回NULL
	do
	{
		if (cur->element == elem)
			return cur;
		cur = cur->next;
	} while (cur != rear->next);
	return NULL;
}

2.返回和指定值相同的第一个结点的序号

查找指定值的序号和查找指定值的元素地址思路是一样的,查序号只是多一个计数的变量pos。
我修改了一下循环判断的条件,思路是一样的。
我把是否和指定值相同的判断放到了循环条件里面。
把再次找到头结点的条件放到了if里面,如果if成立,就认为没找到,就返回0。

int LocatePos(const CircList rear, ElemType pos)
{
	// 空表的异常处理
	if(IsEmpty(rear))
	{
		puts("The circular linked list is empty!");
		exit(EXIT_FAILURE);
	}

	Position cur = rear->next;
	int count = 1;
	while (cur->element != elem)
	{
		cur = cur->next;
		count++;
		if (cur == rear->next)
			return 0;
	}
	return count;
}

3.删除和指定值相同的第一个结点

循环链表的删除操作都不简单。
有1个异常情况:空表的异常。
有2个特殊情况:
1.原链表只有一个结点。
2.被删除结点是尾结点。
删除的基本操作分为2步:
1.找被删除结点的前驱结点。
2.核心删除操作(连接被删除结点的前驱和后继,释放被删除结点的空间)。

/*
 * 删除单循环链表内和指定值相同的第一个元素
 * 如果没找到和指定值相同的元素,则打印提示信息,并终止程序
 */
void RemoveElem(CircList* ppRear, ElemType elem)
{
	// 处理空表的异常
	if (IsEmpty(*ppRear))
	{
		puts("The circular linked list is empty!");
		exit(EXIT_FAILURE);
	}
	
	// 如果只有一个结点
	if ((*ppRear)->next == *ppRear)
	{
		// 如果该结点的值就是要删除的指定值,就清空链表
		if ((*ppRear)->element == elem)
		{
			Clear(ppRear);
			return;
		}
		// 否则就做没查找到的异常处理
		else
		{
			puts("The value is not in the list!");
			exit(EXIT_FAILURE);
		}
	}

	// 找被删除结点的前驱元
	Position prec = *ppRear;
	while (prec->next->element != elem)
	{
		prec = prec->next;
		// 如果找到了尾结点,说明链表内没有指定值,就处理异常
		if (prec == *ppRear)
		{
			puts("The value is not in the list!");
			exit(EXIT_FAILURE);
		}
	}
	Position cur = prec->next;
	// 如果被删除结点是尾结点,需要让尾指针前移一位
	if (cur == *ppRear)
		*ppRear = prec;

	// 删除操作
	prec->next = cur->next;
	free(cur);
}

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