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



前言


头结点是什么?

头结点也叫作表头(header)或者哑结点(dumy node)。
头结点是在第一个数据结点前再创建一个特殊结点。它的数据域可以为空,或者用来存储单链表的相关信息,比如单链表的长度。它的指针域指向第一个数据结点。本文实现的单链表是有头结点的,它的数据域为空。

头指针是什么?

头指针一般用于表示链表,它指向链表的第一个结点
由于本文的单链表有头结点,所以,头指针会指向头结点
如果没有头结点,头指针会指向第一个数据结点

头结点的作用

头结点主要是避免删除、插入等操作的特殊化。
如果没有头结点,第一个数据结点相比其他结点就是特殊结点,因为它是整个链表的开头。
所以,如果没头结点,对第一个结点位置的插入、删除等操作,都得做特殊处理
头结点相当于第零个结点,它连接着第一个数据结点。
所以,有了头结点,所有数据结点都可做一般的处理。
头结点并不是必须的,但是头指针是必须的。

包含相关声明的头文件

C语言实现单链表所包含的头文件,typedef语句,宏定义,函数声明,结构定义
将以下代码放到专门的头文件linkList.h中,这样做可以方便管理。
想要使用单链表,只需要包含该头文件:#include “linkList.h”,再将实现函数的文件linkList.c加入项目。

#include <stdio.h>					// 包含标准输入输出头文件
#include <stdlib.h>					// 包含标准库头文件
#include <assert.h>					// 包含断言头文件
#include <stdbool.h>				// 包含标准布尔类型头文件


typedef int ElemType;				// 设置链表元素的类型,可以随时通过这行代码改变元素类型

/* 
 * 下面通过typedef定义两个名称:LinkList和Position,它们本质都是(struct LNode*)
 * Position名称用于表示结点的next指针域,可以更明确的表明next变量用于存储下一个结点的地址
 * LinkList名称用于创建指向表头的头指针,常用头指针访问整个链表,所以用LinkList定义非常合理
 */
struct LNode;						// 定义结构体LNode,暂时不创建该结构体的模板
typedef struct LNode* PtrToLNode;	// 定义名称PtrToNode(意思是,指向node的指针,本质是struct LNode*)
typedef PtrToLNode LinkList;		// 定义名称LinkList(本质是struct LNode*),常用于头指针
typedef PtrToLNode Position;		// 定义名称Position(本质是struct LNode*),常用于指向普通结点

// 定义单个结点,包含数据域element和指针域next
typedef struct LNode {
    
    
	ElemType element;				// 表示单链表中某个结点的数据部分
	Position next;					// next指针域指向下一个结点
}LNode;

// 单链表的通用基础操作的函数声明
void InitList(LinkList* PPHead);
bool ListIsEmpty(const LinkList L);
Position CreateLNode(const ElemType elem);
Position ListGetElem(const LinkList L, int pos);
void ListInsertElem(LinkList L, int pos, ElemType elem);
void ListPushBack(LinkList L, ElemType elem);
ElemType ListDeleteElem(LinkList L, int pos);
ElemType ListPopBack(LinkList L);
void ListModifyElem(const LinkList L, int pos, ElemType elem);
int ListGetLength(const LinkList L);
void ClearList(LinkList L);
void DestroyList(LinkList* PPHead);

// 整型单链表的基础操作,数据类型固定是整型
Position ListLocatePrevious(const LinkList L, ElemType elem);
void ListRemoveElem(LinkList L, ElemType elem);
Position ListLocateElem(const LinkList L, ElemType elem);
int ListLocatePos(const LinkList L, ElemType elem);
bool InputInteger(int* num);
void List_HeadInsert(LinkList* PPHead);
void List_TailInsert(LinkList* PPHead);
void PrintList(const LinkList L);

何时需要二级指针?

注意看上述函数声明传入的参数。
大部分时候都是传入LinkList L。
只有4个函数需要传入LinkList* PPHead,也就是二级指针。
它们分别是:

void InitList(LinkList* PPHead);
void DestroyList(LinkList* PPHead);
void List_HeadInsert(LinkList* PPHead);
void List_TailInsert(LinkList* PPHead);

