数据结构-单链表

目录

1. 链表

1.1 链表的概念和结构

1.2 链表的分类

2. 单链表的实现:

单链表头插数据:

 单链表尾插数据

单链表头删数据:

单链表尾删数据:

单链表查找并修改数据:

什么时候需要断言?

单链表在pos之前插入数据:

单链表在pos之后插入数据:

单链表在pos位置删除数据:

3.完整代码:

test.c:

SList.h:

SList.c:


在学习链表之前,我们先说说之前学的顺序表,在前面的章节中,我们说,顺序表实际上是对数组的操纵,它的内存空间是连续的,可以通过数组下标随机访问,但是链表就不行,这是顺序表的优点,那它有没有缺点呢?

答案是有的。

顺序表的问题:

1. 中间/头部的插入删除,时间复杂度是O(N)。

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

3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容      到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。

如何解决上述问题呢?下面我们来学习链表。

1. 链表

1.1 链表的概念和结构

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。 

顺序表的问题本质就在于,它是一块连续的物理空间,而链表就不一样了。链表在内存中申请的是一块一块的内存空间,相互之间通过节点联系在一起。

开始时给一个指针指向第一个空间,然后通过节点找到第二个,第二个找到第三个........最后一个节点指向空(NULL)

如下图: 

现在我们首先来定义一个结构体:

typedef int SLDataType;
typedef struct SListNode
{
	SLDataType data;
	struct SListNode* next;
}SLTNode;

结构体中有一个数据data,还有一个结构体指针next。

下面我们简单来遍历打印一个链表:

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

上述代码中,把头节点赋给cur,cur->data是结构体中的数据,代码中的cur=cur->next刚开始可能有点难以理解,实际上cur->next访问的是存放在结构体中的下一个结构体的地址,->相当于对其进行解引用,这样cur就可以找到下一个节点,然后打印下一个结构体的数据,直到最后一个节点的cur->next指向空指针(NULL)。

上图其实就是链表的逻辑结构,这是为了方便形象理解,想象出来的。实际上链表在内存中是下面的物理结构

实际的物理结构,变量phead中存放的是第1个节点的地址,第1个节点的结构体中存放的是第2个节点的地址, 2中存放3节点的地址,3中存放4节点的地址.......直到最后的节点中存放的是空。

这样上述代码中的cur = cur->next就更好理解了,它实际上相当把next中的地址拷贝到cur中去,然后cur就指向了下一个节点。

1.2 链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:

1. 单向或者双向:

2. 带头或者不带头

3. 循环或者非循环

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

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

2. 单链表的实现:

单链表头插数据:

头插数据只需要使新开辟出来的newnode节点下一个指向的是头节点phead,然后把头节点指向在newnode的地方。

下面我们来看看下面代码实现: 

//单链表头插数据
void SLPushFront(SLTNode* phead, SLDataType x)
{
	//动态内存开辟
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;
    //头插
	newnode->next = phead;
	phead = newnode;
}

问题来了,上述代码正确吗?

看起来逻辑十分严密是吧,但是我们测试运行一下就会发现,什么都没有打印出来:

这又是为什么呢?

因为我们在头插的时候改变的是指针,例如:newnode->next = phead; phead = newnode;改变指针实际上就是,把指针的值拷贝过去,当我们传plist,并用phead接收时,实际上就是phead把plist的值拷贝过去,但是后面phead = newnode,又把newnode的值拷贝过去了,原有的值被覆盖了,plist和phead建立不上联系。

那该怎么办呢?

很简单,既然我们要改变指针,那就传指针的地址,用二级指针接收,使用时解引用就行。通过*phead=newnode可以使newnode和plist直接建立联系。

代码修改如下:

//单链表头插数据
void SLPushFront(SLTNode** phead, SLDataType x)
{
	//动态内存开辟
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;

	newnode->next = *phead;
	*phead = newnode;
}

测试:

 单链表尾插数据

尾插数据,因为链表不是连续物理空间,所以我们要先找到链表的尾,然后再指向新开辟的节点newnode,并把newnode->next置为NULL。

