【数据结构】链表-增删查改

1、链表的概念及结构

概念:链表是一种物理结构上非连续、非顺序的存储结构,数据元素的逻辑是通过链表中的指针链接次序实现的。链表通过结构体实现,结构体成员分为数据域与指针域,这部分内容可以查看文章《c语言自定义类型:结构体、枚举、联合》中的结构体的自引用部分。

链表的物理连接如下:
在这里插入图片描述
每个节点(结构体)有两个区域,一是数据域,用来储存数据,如存储的1、2、3、4,二是指针域,用来存储下一个节点的地址。

注意:

  1. 从上图可以看出,链式结构在逻辑上是连续的,但是在物理上不一定连续。
  2. 现实中的节点一般都是从堆上申请出来的。
  3. 从堆上申请的空间(动态内存开辟的空间),是按照一定策略来分配的,两次申请的空间可能连续,也可能不连续。

假设在32位系统上,节点的数据域为 int 类型,那么一个节点的大小一共为8个字节。

2、链表的分类

实际中链表的结构非常多样,第一节中展示的是单向不带头不循环链表,以下情况组合起来有8种链表结构:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
2×2×2=8,共有8种链表结构。
虽然有这么么的链表结构,但是我们实际种最常用的是下面两种结构:
在这里插入图片描述
在这里插入图片描述

  1. 无头单向非循环链表:结构简单,一般不会单独用来存储数据。实际中更多是作为其他数据结构的的子结构,如哈希桶、图的邻接表等等。
  2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现这个结构会带来很多优势,实现反而简单了。

3、无头单向非循环链表的实现

本节主要实现无头单向非循环链表

3.1 链表的创建

typedef int SLTDataType;  //int类型重定义,方便以后更改为其他数据类型

typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;  //该结构体成员为结构体指针,指向的是下一个结构体的位置
}SLTNode;                //结构体struct SListNode重命名为SLTNode
  1. 先定义一个结构体 struct SListNode,里面有数据域,用来存放整型数据,指针域 struct SListNode* next,存放下一个节点(下一个结构体)的位置,所以指针域是结构体指针。并且结构体类型都是 struct SListNode,属于结构体的自引用。
  2. 为了方便以后更改节点中的数据域为其他数据类型,将int 类型用 typedef重命名。
  3. 为了书写方便,将结构体类型 struct SListNode重命名为 SLTNode。

链表创建完成后不需要初始化,链表过于简单,就是由一个个的结构体通过指针域串起来的,在不存储数据的时候 ,可以没有任何节点,即没有结构体,在主函数中将指向结构体的指针置为空就行了。

int main()
{
	SLTNode* plist = NULL;  //plist为头节点的地址,即第一个结构体的地址
	return 0;
}

结构体创建一个结构体指针 plist,当作第一个节点的地址(即第一个结构体的地址),此时链表中没有数据的存储,将第一节点的地址置为空。

3.2 链表的尾插函数

在一个链表的尾部插入数据可以分为两种情况,一种是链表为空,即没有任何数据,头节点指向空指针。第二种是链表存储了数据。
在这里插入图片描述
在这里插入图片描述

尾插函数代码展示

//动态申请一个节点
SLTNode* BuySLTNode(SLTDataType x)
{
	SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode)); //为插入的数据创建新的结构体空间
	node->data = x;
	node->next = NULL;

	return node;
}


