数据结构与算法——线性表的链式储存结构

目录

前言

一、链式储存结构的定义

二、单链表的读取操作

三、单链表的插入与删除

3.1   单链表的插入

 3.2   单链表的删除

总结


 

前言

上一篇我们讲的线性表的顺序储存结构,实际上它是有缺陷的。其中最大的缺陷就在于插入和删除的时候需要移动大量元素,这显然就需要耗费大量的时间。所以,我们需要考虑用什么办法来解决这个问题。

要解决这个问题,在这里我们就要引入线性表的链式储存结构的概念。简单来说,就是不需要将所有的数据元素按照顺序连续的进行排列,只需要将数据元素安排的到有空位的地方,然后让每个元素知道下一个元素的位置。这样一来,我们就能通过遍历来找到所有元素的位置。

下面我们就来系统的认识一下什么是线性表的数据储存结构吧。


一、链式储存结构的定义

在阐述定义前,我们得先了解几个概念:

  • 我们把存储数据元素信息的域称为数据域
  • 把存储直接后继位置的域称为指针域
  • 这两部分信息组成数据元素的存储映像,称为结点

链式储存结构:n个结点链组成一个链表,即为线性表的链式存储结构。因为此链表的每个结点中只包含一个指针域,所以叫做单链表。我们把链表中第一个结点的存储位置叫做头指针。在单链表中第一个结点前附设一个结点,称为头结点。(头结点的数据域可以不储存任何信息)

下面我们就来看看,线性表的单链表储存结构的代码描述:

typedef struct node{
	
	int data;       //用于储存数据,这里暂定为int类型的数据 
	
	struct node *next;      //这里用于储存下一个元素的位置 

}node,*linklist;      //定义linklist 

 这里稍作解释,假设说 p 是第 i 个元素的指针,那么该元素的数据域我们可以用p.data来表示,指针域就可以用 p.next 来表示,表示指向下一个结点的指针。

二、单链表的读取操作

在顺序储存结构中,要获取一个元素的位置是比较容易的。但对于链式储存结构,由于我们没办法在一开始就知道第 i 个元素在什么位置,所以我们必须从头开始遍历整个单链表。因此,在算法上要相对麻烦些。算法思路为:

  1. 声明一个指针p指向链表的第一个结点,初始化 j 从1开始;
  2. 当 j<i 时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
  3. 若到链表末尾时 p 为空,则说明第 i 个结点不存在;
  4. 否则查找成功,返回结点 p 的数据。

 实现代码算法如下:

int get(linklist a,int i,int *e)
{
	int j=1;        //j为计数器 
	p=a.next;       //让p指向链表a的第一个结点 
	while(p&&j<i)    //当p不为空并且j<i时,循环继续 
	{
		p=p.next;    //让p指向下一个结点 
		j++;
	}
	if(!p||j>i)    //第i个元素不存在的时候,返回wrong 
	{
		return wrong;
	}
	*e=p.data;      //取得第i个元素的数据 
	
	return right;
}

说白了,就是从头开始找。其算法的时间复杂度取决于 i 的位置,如果 i 的位置在第一个,则不需要遍历;如果是最坏的情况,当i=n时,需要遍历n-1次才能找到,时间复杂度为O(n)。其工作的核心思想就是“工作指针后移”,这也是很多算法常用的技术。

三、单链表的插入与删除

3.1   单链表的插入

简单来说,就是要将一个新的结点 s 插入到结点 p 与 p.next 之间即可。代码实现结果如下:


s.next=p.next;   //让p的后继结点赋值给s的后继 
p.next=s;        //将s赋值给p的后继 

这两句的顺序切记不能交换。具体原因留给读者自行思考。

下面给出第 i 个数据插入结点的算法思路:

  1. 利用上面的查找算法,将第 i 个元素的位置找出来;
  2. 若查找成功,在系统中建立一个新的结点;
  3. 将数据元素e赋值给 s.data;
  4. 单链表的插入标准语句 s.next=p.next , p.next=s;
  5. 返回正确。

实现代码算法如下:

int listinsert(linklist *a,int i,int e)
{
	int j=1;
	linklist p,s;
	p=*a;
	while(p&&j<i)       //寻找第i个结点的位置 
	{
		p=p.next
		j++
	}
	if(!p||j>i)
	{
		return wrong;
	}
	s=(linklist)malloc(sizeof(node));   //生成一个新的结点 
	s.data=e;           //将数据e赋值给s 
	s.next=p.next;      //将s进行插入操作 
	p.next=s;
	
	return ok;
	
}

 3.2   单链表的删除

对于单链表的删除操作,就更加简单啦。只需要将结点p的前继节点的指针绕过,直接指向p的后继结点即可,最后再把结点p释放掉。

算法思路如下:

  1. 利用上述的查找算法找出第 i 个元素的位置;
  2. 若查找成功,将欲删除的结点p.next赋值给q;
  3. 单链表的删除标准语句 p.next=q,next;
  4. 将q结点中的数据赋值给e,作为返回;
  5. 释放q结点;
  6. 返回正确。

实现代码算法如下:

int listinsert(linklist *a,int i,int *e)
{
	int j=1;
	linklist p,q;
    p=*a;
    while(p.next&&j<i)        //查找第i个元素 
    {
    	p=p.next;
    	j++;
	}
	if(!(p.next)||j>i)
	{
		return wrong;
	}
	q=p.next;         //标准删除语句 
	p.next=q.next;
	*e=q.data;         //将q的数据由e返回 
	free(q);           //释放q结点 
	
	return right
	
}

总结

相比于顺序储存结构,单链表储存结构在查找数据元素方面要逊于前者。但对于数据元素的插入和删除操作,就会有一定的优势,我们可以来简单分析一下。

单链表的插入和删除操作其实都是有两部分组成:一是遍历查找第 i 个结点;二是插入和删除结点。显然,其时间复杂度为O(n),假设说对于不知道第i个结点的位置,在单链表上进行插入和删除操作,与顺序储存结构没有太大的优势。但是,如果我们希望从第 i 个位置插入10个结点,对于顺序存储结构每一次都需要移动n-i个结点,每次的时间复杂度都是O(n)。对于单链表,当找到了第i个元素的位置,时间复杂度为O(n),但之后无论插入或删除多少个结点,其时间复杂度永远是O(1)。

显然,对于插入或删除数据越频繁的操作,单链表的优势就越明显。

  • 3
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值