链表系列

一、链表基础

1.理论基础

(1)什么是链表?

        链表是一种通过指针串联在一起的线性结构每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
        链接的入口节点称为链表的头结点(head)
        

(2)链表的类型

  • 单链表

  • 双链表
    单链表中的指针域只能指向节点的下一个节点。 双链表的每一个节点有两个指针域一个指向下一个节点,一个指向上一个节点。 双链表既可以向前查询也可以向后查询

  • 循环链表
    循环链表就是链表首尾相连,可以用来解决约瑟夫环问题

(3)链表在内存中的存储方式

        数组是在内存中是连续存储的,而链表在内存中是不连续存储的。
        链表是通过指针域的指针链接在内存中各个节点。 所以链表中的节点在内存中不是连续存储的 ,而是散乱存储在内存中的某地址上,分配机制取决于操作系统的内存管理
        如下图所示,这个链表起始节点为2, 终止节点为7, 各个节点存储在内存的不连续地址空间上,通过指针串联在一起。
        

2.链表的操作

(1)定义一个链表节点

//定义节点
public class ListNode {
    // 节点的两部分组成:结点的值 和 下一个结点
    int val;
    ListNode next;
    
    // 节点的构造函数(无参)
    public ListNode() {
    }
    // 节点的构造函数(有一个参数)
    public ListNode(int val) {
        this.val = val;
    }
    // 节点的构造函数(有两个参数)
    public ListNode(int val, ListNode next) {
        this.val = val;
        this.next = next;
    }
}

(2)删除节点

  • 删除非头结点:删除D节点(如图所示),只要将C节点的next指针指向E节点即可。
        
  • 删除头结点:因为单链表只能指向下一个节点,同时链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点,所以对头结点的处理与其他节点不同。那么如何删除头结点呢?
    • 法一:直接使用原来的链表来进行删除操作,此时只需要将头结点移动到下一个节点,就可以从链表中移除一个头结点。


    • 法二(推荐):设置一个虚拟头结点(dummy node)再进行删除操作,这样原链表的所有节点(包括头结点)就都可以按照统一的方式进行移除了。

      【tips】但是真正的头结点是:dummyNode->next

(3)新增节点

        新增F节点,只需要将F节点的next指针指向D节点,再讲C节点的next指针指向F节点即可。
        
        可以看出链表的增和删都是O(1)操作,不会影响到其他节点。但如果是删除最后一个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)

3.链表与数组的性能对比

        数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
        链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。
        

二、题目类型

