目录

蓝桥备赛13-链表和-list上

蓝桥备赛(13)- 链表和 list(上)


一、模拟题:

在开始之前:先来看一道提升题 –> The Blocks Problem

https://i-blog.csdnimg.cn/direct/86501ff3c83a47bda60f78071bbf1726.png

https://i-blog.csdnimg.cn/direct/e617281a6e96402b93138fb993f24403.png

这道题本质上是一道模拟题 , 模拟题目所需要的流程;

1) 看到题目我们首先就会想到 , 用4 个 if 语句 , 对这4个指令进行 匹配 , 然后执行

2 ) 但是 , 其实只有两种操作 , 分别是 归位 和 移动

https://i-blog.csdnimg.cn/direct/4f85a18d80eb4ff2a499b72788f7f7e8.png

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
typedef pair<int,int> PII;

const int N = 30;
vector<int> p[N];
int n;

PII find(int x)
{
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < p[i].size(); j++)
		{
			if (p[i][j] == x)
			{
				return { i,j };
			}
		}
	}
}

//归位
void clean(int x, int y)
{
	//把[x , y ] 上方的木块归位
	for (int j = y + 1; j < p[x].size(); j++)
	{
		int t = p[x][j];
		p[t].push_back(t);
	}
	p[x].resize(y + 1);
}

//移动
void move(int x1, int y1, int x2)
{
	//把 [ x1 , y1 ] 以及以上的木块放在x2上
	for (int j = y1; j < p[x1].size(); j++)
	{
		p[x2].push_back(p[x1][j]);
	}
	p[x1].resize(y1);
}
int main()
{
	cin >> n;

	//初始化
	for (int i = 0; i < n; i++)
	{
		p[i].push_back(i);
	}

	string op1, op2;
	int a, b;

	while (cin >> op1 >> a >> op2 >> b)
	{
		//查找 a 和 b 的位置
		PII pa = find(a);
		int x1 = pa.first, y1 = pa.second;
		PII pb = find(b);
		int x2 = pb.first, y2 = pb.second;

		//执行
		if (x1 == x2)continue; // 操作不合法

		if (op1 == "move")//把 a 上方归位
		{
			clean(x1, y1);
		}
		if (op2 == "onto") //把 b 上方归位
		{
			clean(x2, y2);
		}
		//移动
		move(x1, y1, x2);
	}
	//打印
	for (int i = 0; i < n; i++)
	{
		cout << i << ":";
		for (int j = 0; j < p[i].size(); j++)
		{
			cout << " " << p[i][j];
		}
		cout << endl;
	}
	return 0;
}

这道题是洛谷从UVa这个外国的OJ平台收录的题目 ,

https://i-blog.csdnimg.cn/direct/ee06437f7eb84ef1b9448214ac1d5b31.png

在提交的答案的时候回显示需要到UVa 里去提交 ;

https://i-blog.csdnimg.cn/direct/744db462d6774c31ac3fd1ad15f2da48.png

在Vitrual Judge 平台显示这个,就表示AC了

https://i-blog.csdnimg.cn/direct/8320e21cbadd4390b329aff85c1dcf7f.png

二 、 链表的概念

1.1 链表的定义

链表 : 用 链式存储 实现的 线性表

https://i-blog.csdnimg.cn/direct/36ade4532fc14e9690b42550d4b2779b.png

1.2 链表的分类

把各种类型的链表排列组合 , 总共有 8 种不同链表的结构

https://i-blog.csdnimg.cn/direct/cd6d318c3c62487aa272efce0303eec4.png

https://i-blog.csdnimg.cn/direct/5c01ba40c40f427bba650787f42709ad.png

虽然链表种类较多 , 我们只需要掌握 单向链表 , 双向链表 和循环链表 即可 。

三、链表的模拟实现

3.1 单链表的模拟实现

3.1.1 实现方式

1) 动态实现 : 通过 new 申请结点 ,然后通过 delete 释放结点 。 这种实现方式最能体现链表的特性 , 代码也很清晰 。 但是频繁的调用 new 和 delete 会有很大的时间开销。

2) 静态实现  : 利用两个数组配合模拟链表 。( 重点 )

https://i-blog.csdnimg.cn/direct/c61775a598334461857dafa411ad7bc8.png

https://i-blog.csdnimg.cn/direct/dca667f4ecca4fcb89cf5b83f86a25d0.png

