算法学习入门---list与算法竞赛中的链表题(C++)

目录

1.各种链表的静态实现

单链表:

双链表:

循环链表:

2.list介绍

3.洛谷---排队顺序

4.洛谷---单向链表

5.洛谷---队列安排

6.洛谷---约瑟夫问题


1.各种链表的静态实现

为什么要学会链表的静态实现?

由于静态链表(数组实现)的代码量远少于动态链表(指针+结构体),同时存储密度比较低,所以在算法竞赛中,一般都会使用静态链表

单链表:

cpp 复制代码
#include<iostream>
using namespace std;

const int N = 1e5 + 10;
int e[N], ne[N], p, id, mp[N];//mp数组,下标为元素,值为元素存放位置

void push_front(int x)
{
	id++;
	e[id] = x;
	mp[x] = id;
	ne[id] = ne[p];
	ne[p] = id;
}

void print()
{
	for(int i = ne[p];i;i = ne[i])
	{
		cout << e[i] << " ";
	}
	cout << endl << endl;
}

int find(int x)
{
	//解法一
	//for (int i = ne[p];i;i = ne[i])
	//{
	//	if (e[i] == x)return i;
	//}
	//return 0;

	//解法二
	return mp[x];
}

void del(int cur)
{
	if (ne[cur])
	{
		mp[e[ne[cur]]] = 0; //把下一个位置的元素下标值为空
		ne[cur] = ne[ne[cur]];
	}
}


int main()
{
	for (int i = 1;i <= 5;i++)
	{
		push_front(i);
		print();
	}
	del(4);
	print();
	cout << find(3) << endl;
	return 0;
}

代码解释:

两个足够大的数组:

  • 一个数组 e 存放数据,充当数据域
  • 一个数组 ne 存放下一个元素的下标,充当指针域;每个模拟结点的 ne 数组存放着下一个模拟结点的 下标

如下图所示,静态链表的一个结点当中有 2 个数组,每个数组下标充当 1 个结点,每个结点会有 e 与 ne 两个数组


两个变量:

  • 一个变量 h 标记头结点的下标(可以把下标为 0 的结点视为头结点)
  • 一个变量 id 标记新来结点的存储位置(尾节点的后一个结点)

注:用 0 来模拟 NULL,即无效 next

如上图所示,当我们要在 B、C 之间插入 D 时,让 D 的 next 指向 3 下标(C结点),B 的 next 指向 4 下标(D结点)

单链表功能实现注意点:

  • 按值查找时可以有两个办法,第一种是遍历整个链表,一个一个依次对比;第二种是创建一个 mp 数组,模仿键值对 <元素值,元素所在位置> (如下图所示)
  • 在删除时,记得要把 mp 数组当中的元素对应关系变为无效
  • 遍历链表打印时,一定要根据 ne 下标来跳着打印,如果只是从头到尾依次输出 e 数组里的值,会把无效值也打印
  • 指定位置后插入即把头插中的 h 换为 cur(指定位置)

注:mp数组来优化按值查找时,只能在元素唯一的情况,例如下标4、下标8都存放了元素100,这种时候就不能用mp数组来优化

双链表:

cpp 复制代码
#include<iostream>
using namespace std;

const int N = 1e5 + 10;
int ne[N], e[N], pre[N], p, id, mp[N];

void push_front(int x)
{
	id++;
	e[id] = x;
	mp[x] = id;
	ne[id] = ne[p];
	pre[id] = p;
	pre[ne[p]] = id;
	ne[p] = id;
}

void print()
{
	for (int i = ne[p];i;i = ne[i])
		cout << e[i] << " ";
	cout << endl << endl;
}

void insert_back(int x,int cur)//在cur位置后插入结点,结点的值为x
{
	id++;
	e[id] = x;
	mp[x] = id;
	ne[id] = ne[cur];
	pre[id] = cur;
	pre[ne[cur]] = id;
	ne[cur] = id;
}

void insert_front(int x, int cur)
{
	id++;
	e[id] = x;
	mp[x] = id;
	ne[id] = cur;
	pre[id] = pre[cur];
	ne[pre[cur]] = id;
	pre[cur] = id;
}

int find(int x)
{
	return mp[x];
}

void erase(int cur)//删除cur位置元素
{
	pre[ne[cur]] = pre[cur];
	ne[pre[cur]] = ne[cur];
}

