单链表的简单理解和基本操作

单链表

1. 链表的概念及结构

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

区分物理结构与逻辑结构

物理结构: 实际存在的,可以通过肉眼观察到
逻辑结构: 实际不存在, 人为想象出来的

在这里插入图片描述

注意:

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

2. 链表的分类

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

在这里插入图片描述

2. 带头或者不带头:
在这里插入图片描述

3. 循环或者非循环:
在这里插入图片描述

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
在这里插入图片描述

  1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结
    构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

  2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都
    是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带
    来很多优势,实现反而简单了,后面我们代码实现了就知道了。

3. 单链表项目创建

文件名功能
SList.h创建单链表,完成函数名的声明
SList.c实现单链表的各个功能函数
test.c测试单链表函数的正确性

3.1 定义单链表

typedef int SLTDataType;

typedef struct SListNode
{
	SLTDataType data;            //需要存储的数据
	struct SListNode* next;      //指向下一个结点的指针
}SLTNode;

3.2 动态申请一个结点

SLTNode* BuySLTNode(SLTDataType x)  
{
	SLTNode* newhead = (SLTNode*)malloc(sizeof(SLTNode));
	if (newhead == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	newhead->data = x;
	newhead->next = NULL;

	return newhead;
}

3.3 构造一个有n个结点的链表(快速构造链表)

SLTNode* CreateList(SLTDataType n)
{
	SLTNode* ptail = NULL;        //首指针
	SLTNode* phead = NULL;        //尾指针

	for (int i = 0; i < n; i++)
	{
		SLTNode* newnode = BuySLTNode(i + 10);

		if (phead == NULL)       //链表为空
		{
			phead = ptail = newnode;
		}
		else                     //链表不为空
		{
			ptail->next = newnode;
			ptail = newnode;     //变成新的尾
		}
	}

	return phead;               //返回保留的链表头
}

3.4 打印单链表

先定义一个新的结点保留链表的头,然后while循环遍历打印

void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;      //保留链表的头

	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}

	printf("NULL\n");
}

3.5 单链表尾插

错误示例
void SListPushBack(SLTNode* phead,SLTDataType x)
{
    SLTNode* tail = phead;
    while(tail->next != NULL) 
    {
          tail = tail->next;
    }
    SLTNode* newnode = BuySLTNode(x);
    tail->next = newnode;
}

上面写法有2处错误:

  1. phead未进行判空, 当链表为空时, 会存在对空指针的解引用, 出现错误。
  2. 函数传参错误plist(实参)的类型为SLTNode *,而我们形参类型也是SLTNode *,这属于值传递,值传递相当于 形参是实参的一份临时拷贝,形参的改变并不会影响实参的值。想要修改实参的值就需要进行传址操作,在这里传plist的地址.形参用二级指针**pphead
思路

判断链表是否为空:

(1)为空, 让头指针指向新开辟的结点

(2)不为空, 遍历找到尾结点(即其next为空),将新开辟的结点连接到后面

在这里插入图片描述

正确写法
void SLTPushBack(SLTNode** pphead,SLTDataType x)
{
	SLTNode* newnode = BuySLTNode(x);

	if (*pphead == NULL)           //链表为空尾插  
	{
		*pphead = newnode;
	}
	else                           //非空链表尾插
	{
		SLTNode* tail = *pphead;
	
		//找尾
		while (tail->next)
		{
			tail = tail->next;
		}
	
		tail->next= newnode;
	}
}

3.6 单链表尾删

判断链表是否为空:

  1. 链表为空不能删除

  2. 只有一个结点(即头结点),直接释放头结点并将其置空

  3. 有一个或一个以上结点, 两种方法: (1) 找尾的前一个, 将其保存起来,释放尾后,使尾的前一个指向空。

(2) 找尾下一个的下一个

在这里插入图片描述

版本1
void SLTPopBack(SLTNode**pphead)
{
	assert(*pphead);                //链表为空不能删除

	if ((*pphead)->next == NULL)    //只有一个结点要单独处理
	{
		free(*pphead);
		*pphead = NULL;
	}
	else                            
	{
		//思路1: 找尾的前一个
		SLTNode* prev = NULL;
		SLTNode* tail = *pphead;
	
		while (tail->next)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		prev->next = NULL;
	}
}