可能还是不怎么理解这里,那么我们看一下下面的题目:还原一下逻辑结构

https://i-blog.csdnimg.cn/direct/ccd5aecfa69a41e6a3951a02a5a28d7a.png

https://i-blog.csdnimg.cn/direct/34035a4d4ebf47eab55bccf083e580e6.png

3.1.2 定义

1) 两个足够大的数组 , 一个用来存储数据 , 一个用来存下一个结点的位置

2)变量 h ,充当头指针 , 来表示头结点的位置

3 ) 变量 id , 为新插入结点的位置

https://i-blog.csdnimg.cn/direct/c09eafcc7ab547759b59fb3afa47a91f.png

#include <iostream>
#include <cstdio>

const int N = 1e5 + 10;

//创建数组
int e[N] , ne[N] , h , id;
 
int main()
{
		
	return 0;
}

3.1.3 头插

在链表的 头部插入一个元素

—-> 放在有效数据的前面 , 即哨兵位的后面

时间复杂度:O(1)

 //头插
 void push_front(int x)
 {
 	//把x 放在 e[N]中 
 	id++;
 	e[id] = x;
 	
 	//修改指针
	 //1.新结点指向下一个结点的位置
	 ne[id] = ne[h];
	 //2.头结点指向新结点
	 ne[h] = id; 
 	
 }

3.1.4 遍历链表

时间复杂度:O(n)

通过指针 , 访问链表中的所有元素

#include <iostream>
#include <cstdio>
using namespace std; 

const int N = 1e5 + 10;

//创建数组
int e[N] , ne[N] , h , id;
 
 
 //头插
 void push_front(int x)
 {
 	//把x 放在 e[N]中 
 	id++;
 	e[id] = x;
 	
 	//修改指针
	 //1.新结点指向下一个结点的位置
	 ne[id] = ne[h];
	 //2.头结点指向新结点
	 ne[h] = id; 
 	
 }

//遍历链表
void print()
{
	//从第一个有效结点开始遍历 
	int i = ne[h];
	//遇到空指针结束 
	while(i != 0)
	{
		cout << e[i] << " ";
		//i 不断向后移动 
		i = ne[i];	
	}	
	cout << endl << endl;
} 
  
int main()
{
	 push_front(1);
	 push_front(2);
	 push_front(3);
	 push_front(4);
	 //4 3 2 1
	 print();
	return 0;
}

3.1.5 按值查找

查询链表中是否存在元素x , 如果存在 , 返回下标 , 不存在 , 返回0 或者 -1

方法一:遍历整个链表

时间复杂度:O(n)

//方法一:遍历整个链表 
int find(int x)
{
	for(int i = ne[h] ; i ; i= ne[i])
	{
		if(e[i] == x)
			return i;
	}
	return 0;
 } 

方法二:标记数组法

时间复杂度:O(1)

https://i-blog.csdnimg.cn/direct/0384ed4436954d3ea69628f3f4caf79c.png

https://i-blog.csdnimg.cn/direct/4e50e9da561c4415a8d73e479dbf7a15.png

https://i-blog.csdnimg.cn/direct/34eb8712c56d46458fc5a77e4e0fdb29.png

3.1.6 在任意位置之后插入元素

存储位置之后 插入数据 , 插入新的元素 x

时间复杂度:O(1)

  1. id++ , 标记新结点的位置 ;同时存储新结点的位置

2)修改新结点的指针域 , 让其指向 p 的下一个位置

3)最后让 p 指向 新结点

 //在任意位置之后插入元素
 void insert(int p , int x) //这里 p 是位置
 {
 	id++;
 	e[id] = x;
 	ne[id]  = ne[p];
 	ne[p] = id;
  } 
  

3.1.7 删除任意位置之后的元素

删除存储位置 p 之后的元素

时间复杂度为O(1)

1) 直接让 p 指向下一个元素的下一个元素即可

ne[p]  = ne[ne[p]]

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

3.1.8 遗留问题

单链表为什么不实现尾插 、 尾删 、 删除任意位置元素等操作?

1) 能实现 , 但是没必要 , 因为时间复杂度是O(N)级别的 , 竞赛中不怎么用

2)使用数据结构是为了方便我们解决问题的 , 而不是添堵的

2.1.9 所有测试代码

