数据结构—单链表

发布于:2024-04-27 ⋅ 阅读:(21) ⋅ 点赞:(0)

含义

1.顺序表的回顾

之前的文章已经谈到了顺序表,关于顺序表,有一下的几种特点

1.中间,头部的删除,复杂度为O(N);

2.增容会有申请新的空间,拷贝数据,释放旧的空间,会有不小的消耗;

3:整容一般是呈2的倍数增长,但是也会造成一定的空间浪费,比如,当前的容量是100,满了以后·增加到200,我们继续插入5个数据,后面没有数据插入,就浪费了95个空间

2.链表的含义

那么,是否存在一种数据结构,能够解决上面的问题:

1.中间,头部数据的删除,不需要挪动数据

2.不需要扩容

3.不会造成空间的浪费

没错,就是今天的链表

链表的组成:

链表是由当前节点要存储的数据+下一个节点的地址组成的,就比如下面的这个

你可以把链表看成一节一节的火车车厢,想要加车厢的时候随时加,前面一个车厢掌管后面一个车厢的钥匙

放下一个节点的地址是为了方便找到下一个节点(这里有一点废话了哈),话不多说,上代码

typedef int SLdatetype;//链表的创建
struct Slist
{
	SLdatetype a;
	struct Slist* arr;
};

有的时候你觉得结构体的类型太长了,懒得写,以后在使用的时候也不太发表,可以换成下面的

typedef int SLdatetype;
typdef struct Slist
{
	SLdatetype a;
	struct Slist* arr;
}SL;
//下面的写法是不行的
typedef int SLdatetype;
typdef struct Slist
{
	SLdatetype a;
	SL* arr;//这种写法是不行的,因为编译器在执行程序的时候,是自上而下来扫描的,SL这种类型还没有
            //创建完毕
}SL;


链表的打印

#include"Slist.h"
void SLprint(SL* phead)
{
	SL* ptemp = phead;//建立临时的变量,防止下次要找头节点的时候找不到了
	while (ptemp!= NULL)
	{
		printf("%d->", ptemp->date);
		ptemp = ptemp->next;
	}
	printf("NULL");
}

测试代码

#include"Slist.h"

void test01()
{
    //在写的时候不建议这样写,这里只是为了测试
	//申请空间
	SL* node1 = (SL*)malloc(sizeof(SL));
	node1->date =1;
	SL* node2 = (SL*)malloc(sizeof(SL));
	node2->date =2;
	SL* node3 = (SL*)malloc(sizeof(SL));
	node3->date =3;
	//连接
	node1->next = node2;
	node2->next = node3;
	node3->next = NULL;
	SLprint(node1);
}


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

输出结果

链表的尾插

SL* SLbuynode(SLdatetype x)
{
	SL* newnode = (SL*)malloc(sizeof(SL));
	newnode->date = x;
	newnode->next = NULL;
	return newnode;
}
void SLpushback(SL** pphead, SLdatetype x)
{
	assert(pphead);//判断有效
	SL* pnew = SLbuynode(x);
	if (*pphead == NULL)
	{
		*pphead = pnew;
		return;
	}
	SL* Ptail = *pphead;
	while (Ptail->next)
	{
		Ptail = Ptail->next;
	}
	Ptail->next = pnew;
}

测试代码

void test01()
{
	SL* pptest = NULL;
	SLpushback(&pptest,1);
	SLprint(pptest);
}

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

代码说明

对上面的代码进行的几点说明

SLpushback(&pptest,1);

1.因为是对pphead进行的复制,所以上面函数进行的是传址调用,如果是传值调用的话,由于形参是实参的一份临时拷贝,所以在进行复制的时候,等到函数结束的时候形参销毁,该函数又没有返回值,所以对形参的改变并不会引起实参的改变

assert(pphead);//判断有效

2.断言部分,当你传入的地址是空指针的时候,编译其会报错,所以加上断言,但是我觉得具体原因是:当你传入空指针的时候,对该地址进行解引用的时候是找不到你要插入的位置的,就像别人给了你一张空头支票,你去银行去取钱,结果柜员一看,根本就找不到支票里面的钱在哪个地方一样,这是你的表情一定会和下面的一样

3.对于*pphead是否需要断言,答案是否定的,那里就是需要插入数据,因此是不需要断言的

链表的头插

void SLpushfront(SL** pphead, SLdatetype x)
{
	assert(pphead);
	SL*pnew= SLbuynode(x);
	pnew->next = *pphead;
	*pphead=pnew;
}

