NO.45十六届蓝桥杯备战|链表和list|静态模拟实现|头插|遍历|按值查找|pos之后插入|pos之前插入|删除pos后元素|动态链表list|4道练习(C++)

链表的概念

链表的定义

线性表的链式存储就是链表。
它是将元素存储在物理上任意的存储单元中,由于⽆法像顺序表⼀样通过下标保证数据元素之间的逻辑关系,链式存储除了要保存数据元素外,还需额外维护数据元素之间的逻辑关系,这两部分信息合称结点(node)。即结点有两个域:保存数据元素的数据域和存储逻辑关系的指针域

链表的分类

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

  • 单向
    • 不带头节点
      • 不带头节点的单链表
      • 不带头节点的单向循环链表
    • 带头节点
      • 带头结点的单链表
      • 带头节点的单向循环链表
  • 双向
    • 不带头节点
      • 不带头节点的双向链表
      • 不带头节点的双向循环链表
    • 带头节点
      • 带头结点的双向链表
      • 带头节点的双向循环链表

单链表的模拟实现

实现⽅式

链表的实现⽅式分为动态实现和静态实现两种

  • 动态实现是通过new申请结点,然后通过delete释放结点的形式构造链表。这种实现⽅式最能体现链表的特性;
  • 静态实现是利⽤两个数组配合来模拟链表。第⼀次接触可能⽐较抽象,但是它的运⾏速度很快,在算法竞赛中会经常会使⽤到
定义-创建-初始化
  1. 两个⾜够⼤的数组,⼀个⽤来存数据,⼀个⽤来存下⼀个结点的位置
  2. 变量 h ,充当头指针,表⽰头结点的位置
  3. 变量 id ,为新来的结点分位置
const int N = 1e5 + 10;  
int h; // 头指针  
int id; // 下⼀个元素分配的位置  
int e[N], ne[N]; // 数据域和指针域  
// 下标 0 位置作为哨兵位  
// 其中 ne 数组全部初始化为 0,其中 ne[i] = 0 就表⽰空指针,后续没有结点  
// 当然,也可以初始化为 -1 作为空指针,看个⼈爱好  
/*  
e[i] 和 ne[i] 是绑定在⼀起使⽤的,也有⼀种写法是定义⼀个结构体,把这两个变量放在⼀起,⽐如:  
struct node
{  
	int e, ne;  
}list[N];  
但是,定义成结构体之后,代码书写不⽅便。我们只要知道 e[i] 和 ne[i] 是绑定在⼀起使⽤的即可  
*/
头插

这是链表中最常⽤也是使⽤最多的操作,后续树和图的存储中的邻接表以及链式前向星就会⽤到这个操作

// 头插  
void push_front(int x)  
{  
	// 先把 x 放在⼀个格⼦⾥⾯  
	id++;  
	e[id] = x;  
	// 修改指针,顺序不能颠倒!  
	// 1. x 的右指针指向哨兵位的后继  
	// 2. 哨兵位的右指针指向 x  
	ne[id] = ne[h];  
	ne[h] = id;  
}

时间复杂度:
只涉及指针的修改,时间复杂度为O(1)

遍历链表
// 打印链表  
void print()  
{  
	// 定义⼀个指针从头结点开始  
	// 通过 ne 数组逐渐向后移动  
	// 直到遇到空指针  
	for(int i = ne[h]; i; i = ne[i])  
	{  
		cout << e[i] << " ";  
	}  
	cout << endl << endl;  
}

时间复杂度:
遍历整个链表,时间复杂度为O(N)

按值查找

解法⼀:遍历整个链表即可
解法⼆:如果存储的值数据范围不⼤,可以⽤哈希表优化。

int mp[N]; // mp[i] 表⽰ i 在这个元素存放的位置  
/*  
push_front 和 insert 的时候,打上标记  
mp[x] = id; // x 这个元素存放的位置是 id  
erase 的时候,消除标记  
mp[x] = 0;  
*/  
// 查询值为 x 的元素存储的位置  
int find(int x) // 注意,这⾥传⼊的是元素的值  
{  
	// 策略⼀:先找到 x,然后返回 x 后⾯的元素
	for(int i = ne[h]; i; i = ne[i]) // 遍历链表  
	{  
		if(e[i] == x) // 如果找到 x  
		{  
			return i;  
		}  
	}  
	// 找不到就返回 0  
	return 0;  
	// // 策略⼆:使⽤哈希表优化  
	return mp[x]; // mp[x] ⾥⾯就存着 x 这个元素存储的位置下标  
}

