力扣刷题记录-链表相关问题

汇总刷题过程中遇到的链表问题。(题目汇总内容来自代码随想录公众号)

题目目录

    • 链表基础操作
      • LeetCode 203. 移除链表元素
      • LeetCode 83. 删除排序链表中的重复元素
      • LeetCode 707. 设计链表(五个基础操作)
    • 反转链表
      • LeetCode 206. 反转链表
      • LeetCode 92. 反转链表 II
      • LeetCode 234. 回文链表(快慢指针+反转链表)
      • LeetCode 24. 两两交换链表中的节点
      • LeetCode 25. K 个一组翻转链表
    • 快慢指针
      • LeetCode 876. 链表的中间结点(快慢指针)
      • LeetCode 19. 删除链表的倒数第 N 个结点(快慢指针)
      • LeetCode 面试题 02.07. 链表相交(快慢指针、有点环形链表的意思)
      • LeetCode 141. 环形链表(快慢指针)
      • LeetCode 142. 环形链表 II(快慢指针+公式推导)
    • 合并链表
      • LeetCode 21. 合并两个有序链表(双指针)
      • LeetCode2. 两数相加
      • LeetCode 23. 合并K个升序链表(优先级队列PriorityQueue)
    • 分解链表
      • LeetCode 86. 分隔链表
    • 复制链表
      • LeetCode 138. 复制带随机指针的链表(hashmap/迭代插入+拆分链表----同剑指 Offer 35. 复杂链表的复制)
    • 链表排序
      • LeetCode 148. 排序链表
    • 双向链表
      • LeetCode 146. LRU 缓存(同剑指 Offer II 031. 最近最少使⽤缓存----双链表+Hash表,数据结构设计题)

链表基础操作

LeetCode 203. 移除链表元素

原题链接

力扣刷题记录-链表相关问题_第1张图片

这是删除链表结点题目中最为简单的一题,删除链表中普通节点与头结点在操作上有些不一样,普通结点的删除大都是通过该结点的前一个结点,将其next指针指向要被删除结点的下一个结点,在java中,被跳过的结点就会被jvm自动回收;而头结点的删除稍有不同,只需要将头结点head向后移一位即可,但是这两种情况的代码不统一,为了代码的统一,可以在头结点head之前再设置一个“虚拟的头结点”–>dummy,具体设置代码如下:

class Solution {
    public ListNode removeElements(ListNode head, int val) {
        //设置一个值不在题目数据范围内的虚拟头节点
        //设置虚拟头结点,统一删除头结点与其它结点的操作
        ListNode dummy=new ListNode(-1);
        dummy.next=head;
        ListNode p=dummy;//用p作为指针遍历链表
        while(p.next!=null){
            if(p.next.val==val)p.next=p.next.next;
            else p=p.next;
        }
        return dummy.next;
    }
}

LeetCode 83. 删除排序链表中的重复元素

原题链接
力扣刷题记录-链表相关问题_第2张图片
代码如下:

class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        if(head==null)return head;
        ListNode slow=head,fast=head;
        while(fast!=null){
            if(fast.val!=slow.val){
                slow.next=fast;
                slow=slow.next;
            }
            fast=fast.next;
        }
        //循环之后需要断开slow的next,因为如果链表最后是相同的值
        //不断开slow与后面节点的连接,会导致尾巴连着重复元素
        slow.next=null; 
        return head;
    }
}

LeetCode 707. 设计链表(五个基础操作)

原题链接

力扣刷题记录-链表相关问题_第3张图片

这题的五大功能涵盖了大部分对单链表的操作,建议熟练掌握。

代码如下:

class MyLinkedList {
    int size;//单链表节点数量
    ListNoe dummy;//虚拟头结点
    
    //定义节点类
    public class ListNoe{
        int val;
        ListNoe next;
        public ListNoe(int val){
            this.val=val;
        }
    }

    //初始化单链表
    public MyLinkedList() {
        size=0;
        dummy=new ListNoe(-1);
    }
    //获取index位置节点值
    public int get(int index) {
        //索引值需要符合链表实际情况
        if(index<0||index>size-1)return -1;
        ListNoe cur=dummy.next;//要获取index位置的节点,需要从真正的头结点出发
        while(index-->0){
            cur=cur.next;
        }
        return cur.val;
    }
    //在链表最前方插入
    public void addAtHead(int val) {
        ListNoe newNode=new ListNoe(val);
        newNode.next=dummy.next;
        dummy.next=newNode;
        size++;
    }
    //在链表最尾巴插入,需要遍历整个链表
    public void addAtTail(int val) {
        ListNoe newNode=new ListNoe(val);
        ListNoe cur=dummy;
        //插入尾巴,需要定位到最后一个节点
        while(cur.next!=null){
            cur=cur.next;
        }
        cur.next=newNode;
        newNode.next=null;
        size++;
    }
    //在index位置前面插入节点
    public void addAtIndex(int index, int val) {
        if(index>size)return;//不会插入节点
        ListNoe newNode=new ListNoe(val);
        ListNoe cur=dummy;//插入需要遍历到index节点前一个节点
        //cur走到index前一个停下
        while(index-->0){
            cur=cur.next;
        }
        newNode.next=cur.next;
        cur.next=newNode;
        size++;
    }
    //删除index处节点
    public void deleteAtIndex(int index) {
        if(index<0||index>size-1)return;
        ListNoe cur=dummy;
        //需要遍历到index处前一个节点
        while(index-->0){
            cur=cur.next;
        }
        cur.next=cur.next.next;
        size--;
    }
}

