一起学数据结构(3)——万字解析:链表的概念及单链表的实现

上篇文章介绍了数据结构的一些基本概念,以及顺序表的概念和实现,本文来介绍链表的概念和单链表的实现,在此之前,首先来回顾以下顺序表的特点:

1.顺序表特点回顾:

1. 顺序表是一组地址连续的存储单元依次存储的线性表的数据结构,逻辑上:顺序表中相邻的数据元素,其物理次序也是相邻的。

2. 顺序表的优点: 任一元素均可以随机存取

3.顺序表的缺点:进行插入和删除操作时,需要移动大量的元素,存储空间不灵活。

2. 链表的分类及概念:

2.1 链表的分类:

1.单链表:结点只有一个指针域的链表,称之为链式线性表或者单链表:

   

2.双链表:结点由两个指针域的链表:

3.循环链表:首尾相连的链表:

 本文将着重介绍单链表,下面给出单链表的概念及相关特点:

2.2 单链表的概念即特点:

单链表指的是链表的每个结点中只包含一个指针域,对于链表,下面给出定义:

线性表链式存储的结构是:用一组任意的存储单元 存储线性表的数据元素(这组存储单元可以连续,也可以不连续)。为了表示每个数据元素a_i与其后记数据元素^{a_{i+1}}之间的逻辑关系,所以,对于存储数据元素a_i的存储单元,还需要存储一个指示后记数据元素^{a_{i+1}}的相关信息,(即存储^{a_{i+1}}所在的地址)。这两部分信息构成了数据元素a_i的存储映像,称为结点。

对于结点,包括了两个域:存储数据元素信息的域称之为数据域,存储后记元素存储位置的域称之为指针域。指针域中存储的信息称作指针或者链。n个结点链成一个链表,即为线性表的链式存储结构

(注:对于链表更详细的定义可以参考人民邮电出版社出版的《数据结构》,本文不再过多说明)

3.单链表的代码实现:

3.1 链表的存储结构及结点的创建:

单链表的定义中提到了,链表是由n个结点构成的,每个结点中包含了两个与,一个是用于存储数据元素信息的数据域,另一个是用于存储下一个数据元素信息地址的指针域。不同类型的信息的保存,可以由结构体进行实现,所以,下面用图给出单个结点的结构:

其中,data表示存储的数据元素信息,next表示下一个数据元素信息的地址。并且,将每一个这种结点命名为newnode,对上述结点用代码表示:

typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;// 对应了存储的元素信息
	struct SListNode* next; // 对应了下一个数据元素信息的地址
}SLTNode;

对于链表,也和顺序表一样,可以实现增删查改各种功能,而实现这些功能的基础,就是如何创造新的结点,为了解决这个问题,可以专门定义一个函数BySListNode来实现。前面说到,链表各个结点之间的链接是通过某个结点存储下一个结点的地址来实现的。所以,对于函数BySListNode的返回类型,应该定义为SLTNode*型,即返回结构体的地址。

对于一个结点的创建,同样可以采用在顺序表中用动态内存函数malloc动态开辟内存的方式进行实现。具体代码如下:

SLTNode* BuySListNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc");
        exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

 下面给出代码来测试函数BySListNode的功能:

为了测试函数的功能,首先需要针对链表来封装一个打印函数,将打印函数命名为SLTPrint,代码如下:

void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
}

在链表的定义中说过,链表取任意元素必须从头节点开始向后依次寻找。所以,为了防止头指针的值被更改,后续无法找到第一个结点,所以,在上述代码中,创建了一个新的变量cur用来存放头指针phead中的地址。

对于cur = cur->next;这一行代码,是用于让cur保存下一个结点的地址,例如下图中cur的变化所示:

 下面,封装一个函数Testlist1来测试插入结点的功能,代码初步表示如下:

void Testlist()
{
	printf("请输入想要插入的元素的个数:");
	int n = 0;
	scanf("%d", &n);
	printf("请输入插入的元素:");
	for (size_t i = 0; i < n; i++)
	{
		int val = 0;
		scanf("%d", &val);
		SLTNode* newnode = BuySListNode(val);
		
	}
}
int main()
{
	Testlist1();
	return 0;
}

但是对于上面给出的代码,并不能完成对多个结点的数据元素的打印,因为在创建结点后,并没有将前后的结点进行链接。同时,也并没有出现上面图中所说的头指针。

为了建立前后结点的链接。所以,在上面Testlist1函数中的代码的基础上,人为建立一个头指针,定义为plist,赋值为NULL

(注:下面为了方便进行演示,对于结点之间建立的过程采用头插的思想,但是并不等价于后面的头插)