链表的尾删

void SLpopback(SL** pphead)
{
	assert(pphead);
	assert(*pphead);
    //链表只有一个节点的情况
	if ((*pphead)->next == NULL)
	{
		*pphead = NULL;
		free(*pphead);
		return;
	}
	SL* tail = *pphead;
	SL* pre = NULL;//尾节点的前一个节点,在尾节点删除的时候,要将前面的节点的下一个节点置为空
	while (tail->next)
	{
		pre = tail;//找到前驱节点
		tail = tail->next;
	}
	pre->next = NULL;
	free(tail);
	tail = NULL;


}

链表的头删

关于链表的头删,只需要把第一个节点释放掉,然后然第二个节点变成第一个节点就行了

void SLpopfront(SL** pphead)
{
	SL* pcur = *pphead;
	*pphead= pcur->next;
	free(pcur);
	pcur = NULL;
}

 记住,在删除链表的头节点的时候,要先把头节点赋值给第二个节点,在对头节点进行释放,比如,写成下面的代码就不行

void SLpopfront(SL**pphead)
{
    freep(*pphead);
    *pphead=(*pphead)->next;
}
//因为已经释放了头节点,头节点已经变成了空指针
//空指针的下一个节点是不知道的

链表的查找

链表的查找和数组元素的查找是一样的,都是通过遍历的方式查找,找到了就返回当前的地址,没有找到就返回空指针

SL* SLfind(SL** pphead, SLdatetype x)
{
	assert(pphead);
	SL* pfind = *pphead;
	while (pfind)
	{
		if (pfind->date == x)
		{
			return pfind;//找到了,返回当前的指针
		}
		pfind = pfind->next;
	}
	return NULL;//没有找到,返回空指针
}

在指定位置之前插入数据

思路:情况一

            该链表只有一个节点,直接调用头插函数

            情况2:

            该链表有多个链表,通过遍历的方式找到前驱节点,改变前驱节点的指向

void SLpushposfront(SL** pphead, SL* pos, SLdatetype x)
{
	assert(pphead);
	assert(*pphead);
	SL* prev = *pphead;//定义前驱节点
	if ((*pphead)->next == NULL)//该链表只有一个节点
	{
		SLpushfront(pphead, x);
         return;
	}
	SL* pnew = SLbuynode( x);
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	//该节点为前驱节点
	
	prev->next = pnew;
	pnew->next = pos;
}

在指定位置之后插入节点

void SLpushposback(SL* pos, SLdatetype x)
{
	assert(pos);
	SL* posback = pos->next;//记录pos后面位置的节点
	SL* newnode = SLbuynode(x);
	pos->next = newnode;
	newnode->next = posback;
}

对上面代码的说明

为啥要在插入节点之前的步骤中先记录pos后面的节点嘞?

原因上面的最后一句代码

newnode->next = posback;

如果不记录的话,posback就变成了要插入的代码了,比如你写成这样

pos->next = newnode;
newnode->next = pos->next;//pos->next是newnode

删除pos节点

思路:情况一:多个节点找到先驱节点,改变先驱的指向,让它指向pos节点的下一个节点

              情况二:只有一个节点,执行头删或尾部删除

void SLerasepos(SL**pphead, SL* pos)
{
	assert(pphead);
	assert(*pphead);
	SL* prev = *pphead;//定义前驱节点
	if ((*pphead)->next == NULL)//只有一个节点,删除尾节点
	{
		SLpopfront(pphead);
        return;
	}
	while (prev->next != NULL)
	{
		prev = prev->next;
	}//退出循环,前驱节点被找到
	prev->next = pos->next;
	free(pos);           
	pos = NULL;
}

注意:上面最后三行代码的顺序不能交换,不能写成下面的代码

free(pos);           
pos = NULL;
prev->next = pos->next;

原因是pos节点已经被释放,后面哪来的pos->next

删除pos之后的节点

void SLTEraseAfter(SLTNode* pos) {
	assert(pos);
	//pos->next不能为空
	assert(pos->next);

	//pos  pos->next  pos->next->next
	SLTNode* del = pos->next;
	pos->next = pos->next->next;
	free(del);
	del = NULL;
}

摧毁链表

void SListDesTroy(SLTNode** pphead) {
	assert(pphead);
	assert(*pphead);

	SLTNode* pcur = *pphead;
	while (pcur)
	{
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
}


网站公告

今日签到

点亮在社区的每一天
去签到