一.链表与数组
链表作为 C 语言中一种基础的数据结构,在平时写程序的时候用的并不多,但在操作系统里面使用的非常多。不管是RTOS还是Linux等使用非常广泛,所以必须要搞懂链表,链表分为单向链表和双向链表,单向链表很少用,使用最多的还是双向链表。单向链表懂了双向链表自然就会了(网上这样说,但我啃了链表三天哭死)。
定义:
链表是一种物理存储上非连续,数据元素的逻辑顺序通过链表中的指针链接次序,实现的一种线性存储结构。
特点:
链表由一系列节点(链表中每一个元素称为节点)组成,节点在运行时动态生成 (malloc),每个节点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个节点地址的指针。
链表最大的作用是通过节点把离散的数据链接在一起,组成一个表(听起来像废话),这大概就是链表 的字面解释了吧。 链表常规的操作就是节点的插入和删除,为了顺利的插入,通常一条链 表我们会人为地规定一个根节点,这个根节点称为生产者。通常根节点还会有一个节点计 数器,用于统计整条链表的节点个数。 双向链表与单向链表的区别就是节点中有两个节点指针,分别指向前后两个节点,其 它完全一样。
下面找到一个数组与链表的对比:
上面我们看的实际上是针对正规链表也就是使用指针来实现的链表,而我们这次使用的二踢脚不对使用的是数组代替指针进行操作, 但本质上是相同的。
二.单向链表
模板:
// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
int head, e[N], ne[N], idx;
// 初始化
void init()
{
head = -1;
idx = 0;
}
// 在链表头插入一个数a
void insert(int a)
{
e[idx] = a, ne[idx] = head, head = idx ++ ;
}
// 将头结点删除,需要保证头结点存在
void remove()
{
head = ne[head];
}
例题:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
map<int,int> e;
int main(){
int n;
cin>>n;
while(n--){
int a,b,c;
cin>>a>>b;
switch(a){
case 1:
cin>>c;
e[c]=e[b];// 插入<y, y的指向>, y的指向为前一个数x的指向
e[b]=c;// 更新x的指向,使其指向y
break;
case 2:
cout<<e[b]<<endl;
break;
case 3:
int cc=e[b];
e[b]=e[cc];
e.erase(cc);
}
}
system("pause");
return 0;
}
三.双向链表
模版:
// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;
// 初始化
void init()
{
//0是左端点,1是右端点
r[0] = 1, l[1] = 0;
idx = 2;
}
// 在节点a的右边插入一个数x
void insert(int a, int x)
{
e[idx] = x;
l[idx] = a, r[idx] = r[a];
l[r[a]] = idx, r[a] = idx ++ ;
}
// 删除节点a
void remove(int a)
{
l[r[a]] = l[a];
r[l[a]] = r[a];
}
例题一:
一定要注意模版中针对的是下标k进行插入,不是值,因此我们使用一个桶来存储插入值的下标。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=500010;
//r[N]储存该结点后一结点的下标
//l[N]储存该结前一结点的下标
//e[N]储存该点的值
//idx表示插入的次数(即下标)
int e[N];
int l[N];
int r[N];
map<int,int> mp;//存储值的下标
int idx;
void init(){
//以下标为0的结点为头结点,以下标为1的结点为尾结点
//初始化
r[0]=1;
l[1]=0;
idx=2;//由于已经进行了两次插入头尾结点的操作,所以idx初始化为2(前面是0,1)
}
//将一结点插入到下标为k的结点的前或后位置处,进行4次指针的变换
//如果要将结点插入至下标为k的结点之前,将add内的实参赋值为(l[k],x)即可
void add(int k,int x){
e[idx]=x;//将待插值x插入e[idx]
mp[e[idx]]=idx;
//将待插结点与前后两结点连接
l[idx]=k;//将k赋值给插入结点的l[idx]
r[idx]=r[k];//将下标为k的结点的r[k]赋值给插入结点的r[idx]
//下面两步操作不能交换顺序
l[r[k]]=idx;
r[k]=idx;
idx++;
}
//将下标为a的数删除,进行两次指针变换即可
void remove(int a){
//将待删点的下一结点与待删点的前一结点相互连接
l[r[a]]=l[a];
r[l[a]]=r[a];
}
int main(){
int n;
cin>>n;
init();
for(int i=1;i<=n;i++){
int x;
cin>>x;
add(l[1],x);
}
int t;
cin>>t;
while(t--){
int a;
cin>>a;
int x;
cin>>x;
//int cnt=0;
/*for(int i=r[0];i!=1;i=r[i]){
if(e[i]==x){
cnt=i;
}
}*/
//cout<<cnt<<" ";
switch(a){
case 1:
int y;
cin>>y;
add(mp[x],y);
break;
case 2:
remove(mp[x]);
break;
}
}
for(int i=r[0];i!=1;i=r[i]){
cout<<e[i]<<" ";
}
system("pause");
return 0;
}
例题二
实现一个单链表,链表初始为空,支持三种操作:
向链表头插入一个数;
删除第 k个插入的数后面的数;
在第 k 个插入的数后插入一个数。
现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。
注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2个插入的数,…第 n 个插入的数。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:
H x,表示向链表头插入一个数 x。
D k,表示删除第 k 个插入的数后面的数(当 k 为 00 时,表示删除头结点)。
I k x,表示在第 k 个插入的数后面插入一个数 x(此操作中 k均大于 00)。
输出格式
共一行,将整个链表从头到尾输出。
数据范围
1≤M≤100000
所有操作保证合法。
输入样例:
10
H 9
I 1 1
D 1
D 0
H 6
I 3 6
I 4 5
I 4 5
I 3 4
D 6
输出样例:
6 4 6 5
#include <iostream>
using namespace std;
const int M = 1e5 + 10;
int e[M],l[M],r[M],idx;
int m;
void init(){
r[0] = 1;
l[1] = 0;
idx = 2; // idx从2开始,所以删除第k个插入的数是remove(k+1)
}
void add(int k,int x){
e[idx] = x;
r[idx] = r[k];
l[idx] = k;
l[r[k]] = idx;
r[k] = idx;
idx ++;
}
void remove(int k){
r[l[k]] = r[k];
l[r[k]] = l[k];
}
int main(){
cin>>m;
init();
while(m--){
string op;
cin>>op;
if(op == "R"){
int x;
cin>>x;
add(l[1],x);
}
else if( op == "L"){
int x;
cin>>x;
add(0,x);
}
else if( op == "D"){
int k;
cin>>k;
remove(k+1); // idx从2开始,所以删除第k个插入的数是remove(k+1)
}
else if (op == "IL"){
int k,x;
cin>>k>>x;
add(l[k+1],x);
}
else{
int k,x;
cin>>k>>x;
add(k+1,x);
}
}
for(int i=r[0];i!=1;i = r[i]) cout<<e[i]<<" ";
}
终于在我三天不懈之努力才弄出来这篇可以弄懂的数组方式实现的链表,虽然五一假期,但代码人永不退缩(真累啊我哭死)!!!