对于链接各个结点,需要分以下两种情况:

1.头指针为NULL,即还未链接任何结点,但是已经创建了一个结点:

为了达到链接的效果,只需要将plist中存储的地址改为新结点newnode的地址即可。 

2.头指针不为空:

 此时,plist中已经通过存储第一结点的地址达到链接第一结点的目的,为了链接第二结点,需要将plist中存储的地址改为第二结点的地址。

注释中提到,为了方便演示采用头插的思想,对于头插,可以用下图进行表示:

例如,将地址为 0x0012ffa0的结点进行头插。为了完成头插,需要进行两步操作:

1.将地址为0x0012ffa0的结点(即新结点)中存储的地址改为0x0012ffb0(插入前的第一个结点)

2.将头指针中存储的地址改为0x0012ffa0(即新结点的地址)

对上述分析进行归纳,代码如下:

void Testlist()
{
	printf("请输入想要插入的元素的个数:");
	int n = 0;
	scanf("%d", &n);
	SLTNode* plist = NULL;
	printf("请输入插入的元素:");
	for (size_t i = 0; i < n; i++)
	{
		int val = 0;
		scanf("%d", &val);
		SLTNode* newnode = BuySListNode(val);
		if (plist == NULL)
		{
			plist = newnode;
		}
		else
		{
			newnode->next = plist;
			plist = newnode;
		}
		
	}
    SLTPrint(plist);
}

其中,newnode是一个结构体指针,所以需要用到->进行解引用。来改变新结点newnode中存储的下一个数据元素信息的地址。

测试效果如下:

3.2 链表功能实现——尾插: 

定义尾插函数SLTPushBack其内部参数如下面代码所示:

void SLTPushBack(SLTNode* phead, SLTDataType x);

对于尾插这一功能,首先需要找到链表的尾端。前面说到,头指针对于链表的各种功能来说都非常重要,所以,为了保证头指针不被更改,这里定义一个新的结构体指针tail来存储头指针中存储的地址。

对于如何找到尾端,下面给出一段示例的代码:

void SLTPushBack(SLTNode* phead, SLTDataType x)
{
	SLTNode* newnode = BuySListNode(x);
	SLTNode* tail = phead;
	while (tail != NULL)
	{
		tail = tail->next;
	}
	tail = newnode;
}

上述代码给出的思路很明确。利用while循环不断检查指针tail是否为NULL,不为空,则tail存储下一个结点的指针。看似没有错误。但是如果将上述过程用图形表示,则上述代码会引起内存泄漏这一错误,具体如下:

一开始,指针tail存储了头指针phead中的地址,此时tail\neq NULL,于是执行循环内部的代码,此时,tail中存储的地址为0x0012ffb0,效果如下图所示:

依次循环上述步骤:

 再次循环上述步骤:

 到最后一步时,指针tail中存储的地址为NULL。到这里,while循环终止。已经寻找到尾端地址。假设,在循环之前新建立的结点如下图所示:

如果按照上面给出的代码来执行,即:tail = newnode;,则,执行结束后,最后一个结点和指针tail中存储的地址如下:

此时,便会出现一个问题,即,指针tail是只存在与函数内部的一个临时变量,出函数便会销毁。但是,最后一个结点中存储的地址仍然为NULL。最后一个结点和新的结点并未建立联系。造成了内存泄露的问题。 

因为,完成尾插的标志,便是最后一个结点存储的地址被更改为新结点的地址。通过上面的错误例子。可以得出一个结论。如果想要将最后一个结点存储的地址改为新结点的地址,则,不可以让临时指针tail赋值最后一个结点中存储的地址。应该赋值最后一个结点的前一个结点的存储的地址。

再通过这个结点存储的地址,来对最后一个结点存储的地址进行修改。所以,代码可以更改为:
 

void SLTPushBack(SLTNode* phead, SLTDataType x)
{
	SLTNode* newnode = BuySListNode(x);
	SLTNode* tail = phead;
	while (tail ->next != NULL)
	{
		tail = tail->next;
	}
	tail -> next = newnode;
}

通过下面的测试来测试尾插的功能:

void Testlist1()
{
	printf("请输入想要插入的元素的个数:");
	int n = 0;
	scanf("%d", &n);
	SLTNode* plist = NULL;
	printf("\n请输入插入的元素:");
	for (size_t i = 0; i < n; i++)
	{
		int val = 0;
		scanf("%d", &val);
		SLTNode* newnode = BuySListNode(val);
		if (plist == NULL)
		{
			plist = newnode;
		}
		else
		{
			newnode->next = plist;
			plist = newnode;
		}
	}
	SLTPrint(plist);
	printf("\n");
	SLTPushBack(plist, 10000);
	SLTPrint(plist);
}