#include <iostream>
#include <cstdio>
using namespace std; 

const int N = 1e5 + 10;

//创建数组
int e[N] , ne[N] , h , id;
 int mp[N];
 
 
 //头插
 void push_front(int x)
 {
 	//把x 放在 e[N]中 
 	id++;
 	e[id] = x;
 	mp[x] = id;//标记x 存储的为位置 
 	
 	//修改指针
	 //1.新结点指向下一个结点的位置
	 ne[id] = ne[h];
	 //2.头结点指向新结点
	 ne[h] = id; 
 	
 }

//遍历链表
void print()
{
	//从第一个有效结点开始遍历 
	int i = ne[h];
	//遇到空指针结束 
	while(i != 0)
	{
		cout << e[i] << " ";
		//i 不断向后移动 
		i = ne[i];	
	}	
	cout << endl << endl;
} 

//按值查找
//方法一:遍历整个链表 
int find(int x)
{
//	for(int i = ne[h] ; i ; i= ne[i])
//	{
//		if(e[i] == x)
//			return i;
//	}
//	return 0;

	return mp[x];
 } 
 
 //在任意位置之后插入元素
 void insert(int p , int x) //这里 p 是位置
 {
 	id++;
 	e[id] = x;
 	ne[id]  = ne[p];
 	ne[p] = id;
  } 
  
// 删除任意位置之后的元素
void erase(int p)
{
	if(ne[p])
	{
		mp[e[ne[p]]] = 0;//将p后面的元素从 mp 中删除 
		ne[p] = ne[ne[p]];//指向下一个元素的下一个元素 
	}
 } 
  
int main()
{
	 push_front(1);
	 push_front(2);
	 push_front(3);
	 push_front(4);
	 //4 3 2 1
	 print();
	
//	cout << find(4) << endl;
//	cout << find(2) << endl;
//	cout << find(99) << endl;

	insert(1,10);
	print();
	//4 3 2 1 10
	insert(4,99);
	print();
	//4 99 3 10 2 1 
	return 0;
}

3.2 双向链表的模拟实现

3.2.1 实现方式

依旧采用静态实现的方式 。

**双向链表无非就是在单链表的基础上加上一个 指向前驱的指针 ,

那就再来一个数组 充当前驱的指针域即可 。**

3.2.2 定义

https://i-blog.csdnimg.cn/direct/11d433bef88e4f87a47422129ab7bb93.png

const int N = 1e5 + 10;
int id;
int h; // 头结点
int pre[N] , ne[N] , e[N];//前后指针域 数据域 
int mp[N];


int main()
{
	
	return 0;
}

3.2.3 头插

时间复杂度为 O(1)