//尾插数据
void SListPushBack(SLTNode** pplist, SLTDataType x)
{
	SLTNode* newnode = BuySLTNode(x);//为要插入的数据开辟空间,并将>数据放入这个空间,返回的是这个空间的地址
	
	
	if (*pplist == NULL)//如果这个链表为空,直接把新创建的结构体当作第一个节点
	{
		*pplist = newnode;
	}
	else    //如果不为空,在链表的最后一个节点的后面插入新的节点
	{
		SLTNode* tail = *pplist; // 尾插数据先找到尾
		while (tail->next != NULL)
		{
			tail = tail->next;   //一直往下一个结构体遍历,直到找到存储的是NULL的结构体,这个结构体即是最后一个结构体
		}

		tail->next = newnode;  //将新的节点的位置储存在之前的最后一个节点处
	}
	
}
  1. 插入数据首先要开辟一个新的节点,先自定义一个新节点开辟函数,SLTNode* BuySLTNode(SLTDataType x);
    参数为给这块开辟的空间要插入的数。
    为要插入的数据开辟空间,并将数据放入这个空间(节点),在不确定这个节点是作为尾节点或者是链表中其他位置的节点时,将这个节点的指针域置空,函数返回的是这个空间的地址
  2. void SListPushBack(SLTNode** pplist, SLTDataType x);尾插函数。
    第一个参数为头节点的地址,第二个参数是要插入的值。
    在链表为空的时候,头节点指向空指针,当创建好一个节点放进来时,要将之前的头节点 plist 指向这个这个节点的位置,因此头节点 plist 本身的值要改变,函数参数本身要改变,因此传递的是实参的地址,plist自身就是一个指针,指针的地址要用二级指针来接收---->SLTNode** pplist。
  3. 当链表为空的时候,直接将头节点指向新节点的地址。
  4. 当链表不为空时,先找到链表的尾部(即最后一个节点),找到尾节点后,将尾节点的指针域存储新节点的地址。
  5. SLTNode* tail = *pplist;
    tail = tail->next; ,头节点指向指针域,并将起赋值给自身,通过循环条件tail->next != NULL;,就能找到最后一个节点的位置。

在主函数中调用尾插函数,查看监视

void TestSList1()
{
	SLTNode* plist = NULL;  //plist为头节点的地址,即第一个结构体的地址
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);
}

int main()
{
	TestSList1();
	return 0;
}

监视结果
在这里插入图片描述
调用四次尾插函数,分别存储1、2、3、4,可以看到要存储的值已经存进去了。

3.3 链表的打印函数

void SListPrint(SLTNode* plist)
{
	SLTNode* cur = plist; //plist保持不变,创建一个第三方结构体指针cur代替plist职责
	while (cur != NULL)
	{
		printf("%d ", cur->data);
		cur = cur->next;   //将当前节点的数据打印后,访问到下一个结构体的位置,赋值给自身,即跳到下一个结构体的位置
	}
	printf("\n");
}
  1. 通过头节点指向指针域,将链表中的所有节点都遍历,直到遍访问到cur->next==NULL,即访问到了尾节点。
  2. 每访问一个节点就将节点的中数据域打印一遍,所有节点访问完毕,数据打印完毕

在主函中调用打印函数

void TestSList2()
{
	SLTNode* plist = NULL;  //plist为头节点的地址,即第一个结构体的地址
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);

	SListPrint(plist);
}

int main()
{
	TestSList2();
	return 0;
}

打印结果
在这里插入图片描述

3.4 链表的头插函数

在这里插入图片描述

头插函数代码

void SListPushFront(SLTNode** pplist, SLTDataType x)
{
	SLTNode* newnode = BuySLTNode(x);//为要插入的数据开辟空间,并将数据放入这个空间,返回的是这个空间的地址
	newnode->next = *pplist;
	*pplist = newnode;   //将开辟的空间的地址作为头地址
}
  1. 插入数据首先要开辟一个新的节点,调用新节点开辟函数
  2. 头节点要指向新开辟节点的地址,即头节点 plist 作为参数本身要改变,传址调用,plist本身就是指针,指针的地址,形参用二级指针接收。
  3. 之前的头节点此时成为第二个节点,新开辟节点的指针域要储存第二个头节点的地址。

在主函数中调用头插函数并调用打印函数

void TestSList3()
{
	SLTNode* plist = NULL;  //plist为头节点的地址,即第一个结构体的地址
	SListPushFront(&plist, 10);
	SListPushFront(&plist, 20);
	SListPushFront(&plist, 30);
	SListPushFront(&plist, 40);

	SListPrint(plist);
}

int main()
{
	TestSList3();
	return 0;
}

打印函数
在这里插入图片描述

3.5 链表的尾删函数

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

