链表面试题

1. 删除链表中等于给定值 val 的所有节点。 OJ链接

这个就相当于removeAllKey.

/**
 * 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) {
            //先判断头节点是否为空
        if(head==null){
            return null;
        }
        ListNode  prev=head;
        ListNode cur=head.next;
        //遍历链表
        while(cur!=null){
            //判断cur是否为空
            if(cur.val==val){
                prev.next=cur.next;
                cur=cur.next;
            }else {
                prev=cur;
                cur=cur.next;
            }
        }
          //判断头节点
            if(head.val==val){
                //如果是,只删除头节点
                head=head.next;
            }
              return head;
    }
   
}

2. 反转一个单链表。 OJ链接 

假如以下图为例:

 我们这里的目的是,直接反转链表的结构。

其实很简单,我们只需要拿着0x46进行头插 ,再拿0x81进行头插,最后拿0x12进行头插,逻辑图如下:

所以,这里我们先把头节点置为空(以上图0x85为例),然后记录下他的下一个,把他的下一个用头插法往前插,依次类推。 

所以这里就相当于头插法的简单应用了。

这里如果hand等于空,就返回空。如果只有一个节点,反转之后还是这个节点。如果不为空也不是一个节点的情况下,定义一个变量,让他等于hand.next,这里hand.next(0x85)不为空,说明这个面部不止一个节点,先将hand.next置为空(0x85)。这里我们要注意hand.next=null和

ListNode  cur=hand.next两个代码的顺序,如果先写hand.next=null,那ListNode  cur=hand.next这个代码就没有意义了。所以先写cur=hand.next,再写hand.next=null。

这里cur=hand.next,cur在0x46这个地方,让cur一直往下走。拿0x46进行头插法,要让cur.next=hand,但是,这里我们要注意如果我们将0x81改了的话,就找不到他的下一个节点0x12了,所以这里我们用curNext标记一下。逻辑图如下:

代码如下:

 public ListNode reverseList() {//这里就不要ListNode head这个参数来,因为在这里他是成员变量
    if(hand==null) {
        return null;
    }
    //只有一个节点
        if(hand.next==null){
            return hand;
        }
        ListNode cur=hand.next;
        hand.next=null;
        while(cur!=null){
            ListNode curNext=cur.next;
            cur.next=hand;
            hand=cur;
            cur=curNext;
        }
        //返回头节点
        return hand;
    }

调试结果如下:

这里我们也可以从指定位置打印,代码如下:

//指定节点位置打印
    public void display(ListNode newNext) {
        ListNode cur=this.hand;
        while(cur !=null){
            System.out.print(cur.val+" ");
            cur=cur.next;
        }
        System.out.println();
    }
   

代码结果如下:

 按上面力扣的形式写如下:

/**
 * 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 reverseList(ListNode head) {
    if(head==null) {
        return null;
    }
    //只有一个节点
        if(head.next==null){
            return head;
        }
        ListNode cur=head.next;
        head.next=null;
        while(cur!=null){
            ListNode curNext=cur.next;
            cur.next=head;
            head=cur;
            cur=curNext;
        }
        //返回头节点
        return head;
    }

    }

 3. 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。OJ链接

假如我们再以下图为例:

 比如拿上图的1来说,他有四个节点,他的中间节点是34,他的长度是4,4/2=2,这样的话定义一个cur,走两步就到0x81了。拿2来说,他有5个节点,他的节点也是34他的长度是5,5/2=2,这里cur走两步也就到0x81了。所以我们这种题的做法就是:

1.先求整个链表的长度

2.再求长度除2,找到这个中间节点

代码如下:

//找中间节点
    public ListNode middleNode() {
        ListNode cur=hand;
        //首先,求长度
        int len=size();
        for(int i=0;i<len/2;i++){
            cur=cur.next;
        }
        return cur;
    }

但是,我们这种写法有一个不好的地方,因为我们求长度要遍历一遍,虽然他的时间复杂度就是

O(N)。那有没有一个更好的方法,虽然他的时间复杂都也是O(N),但是不同于上面这样遍历,这里我们不希望多遍历链表,只要求这里只遍历链表一遍即可。那么下面我们来看一下快慢指针。

如下图一个中间节点的:

一开始我们fast和slow都在头节点0x85这里,接下来让快的一次走两步,让慢的一次走一步,如下图,那这个时候我们可以发现,对于这个链表来说,他的fast的next是空的,而slow所指的位置就是中间节点。

再来看有两个中间节点的 ,还是让fast一次走两步,让slow一次走一步,最终结果如下图

此时,我们发现fast 为空了,因为他是偶数个节点,而slow所指的也是中间节点。

那他们的原理又是什么呢?为什么一个走一步,一个走两步,而慢的那个就是中间节点呢?原因很简单,比如说,两个人跑步,假如两个都要跑100米,当一个人是一个人速度的2倍,当快的那个到达终点的时候,慢的那个一定在中间位置。所以这个逻辑就是路程相同,速度是2倍。

所以如果是偶数节点:fast==null,如果是奇数节点:fast.next==null.所以不管这里是奇数偶数,只要发现为空,就都要结束,结束之后返回slow即可。这样的话相当于同时走这个链表。

如果想让他走两步,只需要让fast=fast.next.next

代码如下:

//找中间节点
    public ListNode middleNode() {
        ListNode fast = hand;
        ListNode slow = hand;
        //两个条件都要成立
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
//当上面有一个条件不成立的时候,此时slow所处的位置就是中间位置
        return slow;
    }

按上面力扣的形式写如下: 

/**
 * 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 middleNode(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        //两个条件都要成立
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
       //当上面有一个条件不成立的时候,此时slow所处的位置就是中间位置
        return slow;
    }
    }

4. 输入一个链表,输出该链表中倒数第k个结点。 OJ链接

假如以下图为例,假如要输出倒数第三个,则在如下图0x81,倒数第二个是0x97,倒数第四个是0x46...

那要怎么做呢?

第一种思路:

首先,先求一下链表的长度。如上图的长度为5,这里我们求倒数第四个节点0x46,意味着我们从头节点0x85这里顺着向后走一个节点,假如要求倒数第二个节点,上图的长度为5,从头节点0x85这里顺着向后走3个节点。所以,我们这里先求len(长度),在让他走len-k步。

第二种思路(奇偶都一样)

这里我们还是使用fast和slow,假如这里我们要找倒数第四个节点0x46,这个地方我们先让fast走三步,再让slow和fast一起走,如果发现fast为空了,slow所指的位置,一定是第key个节点。思路如下图1,2.

再来看倒数第三个节点,先让fast走两步,再让slow和fast一起走,当faxt走到最后一个节点的时候,slow所指的位置,一定是第key个节点。

再来看倒数第二个节点,先让fast走一步,再让slow和fast一起走,当faxt走到最后一个节点的时候,slow所指的位置,一定是第key个节点。

那么,他的原理是如果要找第key个节点,fast和slow永远差key-1步。

思路:

1.fast走k-1步

2.同时走,走到最后一个节点停止

3.fast到最后slow的位置就是倒数k-1

这里 我们首先判断key的合法性。

代码如下:

  //链表中倒数第k个结点
    public ListNode FindKthToTail(int k) {
        if(k<=0 || k>size()){//我们可以看到,这里通过调用size()求长度了,可以不用这样写
            return null;
        }
        ListNode fast=hand;
        ListNode slow=hand;
        //接下来让fast走k-1步
        while(k-1!=0) {
            fast = fast.next;
            k--;
        }
        while(fast.next!=null){
            fast=fast.next;
            slow=slow.next;
        }
        return slow;
    }

此时,上面这个代码最大的问题就是能不能不要存在上面的size()呢。 

以下图为例:

假如这里我们要找倒数第四个节点,对应上面的代码来看,k没有大于size,而k也不小于0,所以这里fast和slow也都从hand开始。而k-1也不等于0 ,k现在是4,也意味着fast这里至少要走三步,走完两步之后fast等于fast.next,fast在0x12这个位置,而fast.next为空,在往后一走,fast就为空了,也就意味着fast还没有走到第三步的时候就已经为空了。这也就意味着此时空指针异常了,所以我们这里面有一个问题,我们这里面如果走完如上之后,发现fast已经为空了,那就证明此时已经多走了,这就意味着k太大了,只有当k很大的时候,他才会跑出去。

此时我们在这里加一个条件,如果fast等于了空,就返回空,此时就不用加k>size了。因为这种情况就是比较的当k和大的时候。

代码如下:

//链表中倒数第k个结点
    public ListNode FindKthToTail(int k) {
        if(k<=0 || hand==null){
            return null;
        }
        ListNode fast=hand;
        ListNode slow=hand;
        int count=0;
        //接下来让fast走k-1步
        while(k-1!=count) {
            fast = fast.next;
            //处理k太大的问题
            if(fast==null){
                return null;
            }
            count++;
        }
        while(fast.next!=null){
            fast=fast.next;
            slow=slow.next;
        }
        return slow;
    }

 按上面牛客网的形式写如下:

/*
public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}*/
public class Solution {
    public ListNode FindKthToTail(ListNode head,int k) {
        if(k<=0 || head==null){
            return null;
        }
        ListNode fast=head;
        ListNode slow=head;
        int count=0;
        //接下来让fast走k-1步
        while(k-1!=count) {
            fast = fast.next;
            if(fast==null){
                return null;
            }
            count++;
        }
        while(fast.next!=null){
            fast=fast.next;
            slow=slow.next;
        }
        return slow;
    }

    }