1) id++ , 标记新结点存储位置 ; 把 新的元素存储起来 : e[id[ = x

  1. 修改新结点的前驱指针 , 让其指向哨兵位:pre[id] = h

  2. 修改新结点的后继指针 , 让其指向哨兵位的下一个位置 : ne[id] = ne[h]

  3. 修改 y 结点的前驱指针 , 让其指向新的结点:pre[ne[h]] = id

  4. 修改哨兵位的后继指针 , 让其指向新的结点:ne[h] = id

https://i-blog.csdnimg.cn/direct/9894b0414a524810b0c6007b1e72cf2f.png

//头插
void push_front(int x)
{
	id++;
	e[id] = x;
	mp[x] = id; // 存一下x这个元素的位置 
	
	pre[id] = h;
	ne[id] = ne[h];
	pre[ne[h]] = id;
	ne[h] = id;
} 

3.2.4 遍历链表

同单链表的遍历方式一样

时间复杂度:O(N)

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

3.2.5 按值查找

时间复杂度:O(1)

#include <iostream>
#include <cstdio>
using namespace std;

const int N = 1e5 + 10;
int id;
int h; // 头结点
int pre[N] , ne[N] , e[N];//前后指针域 数据域 
int mp[N];

//头插
void push_front(int x)
{
	id++;
	e[id] = x;
	mp[x] = id; // 存一下x这个元素的位置 
	
	pre[id] = h;
	ne[id] = ne[h];
	pre[ne[h]] = id;
	ne[h] = id;
}

//遍历链表
void print()
{
	for(int i = ne[h] ; i ; i = ne[i])
	{
		cout << e[i] << " ";
	}
	cout << endl;
 } 
 
 //按值查找
 int find(int x)
 {
 	return mp[x];
 }
  
int main()
{
	push_front(1);
	push_front(2);
	push_front(3);
	push_front(4);
	//4 3 2 1
	print();
	
	cout << find(3) << endl;
	cout << find(0) << endl;
	cout << find(1) << endl; 
	return 0;
}

3.2.6 在任意位置之后插入元素

时间复杂度:O(1)

与头插操作类似 , 只是是在 存储位置 p 之后插入元素

https://i-blog.csdnimg.cn/direct/35a72f37ac3647fcb84322029e46ba71.png

 
 //在任意位置之后插入元素
 void push_back(int p ,int x)
 {
 	id++;
 	e[id] = x;
 	mp[x] = id;
 	
 	//先左指向p , 右指向 p 的后继 
 	pre[id] = p;
 	ne[id] = ne[p];
 	//再让p 的后继的左指针指向id 
 	//p的右指针指向 id 
 	pre[ne[p]] = id;
 	ne[p] = id;
  } 

3.2.7 在任意位置之前插入元素

时间复杂度:O(1)

https://i-blog.csdnimg.cn/direct/2a294d2166f84a9e86b878176424fb42.png

//在任意位置之前插入元素
void push_front(int p,int x)
{
	id++;
	e[id] = x;
	mp[x] = id;
	
	pre[id] = pre[p];
	ne[id] = p;
	ne[pre[p]] = id;
	pre[p] = id;
}

3.2.8 删除任意位置的元素

时间复杂度:O(1)

https://i-blog.csdnimg.cn/direct/d269e90b6201493c90de5a4c6ee51634.png

//删除任意位置的元素 
int rease(int p)
{
	mp[e[p]] = 0;//从标记这里删除
	
	ne[pre[p]] = ne[p];
	pre[ne[p]] = pre[p]; 
}

2.1.9 所有测试代码

#include <iostream>
#include <cstdio>
using namespace std;

const int N = 1e5 + 10;
int id;
int h; // 头结点
int pre[N] , ne[N] , e[N];//前后指针域 数据域 
int mp[N];

//头插
void push_front(int x)
{
	id++;
	e[id] = x;
	mp[x] = id; // 存一下x这个元素的位置 
	
	pre[id] = h;
	ne[id] = ne[h];
	pre[ne[h]] = id;
	ne[h] = id;
}

//遍历链表
void print()
{
	for(int i = ne[h] ; i ; i = ne[i])
	{
		cout << e[i] << " ";
	}
	cout << endl;
 } 
 
 //按值查找
 int find(int x)
 {
 	return mp[x];
 }
 
 //在任意位置之后插入元素
 void insert_back(int p ,int x)
 {
 	id++;
 	e[id] = x;
 	mp[x] = id;
 	
 	//先左指向p , 右指向 p 的后继 
 	pre[id] = p;
 	ne[id] = ne[p];
 	//再让p 的后继的左指针指向id 
 	//p的右指针指向 id 
 	pre[ne[p]] = id;
 	ne[p] = id;
  } 
  
//在任意位置之前插入元素
void insert_front(int p,int x)
{
	id++;
	e[id] = x;
	mp[x] = id;
	
	pre[id] = pre[p];
	ne[id] = p;
	ne[pre[p]] = id;
	pre[p] = id;
}

//删除任意位置的元素 
int erase(int p)
{
	mp[e[p]] = 0;//从标记这里删除
	
	ne[pre[p]] = ne[p];
	pre[ne[p]] = pre[p]; 
}
 
int main()
{
	push_front(1);
	push_front(2);
	push_front(3);
	push_front(4);
	//4 3 2 1
	print();
	
	cout << find(3) << endl;
	cout << find(0) << endl;
	cout << find(1) << endl; 
	
	insert_back(2,5);
	//4 3 2 5 1
	print();
	insert_back(0,99);
	//99 4 3 2 5 1 
	print();
	
	insert_front(4,55);
	//99 55 4 3 2 5 1
	print();
	
	erase(4);
	print();
	return 0;
}

3.3 循环链表的实现

回看之前实现的带头单向链表 。

定义 0 表示空指针 , 其实哨兵位就在 0 的位置 , 所有的结构正好成环。

循环聊表就是再原有的基础上 , 让最后一个元素指向表头即可 。