线性表之单链表~

这次来介绍一下数据结构里面的单链表。

这是一个简单的链表,它是单向的,没有(哨兵位置)头结点,不循环,有一个头指针指向第一个节点。

和上次介绍线性表一样,我们同样研究它的 增,删,查,改。

目录

1.什么是链表

 2.链表的功能 

2.1 基本功能

1)打印链表

2)创建新结点

3)获取链表长度

 4)查找节点

2.2增加元素

1)头部增加

2)尾部增加

3)中间增加

2.3删除元素

1)头部删除   

2)尾部删除   

 2.4修改元素

2.5销毁链表

测试

1.增加元素的测试

2.删除元素测试

3.修改元素测试


1.什么是链表

首先,链表是线性表的一种,它的存储方式是链式存储。链表是由一个个节点组成,连接每个节点的关键是指针,  每一个节点,我们用结构体实现。

代码如下:

typedef int SLTDataType;//int类型的重定义
//用结构体创建一个节点
typedef struct SListNode
{
	SLTDataType data;//节点的数据域
	SListNode *next;//结点的指针域
}SLT;

SLTDataType是int类型的重定义, next 是一个结构体指针,负责存放下一个节点的地址。

我们看一下物理存储结构和逻辑结构:

如图所示:

我们图中有四个节点,1 、2、3、4,节点的前边存的是一个整型数据,后边存的是一个地址。

 这个地址就是后一个节点的地址,最后一个节点,也就是尾结点存的是NULL。

再看看看逻辑结构,对于指针,指针存放的地址,我们认为指针指向这片内存块,所以我们用箭头来表示节点与节点之间的关系,事实上并没有这个箭头,这只是方便我们理解。

链表的结构了解之后,我们来看实现的功能。


 2.链表的功能

1.基本功能1)打印链表2)创建新节点3)获取链表长度 4)查找节点
2.增加元素:1)头部增加  2)尾部增加  3)中间增加
3.删除元素:  1)头部删除     2)尾部删除   3)中间删除
4.修改元素:找到某个元素,把它修改掉        
5.销毁顺序表


2.1 基本功能

1)打印链表

打印链表就是,遍历整个链表,把每个节点存的数据输出出来即可。

代码如下:

void SListPrint( SLT *&phead)
{
	 SLT *cur = phead;
	if (phead == NULL)
	{
		cout << "链表为空,无法打印" << endl;
		return;
	}
	while (cur)
	{
		cout << cur->data << "->";
		cur = cur->next;
	}
	cout << "NULL" << endl;
}

形参是对头指针的引用,如果不了解这个知识的话,这里就把它当做我们头指针的一个别名,当成我们的头指针就行了,它存的是第一个节点的地址。

我们用cur指针来遍历链表,首先,如果链表为空,即我们的头指针存的是NULL,不打印,直接return。链表不为空,一个一个打印。

关键的代码:

cur = cur->next; 

我们用这种写法,把我们下一个节点的地址在上一个节点打印完成后,赋给我们的cur,接下来打印下一个数据。

最后,当cur打印完最后一个数据的时候,前面提到,最后一个节点存的是NULL,把它赋给cur,它为空,然后判断循环,退出循环,打印完毕。

打印效果如下:

2)创建新结点

新结点的创建,就是动态申请一块内存区域,并给它初始化。

代码如下:

SLT *BuyNewNode(SLTDataType x)//创建新的节点
{
	SLT *newnode = new SLT;//new一个结构体大小的空间
//返回值是结构体指针,用newnode来接收
	newnode->data = x;//给data赋值
	newnode->next = NULL;//把里面的指针域置为空
	return newnode;//返回创造节点的地址
//不了解new的话,用malloc来创建效果是一样的
//SLT *newnode=(SLT *)malloc(sizeof(SLT));
//newnode->data=x;
//return newnode;

如果不了解的new的用法,用注释里面的malloc来实现也是完全OK的。

3)获取链表长度

这个功能是来辅助后边的功能的,比较简单,直接上代码:

SLTDataType SListLength(SLT *&phead)
{
	if (phead == NULL)
	{
		return 0;//链表为空返回0
	}
	else
	{
		SLTDataType count = 0;//count来记录节点个数
		SLT *cur = phead;//cur来遍历链表
		while (cur)
		{
			count++;
			cur = cur->next;
		}
		return count;//返回链表个数
	}
}

 4)查找节点

这个查找结点在后边的功能中是要用到的,所以我们先来介绍一下。

我们的单链表有一个特点,就如它的名字一样,单向的,有些功能不做特殊处理,实现起来很尴尬。就比如我们的在中间某个节点前面插入数据,如果仅仅找找到这个节点,把新节点放在那里,我们发现,很尴尬,不知道前一个节点的地址,没法通过找到的那个节点来确定前边节点,这也是它的一个缺点,所以,干脆点,我们就找待找节点前面一个位置的节点,这个问题就解决了。