同理,尾部插入也需要使用二级指针,如果不使用二级指针,在phead=newnode这条语句中,因为此时的newnode和phead是局部变量,出了作用域就销毁了,所以phead和plist建立不上联系。而如果我们用二级指针,*phead=newnode,相当于把newnode的值直接给plist,尽管phead和newnode后面还会销毁,但是新节点和plist的联系已经建立起来了。

不论是头插还是尾插数据,都要开辟新空间,所以我们可以将其封装为一个函数:

//动态内存开辟
SLTNode*CheckSList(SLDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

 尾插数据代码:

//单链表尾插数据
void SLPushBack(SLTNode** phead, SLDataType x)
{
	SLTNode* newnode = CheckSList(x);
	//1.空链表
	//2.非空链表
	if (*phead == NULL)
	{
		*phead = newnode;
	}
	else
	{
		SLTNode* tail = *phead;
		//找到尾
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		//尾插
		tail->next = newnode;
	}
}

注意:要判断链表是不是空链表。 

测试:

单链表头删数据:

要想头删数据,只需要将头节点指向第二个节点,并将头节点free掉,置为NULL。

注意:分三种情况,无节点一个节点多个节点。实现的方式不同,需要判断。 

//单链表头删数据
void SLPopFront(SLTNode** phead)
{
	assert(*phead);
	//一个节点
	//多个节点
	if ((*phead)->next == NULL)
	{
		free(*phead);
		*phead = NULL;
	}
	else
	{
		SLTNode* start = *phead;
		*phead = start->next;
		free(start);
		start = NULL;
	}

}

测试:

单链表尾删数据:

要想尾删数据,可以先找到倒数第二个节点,把它后面的节点free掉,并置为NULL即可。

注意:分三种情况,无节点一个节点多个节点。实现的方式不同,需要判断。

代码如下:

//单链表尾删数据
void SLP0pBack(SLTNode** phead)
{
	assert(*phead);
	//一个节点
	//多个节点
	if ((*phead)->next == NULL)
	{
		free(*phead);
		*phead = NULL;
	}
	else
	{
		SLTNode* tail = *phead;
		//找到倒数第二个节点
		while (tail->next->next != NULL)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

测试:

单链表查找并修改数据:

查找数据很简单,直接把链表遍历一遍就行。而查找和修改可以同时实现。

直接上代码:

//单链表查找数据
SLTNode* SLFind(SLTNode* phead, SLDataType x)
{
	assert(phead);
	SLTNode* cur = phead;
	int count = 0;
	while (cur != NULL)
	{
		if (cur->data == x)
		{
			return cur;
		}
			cur = cur->next;
	}
	return NULL;
}

上述代码可以同时实现查找和修改,查找到数据后,返回节点指针,通过指针修改结构体数据:

    SLTNode* pos = SLFind(plist,2);
	if (pos != NULL)
		pos->data = 30;

测试:

在上文中,我们发现有些参数传的是一级指针,有些传的是二级指针,为什么呢? 

其实给打印和查找函数传的就是一级指针,而其他的头插、尾插、头删、尾删都是二级指针,原因很明显,打印和查找函数不需要改变指针,而其他的需要改变指针,要改变指针就要使用二级指针。

下面我们再来补充一个内容,

什么时候需要断言?

比如:查找函数需要断言assert(phead)吗?

不需要,因为即使phead传过来的是空链表,查找函数也可以找,找不到就返回NULL。同理,打印函数也不需要断言,它可以打印空链表NULL。

头插函数中需要断言assert(phead)

需要,因为phead是头指针plist的地址,永远都不能为空,一旦为空,再对其解引用就成空指针NULL了。

头插函数中需要断言assert(*phead)

不需要,因为它即使为空,空链表也可以插入数据啊。

头插函数断言如下:

//单链表头插数据
void SLPushFront(SLTNode** phead, SLDataType x)
{
	assert(phead);//链表为空,phead也不为空,因为它是头指针plist的地址
	//assert(*phead);不需要断言,链表为空,也需要能插入
	SLTNode*newnode=CheckSList(x);
	newnode->next = *phead;
	*phead = newnode;
}

头删函数中需要断言assert(phead)assert(*phead)吗?

都需要,头删函数中,一旦链表为空就不能再删了,所以需要断言assert(*phead),而断言assert(phead)的原因和上文一致。

头删函数断言如下:

//单链表头删数据
void SLPopFront(SLTNode** phead)
{
	assert(phead);//链表为空,phead也不为空,因为它是头指针plist的地址
	assert(*phead);//链表为空,不能再删。
	//一个节点
	//多个节点
	if ((*phead)->next == NULL)
	{
		free(*phead);
		*phead = NULL;
	}
	else
	{
		SLTNode* start = *phead;
		*phead = start->next;
		free(start);
		start = NULL;
	}

}

 同理,尾删和头删保持一致,尾插和头插保持一致。

下面我们接着讲链表任意位置的插入和删除

单链表在pos之前插入数据:

要想在pos之前插入数据,就要先找到pos前一个节点,然后把使该节点下一个指向newnode,让newnode的下个节点指向pos。

代码如下:

//单链表在pos之前插入数据
void SLInsert(SLTNode** phead, SLTNode* pos, SLDataType x)
{
	assert(phead);
	assert(pos);
	if (*phead == pos)
	{
		SLPushFront(phead, x);
	}
	else
	{
		SLTNode* newnode = CheckSList(x);
		SLTNode* cur = *phead;
		while (cur->next != pos)
		{
			cur = cur->next;
		}
		cur->next = newnode;
		newnode->next = pos;
	}
}

注意:上述代码中复用了头插函数,因为当pos在头节点的位置时,while循环就找不到pos的前一个节点了,所以这种情况需要单独判断,当pos在头节点位置时,直接使用头插。

测试:先用查找函数找到pos,然后在pos前面插入数据:

单链表在pos之后插入数据:

在pos之后插入数据很简单,直接插入就行。

上代码:

//单链表在pos后插入数据
void SLErase(SLTNode** phead, SLTNode* pos, SLDataType x)
{
	assert(phead);
	SLTNode* newnode = CheckSList(x);
	SLTNode* next = pos->next;
	pos->next = newnode;
	newnode->next = next;
}

测试:也是先用查找函数找到pos,然后在pos后面插入数据:

单链表在pos位置删除数据:

经过上文的练习,这个应该很简单吧。直接找到pos前一个节点,让它指向pos下一个节点,然后将pos节点free就行。 

 上代码:

//单链表在pos位置删除数据
void SLPErase(SLTNode** phead, SLTNode* pos)
{
	assert(phead);
	assert(*phead);
	if (*phead == pos)
	{
		SLPopFront(phead);
	}
	else
	{
		SLTNode* prev =*phead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
	}
}

测试:

至于在pos之前和之后删除数据,和前面的思路一样,不再赘述。

3.完整代码:

test.c:

仅供测试用例。

#define  _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
//测试函数
void test1()
{
	SLTNode* plist = NULL;
	SLPushFront(&plist, 1);
	SLPushFront(&plist, 2);
	SLPushFront(&plist, 3);
	SLPushFront(&plist, 4);
	SLTNode* pos = SLFind(plist, 2);
	if (pos != NULL)
	SLPErase(&plist, pos);

	/*SLPushBack(&plist, 5);
	SLPushBack(&plist, 6);
	SLPushBack(&plist, 7);*/
	/*SLTNode* pos = SLFind(plist,2);
	if (pos != NULL)
		pos->data = 30;*/

	/*SLPopFront(&plist);
	SLPopFront(&plist);
	SLPopFront(&plist);*/

	SLPrint(plist);

}

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

SList.h:

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//定义结构体
typedef int SLDataType;
typedef struct SListNode
{
	SLDataType data;
	struct SListNode* next;
}SLTNode;
//打印函数
void SLPrint(SLTNode*phead);
//单链表头插数据
void SLPushFront(SLTNode** phead, SLDataType x);
//单链表尾插数据
void SLPushBack(SLTNode** phead, SLDataType x);
//单链表头删数据
void SLPopFront(SLTNode** phead);
//单链表尾删数据
void SLP0pBack(SLTNode** phead);
//单链表查找数据
SLTNode* SLFind(SLTNode* phead, SLDataType x);
//单链表在pos之前插入数据
void SLInsert(SLTNode** phead, SLTNode* pos, SLDataType x);
//单链表在pos之后插入数据
void SLErase(SLTNode** phead, SLTNode* pos, SLDataType x);
//单链表在pos位置删除数据
void SLPErase(SLTNode** phead, SLTNode* pos);

SList.c:

#define  _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
void SLPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}
//动态内存开辟
SLTNode*CheckSList(SLDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc");
		return NULL;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}
//链表头插数据
void SLPushFront(SLTNode** phead, SLDataType x)
{
	assert(phead);//链表为空,phead也不为空,因为它是头指针plist的地址
	//assert(*phead);不需要断言,链表为空,也需要能插入
	SLTNode*newnode=CheckSList(x);
	newnode->next = *phead;
	*phead = newnode;
}
//链表尾插数据
void SLPushBack(SLTNode** phead, SLDataType x)
{
	assert(phead);//链表为空,phead也不为空,因为它是头指针plist的地址
	//assert(*phead);不需要断言,链表为空,也需要能插入
	SLTNode* newnode = CheckSList(x);
	//1.空链表
	//2.非空链表
	if (*phead == NULL)
	{
		*phead = newnode;
	}
	else
	{
		SLTNode* tail = *phead;
		//找到尾
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		//尾插
		tail->next = newnode;
	}
}
//链表头删数据
void SLPopFront(SLTNode** phead)
{
	assert(phead);//链表为空,phead也不为空,因为它是头指针plist的地址
	assert(*phead);//链表为空,不能再删。
	//一个节点
	//多个节点
	if ((*phead)->next == NULL)
	{
		free(*phead);
		*phead = NULL;
	}
	else
	{
		SLTNode* start = *phead;
		*phead = start->next;
		free(start);
		start = NULL;
	}

}

//链表尾删数据
void SLP0pBack(SLTNode** phead)
{
	assert(phead);//链表为空,phead也不为空,因为它是头指针plist的地址
	assert(*phead);//链表为空,不能再删。
	//一个节点
	//多个节点
	if ((*phead)->next == NULL)
	{
		free(*phead);
		*phead = NULL;
	}
	else
	{
		SLTNode* tail = *phead;
		//找到倒数第二个节点
		while (tail->next->next != NULL)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}
//链表查找数据
SLTNode* SLFind(SLTNode* phead, SLDataType x)
{
	assert(phead);
	SLTNode* cur = phead;
	int count = 0;
	while (cur != NULL)
	{
		if (cur->data == x)
		{
			return cur;
		}
			cur = cur->next;
	}
	return NULL;
}
//单链表在pos之前插入数据
void SLInsert(SLTNode** phead, SLTNode* pos, SLDataType x)
{
	assert(phead);
	assert(pos);
	if (*phead == pos)
	{
		SLPushFront(phead, x);
	}
	else
	{
		SLTNode* newnode = CheckSList(x);
		SLTNode* cur = *phead;
		while (cur->next != pos)
		{
			cur = cur->next;
		}
		cur->next = newnode;
		newnode->next = pos;
	}
}
//单链表在pos后插入数据
void SLErase(SLTNode** phead, SLTNode* pos, SLDataType x)
{
	assert(phead);
	SLTNode* newnode = CheckSList(x);
	SLTNode* next = pos->next;
	pos->next = newnode;
	newnode->next=next;
}
//单链表在pos位置删除数据
void SLPErase(SLTNode** phead, SLTNode* pos)
{
	assert(phead);
	assert(*phead);
	if (*phead == pos)
	{
		SLPopFront(phead);
	}
	else
	{
		SLTNode* prev =*phead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
	}
}

 以上就是今天学习的关于单链表的所有内容。

未完待续。。。 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

成屿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值