反转链表

LeetCode 206. 反转链表

原题链接

力扣刷题记录-链表相关问题_第4张图片

2023.06.06 三刷

反转链表也是比较经典的链表问题,比较容易想出的是迭代法
代码如下;

//迭代法
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode cur=head;
        ListNode pre=null;
        //每一次while结束后,cur和pre之间是没有指针相连的
        //因此cur需要遍历到链表最后一个节点之后,也就是null为止
        //此时头结点就是pre
        while(cur!=null){
            ListNode tmp=cur.next;
            cur.next=pre;
            pre=cur;
            cur=tmp;
        }
        return pre;
    }
}

会写迭代法的话,就可以根据迭代法把递归的写法写出来,代码如下:

//递归法,空间O(n)
class Solution {
    public ListNode reverse(ListNode pre,ListNode cur){
        //终止条件,相当于迭代法到了最后,cur指向null,pre成为新的头结点
        if(cur==null)return pre;
        //与迭代法对应
        ListNode tmp=cur.next;
        cur.next=pre;
        //相当于迭代法中让pre=cur,cur=tmp;
        return reverse(cur,tmp);
    }
    public ListNode reverseList(ListNode head) {
        //相当于迭代法初始化pre=null,cur=head
        return reverse(null,head);
    }
}

LeetCode 92. 反转链表 II

原题链接

力扣刷题记录-链表相关问题_第5张图片

这题是LeetCode 206. 反转链表的进阶版,对指定区间范围内进行链表反转,并且放回原链表对应位置,主体反转思路和206的解法大致相同。

思路:可参考206
1.首先需要虚拟头结点方便统一操作,因为反转部分可能包括头结点head,需要在head之前有一个节点指向head,统一链表指针next的调整;

2.接下来分为两块:首先[]left,right]内部逆序(参考206写法),然后将外部指针调整;

2.要反转[]left,right]内部,需要找到并记录left前一个的节点a,方便后续调整外部指针

3.从left开始,将[left,right]结点指针逆置,可参考206的逆序链表思路,最终的pre指向内部的“头结点”(也就是最开始的right所代表的节点),cur指向right后一个结点,由于翻转特性,最终的pre和cur中间是“断链”的。

4.修改外部指针,内部逆置后的尾巴指向right后一个结点(cur),原left前一个结点指向pre(也就是最开始的right所代表的节点);

5.返回虚拟头结点的下一个结点作为链表的头结点。

代码如下:

 // 举例:1->2->3->4->5 l=2,r=4
class Solution {
    public ListNode reverseBetween(ListNode head, int left, int right) {
        //因为是删除操作,为了统一操作(方便删除头结点),设置虚拟头结点
        ListNode dummy=new ListNode(-1);
        dummy.next=head;
        ListNode a=dummy;
        for(int i=0;i<left-1;i++)a=a.next;//a走到left前一个结点
        //这里开始将left到right中间的链表逆序
        ListNode pre=null,cur=a.next;//pre先指向null,cur指向left
        //最终cur停在right.next处
        for(int i=0;i<right-left+1;i++){//left到right的长度(4-2+1=3)
            ListNode temp=cur.next;
            cur.next=pre;
            pre=cur;
            cur=temp;
        }//[left,right]逆置完毕,pre指向4,cur指向5   这里1-> 2<-3<-4  5
        //                                             a        pre cur  
        //接下来断链 a=1,cur=5,pre=4,要让1(a)指向4,2指向5(cur)
        a.next.next=cur;a.next=pre;//顺序不能乱
        return dummy.next;
    }
}

LeetCode 234. 回文链表(快慢指针+反转链表)

原题链接
力扣刷题记录-链表相关问题_第6张图片
2023.06.06 三刷

此题同

正常判断回文可以采用双指针,从首尾两端向中间遍历,如果都相等就是回文,但是这题是单链表,无法从后向前遍历。结合前面的反转链表,可以想到建一个新链表,这个新链表为原链表的反转的结果,只要同时遍历两个链表,遍历过程都相等,就可以判断是回文链表了。但是这样需要额外O(n)的空间复杂度。

有一种更为巧妙的思路(来自labuladong题解):

1.使用快慢指针找出链表中点,根据链表结点是奇/偶数,将慢指针指向右边区需要检验回文的区间起点:
力扣刷题记录-链表相关问题_第7张图片

如果是偶数,快慢指针停止时,慢指针所指即为右区间起点;如果是奇数,慢指针会在中点停下,右区间起点在中点的下一个节点:
力扣刷题记录-链表相关问题_第8张图片

2.翻转右区间的链表,结果如下:
力扣刷题记录-链表相关问题_第9张图片
3.从翻转之后的新的头结点处开始和左半区链表进行比较,如果中间出现不同则返回false,如果直到遍历完成都相等,则为回文。