因为这4个函数都改变了头指针的值。
初始化单链表需要让头指针指向新创建的头结点。
摧毁单链表需要释放头节点空间,让头指针指向NULL。
所以,必须传入头指针的指针,也就是二级指针。
而头插法和尾插法建表都需要初始化单链表,所以也需要传入二级指针。
而像插入,删除等操作,并不需要传入二级指针
它们虽然改变了单链表的元素,但是最多需要改变头结点的值,不需要改变头指针的值。
可以通过L->next,利用头指针间接改变头结点的值。
就不用传入二级指针了。


单链表的定义

单链表是链式存储结构,它的数据元素并不是连续存储在内存空间中的。
我们要定义的并不是整个单链表,而是一个单独的结点。
它的数据域用于存储单个结点的数据,它的指针域用来指向下一个结点。

// 定义单个结点,包含数据域element和指针域next
typedef struct LNode {
    
    
	ElemType element;				// 表示单链表中某个结点的数据部分
	Position next;					// next指针域指向下一个结点
}LNode;

ElemType可以指代任何所需数据类型,目前设置为int类型,要改变数据类型只需要改变头文件声明里的这一行代码:

typedef int ElemType;				// 设置链表元素的类型,可以随时通过这行代码改变元素类型

Position是用typedef工具重新定义的名称,等价于:struct LNode*,也就是单个结点的结构指针类型。
相关typedef语句为:

struct LNode;						// 定义结构体LNode,暂时不创建该结构体的模板
typedef struct LNode* PtrToLNode;	// 定义名称PtrToNode(意思是,指向node的指针,本质是struct LNode*)
typedef PtrToLNode LinkList;		// 定义名称LinkList(本质是struct LNode*),常用于头指针
typedef PtrToLNode Position;		// 定义名称Position(本质是struct LNode*),常用于指向普通结点

这种命名方式我借鉴了经典书籍《数据结构与算法分析——C语言描述》里的代码。
因为Position名称常用于表示一个普通结点的地址,这和Position的英文含义位置吻合。
如果你想对一般的结点进行操作,定义类型Position更加适合。
而LinkList名称更适合定义头指针,头指针是头结点的地址,头结点的地址虽然也是struct Node*,但它是一个特殊的结点。
有了头指针,就能找到整个单链表,所以把头指针作为参数传入就相当于传入了找到整个单链表的方式。
用LInkList L;定义头指针,就好像定义了一个单链表一样,在函数接口处可以清楚的表明你传入的是头指针,而不是一般的头结点。
虽然LinkList和Position本质上都是struct Node*,但是它们的应用位置是不一样的,不同的名称可以更准确的表明代码的意图。



单链表的基本通用操作


包括以下基本通用操作
1.初始化单链表。
2.单链表是否为空。
3.创建一个新结点。
4.查找单链表指定位置上的元素。
5.在指定位置插入元素。
6.在尾部插入新结点。
7.删除指定位置上的元素。
8.删除尾结点。
9.修改指定位置上元素的值。
10.计算单链表长度。
11.清空单链表。
12.摧毁单链表。

下面的操作只针对整型单链表
1.查找和指定值相同的第一个结点的前驱元的地址。
2.删除和指定值相同的第一个元素。
3.返回和指定值相同的第一个元素。
4.返回和指定值相同的第一个元素的序号。
5.自制输入函数(为了头插和尾插方便)。


1.初始化单链表

初始化单链表需要创建头结点。
让传入的头指针指向头结点,再让头结点的指针域指向NULL。
由于函数是值传递,所以头指针L的值并没有被改变。
所以,需要将改变后的头指针L作为返回值返回。
调用函数需要将返回值赋值给原本的头指针变量,才能完成单链表的初始化。

/*
 * 初始化单链表,让头指针指向新创建的头结点,并让头结点的指针域指向NULL
 * 由于函数传参是值传递,所以得返回改变后的头结点L的值
 */ 
