链表理论基础
- 一种用指针串联在一起的线性结构
- 节点 = 数据域 + 指针域
- 分类
-
单链表
-
双链表
-
循环链表
-
- 存储方式:散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理
链表的定义
public class ListNode {
// 结点的值
int val;
// 下一个结点
ListNode next;
// 节点的构造函数(无参)
public ListNode() {
}
// 节点的构造函数(有一个参数)
public ListNode(int val) {
this.val = val;
}
// 节点的构造函数(有两个参数)
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
链表的操作
链表中增删节点操作的时间复杂度都为
删除节点
删除D节点,只需要把C节点的next指针指向E
Java有内存回收机制,不用手动释放D节点空间
添加节点
- 要插入的节点F的next指针指向插入位置节点D
- 插入位置前的节点C的next指针指向要插入的节点F
链表与数组的比较
| 插入/删除 | 查询 | 适用场景 | |
|---|---|---|---|
| 数组 | 数据量固定,较少增删,频繁查询 | ||
| 链表 | 数据量不固定,频繁增删,较少查询 |
LeetCode 203 移除链表元素
思路
题目要求移除值为val的所有节点,且要返回修改后链表的头节点。考虑到头节点也可能被更改,且删除操作与其他节点不同,可以引入虚拟头节点dummyHead作为头节点的前序节点,也是一个始终不变的头节点。
虚拟头节点
在链表中,操作当前节点必须找前一个结点,而头节点没有前序节点,这就造成了头结点的特殊性。设置虚拟头节点后,头节点操作与其他节点无异,简化代码。 返回头节点时,要返回dummyHead的后继,之前的head可能已经无效
难点
在删除某个节点后,指针会指向下一个新节点。此时两个指针都不应该再移动,需要继续检查当前节点。只有节点不需要被删除时,两个指针才都需要向前移动
LeetCode 707 设计链表
思路
由于之前练习过设计单链表,这次设计一下双链表 MyLinkedList类:属性head,记录链表的头节点 MyListNode类:属性val,prev,next记录节点的值与前后继
初始化链表MyLinkedList()
把属性head初始化为空
根据索引读get(int val)
读节点的值时,头节点操作与其他节点并无分别,无需使用虚拟头节点 只需把指针向后移动到索引位置读取
难点
题设说明可能存在无效的索引输入。读值需要操作的节点即为当前指针,所以循环中需要添加指针是否为空的判断条件 如果指针为空,直接返回。
在链表头部插入节点addAtHead(int val)
头插节点的操作并无特殊性,故不使用虚拟头节点
- 新建节点
- 把节点node的next指针指向现在的头节点
- 如果头节点不为空,将其prev节点指向新节点node
- 把新节点node置为头节点
在链表尾部插入节点addAtTail(int val)
在尾部插入节点,同样有可能插入的是头节点,故引入虚拟头节点以简化代码
⚠️注意:双链表引入虚拟头节点时,要检查头节点是否为空,再将头节点的prev指向虚拟头节点
- 引入虚拟头节点
- 找到链表的最后一个节点
- 把新节点插入在最后一个节点后
- 还原头节点
根据索引添加节点addAtIndex(int index, int val)
索引指定位置可能为头节点,故引入虚拟头节点
- 引入虚拟头节点
- 循环找到插入位置,注意空指针
- 把新节点插入
- 如果不是插入在末尾,需要改变后继节点的prev指针
- 还原头节点
难点
插入索引值可能无效,因此在寻找插入位置时要检查指针是否为空。
根据索引删除节点deleteAtIndex(int index)
索引指定位置可能为头节点,故引入虚拟头节点
- 引入虚拟头节点
- 循环找到要插入位置的前序节点
- 删除元素。注意元素的后继非空才需要修改prev指针
- 还原头节点
LeetCode 206 反转链表
思路
太久没做,翻了代码随想录想起来只需要改变next指针指向 考虑把一个链表整个反转:
- 把第一个节点指向自己的前继
- 反转从第二个节点开始的子链表 故我们可以使用递归
- 递归函数的参数和返回值 链表的头节点,无需返回值 全局变量prev,保存当前head的前一个节点。
- 递归结束条件 head为空,直接返回(处理链表为空的特殊情况) 如果head.next为空,只需要把head.next指针置为prev,之后返回。(这样head最后是落在最后一个节点,这样返回的就是新链表的头节点)
- 每层递归的逻辑 先保存head.next为next 将head.next链表指向prev 更新prev为head 把next传入递归函数进行下一轮递归
难点
在递归中搞明白每一层修改了那些指针,要保存哪些信息。 每一层都修改的是当前head的next指针,那么就需要预留下一个节点和上一个节点。
今日收获总结
今天学习4小时,链表设计细节很多折磨了很久,反转链表的思路也没有及时想出来,以后要多练