【优选算法】专题九——链表

文章目录

一、链表题目的常用解题技巧总结

  1. 画图--->更加直观、清晰的展现链表结构的变化,从而更容易构建起解题思路
  2. 引入虚拟头结点(哨兵位)--->便于处理很多边界情况,题目给了特殊数据也不用怕空指针访问。而且方便我们对链表进行操作(因为头节点的存在,比如我们不用再分情况进行讨论,而是一直在头节点后进行头插就行)
  3. 定义一个新的指针,指向某个节点,得以提前保存该节点,当原本链表中指向它的指针断开后仍可找到它
  4. 快慢双指针--->判断带环链表、找环的入口、找链表中倒数第n个节点、链表中间节点

二、两数相加

Leetcode链接

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

解题思路

  • 由于数字的每位数是按链表逆序放置,所以我们可以直接遍历两链表,将遍历到的数字相加,取结果个位数放入记录链表,十位数为进位数。
  • 可以发现数字每位数逆序放置其实是比较友好的,因为这和数学运算的顺序是一样的。

代码实现及解析

java 复制代码
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 cur1=l1,cur2=l2;//遍历两链表
        ListNode newHead=new ListNode();//哨兵位
        ListNode tail=newHead;
        int sum=0;
        while(cur1!=null||cur2!=null||sum!=0){//注意这里的判断条件是或者
            if(cur1!=null){
                sum+=cur1.val;
                cur1=cur1.next;
            }
            if(cur2!=null){
                sum+=cur2.val;
                cur2=cur2.next;
            }
            ListNode node=new ListNode(sum%10);//取个位数放入节点
            sum/=10;
            tail.next=node;//尾插
            tail=tail.next;
        }
        return newHead.next;
    }
}

总结

  • 复习解题思路

三、两两交换链表中的节点

Leetcode链接

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

对该题仅进行解题技巧总结:

总结

  • 这是链表很常见的题目,就是要求我们对链表节点的位置进行更改/重排列链表,我们通常的做法就是定义几个指针指向每次更改会影响到的几个节点(防止它们因为指向断开而"丢了"),然后依赖这几个指针对节点的指向进行更改,再让这几个指针按规则移动往后遍历。就这样,遍历完成,节点的指向也均已正确更改
  • 我们在做这种"链表位置更改"的题目时,无论是一次性更改,还是分几段更改,都建议使用"哨兵位"来连接每次更改后的一段新链表,如果在原链表中直接操作会变非常复杂(包括"反转链表"也建议使用哨兵位+在哨兵位后面头插的方法,方便且易懂)
  • 我们面对类似链表"节点奇偶数"这样的问题时,其实就是分类讨论,看遍历到最后哪个指针会先为null,此时可以以此来推断奇/偶数,然后做出对应的处理

四、重排链表

Leetcode链接

给定一个单链表 L 的头节点 head ,单链表 L 表示为:

L0 → L1 → ... → Ln - 1 → Ln

请将其重新排列后变为:

L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → ...

不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

解题思路

  • 根据链表排列的规则,我们可以发现重排列的链表可以由两个链表合并而来。当把链表分为两半,将左、右这两部分链表进行合并就可以得到目标链表。
  • 所以思路就清晰了,我们可以分三步解题,1.先找到链表的中间节点 2.再将右半部分反转 3.最后再将左、右两部分链表合并,我们发现这三个步骤都是之前做过的题目:
  1. 找到链表的中间节点--->快慢双指针

  2. 把后面的部分逆序--->三指针+头插法

  3. 合并两个链表--->双指针

  • 至于链表节点个数奇、偶的问题,前面已经讲过,分情况来讨论。本题通过验证,两种情况都可以让slow指针后面的链表归为右半部分,并将其反转,得到的结果链表一样

代码实现及解析