LinkList InitList(LinkList L)
{
    
    
	// 通过malloc函数新建一个头结点,并让头指针指向头结点
	L = (LinkList)malloc(sizeof(LNode));
	// 如果分配空间失败,直接终止程序,并调用abort函数,显示相关提示信息
	assert(L != NULL);
	// 如果头指针已经指向新创建的头结点,此时头结点之后暂时没有其他结点
	L->next = NULL;			// 让头结点的指针域指向NULL
	return L;				// 返回改变后的L,调用函数需要将该返回值赋值给原本的头指针变量
}

另一种实现方式是利用二级指针。
注意,LinkList是typedef工具定义的新名称,实际等价于struct LNode*。
所以,LinkList*实际是struct LNode**,是一个二级指针。
这样实现可以利用二级指针间接改变头指针的值,就不需要返回值。
调用函数需要传入头指针的地址。

/*
 * 初始化链表,让头指针指向新创建的头结点,并让头结点的指针域指向NULL
 * 注意,L是指向头指针的指针,是一个二级指针
 */
void InitList(LinkList* PPHead)
{
    
    
	// 通过malloc函数新建一个头结点,并让头指针指向头结点
	(*PPHead) = (LinkList)malloc(sizeof(LNode));
	// 如果分配空间失败,直接终止程序,并调用abort函数,显示相关提示信息
	assert((*PPHead) != NULL);
	// 如果头指针已经指向新创建的头结点,此时头结点之后暂时没有其他结点
	(*PPHead)->next = NULL;			// 让头结点的指针域指向NULL
}

2.单链表是否为空

只需要一行代码即可完成该操作。
空表头结点的next指针域为NULL,返回true,反之,返回false。

// 如果单链表为空,返回true,否则返回false
bool ListIsEmpty(const LinkList L)
{
    
    
	return L->next == NULL;		// 如果是空表,表达式为真返回true,反之返回false
}

3.创建一个新结点

为链表增加新结点是很基础的功能,我们可以将它单独写成一个模块。
类似插入函数的实现需要创建新结点时,直接在内部使用CreateLNode函数即可。
由于新结点还没有连接上链表,所以它的next指针域默认为NULL。

// 创建一个新结点
Position CreateLNode(const ElemType elem)
{
    
    
	// 用malloc函数为新结点分配内存空间
	Position newNode = (Position)malloc(sizeof(LNode));
	// 如果创建新结点失败,就打印提示信息,并终止程序
	if (newNode == NULL)
	{
    
    
		puts("The creation of a new node failed!");
		exit(EXIT_FAILURE);
	}
	// 如果新结点分配内存空间成功
	newNode->element = elem;	// 将传入的数据赋值给新结点的数据域
	newNode->next = NULL;		// 暂时将新结点的next指针域赋值为NULL
	return newNode;				// 返回新结点的位置
}

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

单链表做越界检测比顺序表复杂,因为得到单链表长度的算法是O(n)复杂度。
所以,做越界检查需要分为2部分,先检测pos是否小于0,如果小于0,就打印提示信息,终止程序。
然后找指定位置的元素,如果找到了就返回该元素的地址,如果没找到就打印提示信息,终止程序。
问:为什么pos为0,要设计成返回头结点的地址?
:因为该函数可以在插入函数的实现中使用,空表也可以进行插入操作,空表的插入是在头结点的后面插入新结点,所以需要头结点的地址。

/*
 * 查找单链表指定位置元素的地址,如果pos为0,则返回头结点的地址
 * 如果指定位置超出范围,就打印提示信息,并终止程序
 */ 
Position ListGetElem(const LinkList L, int pos)
{
    
    
	// 如果指定位置小于0,则打印越界提示信息,并终止程序
	if (pos < 0)
	{
    
    
		puts("The position is out of range!");
		exit(EXIT_FAILURE);
	}
	// cur为位置指针,指向当前结点,初始化为指向头结点
	Position cur = L;
	int count = 0;			// 用来记录cur位置指针移动了多少次
	// 初始cur指向头结点,需要让cur移动pos次,即可找到指定结点的地址
	// 如果指定位置超过单链表长度,那么当cur==NULL就结束循环
	while (cur != NULL && count < pos)
	{
    
    
		cur = cur->next;
		count++;
	}
	// 如果指定位置超过单链表长度,则打印越界提示信息,并终止程序
	if (cur == NULL)
	{
    
    
		puts("The position is out of range!");
		exit(EXIT_FAILURE);
	}
	return cur;			// 如果指定位置在范围内,返回对应的数据元素地址
}

