代码随想录算法训练营第三天 | 203.移除链表元素 707.设计链表 206.反转链表
链表的数据结构基础
- 链表结构
链表是一种通过指针串联在一起的线性结构。每一个节点由两钟部分构成,一部分是数据域,一部分是指针域,指针域存放的指针指向另一个节点。链表最后一个节点指向下一个节点的指针域指向null。链表的头节点称为头节点,也可称为head。
- 链表类型
- 单链表
单链表即只有单向链接的列表,表内的节点只存储指向下一个节点的指针域。 - 双链表
与单链表相比,双链表内的节点多了一个指针域,用于存储指向上一个节点的指针信息,因此允许双向遍历。 - 循环链表
循环链表的最后一个节点,其指向下一个节点的指针域并不为null,而是head,使得链表形成首尾相连的结构。
- 单链表
- 链表存储方式
链表在内存中并非连续分布,节点之间通过指针链接。因此,链表内数据的内存地址并不相连,其分布取决于操作系统的内存管理。 - 链表定义
以最经典的单链表为例,给出Java中的一种定义方式:
class ListNode {
int val;
ListNode next;
ListNode(){}; // 无参构造方法
ListNode(int val){ // 设定初始值的有参构造方法
this.val = val;
}
ListNode(int val, ListNode next) { // 设定初始值和下一个节点的有参构造方法
this.val = val;
this.next = next;
}
}
- 链表基本操作
- 删除节点
结合链表的结构,不难发现对链表内的节点进行操作,实际上就是对节点之间的指针进行操作。因此,要删除一个节点,只需要让该节点的上一个节点的指针指向下一个节点即可。在Java中,因为垃圾回收机制的存在,不需要对这个不存在于链表中但仍然存在于内存中的节点进行额外的删除操作。 - 增加节点
仍然是对节点的指针进行操作。设存在节点A,C,需要将节点B插入节点A和C之间,则需要让B的指针指向C,再将A的指针指向B。务必注意两步操作不可颠倒,否则节点C及其之后的节点都将与链表断开链接。
- 删除节点
- 链表性能分析及与数组的性能对比
- 增加&删除
链表的增加与删除操作只需要修改指针指向即可,因此链表的增删操作时间复杂度为O(1)。数组的增加与删除操作需要对增加与删除的位置之后的元素进行整体的移动操作,因此数组的增删操作时间复杂度为O(n)。 - 查询
查询链表元素需要对链表进行遍历操作,因此链表的查询操作时间复杂度为O(n)。而数组可以通过下标索引的方式快捷访问到数组内元素而无需遍历数组,因此数组的查询操作时间复杂度为O(1)。
- 增加&删除
链表长度可变,增删简单,但查询性能较弱,适用于数据长度不定,需要频繁增删而少查询的环境。
数组长度不变,查询简单,但增删性能较弱,适用于数据长度固定,需要频繁查询而少增删的环境。
203.移除链表元素
本题以链表的删除操作为基础。首先,需要遍历整个链表,寻找是否存在值与给定的目标值相等的节点。当发现这样的节点时,进行删除操作。
对非头节点的节点进行删除操作的逻辑是一致的,但是如果是对头节点进行删除操作呢?此时并不存在一个“头节点之前的节点”需要指向头节点之后的节点。因此,只需要将头节点设置为头节点的下一个节点即可。
可以发现,删除头节点和删除非头节点的逻辑不一致。为了处理这种不一致,存在两种解决方案。一种是单独写一段针对删除头节点的代码,一种是设置虚拟头节点,即“头节点的头节点”。其中,设置虚拟头节点的方式可以应用在很多种其他的链表题目内,因此今天先针对使用虚拟头节点的方法进行了学习。
class Solution {
public ListNode removeElements(ListNode head, int val) {
ListNode dummy = new ListNode();
dummy.next = head;
ListNode pre = dummy;
ListNode cur = dummy.next;
while (cur != null) {
if (cur.val == val) {
pre.next = cur.next; // 此时pre需要保持在原位不动,确保pre与cur仍然相差一位。
} else {
pre = pre.next;
}
cur = cur.next;
}
return dummy.next;
}
}
707.设计链表
一开始甚至没有注意到题目并没有像之前一样给出链表的定义……因此这道题目需要自行定义一个ListNode
类来实现基本的单链表。同时,为了实现题目要求的方法,MyLinkedList
类也需要添加头节点head
(构造方法)和链表长度size
(get
,addAtIndex
和deleteAtIndex
)两个属性。实现题目方法的过程就是对链表增删改查方法实现的复习。
class MyLinkedList {
int size;
ListNode dummy;
public MyLinkedList() {
size = 0;
dummy = new ListNode();
}
public int get(int index) {
if (index < 0 || index >= size) {
return -1;
}
ListNode cur = dummy.next;
for (int i = 0; i < index; i++) {
cur = cur.next;
}
return cur.val;
}
public void addAtHead(int val) {
ListNode insert = new ListNode(val);
insert.next = dummy.next;
dummy.next = insert;
System.out.print(dummy.next.val + " ");
size++;
System.out.println(size);
}
public void addAtTail(int val) {
ListNode insert = new ListNode(val);
ListNode cur = dummy;
while (cur.next != null) {
cur = cur.next;
}
cur.next = insert;
size++;
}
public void addAtIndex(int index, int val) {
if (index == size) {
addAtTail(val);
} else if (index <= 0) {
addAtHead(val);
} else if (index < size){
ListNode insert = new ListNode(val);
ListNode cur = dummy;
for (int i = 0; i < index; i++) {
cur = cur.next;
}
insert.next = cur.next;
cur.next = insert;
size++;
}
}
public void deleteAtIndex(int index) {
if (index >= 0 && index < size) {
ListNode cur = dummy;
System.out.print(1);
for (int i = 0; i < index; i++) {
cur = cur.next;
}
if (cur.next != null) {
cur.next = cur.next.next;
} else {
cur.next = null;
}
size--;
}
}
}
class ListNode {
int val;
ListNode next;
ListNode(){};
ListNode (int val) {
this.val = val;
}
}
206.反转链表
本题最重要的地方在于理解链表反转的步骤。总体而言,反转单个节点的步骤如下:
- Step 1
很容易想到至少需要两个指针,cur
指向需要反转的节点,pre
指向需要反转的节点即将指向的节点。但是,如果直接反转,那么cur
指向的节点及其之后的节点都会与链表断开链接。 - Step 2
因此,需要设置一个tmp
指针,用于保存cur
指向的节点之后的节点。 - Step 3
保证了链表节点不会因反转操作而丢失之后,进行反转即可。 - Step 4
完成反转操作之后,所有指针都向后移动一位,为下一次反转操作做准备。
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
ListNode tmp;
while (cur != null) {
tmp = cur.next;
cur.next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?