时间复杂度:
遍历整个链表查询:时间复杂度O(N) ;
使⽤标记数组优化:时间复杂度O(1)

在任意位置之后插⼊元素
// 在存储位置为 p 的元素后⾯,插⼊⼀个元素 x  
void insert(int p, int x) // ⼀定要注意,这⾥的 p 是位置,不是元素  
{  
	id++; // x 这个元素分配的位置  
	e[id] = x; // 将 x 放在 id 位置处  
	ne[id] = ne[p]; // x 指向 p 的后⾯  
	ne[p] = id; // p 指向 x  
}

时间复杂度:
只有常数级别的操作,时间复杂度为O(1)

删除任意位置之后的元素
// 删除存储位置为 p 后⾯的元素  
void erase(int p) // 注意 p 表⽰元素的位置  
{  
	if(ne[p])  
	{  
		mp[e[ne[p]]] = 0; // 将 p 后⾯的元素从 mp 中删除  
		ne[p] = ne[ne[p]]; // 指向下⼀个元素的下⼀个元素  
	}  
}

时间复杂度:
只⽤修改指针,时间复杂度为O(1)

所有测试代码
#include <iostream>  
using namespace std;  
const int N = 1e5 + 10;  
// 创建  
int e[N], ne[N], h, id;  
int mp[N]; // mp[i] 表⽰ i 这个数存储的位置  
// 遍历链表  
void print()
{  
	for(int i = ne[h]; i; i = ne[i])  
	{  
		cout << e[i] << " ";  
	}  
	cout << endl << endl;  
}  
// 头插  
void push_front(int x)  
{  
	id++;  
	e[id] = x;  
	mp[x] = id; // 标记 x 存储的位置  
	// 先让新结点指向头结点的下⼀个位置  
	// 然后让头结点指向新来的结点  
	ne[id] = ne[h];  
	ne[h] = id;  
}  
// 按值查找  
int find(int x)  
{  
	// // 解法⼀:遍历链表  
	// for(int i = ne[h]; i; i = ne[i])  
	// {  
	// if(e[i] == x) return i;  
	// }  
	// return 0;  
	// 解法⼆:⽤ mp 数组优化  
	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 后⾯的元素
void erase(int p)  
{  
	if(ne[p]) // 当 p 不是最后⼀个元素的时候  
	{  
		mp[e[ne[p]]] = 0; // 把标记清空  
		ne[p] = ne[ne[p]];  
	}  
}  
int main()  
{  
	for(int i = 1; i <= 5; i++)  
	{  
		push_front(i);  
		print();  
	}  
	//cout << find(1) << endl;  
	//cout << find(5) << endl;  
	//cout << find(6) << endl;  
	// insert(1, 10);  
	// print();  
	// insert(2, 100);  
	// print();  
	erase(2);  
	print();  
	erase(4);  
	print();  
	return 0;  
}

双向链表的模拟实现

实现⽅式

依旧采⽤静态实现的⽅式。
双向链表⽆⾮就是在单链表的基础上加上⼀个指向前驱的指针,那就再来⼀个数组,充当指向前驱的指针域即可
![[Pasted image 20250317140956.png]]

const int N = 1e5 + 10;  
int h; // 头结点  
int id; // 下⼀个元素分配的位置  
int e[N]; // 数据域  
int pre[N], ne[N]; // 前后指针域  
// h 默认等于 0,指向的就是哨兵位  
// 此时链表为空,没有任何⼏点,因此 ne[h] = 0
头插
// 头插  
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;  
}

时间复杂度:
只涉及指针的修改,时间复杂度为O(1)

遍历链表

直接⽆视 prev 数组,与单链表的遍历⽅式⼀致

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

时间复杂度:
遍历整个链表,时间复杂度O(N)

按值查找

直接使⽤mp数组优化

// 查找元素 x 在链表中存储的位置  
int find(int x)  
{  
	// ⽤ mp 优化  
	return mp[x];
}