代码如下:

/**
 * 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 boolean isPalindrome(ListNode head) {
        //1.利用快慢指针定位到需要翻转的起点
        ListNode slow=head,fast=head;
        //防止fast空指针异常
        while(fast!=null&&fast.next!=null){
            slow=slow.next;
            fast=fast.next.next;
        }
        //如果是奇数个点,fast停下时指向的是链表最后一个节点
        if(fast!=null)slow=slow.next;

        //2.翻转右半区,返回右半区翻转后起点
        ListNode left=head,right=reverse(slow);

        //3.遍历比较
        while(right!=null){
            if(left.val!=right.val)return false;
            left=left.next;
            right=right.next;
        }
        return true;
    }
    //翻转链表
    public ListNode reverse(ListNode head){
        ListNode pre=null,cur=head;
        while(cur!=null){
            ListNode tmp=cur.next;
            cur.next=pre;
            pre=cur;
            cur=tmp;
        }
        return pre;
    }
}

LeetCode 24. 两两交换链表中的节点

原题链接

2023.06.08 一刷

思路:
1.迭代法,思路清晰,不做更多解释。
2.递归:(官方题解)

  • 终止条件:链表中没有节点,或者链表中只有一个节点,此时无法进行交换。

  • 如果链表中至少有两个节点,则在两两交换链表中的节点之后,原始链表的头节点变成新的链表的第二个节点,原始链表的第二个节点变成新的链表的头节点。链表中的其余节点的两两交换可以递归地实现。在对链表中的其余节点递归地两两交换之后,更新节点之间的指针关系,即可完成整个链表的两两交换。

  • 用 head 表示原始链表的头节点,新的链表的第二个节点,用 newHead 表示新的链表的头节点,原始链表的第二个节点,则原始链表中的后续节点的头节点是 newHead.next。交换过程就是:head->newHead===>newHead->head

  • 令 head.next = swapPairs(newHead.next),表示将其余节点进行两两交换,交换后的新的头节点为 head 的下一个节点。

  • 然后令 newHead.next = head,即完成了所有节点的交换。最后返回新的链表的头节点 newHead。

代码如下:

// 1.迭代法(三结点指针),空间O(1)
class Solution {
    public ListNode swapPairs(ListNode head) {
        ListNode dummy=new ListNode(-1);
        dummy.next=head;
        ListNode cur=dummy;
        while(cur.next!=null&&cur.next.next!=null){
            ListNode p1=cur.next,p2=cur.next.next;
            // 开始交换,注意断链顺序(cur->p1->p2==>cur->p2->p1)
            cur.next=p2;
            p1.next=p2.next;
            p2.next=p1;
            // 更新cur位置到换位置后的p1处
            cur=p1;
        }
        return dummy.next;
    }
} 


// 2.递归法,空间O(n)
class Solution {
    public ListNode swapPairs(ListNode head) {
        // 终止条件
        if(head==null||head.next==null)return head;
        // 先明确每一轮递归中的newHead,是传入的head的下一个
        ListNode newHead=head.next;
        //既然newHead已经记录下来了,就可以把head指向它的指针断开,指向head应该指向的位置
        head.next=swapPairs(newHead.next);
        //完成这两个节点的指向
        newHead.next=head;

        return newHead;//返回的是每一轮递归中的新的头结点
    }
} 

LeetCode 25. K 个一组翻转链表

原题链接
力扣刷题记录-链表相关问题_第10张图片

2023.06.08 二刷

比较好理解的题解:题解链接

总结的思路:

  • 用pre指向待翻转链表区间的前一个结点,end指向待翻转区间的最后一个结点;
  • 如果end还没走完k步,就成了null,说明剩下的待翻转区间长度不足k,不用翻转,直接退出;
  • 用nextNode记录end.next,start记录翻转区间的起始结点。在翻转之前先把end.next指向null(断开),因为翻转链表是靠遍历到null作为停止信号。
  • 翻转长度为k的区间[start,end],pre->翻转后的头结点;翻转后的尾结点(start)->nextNode;

代码如下:

// 时间O(n),空间O(1)
// 所有遍历过程中,每个节点都只要被遍历两遍即可,第一次是end扫描,第二次是reverse翻转
class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode dummy=new ListNode(-1);
        dummy.next=head;
        ListNode pre=dummy;//pre为当前待翻转链表区间前驱节点
        ListNode end=dummy;//end为当前待翻转链表区间尾巴上的节点

        //进行链表翻转
        //如果上一轮中end到了最后一个结点(end.next=null),说明刚好全部翻转完,直接结束
        while(end.next!=null){
            //让end走到待翻转链表区间尾部()
            for(int i=0;i<k&&end!=null;i++)end=end.next;
            if(end==null)break;

            //nextNode记录下一段待翻转区间的链表头
            //因为翻转链表区间,会导致当前翻转区间与下一段待翻转区间的链断开
            ListNode nextNode=end.next;
            ListNode start=pre.next;//start为当前需要翻转区间的链表头

            //在进行reverse之前一定记得这步,因为reverse函数传入的是start
            //翻转的范围是通过遍历到null控制,需要先将当前翻转区间尾巴与后面断开
            //要不然会一直顺着链去翻转
            end.next=null;
            //pre为当前翻转区间的前驱,翻转后pre需要指向翻转后的链表头
            //reverse返回的是翻转后的链表头(也就是原来[start,end]中的end)
            pre.next=reverse(start);
            //翻转后的链尾([start,end]中的start)要与下一段待翻转区间相连
            start.next=nextNode;

            //pre继续作为下一段翻转区间的前驱
            pre=start;
            //经过翻转,原来的end已经变成头了,需要重新指向下一段待翻转区间前驱
            end=pre;
        }
        return dummy.next;

    }
    // 返回的是翻转后的头结点,也就是end,尾巴变成原来的start
    public ListNode reverse(ListNode head){
        ListNode pre=null,cur=head;
        while(cur!=null){
            ListNode tmp=cur.next;//暂时记录cur的下一个节点
            cur.next=pre;
            pre=cur;
            cur=tmp;
        }//退出时cur为null,并且pre和cur无链相连
        return pre;
    }
    
}

快慢指针

LeetCode 876. 链表的中间结点(快慢指针)

原题链接
力扣刷题记录-链表相关问题_第11张图片

很经典的快慢指针题目。

代码如下:

class Solution {
    public ListNode middleNode(ListNode head) {
        ListNode slow=head,fast=head;
        while(fast!=null&&fast.next!=null){
            slow=slow.next;
            fast=fast.next.next;
        }
        return slow;
    }
}

LeetCode 19. 删除链表的倒数第 N 个结点(快慢指针)

原题链接
力扣刷题记录-链表相关问题_第12张图片

2023.06.07 四刷

这题思路比较简单,但因为可能会删除头结点,所以设置一个虚拟头结点会更方便。

代码如下:

/**
 * 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 removeNthFromEnd(ListNode head, int n) {
        ListNode dummy=new ListNode(-1);
        dummy.next=head;
        ListNode fast=dummy,slow=dummy;
        while(n-->0)fast=fast.next;//快指针先走n步
        //不能用fast!=null,这样最终slow会停在要删除节点上
        //为了删除指定节点,slow要停在要删除节点前一个上
        while(fast.next!=null){
            fast=fast.next;
            slow=slow.next;
        }//slow停在要删除节点前一个
        slow.next=slow.next.next;//删除操作
        return dummy.next;
    }
}

LeetCode 面试题 02.07. 链表相交(快慢指针、有点环形链表的意思)

原题链接

力扣刷题记录-链表相关问题_第13张图片
2023.06.06 二刷

(注:此题与LeetCode 160. 相交链表、剑指 Offer 52. 两个链表的第一个公共节点、剑指 Offer II 023. 两个链表的第一个重合节点相同)

链表A和B的长度可能是不一样的,只要求出A和B的长度之差,然后更长的链表的指针先走diff步(diff为A和B长度之差),将链表尾部对齐,然后两个链表指针同时向前即可。

代码如下:

//好理解的写法时间O(m+n),空间O(1)
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
         ListNode pA=headA,pB=headB;
         int lenA=0,lenB=0;
         while(pA!=null){
             lenA++;
             pA=pA.next;
         }
         while(pB!=null){
             lenB++;
             pB=pB.next;
         }
         //默认pA指向A链,pB指向B链
         pA=headA;
         pB=headB;
         //如果B链更长,就让pA指向B链,长度lenA为更长的链的长度
         //总之就是让A链始终是最长的
         if(lenB>lenA){
             int tmp=lenA;
             lenA=lenB;
             lenB=tmp;
             ListNode tmpNode=pA;
             pA=pB;
             pB=tmpNode;
         }
         //求出长度差值
         int diff=lenA-lenB;
         //让更长的先走相差的长度,使得起点对其
         while(diff-->0)pA=pA.next;
         //不相同就一直向前,如果相交,遇到交点,pA与pB相同退出while
         //如果不想交,走到null时,pA与pB相同退出while
         while(pA!=pB){
             pA=pA.next;
             pB=pB.next;
         }
         return pA;
    }
}

另外还有一种更为简洁的思路(官方题解):

力扣刷题记录-链表相关问题_第14张图片

代码如下:

//简洁写法
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
         ListNode pA=headA,pB=headB;
         
         while(pA!=pB){

             if(pA==null)pA=headB;
             else pA=pA.next;

             if(pB==null)pB=headA;
             else pB=pB.next;
         }
         
         return pA;
    }
}

正确性证明:
力扣刷题记录-链表相关问题_第15张图片
链表A长度为m,链表B长度为n,假设链表 A 的不相交部分有 a 个节点,链表 B 的不相交部分有 b 个节点,两个链表相交的部分有 c 个节点,则a+c=m,b+c=n。

如果 a≠b,则指针 pA 会遍历完链表 A,指针 pB会遍历完链表 B,两个指针不会同时到达链表的尾节点,然后指针 pA移到链表 B的头节点,指针 pB移到链表 A的头节点,然后两个指针继续向前走。在指针 pA移动了a+c(在A上走完)+b (走到相交点)次、指针 pB移动了 b+c(在B上走完)+a (走到相交点)次之后,两个指针会同时到达两个链表相交的节点,该节点也是两个指针第一次同时指向的节点。


LeetCode 141. 环形链表(快慢指针)

原题链接
力扣刷题记录-链表相关问题_第16张图片
2023.06.06 四刷

使用快慢指针,快指针一次走两步,如果存在环形,快指针一定能在环内将慢指针套圈重合。

代码如下:

public class Solution {
    public boolean hasCycle(ListNode head) {
        ListNode slow=head,fast=head;
        while(fast!=null&&fast.next!=null){
            slow=slow.next;
            fast=fast.next.next;
            if(fast==slow)return true;
        }
        return false;
    }
}

LeetCode 142. 环形链表 II(快慢指针+公式推导)

原题链接
力扣刷题记录-链表相关问题_第17张图片

2023.06.06 四刷

思路:

  • 快慢指针同时走,有环的话会遇到一起,这时候退出循环;
  • 从head处重新出发一个指针,与slow一起一次走一步,它们相遇的地方就是链表成环的起点

正确性证明参考官方置顶精选题解:精选题解

代码如下:

public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode slow=head,fast=head;
        while(fast!=null&&fast.next!=null){
            slow=slow.next;
            fast=fast.next.next;
            if(slow==fast)break;
        }

        if(fast==null||fast.next==null)return null;

        ListNode ptr=head;
        while(ptr!=slow){
            ptr=ptr.next;
            slow=slow.next;
        }
        return ptr;
    }
}

合并链表

LeetCode 21. 合并两个有序链表(双指针)

原题链接
力扣刷题记录-链表相关问题_第18张图片

2023.06.07 二刷

这题只需要在两个有序链表头设置p1和p2指针,哪个指针所指值更小就先加入新链表,然后对应指针后移即可。

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode dummy=new ListNode(-1);
        ListNode p=dummy,p1=list1,p2=list2;
        while(p1!=null&&p2!=null){
            if(p1.val<=p2.val){
                p.next=p1;
                p1=p1.next;
            }else{
                p.next=p2;
                p2=p2.next;
            }
            p=p.next;
        }
        if(p1!=null)p.next=p1;
        if(p2!=null)p.next=p2;
        return dummy.next;
    }
}

LeetCode2. 两数相加

原题链接
力扣刷题记录-链表相关问题_第19张图片

2023.06.07 三刷

这题可以看作是链表的一种合并,分别遍历两个链表,求每对节点之和存入新的链表,主要考察对遍历链表的终止条件,虚拟头结点、以及进位的逻辑。

代码如下:

class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode dummy=new ListNode(-1);
        ListNode cur=dummy;
        int carry=0;//节点值计算+进位标识

        //若所有节点都走完到了null,但是carry>0的时候,说明还有进位,还需要下一个节点
        //输入示例:
        //[9,9,9,9,9,9,9]
        //[9,9,9,9]
        //输出示例:
        //[8,9,9,9,0,0,0,1]
        while(l1!=null||l2!=null||carry>0){
            if(l1!=null){
                carry+=l1.val;
                l1=l1.next;
            }
            if(l2!=null){
                carry+=l2.val;
                l2=l2.next;
            }
            cur.next=new ListNode(carry%10);
            cur=cur.next;
            carry/=10;
        }
        return dummy.next;

    }
}

LeetCode 23. 合并K个升序链表(优先级队列PriorityQueue)

原题链接

力扣刷题记录-链表相关问题_第20张图片

2023.06.10 二刷

有k条链表,要将它们有序合并,只要每次都能对每条链表找出当前的最小节点,放到新链表的尾部即可,但是要怎么快速找到当前这个k个链表头中的最小节点呢?

可以考虑使用优先级队列PriorityQueue,它通过二叉小顶堆原理实现,将优先级队列存储元素类型设置为ListNode,重写比较器,排序规则为按val值升序。这样每次从优先级队列队头取出的元素就是当前队列中最小的那个节点。

/**
 * 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 mergeKLists(ListNode[] lists) {
    	/*
    	//lambuda写法
    	PriorityQueue pq=new PriorityQueue<>((node1,node2)->node1.val-node2.val);
    	*/
        PriorityQueue<ListNode> priorityQueue=new PriorityQueue<>(new Comparator<ListNode>(){
            //因为需要对ListNode进行排序,需要重写排序规则
            @Override
            public int compare(ListNode node1,ListNode node2){
                return node1.val-node2.val;//按val值升序规则排序
            }
        });
        
        //遍历链表数组每一条链表(实际上就是每条链表的头结点)
        for(ListNode node:lists){
            //当前链表可能是空的,优先级队列不能添加null元素
            if(node!=null)
                priorityQueue.offer(node);
        }

        //用一个新链表承接节点
        ListNode dummy=new ListNode(-1);
        ListNode cur=dummy;
        while(!priorityQueue.isEmpty()){
            ListNode node=priorityQueue.poll();//取出优先级队列中最小的
            //取一个出来,就要放一个进去
            if(node.next!=null)priorityQueue.offer(node.next);
            cur.next=node;//把node接到新链表后面
            cur=cur.next;//新链表指针向前移动
        }
        return dummy.next;
    }
}

