五道LeetCode《中等难度》的单链表题

1. 剑指 Offer II 021. 删除链表的倒数第 n 个结点

在这里插入图片描述
题目描述:

找到链表的倒数第n个结点,并删除该结点

三种解法:

第一种解法(单指针):

1.遍历链表,求出链表长度 L
2.链表长度 L 减去 n ,就是倒数第 n 个结点
3.找到倒数第 n 个结点的前驱结点,让前驱结点的 next 指向倒数第 n 个结点的后一个结点。
4.倒数的n个结点的前驱结点,就是 L-n+1
5.因为如果要删除的是头节点,要删除的头节点没有先驱结点,所以设置一个哨兵位
6.哨兵位的 next 指向 head ,这样头结点删除的情况也被转化为了通用情况

在这里插入图片描述

代码如下:

class Solution {
    
    
    public ListNode removeNthFromEnd(ListNode head, int n) {
    
    
    			
        ListNode dummy = new ListNode(0, head);//设置哨兵位
        int length = getLength(head);//求出链表长度
        ListNode cur = dummy;//用cur去遍历
        //遍历L-n+1次就是要删除结点的前驱结点
        for (int i = 1; i < length - n + 1; ++i) {
    
    
            cur = cur.next;
        }
        //前驱结点的next指向要删除结点的后一个结点
        cur.next = cur.next.next;
        return dummy.next;
    }
		//求出链表长度
    public int getLength(ListNode head) {
    
    
        int length = 0;
        while (head != null) {
    
    
            ++length;
            head = head.next;
        }
        return length;
    }
}

作者总结:

如果要删除的结点是最后一个结点,那么该代码的时间复杂度会达到 2×n ,虽然不是很慢,但是依旧没有达到想要的效果。

复杂度分析:

  • 时间复杂度O(n),其中 N 是给定链表中的结点数目。
  • 空间复杂度O(1)

第二种解法(栈):

实现思路:

我们可以在遍历链表的同时,顺便将链表中的结点依次存放到栈上,根据栈「先进后出」的原则,我们栈中弹出的第n个结点,就是我们要删除的结点,并且目前栈顶的节点就是待删除节点的前驱节点。这样一来,删除操作就变得十分方便了。

具体实现:

1.创建一个栈,将链表中的结点依次压入栈中
2.当链表遍历完成之后,依次从栈中弹出结点,到第n个结点时,就是我们要删除的结点
3.由于当前栈顶的结点刚好时第n个结点前驱结点,所以将栈顶的结点的next指向第 n 个结点的 next 就可以
4.由于需要考虑要删除的是头节点,所以我们设置一个哨兵位,这样即使要删除的结点是头节点,处理方法也和普通结点一样。

如图:

在这里插入图片描述

代码如下:

class Solution {
    
    
    public ListNode removeNthFromEnd(ListNode head, int n) {
    
    
        ListNode dummy = new ListNode(0, head);//创建哨兵位
        Deque<ListNode> stack = new LinkedList<ListNode>();//创建一个栈
        ListNode cur = dummy;//cur代替哨兵位遍历
        while (cur != null) {
    
    
        //依次放入
            stack.push(cur);
            cur = cur.next;
        }
        //弹出倒数n个结点
        for (int i = 0; i < n; ++i) {
    
    
            stack.pop();
        }
        //当前栈顶就是要删除结点的前驱结点
        ListNode prev = stack.peek();
        //
        prev.next = prev.next.next;
        return dummy.next;
    }
}

作者总结:

栈最大的特点是先进后出,所以逆序输出是栈经常用到的一个应用场景。首先把所有元素依次入栈,然后把所有元素出栈并输出,这样就实 现了逆序输出。

复杂度分析:

  • 时间复杂度O(n),其中 N 是给定链表中的结点数目。
  • 空间复杂度O(n),其中 N 是链表的长度,主要为栈的开销。

第三种解法(双指针):

解题思路:

