反转链表
循环迭代
整体思路
- 定义三个临时变量作为循环的指针,通过循环的方式将三个临时变量不断转换位置及改变next指针指向,三个指针分别是当前节点、当前节点的前一个节点、当前节点的后一个节点;
- 当前节点的前一个节点:用于将当前节点的next'指针指向该节点实现节点反转操作;
- 当前节点用于循环判断依据,循环期间不断赋值给下一个节点直到null;
- 当前节点的下一个节点:用于保存临时变量循环过程中赋值给当前节点从而实现节点数据的所有循环,其赋值为当前节点的next节点;
注意事项
- 循环过程中,先进行节点的转换,后进行下一轮节点的赋值;
- 第一轮循环中可以将当前节点赋值为head,当前节点的前一个节点赋值为null,当前节点的下一个节点赋值为head的next,这样可以实现循环转变后不用将转变后的尾部(原头部)节点的next单独设置指向null;
动图展示
代码实现
function reverList(list){
if(head === null || head.next === null) return head;
let pre = null,
current = head,
temp = null;
while(current){
// 节点的next指向前一个节点,需要缓存下一个节点
temp = current.next;
current.next = pre;
pre = current;
current = temp;
}
return pre;
}
复杂度分析
- 时间复杂度:O(n),其中 nn 是链表的长度。需要遍历链表一次。
- 空间复杂度:O(1)。
头插法实现反转
实现原理
在原有链表的基础上,不断循环将原有链表的头部取下,将新取下的头部再插入到新的链表中,依次循环实现反转链表
实现步骤
- 新建一个空的链表new_head;
- 以head为判断依据不断循环取出head,然后插入到新的空链表头部;
- 插入过程中需要将旧的new_head赋值到新取出的新head的next中,然后将取出并且完善后的新头赋值给new_head;
代码实现
function insertReverList(){
let new_head = null,
temp = null;
if(head === null || head.next === null) return head;
while(head !== null){
temp = head;
head = head.next;
// 将 temp 插入到 new_head的头部
temp.next = new_head;
new_head = temp;
}
return new_head;
}
原链表局部逆转法反转链表
实现原理
和头插法类似,只是在操作原链表,需要借助两个指针,反转的过程实际上是在操作者两个指针和head的指向关系;
实现步骤
- 初始化将两个指针分别指向head和head.next;
- 然后以end为判断依据,循环操作转换head、begin和end;
- 转化规则为将end删除,然后将end.next赋值为head,再讲head指向end,最后更改判断依据end为转换后begin的next;
代码实现
function localReverList(){
if(head === null || head.next === null) return head;
let begin = head,
end = head.next;
while(end !== null){
// 删除end节点
begin.next = end.next;
// 此时head指向为链表的第一个节点,且链表在循环的过程中head是不断变化的,链表的顺序也在趋于逆向;
end.next = head;
// 此时end为局部逆转后的链表,需要赋值给头部节点,以便下次更新被删除的end节点
head = end;
end = begin.next;
}
return head;
}
使用栈的方式
实现原理
由于栈的特点
后进先出,将需要反转的链表数据都入栈后再取出就可以实现链表的反转;
代码实现
function stickReverList(){
if(head === null || head.next === null) return head;
let stick = [],
_head = head;
while(_head !== null) {
stick.push(_head)
temp = _head.next;
// 避免形成闭环,拆分整个链表为单个的节点
_head.next = null
_head = temp
}
// stick -> [ [1], [2], [3], [4], [5] ]
if(stick.length === 0) return null;
let node = stick.pop()
let dummy = node
while(stick.length !== 0) {
let temp = stick.pop()
node.next = temp
node = node.next
}
return dummy
}
递归反转链表
实现原理
递归的方法可以将链表划分成多个小的可循环模块,进而在调用递归反转方法时将当前小模块中的数据进行反转,通过不断的循环从而达到链表整体反转的目的;不过在链表特别长的时候就会出现调用栈很深的情况,会有StackOverflowException的情况;
递归的本质是链表的后续遍历,层层压栈,遇到break 之后从倒数第一个开始组装反转后的链表,再层层出栈;
逻辑分析
//reverseList 1->2->3->4->5 5->4->3->2->1
/*
第一轮出栈,head为5,head.next为空,返回5
第二轮出栈,head为4,head.next为5,执行head.next.next=head也就是5.next=4,
把当前节点的子节点的子节点指向当前节点
此时链表为1->2->3->4<->5,由于4与5互相指向,所以此处要断开4.next=null
此时链表为1->2->3->4<-5
返回节点5
第三轮出栈,head为3,head.next为4,执行head.next.next=head也就是4.next=3,
此时链表为1->2->3<->4<-5,由于3与4互相指向,所以此处要断开3.next=null
此时链表为1->2->3<-4<-5
返回节点5
第四轮出栈,head为2,head.next为3,执行head.next.next=head也就是3.next=2,
此时链表为1->2<->3<-4<-5,由于2与3互相指向,所以此处要断开2.next=null
此时链表为1->2<-3<-4<-5
返回节点5
第五轮出栈,head为1,head.next为2,执行head.next.next=head也就是2.next=1,
此时链表为1<->2<-3<-4<-5,由于1与2互相指向,所以此处要断开1.next=null
此时链表为1<-2<-3<-4<-5
返回节点5
出栈完成,最终头节点5->4->3->2->1
*/
动图展示
代码实现
var reverseList2 = function(head) {
// 递归出口,返回最后一个节点作为反转后的头结点
if (head == null || head.next == null) {
return head;
}
console.log(head,'-------',head.next)
// 新头结点newHead指向尾结点,此处进入递归,递归一直到遍历到尾结点时才会返回
const newHead = reverseList2(head.next);
console.log(head,head.next,head.next.next,newHead)
// 每一层递归操作,让head的下一个节点指向自己【反转】
head.next.next = head;
// head指向null。以此达到反转的目的。
head.next = null;
return newHead;
};
// [1,2,3,4,5] ------- [2,3,4,5]
// [2,3,4,5] ------- [3,4,5]
// [3,4,5] ------- [4,5]
// [4,5] ------- [5]
// [4,5] [5] null [5]
// [3,4] [4] null [5,4]
// [2,3] [3] null [5,4,3]
// [1,2] [2] null [5,4,3,2]
复杂度分析
-
时间复杂度:O(n),其中 nn 是链表的长度。需要对链表的每个节点进行反转操作。
-
空间复杂度:O(n),其中 nn 是链表的长度。空间复杂度主要取决于递归调用的栈空间,最多为 nn 层。