分解链表

LeetCode 86. 分隔链表

原题链接

力扣刷题记录-链表相关问题_第21张图片

思路:
前面做过了合并两条链表,其实这题可以把链表分解成两条,其中一条全是小于x的节点,另一条全是大于等于x的节点,然后再把两条链表接起来就行了。

需要注意虚拟头结点的使用,以及在原链表上的断链操作

代码如下:

/**
 * 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 partition(ListNode head, int x) {
        ListNode dummy1=new ListNode(-1);
        ListNode dummy2=new ListNode(-1);
        //p1遍历list1,存放小于x的节点,p2遍历list2,存放大于等于x的节点
        ListNode p1=dummy1,p2=dummy2;
        ListNode p=head;//p遍历原链表
        while(p!=null){
            //
            if(p.val<x){
                p1.next=p;
                p1=p1.next;
            }else{
                p2.next=p;
                p2=p2.next;
            }
            //节点放入list1或list2后需要把该节点在原链表上断开,同时p移动一步
            //否则会和另一条链相连
            ListNode tmp=p.next;
            p.next=null;
            p=tmp;
        }
        //list2接在list1后面
        p1.next=dummy2.next;
        return dummy1.next;
    }
}

复制链表

LeetCode 138. 复制带随机指针的链表(hashmap/迭代插入+拆分链表----同剑指 Offer 35. 复杂链表的复制)

原题链接

2023.06.09 一刷

思路1:迭代+拆分链表(图片来自【王尼玛】的题解)

  • 在原链表的每个原结点后面,都插入一个新结点,值与原结点的值一样(先把新链表的结点创造出来)
    在这里插入图片描述

  • 再遍历新链表,借用原结点的random指针,帮助新结点找到对应的random结点,不过需要注意原结点的random如果不为null,新节点的random指向需要调整为原结点的random指针指向的结点的后一个结点;
    力扣刷题记录-链表相关问题_第22张图片

  • 最后遍历新链表,将原结点和新结点拆分开,注意拆分后的原链表的最后一个结点指向null。
    力扣刷题记录-链表相关问题_第23张图片

代码如下:

//1.迭代+拆分链表,时间O(n):三轮遍历链表,空间O(1)
class Solution {
    public Node copyRandomList(Node head) {
        //1.复制结点(深拷贝),成为新的链表
        Node cur=head;
        while(cur!=null){
            Node newNode=new Node(cur.val);
            newNode.next=cur.next;
            cur.next=newNode;
            cur=newNode.next;
        }

        cur=head;
        //2.给新结点的random明确指向的结点
        while(cur!=null){
            //原结点的随机指针如果为null,那么新结点的random也要指向null
            //新结点的random初始化的时候就是null,无需改变
            if(cur.random!=null){
                cur.next.random=cur.random.next;//这里指向的是cur.random的后一个结点
            }
            //进入下一个原结点,进行新结点的random调整
            cur=cur.next.next;
        }

        // 3.分离新旧链表
        cur=head;//原链表头结点,用来遍历整个链表
        Node dummy=new Node(-1);
        Node newNode=dummy;//用来遍历组装新链表
        while(cur!=null){
            //新链表头结点的next指向新结点
            newNode.next=cur.next;
            // 同时该指针需要向前一步,走到新链表的最后一个结点,方便后面新结点的拼接
            newNode=newNode.next;
            // 新结点已经被新链表接管了,原结点与新结点之间的链就可以断开,指向下一个原结点
            cur.next=newNode.next;
            // cur也要前进到下一个原结点处,准备下一轮的新旧结点分离
            cur=cur.next;
        }
        return dummy.next;  
    }
}

思路2:哈希表(思路来自138题中【王尼玛】的题解)

  • 首先创建一个哈希表,再遍历原链表,遍历的同时再不断创建新节点,我们将原节点作为key,新节点作为value放入哈希表中;

  • 再遍历原链表,这次将新链表的next和random指针给设置上:
    – map.get(原节点),得到的就是对应的新节点
    – map.get(原节点.next),得到的就是对应的新节点.next
    – map.get(原节点.random),得到的就是对应的新节点.random
    力扣刷题记录-链表相关问题_第24张图片

  • 最后,返回map.get(head),也就是对应的新链表的头节点,就可以解决此问题了。

代码如下:

//2.哈希表,时间O(n):两轮遍历链表,空间O(n)
class Solution {
    public Node copyRandomList(Node head) {
        //1.遍历一遍原链表,建立原结点key-新结点value的哈希键值对
        Map<Node,Node> map=new HashMap<>();
        Node cur=head;
        while(cur!=null){
            map.put(cur,new Node(cur.val));
            cur=cur.next;
        }
        //2.再遍历原链表,设置新链表的next和random指针
        cur=head;
        while(cur!=null){
            // cur对应的新结点
            Node newNode=map.get(cur);
            //新结点的next=原结点的next对应的新结点
            newNode.next=map.get(cur.next);
            //同理
            newNode.random=map.get(cur.random);
            cur=cur.next;
        }
        // 如果链表为null,不会进入那些while,直接返回null
        // 不为空则返回新链表的头结点
        return map.get(head);
    }
}



链表排序

LeetCode 148. 排序链表

原题链接

2023.06.10 一刷

思路1:
使用自底向上的方法实现归并排序,则可以达到 O(1) 的空间复杂度。

首先求得链表的长度length,然后将链表拆分成子链表进行合并。
具体做法如下:

  • 用subLength表示每次需要排序的子链表的长度,初始时subLength=1。
  • 每次将链表拆分成若干个长度为subLength的子链表(最后一个子链表的长度可以小于 subLength),按照每两个子链表一组进行合并,合并后即可得到若干个长度为subLength×2的有序子链表(最后一个子链表的长度可以小于 subLength×2)。合并两个子链表仍然使用「21. 合并两个有序链表」的做法。
  • 将subLength 的值加倍,重复上一步,对更长的有序子链表进行合并操作,直到有序子链表的长度大于或等于length,整个链表排序完毕。

代码如下:

//归并排序(自底向上的方法),时间O(nlogn),空间O(1)
class Solution {
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) return head;
        //统计链表长度
        int length = 0;
        ListNode node = head;
        while (node!=null) {
            length++;
            node=node.next;
        }
        
        ListNode dummy=new ListNode(0, head);
        //子链表长度,从1开始合并,每次翻倍(1->2->4……)。
        for (int subLength = 1; subLength < length; subLength <<= 1) {
            ListNode pre=dummy;//prev用于连接合并后排序好的链表,相当于记录结果
            ListNode cur=dummy.next;//cur遍历链表,记录拆分链表的位置

            //遍历整条链表,将链表【拆分】成若干个长度为subLength的子链表,然后【合并】。
            while (cur != null) {
                //1.拆分subLength长度的链表1
                ListNode head1 = cur;// 第一个子链表的头节点
                //找到第一个子链表的尾结点
                //cur.next!=null是为了防止下面的head2=cur.next(cur=null报错)
                //或者也可以像下面next一样先判断一下cur!=null
                for (int i=1;i<subLength&&cur!=null&&cur.next!=null;i++) {
                    cur = cur.next;
                }//cur会停在子链表1的尾结点

                //2.拆分subLength长度的链表2
                ListNode head2=cur.next;//第二个子链表的头结点,即链表1尾部的next
                cur.next=null;//断开第一个链表和第二个链表的连接
                cur=head2;//cur走到第二个链表的头节点,继续寻找第二个链表的尾结点
                //寻找第二个链表的尾结点
                //cur.next!=null是为了防止下面的next=cur.next(cur=null报错)
                for (int i=1;i<subLength&&cur!=null&&cur.next!=null;i++) {
                    cur = cur.next;
                }//cur会停在子链表2的尾结点,如果剩余长度不够subLength,cur会走到null

                //3.断开第二个链表和剩下链表的连接
                //next为剩下链表的头(即拆分两个子链后,第二个子链尾的下一个结点)
                ListNode next=null;//可能没有剩下部分了
                //第二个链表的尾结点cur可能为空,不为空时才能更新next
                if (cur!=null) {
                    next = cur.next;
                    cur.next = null;//断开第二个子链表和剩下链表的连接
                }
                //到此,123步已经将子链表1、2从原链表中拆分出来
                //head1、head2为各自的头结点,next指向剩下未拆分的链表

                //4.合并两个subLength长度的有序链表
                //pre.next指向排序好的链表,连接结果
                pre.next = mergeTwoLists(head1, head2);
                //将pre移动到subLength×2的位置(或着说是合并后链表的尾巴处)
                while (pre.next != null) {
                    pre = pre.next;
                }
                //cur指向剩余链表部分的头,继续下一次的拆分(循环合并剩下的链表)
                cur=next;
            }
        }
        return dummy.next;
    }

    // 题21. 合并两个有序链表
    public ListNode mergeTwoLists(ListNode head1, ListNode head2) {
        ListNode dummy = new ListNode(-1);
        ListNode p= dummy, p1 = head1, p2 = head2;
        while (p1!= null && p2!= null) {
            if (p1.val <= p2.val) {
                p.next = p1;
                p1= p1.next;
            } else {
                p.next = p2;
                p2= p2.next;
            }
            p= p.next;
        }
        if (p1!= null)p.next = p1;
        if (p2!= null)p.next = p2;
        return dummy.next;
    }
}

双向链表

LeetCode 146. LRU 缓存(同剑指 Offer II 031. 最近最少使⽤缓存----双链表+Hash表,数据结构设计题)

原题链接

2023.06.11 一刷

思路:
参考labuladong题解:链接在此
或者参考labuladong个人网站里面的题解(更详细,排版舒服点):链接在此

个人思路梳理:
思路:整体思路参考labuladong题解

  • 要在O(1)时间内找到key对应的值,可以想到需要用到哈希表。
  • 每次访问 cache 中的某个 key,需要将这个元素变为最近使用的,也就是说 cache 要支持在任意位置快速插入和删除元素。这时可以想到双向链表结构,通过 key 快速映射到任意一个链表节点,然后进行插入和删除。

所以

  1. 需要先定义一个双向链表的结点的数据结构Node,用以支持后续双向链表的操作。

  2. 分析双向链表内部需要定义什么方法用来支持LRU机制:
    2.1. 从get(key)入手
     2.1.1 如果get(key)在链表中有对应结点:

  • 返回该key对应结点的val===>return map.get(key).val;
  • 同时由于最近访问了这个key对应的结点,在返回val前,该结点需要标记为“最近被访问过”;
  • 如何实现这个标记?==>规定双链表尾部接纳“最近被访问过”结点,只要结点最近被访问过,就将其在链表中删除,添加到链表尾部。
  • 实现上面的想法需要在双链表中有两个方法:①void remove(Node x)–删除结点x;②void addLast(Node x)–在双链表尾部添加结点x。

   2.1.2 如果key在链表中没有对应结点:

  • return -1;

  2.2 从put(key,val)入手
   2.2.1 如果存在对应的hash映射

  • 根据key找到对应的链表中的结点,删除这个结点,并在链表尾部添加Node(key,val)
    2.2.1 如果不存在对应hash映射
  • 那就意味着需要为这个键值对添加hash映射(key,node(key,val))
  • 添加之前还需要判断当前的双链表长度size是否达到了cache规定的容量capacity,如果已经到了,添加之后会超出,需要先将队头元素删除,再添加这个键值对结点到队尾;
  • 这里就需要双链表有一个新的方法:Node removeFirst();删除双链表第一个结点。可能会疑惑这里为什么需要返回这个结点,因为删除结点后,还需要将hash中这个结点对应的映射关系给删除,需要这个被删除结点的key(map.remove(node.key))
  • 还有双链表的长度也需要一个方法int size()–返回当前链表的长度

  2.3 汇总需要自己设计完成的双链表方法:

  • void remove(Node x)–删除结点x
  • void addLast(Node x)–在双链表尾部添加结点x
  • Node removeFirst()–删除双链表第一个结点
  • int size()–返回当前链表的长度
  1. 编写双链表中的方法
  2. 依赖双链表中的方法编写get与put方法,注意删除结点可能需要删除map映射,添加新结点也可能需要增加对应map映射

代码如下:

// 自己实现双链表

//双向链表的结点结构
class Node{
    public int key,val;//结点需要存储键值对
    public Node pre,next;//前向和后向指针
    public Node(int key,int val){
        this.key=key;
        this.val=val;
    }
}

//双向链表类
class DoubleList{
    //双链表虚拟头、尾结点
    private Node dummyHead,dummyTail;
    private int size;//注意对size的更新时机

    //构造函数,初始化只有虚拟头尾结点的双链表
    public DoubleList(){
        dummyHead=new Node(0,0);
        dummyTail=new Node(0,0);
        dummyHead.next=dummyTail;
        dummyTail.pre=dummyHead;
    }

    // 删除双链表的结点x,时间 O(1)
    public void remove(Node x){
        x.pre.next=x.next;
        x.next.pre=x.pre;
        size--;
    }

    // 在双链表尾巴添加结点x,时间 O(1)
    public void addLast(Node x){
        x.next=dummyTail;//先让x的指针找好要指谁
        x.pre=dummyTail.pre;
        dummyTail.pre=x;//再将原来连接的链断开,指向x
        x.pre.next=x;
        size++;
    }

    // 删除链表头结点,时间 O(1)
    public Node removeFirst(){
        // 链表可能是空的
        if(dummyHead.next==dummyTail){
            return null;
        }
        Node first=dummyHead.next;
        remove(first);//不用在这里面size--,在remove会进行size--
        return first;
    }

    // 返回当前双链表长度
    public int size(){
        return size;
    }
}


class LRUCache {
    // 链表结构:Node(k1, v1) <-> Node(k2, v2)...
    private DoubleList cache;
    // 映射关系:key -> Node(key, val)
    private Map<Integer,Node> map;
    private int cap;

    public LRUCache(int capacity) {
        this.cap=capacity;
        map=new HashMap<>();
        cache=new DoubleList();
    }
    
    public int get(int key) {
        // 先判断是否存在对应的映射
        if(!map.containsKey(key)){
            return -1;
        }
        // 走到这一定存在,对应结点要标记为-最近访问过
        Node x=map.get(key);
        cache.remove(x);
        cache.addLast(x);
        return x.val;
    }
    
    public void put(int key, int value) {
        //先判断是否存在映射关系
        if(map.containsKey(key)){
            // 如果存在,就先删这个结点,再把结点添加到尾巴(注意vale可能不一样)
            Node x=map.get(key);
            cache.remove(x);
            map.remove(key);//结点删除同时映射也要删除
            Node newNode=new Node(key,value);
            cache.addLast(newNode);//把带有新val的结点添加到尾部
            map.put(key,newNode);//添加新的映射
        }else{//如果不存在映射,代表要添加新的结点
            // 先判断容量,已经满了就要先删除最久未使用的元素【注意也要删除映射关系】
            if(cache.size()==cap){
                Node first=cache.removeFirst();
                map.remove(first.key);
            }
            // 添加新结点、新映射
            Node x=new Node(key,value);
            cache.addLast(x);
            map.put(key,x);
        }
    }
}

你可能感兴趣的:(力扣刷题记录,链表,leetcode)