5. 将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。OJ链接 

以下图为例,假如这里给了两个链表,这两个链表都是有序的,如下图hamd1和hand2,现在要把如下这两个链表合并成一个有序的链表 ,如下图hand.

 那合并怎么合呢?

首先,hand1和hand2他们两个在比较的时候一定先放小的,再放大的,那在这里面,我们申请一个节点,这个节点不存放具体的数据,我们可以认为它相当于一个虚拟节点,,证明这个节点他里面的数据不是一个真实的数据,把这个节点定义成newH,那newH具体该怎么做呢?如下:

让上面这两个链表进行比较,谁小,就把他的节点放在newH的后面,如下图,比如hand1中的0x851和hand2中的0x85比较,0x851小,就把0x851放到newH的next里面,而newH的下一个就是0x851.接下来之后让hand1往后,走到0x461这个位置,再让其0x461和hand2的0x85比较...

这里我们在定义一个tmpH,让他记录当前已经串连好这个链表的最后一个节点,比如说12和5比较完之后,newH的下一个就是5,此时hand1往后走到0x461这个位置,而tmpH走到5这个位置,接下来25和12比较,12小,就把12插入到tmpH的下一个节点,此时就是把0x851这个节点的next变成0x85,让hand2往下走一个节点,到0x46这个位置了,而tmpH此时就走到了0x85这个位置,。这个时候如果hand1所指的节点和hand2所指的节点找到了最小的值,那么这个最小的值一定要放到12的后面,也就是地址0x85的后面,而我们发现23小,就让hand2往后走,tmpH就走到了23这个位置,以此类推。