所以,代码如下:

SLT * NodeFind(SLT *&phead, SLTDataType i)//查找某个节点,返回前一个位置的地址
{
	SLTDataType len = SListLength(phead);
	if (i<0 || i>len)
	{
		cout << "输入的节点不合法!!" << endl;
		return NULL;
	}
	SLTDataType pos = 1;
	SLT *cur = phead;
	while (pos<i-1)
	{
		pos++;
		cur = cur->next;
	}
	return cur;
}

这里我们先判断输入的节点是否合理,前面说的获取链表长度的函数就有用了。为了找到目标节点前一个位置的节点,我们最关心的就是循环的次数,什么时候算找到。其实当我们限定了节点的输入范围,循环结束后,肯定是能找到它的。

看我们的循环条件,i-1,i是1或2的时候,我们返回的都是首节点的地址,1的时候是头插,比较特特殊,2的时候正常,就是第二个节点前边的节点,也就是首节点地址。

其它情况,比如,我们i为6,i-1为5,从1到5(不包括5),刚好四次,找到了第五个节点,即找到第六个节点前边的节点。

这样,查找功能就实现了。

单链表这样找前一个节点,看上去有点麻烦,后边会介绍双链表,它的查找直接找目标节点就可以了。


2.2增加元素

1)头部增加

在链表的头部增加节点。

思路是:让新的头结点指向旧的头结点,头指针指向新的头结点即可。

代码如下:

void SListPushFront(SLT *&phead, SLTDataType x)//头插
{
	    SLT *newnode = BuyNewNode(x);//创建新节点
		newnode->next = phead;//新的首节点 指向旧的首节点
		phead = newnode;//头节点指向新的首节点
}

2)尾部增加

在链表尾部增加节点。

 思路是:找到尾,连接新节点。

考虑,如果链表为空,这个时候的尾部增加,就是头插。

代码如下:

void SListPushBack(SLT *&phead, SLTDataType x)//尾插
{
	SLT *newnode = BuyNewNode(x);
	if (phead == NULL)//链表为空的尾插就是头插
	{
		phead = newnode;
	}
	else//不为空,单链表的尾插就要找尾节点
	{
		SLT *tail = phead;
		while (tail->next != NULL)
		{
			tail = tail->next;//这种类型的表达是链表最常用写法
		}                     //这种写法让指针在链表中移动起来
	//退出循环,tail就在尾部
		tail->next = newnode;//新节点接在尾部
	}
}

while(tail->next != NULL,看这个循环条件,我们要找到的是尾节点,我们判断tail当前位置的下一个位置是否为空,如果为空,那么它就是尾节点,我们退出循环。

但是while(tail != NULL),如果这样写,会出现很尴尬的情况,就是我们的tail最后会指向空,而不是刚好停留在尾节点上。

3)中间增加

中间增加,就是找到目标节点前面的节点,在它的后边增加即可,这时候,前面写的查找函数就用上了。

代码如下:

void SListInsert(SLT *&phead, SLTDataType x,SLTDataType pos)//任意位置插入(前边)
{
	if (phead == NULL)
	{
		cout << "链表为空,无法插入" << endl;
		return;
	}
	else{
		if (pos == (SListLength(phead) + 1))//特殊情况,尾插
		{
			SListPushBack(phead, x);
		}
		else
		{
			SLT *temp = NodeFind(phead, pos);
			if (temp == phead&&pos==1)
           //特殊情况,pos为1时,返回的是首节点的地址,这时相当于头插
			{
				SListPushFront(phead, x);
			}
			else
			{
				SLT *newnode = BuyNewNode(x);
				SLT *cur = temp->next;
				temp->next = newnode;
				newnode->next = cur;
			}
		}
		}
}

看代码,因为我们的插入是向某个节点的前面插入,所以当链表为空的时候,我们直接退出不让插入。

不为空的时候

第一考虑尾插,因为我们的插入是向某个节点的前面插入,所以尾插很尴尬,我们只能单独判断用户输入的节点,调用尾插函数。

第二考虑头插,就是前面我们提到的,查找函数返回值是首元素地址的时候的一种情况。

看一般情况:

               {
               SLT *newnode = BuyNewNode(x);//创建新节点
				SLT *cur = temp->next;//记录目标节点
				temp->next = newnode;//目标节点前一个节点指向新节点
				newnode->next = cur;//新节点指向目标节点
                }

我们先创建一个新节点,然后我们通过SLT *cur = temp->next; 来保存我们想要找的节点,因为temp存的是目标节点前一个位置的地址,然后我们就把它们连接起来就好了。


2.3删除元素

1)头部删除   

这个比较简单, 因为我们的头指针指向的就是首节点,但要注意的是,删除之前,要把首节点后边的一个节点的地址给头指针,不然直接释放它,就找不到后边的节点了。