int main()
{
	for (int i = 1;i <= 5;i++)
	{
		push_front(i);
		print();
	}
	insert_front(300, 5);
	erase(1);
	print();
	return 0;
}

与单链表相比,双链表多出了pre数组,即每个结点指向前面的指针

也正因为有了指向前面的指针,所以得以删除cur时可以不用去找到其前驱节点,直接删除cur结点本身

循环链表:

因为静态数组实现时,最后一个结点的next指向0,即指向头结点,所以已经满足了循环链表的性质

2.list介绍

cpp 复制代码
#include<iostream>
#include<list>
using namespace std;

void print(list<int>& l)
{
	for(auto x:l) cout<<x<<" ";
	cout<<endl;
}


int main()
{
	list<int> l;
	
	for(int i=1;i<=5;i++)
	{
		l.push_back(i);
		print(l);
	}
		for(int i=1;i<=5;i++)
	{
		l.push_front(i);
		print(l);
	}
		for(int i=1;i<=5;i++)
	{
		l.pop_back();
		print(l);
	}
		for(int i=1;i<=5;i++)
	{
		l.pop_front();
		print(l);
	}
	return 0;
}

list 是 c++ stl 库中提供的动态链表模板,主要有以下几个函数功能

  • 初始化:list<size> list_name
  • push_back:尾插
  • pop_back:尾删
  • push_front:头插
  • pop_front:头删

3.洛谷---排队顺序

如下图所示,不难发现小朋友编号即为静态数组中的next指针,而下标就可以看成是元素(元素与下标一一对应),1下标对应的next指针为 4,所以是 1 4;4下标对应的next为2,所以是 1 4 2......

拓展到一般情况,第一个小朋友的编号为 h,h 的 next指针为 a[h],最后一个小朋友的next指针 a[h] = 0

代码:

cpp 复制代码
#include<iostream>
using namespace std;

int main()
{
	int n;cin>>n;
	int a[n+1]={0};
	for(int i=1;i<=n;i++)cin>>a[i];
	int h;cin>>h;
	for(int i=h;i;i=a[i]) cout<<i<<" ";//a中存放next指针 
	return 0;
}

4.洛谷---单向链表

模拟实现一个单链表

操作1为指定位置后插入,操作2为查询指定位置后元素,操作3为删除指定位置后元素

所有数字均不相同,可以使用mp数组优化

代码:

cpp 复制代码
#include<iostream>
using namespace std;

const int N = 1e6 + 10;
int ne[N],e[N],phead,id,mp[N];

int find(int x)
{
	return mp[x];
}

void insert_back(int x,int y)//在x元素后插入y元素,先得找到x元素位置 
{
	int cur = find(x);
	id++;
	e[id]=y;
	mp[y]=id;
	ne[id]=ne[cur];
	ne[cur]=id;
}

int find_ret(int x)
{
	int cur = find(x);
	if(ne[cur])
	{
		return e[ne[cur]];
	}
	return 0;
}

void erase(int x)
{
	int cur = find(x);
	if(ne[cur])
	{
		mp[e[ne[cur]]] = 0;
		ne[cur] = ne[ne[cur]];
	}
}


int main()
{
	int x,y;
	int q;cin>>q;
	insert_back(phead,1);
	while(q--)
	{
		int in;cin>>in;
		if(in == 1)
		{
			cin>>x>>y;
			insert_back(x,y);
		}
		else if(in == 2)
		{
			cin>>x;
			cout<<find_ret(x)<<endl;
		}
		else
		{
			cin>>x;
			erase(x);
		}
	}
	return 0;
}

5.洛谷---队列安排

现有1个同学进入,然后编号为2~N的同学依次进入,可以指定编号为 i 的同学站在编号为 1~N 中某位同学(先前已经入列)的左边或右边,左边 p = 0,右边 p = 1

然后去掉M个同学,如果指定删除的同学不在队列中,忽略本次操作


通过链表来解决这个问题,指定位置的插入与删除,但考虑到插入可以是结点的左边也可以是其右边(如下图所示),所以需要通过双向链表来解决,即有前驱指针(左插入)与后继指针(右插入)

所以本题就是模拟实现一个双向链表,小朋友编号唯一,所以可以用mp数组优化

代码:

cpp 复制代码
#include<iostream>
using namespace std;

const int N = 1e6 + 10;
int ne[N],e[N],phead,id,mp[N],pre[N];

int find(int x)
{
	return mp[x];
}

void insert_back(int x,int y)
{
	int cur = find(x);
	id++;
	e[id]=y;
	mp[y]=id;
	ne[id]=ne[cur];
	pre[id]=cur;
	pre[ne[cur]]=id;
	ne[cur]=id;
}

void insert_front(int x,int y)
{
	int cur = find(x);
	id++;
	e[id]=y;
	mp[y]=id;
	ne[id]=cur;
	pre[id]=pre[cur];
	ne[pre[cur]]=id;
	pre[cur]=id;
}

void erase(int x)
{
	int cur = find(x);//删除当前元素
	if(cur)
	{
		mp[e[cur]]=0;	
		ne[pre[cur]]=ne[cur];
		pre[ne[cur]]=pre[cur];
	} 
}

void print()
{
	for(int i=ne[phead];i;i=ne[i]) cout<<e[i]<<" ";
}


int main()
{
	int n;cin>>n;
	n--;
	insert_back(phead,1);
	int num = 2;
	while(n--)
	{
		int k,p;cin>>k>>p;
		if(p)insert_back(k,num);
		else insert_front(k,num);
		num++;
	}
	int m;
	cin>>m;
	while(m--)
	{
		int del;cin>>del;
		erase(del);
	}
	print();
	return 0;
}

6.洛谷---约瑟夫问题

实现一个循环链表,即 ptail -> next = phead 的链表(最后一个结点指向头结点)

然后每间隔 m 个结点,删除一个数,同时打印那个被删掉的数

因此本题需要模拟实现一个循环链表

代码:

cpp 复制代码
#include<iostream>
using namespace std;

const int N = 110;
int ne[N];

int main()
{
	int n,m;cin>>n>>m;
	for(int i=1;i<n;i++)ne[i]=i+1;
	ne[n]=1;
	int t=n;
	for(int i=1;i<=n;i++)//n次出圈操作
	{
		for(int j=1;j<m;j++)//让j往后移动m-1位
			t = ne[t];
		cout<<ne[t]<<" ";//打印当前结点的下一个结点
		ne[t] = ne[ne[t]];//删除当前结点的下一个结点		
	} 
	return 0;
}

代码解释:

  • 因为用的是单链表的循环链表,所以删除时需要先找到删除结点的前一个结点,即每次循环到 m-1 然后把 m 删除
  • 如果循环链表使用头结点,那么 删除结点 将会很难判断,因为是否会经过 头结点 是不确定的,所以我们选择不使用 头结点
  • 如下图所示,初始化时可以指向整个链表的表尾,把 表尾 作为开始就相当于把 表尾 看作了一个有值的 头结点;我们把表尾结点的值搞成1,表头结点的值从2开始
  • 因为循环链表的 结点值 是 [1,2,3,......],与下标对应,所以无需要数据域与id来实现静态链表的插入过程了
  • 每次 t 往后移动,都是在链表中的结点与结点之间移动,千万不要是遍历数组的方式移动
相关推荐
CoderYanger1 小时前
动态规划算法-路径问题:9.最小路径和
开发语言·算法·leetcode·动态规划·1024程序员节
老欧学视觉1 小时前
0012机器学习KNN算法
人工智能·算法·机器学习
月明长歌1 小时前
【码道初阶】一道经典的简单题:Boyer-Moore 多数投票算法|多数元素问题(LeetCode 169)
算法·leetcode·职场和发展
Aevget1 小时前
从业务面板到多视图协同:QtitanDocking如何驱动行业级桌面应用升级
c++·qt·ui·ui开发·qt6.3
CoderYanger1 小时前
动态规划算法-路径问题:7.礼物的最大价值
开发语言·算法·leetcode·动态规划·1024程序员节
蕓晨1 小时前
钱币找零问题-贪心算法解析
c++·算法·贪心算法
hetao17338371 小时前
2025-12-04 hetao1733837的刷题记录
c++·算法
mjhcsp2 小时前
C++ 后缀自动机(SAM):原理、实现与应用全解析
java·c++·算法
gihigo19982 小时前
一维光栅结构严格耦合波分析(RCWA)求解器
算法