硅基计划4.0 算法 链表

链表是一个逻辑性非常强的数据结构,在做这类题的时候,我们一般有几个技巧
- 引入虚拟头节点,避免一些边界情况(比如原链表头节点变动等情况)
- 尽量多的定义变量以理清各个节点之间的关系
比如一个双向链表[1](preV)<-->[2],我要在其中插入一个节点[3](current)
那我是不是得用一系列的关系指向避免链表中断
我们不妨把下一个节点定义为nextNode,然后让preV,current,nextNode之间正常连接就好 - 常用双指针寻找链表换入口,倒数第几个节点,判断环等等,之前数据结构的时候已经有讲过了
一、两数相加
题目链接
题目中的示例是这样的
2 4 3 --> 3 4 2,5 6 4 --> 4 6 5,然后3 4 2 + 4 6 5 = 8 0 7
结果还要再逆置7 0 8
再比如
2 4 --> 4 2,5 6 4 --> 4 5 6,然后4 2 + 4 5 6 = 5 0 7,逆置7 0 5
题目为什么要直接给逆序的呢,是便于我们加法,比如刚刚的2 4和5 6 4
2 4
5 6 4
————
7 0 5
我们可以看到,我们每个链表的每一位是要对齐的,一个链表为空了如果另一个不为空还是要进行计算的
也就是说,我们定义两个指针,如果一个指针是空,也要进行计算
我们还要存储两数相加结果,我们可以相加计算完后,我们提取出结果的个位数,这样就方便处理进位的情况
提取完毕后再除以10,这样做是为了保留进位
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
//不知道哪个链表比较长
ListNode newHead = new ListNode();
ListNode newCurrent = newHead;
ListNode current1 = l1;
ListNode current2 = l2;
int num = 0;
while(current1 != null || current2 != null || num != 0){
//当两个指针为空的时候,但是进位num里还是存在数据,也要进行加法运算
if(current1 != null){
num += current1.val;
current1 = current1.next;
}
if(current2 != null){
num += current2.val;
current2 = current2.next;
}
ListNode node = new ListNode(num%10);
newCurrent.next = node;
newCurrent = newCurrent.next;
num /= 10;
}
return newHead.next;
}
}
二、两两交换链表中的节点
题目链接
我们使用虚拟头节点就很方便,我们两个节点为一组,进行翻转,最后若剩下节点不足两个,就不用变化

我们定义preV指向交换区域的前置节点,node1 node2是两个待交换的节点,而node3是后面的节点,以防链表断开
可以看到,如果我们链表节点是偶数个,当我们的node1是空的就可以停止交换了

可以看到如果是奇数个,当我们的node2是空的也可以停止交换了
但是我们在循环的时候,想要进行下一次交换的时候,得提前判断node1和node2是否是空的,不然会发生空指针异常
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode swapPairs(ListNode head) {
if(head == null){
return null;
}
if(head.next == null){
return head;
}
//使用哨兵节点
ListNode newHead = new ListNode();
ListNode preV = newHead;
ListNode node1 = head;
ListNode node2 = node1.next;
ListNode node3 = node2.next;
while(node1 != null && node2 != null){
preV.next = node2;
node2.next = node1;
node1.next = node3;
//注意交换后节点位置变化
preV = node1;
node1 = preV.next;
//提前判断,防止空指针异常
if(node1 != null){
node2 = node1.next;
}
if(node2 != null){
node3 = node2.next;
}
}
return newHead.next;
}
}
三、重排链表
题目链接
这一题核心就是两个链表合并,哪两个链表,对于原始链表的前半部分和后半部分的逆序的合并
因此我们要先找到中间节点,然后断开链表,再合并就好
逆序我们使用头插法就好,双指针也行但麻烦
好,问题来了,我们找到中间节点后
究竟是要逆置中间节点后面(不包括中间节点)链表还是中间节点及以后的链表呢?我们可以来对比下

