今天好兄弟遇到问题了。为什么链表的操作看起来像是基础类型,又像是引用类型。刚学的时候我也有过这样子的迷惑,分享一下我的经验。
首先回答第一个问题,链表在前端中本质上就是对象。但是这是一个嵌套对象,因为它包裹着下一个节点,下一个节点又包裹着下下个节点。那么为什么能直接进行赋值呢? 直接赋值是用来缓存当前节点的位置。比如:
在链表 1 => 2 => 3 中,你每次要给节点的位置加1, 那么你会通过一个while循环来实现。cur = cur.next可以不断取到下一个节点,但是当你要返回原本的头节点的时候,你就需要在循环开始前, 前用变量head = cur 来缓存原本头节点的位置。这里的知识点分为以下几个小点:
- 首先明确一点 head = head.next,会不会对当前节点有影响?答案是肯定的,这里改变了head在链表中的位置。
- 但是对整条链表有没有影响?你会发现并没有,因为每个节点对象的属性并没有发生变化。
- 所以如果加个变量去缓存链表的头节点,这时候返回你会发现链表还是原来的链表。
- 然后再搞懂head.next = head, 会不会对当前节点有影响?答案同样是肯定的,因为已经改变了对象中的next属性。
- 对整条链表有没有影响呢? 答案也是肯定的,因为链表中的某个对象发生了实质性的改变。
- 然后回到翻转链表这道题目中来,首先我们需要不断进行head = head.next,去改变每一个位置节点的next的指向。
- 那我们改变当前节点的时候,为什么不能直接把当前节点的next指向prev上一个节点呢? 因为你直接指向了prev上一个节点后,你下一轮怎么办,你就拿不到下一个节点了, 所以我们修改当前节点的next指向之前,需要缓存一下原本的next节点。
- 这样子当前这一轮指向修改完成后,下一轮才能继续接着进行。
举例
举个栗子。
我们要修改链表 1 => 2 => 3。
一开始创建一个新链表newHead = null, 然后进入第一轮的时候,我们拿到的是1这个节点, 我们在修改节点1的next指向之前,需要先缓存原本的next节点2,因为下一轮循环要修改节点2。 然后呢, 我们把节点1的next, 指向到 newHead身上, 这样子就形成了 1 => null, 然后我们把1 => null赋值,成位新的newHead, 同时当前节点向后走一步, 对节点2做同样的修改。
实战
剑指 Offer II 024. 反转链表
给定单链表的头节点 head ,请反转链表,并返回反转后的链表的头节点。
示例 1:
输入: head = [1,2,3,4,5]
输出: [5,4,3,2,1]
示例 2:
输入: head = [1,2]
输出: [2,1]
示例 3:
输入: head = []
输出: []
提示:
- 链表中节点的数目范围是
[0, 5000] -5000 <= Node.val <= 5000
代码
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function(head) {
let newHead = null; // 创建新链表,这一步没错
// 这一步是为了缓存原本的头节点, 比如删除链表中的某个节点的时候,你删除完,你需要返回原本的头节点
// 但是这道题其实我们可以直接操作head,因为待会儿我们只需要返回newHead
let nowHead = head;
// 我们需要不断遍历,也就是不断进行nowHead = nowHead.next
// 但是我们又要更改当前节点next的指向,所以我们在更改前,我们缓存原本的下一个节点
while (nowHead) {
const next = nowHead.next; // 缓存原本的下一个节点,确保我们下一轮改它
nowHead.next = newHead; // 改变当前节点的next指针,指向自己的上一个节点
newHead = nowHead; // 上一个节点添加了当前节点作为链表头
nowHead = next; // 下一轮改原本的下一个节点, 不要影响我们的原计划
}
return newHead;
};
花了大半个小时去组织语言想着怎么解释能让大家都尽量看得懂。希望能够帮到你呀好兄弟。我尽力了哈哈!