结果如下:

上面关于尾插的代码,只能是在有结点的情况下运行。对于头指针为空的情况下并不适用,下面将对上面的尾插代码进行完善:

给出下列代码:

void SLTPushBack(SLTNode* phead, SLTDataType x)
{
	SLTNode* newnode = BuySListNode(x);
	if (phead == NULL)
	{
		phead = newnode;
	}
	SLTNode* tail = phead;
	while (tail ->next != NULL)
	{
		tail = tail->next;
	}
	tail -> next = newnode;
}

这里给出的代码对比尾插,仅仅是多了一个对于头指针phead = NULL的情况的判定。中心思想就是在头指针phead = NULL时,将头指针保存的地址改为第一个结点的地址。但是这种做法并不正确。因为这里所说的头指针phead只是函数内部的一个形式参数。真正的头指针时上面定义的plist。此时,函数形式参数传递的时形参phead中保存的值,在前面关于C语言函数的文章中曾提到函数的两个传递参数的方式:传值和传址。对于传值调用,并不能改变外部变量的值。所以,这里虽然对头指针phead存储的地址进行改变。但是却没有真正的改变函数外部实际参数plist中保存的地址。

对于上面的错误,可以通过传址调用来改变头指针plist中保存的地址。在前面对于C语言函数的文章的介绍中曾写过一个用传址调用来实现交换函数。所以,通过函数来交换两个变量中的值,需要用到一级指针。相对于,对于头指针plist,想要通过函数来改变他的值,需采用二级指针。

所以,正确的尾插代码应为:

void SLTPushBack(SLTNode** phead, SLTDataType x)
{
	SLTNode* newnode = BuySListNode(x);
	if (*phead == NULL)
	{
		*phead = newnode;
	}
    else
    {
	   SLTNode* tail = *phead;
	   while (tail ->next != NULL)
	     {
		   tail = tail->next;
	     }
	   tail -> next = newnode;
    } 
}

运用下面的测试,来测试尾插的功能:

void Testlist2()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPushBack(&plist, 5);
	SLTPrint(plist);
}

结果如下:

3.3 链表功能实现——头插:

对于头插功能的实现,上面已经给出来了大体思路。但是上面的头插并不是通过函数实现的。根据刚才尾插的实现可以发现。每进行一次头插,都需要对头指针plist中存储的地址进行更改。所以。在函数传递参数时,也需要传递二级指针。

代码如下:

void SLTPushFront(SLTNode** phead, SLTDataType x)
{
	SLTNode* newnode = BuySListNode(x);
	newnode->next = *phead;
	*phead = newnode;
}

用下列代码对头插功能进行测试:

void Testlist3()
{
	SLTNode* plist = NULL;
	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 4);
	SLTPushFront(&plist, 5);
	SLTPushFront(&plist, 6);
	SLTPrint(plist);
}

结果如下:

3.4 链表功能实现——尾删:

对于尾删功能的实现,需要考虑下面的三种情况:

1. 链表整体为空:

2.链表内只有一个结点

3.链表有多个结点

对第一种情况,只需要在进行尾删功能前,检查一下地址是否合法即可。

对于第三种情况,需要两步。第一步是删除处于尾部的结点。第二种情况是将前一个结点中保存的地址改为NULL即:

 第二种情况也需要分为两步,首先删除尾部结点。再把头指针中存储的地址改为NULL。与第三步不同的时,第三步更改地址的对象是结构体中的一个成员。第二步中更改的对象时头指针。所以,在进行对于第二种情况的地址改动时,需要传递二级指针。

下面先给出针对第一、第二种情况下的代码:

//尾删
void SLTPopBack(SLTNode** phead)
{
	assert(*phead);
	if ((*phead)->next == NULL)//只有一个结点的情况
	{
		free(*phead);
		*phead = NULL;
	}

}

对于第三种情况,假设此时有三个结点:

对于尾删功能,与尾插功能相同,第一步都是需要找到尾结点:

寻找尾结点时,采用与尾插寻找尾结点相同的方式,创建一个函数内部的指针变量SLTNode*tail来保存头指针保存的地址。当tail找到尾结点时:

如果此时就删除尾结点,还是会造成在讲解尾插原理中的错误。即,没能更改尾部结点前一个结点中存储的地址。如下图所示:

此时最后一个由malloc函数开辟的结点已经被free 。

为了解决上面的问题,可以在创建一个临时变量也用于保存phead中存储的地址。不过,这个变量的作用是用于更改尾结点前一个结点中存储的地址。这里将这个指针命名为tailprev,再有了这个指针后,当找到尾结点时,这两个指针的关系如下图所示:

先用代码表示指针tail

SLTNode* tail = *phead;
		SLTNode* tailprev = *phead;
		while (tail->next != NULL)
		{
			tail = tail->next;

		}

 为了达到图中两个指针一前一后的关系,可以让循环中在执行tail = tail -> next之前,让tailprev存储一次tail中的地址。代码如下:

//尾删
void SLTPopBack(SLTNode** phead)
{
	assert(*phead);
	if ((*phead)->next == NULL)//只有一个结点的情况
	{
		free(*phead);
		*phead = NULL;
	}
	else
	{
		SLTNode* tail = *phead;
		SLTNode* tailprev = NULL;
		while (tail->next != NULL)
		{
			tailprev = tail;
			tail = tail->next;

		}
		free(tail);
		tailprev->next = NULL;
	}
}

测试功能的代码如下:
 

void Testlist3()
{
	SLTNode* plist = NULL;
	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 4);
	SLTPushFront(&plist, 5);
	SLTPushFront(&plist, 6);
	SLTPrint(plist);
	printf("\n尾删");
	SLTPopBack(&plist);
	SLTPopBack(&plist);
	SLTPopBack(&plist);
	printf("\n");
	SLTPrint(plist);
}

结果如下:

3.5 链表功能实现——头删:

对于头删功能,依旧分为以下三种情况:

1. 链表整体为空:

2.链表内只有一个结点

3.链表有多个结点

对于第一种情况,直接检查头指针合法性即可。对于第三种情况,即多个结点,需要分为两步来完成头删:首先将头指针存储的地址改为第二个结点的地址。再把第一个结点free。对于第二种情况,和第三种情况相同,虽然只有一个结点。但是可以将NULL看作第二个结点的地址。代码如下:

//头删
void SLTPopFront(SLTNode** phead)
{
	assert(*phead);
	SLTNode* newhead = (*phead)->next;
	free(*phead);
	*phead = newhead;
}

测试函数如下:

void Testlist4()
{
	SLTNode* plist = NULL;
	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 4);
	SLTPushFront(&plist, 5);
	SLTPushFront(&plist, 6);
	printf("\n");
	printf("头删");
	printf("\n");
	SLTPrint(plist);
	SLTPopFront(&plist);
	SLTPopFront(&plist);
	SLTPopFront(&plist);
	printf("\n");
	SLTPrint(plist);

}

 结果如下:

 4. 单链表的代码实现——针对某一元素对应的位置进行操作:

4.1 通过某一具体元素来找到特定位置:

例如给出下面所示的一个单链表:

如果需要找到元素2所对应的位置,只需要将整体单链表进行一次遍历,若某个结点中的元素= 想要寻找的元素。则返回这个元素对应的结点的坐标,具体代码如下:

//寻找某个元素所对应的节点的地址:
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* tail = phead;
	while (phead != NULL)
	{
		if (tail->data == x)
		{
			return tail;
		}
		else
		{
			tail = tail->next;
		}
	}
	return NULL;
}

4.2 在某一特定数据对应的结点前插入新结点

前面知道了如何找到一个特定数据对应的结点的位置后,如果需要在这个结点之前插入一个新的结点。

对于这种形式的插入,需要分为两种情况:

1. 头指针为空,此时无法插入,检查指针合法性即可

2. 链表中只有一个结点,此时的插入就等于头插

3. 链表中有多个结点

对于前两种情况,具体的代码如下:

void SLTInsert(SLTNode** phead, SLTNode* pos, SLTDataType x)
{
	assert(*phead);
	if (*phead == NULL)
	{
		SLTPushFront(phead, x);
	}
}

对于第三种情况,需要考虑到,上面给出的查找函数返回的地址并不是插入新结点的地址,而是在这个地址对应的结点的前面进行插入。所以,此时的插入可以分为两步:

1.将新结点存储查找函数找到的结点的地址,这里将用查找函数找到的地址用指针pos存储。即:

2. 将原来pos对应的结点的前一个结点中存储的地址,改为存储新结点的地址,即:

 用代码表示上述过程,即:

void SLTInsert(SLTNode** phead, SLTNode* pos, SLTDataType x)
{
	assert(*phead);
	if (*phead == NULL)
	{
		SLTPushFront(phead, x);
	}
	else
	{
		SLTNode* prev = *phead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLTNode* newnode = BuySListNode(x);
		prev->next = newnode;
		newnode->next = pos;

	}
}

用下面的函数测试前插的功能:

void Testlist5()
{
	SLTNode* plist = NULL;
	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 4);
	SLTPushFront(&plist, 5);
	SLTPrint(plist);
	printf("\n");
	int x = 0;
	printf("请输入想要查找的值");
	scanf("%d", &x);
	SLTNode* pos = SLTFind(plist, x);
	printf("插入后:");
	SLTInsert(&plist, pos, x*10);
	SLTPrint(plist);
}

结果如下:

4.3 在某一特定数据对应的结点后插入新结点:

原理与前插相似,这里不再叙述,只给出图形表示:

对应代码如下:

void SLTInsertafter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySListNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

测试函数如下:

void Testlist5()
{
	SLTNode* plist = NULL;
	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 4);
	SLTPushFront(&plist, 5);
	SLTPrint(plist);
	printf("\n");
	int x = 0;
	int y = 0;
	printf("请输入想要查找的值");
	scanf("%d %d", &x,&y);
	SLTNode* pos = SLTFind(plist, x);
	printf("插入后:");
	SLTInsert(&plist, pos, x*10);
	SLTPrint(plist);
	printf("\n");
	printf("前插\n");
	SLTInsertafter(pos, y * 10);
	SLTPrint(plist);
}

结果如下:

4.4 删除某一特定元素对应位置的结点:

对于删除结点,也要分三种情况:

1. 链表整体为空,检查指针合法性即可

2.链表内只有一个结点,相当于头删 

3.链表有多个结点。

对于第三种情况。与前插相同,也需要创建一个指针来改变pos前面的结点中存储的地址,具体对应的代码如下:

void SLTErase(SLTNode** phead, SLTNode* pos)
{
	assert(pos);
	if (pos == *phead)
	{
		SLTPopFront(phead);
	}
	else
	{
		SLTNode* prev = *phead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
	}
}

测试函数如下:

void Testlist6()
{
	SLTNode* plist = NULL;
	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 4);
	SLTPushFront(&plist, 5);
	SLTPrint(plist);
	printf("\n");
	printf("请输入想要查找的值");
	int x = 0;
	scanf("%d", &x);
	SLTNode* pos = SLTFind(plist, x);
	SLTErase(&plist,pos);
	printf("\n");
	SLTPrint(plist);
}

结果如下:

 4.5删除某一特定元素对应位置后一个结点:

随机给出一个链表:

通过观察不难发现: 对于删除某一特定元素对应结点的后一个结点这个功能,对于两种情况是没有意义的:

1. 链表中没有结点。

2. 对应元素的结点恰好是最后一个结点。

所以,在进行删除之前,应该针对这两种情况进行地址的检查。

而对于删除,也需要创建一个新的指针,用来保存pos->nxet这个地址。如果不保存,则删除结点和链接结点之间会出现矛盾,例如:

如果不保存pos->nxet,若选择直接链接数据元素为3的结点,此时没有指针保存数据为2的结点的地址。如果先删除pos->nxet,也无法得到数据元素为3的结点的地址。所以应该创建一个新的指针,pos->nxet,用指针变量posNext保存这个地址。在进行链接数据元素为1,3这两个结点时,可以pos->next = posNext -> next来实现。删除数据元素为2的结点时,直接free(posNext),代码如下:

//删除对应位置后一个结点:
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);//检查是否是最后一个结点
	SLTNode* posNext = pos->next;
	pos->next = posNext->next;
	free(posNext);
}

测试函数如下:

void Testlist6()
{
	SLTNode* plist = NULL;
	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 4);
	SLTPushFront(&plist, 5);
	SLTPrint(plist);
	printf("\n");
	printf("请输入想要查找的值");
	int x = 0;
	scanf("%d", &x);
	SLTNode* pos = SLTFind(plist, x);
	/*SLTErase(&plist,pos);
	printf("\n");
	SLTPrint(plist);*/
	SLTEraseAfter(pos);
	printf("\n");
	SLTPrint(plist);

}

结果如下:

 5.总体代码:

5.1 头文件 SList.h:

#include<assert.h>
#include<stdio.h>
#include<stdlib.h>
typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

//创建结点
SLTNode* BuySListNode(SLTDataType x);

//打印各节点的信息
void SLTPprint(SLTNode* phead);

//尾插
void SLTPushBack(SLTNode** phead, SLTDataType x);

//头插
void SLTPushFront(SLTNode** phead, SLTDataType x);

//尾删
void SLTPopBack(SLTNode** phead);

//头删
void SLTPopFront(SLTNode** phead);

//寻找某个元素所对应的节点的地址:
SLTNode* SLTFind(SLTNode* phead,SLTDataType x);

//对应位置前插入
void SLTInsert(SLTNode** phead, SLTNode* pos, SLTDataType x);