而这个newH的作用是,将来返回串联好新链表的节点的时候,只需要看newH的next是谁即可。如下图他的next是0x851,就返回这个节点。

这里如果在IDEA上面写的时候建议把这段代码写在Test里面, 不放在MySingleList里面的原因是因为MySingleList里面操作的是某一个链表,而我们现在操作的是2个链表.

此时我们发现他报错了,因为他是内部类,此时我们应该如下图这样做即可。

首先,我们先定义一个虚拟节点newH,虚拟节点有了之后,接下来,对应的hand1和hand2这两个链表所指向的节点的val进行比较,那我们一定得保证,hand1和hand2都不为空,hand1和hand2他们的val才能去比较。当他们都不为空了时,如果hand1.val<hand2.val了我们发现hand1小,我们就把hand1串起来(tmpH.next=hand1),后面以此类推,否则就变成了hand2,和上面情况一样,后面以此类推。但是我们发现在整个比较的过程中,一定会有一个链表先走完,假如如下图hand1中,我们发现,31之后,hand1为空了,此时 while(hand1!=null && hand2!=null) 中hand1!=null这个条件就进不来了,但是hand2不为空。

所以我们这里就要写出另两种情况,一种if(hand1!=null),说明剩下的节点都比hand2大,还有一种if(hand2!=null),说明剩下的节点都比hand1大。

所以我们再来对应着上面这幅图看,让hand1一直往后走走到空,让tmpH走到如下图位置,那此时有人会想,tmpH走到如下图这个位置,后面应该串谁呢?很简单,这里直接串hand2中0x81这个节点(相当于代码tmpH.next=hand2).最后只需返回newH.next.因为newH.next是合并之后的头节点。

代码如下:

public class Test {
    public static MySingleList.ListNode mergeTwoLists(MySingleList.ListNode hand1, MySingleList.ListNode hand2) {//list1,list2指链表
        //定义一个虚拟节点
        MySingleList.ListNode newH = new MySingleList.ListNode(-1);
        MySingleList.ListNode tmpH = newH;
        while (hand1 != null && hand2 != null) {
            if (hand1.val < hand2.val) {
                tmpH.next = hand1;
                hand1 = hand1.next;
            } else {
                tmpH.next = hand2;
                hand2 = hand2.next;
            }
            tmpH = tmpH.next;
        }

        if (hand1 != null) {
            //hand1不等于空,把hand1传进来
            tmpH.next = hand1;
        }
        if (hand2 != null) {
            //hand2不等于空,把hand2传进来
            tmpH.next = hand2;
        }
        return newH.next;
    }

