此篇文章与大家分享链表专题的第一部分
如果有不足的或者错误的请您指出!
1.链表常用技巧总结
1.1引入虚拟头结点
在力扣上,基本提供的链表题目都是"无头的",但是针对无头链表,我们最怕的就是参数链表是一个空链表,就容易出现对null的访问.当我们引入虚拟头结点后,我们并不关心这个"头结点"里面放的是什么值,而这个节点只是起到一个哨兵的左右
举一个例子,如果我们要合并两个链表,引入第三链表将两个链表合并
在没有引入虚拟头结点的情况下:
那么我们的第三个链表就要先判断是否已经有节点了??如果有,直接插在后面即可;如果没有就要将插入的节点作为头结点
但是如果我们引入虚拟头结点
此时我们就不需要判断了,只需要将两个链表的节点依次插入到newHead后面即可
引入虚拟头结点的优势还有很多,我们在后面的题目中更能感受到
1.2不要吝啬空间
在链表专题,如果我们吝啬空间,有时候会给我们带来很大麻烦
举个例子:
我们通常会做到这样一种题目:在prve和prev.next节点之间插入cur节点
我们通常会这样做:
①cur.next = prev.next
②prev.next.prev = cur
③prev.next = cur
④cur.prev = prev
这四步操作中,前两步是一定要在后两个之前的
但是如果我们直接将prev.next用用一个临时节点存储起来:
这是就没必要去担心什么顺序问题了,直接针对三个节点进行操作即可
1.3快慢双指针的利用
快慢双指针在链表操作中经常遇到,而最常用的是我们下面这三种操作,最重要的是这三种操作往往只是作为一道题目里面的一小部分存在
1.3.1判断链表是否有环
题目:判断链表是否有环
在这道题中,我们定义一个fast指针和slow指针,fast指针一次走两步,slow指针一次走两步,那么如果链表没环,那么fast指针一定会走到null,如果链表没环,那么fast指针和slow指针一定会在环的某一点相遇
代码呈现:
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
if(fast == slow){
return true;
}
}
return false;
}
}
1.3.2找到链表环的入口
题目:找到环的入口
在上一题的基础上,如果链表有环,我们需要找到环的入口
实际上这道题有一个数学推导出来的结论:在fast指针和slow指针相遇点,此时这个相遇点距离环的入口,与起跑点距离环的入口,距离是一样的
即蓝色路程和红色路程是一样的
那么我们再定义一个节点node在起跑点位置,slow和node同时移动,二者相遇点就是环的入口
代码呈现:
public class Solution {
private ListNode isCircle(ListNode head){
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
if(fast == slow){
return slow;
}
}
return null;
}
public ListNode detectCycle(ListNode head) {
ListNode node = isCircle(head);
if(node == null || node.next == null){
return null;
}
ListNode cur = head;
while(cur != node){
cur = cur.next;
node = node.next;
}
return cur;
}
}
1.3.3删除链表的倒数第k个节点
同样利用双指针,我们让fast指针先走k步,此时fast和slow之间的距离就是k;接着让fast和slow指针同时走,直到fast为null,此时slow和空节点的距离就是k,即为倒数第k个节点
代码呈现:
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
if(head == null || head.next == null){
return null;
}
ListNode node = new ListNode(-1,head);//解决头结点被删的情况
ListNode fast = node;
ListNode slow = node;
while(n > 0){
n--;
fast = fast.next;
}
while(fast.next != null){
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return node.next;
}
}
1.3.4寻找中间节点
题目:链表的中间节点
固定套路:定义fast指针和slow指针,fast一次走两步,slow一次走一步,当fast为空或者fast.next 为空时,slow所在位置就是中间节点的位置
代码:
class Solution {
public ListNode middleNode(ListNode head) {
ListNode fast = head.next;
ListNode slow = head;
while(fast != null){
if(fast.next != null){
fast = fast.next.next;
}else{
fast = fast.next;
}
slow = slow.next;
}
return slow;
}
}
1.4反转链表
题目:反转链表
这道题如果引入"虚拟头结点",那么在时间复杂度为O(n)的情况下就能完成
即直接往虚拟头结点后面进行头插即可
class Solution {
public ListNode reverseList(ListNode head) {
ListNode newHead = new ListNode(0);
ListNode cur = head;
while(cur != null){
ListNode tmp = cur.next;
cur.next = newHead.next;
newHead.next = cur;
cur = tmp;
}
return newHead.next;
}
}
2.两数相加
题目:两数相加
2.1解析
思路比较简单,用两个指针遍历两个链表,每次将两个节点里面的值相加,将得到的数的个位数作为新节点的值,把新节点插入到引入的虚拟头结点,同时记录下此时两数相加的进位,在下次计算的时候要加上进位
2.2题解
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode cur1 = l1;
ListNode cur2 = l2;
int tmp = 0;
ListNode newHead = new ListNode(0);
ListNode cur = newHead;//记录新链表的最后一个节点
while(cur1 != null || cur2 != null || tmp != 0){//tmp != 0 是为了解决最后计算完还有进位的情况
if(cur1 != null){
tmp += cur1.val;
cur1 = cur1.next;
}
if(cur2 != null){
tmp += cur2.val;
cur2 = cur2.next;
}
ListNode node = new ListNode(tmp % 10);
tmp /= 10;
cur.next = node;
cur = cur.next;
}
return newHead.next;
}
}
3.两两交换链表中的节点
题目:两两交换链表中的节点
3.1解析
这道题就能体现出我们之前说过的"不要吝啬变量"的好处
为了方便,我们还是引入"虚拟头节点"
对此链表进行两两翻转操作
要翻转两个节点,不仅需要记录这两个节点,还要记录前一个节点和后一个节点
因此我们直接引入这些变量:
就是对cur和next进行翻转操作,就很简单明了了,直接让prev.next = next; next.next = cur; cur.next = nnext;
此时前两个就翻转完了
接下来就要让prev指向当前cur的位置,继续进行刚才的操作
那么循环什么时候结束呢??会有两种情况
(1)就像我们上面这一组一样,当我们再次把prev指向cur的时候
此时,没翻转的节点就一个,那么就不用翻转了,即当next == null的时候是循环结束
(2)假设我们没有最后一个节点
那么此时已经没有节点了,自然就不需要翻转.因此cur == null也是循环结束的标志
3.2题解
class Solution {
public ListNode swapPairs(ListNode head) {
if(head == null || head.next == null){
return head;
}
ListNode newHead = new ListNode(0,head);
ListNode prev = newHead;
ListNode cur = prev.next;
ListNode next = cur.next;
ListNode nnext = next.next;
while(cur != null && next != null){
prev.next = next;
next.next = cur;
cur.next = nnext;
prev = cur;
cur = prev.next;
if(cur != null){
next = cur.next;
if(next != null){
nnext = next.next;
}
}
}
return newHead.next;
}
}
4.重排链表
题目:重排链表
4.1解析
此题最简单的思路就是将数组划分为两部分,并将右边的部分逆置,随后依次插入新的链表中
而其中的操作就是寻找中间节点、链表逆置、合并链表,我们在一开始都是讲过的,因此我们直接代码呈现\
4.2
private ListNode findCenter(ListNode head){
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
private ListNode reverse(ListNode head){
ListNode newHead = new ListNode(1);
ListNode cur = head;
while(cur != null){
ListNode tmp = cur.next;
cur.next = newHead.next;
newHead.next = cur;
cur = tmp;
}
return newHead.next;
}
public void reorderList(ListNode head) {
ListNode center = findCenter(head);
ListNode node = reverse(center.next);
//让中心节点的后面部分逆置
center.next = null;//断开两部分
ListNode cur1 = head;//遍历前一部分
ListNode newHead = new ListNode(0);
ListNode cur = newHead;//遍历新链表
//合并
while(cur1 != null){//前面的部分一定比后面的部分长
cur.next = cur1;
cur1 = cur1.next;
cur = cur.next;
if(node != null){
cur.next = node;
node = node.next;
cur = cur.next;
}
}
head = newHead.next;
}