但是为什么推荐上面那一种,因为在图中你也能看到,中间节点和后面节点还有联系
本应该中间节点以后的节点是逆置的,是一个独立的链表,因此我们要断开两个链表联系
如果我们采用上面那一种,我们直接让middle.next = null就可以了
而下面那一种还要去寻找middle的前置节点,比较麻烦
讲起来很容易,但是还是要画图,尤其是两个指针的位置你要搞明白
什么时候头插逆置链表,什么时候尾插合并链表,都要搞得很清楚
这些代码我整整花了两个小时梳理QAQ
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public void reorderList(ListNode head) {
if(head == null || head.next == null || head.next.next == null){
return;
}
//寻找中间节点
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
//分割链表
ListNode newSlow = slow.next;
slow.next = null;
//逆置slow以后的链表
ListNode head2 = new ListNode(0);
while(newSlow != null){
ListNode tmpNewSlow = newSlow.next;
newSlow.next = head2.next;
head2.next = newSlow;
newSlow = tmpNewSlow;
}
//合并两个链表
ListNode current1 = head;
ListNode current2 = head2.next;
ListNode retHead = new ListNode(0);
ListNode newCurrent = retHead;
while(current1 != null){
newCurrent.next = current1;
newCurrent = current1;
current1 = current1.next;
if(current2 != null){
newCurrent.next = current2;
newCurrent = current2;
current2 = current2.next;
}
}
}
}
另外再附上我自己觉得巧妙的一种解法(可以不用看)
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public void reorderList(ListNode head) {
if(head == null){
return;
}
if(head.next == null){
return;
}
//我们发现中间节点永远都在最后,不论奇数偶数
ListNode middle = searchMiddle(head);
//不是双向链表,无法定义两个指针从两边向中间靠拢
//如果硬是要这么干,会导致每一次中间节点的后面节点排序都要找到前置节点,时间复杂度很高
//但是我们是不是可以把中间节点以后的节点进行逆置,对于偶数,我们中间节点是第二个
//我们要定义一个临时变量去代替中间节点行动
ListNode currentMiddle = middle;
//我们先设定好反转后的头节点
ListNode preV = currentMiddle.next;
//先把中间节点的next地址置空,防止成环
middle.next = null;
//开始反转,preV头节点位置一直变化,直到到达链表末尾的空节点位置
while(preV != null){
ListNode tmp = preV.next;
preV.next = currentMiddle;
currentMiddle = preV;
preV = tmp;
}
//由于此时preV为空,因此我们要让它等于链表最后一个节点
preV = currentMiddle;
//中间变量代替头节点行动
ListNode currentHead = head;
//开始重排链表,排着排着后面的头节点preV会越来越接近链表中间节点
//直到重合,偶数奇数都一样
//不能是currentHead,虽然奇数个节点会和中间节点重合,但是偶数个节点会停在中间节点的前一个节点
while(preV != middle){
ListNode tmpLeft = currentHead.next;
ListNode tmpRight = preV.next;
currentHead.next = preV;
preV.next = tmpLeft;
preV = tmpRight;
currentHead = tmpLeft;
}
}
private ListNode searchMiddle(ListNode head){
//我们的老熟人快慢指针
ListNode fast = head;
ListNode slow = head;
//防止fast越界且判断奇数和偶数节点情况
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
}
四、k个一组翻翻转链表——hard
题目链接
我们可以先计算出链表的节点个数,然后决定翻转多少组,每一组内部再进行头插法逆置链表即可
我们使用preV来表示每一组的头插的头节点
但是,我们完成一组逆序后,我们要进行下一组逆序,此时的头插的头节点会改变,因此我们提前要标记好下一次头插的头节点位置sign

/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
//先求出节点个数,决定翻转多少组
ListNode current = head;
int count = 0;
while(current != null){
count++;
current = current.next;
}
//翻转多少组
int times = count/k;
//翻转每组需要多少次
int step = k;
//current回到起始位置
current = head;
//设立新链表接受翻转
ListNode retHead = new ListNode(0);
ListNode preV = retHead;
//记录每次头插位置
while(times > 0){
ListNode sign = current;
for(int i = 0;i < step;i++){
ListNode nextNode = current.next;
current.next = preV.next;
preV.next = current;
current = nextNode;
}
preV = sign;
times--;
}
//处理剩下节点,此时sign标记是在下一次插入的位置
//但是此时已经循环完了,因此后续节点无需逆序,直接放入就好
preV.next = current;
return retHead.next;
}
}
1862

被折叠的 条评论
为什么被折叠?