5.查找指定位置的前驱元

该函数主要用于删除函数,可以让删除操作实现模块化

/*
 * 查找单链表指定位置元素的前驱元,pos的合法范围为[1,length]
 * 如果pos为1,则返回头结点的地址
 */
Position ListGetElemPrevious(const LinkList L, int pos)
{
    
    
	// 空表的异常处理
	if (ListIsEmpty(L))
	{
    
    
		puts("The simple link list is empty!");
		exit(EXIT_FAILURE);
	}
	// 如果左边越界,就处理异常
	if (pos <= 0)
	{
    
    
		puts("The position is out of range!");
		exit(EXIT_FAILURE);
	}
	Position prec = L;
	int count = 0;
	while (count < pos - 1)
	{
    
    
		prec = prec->next;
		count++;
		if (prec->next == NULL)
		{
    
    
			puts("The position is out of range!");
			exit(EXIT_FAILURE);
		}
	}
	return prec;
}

6.在指定位置插入元素

空表也可以执行插入操作,不用检查单链表是否为空。
在指定的位置上插入元素,需要找到当前位置的前驱元。
需要先让新结点连接上链表,再让当前位置的前驱元连接上新结点。
如果先让前驱元连接上新结点,则会丢失链表后面的元素。
插入位置越界的异常处理由ListGetElem函数完成,插入函数本身不用重复检查越界

// 在单链表的指定位置上插入元素,位置范围为:[1,length+1]
void ListInsertElem(LinkList L, int pos, ElemType elem)
{
    
    
	// 返回指定位置的前驱元素的地址
	// ListGetElem函数会做范围越界的异常处理
	Position prec = ListGetElem(L, pos - 1);
	// 创建一个新结点,让它数据域的值为elem
	Position newLNode = CreateLNode(elem);
	/* 插入结点操作 */
	newLNode->next = prec->next;	// 先让新结点连接上链表
	prec->next = newLNode;			// 再让指定位置的前驱元素连接上新结点
}

7.在尾部插入新结点

利用自制的CreateLNode函数可以很优雅的实现该函数。
因为CreateLNode函数创建的新结点的next指针域默认为NULL。
只需2步:
1.找到尾结点。
2.让尾结点的next指针域指向新结点。

// 在单链表最后插入一个新结点
void ListPushBack(LinkList L, ElemType elem)
{
    
    
	// 用tail指针找到单链表的尾部,从头结点开始找
	Position tail = L;
	// 循环结束后,tail指向最后一个结点
	while (tail->next != NULL)
		tail = tail->next;
	// 用CreateLNode函数创建一个新结点,新结点的next指针域刚好默认为NULL
	Position newLNode = CreateLNode(elem);
	// 让原本的尾结点指向新结点
	tail->next = newLNode;
}

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

删除操作需要检查单链表是否为空。
该函数不仅会删除指定位置的元素,还返回被删除元素的值。
核心逻辑为
1.找到被删除位置的前驱元。
2.保存被删除元素的值。
3.让前驱元直接指向下下个元素,这样被删除元素就从链表中断开了。
4.释放被删除元素的空间。
5.返回被删除元素的值。

/ 删除单链表指定位置上的元素,并返回该元素的值,位置范围为:[1,length]
ElemType ListDeleteElem(LinkList L, int pos)
{
    
    
	// 返回指定位置的元素的前驱元的地址
	Position prec = ListGetElemPrevious(L, pos);
	/* 删除结点操作 */
	Position cur = prec->next;					// 用于保存将要删除结点的地址
	ElemType deleteElem = cur->element;			// 保存将被删除的元素的值;
	prec->next = cur->next;						// 将被删除结点从链表中断开
	free(cur);									// 释放被删除的结点
	return deleteElem;							// 返回被删除元素的值
}

9.删除尾结点