//对应位置后插入:
void SLTInsertafter(SLTNode* pos, SLTDataType x);

//对应位置前删除
void SLTErase(SLTNode** phead, SLTNode* pos);

//删除对应位置后一个结点:
void SLTEraseAfter(SLTNode* pos);

5.1 函数功能实现——SLIst.c:

#include"SList.h"

//创建结点
SLTNode* BuySListNode(SLTDataType  x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

//打印结点信息
void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
}

//尾插
void SLTPushBack(SLTNode** phead, SLTDataType x)
{
	SLTNode* newnode = BuySListNode(x);
	if (*phead == NULL)
	{
		*phead = newnode;
	}
	else
	{
		SLTNode* tail = *phead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

//头插:
void SLTPushFront(SLTNode** phead, SLTDataType x)
{
	SLTNode* newnode = BuySListNode(x);
	newnode->next = *phead;
	*phead = newnode;
}

//尾删
void SLTPopBack(SLTNode** phead)
{
	assert(*phead);
	if ((*phead)->next == NULL)//只有一个结点的情况
	{
		free(*phead);
		*phead = NULL;
	}
	else
	{
		SLTNode* tail = *phead;
		SLTNode* tailprev = NULL;
		while (tail->next != NULL)
		{
			tailprev = tail;
			tail = tail->next;

		}
		free(tail);
		tailprev->next = NULL;
	}
}


//头删
void SLTPopFront(SLTNode** phead)
{
	assert(*phead);
	SLTNode* newhead = (*phead)->next;
	free(*phead);
	*phead = newhead;
}


//寻找某个元素所对应的节点的地址:
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* tail = phead;
	while (phead != NULL)
	{
		if (tail->data == x)
		{
			return tail;
		}
		else
		{
			tail = tail->next;
		}
	}
	return NULL;
}


//特定位置前插入:
void SLTInsert(SLTNode** phead, SLTNode* pos, SLTDataType x)
{
	assert(*phead);
	if (*phead == NULL)
	{
		SLTPushFront(phead, x);
	}
	else
	{
		SLTNode* prev = *phead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLTNode* newnode = BuySListNode(x);
		prev->next = newnode;
		newnode->next = pos;

	}
}

//特定位置后插入新结点
void SLTInsertafter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySListNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}


void SLTErase(SLTNode** phead, SLTNode* pos)
{
	assert(pos);
	if (pos == *phead)
	{
		SLTPopFront(phead);
	}
	else
	{
		SLTNode* prev = *phead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
	}
}


//删除对应位置后一个结点:
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);//检查是否是最后一个结点
	SLTNode* posNext = pos->next;
	pos->next = posNext->next;
	free(posNext);
}

5.3 函数测试文件Test.c:

#include"SList.h"


void Testlist1()
{
	printf("请输入想要插入的元素的个数:");
	int n = 0;
	scanf("%d", &n);
	SLTNode* plist = NULL;
	printf("\n请输入插入的元素:");
	for (size_t i = 0; i < n; i++)
	{
		int val = 0;
		scanf("%d", &val);
		SLTNode* newnode = BuySListNode(val);
		if (plist == NULL)
		{
			plist = newnode;
		}
		else
		{
			newnode->next = plist;
			plist = newnode;
		}
	}
	SLTPrint(plist);
	printf("\n");
	SLTPushBack(&plist, 10000);
	SLTPrint(plist);
	printf("\n");
}

void Testlist2()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPushBack(&plist, 5);
	printf("尾插");
	printf("\n");
	SLTPrint(plist);
	printf("\n");
}

void Testlist3()
{
	SLTNode* plist = NULL;
	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 4);
	SLTPushFront(&plist, 5);
	SLTPushFront(&plist, 6);
	SLTPrint(plist);
	printf("\n尾删");
	SLTPopBack(&plist);
	SLTPopBack(&plist);
	SLTPopBack(&plist);
	printf("\n");
	SLTPrint(plist);

}
void Testlist4()
{
	SLTNode* plist = NULL;
	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 4);
	SLTPushFront(&plist, 5);
	SLTPushFront(&plist, 6);
	printf("\n");
	printf("头删");
	printf("\n");
	SLTPrint(plist);
	SLTPopFront(&plist);
	SLTPopFront(&plist);
	SLTPopFront(&plist);
	printf("\n");
	SLTPrint(plist);

}

