链表
链表
数组很好用,但是无法在任意位置插入或删除元素;
所以我们就需要另外一种数据结构来实现这种操作,于是链表就诞生了;
链表支持在任意位置插入或删除,但只能按顺序依次访问其中的元素。
面试中一般用结构体加指针实现动态链表使用较多;
但是这种方法在笔试题和算法题里面最好不要用,操作是非常慢的,每次都要new一个Node;
所以用数组模拟单链表,用的最多的是邻接表,邻接表存储树和图;
单链表只往后看不往前看,所以每次回顾会直接从头开始;
数组模拟的双链表,主要用来优化某些题目;
单链表
一般为静态链表,这里演示静态链表的头插法;
头插法
1.初始化
设定一个头节点head和一个下标idx
最开始的时候,链表的头节点head要指向-1,为的就是在后面进行不断操作后仍然可以知道链表是在什么时候结束;
head其实本质上就是一个特殊的指针,在初始化的时候指向-1,来表示链表离没有内容;
而在链表里有元素的时候,它变成了一个指向第一个元素的指针;
idx则要等于0,即数组里的第一个元素下标;
idx就比较有点说法,它在一开始的时候,作为链表的下标,让我们方便寻找;
而在链表进行各种插入,删除等操作的时候,给每一个操作需要插入的元素作为一个临时的下标;
因此下标会显得很混乱,但是由于后文要提到的ne[]数组的原因,下标对于后面遍历的影响就很小了;
2.插入到头节点
和链表中间插入的区别就在于它有head头节点;
设置ne[]和e[]数组;
链表里,每个元素都绑定了两个属性————元素本身的值和下一个元素的下标;
所以设e[i]是下标为i的元素内部的值,ne[i]是下标为i的元素下一个元素的下标;
先将值,假设为x放到当前idx指向的e[]位置,即e[idx] = x;
因为是头插,所以idx的下一位必定是head,即ne[idx] = head;
注意此时链表至少有x这一个元素,所以头指针不能再指向-1,所以指向第一个元素的下标,即让head = idx;
而且因为idx下标的元素已经填入,所以idx++;
3.x插入到下标为k的点的后面
因为每个元素都有两个属性,假设x要插入到下标为i和j的中间,那么ne[i]就要指向x插入的位置,而ne[x]就会指向j的位置;
先将元素插进去,e[idx] = x;
再让元素x配套的指针,指向它要占位的元素的下一个位置,ne[idx] = ne[k];
接着让上一个元素k的指针指向自己,ne[k] = idx;
最后让idx往前走一格,idx++;
4.将下标是k的点后面的一个元素删掉
非常简单,只要将k的ne[k]跳过当前元素,直接指向k+2就行了;
元素本身还存在,只是不加入链表里了;
ne[k] = ne[ne[k]];
例题
https://www.acwing.com/problem/content/828/
实现一个单链表,链表初始为空,支持三种操作:
1.向链表头插入一个数;
2.删除第 k 个插入的数后面的数;
3.在第 k 个插入的数后插入一个数。
现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。
题解
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n;
int h[N], e[N], ne[N], head, idx;
//1.初始化
void init()
{
//最开始的时候,链表的头节点head要指向-1,idx指向第一个元素的下标
head = -1;
idx = 0;
}
//2.x插入到头节点上
void int_to_head(int x)
{
e[idx] = x;//先将值放进去
ne[idx] = head;//head作为一个指针指向空节点,现在ne[idx] = head;
head = idx;//head现在表示指向第一个元素了,它不在是空指针
idx ++;//指针向下移一位,为下一次插入元素做准备。
}
//3.x插入到下标为k的点的后面
void add(int k, int x)
{
e[idx] = x;//先将元素插进去
ne[idx] = ne[k];//让元素x配套的指针,指向它要占位的元素的下一个位置
ne[k] = idx;//让原来元素的指针指向自己
idx ++;//将idx向后挪
}
//4.将下标是k的点后面的点个删掉
void remove(int k)
{
ne[k] = ne[ne[k]];//让k的指针指向,k下一个人的下一个人,那中间的那位就被挤掉了。
}
int main()
{
cin >> n;
init();//初始化
for (int i = 0; i < n; i ++ )
{
char s;
cin >> s;
if (s == 'H')
{
int x;
cin >> x;
int_to_head(x);
}
if (s == 'D')
{
int k;
cin >> k;
if (k == 0) head = ne[head];//删除头节点
else remove(k - 1);//注意删除第k个输入后面的数,那函数里放的是下标,k要减去1
}
if (s == 'I')
{
int k, x;
cin >> k >> x;
add(k - 1, x);//同样的,第k个数,和下标不同,所以要减1
}
}
for (int i = head; i != -1; i = ne[i])
cout << e[i] << ' ' ;
cout << endl;
return 0;
}
双链表
单链表的变种,每个元素拥有三个属性,本身的值e[i],左边/前面的下标l[i],右边/后面的下标r[i];
同时链表既有头指针head,还有尾指针tail;
1.初始化
先建立一对头尾指针;
在初始化中,由于只有两个元素,下标为0时只有右边的元素,下标为1时只有左边元素;
所以r[0] = 1 , l[1] = 0;
又因为0和1已经用过了,所以下标从idx = 2开始使用;
2.在下标k的后面插一个元素x
假如idx要想在下标为i和j中插个队,需要将i的右指针指向idx,idx的左指针指向i,idx的右指针指向j,j的左指针指向idx;
先赋值,e[idx] = x;
因为在k的后面,所以idx的右指针会指向和k右指针一样的数字,r[idx] = r[k];
而idx的左指针会直接指向k,l[idx] = k;
下面两行不能反过来写
再让k之前右指针指向的点的左指针指向idx,l[r[k]] = idx;
最后让k的右指针指向idx,r[k] = idx;
3.在k的前边插入一个元素y
跟往k的后面插x差不多,所以这个往往可以和往k的后面直接合并;
先赋值,e[idx] = y;
同样不能写反
因为k以前的前面的元素的右指针指向的是k,所以现在要改为指向x,r[l[k]] = idx;
而且在k的前面,所以k的左指针指向idx,l[k] = idx;
再把当前idx的左右指针设置一下,r[idx] = k , l[idx] = l[k];
4.删除下标为k的点
也很简单,只要跳过,不用删除;
让k左元素的右边指向k的右元素,r[l[k]] = r[k];
让k右元素的左边指向k的左元素,l[r[k]] = l[k];
例题
https://www.acwing.com/problem/content/description/829/
实现一个双链表,双链表初始为空,支持 5 种操作:
1.在最左侧插入一个数;
2.在最右侧插入一个数;
3.将第 k 个插入的数删除;
4.在第 k 个插入的数左侧插入一个数;
5.在第 k 个插入的数右侧插入一个数
现在要对该链表进行 M 次操作,进行完所有操作后,从左到右输出整个链表。
题解
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int m;
int e[N], l[N], r[N], idx;
//1. 在节点k的右边插入一个数x,左右插入其实可以合并,如果想不明白可以额外再写一个从左边插;
void insert(int k, int x)
{
e[idx] = x;
l[idx] = k, r[idx] = r[k];
l[r[k]] = idx, r[k] = idx ++ ;
}
//2. 删除节点k
void remove(int k)
{
l[r[k]] = l[k];
r[l[k]] = r[k];
}
int main()
{
cin >> m;
// 初始化,0是左端点,1是右端点
r[0] = 1, l[1] = 0;
idx = 2;
while (m -- )
{
string op;
cin >> op;
int k, x;
if (op == "L")
{
cin >> x;
insert(0, x);
}
else if (op == "R")
{
cin >> x;
insert(l[1], x);
}
else if (op == "D")
{
cin >> k;
remove(k + 1);
}
else if (op == "IL")
{
cin >> k >> x;
insert(l[k + 1], x);
}
else
{
cin >> k >> x;
insert(k + 1, x);
}
}
for (int i = r[0]; i != 1; i = r[i])
cout << e[i] << ' ';
cout << endl;
return 0;
}

浙公网安备 33010602011771号