删除操作需要检查单链表是否为空。
该函数不仅会删除尾结点,还返回尾结点的值。
核心逻辑为:
1.找到尾结点的前驱结点。
2.保存被删除的值。
3.释放尾结点的空间。
4.让尾结点的前驱结点的next指针域为NULL。
5.返回被删除的值。

// 删除单链表最后一个结点,并返回该结点的值
ElemType ListPopBack(LinkList L)
{
    
    
	// 如果单链表为空,就打印提示信息,并终止程序
	if (ListIsEmpty(L))
	{
    
    
		puts("The single linked list is empty!");
		exit(EXIT_FAILURE);
	}
	// 用prec前驱指针指向尾结点的上一个结点,初始化为指向头结点
	Position prec = L;
	// 通过循环,找到尾结点的前驱结点的地址
	while (prec->next->next != NULL)
		prec = prec->next;
	// 保存被删除结点的值
	ElemType deleteElem = prec->next->element;
	free(prec->next);					// 释放掉尾结点的内存空间
	prec->next = NULL;					// 让倒数第二个结点的next指针域为NULL
	return deleteElem;
}

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

修改前要检查单链表是否为空。
需要单独做pos为0的越界处理和空表异常处理。

// 修改单链表指定位置上的元素,位置范围为:[1,length]
void ListModifyElem(const LinkList L, int pos, ElemType elem)
{
    
    
	// 如果单链表为空,就打印提示信息,并终止程序
	if (ListIsEmpty(L))
	{
    
    
		puts("The single linked list is empty!");
		exit(EXIT_FAILURE);
	}
	// 查找指定位置的元素
	if (pos = 0)
	{
    
    
		puts("The position is out of range!");
		exit(EXIT_FAILURE);
	}
	Position cur = ListGetElem(L, pos);
	cur->element = elem;		// 修改该元素的值
}

11.计算单链表长度

单链表不能直接得出表长,计算表的长度需要O(n)的复杂度。

// 得到单链表的元素个数
int ListGetLength(const LinkList L)
{
    
    
	// 初始化计数变量为0
	int length = 0;
	// cur为位置指针,指向当前结点,初始化为指向头结点
	Position cur = L;
	// 每移动一个位置,length+1,如果当前位置的next指针域为空,说明到结尾,则跳出循环
	while (cur->next)
	{
    
    
		cur = cur->next;
		length++;
	}
	return length;				// 返回单链表的长度
}

12.清空单链表

清空单链表的数据结点,释放分配的空间,只留下头节点。
需要指向当前被释放结点的后继结点的指针,否则释放掉该结点的空间后,找不到链表了。
最后让头节点的next指针域为NULL。

// 清空单链表,释放所有数据结点的空间,只保留头结点
void ClearList(LinkList L)
{
    
    
	// cur为位置指针,指向当前结点,初始化为指向第一个数据结点
	Position cur = L->next;
	// 依次释放每一个结点的空间,用指向后继结点的succ指针记录下一个结点的位置
	while (cur)
	{
    
    
		Position succ = cur->next;	// succ指针指向当前将被释放结点的后继元
		free(cur);					// 释放当前结点的内存空间
		cur = succ;					// 让cur指针指向下一个结点
	}
	L->next = NULL;					// 让头结点的next域为NULL
}

13.摧毁单链表

释放所有结点的空间,包括头结点。
*PPHead是头指针,指向表头。删除结点从头结点开始,新表头永远是被删除结点的下一个结点。
让表头永远指向被删除结点的下一个结点,免得丢失掉链表。

// 摧毁单链表,释放已分配的所有空间,包括头结点
void DestroyList(LinkList* PPHead)
{
    
    
	// 让*PPHead指向被释放结点的下一个结点
	// 相当于每释放一个结点,单链表的新表头就是被释放结点的下一个结点
	// 循环结束时,*PPHead=NULL,防止了野指针的问题
	while (*PPHead)
	{
    
    
		Position cur = *PPHead;			// cur指向当前被释放的结点
		// 让表头指向下一个结点,因为上一个结点即将被释放
		(*PPHead) = (*PPHead)->next;	
		free(cur);						// 释放当前结点的空间
	}
}


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


