目录
前言
上一篇我们讲的线性表的顺序储存结构,实际上它是有缺陷的。其中最大的缺陷就在于插入和删除的时候需要移动大量元素,这显然就需要耗费大量的时间。所以,我们需要考虑用什么办法来解决这个问题。
要解决这个问题,在这里我们就要引入线性表的链式储存结构的概念。简单来说,就是不需要将所有的数据元素按照顺序连续的进行排列,只需要将数据元素安排的到有空位的地方,然后让每个元素知道下一个元素的位置。这样一来,我们就能通过遍历来找到所有元素的位置。
下面我们就来系统的认识一下什么是线性表的数据储存结构吧。
一、链式储存结构的定义
在阐述定义前,我们得先了解几个概念:
- 我们把存储数据元素信息的域称为数据域。
- 把存储直接后继位置的域称为指针域。
- 这两部分信息组成数据元素的存储映像,称为结点。
链式储存结构:n个结点链组成一个链表,即为线性表的链式存储结构。因为此链表的每个结点中只包含一个指针域,所以叫做单链表。我们把链表中第一个结点的存储位置叫做头指针。在单链表中第一个结点前附设一个结点,称为头结点。(头结点的数据域可以不储存任何信息)
下面我们就来看看,线性表的单链表储存结构的代码描述:
typedef struct node{
int data; //用于储存数据,这里暂定为int类型的数据
struct node *next; //这里用于储存下一个元素的位置
}node,*linklist; //定义linklist
这里稍作解释,假设说 p 是第 i 个元素的指针,那么该元素的数据域我们可以用p.data来表示,指针域就可以用 p.next 来表示,表示指向下一个结点的指针。
二、单链表的读取操作
在顺序储存结构中,要获取一个元素的位置是比较容易的。但对于链式储存结构,由于我们没办法在一开始就知道第 i 个元素在什么位置,所以我们必须从头开始遍历整个单链表。因此,在算法上要相对麻烦些。算法思路为:
- 声明一个指针p指向链表的第一个结点,初始化 j 从1开始;
- 当 j<i 时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
- 若到链表末尾时 p 为空,则说明第 i 个结点不存在;
- 否则查找成功,返回结点 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 个数据插入结点的算法思路:
- 利用上面的查找算法,将第 i 个元素的位置找出来;
- 若查找成功,在系统中建立一个新的结点;
- 将数据元素e赋值给 s.data;
- 单链表的插入标准语句 s.next=p.next , p.next=s;
- 返回正确。
实现代码算法如下:
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释放掉。
算法思路如下:
- 利用上述的查找算法找出第 i 个元素的位置;
- 若查找成功,将欲删除的结点p.next赋值给q;
- 单链表的删除标准语句 p.next=q,next;
- 将q结点中的数据赋值给e,作为返回;
- 释放q结点;
- 返回正确。
实现代码算法如下:
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)。
显然,对于插入或删除数据越频繁的操作,单链表的优势就越明显。