时间复杂度:
直接拿值,时间复杂度为O(1)

在任意位置之后插⼊元素
// 在存储位置为 p 的元素后⾯,插⼊⼀个元素 x  
void insert_back(int p, int x)  
{  
	id++;  
	e[id] = x;  
	mp[x] = id; // 存⼀下 x 这个元素的位置  
	// 先左指向 p,右指向 p 的后继  
	pre[id] = p;  
	ne[id] = ne[p];  
	// 先让 p 的后继的左指针指向 id  
	// 再让 p 的右指针指向 id  
	pre[ne[p]] = id;  
	ne[p] = id;  
}

时间复杂度:
只涉及指针的修改,时间复杂度为O(1)

在任意位置之前插⼊元素
// 在存储位置为 p 的元素前⾯,插⼊⼀个元素 x
void insert_front(int p, int x)  
{  
	id++;  
	e[id] = x;  
	mp[x] = id; // 存⼀下 x 这个元素的位置  
	// 先左指针指向 p 的前驱,右指针指向 p  
	pre[id] = pre[p];  
	ne[id] = p;  
	// 先让 p 的前驱的右指针指向 id  
	// 再让 p 的左指针指向 id  
	ne[pre[p]] = id;  
	pre[p] = id;  
}

时间复杂度:
只涉及指针的修改,时间复杂度为O(1)

删除任意位置的元素
// 删除下标为 p 的元素  
void erase(int p)  
{  
	mp[e[p]] = 0; // 从标记中移除  
	ne[pre[p]] = ne[p];  
	pre[ne[p]] = pre[p];  
}

时间复杂度:
只涉及指针的修改,时间复杂度为O(1)

所有测试代码
#include <iostream>  
using namespace std;  
const int N = 1e5 + 10;  

// 创建双链表  
int e[N], ne[N], pre[N], id, h;  
int mp[N]; // mp[i] 表⽰:i 这个值存储的位置  

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

// 头插  
void push_front(int x)  
{  
	id++;  
	e[id] = x;  
	mp[x] = id;  
	// 先修改新来结点的左右指针  
	pre[id] = h;  
	ne[id] = ne[h];  
	// 修改哨兵位下⼀个结点的左指针  
	pre[ne[h]] = id;  
	ne[h] = id;  
}  

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

// 在任意位置 p 的后⾯插⼊新的元素 x  
void insert_back(int p, int x)  
{  
	id++;  
	e[id] = x;
	mp[x] = id;  
	pre[id] = p;  
	ne[id] = ne[p];  
	pre[ne[p]] = id;  
	ne[p] = id;  
}  

// 在任意位置 p 的前⾯插⼊新的元素 x  
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;  
}  

// 删除任意位置 p 的元素  
void erase(int p)  
{  
	mp[e[p]] = 0; // 把标记清空  
	ne[pre[p]] = ne[p];  
	pre[ne[p]] = pre[p];  
}  

int main()  
{  
	for(int i = 1; i <= 5; i++)  
	{  
		push_front(i);  
		print();  
	}  
	//cout << find(3) << endl;  
	//cout << find(5) << endl;  
	//cout << find(0) << endl;  
	// insert_front(2, 22);  
	// print();  
	// insert_front(3, 33);  
	// print();  
	// insert_front(4, 44);
	// print();  
	erase(2);  
	print();  
	erase(4);  
	print();  
	return 0;  
}

循环链表的模拟实现

循环链表就是在原有的基础上,让最后⼀个元素指向表头即可

动态链表-list

new和delete是⾮常耗时的操作,
在算法⽐赛中,⼀般不会使⽤new和delete去模拟实现⼀个链表。
⽽STL⾥⾯的list的底层就是动态实现的双向循环链表,增删会涉及new和delete,效率不⾼,竞赛中⼀般不会使⽤,这⾥了解⼀下即可

创建list
#include <list>  
using namespace std;  
int main()  
{  
	list<int> lt; // 创建⼀个存储 int 类型的链表  
	return 0;  
}
push_front/push_back
  1. push_front :头插;
  2. push_back :尾插。
    时间复杂度:O(1)