    public static void main(String[] args) {
        MySingleList mySingleList = new MySingleList();
        mySingleList.addLast(12);
        mySingleList.addLast(23);
        mySingleList.addLast(34);
        mySingleList.addLast(45);
        mySingleList.addLast(56);
        mySingleList.addLast(67);
        mySingleList.display();

        MySingleList mySingleList2 = new MySingleList();
        mySingleList2.addLast(12);
        mySingleList2.addLast(23);
        mySingleList2.addLast(28);
        mySingleList2.addLast(29);
        mySingleList2.addLast(30);
        mySingleList2.addLast(167);
        mySingleList2.display();
        System.out.println("==============");
        //这里我们要调用mergeTwoLists这个方法,直接调用在上面要加static
        //这里接收一下合并之后的链表,类型还是 MySingleList.ListNode
      MySingleList.ListNode head=mergeTwoLists(mySingleList.hand,mySingleList2.hand);
      mySingleList.display(head);
    }

按上面力扣的形式写如下: 

/**
 * 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 mergeTwoLists(ListNode list1,ListNode list2){
          //定义一个虚拟节点
       ListNode newH=new ListNode(-1);
       ListNode tmpH=newH;
        while(list1!=null && list2!=null){
            if(list1.val<list2.val){
                tmpH.next=list1;
                list1=list1.next;
            }else{
            tmpH.next=list2;
           list2=list2.next;
            }
            tmpH=tmpH.next;
        }

        if (list1!=null){
            //hand1不等于空,把hand1传进来
            tmpH.next=list1;
        }

        if(list2!=null){
            // //hand2不等于空,把hand2传进来
            tmpH.next=list2;
        }
          return newH.next;
    }

}

7. 链表的回文结构。 OJ链接

什么是回文呢?

如下图,像图2这样的就叫回文,图1不是回文。 

那这里我们应该怎么做呢?此时肯定有人说 定义一个引用再0x85这个位置,让他从前往后走,定义一个引用在0x12这个位置,让他从后往前走。其实这样是不可以的,因为他是单向链表,单向链表是无法从0x12这个位置往0x97这个位置走的,只能从hand往后走。

那我们应该怎么做呢?

其实很简单,这里我们把0x97和0x12如下图翻转一下,定义两个引用,如上面说的,让一个从0x85这个位置,从前往后走,让一个从0x12这个位置,让他从后往前走,让他们相遇到0x81这个节点。

但是这里又有一个疑问,从哪开始翻转呢?

当然,这里是从中间位置开始翻转,所以,思路如下:

1.找到中间位置

2.翻转(从中间节点以后开始翻转)

3.一个节点从前到后开始走,一个节点从后往前开始走

第一步在这里找到中间位置,其实找到中间位置很简单, 如上图这样,这里定义一个fast和slow,让fast一次走两步,让slow一次一步,当fast的next为空,或者fast为空,slow的位置就是中间位置。

第二步在第一步的基础上开始翻转,而翻转在如上图的0x97这个位置开始翻转,这里把他定义成cue,而0x12这个地方是curNext。而我们翻转的时候要让cur.next等于0x81,刚好是slow,所以也就是cur.next=slow.但是在修改之前我们不敢把cur.next修改成0x81,不然就会把0x12覆盖掉,所以我们这里应该让curNext=cur.next,把0x12先记录下来。修改完之后slow就变成了0x97,cur就变成了0x12,也意味着slow=cur,cur=curNext。这里0x12的下一个在等于slow,这里就和我们上面说的一样了,直到cur为空停止,此时这个地方就翻转了,翻转完成之后0x12这个位置的next就是0x97了,此时slow就走到0x12这个位置了,而cur和curNext就为空了。

此时如下图:

此时让hand在如上图这个位置往后走,让slow在如上图这个位置往前走(注意:这里不用fast,因为他有两种情况,偶数情况和奇数情况),直到他们两个在0x81这个地方相遇。

这里我们再来说一下偶数情况:

这里还是如上第一幅图一样定义两个引用,一个为fast,一个为slow,fast还是走两步,slow走一步。然后对0x121这个地方进行翻转,此时定义一个cur,一个curNext,curNext一上来就是空的,如上图。然后让cur再往后走到了fast的位置,为空了,让slow也往后走到0x121,这时slow一定在最后一个节点。此时让hand从如图位置往后走,让slow从0x121这个地方往前走。到hand走到0x461这个位置,slow走到0x811这个位置,我们发现他们相遇不了。此时如下的这段代码就有问题了

所以我们这里给他加个偶数情况下,当hand.next=slow的情况下就相遇了。 

代码如下:

public boolean chkPalindrome() {
            if(hand==null || hand.next==null){
                return true;
            }
        ListNode fast=hand;
        ListNode slow=hand;
        //1.找到中间位置
        while(fast!=null && fast.next!=null){
            fast=fast.next.next;
            slow=slow.next;
        }
        //2.翻转
        ListNode cur=slow.next;
        while(cur!=null){
            ListNode curNext=cur.next;
            cur.next=slow;
            slow=cur;
            cur=curNext;
        }
        //3.从前到后,从后到前
        while (hand!=slow){//hand!=slow说明两个没有相遇
            //奇数情况下(奇数当hand和slow不同相遇)
            if(hand.val!=slow.val){//值不一样
                return false;
            }
            //偶数情况下(偶数当hand和slow相同相遇)
            if(hand.next==slow){
                return true;
            }
            //上面两个if不能互换
            //值一样说明相遇了
            hand=hand.next;
            slow=slow.next;
        }
        return true;
    }

按上面牛客网的形式写如下: 

import java.util.*;

/*
public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}*/
public class PalindromeList {
    public boolean chkPalindrome(ListNode hand) {
        // write code here
        if(hand==null || hand.next==null){
                return true;
            }
        ListNode fast=hand;
        ListNode slow=hand;
        //1.找到中间位置
        while(fast!=null && fast.next!=null){
            fast=fast.next.next;
            slow=slow.next;
        }
        //2.翻转
        ListNode cur=slow.next;
        while(cur!=null){
            ListNode curNext=cur.next;
            cur.next=slow;
            slow=cur;
            cur=curNext;
        }
        //3.从前到后,从后到前
        while (hand!=slow){//hand!=slow说明两个没有相遇
            //奇数情况下
            if(hand.val!=slow.val){//值不一样
                return false;
            }
            //偶数情况下
            if(hand.next==slow){
                return true;
            }
            //上面两个if不能互换
            //值一样说明相遇了
            hand=hand.next;
            slow=slow.next;
        }
        return true;
    }
    }

这里还有一个问题,如下:

这里如上图这两个红色框里的条件能否互换位置呢?

当然不能,如果互换了,会有一种情况,有可能fast是空的,此时会出现空指针异常。 


6. 编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 。OJ链接

这个意思就是说,假设是如下第一幅图,这里我们给的x=15(这里没有说x一定是链表中的值),最终我们希望,把小于x的值排在x之前,大于或等于x的排在x后面,就是像如下第二幅图一样。

所以在这个地方我们知道必须改变链表的结构 。

这里我们先想一个问题,在这个全新的链表当中,能不能定义一个cur,让cur去遍历链表,只要说小于x的把他放在小于x的一边,如果说是大于x的,就把他放在大于x的一边,最后放好之后把小于x的一端和大于x的一端串起来。然后题目说不能改变原来数据的顺序,就是如上第二幅图这样,原来12在3和5的前面,排完之后12还是在3和5的前面,后面34在56的前面,排完之后34还是在在56的前面。这样我们就要想,既然要让原来的顺序不变且要把小于x的放一起,把大于等于x的放一起,那么我们怎么才能把它们放在一起呢?这里指的是采用尾插法。

这里我们如上图一样定义bs,be标志0x85到0x97的 开始和结束,定义as,ae标志0x81到0x12的开始和结束。这里be一定指向小于x的最后一个节点,be的next等于as就把他们串起来了。

当cur.val<x的时候,12小于15,把12往如上这幅图小于x的地方通过尾插法往进放,采用尾插法的情况下bs和be他们都是空,也意味着,当插入0x85这个节点的时候bs和be都指向这个节点。那也意味着我们得判断一下如果bs==null,证明我们在插入第一个节点,此时,bs和be指向当前cur这个节点,这个时候我们就把12插进来了,插进来之后cur=cur.next,让cur往后走。

接下来是0x46,我们发现,他也比x小,此时我们拿着0x46这个节点往0x85这个节点后面插入怎么插呢?这里让be的next等于cur,再让be=be.next,也就是说,让be的next等于0x46,让be走到0x46这个位置,这样就相当于把它们串起来了,如下图:

此时肯定有人会疑惑,那bs也在0x85这个位置,为什么不是bs的next等于cur这个节点呢?

这里假如再来一个节点,他一直找的是be,,所以现在就相当于,be永远指向的是最后一个节点,所以我们在代码中写了be=be.next这段代码。

接着cur往下走到34,此时cur.val不小于15了,他就走else了,此时as和ae都指向了0x81这个节点 ,如上第三幅图.

接着cur往下走到5,而5比15小,所以把他插到be的后面。紧接着cur再往下走,而0x12是大于15的,所以应该把他放在大于等于15的那边,然后尾插法把他放在0x81这个节点的后面,然后把他串起来,然后cur等于cur.next,此时cur.next为空,说明链表走完了。链表走完之后,最后将大于15这段和小于15这段穿起来即可。

此时上面说的代码如下:

public class Partition {
    public ListNode partition(ListNode pHead, int x) {
        // write code here
        if(pHead==null){
            return null;
        }
        //第一个段的开始和结束
        ListNode bs=null;
        ListNode be=null;
        //第二个段的开始和结束
        ListNode as=null;
        ListNode ae=null;

        ListNode cur=pHead;
        while(cur!=null){
            if(cur.val<x){
                if(bs==null){
                    //插入第一个节点
                    bs=cur;
                    be=cur;
                }else{
                    be.next=cur;
                    be=be.next;
                }
            }else{
                if(as==null){
                    as=cur;
                    ae=cur;
                }else{
                    ae.next=cur;
                    ae=ae.next;
                }
            }
            cur=cur.next;
        }
        //开始串起来
        be.next=as;
        return pHead;

    }
}

代码结果如下: 

这里我们运行之后我们发现出现了空指针异常的错误 。(如上我们看到,输入3,3,3,并没有输出预想结果)

这里我们就要像,为什么空指针异常呢?这里相当于没有前半段.也就是说,当里面全部为3的时候,以3分割,前半段bs到be是没有数据的,说明be的值null,后半段都是3。而be为null之后,他就没有next,所以代码be.next=as就会空指针异常。那也意味着,当你给定一个值得时候,以他划分之后,他不一定同时存在左右。所以此时我们应该给一个条件,如果当bs不等于空的时候,这里应该返回as。如果 as也为空的话,那就返回空,说明两端都没有我们要找的数据。

此时我们运行,如下图:

为社么代码又报错呢?我们看报错原因是数组越界异常,哪来的数组呢?

这里隐藏的最大的问题在哪里呢?

这里隐藏的最大问题是最后一个节点的next没有置空,导致bs给到牛客的后台验证的时候一直在死循环,导致他自己报了个数组越界异常。所以我们应该在第一个区间有数据并且第二个区间也有数据的情况下把他置为空,那也意味着,当我们把小于15,并且把大于等于15这两端串起来之后,我们要再判断第二个区间是否有数据,那么当你第二个区间有数据的情况下,直接ae.next=null.此时肯定有人说,人家最后一个本来就是空,其实这里防的就是这种情况(第一端不为空,第二端也不为),这里不管怎么样,一定要手动在置空一次。

代码如下:

public class Partition {
    public ListNode partition(ListNode pHead, int x) {
        // write code here
        if(pHead==null){
            return null;
        }
        //第一个段的开始和结束
        ListNode bs=null;
        ListNode be=null;
        //第二个段的开始和结束
        ListNode as=null;
        ListNode ae=null;

        ListNode cur=pHead;
        while(cur!=null){
            if(cur.val<x){
                if(bs==null){
                    //插入第一个节点
                    bs=cur;
                    be=cur;
                }else{
                    be.next=cur;
                    be=be.next;
                }
            }else{
                if(as==null){
                    as=cur;
                    ae=cur;
                }else{
                    ae.next=cur;
                    ae=ae.next;
                }
            }
            cur=cur.next;
        }
        //开始串起来
        if(bs==null){
            //如果bs==null,说明第一个区间没有数据
            return as;
        }
        //如果从这往后走,说明第一个区间有数据
        be.next=as;
        if(as!=null){
            //第二个区间有数据
            ae.next=null;
        }
        return bs;
    }
}