//尾删数据
void SListPopBack(SLTNode** pplist)
{
	//1、没有节点
	//2、一个节点
	//3、多个节点
	if (*pplist == NULL)
	{
		return;  //链表为空,没得删,直接返回
	}
	else if ((*pplist) ->next == NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		SLTNode* prev = NULL;

		//尾删数据先找到尾
		SLTNode* tail = *pplist;   
		while (tail->next != NULL)
		{
			prev = tail;   //通过不断循环,找最后一个的节点的同时,找到倒数第二个节点
			tail = tail->next;  //找到尾
		}

		free(tail); //将最后一个结构体free掉,这个结构体储存的数据就没了。
		tail = NULL;//及时置空

		prev->next = NULL; //此时倒数第二个节点成为最后一个节点,要将最后一个结构体的指针域置为空。
	}
	
}
  1. 链表为空时,没得删,直接返回。
  2. 链表只有一个节点时,将这个节点删掉后,头节点 plist 要指向空指针,即头节点本身要改变,需要传值调用,头节点 plist 本身是指针,所以形参用二级指针接收。
  3. 链表有多个节点,尾删数据先要找到尾节点,将尾节点直接free掉,尾节点中的内容就清空了,尾节点的内容清空了。
  4. 尾节点内容清空后,但是尾节点的地址依然存在,倒数第二个节点中的指针域依然指向这里,倒数第二个节点此时变为尾节点,根据单向不循环链表的性质,所以要将倒数第二个节点的指针域置空。
  5. 将倒数第二个节点的指针域置空,要先找到倒数第二个节点。
    SLTNode* prev = NULL;
    SLTNode* tail = *pplist;
    while (tail->next != NULL)
    {
    prev = tail;//找到倒数第二个节点
    tail = tail->next; //找到尾
    }

在主函数中调用尾删函数,并且打印

void TestSList4()
{
	SLTNode* plist = NULL;  //plist为头节点的地址,即第一个结构体的地址
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);

	SListPrint(plist);


	SListPopBack(&plist);
	SListPopBack(&plist);

	SListPrint(plist);
}

int main()
{
	TestSList4();
	return 0;
}

打印结果
在这里插入图片描述

  1. 调用两次尾删函数,被删掉了两个数据。

3.6 链表的头删函数

在这里插入图片描述

void SListPopFront(SLTNode** pplist)
{
	if (*pplist == NULL)
	{
		return;
	}
	else
	{
		SLTNode* next = (*pplist)->next;  //先将第二个节点保存起来,免得找不到
		free(*pplist); //将第一个节点中的内容释放掉

		*pplist = next; //这时第二个节点变为第一个节点
	}
}
  1. 链表为空时,没得删,直接返回
  2. 链表不为空,直接将第一个节点free掉,第一个节点的内容直接清空。
  3. 头节点被删除后,此时第二个节点变为头节点,根据单向不循环链表的性质,要将第二个节点的地址给头节点 plist,即头节点本身要改变,需要传值调用,头节点 plist 本身是指针,所以形参用二级指针接收。
  4. 在free头节点的内容之前,要先将第二个节点的地址给记录下来,以便第二个节点赋值给头节点。如果没有实现记录下来,在free的时候,第二个节点作为头节点中的成员(指针域),就清空了,那么再也找不到第二个节点的地址了。

在主函数中调用头删函数并打印

void TestSList5()
{
	SLTNode* plist = NULL;  //plist为头节点的地址,即第一个结构体的地址
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);

	SListPrint(plist);


	SListPopFront(&plist);
	SListPopFront(&plist);
	SListPopFront(&plist);

	SListPrint(plist);
}

int main()
{
	TestSList5();
	return 0;
}

打印结果
在这里插入图片描述

  1. 头删函数调用了三次,链表中储存好的1、2、3、4中的1、2、3都被删掉了。

3.7 链表的查找函数

SLTNode* SListFind(SLTNode* plist, SLTDataType x)
{
	SLTNode* cur = plist;
	while (cur != NULL)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}

	return NULL;  //链表走完了还没找到,返回NULL
}
  1. 给定一个数x,去找这个数在链表中存不存在
  2. 函数的参数是头节点的地址 plist 和要找的数 x。返回值是要找的数 x 的地址。
  3. 跟找节点尾的逻辑是一样的,只不过这里在等于x时就返回其地址了
  4. 当链表从头遍历到节点尾还没找到要找的数x,那么链表中这个数不存在,返回空指针。