1.  203. 移除链表元素

  • 思路一:直接使用原链表,需要对删除头结点单独处理。细节多,麻烦,不推荐。
    /**
     * 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 removeElements(ListNode head, int val) {
            //先删除值等于val的头结点
            while(head!=null&&head.val==val){//★这里要写上head!=null,因为head等于空时根本没有val,会报错!
                head=head.next;
            }
            //★空链表 或者 所有节点值都等于val的情况,经while循环后head也是=null
            if(head==null){
                return head;
            }
            //head.val肯定不等于val了
            //★前一个节点和当前节点,因为head.val肯定不等于val了,所以从head的下一个节点开始处理,因此当前节点初始值是head.next
            ListNode pre=head;
            ListNode curr=head.next;
            //遍历链表
            while(curr!=null){
                //如果当前节点的val==val,删除当前节点,把当前节点的上一个节点的next指向当前节点的下一个节点,当前节点后移
                if(curr.val==val){
                    pre.next=curr.next;
                }else{//如果当前节点的值不等于val,上一个节点和当前节点都后移
                    pre=curr;
                }
                curr=curr.next;
            }
            return head;
        }
    }
  • 思路二(推荐):设置虚拟头节点,对所有节点的删除处理都一样,返回的头结点是虚拟头节点的下一个节点。
    public ListNode removeElements(ListNode head, int val) {
        //创建一个虚拟节点
        ListNode dummyNode=new ListNode(-1,head);
        //前一个节点和当前节点
        ListNode pre=dummyNode;
        ListNode curr=head;
        //遍历链表
        while(curr!=null){
            if(curr.val==val){
                pre.next=curr.next;
            }else{
                pre=curr;
            }
            curr=curr.next;
        }
        return dummyNode.next;
    }
    【tips】可以看到设置虚拟头节点后,直接统一处理要删除的节点即可,而这部分代码与直接使用原链表对非头部节点的处理一样。

2.  ★707. 设计链表

  • 这道题目涵盖了链表的常见操作:获取指定节点的值、删除节点、插入节点(头、尾、中间)。所有操作全采用虚拟头结点来做。
    //定义节点
    class ListNode{
        //当前节点的值
        int val;
        //指向下一个节点的指针
        ListNode next;
        //无参构造
        public ListNode(){};
        //两个带参构造
        public ListNode(int val){
            this.val = val;
        }
        public ListNode(int val,ListNode next){
            this.val = val;
            this.next=next;
        }
            //没用到get/set方法,所以不用定义
    }
    //定义单链表
    class MyLinkedList {
        //链表节点数,★用来判断index是否合法
        int size;
        //★虚拟头节点,因为初始单链表里面是没有节点的,所以这个head是虚拟头结点。
        ListNode head;
        //★构造方法,初始化链表:长度为0,因为head为虚拟头节点因此head的next不用给,★也不能是null!(虚拟头结点嘛,next应该是真正的head,但是初始化时没有头,所以next不给。)
        public MyLinkedList(){
            head=new ListNode();//虚拟头结点的值随便给,不给也行
            size=0;
        }
        //获取链表中第 index 个节点的值。如果索引无效,则返回-1。【★注意虚拟头结点不算在index内,头结点才是第0个节点,所以index也不能等于size!】
        public int get(int index) {
            //索引无效,返回-1
            if(index<0||index>=size){
                return -1;
            }
            ListNode curNode=head.next;
            //找第index个节点
            for(int i=0;i<index;i++){//关于循环条件的判断:通过找极端情况,看当index=0时(即返回头结点val)是否成立。
                curNode=curNode.next;
            }
            return curNode.val;
        }
        //在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
        public void addAtHead(int val) {
            addAtIndex(0,val);
        }
        //将值为 val 的节点追加到链表的最后一个元素。
        public void addAtTail(int val) {
            addAtIndex(size,val);
        }
        //在链表中的第index个节点之前添加值为val的节点:如果index等于链表长度,则该节点将附加到链表的末尾;如果index大于链表长度,则不会插入节点(即返回空);如果index小于0,则在头部插入节点
        public void addAtIndex(int index, int val) {
            //index 大于链表长度,则不会插入节点
            if(index>size){
                return;
            }
            //★index小于0,则在头部插入节点,即在第0个节点前插入新节点,所以index<0等价于index=0。
            if(index<0){
                index=0;
            }
            //初始化第0个节点的前一个节点
            ListNode preNode=head;
            //找第index个节点的前一个节点
            for(int i=0;i<index;i++){//在第 index 个节点前插,所以不能=index
                preNode=preNode.next;
            }
            //创建新节点
            ListNode newNode=new ListNode(val);
            //插入新节点
            newNode.next=preNode.next;
            preNode.next=newNode;
            //插入成功,size++
            size++;
        }
        //如果索引 index 有效,则删除链表中的第 index 个节点。
        public void deleteAtIndex(int index) {
            //索引无效,直接返回
            if(index<0||index>=size){
                return;
            }
            //初始化第0个节点的前一个节点
            ListNode preNode=head;
            //找第 index 个节点的前一个节点
            for(int i=0;i<index;i++){
                preNode=preNode.next;
            }
            //删除节点
            preNode.next=preNode.next.next;
            //删除成功,size--
            size--;
        }
    }
    

3.  ★206. 反转链表

  • 思路一:实现链表元素的反转只需要改变链表next指针的指向,直接将链表反转。要进行节点反转,就需要知道上一个节点、当前节点以及反转后要移动到下一个节点,因此我们先定义两个指针pre和cur,分别指向上一个节点和当前要反转的节点;反转完该节点后,因为该节点的next已经指向上一个节点了,这样就找不到它的下一个节点了,所以我们定义了一个临时节点,在反转节点前先来暂存它的下一个节点。
    public ListNode reverseList(ListNode head) {
        ListNode pre=null;
        ListNode cur=head;
        //临时节点
        ListNode temp=null;
        while(cur!=null){
            //★保存当前节点的下一个节点。因为单链表的节点只能通过指针获得,而如果将cur.next指向pre后,就没有指针指向原来cur的下一个节点了,无法进行cur的后移了,因此需要这样一个临时节点来暂存cur的下一个节点。
            temp=cur.next;
            //反转节点
            cur.next=pre;
            //pre和cur后移
            pre=cur;
            cur=temp;
        }
        return pre;
    }
  • 思路二:递归法。
    public ListNode reverseList(ListNode head) {
        return reverseNode(null,head);//  ★这里当前节点是头结点,反转后成为最后一个节点,所以指向的前一个节点是null
    }
    // 反转当前节点cur和前一个节点pre public ListNode reverseNode(ListNode pre,ListNode cur){
        // 递归结束的条件
        if(cur==null){
            return pre;
        }
        // 暂存下一个节点
        ListNode temp=cur.next;
        // 反转
        cur.next=pre;
            //  移动指针
        return reverseNode(cur,temp);
    }

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

  • 思路一:令 temp 表示当前两两交换的双节点子链的虚拟头结点(即每次需要交换 temp 后面的两个节点),初始时 temp = dummyHead。
                 具体实现:
                        如果 temp 的后面没有节点或者只有一个节点,则没有更多的节点需要交换,因此结束交换。
                        否则,获得 temp 后面的两个节点 first 和 second,即交换之前的节点关系是 temp -> first -> second,交换之后的节点关系要变成 temp -> second -> first;再令 temp = first,一个双节点子链交换完。
                        对链表中的其余节点进行两两交换,直到全部节点都被两两交换。
                        两两交换链表中的节点之后,新链表的头节点是 dummy.next,返回即可。
    public ListNode swapPairs(ListNode head) {
        //虚拟头结点,用来获取头结点:dummy.next=head
        ListNode dummy=new ListNode(-1);
        dummy.next=head;
        //用来在每次交换节点时代替虚拟头结点的作用。因为虚拟头结点是用来返回最后的头结点的,不能在交换节点时移动它,所以需要用temp来代替。
        ListNode temp=dummy;
        //至少有两个节点才可以交换
        //交换临时虚拟头结点后的两个节点
        while(temp.next!=null&&temp.next.next!=null){
            //上面的循环条件同时保证了first和second不为空
            ListNode first=temp.next;
            ListNode second=first.next;
            //交换temp后的两个节点
            first.next=second.next;
            second.next=first;
            //★将上一次两两交换后的链表与这一次链表联系起来!
            temp.next=second;
            //将temp后移两个位置
            temp=first;
        }
        return dummy.next;
    }
  • 思路二:递归法。递归法的三个问题:终止条件、返回值、单次过程,其实就是搞清楚原来的函数返回的是什么?函数作用是什么?找准终止条件即可。这道题终止条件很明显:当链表为空或者链表只有一个节点时终止。而这个swapPairs()函数的作用就是交换以形参为第一个节点的两个节点,并返回交换后的头结点。至此递归的三个问题我们搞清楚了。
                  具体实现:我们将前三个节点当成one、two、three三个节点,那么就有one=head,two=one.next,three=two.next,这样更好理解一点。
    //★swapPairs(ListNode head)的意义就是交换以head为头结点的两个节点,并返回交换后的新的头结点。
    public ListNode swapPairs(ListNode head) {
        //终止条件
        if(head==null||head.next==null){
            return head;
        }
        //one->two->three->...
        ListNode one=head;
        ListNode two=one.next;
        ListNode three=two.next;
        //交换节点one和two
        two.next=one;
        one.next=swapPairs(three);//★one.next应该是下一组两两交换后的新的头结点,所以通过swapPairs来返回这个新的头结点。
        //返回头结点:交换后的头结点一定是第二个节点
        return two;
    }

5.  19.删除链表的倒数第N个节点

  • 思路一:转为删除链表的正数节点问题。先求出链表长度size,那么删除倒数第n个节点等价于删除正数第(size-n)个节点(头结点为第0个节点)。
    public ListNode removeNthFromEnd(ListNode head, int n) {
        //题目已规定n一定有效
        //虚拟头结点
        ListNode dummy=new ListNode(-1);
        dummy.next=head;
        //循环指针
        ListNode temp=new ListNode(-1);
        temp.next=head;
        //链表长度
        int size=0;
        while(temp.next!=null){
            temp=temp.next;
            size++;
        }
        //删除倒数第n个节点即删除正数第(size-n)个节点(从0开始算)
        int index=size-n;
        ListNode pre=dummy;
        ListNode cur=dummy.next;
        for(int i=0;i<index;i++){
            pre=pre.next;
            cur=cur.next;
        }
        //删除节点
        pre.next=cur.next;
        return dummy.next;
    }
  • 思路二:★双指针法。因为删除节点需要知道前一个节点,所以可以利用双指针,让两个指针始终保持相距(n+1)个位置因为只有这样同时移动的时候slow才能指向删除节点的上一个节点方便做删除操作,然后同时移动,这样当快指针为空时,慢指针刚好指向要删除节点的前一个节点。
    public ListNode removeNthFromEnd(ListNode head, int n) {
        //因为删除节点一定是需要知道前一个节点的,所以可以利用双指针,让两个指针保持相距(n+1)个位置的同时移动,这样当快指针为空时,慢指针刚好指向要删除节点的前一个节点。
        ListNode dummy=new ListNode(-1);
        dummy.next=head;
        //初始化双指针为虚拟头结点
        ListNode fast=dummy;
        ListNode slow=dummy;
        //先让快指针走n+1,这样就保证了后面双指针同时移动时二者始终保持n+1的距离在移动。
        for(int i=1;i<=n+1;i++){
            fast=fast.next;
        }
        //同时移动双指针,直到快指针为空,这时慢指针一定指在要删除节点的前一个节点
        while(fast!=null){
            fast=fast.next;
            slow=slow.next;
        }
        //删除节点
        slow.next=slow.next.next;
        return dummy.next;
    }

6.  面试题 02.07. 链表相交/160. 相交链表

  • 这道题难在理解上。让pA指向链表A的头,pB指向链表B的头,然后分别用它们遍历两个链表,遍历完自己的链表后再从头开始遍历另一条链表,此时pA和pB是在“同一位置上”移动了,这样如果有交点,pA和pB就会相遇,返回交点;如果再遍历完,说明没交点,返回空。
  • 具体实现:
            当pA不为空,则将pA移到下一个节点;当pB不为空,则将pB移到下一个节点;
            如果pA为空,说明链表A遍历完了,则将pA 移到链表B的头节点;如果pB 为空,说明链表B遍历完了,则将pB移到链表A的头节点;
            ★当pA和pB指向同一个节点或者都为空时,返回它们指向的节点或者null。(这也是为什么循环条件是pA!=pB,以及if判断是pA==null而不是pA.next==null,两个条件要一致)
  • 现在来解释一下为什么要先遍历自己再遍历别人:
            第①种情况——相交:那么相交部分节点数为c,不相交部分链表A有a个节点,链表B有b个节点。现在开始遍历:
                    1)a≠b:pA和pB在第一次遍历各自链表时不会相遇,pA遍历完链表A走了(a+c)个节点,再从链表B开始走b个节点,共走了(a+c+b)个节点;同理,pB遍历完链表B走了(b+c),再从链表A的头走a,共走了(b+c+a)个节点,显然a+c+b=b+c+a,所以现在二者在交点相遇。
                    2)a=b:pA和pB在第一次遍历就相遇在交点了。
            
            第②种情况——不相交:因为循坏条件是pA != pB,同时又没有交点,所以无论链表A、B长度是否相同,最终都会同时变为null,不满足进入循环的条件然后退出循环,返回的也就是null了。
  • public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        // 任一链表为空,则必不存在相交节点
        if(headA==null||headB==null){
            return null;
        }
        ListNode pA=headA;
        ListNode pB=headB;
            //退出循环的条件时pA=pB,要注意:如果存在交点,那么最终pA=pB=交点退出循环,返回交点;如果不存在交点,pA和pB一定是遍历完自己和对方后,同时到达null,所以还是pA=pB=null退出循环,返回空。
        while(pA!=pB){
            pA=pA==null?headB:pA.next;
            pB=pB==null?headA:pB.next;
        }
        return pA;
    }

7.  ★142.环形链表II

  • 原理:这道题有两大难点,参考代码随想录https://www.programmercarl.com/0142.%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8II.html#_142-%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8ii
    (一)如何判断是否有环?
    ——可以使用双指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针相遇,说明这个链表有环。同时,如果二者能相遇,那么一定是在环内相遇,因为fast走的快,一定会在未相遇前先进入环

    (二)若有环,如何找入环节点?

    ——fast和slow在环内相遇后,再令定义两个指针index1=相遇节点index2=head,让index1和index2同时移动,每次走一步,那么二者相遇的节点,就是入环节点

  • 思路:解决了以上两个问题后,代码就好写了:
    public ListNode detectCycle(ListNode head) {
            //从头结点出发
        ListNode fast=head;
        ListNode slow=head;
            //至少有两个节点时才可能有环。如果连这个条件都不满足,不进while,直接return null
        while(fast!=null && fast.next!=null){
                    //快指针每次走两步,慢指针每次走一步
            fast=fast.next.next;//上面的循环条件同时也保证了fast.next.next有效。
            slow=slow.next;
            if(fast==slow){//有环,环内相遇
                //找入环节点,分别从头结点和相遇节点开始走
                ListNode idx1=head;
                ListNode idx2=fast;
                while(idx1!=idx2){
                    idx1=idx1.next;
                    idx2=idx2.next;
                }
                            //返回入环节点
                return idx1;
            }
        }
        //fast==null了,也无环
        return null;
    }

8.2. 两数相加

  • 思路一:双指针遍历给出的两个链表,用一个变量flag判断上一次是否有进位(即本次需不需要加1),同时要判断本次相加后需不需要向后进位。而且如果两链表长度不同,剩下的直接移到新链表上即可。注意要判断最后一次需不需要进位,如果需要进位,还要再创建一个值为1的节点。
    class Solution {
        public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
            //有一个为空,结果就是另一个链表
            if(l1==null) return l2;
            if(l2==null) return l1;
            //双指针,用来遍历两个链表
            ListNode p1=l1;
            ListNode p2=l2;
            ListNode dummyNode=new ListNode();
            ListNode pre=dummyNode;
            //用来判断上一个节点是否进位了
            boolean flag=false;
            //只要有一个为空,跳出while
            while(p1!=null&&p2!=null){
                int sum=p1.val+p2.val;
                ListNode node=new ListNode();
                if(flag){
                    sum+=1;
                }
                //不需要进位
                if(sum<10){
                    node.val=sum;
                    flag=false;
                }else{
                    //需要进位,最多进1
                    node.val=sum%10;
                    flag=true;
                }
                pre.next=node;
                pre=node;
                p1=p1.next;
                p2=p2.next;
            }
            //链表长度不同时
            while(p1!=null){
                ListNode node=new ListNode();
                int sum=p1.val;
                if(flag){
                    sum+=1;
                }
                if(sum<10){
                    node.val=sum;
                    flag=false;
                }else{
                    node.val=sum%10;
                    flag=true;
                }
                pre.next=node;
                pre=node;
                p1=p1.next;
            }
            while(p2!=null){
                ListNode node=new ListNode();
                int sum=p2.val;
                if(flag){
                    sum+=1;
                }
                if(sum<10){
                    node.val=sum;
                    flag=false;
                }else{
                    node.val=sum%10;
                    flag=true;
                }
                pre.next=node;
                pre=node;
                p2=p2.next;
            }
            //对最后一次是否进位的判断
            if(flag){
                ListNode node=new ListNode(1);
                pre.next=node;
            }
            return dummyNode.next;
        }
    }
  • ★优化:
    class Solution {
        public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
            ListNode dummyHead = new ListNode(-1), pre = dummyHead;
            int t = 0;
            while (l1 != null || l2 != null || t != 0) {
                if (l1 != null) {
                    t += l1.val;
                    l1 = l1.next;
                }
                if (l2 != null) {
                    t += l2.val;
                    l2 = l2.next;
                }
                pre.next = new ListNode(t % 10);
                pre = pre.next;
                t /= 10;
            }
            return dummyHead.next;
        }
    }

9.21. 合并两个有序链表

  • 思路一:原地合并。
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        if(list1==null) return list2;
        if(list2==null) return list1;
        ListNode dummy=new ListNode();
        ListNode cur=dummy;
        while(list1!=null&&list2!=null){
            if(list1.val<=list2.val){
                cur.next=list1;
                cur=cur.next;
                list1=list1.next;
            }else{
                cur.next=list2;
                cur=cur.next;
                list2=list2.next;
            }
        }
        if(list1==null){
            cur.next=list2;
        }
        if(list2==null){
            cur.next=list1;
        }
        return dummy.next;
    }
  • 思路二:递归。
    class Solution {
        public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
            if(list1==null) return list2;
            if(list2==null) return list1;
            if(list1.val<=list2.val){
                list1.next=mergeTwoLists(list1.next,list2);
                return list1;
            }else{
                list2.next=mergeTwoLists(list1,list2.next);
                return list2;
            }
        }
    }

10.23. 合并 K 个升序链表

  • 思路一:优先级队列——小顶堆。
    class Solution {
        //lists里放的是各个链表的头结点!
        public ListNode mergeKLists(ListNode[] lists) {
            int k=lists.length;
            if(lists==null||k==0) return null;
            //小顶堆(从队头到队尾递减)
            // PriorityQueue<ListNode> pq=new PriorityQueue<>(new Comparator<ListNode>(){
            //     @Override
            //     public int compare(ListNode o1,ListNode o2){
            //         return o1.val-o2.val;
            //     }
            // });
            PriorityQueue<ListNode> pq=new PriorityQueue<>((o1,o2)->o1.val-o2.val);
            //先把k个链表的每个头结点放入pq
            for(ListNode list:lists){
                //★注意判断list为空的情况
                if(list!=null){
                    pq.offer(list);
                }
            }
            ListNode dummyNode=new ListNode(-1);
            ListNode cur=dummyNode;
            while(!pq.isEmpty()){
                ListNode newNode=pq.poll();
                cur.next=newNode;
                cur=cur.next;
                //始终维护容量为k的小顶堆
                if(newNode.next!=null){
                    pq.offer(newNode.next);
                }
            }
            return dummyNode.next;
        }
    }
  • 思路二:★分治。将k个链表两两合并,一轮后再两两合并,直到最后只剩下一个链表。
    class Solution {
        public ListNode mergeKLists(ListNode[] lists) {
            int k=lists.length;
            //★分治终止条件
            //1.lists为空
            //2.lists只有一个链表头结点,直接返回该头结点
            //3.lists只有两个链表的头结点,返回合并这两个头结点
            if(lists==null||k==0) return null;
            if(k==1) return lists[0];
            if(k==2) return merge2Lists(lists[0],lists[1]);
            //2路归并
            int mid=k/2;
            //分治(分成两组)
            ListNode[] l1=new ListNode[mid];
            for(int i=0;i<mid;i++){
                l1[i]=lists[i];
            }
            ListNode[] l2=new ListNode[k-mid];
            for(int i=0;i<k-mid;i++){
                l2[i]=lists[mid+i];
            }
            //归并(★每组再分治再合并)
            return merge2Lists(mergeKLists(l1),mergeKLists(l2));
        }
        //合并两个有序链表
        public ListNode merge2Lists(ListNode l1,ListNode l2){
            if(l1==null) return l2;
            if(l2==null) return l1;
            ListNode dummy=new ListNode();
            ListNode cur=dummy;
            while(l1!=null&&l2!=null){
                if(l1.val<=l2.val){
                    cur.next=l1;
                    cur=cur.next;
                    l1=l1.next;
                }else{
                    cur.next=l2;
                    cur=cur.next;
                    l2=l2.next;
                }
            }
            if(l1==null){
                cur.next=l2;
            }else{
                cur.next=l1;
            }
            return dummy.next;
        }
    }

11.★148. 排序链表

        要求:O(nlogn) 时间复杂度、O(1)空间复杂度。看到O(nlogn)的时间复杂度,首先要想到用归并和快排。对数组的常规归并需要额外创建数组,满足不了O(1)的空间复杂度,但是对链表的归并可以不使用递归来实现O(1)的空间复杂度。
  • 思路一归并排序+递归。利用归并的思想,递归地将当前链表分为两段,然后merge。分两段的方法是使用快慢指针找中间位置。merge时,就是合并两个有序链表的知识了。
    class Solution {
        public ListNode sortList(ListNode head) {
            return mergeSort(head);
        }
        //归并排序
        public ListNode mergeSort(ListNode head){
            //没有节点或只有一个节点,不需要排序,直接返回
            if(head==null||head.next==null) return head;
            //快慢指针
            ListNode slow=head,fast=head;
            //★用来记录中点的上一个节点,即左半部分链表的最后一个节点
            ListNode last=null;
            while(fast!=null&&fast.next!=null){
                last=slow;
                fast=fast.next.next;
                slow=slow.next;
            }
            //★断链——将左半部分最后一个节点的next置为空,以此来断开原链表
            last.next=null;
            //对左、右半链表进行归并排序
            ListNode left=mergeSort(head);
            ListNode right=mergeSort(slow);
            //合并
            return mergeList(left,right);
        }
        //合并两个链表
        public ListNode mergeList(ListNode left,ListNode right){
            ListNode dummy=new ListNode(-1);
            ListNode cur=dummy;
            while(left!=null&&right!=null){
                if(left.val<right.val){
                    cur.next=left;
                    cur=cur.next;
                    left=left.next;
                }else{
                    cur.next=right;
                    cur=cur.next;
                    right=right.next;
                }
            }
            if(left==null){
                cur.next=right;
            }else{
                cur.next=left;
            }
            return dummy.next;
        }
    }
  • ★思路二归并+自底向上。自底向上的归并思路是:先两个两个的 merge,完成一趟后,再 4 个4个的 merge,...,直到结束。
        举例:对于[4,3,1,7,8,9,2,11,5,6],
            
    class Solution {
        public ListNode sortList(ListNode head) {
            if(head==null||head.next==null) return head;
            //计算链表长度
            ListNode p=head;
            int len=0;
            while(p!=null){
                len++;
                p=p.next;
            }
            //虚拟头结点
            ListNode dummy=new ListNode(-1);
            dummy.next=head;
            //每次将链表拆分成若干个长度为step的子链表,并两两合并
            for(int step=1;step<len;step*=2){
                ListNode pre=dummy,cur=dummy.next;
                //★每次循环生成l1、l2、left三条链,将l1和l2合二为一,下一次循环对left进行重复处理
                while(cur!=null){
                    ListNode l1=cur;
                    //★将cur移动到当前子串末尾!注意不是下一个子串开头,因为这样就没法断链了(cur.next=null)!
                    for(int i=1;i<step && cur!=null && cur.next!=null;i++){
                        cur=cur.next;
                    }
                    ListNode l2=cur.next;
                    //断链
                    cur.next=null;
                    cur=l2;
                    for(int i=1;i<step && cur!=null && cur.next!=null;i++){
                        cur=cur.next;
                    }
                    ListNode left=null;
                    //★记得判断!
                    if(cur!=null){
                        left=cur.next;
                        cur.next=null;
                    }
                    cur=left;
                    ListNode merged=mergeList(l1,l2);
                    //★pre的作用:将上次合并链表与这次合并的链表连接起来!
                    pre.next=merged;
                    //pre移动到本次合并后的链表尾部
                    while(pre.next!=null){
                        pre = pre.next;
                    }
                }
            }
            return dummy.next;
        }
        //合并两个链表
        public ListNode mergeList(ListNode left,ListNode right){
            ListNode dummy=new ListNode(-1);
            ListNode cur=dummy;
            while(left!=null&&right!=null){
                if(left.val<right.val){
                    cur.next=left;
                    cur=cur.next;
                    left=left.next;
                }else{
                    cur.next=right;
                    cur=cur.next;
                    right=right.next;
                }
            }
            if(left==null){
                cur.next=right;
            }else{
                cur.next=left;
            }
            return dummy.next;
        }
    }
  • 思路三快速排序
    class Solution {
        public ListNode sortList(ListNode head) {
            return quickSort(head);
        }
        public ListNode quickSort(ListNode head) {
            if (head==null || head.next==null) return head;
            //用中间节点的原因是,如果每次用最左边的结点,对于纯递增和纯递减的case就退化为O(n)
            //以中间节点作为pivot,链表可被分为三个子链,即l1:小于pivot、l2:等于pivot、l3:大于pivot
            int pivot = getMid(head).val;
            //★分别定义三个子链的头l(虚拟头)和尾t
            ListNode l1 = new ListNode();
            ListNode l2 = new ListNode();
            ListNode l3 = new ListNode();
            ListNode t1 = l1;
            ListNode t2 = l2;
            ListNode t3 = l3;
            ListNode curr = head;
            //遍历链表节点并与pivot比较,将节点放到对应子链l中
            while (curr!=null) {
                ListNode next = curr.next;
                if (curr.val < pivot) {
                    curr.next = null;
                    t1.next = curr;
                    t1 = t1.next;
                } else if (curr.val == pivot) {
                    curr.next = null;
                    t2.next = curr;
                    t2 = t2.next;
                } else {
                    curr.next = null;
                    t3.next = curr;
                    t3 = t3.next;
                }
                curr = next;
            }
            //对l1和l3分别进行递归快排
            l1 = quickSort(l1.next);
            l3 = quickSort(l3.next);
            //★l1和l3不一定有元素,l2一定有元素(至少是pivot),所以先将l2和l3连起来(为什么先连l2和l3呢?因为l3为空相当于l2最后一个节点->null)。
            l2 = l2.next;//此时的l2是真正的头节点了
            t2.next = l3;
            //★如果l1链表为空,直接返回l2
            if (l1==null) {
                return l2;
            } else {
                // 否则找到l1的结尾,连上l2的头,返回l1
                t1 = l1;
                // 找到t1链表的结尾
                while (t1.next!=null) {
                    t1 = t1.next;
                }
                t1.next = l2;
                return l1;
            }
        }
        //找中间节点
        public ListNode getMid(ListNode head) {
            ListNode fast = head;
            ListNode slow = head;
            while (fast!=null && fast.next!=null) {
                fast = fast.next.next;
                slow = slow.next;
            }
            return slow;
        }
    }
12.




















全部评论

相关推荐

杨柳哥:这不是普通人,那这个钱的是天才
点赞 评论 收藏
分享
11-18 09:44
Java
小白也想要offer:简历别放洋屁,搞不还还放错了,当然你投外企除外,以上纯属个人观点
点赞 评论 收藏
分享
1 3 评论
分享
牛客网
牛客企业服务