 8. 输入两个链表,找出它们的第一个公共结点。OJ链接

什么是相交呢?如下图:

相交指的是 1图中0x811这个节点的next并不是0x971,而是和2图中0x81的next一样,都是0x97,也意味着,他们的后续都是相同的。他们是结构上的相同,而不是数字上的相同。

此时0x97就是要找的相交的节点,那这里应该怎么做呢?

这里肯定有人会想,很简单,让hand1和hand2一个一步往后走,比如hand1先走一步,hand2走一步,hand1再走一步...直到hand1和hand2相遇了,也就是如上图0x97这个节点。

上面这种情况是刚刚好,就是hand1和hand2长度一样,都是5个节点。但是,这里我们要说一种特殊情况,如下图:

此时,如上图hand1和hand2长度不一样了,他们一个一步走,当hand1走到0x97时,hand2在0x81这个位置,他们并没有相遇。

所以 这里它的难度在于,如果两个链表的长度不一样,无法以相同的速度走到要相交的这个节点的。那此时我们就要想,如果两个链表长度不一样,差在哪里?当然,这里肯定是插在了他们要相交的这个节点之前,因为相交之后的节点都是一样的。那我们在这里分别求两个长度len1和len2,然后让len1减len2,此时我们并不知道两个那个长。这里取 len1和len2的绝对值,此时一定会得到一个正的len(abs(len1-len2)=len).那我们就让最长的先走len步。

那这个时候,我们一算,hand1长度为4,hand2的长度为5。所以我们这里先让hand2走一步,然后再让hand1和hand2同时走,直到相遇。

所以这个题的思路是,先分别求长度,看谁长,然后看他们差值步,然后让最长的走差值步再让他们两个一起走,然后他们相遇的点就是交点。

那我们怎么知道那个链表长呢?

首先我们不知道谁长谁短,先定义一个pl,假如把pl当成最长的,代码如下:

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        //假设这里pl先为最长的
        ListNode pl=headA;
        ListNode ps=headB;