在主函数中调用查找函数

void TestSList6()
{
	SLTNode* plist = NULL;  //plist为头节点的地址,即第一个结构体的地址
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);

	SListPrint(plist);

	SLTNode* pos = SListFind(plist, 3);
	if (pos)
	{
		printf("找到了\n");
	}
	else
	{
		printf("找不到\n");
	}

}

int main()
{
	TestSList6();
	return 0;
}

打印结果
在这里插入图片描述

3.8 在给定位置pos之后插入x

在这里插入图片描述

void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySLTNode(x);//为要插入的数据开辟空间,并将数据放入这个空间,返回的是这个空间的地址
	newnode->next = pos->next;
	pos->next = newnode;

}
  1. 在pos节点后插入数据,首先要保证pos节点的位置不为空,也即链表不能为空链表,对形参 pos进行断言。
  2. 函数参数为要插入的位置,要插入的具体数据。
  3. 插入数据首先要开辟一个新的节点,调用新节点开辟函数。
  4. 将 pos 节点原本后面的节点的位置赋值给新开辟节点的指针域,再将新开辟节点的地址赋值给 pos 节点的指针域。

在主函数中调用此函数

void TestSList7()
{
	SLTNode* plist = NULL;  //plist为头节点的地址,即第一个结构体的地址
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);

	SListPrint(plist);

	SLTNode* pos = SListFind(plist, 3);
	SListInsertAfter(pos, 30);  //在3之后插一个30

	SListPrint(plist);
}

int main()
{
	TestSList7();
	return 0;
}

打印结果
在这里插入图片描述

  1. 在3的后面插入了一个30。

3.9 在给定位置pos之前插入x

在这里插入图片描述

在这里插入图片描述

void SListInsertBefore(SLTNode** pplist, SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySLTNode(x);//为要插入的数据开辟空间,并将数据放入这个空间,返回的是这个空间的地址

	if (pos == *pplist)  //这种情况是头插
	{
		newnode->next = pos;  //或者写成 newnode->next = *pplist;
		*pplist = newnode;
	}
	else
	{
		SLTNode* prev = NULL;
		SLTNode* cur = *pplist;
		while (cur != pos)  //找pos节点与pos节点的上一个节点
		{
			prev = cur;
			cur = cur->next;
		}
		prev->next = newnode;
		newnode->next = pos;
	}
	
}
  1. 在pos节点前插入数据,首先要保证pos节点的位置不为空,也即链表不能为空链表,对形参 pos进行断言。
  2. 函数参数为要插入的位置,要插入的具体数据。
  3. 插入数据首先要开辟一个新的节点,调用新节点开辟函数。
  4. 在第一个节点前面插入数据,情况变为头插,头插要改变头节点的地址,即头节点 plist 作为参数本身要改变,传址调用,plist本身就是指针,指针的地址,形参用二级指针接收。
  5. 当不是头插时,要找到 pos 节点前一个节点的地址。因为要将新开辟的节点的的地址赋值给 pos 节点前一个节点的指针域
  6. 此时pos节点变为新开辟节点的后一个节点,所以将新开辟节点的地址赋值给 pos 节点的指针域。

在主函数中调用此函数

void TestSList8()
{
	SLTNode* plist = NULL;  //plist为头节点的地址,即第一个结构体的地址
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);

	SListPrint(plist);

	SLTNode* pos = SListFind(plist, 3);

	SListInsertBefore(&plist, pos, 300); //在3之前插一个300

	SListPrint(plist);
}

int main()
{
	TestSList8();
	return 0;
}

打印结果
在这里插入图片描述

  1. 在3之前插入了一个300。

3.10 删除给定位置pos处的数据

在这里插入图片描述
在这里插入图片描述