通过双指针,由于是删除倒数第 n 个结点,所以我们可以用距离的差,快指针先走 n+1 步,接下来两个指针一起走,当快指针为空时,此时的慢指针就是倒数第n-1个结点( n-1 是 n 的前驱结点)。

具体实现:

  1. 定义两个指针,curprev
  2. cur先走 n+1 步,然后两个指针一起向后遍历,直到cur为空时,此时prev就是要删除结点的前驱结点,prev.next=prev.next.next 就行。
  3. 定义一个哨兵位,使 curprev 最开始都指向 newHead

如图:
在这里插入图片描述
代码如下:

class Solution {
    
    
    public ListNode removeNthFromEnd(ListNode head, int n) {
    
    
    //判断是否为空
        if(head==null) {
    
    
            return head;
        }
        //创建一个哨兵位
        ListNode newHead=new ListNode(0);
        //哨兵位的next指向head
        newHead.next=head;
        //将cur和prev同时指向newHead
        ListNode cur=newHead;
        ListNode prev=newHead;
		//快指针先走n+1步(也可以快指针总head走,走n步就可以,核心思路就是慢指针要和快指针之间的距离差1)
        for(int i=0;i<n+1;i++) {
    
    
            cur=cur.next;
        }
        //两个指针一起走
        while(cur!=null) {
    
    
            cur=cur.next;
            prev=prev.next;
        }
        //最后删除倒数第n个结点
        prev.next=prev.next.next;
        //返回newHead的next
        return newHead.next;
        
    }
}

作者总结:

不同于之前做的找到倒数第k个结点,该题是要删除倒数第k个结点,所以我们需要找到倒数第k个数的前驱节点,核心思路就是,快指针和慢指针最开始的距离差要为 n+1,不论是相同位置快指针先走 n+1 步,又或者慢指针是快指针的前一个结点,快指针走 n 步,最终要到达的目的都是和快指针的距离为 n+1,所以掌握核心思路,再去思考这道题就会很轻松。

复杂度分析:

  • 时间复杂度O(n),其中 N 是给定链表中的结点数目。
  • 空间复杂度O(1)

2. 删除排序链表中的重复元素 II(重点)

在这里插入图片描述

题目描述:

将有序链表中所有重复出现的数,都删除掉,只留下出现一次的数字。

需要考虑的事项:

  1. 考虑头删和尾删,
  2. 注意空指针异常,

一种普通状态:

1->2->3->3->4->4->5->null (中间删)

两种特殊状态:

1->1->1->2->3->null (头删)
1->2->3->3->3->null (尾删)

普通状态

在这里插入图片描述

解题思路:

  1. 定义三个指针,prev cur curNext
  2. 判断 cur 和 curNext 他们的 val 是否相等,如果不相等则把 cur 给 prev, curNext 给 cur,curNext=curNext.next.

如图:

在这里插入图片描述

重复上述操作,直到 cur.val == curNext.val

在这里插入图片描述

再次判断 curNext.val == cur.val 是否为真,如果为真,继续重复上述操作,

最后运行结果:

在这里插入图片描述

特殊状态(头结点重复时)

在这里插入图片描述

如果此时依旧是通过上述的代码运行,会爆出空指针异常的错误,因为的的 prev 是 null ,所以prev.next 就会产生空指针异常,解决的方法就是,判断是否是头删,如果是头删则 head=curNext,将头节点后移。

在这里插入图片描述

此时我们的头删就完成了

运行结果:

在这里插入图片描述

特殊状态(删除尾结点时)

在这里插入图片描述

这里需要注意空指针异常,具体细节如下

在这里插入图片描述
解决之后运行结果:

在这里插入图片描述
具体代码如下:

class Solution {
    
    
    public ListNode deleteDuplicates(ListNode head) {
    
    
        if(head==null||head.next==null) {
    
    
            return head;
        }
        ListNode prev=null;
        ListNode cur=head;
        ListNode curNext=cur.next;
        while(curNext!=null) {
    
    
            //当他们的值不同时,集体后移,注意顺序
            if(cur.val!=curNext.val) {
    
    
                prev=cur;
                cur=curNext;
                curNext=curNext.next;
            } else {
    
    
                //当他们的值相同时,并且curNext不为空,进入循环
                //注意要将curNext!=null的判断条件写在左边
                //逻辑运算&&,先判断左边,如果左边为真才判断右边,
                //当你的curNext!=null写在右边时,
                //他会先进行curNext.val的操作,此时你的curNext已经为空
                //再去访问就会发出空指针异常
                while(curNext!=null&&cur.val==curNext.val) {
    
    
                    curNext=curNext.next;
                }
                //判断prev是否为空,如果为空就是头删
                if(prev!=null) {
    
    
                    //不为空将prev的next指向curNext
                    prev.next=curNext;
                } else {
    
    
                    //为空则头节点变成curNext
                    head=curNext;
                }
                //后续过程不变
                cur=curNext;
                //判断curNext是否为空,如果为空则不进行该操作
                if(curNext!=null)
                curNext=curNext.next;
            }
        }
        return head;
    }
}

作者总结

这道题主要考虑的是答题人的细心程度,有一点不注意就有可能会造成越界访问,所以需要时刻注意可能会爆出错误的地方,并且我们自己画图会对写这种编程题有很大的益处,它可以帮我们找到平时没有注意到的小问题。

复杂度分析:

  • 时间复杂度O(n),其中 N 是给定链表中的结点数目。
  • 空间复杂度O(1)

3. 删除链表中的节点

在这里插入图片描述
题目描述:

给你一个链表的结点,不能访问到头节点,要将链表的这个结点删除
用例如下:1->2->3->4->5->null node=3;
输出为:1->2->4->5->null

解题思路:

题目中提到了,我们无法访问到头节点,在平时我们删除结点,都是找到要删除结点的前驱结点,让前驱结点的next指向要删除结点的下一个结点。
在这里插入图片描述

但是题目直接给了要删除的结点,我们无法访问的它的前一个结点,我们可以考虑,将下一个结点的 val 和 next 都给要删除的结点,由于下一个结点的next给了我们的node所以我们找不到原来的node的下一个结点,自然就完成了删除,如果是C语言需要释放内存,则可以提前保存下一个结点,最后free().

如下图:
在这里插入图片描述

代码如下:

class Solution {
    
    
    public void deleteNode(ListNode node) {
    
    
        node.val=node.next.val;//替换为下一个结点的val
        node.next=node.next.next;//替换为下一个结点的next
	}
}

总结:

既然不能先删除自己,那就把自己整容成儿子,再假装自己就是儿子来养活孙子

复杂度分析:

  • 时间复杂度O(1)
  • 空间复杂度O(1)

4. 重排链表

在这里插入图片描述

题目描述:

大概意思就是:
在这里插入图片描述

解题思路:

思路一:

单链表并不支持随机访问,也不支持从后往前遍历,所以我们可以创建一个顺序表,将单链表中的结点一个一个存入顺序表中,最后根据下标来找到结点并重建链表。

具体实现:

  1. 构建一个顺序表
  2. 将链表存入到顺序表中
  3. 建立一个哨兵位,将后续重新排序的结点一个一个衔接在后面

代码如下:

class Solution {
    
    
    public void reorderList(ListNode head) {
    
    
        if(head==null||head.next==null) {
    
    
            return;
        }
        //利用顺序表存储,然后按指定位置
        //我这里没有直接使用库的顺序表,
        ListNode [] array=new ListNode[50000];
        ListNode cur=head;//
        int i=0;
        //从0下标存入
        while(cur!=null) {
    
    
            array[i]=cur;
            cur=cur.next;
            i++;
        }
        //最后i是元素个数
        ListNode newHead=new ListNode(0);
        ListNode newPrev=newHead;
        int a=0;//a是下标
        int b=i-1;//b也是下标,i是元素个数,元素个数-1就是最后一个元素的下标
        int count=1;//根据奇偶判断读取哪个下标的元素
        //最后i+1就是链表的结点个数+1,
        //count是从1开始的,所以当count=i+1时,就代表读取完了所有结点
        while(count!=i+1) {
    
    
        //如果是奇数,就顺序读取
            if(count%2!=0) {
    
    
               newPrev.next=array[a];
               newPrev=newPrev.next;
                a++;//
            } else{
    
    
            //如果是偶数就逆序读取
                 newPrev.next=array[b];
                 newPrev=newPrev.next;
                 b--;
        }
        count++;
    }
    //将左后一个结点呢next置为kong,防止循环
       newPrev.next=null;
       //将头节点改为newHead的next
        head=newHead.next;
    }
}

复杂度分析:

  • 时间复杂度O(n),n是链表的长度
  • 空间复杂度O(n),n是顺序表的长度

思路二(寻找链表中点 + 链表逆序 + 合并链表)

思路二:

通过找到链表的中点,从中点将链表逆序,此时的实现和判断回文链表很是相似!将链表逆序后,一个结点从头开始,一个结点从逆序的链表的头开始,依次向后遍历,最后利用哨兵位合并这个链表

如图:

在这里插入图片描述
注意:

我们需要考虑的是合并链表的退出条件,如果是奇数个结点的链表,我们采取cur!=prev,就会导致最后一个中间结点没有被合并到新链表中,所以需要单独考虑

具体步骤:

  1. 找到中间结点
  2. 逆序包含中间及后面的所以结点
  3. 合并单链表

代码如下(注意注释)

class Solution {
    
    

     public void reorderList(ListNode head) {
    
    
     if(head==null||head.next==null) {
    
    
             return;
         }
        //找到中间结点
        ListNode prev=head;//慢指针
        ListNode cur=head;//快指针
        while(cur!=null&&cur.next!=null) {
    
    
            cur=cur.next.next;
            prev=prev.next;
        }
        //逆序单链表
        cur=reverse(prev);
        prev=head;//prev从头遍历
        ListNode newHead=new ListNode(0);
       ListNode newPrev=newHead;
        int i=1;//来判断调用谁的结点
        while(true) {
    
    
            if(i%2!=0) {
    
    
               newPrev.next=prev;//衔接该结点
             //当两个结点相等时退出循环
               if(prev==cur) {
    
    
               //并将newPrev置为下一个结点
            		newPrev=newPrev.next;
                  	 break;
               }
               prev=prev.next;//prev指向下一个结点
            } else {
    
    
           	  newPrev.next=cur;//衔接该结点
           	 //当两个结点相等时退出循环
           	 if(prev==cur) {
    
    
           	 //并将newPrev置为下一个结点
           		newPrev=newPrev.next;
             	   break;
          	  }
            	cur=cur.next;//cur指向下一个结点
            }
            i++;
            newPrev=newPrev.next;//置为下一个结点
        }
        newPrev.next=null;//将尾结点的next置为null,防止循环

     }
     //逆置链表函数
    public ListNode reverse(ListNode head) {
    
    

        ListNode prev=null;
        ListNode cur=head;
        while(cur!=null) {
    
    
        ListNode curNext=cur.next;
        cur.next=prev;
        prev=cur;
        cur=curNext;
        }
        return prev;
    }

作者总结:

这道题作者最开始做的时候也没有思路,但是通过画图,也就慢慢的有了思路,关于我合并链表中,退出循环的条件,大家可以画图看一下,对应着图才能更好的掌握规律,包括作者为什么要将 if ( prev == cur ) 写在prev=prev.next或cur=cur.next的前面,大家可以画图思考一下,总之就是多画图,多思考,多上手敲代码。

复杂度分析:

  • 时间复杂度O(n),n是链表的长度
  • 空间复杂度O(1)

5. 剑指 Offer II 077. 链表排序(重点!)

在这里插入图片描述

题目描述:

给你一个无序链表,你需要将该链表排序

解题思路:

以链表的第一个结点为有序结点,后续结点和该有序链表的头进行对比,如果比头节点小,那就进行头插,如果比有序链表的尾小,那就尾插,否则就是中间插入。

如图:

在这里插入图片描述

我们先把会用到的指针都列举出来

  ListNode cur=head.next;//后续链表的头指针
  
  ListNode newhead=head;//当前有序链表的头指针
  
  newhead.next=null;//与后续链表断开联系
  
  ListNode prev=newhead;//有序链表向后遍历指针的前驱指针
  
  ListNode prevNext=newhead.next;//用于有序链表向后遍历的指ListNode last=newhead;//尾结点
  
  ListNode curNext=cur.next;//用于找回后续链表的指针

具体实现:

  1. 先将链表分割为一个有序链表,和一个无序链表
  2. 有序链表的头结点和无序链表中cur指向的结点进行对比,如果头节点小于cur对应的结点,就进行头插
  3. 有序链表的尾结点和无序链表中的cur指向的结点进行对比,如果 尾结点小于cur对应的结点,那就代表这个链表没有比这个结点还要大的结点了,所以直接尾插就好了!
  4. 其余情况就是中间插入了,我们通过找到第一个比cur对应的结点的大的结点,将cur插入在该结点前面,完成中间插入
  5. 完成了插入后,让 prev 重新回归头节点的位置,即prev=newHead,让 prevNext 重新成为有序链表的第二个结点,即prevNext=prev.next;

插入过程:
(有小瑕疵,cur跟尾结点比较的过程忽略了,直接按顺序对比去了,但是大体思路是这样)
在这里插入图片描述

代码如下:

class Solution {
    
    
    public static ListNode sortList(ListNode head) {
    
    
        if (head == null || head.next == null) {
    
    
             return head;
         }
         ListNode cur=head.next;//后续链表的头节点
         ListNode newhead=head;//当前有序链表的头节点
         newhead.next=null;//与后续链表断开联系
         ListNode prevNext=newhead.next;//有序链表向后遍历的结点
         ListNode prev=newhead;//有序链表向后遍历结点的前驱结点
         ListNode last=newhead;//尾结点
         while(cur!=null) {
    
    
             //头插
             ListNode curNext =cur.next;
             if(newhead.val>=cur.val) {
    
    
                 cur.next=newhead;
                 newhead=cur;
             } else if(last.val<cur.val) {
    
    
             		//尾插
                 last.next=cur;
                 last=cur;
                 last.next=null;
             } else {
    
    //中间插入
             //找到大于等于cur.val的结点
                while(cur.val>prevNext.val) {
    
    
                //prev一直保存prevNext的前一个结点
                    prev=prevNext;
                    //prevNext置为它的下一个结点
                    prevNext=prevNext.next;
                }
                //将cur插入prev和prevNext之间
                prev.next=cur;
                cur.next=prevNext;
             }
             //重置prev
             prev=newhead;
             //重置prevNext
             prevNext=prev.next;
             //重置cur
            cur=curNext;
         }
         //返回新头节点
    return newhead;
   } 
}

作者总结:

这道题的主要思路就是,将第一个结点单独出来,一步一步和这个链表的后续结点进行对比,分别进行头插,尾插,中间插入,该题的难度并不简单,对于初学者来说。
并且这道题属于经常会考到的面试题,最优的解法可以将时间复杂度缩短到 O(nlogn),后续我在学习归并排序后,会优化该题解。

复杂度分析:

  • 时间复杂度O(n^2),n是链表的长度
  • 空间复杂度O(1)

删除链表倒数第n个结点oj
删除链表中的重复结点II
删除链表中的节点
重排链表
链表排序

猜你喜欢

转载自blog.csdn.net/xiaoyubuhuiqiche/article/details/128225606