【编程与算法基础】数据结构——线性表之单链表

单链表

观看这里的uu建议先看顺序表
线性表之顺序表点击这里

一、前言

线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列等。
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
链表是典型的线性表之一,以链式结构的形式存储。
链表又被分为单链表和双链表。
本篇仅对单链表详细说明。

二、单链表

1·单链表的概念及其结构

概念:单链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针单向链接次序实现的。
逻辑结构图如下:
在这里插入图片描述
根据单链表的逻辑结构可以想到单链表这种结构体中包含存入的数据和指向下一个结构体的结构体指针。

物理结构图如下:
在这里插入图片描述
单链表需要增加空间存储新数据时只需要动态申请一个结构体大小的空间即可。对空间的使用率非常高。完美解决了顺序表频繁扩容浪费空间的缺点。

2·单链表的分类

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

  • 不带头、带头非循环单链表
    在这里插入图片描述

  • 单链表、循环单链表
    在这里插入图片描述

  • 无头单向非循环链表
    在这里插入图片描述
    虽然有这么多的单链表结构,但是在实际中最常用的是无头单向非循环链表
    无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。

单链表是否带头的区别:当不带头要改变头指针的指向时,那么就需要传二级指针形参来改变头指针的指向;当带头时只需要传一级指针,操作头节点来处理后续数据的操作即可。
单链表是否循环的区别:当非循环时头节点与尾节点没有逻辑上的联系,某些操作会不方便;当循环时头节点与尾节点有逻辑上的联系,某些操作会方便许多。

说明:
单链表无论带头或不带头、循环或非循环的实现都大同小异,这里只详细说明不带头+单向+非循环链表的实现。
是否带头的唯一区别:

3·单链表接口函数

单链表头文件Slist.h的声明如下:

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

// 无头+单向+非循环链表增删查改实现
typedef int SLTDateType;

//无头+单向+非循环链表结构体
typedef struct SListNode
{
	//存储的数据
	SLTDateType data;
	//指向下一个结构体数据的结构体指针
	struct SListNode* next;
}SLNode;

// 动态申请一个节点
SLNode* BuySListNode(SLTDateType x);

// 单链表打印
void SListPrint(SLNode* phead);

// 单链表尾插
void SListPushBack(SLNode** pphead, SLTDateType x);

// 单链表的头插
void SListPushFront(SLNode** pphead, SLTDateType x);

// 单链表的尾删
void SListPopBack(SLNode** pphead);

// 单链表头删
void SListPopFront(SLNode** pphead);

// 单链表查找
SLNode* SListFind(SLNode* phead, SLTDateType x);

// 单链表在pos位置之后插入x
void SListInsertAfter(SLNode* pos, SLTDateType x);

// 单链表在pos位置之后删除x
void SListEraseAfter(SLNode* pos);

// 单链表在pos位置插入x
void SListInsert(SLNode** pphead, SLNode* pos, SLTDateType x);

// 单链表删除pos位置的值
void SListErase(SLNode** pphead, SLNode* pos);

//释放链表
void SListDestroy(SLNode** pphead);

三、单链表的实现

说明:以下函数的实现思路在代码注释中已详细解释。

1·申请新节点、打印链表和释放链表

申请新节点函数在Slist.c中如下:

// 动态申请一个节点,返回一个节点首地址
SLNode* BuySListNode(SLTDateType x)
{
	//开辟一个sizeof(SLNode)的新节点
	SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
	//开辟失败则报错且终止程序
	if (newnode == NULL)
	{
		printf("malloc fail");
		exit(-1);
	}
	//开辟成功则进行赋值
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

打印链表函数在Slist.c中如下:

// 单链表打印
/*只需要输出当前指针解引用data后的值,一级指针传参*/
void SListPrint(SLNode* plist)
{
	SLNode* cur = plist;
	/*当当前指针cur不为空时说明还未到尾部,
	此时打印当前数据并将当前的指针cur移动至下一个结构体以此循环*/
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		//cur->next与plist的区别就是plist是首字节的指针
		cur=cur->next;
	}
	printf("NULL");
}

说明:申请节点只要插入节点都会用到,因此申请节点在尾部插入节点、头部插入节点和任意位置插入节点据函数中嵌套使用。

释放链表函数在Slist.c中如下:

//释放链表
void SListDestroy(SLNode** pphead)
{
	SLNode* cur = *pphead;
	while (cur != NULL)
	{
		//保存当前要释放的下一节点的位置
		SLNode* next = cur->next;
		//释放当前节点
		free(cur);
		//转移当前节点
		cur = next;
	}
	*pphead = NULL;
}