        int len1=0;
        int len2=0;

        //1.求pl长度
        while(pl!=null){
            len1++;
            pl=pl.next;
        }
        //求ps长度
        while(ps!=null){
            len2++;
            ps=ps.next;
        }
        int len=len1-len2;
        //这里有可能len为负数
        if(len < 0){//如果len小于0,ps就长
            //修正一下pl和ps的指向
            pl=headB;
            ps=headA;
            len=len2-len1;
        }
        //代码执行到这里,能保证两点 1.pl一定指向最长的链表 ps一定指向最短的链表 2.len一定是一个正数
        
    }
}

如上代码有个问题,就是当len大于0会存在什么问题呢?

当len大于0,pl和ps就为空了,为空之后两个while循环就进不去了。所以这里当把两个循环走完,再让pl=headA,ps=headB. 

这里我们来看个细节问题,此时,当有人看见如下这段代码的时候就会说pl和ps就相遇了,其实这句话是不对的,为什么呢?接着往下看。

我们来看如下这幅图:

如上图,pl和ps的长度一模一样,且差值都是0,让pl和ps一个一步走,一直走到如上图位置,此时 pl和ps都为空,我们来看下面这段代码,而此时pl和ps一样不满足如下循环条件,而他们也根本没有相遇的点。

而我们来看下面这幅图发现运行通过了,为什么呢? 

 

这是因为没有相遇的点,返回pl了,而pl和ps刚好是null,所以运行通过了。

所以这段代码从理解的角度来说这样写并不好.这里加一段代码,如下:

这段代码证明pl和ps没有相遇,他们是走到了如上第四幅图的情况。否则才会返回return pl这段代码,返回这段代码才说明ps不为空,她一定有相遇的点 。

全部代码如下:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        //假设这里pl先为最长的
        ListNode pl=headA;
        ListNode ps=headB;

        int len1=0;
        int len2=0;

        //1.求pl长度
        while(pl!=null){
            len1++;
            pl=pl.next;
        }
        //求ps长度
        while(ps!=null){
            len2++;
            ps=ps.next;
        }
        pl=headA;
        ps=headB;
        int len=len1-len2;
        //这里有可能len为负数
        if(len < 0){//如果len小于0,ps就长
            //修正一下pl和ps的指向
            pl=headB;
            ps=headA;
            len=len2-len1;
        }
        //代码执行到这里,能保证两点 1.pl一定指向最长的链表 ps一定指向最短的链表 2.len一定是一个正数
        //2.最长的走差值步
        while(len!=0){
            pl=pl.next;
            len--;
        }
        //3.同时走  4.相遇
        while(pl!=ps){
            pl=pl.next;
            ps=ps.next;
        }
        
        //这段代码说明ps也为null
        if(ps==null){
            return null;
        }
        return pl;//这里返回pl和ps都一样(如果这里写return ps,上面就要写pl==null)
    }
}

这里我们抛开牛客上面的答案,假如现在在IDEA上面测两个链表,那怎么让两个链表相交呢? 

如上图,假设这里让0x46的next等于0x811,这里我们就要想办法,让他们两个产生联系。

在hb中0x811这样写,hb.next是0x461,再next是0x811(hb.next.next),把 hb.next.next赋值给ha.next.next,而hb.next.next等于0x811,ha.next是0x46,再next相当于0x46的next。ha.next.next=hb.next.next 相当于把0x46next的值改成了0x811,而ha中0x46后面的值全部就丢掉了。

那按照这样的想法,代码如下:

public class Test {
    public static MySingleList.ListNode getIntersectionNode(MySingleList.ListNode headA, MySingleList.ListNode headB) {
        //假设这里pl先为最长的
        MySingleList.ListNode pl=headA;
        MySingleList.ListNode ps=headB;

        int len1=0;
        int len2=0;

        //1.求pl长度
        while(pl!=null){
            len1++;
            pl=pl.next;
        }
        //求ps长度
        while(ps!=null){
            len2++;
            ps=ps.next;
        }
        pl=headA;
        ps=headB;
        int len=len1-len2;
        //这里有可能len为负数
        if(len < 0){//如果len小于0,ps就长
            //修正一下pl和ps的指向
            pl=headB;
            ps=headA;
            len=len2-len1;
        }
        //代码执行到这里,能保证两点 1.pl一定指向最长的链表 ps一定指向最短的链表 2.len一定是一个正数
        //2.最长的走差值步
        while(len!=0){
            pl=pl.next;
            len--;
        }
        //3.同时走  4.相遇
        while(pl!=ps){
            pl=pl.next;
            ps=ps.next;
        }
        if(ps==null){
            return null;
        }
        return pl;//这里返回pl和ps都一样
    }
    public static void createCut(MySingleList.ListNode headA, MySingleList.ListNode headB ){
        headA.next.next=headB.next.next;
    }
    public static void main(String[] args) {
        MySingleList mySingleList = new MySingleList();
        mySingleList.addLast(12);
        mySingleList.addLast(23);
        mySingleList.addLast(34);
        mySingleList.addLast(45);
        mySingleList.addLast(56);
        mySingleList.addLast(67);
        mySingleList.display();

        MySingleList mySingleList2 = new MySingleList();
        mySingleList2.addLast(12);
        mySingleList2.addLast(23);
        mySingleList2.addLast(28);
        mySingleList2.addLast(29);
        mySingleList2.addLast(30);
        mySingleList2.addLast(167);
        mySingleList2.display();

        createCut(mySingleList.hand,mySingleList2.hand);
        MySingleList.ListNode ret=getIntersectionNode(mySingleList.hand,mySingleList2.hand);
        System.out.println(ret.val);

    }

代码结果如下:


9. 给定一个链表,判断链表中是否有环。 OJ链接 

【思路】
快慢指针,即慢指针一次走一步,快指针一次走两步,两个指针从链表起始位置开始运行,如果链表
带环则一定会在环中相遇,否则快指针率先走到链表的末尾。比如:陪女朋友到操作跑步减肥。

【扩展问题】
1.为什么快指针每次走两步,慢指针走一步可以?
假设链表带环,两个指针最后都会进入环,快指针先进环,慢指针后进环。当慢指针刚进环时,可能就和快
指针相遇了,最差情况下两个指针之间的距离刚好就是环的长度。此时,两个指针每移动一次,之间的距离
就缩小一步,不会出现每次刚好是套圈的情况,因此:在慢指针走到一圈之前,快指针肯定是可以追上慢指针的,即相遇。 

2.快指针一次走3步,走4步,...n步行吗

什么是有环的链表呢?如下图,0x12的下一个连接的是0x81,这就叫有环的链表。

这里我们想一个问题,我们怎么才能知道这个链表有环呢?假如如上图,我们怎么才能知道0x12前面这些链表的地址呢?是不是我们在做的时候还要定义 他存起来,那他的时间复杂度不就是O(N)了吗?

所以这里我们不能这样做,这里要求空间复杂度为O(1).

这里我们用快慢指针,让fast一次走两步,slow一次走一步,那为什么要这样做呢?

很简单,只要这两个指针有时差,在没环的情况下,slow永远追不上fast,在有环的情况下,slow就能追上fast,这就相当于数学问题上的追及问题,假如a在追b,有环ab一定相遇。

但是这里有一个问题,能不能让fast一次走三步,slow一次走一步可不可以呢?

这就出现了我们上面说的那个问题,假设如下图只有两个节点,这俩个节点构成了一个环。

根据这个图中的描述,我们发现,只要他们差的不是一步的情况下,一定不会相遇的。

这里我们在想一个问题,假设一个走三步,一个走两步,这种情况也差一步,可以不? 

当然,这里一个走两步一个走一步是最快相遇的,而一个走三步一个走两步这个不是最快相遇的 ,因为一个走两步一个走一步他们最多追击一个环的长度。这里一定不会出现slow在里面走很多圈的情况。

所以,这里我们判断环很简单,定义两个引用fast和slow只需要让一个走一步一个走两步,只要他们两个相遇,就说明有环。

但如果是没环的情况下,定义两个引用fast和slow,一个走的快一个走的慢,要么fast==null,要么fast.next==null了,这里最快的一定先为空。

这里fast和slow从头节点开始走,且每次一个走完2步,一个走完1步就要判断他们两个相遇了没。

上述代码如下:(这里改一下代码,但是和下面力扣代码意思一样)

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) {
                break;
            }
        }
        if(fast==null || fast.next==null){
            return false;
        }
        return true;
    }