在这里插入图片描述

版本2
void SLTPopBack(SLTNode**pphead)
{
	assert(*pphead);                //链表为空不能删除

	if ((*pphead)->next == NULL)    //只有一个结点要单独处理
	{
		free(*pphead);
		*pphead = NULL;
	}
	else                            
	{
		SLTNode* tail = *pphead;

		while (tail->next->next)
		{
			tail = tail->next;
		}
        
		free(tail->next);
		tail->next = NULL;
	}
}

3.7 单链表头插

思路:直接开辟一个新的结点,将这个结点连接到头结点的前面,这个结点变成新的头结点

在这里插入图片描述

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuySLTNode(x);
	newnode->next=*pphead;
	*pphead = newnode;
}

3.8 单链表头删

思路:将头结点的后一个结点保存起来,释放头结点,原头结点的后一个结点变成新的头结点

注意: 链表为空不能删除

在这里插入图片描述

void SLTPopFront(SLTNode** pphead)
{
	assert(*pphead);                    //链表为空不能删除

	SLTNode* next = (*pphead)->next;    //保存后一个结点,作为新链表的头
	free(*pphead);
	*pphead = next;                     //变成新的头
}

3.9 查找单链表中的元素

思路:先将链表头指针记录下来,然后while循环遍历查找元素,找到返回结点指针,找不到返回NULL

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* cur = phead;

	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

3.10 单链表在pos位置之后插入元素

思路: 和头插思路相似,找到pos位置后,将新开辟的结点连接到pos的后面

注意: 判断pos位置的合法性

void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);     //pos位置的合法性

	SLTNode* newnode = BuySLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

3.11 单链表删除pos位置之后的元素

思路: 和头删思路相似,找到pos位置后,链表中只有一个结点直接释放,

有多个结点时,保存pos后一个结点,pos后一个结点连接到pos后一个结点的后一个,释放掉原pos的后一个结点

注意: 判断pos位置的合法性

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

	if (pos->next == NULL)   //链表中只剩一个结点
	{
		return;
	}
	else
	{
		SLTNode* nextnode = pos->next;
		pos->next = nextnode->next;
		free(nextnode);                //nextnode置不置空都行,它是局部变量,出了作用域变量销毁
	}
}

3.12 单链表在pos位置之前插入元素

思路: 和尾插思路相似,找到pos位置后,pos位置是头直接复用头插

不是头,找pos的前一个,将新开辟的结点连接到pos前一个结点的后面,pos结点的前面

注意: 判断pos位置的合法性

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pos);

	if (*pphead == pos)        //pos是链表的第一个结点,头删
	{
		SLTPushFront(pphead, x);
	}
	else
	{   //找pos的前一个
		SLTNode* prev = *pphead;

		while (prev->next != pos)
		{
			prev = prev->next;
		}

		SLTNode* newnode = BuySLTNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}

3.13 单链表删除pos位置的元素

思路: 和尾删思路相似,找到pos位置后,pos位置是头直接复用头删

不是头,找pos的前一个记录下来,将pos的后一个结点连接到pos前一个结点的后面,释放pos结点

注意: 判断pos位置的合法性

void SLTErase(SLTNode** pphead,  SLTNode* pos)
{
	assert(pos);

	if (*pphead == pos)          //pos是链表的第一个结点,头删
	{
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;

		while (prev->next != pos)
		{
			prev = prev->next;
		}

		prev->next = pos->next;
		free(pos);                //pos置空时要用二级指针
	}
}

3.14 销毁单链表

while循环遍历销毁,链表中的结点需要一个一个释放

void SLTDetroy(SLTNode** pphead)
{
	SLTNode* cur = *pphead;

	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;     //将plist置空
}

4. 总结

优缺点:

单链表的头插和头删很简单, 尾插和尾删比较复杂(要考虑两种情况)

顺序表和链表的区别