void testadd()  
{  
	list<int> lt;  
	// 尾插  
	for(int i = 1; i <= 5; i++)  
	{  
		lt.push_back(i);  
		print(lt);  
	}  
	// 头插  
	for(int i = 1; i <= 5; i++)  
	{  
		lt.push_front(i);  
		print(lt);  
	}  
}
pop_front/pop_back
  1. pop_front :头删
  2. pop_back :尾删
    时间复杂度:O(1)
// 删  
void testdelete()  
{  
	list<int> lt;
	// 尾插  
	for(int i = 1; i <= 5; i++)  
	{  
		lt.push_back(i);  
	}  
	// 头插  
	for(int i = 5; i >= 1; i--)  
	{  
		lt.push_front(i);  
	}  
	// 头删  
	for(int i = 1; i <= 3; i++) lt.pop_front();  
	// 尾删  
	for(int i = 1; i <= 3; i++) lt.pop_back();  
	print(lt);  
}
所有测试代码
#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 <= 3; i++)  
	{  
		l.pop_front();  
	}  
	print(l);  
	// 尾删  
	for(int i = 1; i <= 3; i++)  
	{  
		l.pop_back();  
	}  
	print(l);  
	
	return 0;  
}

练习

B3630 排队顺序 - 洛谷

本题相当于告诉了我们每⼀个点的后继,使⽤静态链表的存储⽅式能够很好的还原这个队列。
数组中[1, n] 的下标可以当做数据域,根据题意修改指针域即可

#include <bits/stdc++.h>
using namespace std;

const int N = 1e6 + 10;
int n, h;
int ne[N];

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (int i = 1; i <= n; i++) cin >> ne[i];
    cin >> h;
    for (int i = h; i; i = ne[i])
    {
        cout << i << " ";        
    }
    
    return 0;
}
B3631 单向链表 - 洛谷

链表模板题,直接实现⼀个单链表即可

#include <bits/stdc++.h>
using namespace std;

const int N = 1e5 + 10, M = 1e6 + 10;
int h, id, e[N], ne[N];
int mp[M];
int q;
int op, x, y;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);

    id++;
    e[id] = 1;
    mp[1] = id;
    
    cin >> q;
    while (q--)
    {
        cin >> op >> x;
        int p = mp[x]; // x存的位置
        if(op == 1)
        {
            cin >> y;

            id++;
            e[id] = y;
            ne[id] = ne[p];
            ne[p] = id;

            mp[y] = id;
        }
        else if (op == 2)
        {
            cout << e[ne[p]] << endl;
        }
        else
        {
            ne[p] = ne[ne[p]];
        }
            
    }

    return 0;
}
P1160 队列安排 - 洛谷

频繁的在某⼀个位置之前和之后插⼊元素,因此可以⽤双向循环的链表来模拟

#include <bits/stdc++.h>
using namespace std;

const int N = 1e5 + 10;

int h, pre[N], ne[N];
int n, m;
bool st[N]; //表示当前元素是否已经被删除

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    pre[1] = h;
    ne[h] = 1;

    for (int i = 2; i <= n; i++)
    {
        int k, p; cin >> k >> p;
        if (p == 0)
        {
            ne[i] = k;
            pre[i] = pre[k];
            ne[pre[k]] = i;
            pre[k] = i;
        }
        else
        {
            ne[i] = ne[k];
            pre[i] = k;
            pre[ne[k]] = i;
            ne[k] = i;
        }
    }
    cin >> m;
    while (m--)
    {
        int x; cin >> x;
        if(st[x] == true) continue;
        ne[pre[x]] = ne[x];
        pre[ne[x]] = pre[x];
        st[x] = true;
    }
    for (int i = ne[h]; i; i = ne[i])
    {
        cout << i << " ";        
    }
    
    return 0;
}
P1996 约瑟夫问题 - 洛谷

用循环链表来模拟整个过程

  1. 因为要执行删除操作,所以计数的时候少算一次
  2. 初始化的时候可以让t = n,方便统一处理
#include <bits/stdc++.h>
using namespace std;

const int N = 110;
int n, m;
int ne[N];

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);

    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++)
    {
        for (int j = 1; j < m; j++) //让t向后移动m-1次    
        {
            t = ne[t];        
        }
        cout << ne[t] << " ";
        ne[t] = ne[ne[t]];
    }
    
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值