java 复制代码
/**
 * 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) {
        //1.找到链表的中间节点
        ListNode fast=head,slow=head;
        while(fast!=null&&fast.next!=null){
            fast=fast.next.next;
            slow=slow.next;
        }
        //2.反转slow后面的链表
        ListNode newHead=new ListNode();
        ListNode cur=slow.next;
        ListNode curN=null;
        while(cur!=null){
            curN=cur.next;
            cur.next=newHead.next;
            newHead.next=cur;
            cur=curN;
        }
        slow.next=null;//将左半部分的链表与右半部分的链表断开
        //3.合并左、右两个链表
        ListNode ansHead=new ListNode();
        ListNode cur1=head,cur2=newHead.next;
        ListNode tail=ansHead;
        while(cur1!=null){//本题中左半部分的链表较长,所以一定是cur2先遍历完
            //先放左半部分链表的节点
            tail.next=cur1;
            cur1=cur1.next;
            tail=tail.next;
            //再放右半部分链表的节点
            if(cur2!=null){
                tail.next=cur2;
                cur2=cur2.next;
                tail=tail.next;
            }
        }
        
    }
}

总结

  • 复习解题思路

五、合并 K 个升序链表

Leetcode链接

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

解题思路

方法一:

  • 合并两个有序链表已经做过,那其实合并一堆有序链表的思路和合并两个的思路一致,只不过后者是在两个节点中找出较小的,而前者是在一堆节点中查找出最小的那个。而在一堆数据中不要求严格对其排序,而要求每次找出最值并将其拿出,这不就是优先级队列来干的事吗?

方法二:

  • 使用专题七、八所介绍的分治思想解题,思路很简单、代码框架也没变。

代码实现及解析

方法一(使用优先级队列):

java 复制代码
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) {
        //构建小根堆
        PriorityQueue<ListNode> heap=new PriorityQueue<>(new Comparator<ListNode>(){
            public int compare(ListNode o1,ListNode o2){//这里compare方法的权限一定不能比父类小,所以一定要加上public修饰
                return o1.val-o2.val;
            }
        });
        //先将每个链表的第一个节点放入堆中
        for(ListNode list:lists){
            if(list!=null)//链表不能为空
                heap.offer(list);
        }
        //再利用优先级队列的特性每次将较小的节点拿出尾插到newHead进行排序
        ListNode newHead=new ListNode();
        ListNode tail=newHead;
        while(!heap.isEmpty()){
            ListNode tmp=heap.poll();
            tail.next=tmp;
            if(tmp.next!=null){
                heap.offer(tmp.next);//poll出节点后就要立即将该节点后面的节点offer入heap中
            }
            tail=tail.next;
        }
        return newHead.next;
    }
}

方法二(递归实现):

java 复制代码
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) {
        return mergeSort(lists,0,lists.length-1);
    }
    public ListNode mergeSort(ListNode[] lists,int begin,int end){
        if(begin>end) return null;
        if(begin==end) return lists[begin];

        int midIndex=(begin+end)/2;
        ListNode l1=mergeSort(lists,begin,midIndex);//合并左半部分
        ListNode l2=mergeSort(lists,midIndex+1,end);//合并右半部分

        //再合并l1、l2
        ListNode newHead=new ListNode();
        ListNode cur1=l1,cur2=l2;
        ListNode tail=newHead;
        while(cur1!=null&&cur2!=null){
            if(cur1.val<=cur2.val){
                tail.next=cur1;
                cur1=cur1.next;
                tail=tail.next;
            }else{
                tail.next=cur2;
                cur2=cur2.next;
                tail=tail.next;
            }

        }
        if(cur1!=null) tail.next=cur1;
        else tail.next=cur2;

        return newHead.next;
    }
}

总结

  • 复习解题思路

六、K 个一组翻转链表

Leetcode链接

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

解题思路

  • 又是一个"更改链表节点位置 "题目,我们依然使用"哨兵位" newHead 来接收翻转后的链表,对原链表的节点进行每轮k个节点的多轮头插到新链表中。
  • 以上就是大体思路,一个很重要的细节是cur在遍历原链表将节点头去插入新链表中后会回原链表以便下一次插入(curN=cur.next; ... cur=curN;),这样的话下一轮再想去新链表 尾部进行头插的话就找不到新链表的尾部了(因为cur已经回去了),所以在此轮头插之前先保存下来此轮头插后的链表尾部,其实就是第一个头插的节点(头插的逻辑导致第一个节点会成为链表的尾部),那么直接保存每轮翻转时(头插时)cur的初始值就行(ListNode nextInsertPos=cur;)。

代码实现及解析

java 复制代码
class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        //先统计一下链表长度
        ListNode cur=head;
        int count=0;
        while(cur!=null){
            count++;
            cur=cur.next;
        }
        count/=k;//统计完链表长度,再以此计算需要翻转的次数

        //进行正式操作
        ListNode newHead=new ListNode();
        ListNode tail=newHead;
        cur=head;
        ListNode curN=cur.next;
        for(int i=0;i<count;i++){//进行count轮翻转
            ListNode nextInsertPos=cur;//记录链表的结尾,也就是下轮翻转进行头插的位置(头插,所以第一个插入的节点是最后的末尾)
            for(int j=0;j<k;j++){//每轮翻转k个节点
                //头插k个节点
                curN=cur.next;
                cur.next=tail.next;
                tail.next=cur;
                cur=curN;
            }
            tail=nextInsertPos;//更新下轮头插入位置
        }
        tail.next=cur;//最后再处理下原链表末尾不够k个一组的节点
        return newHead.next;
    }
}

总结

  • 复习解题思路,尤其是第二段的细节处理,这样的情况不常见
相关推荐
weixin_649555673 小时前
C语言程序设计第四版(何钦铭、颜晖)第七章利用数组判断上三角矩阵
算法
星爷AG I3 小时前
14-4 运动控制理论:协同理论(AGI基础理论)
算法·机器学习·agi
I_LPL3 小时前
day48 代码随想录算法训练营 图论专题1
java·算法·深度优先·图论·广度优先·求职面试
absunique4 小时前
多路归并算法在外部排序中的实现与优化的技术7
算法
叶宇燚4 小时前
Java整理--数据结构篇
java·开发语言·数据结构
鹿鸣悠悠4 小时前
【AI-08】Prompt(提示词)
人工智能·算法
数据中穿行4 小时前
12种经典排序算法完整C++实现
算法
晚枫歌F4 小时前
btree B树实现key-value存储
开发语言·数据结构
wangchen_04 小时前
B树、B+树详解
数据结构·b树·哈希算法