1.查找和指定值相同的第一个结点的前驱元的地址

查找操作要检查是否为空表。
如果当前结点的下一个结点的element数据域和指定值相同,就认为找到了。
如果找到的结点是第一个结点,那就返回它的前驱元头结点。
如果已经找到最后一个结点,就打印提示信息,并终止程序。

/*
 * 返回单链表内和指定值相同的第一个元素的前驱元素地址
 * 如果和指定值相同的第一个元素为第一个数据结点,那就返回头结点
 * 如果没找到,就打印提示信息,并结束程序
 */
Position ListLocatePrevious(const LinkList L, ElemType elem)
{
    
    
	// 如果单链表为空,就打印提示信息,并终止程序
	if (ListIsEmpty(L))
	{
    
    
		puts("The single linked list is empty!");
		exit(EXIT_FAILURE);
	}
	// prec指向当前的前驱结点,初始化为指向头结点
	Position prec = L;
	while (prec->next != NULL && prec->next->element != elem)
		prec = prec->next;		// 让前驱结点往下移动一次
	// 如果没找到,就打印提示信息,并终止程序
	if (prec->next == NULL)
	{
    
    
		puts("The value is not in the sequence list!");
		exit(EXIT_FAILURE);
	}
	return prec;				// 返回要找元素的前驱元素
}

2.删除和指定值相同的第一个元素

异常处理由ListLocatePrevious函数完成。
只需要让被删结点的前驱结点连接下下个结点,就可以完成删除操作。
之后再释放被删除结点的内存空间。

/*
 * 删除单链表内和指定值相同的第一个元素
 * 如果没找到和指定值相同的元素,则打印提示信息,并终止程序
 * 异常处理部分由ListLocatePrevious函数实现
 */
void ListRemoveElem(LinkList L, ElemType elem)
{
    
    
	// prec指向要找元素的前驱元素
	Position prec = ListLocatePrevious(L, elem);
	Position cur = prec->next;  // cur为位置指针,指向要找的结点
	prec->next = cur->next;		// 将被删除结点从单链表中断开
	free(cur);					// 释放被删除结点的内存空间
}

3.返回和指定值相同的第一个元素

查找操作需要检查是否为空表。
如果没找到指定值,就返回NULL,如果找到了,就返回该结点。

// 返回单链表内和指定值相同的第一个元素的地址,如果没有找到就返回NULL
Position ListLocateElem(const LinkList L, ElemType elem)
{
    
    
	// 如果单链表为空,就打印提示信息,并终止程序
	if (ListIsEmpty(L))
	{
    
    
		puts("The single linked list is empty!");
		exit(EXIT_FAILURE);
	}
	// cur为位置指针,指向当前结点,初始化为指向第一个数据结点
	Position cur = L->next;	
	// 如果没找到指定元素同时当前位置没到末尾,就继续循环
	while (cur != NULL && cur->element != elem)
		cur = cur->next;
	return cur;			// 返回找到的元素的地址,如果没找到就返回NULL
}

4.返回和指定值相同的第一个元素的序号

查找需要先做非空检测。
从第一个数据结点开始找,每向后移动一个结点,pos+1。
如果找到了,就返回pos,如果移动到末尾还没找到,就返回0。

/ 返回单链表内和指定值相同的第一个元素的序号,如果没有找到就返回0
int ListLocatePos(const LinkList L, ElemType elem)
{
    
    
	// 如果单链表为空,就打印提示信息,并终止程序
	if (ListIsEmpty(L))
	{
    
    
		puts("The single linked list is empty!");
		exit(EXIT_FAILURE);
	}
	int pos = 1;			// 记录当前元素序号的变量
	// cur为位置指针,指向当前结点,初始化为指向第一个数据结点
	Position cur = L->next;
	/* 找指定元素的操作 */
	while (cur != NULL && cur->element != elem)
	{
    
    
		cur = cur->next;  
		pos++;			    // 循环一次,当前序号加1
	}
	// 如果没有找到就返回0
	if (cur == NULL)
		pos = 0;
	return pos;			    // 返回找到元素的序号
}

5.自制输入函数(为了头插和尾插方便)