以上函数的具体实现情况尾部、头部和任意位置操作时大同小异且比较简单,不做具体说明。

2·尾部插入节点&尾部删除节点

尾部插入节点函数在Slist.c中如下:

// 单链表尾插
/*先申请一个新节点后分两种情况*/
/*1·链表中有数据,此时不需要二级指针的参与,定义一个结构体指针tail,用遍历移动的方式指向尾节点,
用tail指针修改尾节点next的值为新节点的地址,即可将尾节点与新节点链接/

/*2·链表中无数据,此时需要用二级指针传参是因为当链表中无数据时,
单纯的一级指针传参无法改变头指针plist指针的指向,
因此需要用二级指针解引用来修改头指针的指向为新节点的地址*/

//实现单链表尾部插入不是只有这一种方式的函数,这里采取的只是最典型的一种方式
void SListPushBack(SLNode** pphead, SLTDateType x)
{
	//申请一个新节点
	SLNode* newnode = BuySListNode(x);
	//如果传参的节点为空,说明链表中没有数据,那么需要解引用二级指针修改头指针的指向
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	//如果传参的节点不为空,说明链表中有数据,那么就定义一个结构体指针tail
	else
	{
		//1·用遍历移动的方式指向尾节点
		SLNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		//2·用tail指针修改尾节点next的值为新节点的地址,即可将尾节点与新节点链接
		tail->next = newnode;
	}
}

时间复杂度为O(N),效率一般。

尾部删除节点函数在Slist.c中如下:

// 单链表的尾删
/*尾删会遇到三种情况*/
/*1·空链表时尾删,就终止程序并报错*/
/*2·链表中只有1个数据,tail->next->next无法访问,需要特殊处理*/
/*3·链表中有1个以上数据,这是普遍情况*/
void SListPopBack(SLNode** pphead)
{	
	//空链表,就终止程序并报错
	assert(*pphead != NULL);

	//链表中只有1个数据
	if ((*pphead)->next == NULL)
	{
		//1·释放掉头指针指向的空间
		free(*pphead);
		//2·将头指针指向空
		*pphead = NULL;
	}
	//链表中有1个以上数据
	else
	{
		SLNode* tail = *pphead;
		//使用tail->next->next遍历链表,这样既可以释放尾部空间,
		//也可以将尾节点的上一节点的next指向空指针
		while (tail->next->next != NULL)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

时间复杂度为O(N),效率一般。

具体使用方式如下:

#include <iostream>
#include  <stdio.h>
#include <stdlib.h>
#include "Slist.h"

//尾插尾删
void TestSList1()
{
	//由于链表是指针管理,因此初始化创建一个结构体指针管理即可
	SListNode* plist = NULL;
	//初始化
	//尾部插入节点
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);
	//打印尾插节点
	SListPrint(plist);
	printf("\n");

	//尾部删除节点
	SListPopBack(&plist);
	SListPopBack(&plist);
	//打印尾删后的节点
	SListPrint(plist);
	printf("\n");
	//继续尾部删除节点
	SListPopBack(&plist);
	SListPopBack(&plist);
	SListPopBack(&plist);
	//打印尾删后的节点
	SListPrint(plist);
	printf("\n");
}

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

执行调试结果如下:
1·向尾部添加4个节点,并对相应变量进行监测:
在这里插入图片描述
2·以上面情况为基础,从尾部删除2个节点,并对相应变量进行监测:
在这里插入图片描述
3·以上面情况为基础,再从尾部删除3个节点,并对相应变量进行监测:
在这里插入图片描述

3·头部插入节点&头部删除节点

头部插入节点函数在Slist.c中如下:

// 单链表的头插
void SListPushFront(SLNode** pphead, SLTDateType x)
{
	//申请一个新节点
	SLNode* newnode = BuySListNode(x);
	//无论传参节点是否为空都是同样的操作
	//1·将新节点设为首节点
	newnode->next = *pphead;
	//2·将头指针指向新节点
	*pphead = newnode;
}

时间复杂度为O(1),基本没有效率损失。

头部删除节点函数在Slist.c中如下:

// 单链表头删
/*头删会遇到两种情况*/
/*1·空链表时头删,就终止程序并报错*/
/*2·链表中有1个以上数据是普遍情况*/
void SListPopFront(SLNode** pphead)
{
	//空链表时头删,就终止程序并报错
	assert(*pphead != NULL);

	//链表中有1个以上数据
	SLNode* fro = *pphead;
	//1·头指针指向下一个字节
	*pphead = (*pphead)->next;
	//2·释放掉头指针指向的空间
	free(fro);
	//3·将指针指向空,以免出现野指针
	fro = NULL;
}

时间复杂度为O(1),基本没有效率损失。

具体使用方式如下:

#include <iostream>
#include  <stdio.h>
#include <stdlib.h>
#include "Slist.h"

void TestSList2()
{
	//由于链表是指针管理,因此初始化创建一个结构体指针管理即可
	SListNode* plist = NULL;
	//尾部插入节点
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	//打印尾插节点
	SListPrint(plist);
	printf("\n");
	//头部插入节点
	SListPushFront(&plist, 1);
	SListPushFront(&plist, 2);
	//打印头插节点
	SListPrint(plist);
	printf("\n");

	//头部删除节点
	SListPopFront(&plist);
	SListPopFront(&plist);
	SListPrint(plist);
	printf("\n");
	//继续删除头部节点
	SListPopFront(&plist);
	SListPopFront(&plist);
	//会报错
	SListPopFront(&plist);
}

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

执行调试结果如下:
1·在尾部添加2个节点情况下,向头部添加2个节点,并对相应变量进行监测:
在这里插入图片描述
2·以上面情况为基础,从头部删除2个节点,并对相应变量进行监测:
在这里插入图片描述
3·以上面情况为基础,再次从头部删除3个节点,并对相应变量进行监测:
在这里插入图片描述

4·查找节点

查找节点函数在Slist.c中如下:

// 单链表查找
SLNode* SListFind(SLNode* phead, SLTDateType x)
{
	SLNode* cur = phead;
	//遍历链表
	while (cur != NULL)
	{
		//如果找到了,返回该节点
		if (cur->data == x)
		{
			return cur;
		}
		//如果遍历中未找到,向后移动当前节点
		else
		{
			cur = cur->next;
		}
	}
	//遍历结束未找到,就返回空指针
	return NULL;
}

时间复杂度为O(N),效率一般。

具体使用方式如下:

#include <iostream>
#include  <stdio.h>
#include <stdlib.h>
#include "Slist.h"

//查找
void TestSList4()
{
	//由于链表是指针管理,因此初始化创建一个结构体指针管理即可
	SListNode* plist = NULL;
	//尾部插入节点
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	//打印尾插节点
	SListPrint(plist);
	printf("\n");
	//头部插入节点
	SListPushFront(&plist, 3);
	SListPushFront(&plist, 4);
	//打印头插节点
	SListPrint(plist);
	printf("\n");

	//查找
	SLNode* pos1 = SListFind(plist, 1);
	printf("pos1->data:%p->%d", pos1, pos1->data);
	printf("\n");
	SLNode* pos2 = SListFind(plist, 3);
	printf("pos2->data:%p->%d", pos2, pos2->data);
	printf("\n");
	//无此数据时查找
	SLNode* pos3 = SListFind(plist, 5);
	printf("pos3->data:%p->%d", pos3, pos3->data);
	printf("\n");
}

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

执行调试结果如下:
在数据为4->3->1->2->NULL情况下,查找1、3、5:
在这里插入图片描述

5·任意位置插入节点&任意位置删除节点

这组函数共有两种方式实现
说明:这组函数常与查找函数配合使用,即先找到对应数据,然后在对应数据周围或数据本身处进行操作。

5.1任意位置后插入节点&任意位置后删除节点

任意位置后插入节点函数在Slist.c中如下:

// 单链表适合在pos位置之后插入数据,因为基本没有效率损失
/* 此函数与查找函数配合使用,不存在不合法的插入,
因为只有找到存在的节点才能插入节点*/
//无法添加头部节点
void SListInsertAfter(SLNode* pos, SLTDateType x)
{
	//申请一个节点
	SLNode* newnode = BuySListNode(x);
	//尾插也适用
	/*1·将新节点的next指向pos位置的下一节点*/
	newnode->next = pos->next;
	/*2·再将当前位置的next指向新节点*/
	pos->next = newnode;
}

时间复杂度为O(1),基本没有效率损失。

删除任意位置后节点函数在Slist.c中如下:

// 单链表删除pos位置后的节点
//无法删除头部节点
//禁止删除尾部节点
void SListEraseAfter(SLNode* pos)
{
	//禁止尾删
	assert(pos->next != NULL);
	//修改指定位置的next指向该指定位置后的第二个节点处
	pos->next = pos->next->next;
	//释放指定位置下一个节点的空间
	free(pos->next);
	pos->next = NULL;
}

时间复杂度为O(1),基本没有效率损失。

具体使用方式如下:

#include <iostream>
#include  <stdio.h>
#include <stdlib.h>
#include "Slist.h"
#include <assert.h>

//任意位置后插入、任意位置后删除
void TestSList3()
{
	//由于链表是指针管理,因此初始化创建一个结构体指针管理即可
	SListNode* plist = NULL;
	//尾部插入节点
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	//打印尾插节点
	SListPrint(plist);
	printf("\n");

	//在1后插入4
	SLNode* pos1 = SListFind(plist, 1);
	assert(pos1 != NULL);
	SListInsertAfter(pos1, 4);
	//在3后插入5
	SLNode* pos2 = SListFind(plist, 3);
	assert(pos2 != NULL);
	SListInsertAfter(pos2, 5);
	//打印插入后节点
	SListPrint(plist);
	printf("\n");

	//删除3后的5
	SLNode* pos3 = SListFind(plist, 3);
	SListEraseAfter(pos3);
	//打印删除后节点
	SListPrint(plist);
	printf("\n");
	//禁止删除尾部后无空间的数据
	SLNode* pos4 = SListFind(plist, 3);
	SListEraseAfter(pos4);
}
int main()
{
	TestSList3();
	return 0;
}

执行调试结果如下:

1·在已有节点123基础上,在1后插入4,3后插入5:
在这里插入图片描述
2·以上面情况为基础,删除3后的5
在这里插入图片描述
3·以上面情况为基础,删除3后的非法访问内存
在这里插入图片描述

5.2任意位置处插入节点&任意位置处删除节点

任意位置处插入节点函数在Slist.c中如下:

//在pos处插入一个节点
//无法进行尾插
void SListInsert(SLNode** pphead, SLNode* pos, SLTDateType x)
{
	SLNode* newnode = BuySListNode(x);
	//头插
	if (pos == *pphead)
	{
		SListPushFront(pphead, x);
	}
	//正常插入
	else
	{
		//找到pos前一个位置
		SLNode* posPrev = *pphead;
		//遍历pos前的节点
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}
		//1·将指定位置的前一个节点next指向新节点
		posPrev->next = newnode;
		//2·新节点的next指向pos处
		newnode->next = pos;
	}
}