void SListErase(SLTNode** pplist, SLTNode* pos)
{
	assert(pplist);
	assert(pos);

	if (*pplist == pos)
	{
		SListPopFront(pplist);  //这种情况是头删,直接调用头删函数
	}
	else
	{
		SLTNode* prev = *pplist;
		while (prev->next != pos) //找pos节点的上一个节点
		{
			prev = prev->next;
			assert(prev); //走完了还没有,说明没有pos这个节点,检查pos不是链表中的节点,即参数传错了
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}
  1. 如果删除的是第一个节点处的数据,那么是头删的情况,直接调用头删函数。
  2. 除了头删,正常删除链表中的一个节点时,要找到被删除节点的上一个节点,以便被删除节点被删除后,上一个节点的地址能指向被删除节点的下一个节点。
  3. 将要删除的节点free掉,这个节点中的内容就被删除了。

在主函数中调用该函数

void TestSList9()
{
	SLTNode* plist = NULL;  //plist为头节点的地址,即第一个结构体的地址
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);

	SListPrint(plist);

	SLTNode* pos = SListFind(plist, 3);

	if (pos)
	{
		SListErase(&plist, pos);//将3删掉
	}
	
	SListPrint(plist);
}

int main()
{
	TestSList9();
	return 0;
}

打印结果
在这里插入图片描述

  1. 新找到 3 的位置,在将3的位置传递给删除函数,从打印结果看,3被删除了。

3.11 删除给定位置pos之后的数据

void SLisEraseAfter(SLTNode* pos)
{
	assert(pos);
	if (pos->next == NULL)
	{
		return;
	}
	else
	{
		SLTNode* next = pos->next;  //先将pos的下一个节点记录下来
		pos->next = next->next;
		free(next);
		next = NULL;//及时置空
	}
}
  1. 先将pos位置后二个节点的地址通过指针域找到并存储在pos位置的指针域中。
  2. 然后free掉 pos 位置后面的节点。
  3. 实现这个函数的时间复杂度是O(1)。因为pos位置是确定的参数,那么他后面两个节点的位置也是确定的了,整个函数没有遍历链表这个操作,所以时间复杂度是O(1)。

在主函数中调用

void TestSList10()
{
	SLTNode* plist = NULL;  //plist为头节点的地址,即第一个结构体的地址
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);

	SListPrint(plist);

	SLTNode* pos = SListFind(plist, 1);

	if (pos)
	{
		SLisEraseAfter(pos);//将2删掉
	}

	SListPrint(plist);
}

int main()
{
	TestSList10();
	return 0;
}

打印结果
在这里插入图片描述

  1. 1后面的2被删掉了。

3.12 删除给定位置pos处的数据—时间复杂度为O(1)的实现方式

在这里插入图片描述

void SListErase1(SLTNode* pos)
{
	assert(pos);

	SLTDataType tmp = pos->data;  //交换pos节点与他下一个节点中的数据域
	pos->data = pos->next->data;
	pos->next->data = tmp;
	SLisEraseAfter(pos);  //再调用删除pos后面节点的函数
	
}
  1. 删除pos节点的数据,可以先采用将pos节点与他的下一个节点中的数据域互相交换,那么删除pos节点的下一个节点,就可以达到本来的目的了。
  2. 这样不用去遍历找pos节点的上一个节点,并且调用的删下一个节点的函数SLisEraseAfter(pos);时间复杂度也是O(1),所以这个函数的时间复杂度是O(1)。

在主函数中调用此函数

void TestSList11()
{
	SLTNode* plist = NULL;  //plist为头节点的地址,即第一个结构体的地址
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);

	SListPrint(plist);

	SLTNode* pos = SListFind(plist, 3);

	if (pos)
	{
		SListErase1(pos);//将3删掉
	}

	SListPrint(plist);
}

int main()
{
	TestSList11();
	return 0;
}

打印结果
在这里插入图片描述

3.13 删除给定位置pos之前的数据

在这里插入图片描述

void SListEraseBefore(SLTNode** pplist, SLTNode* pos)
{
	assert(pplist);
	assert(pos);

	SLTNode* prev = *pplist;
	while (prev->next != pos)
	{
		prev = prev->next;
		assert(prev);
	}
	SLTDataType tmp = prev->data;
	prev->data = pos->data;
	pos->data = tmp;
	SLisEraseAfter(prev);

}
  1. 将pos节点之前的数据,可以采用将pos节点与pos节点前一个节点prev的数据互相交换,再删除prev的后一个节点(即pos节点),就可以达到目的。

