静态单链表
食用指南:
对该算法程序编写以及踩坑点很熟悉的同学可以直接跳转到代码模板查看完整代码
只有基础算法的题目会有关于该算法的原理,实现步骤,代码注意点,代码模板,代码误区的讲解
非基础算法的题目侧重题目分析,代码实现,以及必要的代码理解误区
题目描述:
-
实现一个单链表,链表初始为空,支持三种操作:
向链表头插入一个数;
删除第 k 个插入的数后面的数;
在第 k 个插入的数后插入一个数。
现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:
H x,表示向链表头插入一个数 x。
D k,表示删除第 k 个插入的数后面的数(当 k 为 0 时,表示删除头结点)。
I k x,表示在第 k 个插入的数后面插入一个数 x(此操作中 k 均大于 0)。
输出格式
共一行,将整个链表从头到尾输出。数据范围
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 -
题目来源:https://www.acwing.com/problem/content/828/
题目分析:
-
实现静态链表:
三种操作:头插 & 删除第k个插入的数 & 第k个插入的数后插入
-
为什么强调是第k个数?
- 链表的劣势:静态链表和普通链表一样,找到链表中第k个数需要遍历,非常麻烦
- 静态链表的优势:可以O(1)找到第k个插入的数
算法原理:
传统链表:
- 传统链表模型:
struct Node{
int val;
Node* next;
};
-
传统链表的劣势:
new Node()动态链表时间消耗太大了法一是选择提前Node nodes[N]
法二就是采用静态链表,也就是数组模拟链表
静态链表:
- 其实我先写的理解静态链表后代码实现的博客,但感觉解释的不是很清楚,还是从实现到理解比较容易。
1. 存储形式:
-
节点值数组:arr[N]
存放所有节点的数值域 -
节点next指向数组:next[N]
存放所有节点的指针域,next[i] = j 表示第i个节点的下一个节点是j -
节点序号:int idx;
每创建一个节点:arr[idx]记录值,next[idx]记录下一点,记录完后idx++;
初始链表为空,idx = 0; -
空节点:-1
由于所有节点以数组存储,数组下标从0开始,所以当next[i] = -1 表明第i个节点下一节点为0
毕竟没有arr[-1],也没有next[-1]; -
链表头:int head;
初始链表为空,head = -1;
head对于链表来说就像top对于栈,前者永远指向链表的第一个元素,后者永远指向栈的顶为了保持head指向链表头,每创建一个新节点后,马上head从原表头移动到新表头
可能你现在理解上面这句话有点难,我画个示意图:
2. 深刻意义:
-
单链表的创建很有意思,所有节点创建都是arr[idx]= ; next[idx]= ; idx++;
那么这个链表序号idx,其实是所有节点的物理地址,虽然arr[1] 和 arr[2]物理地址相邻
但是逻辑相邻需要看next[],当next[1] = 3,next[2] = 5的时候,这两个节点逻辑地址相去甚远另一方面,物理地址相邻的两个节点,创建的顺序也是相邻的
arr[i]就是表示第i+1个创建的节点的值,
说创建顺序是i的点的查找,时间复杂度必然O(1)
说逻辑顺序/链表上第i个点的查找,时间复杂度必然是O(n),需要遍历一遍链表 -
单链表的连接:
就像上图,所有新节点都是连接到原本链表的head节点
arr[idx] = val; next[idx] = head; head = idx;
代码写作:
1. 头插法:
- 每次创建的新节点下一节点是head:
const int N = 100010;
int head = -1, idx = 0;
int arr[N], next[N];
void head_insert(int x){
arr[idx] = x;
next[idx] = head;
head = idx++;
}
2. 指定插:
- 将x值插在第k个插入的节点之后:
void insert(int x, int k){
arr[idx] = x;
//从0开始存储,第k个插入的数其实是arr[k-1]
next[idx] = next[k-1];
next[k-1] = idx;
idx++;
}
-
指定插入其实和普通链表一样,先将k节点后面的链条连接到一个节点后面,之后将这个新链条连接在k节点后面
-
经典猜谜:把大象放进冰箱有几步?
-
创建一个节点
-
移植后续链条到新节点上
-
把新链条复原到原节点上
-
3. 删除节点:
- 删除分两种,删头节点 / 其他节点
原因就是头节点遍历的时候用,head一定要有所指向
//删除第k个插入的节点后一个节点
void remove(int k){
if(next[k-1] == head){
head = next[head];
//发现不对劲了吗?你的下一个节点是头节点,那你是什么?头节点的头节点?
//哈哈,其实这个if永远不会执行,但是需要留意一下哦
}else{
next[k-1] = next[next[k-1]];
}
}
- 由于数组开的很长,且一个节点也就是4Byte,所以不必释放空间
- 示意图:
4. 遍历:
- 理解遍历的每一步,那你就理解了静态单链表
int t = head;
while(t != -1){
cout <<arr[t];
t = next[t];
}
代码误区:
1. 四句话拿捏静态单链表,看看你悟了没:
- 总结静态链表就四句话,下一节点索引是next[i],头节点索引是head,空节点索引是-1,所有链表的最后一个节点都指向-1
2. 静态链表模拟的链表的数值域只能有一个属性吗?
- 当然不是
- struct{};中可以给链表加很多属性
其实你再创建其他数组:string[],int[],char[],就能记录更多属性
毕竟查找的顺序是看next[]这一个数组,其他所有属性数组都统一索引于next[]数组
包括最重要的arr[],本身也是idx物理相邻存储的,但是插入顺序不一样,链表逻辑地址不同
3. 别忘了删除节点分两种情况啊!!!
- 删除头节点一行代码:head = next[head];
- 删除非头节点一行代码:next[k-1] = next[next[k-1]];
本篇感想:
- 几个方法中最常用的就是头插,拉链法实现哈希表也使用的头插,对,我写本篇就是为了手动实现哈希
- 在算法题中,肯定能用还是用静态链表,毕竟代码写的比结构体快
- 今天学了一点Android studio的WebView,做了做学校的DDL,所以更新的比较晚,感恩今天周五,我可以熬夜更新
- 看完本篇博客,恭喜已登 《练气境-中期》
距离登仙境不远了,加油