按上面理扣的形式写如下:

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
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;
    }
}

10. 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL 。OJ链接 

结论:
让一个指针从链表起始位置开始遍历链表,同时让一个指针从判环时相遇点的位置开始绕环运行,两个指针都是每次均走一步,最终肯定会在入口点的位置相遇。

证明:

假如还是如下这幅图 :

这里要求环的入口点,而环的入口点在0x81这个地方,那怎么求呢?

如下图,这里我们设起始点head到入口点这段距离为x,,入口点到相遇点这段距离为y,环的长度为C。

1.(fast走了一圈半才和slow相遇的情况)假设,如图fast和slow想遇到这个相遇点的时候,fast已经走了一圈半且和slow相遇了(slow比fast慢),那么此时我们求快指针走的路程和慢指针走的路程。而我们看图,慢指针走了x加入口点到相遇点这部分,而入口点到相遇点这部分长度是C-y,所以慢指针走了x+(C-y)。而快指针的速度是慢指针的2倍,且快指针走的路程等于慢指针走的路程的2倍,所以快指针走了x+C+(C-y)。

所以2*(x+(C-y))=x+C+(C-y), 2*(x+(C-y))指2倍的慢指针。

2*(x+(C-y)=x+C+(C-y)

x+x+C+C-y-y=x+C+C-y

x-y=0

x=y

最后化简出来我们发现,x=y,那也就意味着起始点head到入口点,和入口点到相遇点的距离一样。

那也意味着,让slow从起始点head开始走,让fast从相遇点开始走,让他们一人一步走,他们两个路程一模一样,且以相同的速度走, 则他们相遇的地方不就是入口点吗。

2.(fast走了很多圈才和slow相遇的情况(之所以说fast转了很多圈就是圈小))如下图,假设这里让fast和slow同时从起始点head开始走,fast先进入环,因为起始点到入口点的路程长,当slow走进环的过程当中fast已经在环里面转了很多圈,但是不管fast转了多少圈,他们的路程还是一样。

这里还是假设起始点到入口点的距离为x,,入口点到相遇点为y。

假设fast在里面转了n圈,所以快指针的路程就是 x+NC+(C-y),而slow不会转很多圈,他一定是在追赶完一圈的情况下才和fast相遇的,所以慢指针的路程还是x+(C-y) 。所以快慢指针走的路程相当于多了个NC。

2*(x+(C-y)=x+NC+(C-y)

x+x+C+C-y-y=x+NC+C-y

x+C-y=NC

x=NC-C+y

x=(N-1)*C+y

N=1

x=y

这里还是让slow从起始点head开始走,让fast从相遇点开始走,一人一步走.

代码如下:

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
          ListNode fast = head;
        ListNode slow = head;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow) {
                break;
            }
        }
        //相当于无环
        if(fast==null || fast.next==null){
            return null;
        }
        //有环
      slow=head;
      while(slow!=fast){
          fast=fast.next;
          slow=slow.next;
      }
      return slow;
    }
}

习题就到这里喽。 

 


 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值