将键盘输入的一个整数赋值给num。
如果输入的是整数,就返回true。
如果输入的是非数字字符,就清空缓冲区,并返回false。

/*
 * 由于头插和尾插都需要连续输入多次数字,而scanf函数不能完美完成这项功能
 * 所以我另写了一个函数专用于将输入的整数存入指定整型变量
 * 如果输入的是整数,就返回true,如果输入非数字字符,就返回false,并且清空缓冲区
 */ 
bool InputInteger(int* num)
{
    
    
	int ok;					// ok用于存储scanf函数成功读入的整数个数
	// 输入num的值,如果合法,ok为1,否则ok不为1
	ok = scanf("%d", num);
	// 如果输入的字符非法,利用下面代码去掉缓冲区里多余的字符
	if (ok != 1)
	{
    
    
		while (getchar() != '\n')
			continue;
	}
	// 如果输入的是整数,就返回true,如果输入非法字符,就返回false
	return ok == 1;		
}

6.头插法建立整型单链表

头插法每次会将新结点插入到头结点后面,成为第一个数据结点。
所以,头插法输入数据的顺序会和单链表中的数据顺序相反。
输入整数的函数是我内置的InputInteger函数,它可以输入任意的整数,比直接使用scanf函数好。
插入时要先让新结点连接上链表,再让头结点指向新结点。

/*
 * 使用头插法建立整型单链表,输入数据会依次插入单链表的头部
 * 所以,输入数据的顺序和单链表中的数据顺序会相反
 */
void List_HeadInsert(LinkList* PPHead)
{
    
    
	InitList(PPHead);			// 先初始化链表
	int num;				// num用于给每个结点的数据域赋值
	puts("Input a series of integers(enter q to quit):");
	// 可以利用InputInger函数每次输入一个整数给num,如果输入q就结束循环
	while(InputInteger(&num))
	{
    
    
		// 用CreateLNode函数创建一个新结点,并将num的值赋值给数据域
		Position tmpLNode = CreateLNode(num);
		tmpLNode->next = (*PPHead)->next;  // 必须先让新结点连上已经建立的结点
		(*PPHead)->next = tmpLNode;		   // 再让头结点的指针域指向新结点
	}
}

7.尾插法建立整型单链表

需要用一个尾指针指向当前单链表的尾结点。
每次插入只需要让当前尾结点连接上新建的结点,再让尾指针往后移动一位。

/*
 * 使用尾插法建立整型单链表,输入数据会依次插入单链表的尾部
 * 所以,输入数据的顺序和单链表中的数据顺序相同
 */
void List_TailInsert(LinkList* PPHead)
{
    
    
	InitList(PPHead);			// 先初始化链表
	int num;				// num用于给每个结点的数据域赋值
	// tail指针指向单链表的尾部,因为链表刚初始化,所以指向头结点
	Position tail = *PPHead;
	puts("Input a series of integers(enter q to quit):");
	// 可以利用InputInger函数每次输入一个整数给num,如果输入q就结束循环
	while (InputInteger(&num))
	{
    
    
		// 用CreateLNode函数创建一个新结点,并将num的值赋值给数据域
		// 用CreateLNodE函数创建的新结点的next指针域正好为NULL,适合放到末尾
		Position tmpLNode = CreateLNode(num);
		tail->next = tmpLNode;		// 让单链表尾部连接上新结点
		tail = tail->next;			// 尾指针后移一位
	}
}

8.打印整型单链表

每次打印一个结点的整数值,打印完所有结点后,再在结尾输出NULL,并且换行。
效果为:
1->2->3->4->5->NULL

// 依次打印单链表的数据
void PrintList(const LinkList L)
{
    
    
	// cur为位置指针,指向当前结点,初始化为指向第一个数据结点
	Position cur = L->next;
	// 如果cur不为空,说明没到单链表结尾,就依次打印数据元素
	while (cur)
	{
    
    
		printf("%d->", cur->element);	// 打印当前结点的数据元素
		cur = cur->next;				// 将位置指针指向下一个结点
	}
	puts("NULL");
}

猜你喜欢

转载自blog.csdn.net/qq_983030560/article/details/128104204