数据结构五、链表

链表的定义

       线性表的链式存储就是链表,它是把元素存储在任意的存储单元中,因此,我们不能像顺序表那样直接通过下标来保证数据元素间的逻辑关系。所以,链式存储除了要保存元素之外,还需要额外维护数据元素之间的逻辑关系,这两部分信息合称为节点。即节点有两个域:保存元素的数据域和存储逻辑的指针域。链表的种类有很多,在算法竞赛中,我们只需掌握单向链表双向链表循环链表即可。下面我们先来重点讲解单向链表。

单链表的模拟实现

       和顺序表一样,在算法竞赛中,我们仍然采用静态实现的方式来模拟实现链表。根据链表的定义,我们需要创建两个足够大的数组,其中一个数组e(元素element的简写)来存储数据,充当数据域,另一个数组ne(next的简写)来存储下一个元素的下标,充当指针域。这里要注意,尽管我们创建的是两个数组,但我们仍然要把它们连在一起看上下两个格子描述的是同一个节点。如下图所示:

 此外,我们还需要两个变量,变量h用来标记头结点的下标,变量id用来标记新节点的存储位置。单链表的创建代码如下:

#include <iostream>
using namespace std;

const int N = 1e5 + 10;
int h, id;
int e[N], ne[N];

单链表——头插

       头插操作是链表中最常用的操作,也是后续树和图的存储中的邻接表以及链式前向星的基础操作,因此必须要掌握。 所谓头插,顾名思义,就是在链表的头部插入元素。

如下图所示:

 那么这就带来一个问题,待会我们写代码的时候,是先修改②再修改①,还是先修改①再修改②呢?如果我们先修改①号指针的话,那么初始指针就会被覆盖,那么连带着后面链表的内容就会消失,这是很致命的情况。因此,我们一定要先修改②,再修改①。代码如下:

//头插
void push_front(int x)
{
	id++;            //标记新节点的位置
	e[id] = x;       //存储新节点的值
	ne[id] = ne[h];  //修改新节点的指针域,让其指向哨兵位的下一个位置
	ne[h] = id;      //修改哨兵位的指针域,让其指向新节点
}

 单链表——遍历

      首先我们定义一个变量i,按照平时我们可能会把i初始化成0,但是在链表中,头结点(也就是0位置处)我们是不存储有效信息的,有效信息都是从头结点指向的下一个位置开始存储的。因此,这里我们会把i初始化成ne[h]。那么如何通过i去访问下一个元素,我们只需要让i = ne[i]即可。

代码如下:

//遍历
void print()
{
	for(int i = ne[h];i;i = ne[i])
	{
		cout << e[i] << " ";
	}
	cout << endl;
}

 单链表——按值查找

        按值查找,就是查找链表中是否存在元素x,如果存在,返回该元素存储的下标。 这里提供两种解法。

        解法一:遍历整个链表,一个一个的进行比较。代码如下:

//按值查找(解法一)
int find(int x)
{
	for(int i = ne[h];i;i = ne[i])
	{
		if(e[i] == x)
			return i;
	}
	return 0;
}

         解法二:使用哈希表进行优化。此时,我们会新创建一个数组mp,用来存放每一个元素对应的下标,例如mp[i]表示i这个元素存储的位置。这是算法竞赛中经常用的操作,用空间上的额外开辟来换取时间上的优化。不过解法二也有一定的局限性:首先,数据的值不能过大,比如存1e9这个数的存储位置,我们就不能开辟这么大的数组;其次,链表中不能存储重复的数字,否则哈希表内就无法存储位置。以上这两点局限性也被称为哈希冲突。在后续介绍哈希表的时候会讲解如何处理哈希冲突,这里就先不做过多介绍了。直接上代码吧:

#include <iostream>
using namespace std;

const int N = 1e5 + 10;
int h, id;
int e[N], ne[N];

int mp[N];

void push_front(int x)
{
	id++;    
	e[id] = x;  
	
	mp[x] = id;  //标记x存储的位置
	
	ne[id] = ne[h];
	ne[h] = id; 
}

//按值查找(解法二)
int find(int x)
{
	return mp[x];
}

单链表——在任意位置之后插入元素

        我们现在需要在存储位置为p的位置后插入一个元素x,有了之前头插操作的铺垫,那么实现这个代码就变得特别轻松。

//任意位置插入
void insert(int p, int x)
{
	id++;
	e[id] = x;
	mp[x] = id;
	ne[id] = ne[p];
	ne[p] = id;
}

单链表——删除任意位置之后的元素

        删除存储位置为p后面的元素,我们直接让p指向下一个元素的下一个元素即可。 但直接这样实现会出现一个bug,当我们删除的是链表中的最后一个元素时,ne[ne[p]]会找到哨兵位的下一个节点,此时再去赋值会让链表成环,破坏了实际结构。如下图所示:

所以我们在执行删除操作时,一定要先判断一下,如果ne[p]=0,那么说明p位置是最后一个位置,此时不能执行删除操作。 代码如下:

//删除任意位置之后的元素
void erase(int p)
{
	if (ne[p])
	{
		ne[p] = ne[ne[p]];
		mp[e[ne[p]]] = 0;  //将p后面的元素从mp数组中删除
	}
}

小结

        以上就是单链表的全部内容了,那么我们为什么不像顺序表那样实现尾删、尾插等操作呢?可以实现,但是时间复杂度很高,在后续做题中也不会用到,所以也就不做讲解和代码实现了。毕竟,使用各种数据结构是方便我们去解决实际问题的,而不是添堵(增加时间复杂度)的~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值