代码如下:

void SListPopFront(SLT *&phead)//头删
{
	if (phead == NULL)
	{
		cout << "链表为空,无法删除!!" << endl;
		return;
	}
	else
	{
		SLT *second = phead->next;
		delete phead;//free(phead);
		phead = second;
		//两种写法都可以
		//SLT *first = phead;
		//SLT *second = first->next;
		//phead = second;
		//delete first;free(first)
	}
}

首先,链表为空,不删除。

其次就是正常的释放空间了,上面给了两种写法,其实是一样的,只不过第二种是把phead的值作了传递,最后还是释放首节点,效果是一样的,只是或许看上去会更好理解一点。

delete不太熟悉,就用free,但是new必须和delete搭配,malloc和free必须搭配,切不可混着用。

2)尾部删除   

尾部删除就得找尾,单链表找尾,我们选择的是遍历找尾。

代码如下:

void SListPopBack(SLT *&phead)//尾删
{
	if (phead == NULL)
	{
		cout << "链表为空,无法删除!!" << endl;
	}
	else if (phead->next==NULL)//特殊情况,只有一个节点
	{
		delete phead;
		phead = NULL;
	}
	else//一般情况
	{
		SLT *tail = phead;//找尾
		SLT *cur = NULL;//记录tail前一个节点
		while (tail->next != NULL)
		{
			cur = tail;
			tail = tail->next;
		}
		cur->next = NULL;//cur即新的尾,尾要指向空
		delete tail;//free(tail);
	}
}

首先链表为空,我们不删。

我们先考虑一般情况,tail是用来找尾的,cur是用来找新的尾节点,while (tail->next != NULL)这循环条件,最终tail会停到尾节点上,cur刚好停在尾结点前面一个节点上,即新的尾节点。我们找到尾结点后,释放它,再让新的尾结点指向空,就完成了。

再说特殊情况,假如我们的链表只有一个节点,还按照一般情况处理,就会出问题。

这个时候,tail->next是为空的,循环不会进去,cur也是空,再写cur->next,就不合适了,对NULL进行->操作,也就是解引用操作是不可以的。

所以,我们要单独考虑,即直接释放第一个节点,在让头指针指向空就好了。

3)中间删除

找到它,释放掉,连接它前后的节点。

代码如下:

void SListDelete(SLT *&phead, SLTDataType pos)//任意位置删除
{
	if (phead == NULL)
	{
		cout << "链表为空,无法删除!!" << endl;
		return;
	}
	else
	{
			SLT *temp = NodeFind(phead, pos);//删除位置前一个节点的地址
			if (temp == phead&&pos==1)//特殊情况,头删
			{
				SListPopFront(phead);
				return;
			}
			else
			{
				SLT *cur = temp->next;//要删除的节点
				SLT *next = temp->next->next;//我们还要找到删除节点后一个节点
				temp->next = next;//连接前后节点
				delete cur;
			}
	}
}

同样的,链表为空,我们不删。再来,特殊情况,pos为1那么就是头删,其它情况。我们temp找的是待删节点前面的一个,所以为了找到后边的节点,我们必须写出这样的代码SLT *next = temp->next->next,cur记录要删的节点,链接完成之后,释放cur指向的节点就好了。

不知道大家有没有疑惑,为什么找节点,这么麻烦,直接根据数据元素找不就行了,但你有没有想过,数据元素是可以重复的,那么这样你只能找到第一个相同的元素,找不到后边相同的元素。

而且为啥非得找前一个,直接找它不就好了,这个刚刚我也提到了,这就是单链表的一个缺点,在之后的双链表中,这个问题可以很好的解决。


 2.4修改元素

这个简单,明白我们如何查找之后,修改它就很容易实现了。

代码如下:

void SListModify(SLT *&phead, SLTDataType pos, SLTDataType y)
{
	if (phead == NULL)
	{
		cout << "链表为空,无法修改" << endl;
		return;
	}
	else
	{
		SLT *temp = NodeFind(phead, pos);
		if (temp == phead&&pos == 1)//特殊情况,修改第一个元素
		{
			phead->data = y;
		}
		else
		{
			SLT *cur = temp->next;//记录要修改的节点
			cur->data = y;//修改它的值
		}
	}
}

首先,链表为空,不修改。

其次,特殊情况,修改第一个节点的值单独考虑,再来,其它的情况,就通过temp找到它,用cur来修改就好了。


最后,销毁链表~~

2.5销毁链表

链表的销毁也很简单,但要注意的是,销毁前一个节点之前,要把后一个节点的地址记录一下,然后用循环一次销毁就好了。

代码如下:
 

