1.学习链表理论基础:代码随想录 (programmercarl.com)
知道链表的类型有啥,单链表、双链表、循环列表都是啥,为啥分这几类,能解决啥问题。
存储方式是啥,在内存里怎么存储的,和数组的区别,链表在内存里不是连续分布,链表是通过指针域的指针链接在内存中各个节点。数组则是连续分布。
知道存储方式了再想想这两个有什么优点,从查询和修改两个角度看,查询数组和链表谁快,为啥。修改数组和链表谁快,为啥。在代码随想录中都有介绍,忘了就复习下。
原因:数组连续分布,故地址知道一个就知道所有了,因为就是加一下。所以查询快。链表无法知道所有,要查询只能一个个查,查了第一个才知道第二个,以此类推,所以查询慢。
修改包括增加、删除。数组的增删都需要移动增删后面的所有元素,因为无法消除,所以一个个移,然后覆盖。所以修改慢。链表要修改哪个只需要改指针的指向,即修改指针域的指针链接,所以修改快。
熟悉下如何创建ListNode,Java好理解,Java中其实感觉就是ListNode嵌套实现逻辑上的链表。
C++以后可以看看。
重点理解链表的操作,删除和增加的过程。这部分周末需要再看看加深理解,mark一下。
2.移除链表中的元素:代码随想录 (programmercarl.com)
要知道移除元素的情况有几种,在头节点的情况怎么移,不在头节点的情况怎么移
这样就能知道为什么要加个虚拟节点,实际上打印出来还是很简单的。具体看附加内容问题,个人整理。
链表部分加了注释,方便个人理解。
package LuStudy;
/**
* ListNode
*
* 链表理论,要知道怎么定义的
* Java中其实感觉就是ListNode嵌套实现逻辑上的链表
*/
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;
}
}
public class _3_1RemoveLinkedListElement {
private static ListNode removeLinkedListNode(ListNode head, int val) {
// 用原来的链表操作
// 处理头节点为为val的情况
while (head != null && head.val == val) {
head = head.next;
}
ListNode currentNode = head;
// 处理头节点不为val的情况
while (currentNode != null && currentNode.next != null) {
if (currentNode.next.val == val) {
currentNode.next = currentNode.next.next;
} else {
currentNode = currentNode.next;
}
}
return head;
}
public static void main(String[] args) {
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(6);
head.next.next.next = new ListNode(3);
head.next.next.next.next = new ListNode(4);
head.next.next.next.next.next = new ListNode(5);
head.next.next.next.next.next.next = new ListNode(6);
head = removeLinkedListNode(head, 6);
ListNode current = head;
while (current != null) {
System.out.print(current.val + " ");
current = current.next;
}
}
}
3.设计链表:代码随想录 (programmercarl.com)
先自己实现下,写不出来再看看,有助于个人对链表操作原理的理解。知道了这个就知道了之前用的方法是为什么而来了,还能大概想到过程和思路。
设计链表的代码比较多,但是还是好理解的。先知道有什么方法,再从main入口看对应的操作。个人代码里打印了下结果马,方便查看和理解。
package LuStudy;
/**
* LinkNode
*/
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;
}
}
/*
* 设计链表
*
* https://programmercarl.com/0707.%E8%AE%BE%E8%AE%A1%E9%93%BE%E8%A1%A8.html
*
* 实现
* get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
* addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
* addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
* addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index
* 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
* deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
*
*/
class MyLinkedList { // 单链表结构
int size; // 链表大小
ListNode head; // 虚拟头节点
// 初始化链表
public MyLinkedList() {
size = 0;
head = new ListNode(0);
}
// get(index)
public int get(int index) {
if (index < 0 || index > size) {
return -1;
}
ListNode currentNode = head;
for (int i = 0; i <= index; i++) {
currentNode = currentNode.next;
}
return currentNode.val;
}
// addAtHead(val)
public void addAtHead(int val) {
ListNode addNode = new ListNode(val);
addNode.next = head.next;
head.next = addNode;
size++;
}
// addAtTail(val)
public void addAtTail(int val) {
ListNode currentNode = head;
ListNode addAtTailNode = new ListNode(val);
while (currentNode.next != null) {
currentNode = currentNode.next;
}
currentNode.next = addAtTailNode; // currentNode.next需要赋值
size++;
}
// addAtIndex(index,val)
public void addAtIndex(int index, int val) {
if (index > size) {
return;
}
if (index == size) {
addAtTail(val);
}
if (index < 0) {
index = 0;
}
size++;
ListNode cur = head;
for (int i = 0; i < index; i++) {
cur = cur.next;
}
ListNode addAtIndexNode = new ListNode(val);
addAtIndexNode.next = cur.next;
cur.next = addAtIndexNode;
}
// deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
public void deleteAtIndex(int index) {
if (index >= size || index < 0) {
return;
}
ListNode cur = head;
for (int i = 0; i < index; i++) {
cur = cur.next;
}
cur.next = cur.next.next;
size--;
}
// 重写toString()方法,为了打印出可视链表数据
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[");
ListNode current = head;
while (current != null) {
sb.append(current.val);
if (current.next != null) {
sb.append(", ");
}
current = current.next;
}
sb.append("]");
return sb.toString();
}
}
public class _3_2DesignSingleLinkedList {
// main + test
public static void main(String[] args) {
MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
System.out.println(linkedList.toString());
linkedList.addAtTail(3);
System.out.println(linkedList.toString());
linkedList.addAtIndex(1, 2);
System.out.println(linkedList.toString());
int getIndexBefore = linkedList.get(1);
System.out.println("Before deleteAtIndex(1) , get(1): " + getIndexBefore);
linkedList.deleteAtIndex(1);
linkedList.get(1);
System.out.println("After deleteAtIndex(1) , get(1): " + linkedList.get(1));
System.out.println(linkedList.toString());
}
}
4.反转链表:代码随想录 (programmercarl.com)
看文章有三种写法,今晚先看了第一个双指针写法。比较好理解,双指针还是要熟练的。
个人代码里写了很多注释,用来加深理解,这个思路过程分享下:
package LuStudy;
/*
* 反转链表
*
* https://programmercarl.com/0206.%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A8.html#%E5%85%B6%E4%BB%96%E8%AF%AD%E8%A8%80%E7%89%88%E6%9C%AC
*
* 解法有:双指针法 | 递归 | 从后往前递归
*/
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;
}
}
public class _3_3ReverseLinkList { // 双指针解法
public ListNode reverListNodeByDoublePointer(ListNode head) {
// 需要定义三个量,一个用来往前跑,一个用来记录,还有一个中间缓存变量,用于指明往前跑变量的方向
// 声明注意 三个量的初始化
ListNode pre = null; // pre就是最后的头节点,pre相当于管理层
ListNode cur = head; // cur相当于遍历和具体操作
ListNode temp = null; // cur和pre的中间层,指明灯
while (cur != null) { // 边界条件,当cur走到null时候程序就退出了
// 先存档
temp = cur.next;
// 开始反转
// 由于程序是从右往左赋值,这一步个人可以这样加深理解看,pre对cur说:“我才是你的下一个”;
cur.next = pre;
// 反转完,各走各的路,但是生活继续,还要反转,直到死亡
// 先让pre过来,因为此时cur还不能先往前走,这样子pre不知道下一步的路
pre = cur;
// pre安全到达,cur往前跑,temp指明cur的方向
cur = temp;
}
return pre; // 返回头节点
}
}
mark一下:链表的增删过程周末需要理解。反转链表后面递归的思路周六下午可以看看,计划上午完成day4任务,下午复习这几天的薄弱点和遗忘点。
往下都是个人写题时候想到的问题和细节:
(问了下gpt,整合了下答案)看不懂直接跳过
在移除链表中元素那道题:
1. Q:这个函数在链表的末尾会怎么判断
最后一个节点应该还是链表的最后一个节点,但是在上一个节点的循环中被判断了,上一个节点的下一个节点就是最后一个节点,所以就算最后这个节点的值等于val也会被移除,倒数第二个节点最后指向null,下一次直接结束循环
A:您的理解是正确的,但有一点需要澄清:在链表操作中,当我们说“移除”一个节点时,实际上并不是从物理内存中删除该节点,而是更改前一个节点(如果存在)的next指针,使其不再指向要移除的节点,而是指向要移除节点的下一个节点(如果存在)。这样,被“移除”的节点就变成了孤立节点,但在垃圾回收机制(如果适用)的语言中,它最终会被清理。
在给出的代码中,确实存在两个while循环:
第一个循环处理头节点(或连续的头节点)值为val的情况。如果头节点的值等于val,则更新头节点为下一个节点,直到头节点的值不等于val或头节点变为null。
第二个循环从更新后的头节点(或原始头节点,如果它的值不等于val)开始,遍历链表中的剩余节点。如果当前节点的下一个节点的值等于val,则更新当前节点的next指针,使其跳过下一个节点。这样,下一个节点就被“移除”了。这个过程会一直持续到链表末尾(即当前节点的next为null)。
关于最后一个节点:
如果最后一个节点的值等于val,并且它是通过第一个循环被“移除”的(即它是原始的头节点或连续的头节点之一),那么第二个循环根本不会开始,因为头节点已经更新为不包含该值的节点。
如果最后一个节点是在链表的中间或末尾(但不是头节点),并且它的值等于val,那么第二个循环会在遍历到它的前一个节点时检测到这一点,并更新前一个节点的next指针为null,从而“移除”最后一个节点。
如果最后一个节点的值不等于val,那么它不会被第二个循环影响,因为循环会在到达它之前因为currentNode.next为null而终止。
因此,无论最后一个节点的值是什么,代码都能正确地处理它,要么在第一个循环中“移除”它(如果它是头节点或连续的头节点之一),要么在第二个循环中“移除”它(如果它位于链表的中间或末尾且值等于val),或者简单地忽略它(如果它的值不等于val)。在第二种情况下,最后一个节点实际上是被“移除”的,因为前一个节点的next指针被设置为了null。
因为Java中有垃圾回收机制,所以链表这里没什么特别要标明回收的。
2. 告诉我一下java的垃圾回收机制
Java的垃圾回收(Garbage Collection, GC)机制是Java内存管理中的一个核心部分,它负责自动管理程序运行时的内存分配和释放。在Java中,程序员不需要(也不建议)直接释放对象占用的内存,而是由垃圾回收器在适当的时候自动回收那些不再被使用的对象所占用的内存。
垃圾回收的基本原理
垃圾回收器的主要任务是识别并回收那些不再被当前程序使用的对象。一个对象被认为是“不再被使用”的,当且仅当没有任何引用(Reference)指向它时。Java中主要有四种引用类型,用于描述对象与引用之间的不同关系,从而影响垃圾回收的行为:
强引用(Strong Reference):最常见的引用类型,只要强引用还存在,垃圾回收器就永远不会回收掉被引用的对象。
软引用(Soft Reference):在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
弱引用(Weak Reference):比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
虚引用(Phantom Reference):也称为幽灵引用或幻影引用,是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
垃圾回收算法
Java垃圾回收器使用了多种算法来识别并回收垃圾对象,包括但不限于:
标记-清除(Mark-Sweep):首先标记出所有需要回收的对象,然后统一回收被标记的对象。但这种方式会留下许多内存碎片。
复制(Copying):将内存分为大小相同的两块,每次只使用其中一块。当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用的内存空间一次清理掉。这种方式效率高,但代价是内存利用率低。
标记-整理(Mark-Compact):结合了标记-清除和复制的优点,首先标记出所有需要回收的对象,在存活的对象都向一端移动,然后清理掉边界以外的内存。
分代收集(Generational Collection):根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
触发垃圾回收的时机
Java虚拟机(JVM)并没有规定垃圾回收的具体时间,但是有一些因素可以触发垃圾回收:
系统内存不足:当JVM向操作系统请求更多的内存,但操作系统无法满足时,JVM会抛出OutOfMemoryError错误,并可能在抛出异常前进行垃圾回收。
**显式调用System.gc()**:程序员可以显式地请求JVM执行垃圾回收,但JVM可以忽略这个请求。
JVM内部逻辑:JVM实现中可能会包含一些启发式算法来预测何时需要进行垃圾回收。
总之,Java的垃圾回收机制是自动的、不可预测的,并且旨在提高程序的稳定性和性能。程序员应该避免编写依赖于垃圾回收器行为的代码,而是应该专注于编写清晰、高效、易于维护的代码。
3.为什么要加虚拟头节点
虚拟头结点(Dummy Node)是数据结构中的一种技巧,特别是在链表操作中非常常见。它是指一个并不存储实际数据,但在链表头部添加的额外节点,用于简化操作和提高代码的可读性。虚拟头结点的优点主要包括:
简化操作:
虚拟头结点使得链表操作逻辑更为统一。例如,在插入和删除节点时,无需特别处理头节点为空的情况,因为虚拟头结点始终存在。
在处理链表相加、反转等操作时,虚拟头结点可以作为一个统一的起点,简化边界情况的处理。
提高代码可读性:
引入虚拟头结点后,代码结构更加清晰和统一,减少了因为边界条件而引入的复杂性。
虚拟头结点使得链表操作逻辑更加直观易懂,提高了代码的可读性和可维护性。
减少边界条件:
避免了在操作链表时需要判断链表是否为空或者头节点是否存在的问题,减少了代码中的条件判断。
统一代码结构:
通过引入虚拟头结点,可以统一对链表的操作逻辑,使得代码结构更加清晰和统一。
综上所述,虚拟头结点在链表操作中起到了简化逻辑、提高代码可读性和可维护性的作用。掌握虚拟头结点的使用技巧,对于编写高效、清晰的代码至关重要。
需要注意的是,虚拟头结点并不存储实际的数据,其主要作用是作为一个标识符或者占位符,方便链表的操作。在实际应用中,虚拟头结点常被用于链表相关的算法中,如链表反转、删除节点、合并链表等。
在反转链表那边,有个动画:代码随想录 (programmercarl.com)
4.为什么(纠正:动画应该是先移动pre,在移动cur)
因为要用pre来判断后面还有没有节点了,所以得先移动pre再移动cur,不然就会报错了?(个人理解是这样的)