在主函数中调用此函数

void TestSList12()
{
	SLTNode* plist = NULL;  //plist为头节点的地址,即第一个结构体的地址
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);

	SListPrint(plist);

	SLTNode* pos = SListFind(plist, 3);

	if (pos)
	{
		SListEraseBefore(&plist, pos);
	}

	SListPrint(plist);
}

int main()
{
	TestSList12();
	return 0;
}

打印结果
在这里插入图片描述

  1. 3前面的2被删掉了。

3.14 在给定位置pos之前插入x—时间复杂度为O(1)的实现方式

在这里插入图片描述

void SListInsertBefore1(SLTNode* pos, SLTDataType x)
{
	SListInsertAfter(pos, x);  //在pos之后插入
	SLTNode* newnode = pos->next;
	SLTDataType tmp = newnode->data;
	newnode->data = pos->data;
	pos->data = tmp;
}
  1. 在pos位置之前插入数据x,可以采用先将在pos位置之后插入x,在将x与pos位置的数据交换,那么可以达到目的。
  2. 这样不用去遍历找pos节点的上一节点,并且调用的(给定位置pos之后插入x)的函数的时间复杂度也是O(1),所以这个函数的时间复杂度是O(1)。

在主函中调用该函数

void TestSList13()
{
	SLTNode* plist = NULL;  //plist为头节点的地址,即第一个结构体的地址
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);

	SListPrint(plist);

	SLTNode* pos = SListFind(plist, 3);
	if (pos)
	{
		SListInsertBefore1(pos, 300);
	}
	
	SListPrint(plist);
}

int main()
{
	TestSList13();
	return 0;
}

打印结果
在这里插入图片描述

  1. 在3的前面插入了300。

3.15 链表的销毁函数

void SListDestory(SLTNode** pplist)
{
	assert(pplist);
	SLTNode* cur = *pplist;
	while (cur != NULL)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pplist = NULL;
}
  1. 从头节点开始,一个一个完后面的节点遍历,遍历一个节点销毁一个节点,再全部销毁完毕后,再将头节点置空,那么整个链表也就空了。

在主函数中调用销毁函数

void TestSList14()
{
	SLTNode* plist = NULL;  //plist为头节点的地址,即第一个结构体的地址
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);

	SListPrint(plist);

	SListDestory(&plist);
	SListPrint(plist);
}

int main()
{
	TestSList14();
	return 0;
}

打印结果
在这里插入图片描述
1 存储好后打印一次,链表中的数据是 1 2 3 4,调用销毁函数后再打印一次,什么也么打印出来。

3.16 无头单向非循环链表的所有代码

SList.h文件:写函数的声明,函数的头文件,其他 .c文件只要包含SList.h文件—>#include “SList.h”,就相当于写了函数的声明与头文件。如下:

#pragma once
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>

//单向+不带头+不循环 链表

typedef int SLTDataType;  //int类型重定义,方便以后更改为其他数据类型

typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;  //该结构体成员为结构体指针,指向的是下一个结构体的位置
}SLTNode;                //结构体struct SListNode重命名为SLTNode



//链表打印函数
void SListPrint(SLTNode* plist);

//尾插数据
void SListPushBack(SLTNode** pplist, SLTDataType x); //形参传的是plist的地址,因为如果链表为空的话,要将开辟的空间作为链表
                                                   //第一个节点的地址,即第一个节点plist本身要改变,自身要改变,就要传自身的地址
                                                   //plist储存的是第一个SLTNode的位置,所以实参&plist传的是一个二级结构体指针。
                                                   //形参用二级结构体指针接收。

//头插数据
void SListPushFront(SLTNode** pplist, SLTDataType x); //头插也要将头节点plist本身改变,所以传plist的地址

//尾删数据
void SListPopBack(SLTNode** pplist);

//头删数据
void SListPopFront(SLTNode** pplist);

