这道题在力扣(LeetCode)上有两道题不同难度的题,下面由易到难梳理一下逻辑。
题一:简单难度。
众所周知,js是没有指针的概念的,但是有引用类型的object,跟指针类似,也是访问的内存地址,所以js里一般用object来模拟指针,为了方便理解,后文统一按指针来讲。
这道题如果了解链表的概念的话,思路其实很简单:
改变一下每一项的指针指向
将第一项的指针指向null
其余每一项的指针都指向前一个节点。
比较简单,直接看代码。
var reverseList = function(head) {
let next = null; // 临时变量,存储每个节点的指向
let newHead = null; // 临时变量,存储前一个节点
while(head) { // 当前节点为空时退出循环
next = head.next; // 将下一个节点的指向存下来
head.next = newHead; // 将下一个节点指向前一个节点
newHead = head; // 将当前节点变为前一个节点
head = next; // 将下一个节点变为当前节点,开始新一轮的循环
}
return newHead; // 最后,输出前一个节点,就会倒着将所有节点输出了
};
题二:进阶版的反转链表,中等难度。
当我们学会了第一道反转链表的方法,再看到这道题就简单很多了。大致思路如下。
首先将链表分为三部分,中间待转区域为一部分,两边不需要反转的区域各为一部分。
然后将待转区域的链表采用上文题1的方法进行反转。
最后再将两边的区域和反转过后的链表拼接起来即可。
思路比较简单,不过本题的难点是如何对链表进行分割,直接看代码,注释比较清楚:
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} left
* @param {number} right
* @return {ListNode}
*/
var reverseBetween = function(head, left, right) {
let newHead = new ListNode(-1); // 首先创建一个虚拟节点,因为头节点在处理过程中可能发生变化,使用虚拟头节点为了存一下头节点的位置
newHead.next = head; // 虚拟节点下一个节点的指针指向链表头节点
let pre = newHead; // pre从头节点出发,循环到left - 1之前的位置
for(let i = 0; i < left-1; i++) { // 循环结束后,pre位于反转区域的前一区域的最后一个节点位置
pre = pre.next;
}
let net = pre; // net 从反转区域最左边节点出发,循环到反转区域最右边节点位置
for(let i = left - 1; i < right; i++) { // 循环结束后,net位于反转区域的最后一个节点位置
net = net.next;
}
let two = pre.next; // pre的下一个节点则是反转区域的头节点
let three = net.next; // net的下一个节点则是反转区域的后一区域的头节点,将这个节点位置存储下来,后面拼接时需要使用
net.next = null; // 将net的下一个节点位置置为null,截断反转区域和第三区域的联系
pre.next = null; // 将pre的下一个节点位置置为null,截断第一区域和反转区域的联系
reverseList(two); // 调用题一的方法,进行链表反转
// 将三部分区域链表重新拼接
pre.next = net; // net最后的位置是反转区域的最后一个位置,反转过后则为反转区域的第一个位置,故第一区域指向该节点
two.next = three;
return newHead.next; // 返回之前虚拟节点存储的链表头节点,系统则会依次输出
};
这种方法思路比较简单,也容易实现,不过有一个缺点就是,当left和right正好是链表两端的值时,需要遍历两遍链表才能实现该算法。感兴趣的可以看一下下面的方法二,如何通过一次遍历就实现该算法的。
方法二的基本思路是,头插法:
想象一下,当我们在日常开发中,一下子接到4个需求,默认文档里排序是4,3,2,1,对于有强迫症的我们,就想把他们重新排一下顺序。
于是我们从第二项开始,将每一项跟第一项比较
发现3比4优先级高,排到4前面,结果为3,4,2,1。
再往后看,2比3高,排到3前面,结果为2,3,4,1。
再往后看,1比2高,排到2前面,结果为1,2,3,4。
这就是所谓的头插法(别问为什么不用Excel什么的,它只是个栗子)。
这道题我们也可以采用这种方法来解,上代码:
* }
*/
/**
* @param {ListNode} head
* @param {number} left
* @param {number} right
* @return {ListNode}
*/
var reverseBetween = function(head, left, right) {
let newHead = new ListNode(-1); // 创建虚拟节点
newHead.next = head; // 指向头节点
let pre = newHead; // 从头节点开始,我们需要循环到需要反转的区域开始位置
for(let i = 0; i < left - 1; i++) { // 循环结束,pre指向反转区域的第一个节点
pre = pre.next;
}
let curr = pre.next; // curr表示待反转区域的第一个节点的位置,每次插一个数据,curr会往后移动一个位置(curr可以理解为当前要处理的节点)
for(let i = left; i < right; i++) { // 需要反转的区域有几个节点,我们就插n-1次(从反转区域第二个节点开始)
let next = curr.next; // next表示curr的下一个节点(这里是将当前节点的下一个节点存下来)
curr.next = next.next; // 当前节点的下一个节点指向下一个节点的下一个节点(其实就是往后移动一个位置,为了下一次循环)
next.next = pre.next; // 当前节点的下一个节点的下一个节点指向反转区域的第一个节点。(这里可能不好理解了,可以结合下面配图来看)
pre.next = next; // 最后再把反转区域的第一个节点修改为当前节点的下一个节点(四步下来,实现了调换)
}
return newHead.next;
};
考虑到这种方法,可能有点绕,所以这里对循环增加一点配图讲解
第19行代码:
第21行代码:
第22行代码:
第23行代码:
第24行代码:
循环到第21行代码:
后面重复的操作,不再赘述。
虽然了解链表的基本概念,但是却没有用js实现过链表的算法,这次也算是一次尝试。还有就是链表类型的题,建议还是要在纸上画一遍的,能更好的理解它。哪里有不对的地方,欢迎指正。