void Testlist5()
{
	SLTNode* plist = NULL;
	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 4);
	SLTPushFront(&plist, 5);
	SLTPrint(plist);
	printf("\n");
	int x = 0;
	int y = 0;
	printf("请输入想要查找的值");
	scanf("%d %d", &x,&y);
	SLTNode* pos = SLTFind(plist, x);
	printf("插入后:");
	SLTInsert(&plist, pos, x*10);
	SLTPrint(plist);
	printf("\n");
	printf("前插\n");
	SLTInsertafter(pos, y * 10);
	SLTPrint(plist);
	printf("\n");
}

void Testlist6()
{
	SLTNode* plist = NULL;
	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 4);
	SLTPushFront(&plist, 5);
	SLTPrint(plist);
	printf("\n");
	printf("请输入想要查找的值");
	int x = 0;
	scanf("%d", &x);
	SLTNode* pos = SLTFind(plist, x);
	/*SLTErase(&plist,pos);
	printf("\n");
	SLTPrint(plist);*/
	SLTEraseAfter(pos);
	printf("\n");
	SLTPrint(plist);

}
int main()
{
	/*Testlist1();*/
	/*Testlist2();*/
	/*Testlist3();
	Testlist4();*/
	/*Testlist5();*/
	Testlist6();
	return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/69356.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

11_Pulsar Adaptors适配器、kafka适配器、Spark适配器

2.3. Pulsar Adaptors适配器 2.3.1.kafka适配器 2.3.2.Spark适配器 2.3. Pulsar Adaptors适配器 2.3.1.kafka适配器 Pulsar 为使用 Apache Kafka Java 客户端 API 编写的应用程序提供了一个简单的解决方案。 在生产者中, 如果想不改变原有kafka的代码架构, 就切换到Pulsar的…

sentinel核心流程源码解析

sentinel的处理槽(ProcessorSlot) 可以说&#xff0c;sentinel实现的各种功能就是由各处理槽完成的 ,ProcessorSlot定义了四个方法&#xff1a; 当进入该处理槽时触发该方法 处理完 entry方法之后触发该方法 退出该处理槽时触发该方法 exit方法处理完成时触发该方法 sentinel的…

【MySQL安装】卸载与安装MySQL 5.7.X版本

最近由于各种原因&#xff0c;需要重新安装MySQL。之前我的版本是8.0版本&#xff0c;现在装的5.7版本。记录一下自己的安装过程。 目录 1、卸载MySQL8.0 2、安装MySQL5.7 1、卸载MySQL8.0 如何彻底卸载MySQL_mysql 完全卸载_m0小麦麦的博客-CSDN博客相信不少小伙伴们在安装…

ddia(3)----Chapter3. Storage and Retrieval

However, first we’ll start this chapter by talking about storage engines that are used in the kinds of databases that you’re probably familiar with: traditional relational databases, and also most so-called NoSQL databases. We will examine two families o…

捕捉时刻:将PDF文件中的图像提取为个性化的瑰宝(从pdf提取图像)

应用场景&#xff1a; 该功能的用途是从PDF文件中提取图像。这在以下情况下可能会很有用&#xff1a; 图片提取和转换&#xff1a;可能需要将PDF文件中的图像提取出来&#xff0c;并保存为单独的图像文件&#xff0c;以便在其他应用程序中使用或进行进一步处理。例如&#xff…

pdf怎么压缩到1m?这样做压缩率高!

PDF是目前使用率比较高的一种文档格式&#xff0c;因为它具有很高的安全性&#xff0c;还易于传输等&#xff0c;但有时候当文件体积过大时&#xff0c;会给我们带来不便&#xff0c;这时候简单的解决方法就是将其压缩变小。 想要将PDF文件压缩到1M&#xff0c;也要根据具体的情…

QGIS开发五:VS使用QT插件创建UI界面

前面我们说了在创建项目时创建的是一个空项目&#xff0c;即不使用 Qt 提供的综合开发套件 Qt Creator&#xff0c;也不使用 Qt Visual Studio Tools 这类工具。 但是后面发现&#xff0c;如果我想要有更加满意的界面布局&#xff0c;还是要自己写一个UI文件&#xff0c;如果不…

世微AP2400 电动车 摩托车灯照明 汽车灯照明 手电筒照明LED灯降压恒流驱动IC

PCB 布板参考 1. 大电流路径走线要粗&#xff0c;铺铜走线比较好。 2. 大电路回路面积以最短、最宽路径完成比较好。 3. 开关切换连接点&#xff1a;电感 L、开关管漏级与续流肖特基二极管&#xff0c;走线要短与粗&#xff0c;铺铜走线比较好&#xff0c;但同时需要适当面积作…

MySQL索引3——Explain关键字和索引使用规则(SQL提示、索引失效、最左前缀法则)

目录 Explain关键字 索引性能分析 Id ——select的查询序列号 Select_type——select查询的类型 Table——表名称 Type——select的连接类型 Possible_key ——显示可能应用在这张表的索引 Key——实际用到的索引 Key_len——实际索引使用到的字节数 Ref ——索引命…

机器学习深度学习——注意力提示、注意力池化(核回归)

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位即将上大四&#xff0c;正专攻机器学习的保研er &#x1f30c;上期文章&#xff1a;机器学习&&深度学习——常见循环神经网络结构&#xff08;RNN、LSTM、GRU&#xff09; &#x1f4da;订阅专栏&#xff1a;机器…

SqlServer基础之(触发器)

概念&#xff1a; 触发器&#xff08;trigger&#xff09;是SQL server 提供给程序员和数据分析员来保证数据完整性的一种方法&#xff0c;它是与表事件相关的特殊的存储过程&#xff0c;它的执行不是由程序调用&#xff0c;也不是手工启动&#xff0c;而是由事件来触发&#x…

JVM G1垃圾回收机制介绍

G1(Garbage First)收集器 (标记-整理算法)&#xff1a; Java堆并行收集器&#xff0c;G1收集器是JDK1.7提供的一个新收集器&#xff0c;G1收集器基于“标记-整理”算法实现&#xff0c;也就是说不会产生内存碎片。此外&#xff0c;G1收集器不同于之前的收集器的一个重要特点是&…

钓鱼攻击:相似域名识别及如何有效预防攻击

网络犯罪分子很乐意劫持目标公司或其供应商或业务合作伙伴的官方域名&#xff0c;但在攻击的早期阶段&#xff0c;他们通常没有这种选择。相反&#xff0c;在有针对性的攻击之前&#xff0c;他们会注册一个与受害组织的域名相似的域名 - 他们希望您不会发现其中的差异。此类技术…

SpringBoot 的自动装配特性

1. Spring Boot 的自动装配特性 Spring Boot 的自动装配&#xff08;Auto-Configuration&#xff09;是一种特性&#xff0c;它允许您在应用程序中使用默认配置来自动配置 Spring Framework 的各种功能和组件&#xff0c;从而减少了繁琐的配置工作。通过自动装配&#xff0c;您…

TepeScript 问题记录

问题 对object的所有属性赋值或清空&#xff0c;提示类型错误不能赋值 type VoiceParams {_id?: string | undefined;name: string;sex: string;vc_id: string;model_url: string;preview_url: string;isPrivate: boolean;visible: boolean; }const formData reactive<V…

【Minecraft】Fabric Mod开发完整流程2 - 创造模式物品栏与第一个方块

创造模式物品栏 添加到当前已有物品栏 再添加自定义的创造模式物品栏之前&#xff0c;请确保你的确有这个需求&#xff01;否则建议直接添加到当前已有的物品栏内部 创建新文件&#xff1a;com/example/item/ModItemGroup.java package com.example.item;import net.fabricmc.…

出于网络安全考虑,印度启用本土操作系统”玛雅“取代Windows

据《印度教徒报》报道&#xff0c;印度将放弃微软系统&#xff0c;选择新的操作系统和端点检测与保护系统。 备受期待的 "玛雅操作系统 "将很快用于印度国防部的数字领域&#xff0c;而新的端点检测和保护系统 "Chakravyuh "也将一起面世。 不过&#xf…

2024考研408-计算机网络 第五章-传输层学习笔记

文章目录 前言一、传输层提供的服务1.1、传输层的功能1.2、传输层的两个协议&#xff08;TCP、UDP&#xff09;1.3、传输层的寻址与端口&#xff08;常见端口介绍&#xff09; 二、UDP协议2.1、认识UDP功能和特点2.2、UDP首部格式2.3、UDP伪首部字段分析2.4、伪首部校验UDP用户…

【24择校指南】南京大学计算机考研考情分析

南京大学(A) 考研难度&#xff08;☆☆☆☆☆&#xff09; 内容&#xff1a;23考情概况&#xff08;拟录取和复试分数人数统计&#xff09;、院校概况、23初试科目、23复试详情、参考书目、各科目考情分析、各专业考情分析。 正文2178字&#xff0c;预计阅读&#xff1a;6分…

网络原理(JavaEE初阶系列11)

目录 前言&#xff1a; 1.网络原理的理解 2.应用层 2.1自定义协议的约定 2.1.1确定要传输的信息 2.1.2确定数据的格式 3.传输层 3.1UDP 3.1.1UDP报文格式 3.2TCP 3.2.1确认应答 3.2.2超时重传 3.2.3连接管理 3.2.3.1三次握手 3.2.3.2四次挥手 3.2.4滑动窗口 3.…