不同点顺序表链表
存储空间上物理上一定连续逻辑上连续,但物理上不一定连续
随机访问支持 O(1)不支持:O(N)
任意位置插入或者删除元素可能需要搬移元素,效率低 O(N)只需修改指针指向
插入动态顺序表,空间不够时需要扩容没有容量的概念
应用场景元素高效存储+频繁访问任意位置插入和删除频繁
缓存利用率高低

备注:缓存利用率参考存储体系结构 以及 局部原理性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
数据结构链表插入、删除和修改实验报告 一、实验目的 1.理解数据结构中带头结点链表的定义和逻辑图表示方法。 2.掌握链表中结点结构的JAVA描述。 3.熟练掌握链表的插入、删除和查询算法的设计与JAVA实现。 4.熟练掌握简单的演示菜与人机交互设计方法。 二、实验内容 1. 编制一个演示链表插入、删除、查找等操作的程序。 三、实验步骤 1.需求分析 本演示程序用JAVA编写,完成链表的生成,任意位置的插入、删除,以及确定某一元素在链表中的位置。 ① 输入的形式和输入值的范围:插入元素时需要输入插入的位置和元素的值;删除元素时输入删除元素的位置;查找操作时需要输入元素的值。在所有输入中,元素的值都是整数。 ② 输出的形式:在所有三种操作中都显示操作是否正确以及操作链表的内容。其中删除操作后显示删除的元素的值,查找操作后显示要查找元素的位置。   ③ 程序所能达到的功能:完成链表的生成(通过插入操作)、插入、删除、查找操作。 ④ 测试数据:  A. 插入操作中依次输入11,12,13,14,15,16,生成一个链表    B. 查找操作中依次输入12,15,22返回这3个元素在链表中的位置    C. 删除操作中依次输入2,5,删除位于2和5的元素 2.概要设计 1)为了实现上述程序功能,需要定义链表的抽象数据类型:   ADT LinkList {    数据对象:D={ai|ai∈IntegerSet,i=0,1,2,…,n,n≥0}    数据关系:R={|ai,ai+1 ∈D}    基本操作: (1)insert 初始化状态:链表可以不为空集;操作结果:插入一个空的链表L。   (2)decelt     操作结果:删除已有的链表的某些结点。 (3)display     操作结果:将上述输入的元素进行排列显示。    (4)modify     操作结果:将上述输入的某些元素进行修改。    (5)save     操作结果:对上述所有元素进行保存。    (6)load     操作结果:对上述元素进行重新装载。   }   2)本程序包含7个函数:   ① 主函数main()   ② 保存链表函数save()   ③ 重载操作函数load()   ④ 显示链表内容函数display ()   ⑤ 插入元素函数insert ()   ⑥ 删除元素函数decelt ()   ⑦ 修改元素函数modify()   各函数间关系如下: 3.详细设计   实现概要设计中定义的所有的数据类型,对每个操作给出伪码算法。对主程序和其他模块也都需要写出伪码算法。   1) 结点类型和指针类型   typedef struct node {    int data;    struct node *next;   }Node,*singleLIST.java;   2) 链表基本操作   为了方便,在链表中设头结点,其data域没有意义。 bool insert(singleLIST) (伪码算法)   bool modify(singleLIST) (伪码算法)   void delect(singleLIST)   (伪码算法)   void display()   (伪码算法) 3) 其他模块伪码算法 4.调试分析   (略) 5.使用说明 程序名为 ,运行环境为Windows。程序执行后显示   ========================   0----EXIT   1----INSERT   2----DELETE   3----DISPLAY 4----MODIFY 5----EXIST =======================   SELECT:   在select后输入数字选择执行不同的功能。要求首先输入足够多的插入元素,才可以进行其他的操作。每执行一次功能,就会显示执行的结果(正确或错误)以及执行后链表的内容。 选择5:退出程序   选择1:显示"INSERT =" ,   要求输入要插入的位置和元素的值(都是整数)。   选择2:显示"DELETE =" ,   要求输入要删除元素的位置,执行成功后返回元素的值。   选择3:显示"MODIFY = " , 选择要修改的对象,执行成功后返回新的元素值。 选择4:显示"DIAPLAY= "   显示所有链表中的元素,自动进行排序。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值