void SListDestory(SLT *&phead)//销毁链表
{
	if (phead == NULL)
	{
		return;//链表为空,没有需要销毁的节点,直接推出
	}
	else
	{   SLT *cur = phead;//要释放的节点
		SLT *next = NULL;
		while (cur)
		{
			next = cur->next;//记录下一个要释放的节点
			delete cur;//释放当前节点
			cur = next;//后节点传递
		}
		phead=cur = next = NULL;
	}
}

首先,链表为空,就不用销毁了,也不用头指针置为空了。

再来,分析过程,我们知道,next是指向cur后边一个的节点,那么当我们的cur成功释放掉最后一个节点时,next是指向NULL的,那么赋值后,cur为空,那么循环就结束了。

最后,把指针都置为空,这个功能就实现了。

以上就是,我们单链表的常用功能!!!


老规矩,接下来,我们做测试!!!

测试

1.增加元素的测试

代码如下:

void test03()
{
	SLT *phead = NULL;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		SListPushBack(phead, i + 1);
	}
	cout << "初始状态,链表值为:" << endl;
	SListPrint(phead);
	cout << endl;
	//头插区
	SListPushFront(phead, 450);
	cout << "头插450后,链表值为:" << endl;
	SListPrint(phead);
	SListPushFront(phead, 500);
	cout << "头插500后,链表值为:" << endl;
	SListPrint(phead);
	//-------------//
	//尾插区
	SListPushBack(phead, 300);
	cout << "尾插300后,链表值为:" << endl;
	SListPrint(phead);
	SListPushBack(phead, 700);
	cout << "尾插700后,链表值为:" << endl;
	SListPrint(phead);
	//-------------//
	//中间插
	SListInsert(phead, 100, 5);//一般情况
	cout << "第5个位置插入100后,链表值为:" << endl;
	SListPrint(phead);
	SListInsert(phead, 74, 1);//头插
	cout << "用Insert头插后,链表值为:" << endl;
	SListPrint(phead);
	SListInsert(phead, 600, 2);//第二个位置插入
	cout << "第2个位置插入600后,链表值为:" << endl;
	SListPrint(phead);
	SListInsert(phead, 780, SListLength(phead) + 1);//尾插
	cout << "用Insert尾插780后,链表值为:" << endl;
	SListPrint(phead);
	//-------------//
}

运行结果如下:

2.删除元素测试

代码如下:

void test04()
{
	SLT *phead = NULL;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		SListPushBack(phead, i + 1);
	}
	cout << "初始状态,链表值为:" << endl;
	SListPrint(phead);
	cout << endl;
	//头删区
	SListPopFront(phead);
	cout << "头删 1 后,链表的值为" << endl;
	SListPrint(phead);
	cout << endl;
	//尾删区
	SListPopBack(phead);
	cout << "尾删10后,链表的值为:" << endl;
	SListPrint(phead);
	cout << endl;
	//中间删
	SListDelete(phead, 1);
	cout << "用Delete头删 2 后,链表的值为:" << endl;
	SListPrint(phead);
	cout << endl;
	SListDelete(phead, 2);
	cout << "用Delete 删 4 后,链表的值为:" << endl;
	SListPrint(phead);
	cout << endl;
	SListDelete(phead, SListLength(phead));//尾删
	cout << "用Delete尾删 9 后,链表的值为:" << endl;
	SListPrint(phead);
	cout << endl;
//-------------//
}

运行结果如下:

3.修改元素测试

代码如下:

void test05()
{
	SLT *phead = NULL;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		SListPushBack(phead, i + 1);
	}
	cout << "初始状态,链表值为:" << endl;
	SListPrint(phead);
	cout << endl;
	SListModify(phead, 1, 30);
	cout << "用Modify修改把第1个节点的数据修改为30后,链表的值为:" << endl;
	SListPrint(phead);
	cout << endl;
	SListModify(phead, 2, 45);
	cout << "用Modify修改把第2个节点的数据修改为45后,链表的值为:" << endl;
	SListPrint(phead);
	cout << endl;
	SListModify(phead, 6, 78);
	cout << "用Modify修改把第6个节点的数据修改为78后,链表的值为:" << endl;
	SListPrint(phead);
	cout << endl;
	SListModify(phead,SListLength(phead), 100);
	cout << "用Modify修改把尾节点的数据修改为100后,链表的值为:" << endl;
	SListPrint(phead);
	cout << endl;
	SListDestory(phead);
}

 运行结果:

 销毁也在这里测试了。

测试结果很好!!!


综上,就是我们单链表的全部内容啦~~

创作不易,如果帮到你的话,博主在这里求一个三连~~~

有什么问题,欢迎大佬点评~~~

如果需要源代码,可以去博主的Gitee的仓库自取哦~~

链接在这:

琦琦的日常代码: 平时做的程序 (gitee.com)https://gitee.com/qiqi-loves-to-knock-code/qiqis-daily-code


那么,我们下期再见~~~ 

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值