//单链表查找
SLTNode* SListFind(SLTNode* plist, SLTDataType x);

//单链表在pos之后插入x
void SListInsertAfter(SLTNode* pos, SLTDataType);

//单链表在pos之前插入x(很麻烦,不合适)
void SListInsertBefore(SLTNode** pplist, SLTNode* pos, SLTDataType x);

//删除pos位置
void SListErase(SLTNode** pplist, SLTNode* pos);

//删除pos之后的x
void SLisEraseAfter(SLTNode* pos);

//删除pos位置的数据的第二种写法,这种时间复杂度是 1,缺点是这种写法要删除的pos位置不能是尾节点
void SListErase1(SLTNode* pos);

//删除pos位置之前的数据
void SListEraseBefore(SLTNode** pplist, SLTNode* pos);

//单链表在pos之前插入x,要求时间复杂度为 1
void SListInsertBefore1(SLTNode* pos, SLTDataType x);

//链表的销毁
void SListDestory(SLTNode** pplist);

SList.c文件:用来实现链表的各种功能接口函数。如下:

#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"

//链表的打印
void SListPrint(SLTNode* plist)
{
	SLTNode* cur = plist; //plist保持不变,创建一个第三方结构体指针cur代替plist职责
	while (cur != NULL)
	{
		printf("%d ", cur->data);
		cur = cur->next;   //将当前节点的数据打印后,访问到下一个结构体的位置,赋值给自身,即跳到下一个结构体的位置
	}
	printf("\n");
}



//动态申请一个节点
SLTNode* BuySLTNode(SLTDataType x)
{
	SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode)); //为插入的数据创建新的结构体空间
	node->data = x;
	node->next = NULL;

	return node;
}



//尾插数据
void SListPushBack(SLTNode** pplist, SLTDataType x)
{
	SLTNode* newnode = BuySLTNode(x);//为要插入的数据开辟空间,并将数据放入这个空间,返回的是这个空间的地址
	
	
	if (*pplist == NULL)//如果这个链表为空,直接把新创建的结构体当作第一个节点
	{
		*pplist = newnode;
	}
	else    //如果不为空,在链表的最后一个节点的后面插入新的节点
	{
		SLTNode* tail = *pplist; // 尾插数据先找到尾
		while (tail->next != NULL)
		{
			tail = tail->next;   //一直往下一个结构体遍历,直到找到存储的是NULL的结构体,这个结构体即是最后一个结构体
		}

		tail->next = newnode;  //将新的节点的位置储存在之前的最后一个节点处
	}
	
}



//头插数据
void SListPushFront(SLTNode** pplist, SLTDataType x)
{
	SLTNode* newnode = BuySLTNode(x);//为要插入的数据开辟空间,并将数据放入这个空间,返回的是这个空间的地址
	newnode->next = *pplist;
	*pplist = newnode;   //将开辟的空间的地址作为头地址
}



//尾删数据
void SListPopBack(SLTNode** pplist)
{
	//1、没有节点
	//2、一个节点
	//3、多个节点
	if (*pplist == NULL)
	{
		return;  //链表为空,没得删,直接返回
	}
	else if ((*pplist) ->next == NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		SLTNode* prev = NULL;

		//尾删数据先找到尾
		SLTNode* tail = *pplist;   
		while (tail->next != NULL)
		{
			prev = tail;   //通过不断循环,找最后一个的节点的同时,找到倒数第二个节点
			tail = tail->next;  //找到尾
		}

		free(tail); //将最后一个结构体free掉,这个结构体储存的数据就没了。
		tail = NULL;//及时置空

		prev->next = NULL; //此时倒数第二个节点成为最后一个节点,要将最后一个结构体的指针域置为空。
	}
	
}



//头删
void SListPopFront(SLTNode** pplist)
{
	if (*pplist == NULL)
	{
		return;
	}
	else
	{
		SLTNode* next = (*pplist)->next;  //先将第二个节点保存起来,免得找不到
		free(*pplist); //将第一个节点中的内容释放掉

		*pplist = next; //这时第二个节点变为第一个节点
	}
}