时间复杂度为O(N),效率一般。

任意位置处删除节点函数在Slist.c中如下:

//删除pos处的节点
void SListErase(SLNode** pphead, SLNode* pos)
{
	//头删
	if(pos == *pphead)
	{
		SListPopFront(pphead);
	}
	//尾删同样适用
	else
	{
		//找到pos前一个位置
		SLNode* posPrev = *pphead;
		//遍历pos前的节点
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}
		//1·将指定位置上一节点的next指向指定位置的next
		posPrev->next = pos->next;
		//释放指定位置的空间
		free(pos);
		pos = NULL;
	}
}

时间复杂度为O(N),效率一般。

具体实现方式如下:

#include <iostream>
#include  <stdio.h>
#include <stdlib.h>
#include "Slist.h"
#include <assert.h>

void TestSList3()
{
	//由于链表是指针管理,因此初始化创建一个结构体指针管理即可
	SListNode* plist = NULL;
	//尾部插入节点
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	//打印尾插节点
	SListPrint(plist);
	printf("\n");

	//在1前插入4
	SLNode* pos1 = SListFind(plist, 1);
	assert(pos1 != NULL);
	SListInsert(&plist, pos1, 4);
	//在3前插入5
	SLNode* pos2 = SListFind(plist, 3);
	assert(pos2 != NULL);
	SListInsert(&plist, pos2, 5);
	//打印插入后节点
	SListPrint(plist);
	printf("\n");

	//删除2
	SLNode* pos3 = SListFind(plist, 2);
	SListErase(&plist, pos3);
	//删除3
	SLNode* pos4 = SListFind(plist, 3);
	SListErase(&plist, pos4);
	//删除4
	SLNode* pos5 = SListFind(plist, 4);
	SListErase(&plist, pos5);
	//打印删除后节点
	SListPrint(plist);
	printf("\n");
}

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

执行调试结果如下:
1·在已有节点123基础上,在1处插入4,3处插入5:
在这里插入图片描述
2·以上面情况为基础,删除2、3、4
在这里插入图片描述

5.3两组实现方式对比
  • 对于任意位置后插入节点&任意位置后删除节点,这种方式不需要找到上一节点的位置,时间复杂度为O(1),因此基本不损失效率,但是这种方式比较不符合实际。
  • 对于任意位置处插入节点&任意位置处删除节点,这种方式需要找到上一节点的位置,时间复杂度为O(N),这个代价是比较大的,因此效率一般,但是这种方式比较符合实际。

四、单链表的缺点

单链表相比双链表的缺点就是当需要上一个节点的地址时,需要遍历并且重新定义一个变量保存上一节点的地址,这样操作起来非常不方便,并且效率一般。而双链表很好的解决了这个问题。
线性表之双链表点击这里

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值