//单链表查找
SLTNode* SListFind(SLTNode* plist, SLTDataType x)
{
	SLTNode* cur = plist;
	while (cur != NULL)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}

	return NULL;  //链表走完了还没找到,返回NULL
}



//单链表在pos之后插入x
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySLTNode(x);//为要插入的数据开辟空间,并将数据放入这个空间,返回的是这个空间的地址
	newnode->next = pos->next;
	pos->next = newnode;

}



//单链表在pos之前插入x
void SListInsertBefore(SLTNode** pplist, SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySLTNode(x);//为要插入的数据开辟空间,并将数据放入这个空间,返回的是这个空间的地址

	if (pos == *pplist)  //这种情况是头插
	{
		newnode->next = pos;  //或者写成 newnode->next = *pplist;
		*pplist = newnode;
	}
	else
	{
		SLTNode* prev = NULL;
		SLTNode* cur = *pplist;
		while (cur != pos)  //找pos节点与pos节点的上一个节点
		{
			prev = cur;
			cur = cur->next;
		}
		prev->next = newnode;
		newnode->next = pos;
	}
	
}



//删除pos位置的数据
void SListErase(SLTNode** pplist, SLTNode* pos)
{
	assert(pplist);
	assert(pos);

	if (*pplist == pos)
	{
		SListPopFront(pplist);  //这种情况是头删,直接调用头删函数
	}
	else
	{
		SLTNode* prev = *pplist;
		while (prev->next != pos) //找pos节点的上一个节点
		{
			prev = prev->next;
			assert(prev); //走完了还没有,说明没有pos这个节点,检查pos不是链表中的节点,即参数传错了
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}



//删除pos之后的x
void SLisEraseAfter(SLTNode* pos)
{
	assert(pos);
	if (pos->next == NULL)
	{
		return;
	}
	else
	{
		SLTNode* next = pos->next;  //先将pos的下一个节点记录下来
		pos->next = next->next;
		free(next);
		next = NULL;//及时置空
	}
}



//删除pos位置的数据的第二种写法,这种时间复杂度是 1,缺点是这种写法要删除的pos位置不能是尾节点
void SListErase1(SLTNode* pos)
{
	assert(pos);

	SLTDataType tmp = pos->data;  //交换pos节点与他下一个节点中的数据域
	pos->data = pos->next->data;
	pos->next->data = tmp;
	SLisEraseAfter(pos);  //再调用删除pos后面节点的函数
	
}



//删除pos位置之前的数据
void SListEraseBefore(SLTNode** pplist, SLTNode* pos)
{
	assert(pplist);
	assert(pos);

	SLTNode* prev = *pplist;
	while (prev->next != pos)
	{
		prev = prev->next;
		assert(prev);
	}
	SLTDataType tmp = prev->data;
	prev->data = pos->data;
	pos->data = tmp;
	SLisEraseAfter(prev);

}



//单链表在pos之前插入x,要求时间复杂度为 1
void SListInsertBefore1(SLTNode* pos, SLTDataType x)
{
	SListInsertAfter(pos, x);  //在pos之后插入
	SLTNode* newnode = pos->next;//找到在pos后插入的新节点newnode的地址
	SLTDataType tmp = newnode->data;
	newnode->data = pos->data;
	pos->data = tmp;
}



//链表的销毁
void SListDestory(SLTNode** pplist)
{
	assert(pplist);
	SLTNode* cur = *pplist;
	while (cur != NULL)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pplist = NULL;
}

test.c文件:主函数写在这,在主函数中可以调用 SList.c文件中实现的各种接口。如下:

int main()
{
	SLTNode* plist = NULL;  //plist为头节点的地址,即第一个结构体的地址
	return 0;
}

以上就是无头单向非循环链表链表的所有代码,在SList.c文件中实现的函数接口有以下注意:

  1. 在函数中如果需要改变链表中的头节点地址,则传参的时候需要传址调用,结合参数已经是指针了,形参用二级指针接收。
  2. 如果函数对指定位置pos的之前的节点prev进行操作,需要对链表进行遍历找到prev,这样这个函数的时间复杂度至少是O(n)。
  3. 对指定的地址要进行断言,避免传过来的是空指针。
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值