【数据结构】必掌握的链表面试题

    我们在学习了链表的有关知识后,有必要来看几个链表的经典面试题,让我们一起来学习一下吧。

目录

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

2.反转链表

3.链表的中间节点

4.链表中倒数第k个节点

5.合并两个有序链表

6.链表的回文结构 

7.链表分割

8.相交链表

9.环形链表

10.找环形链表开始入环的第一个节点


 

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

力扣

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点

示例

【数据结构】必掌握的链表面试题_第1张图片

输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

这个删除的核心是:要找到需要删除的节点的上一个节点  

【数据结构】必掌握的链表面试题_第2张图片  由于在一开始就处理头节点就需要删除的特殊情况比较困难,所以我们最后再处理

【数据结构】必掌握的链表面试题_第3张图片

然后我们看一下完整的代码吧

/**
 * 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 cur = head.next;
        ListNode prev = head;
        while(cur != null){
            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.反转链表

力扣

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。 

示例: 

【数据结构】必掌握的链表面试题_第4张图片

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

 我们想达成的效果

【数据结构】必掌握的链表面试题_第5张图片

分析:

【数据结构】必掌握的链表面试题_第6张图片

然后我们具体实现这样操作的代码大概是

【数据结构】必掌握的链表面试题_第7张图片

 我们对照代码走两步看一下,体会一下这几行代码

【数据结构】必掌握的链表面试题_第8张图片

好了,我们大概已经了解了 反转链表的操作了,我们试着写一下代码吧!

/**
 * 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;
        ListNode curNext = null;
        head.next = null;//第一个节点要置为空
        
        while(cur != null){
            curNext = cur.next;
            cur.next = head;
            head = cur;
            cur = curNext;
        }
        return head;
    }
}

有几个需要注意的点:

  1. 不要忘记把 head 置为null
  2. 不要在cur定义之前把head置为null

3.链表的中间节点

给定一个头结点为 head 的非空单链表,返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。

力扣

【数据结构】必掌握的链表面试题_第9张图片

这道题目我们用到了“快慢指针”的算法思想

具体是怎么用的呢?

【数据结构】必掌握的链表面试题_第10张图片

我们先来验证一下是否可行

  我们先看偶数个结点的情况 

 【数据结构】必掌握的链表面试题_第11张图片

  再看奇数个节点的情况

【数据结构】必掌握的链表面试题_第12张图片

我们发现,这个方法完全可行

那么,这个思想的原理是什么呢?

  当路程一定的时候,fast 的速度如果是slow速度的两倍,那么当fast走到最后的时候,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;
        }
        return slow;
    }
}

 这里有一个问题

【数据结构】必掌握的链表面试题_第13张图片

为什么?  因为 a&&b ,语句a先执行 

如果此时fast为null, 而先执行 fast.next != null 的话,那么 fast.next 就会空指针异常 

4.链表中倒数第k个节点

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

链表中倒数第k个结点_牛客题霸_牛客网 (nowcoder.com)

输入:1,{1,2,3,4,5}
返回值:{5}

 这道题目我们也是用到了“快慢指针”的思想

只是用法与上一个题目略有不同

【数据结构】必掌握的链表面试题_第14张图片 我们通过一个例子来说明

【数据结构】必掌握的链表面试题_第15张图片

可见,这个方法是可行的

但是还有一个问题:如何判断k是否合法
  如果我们通过常规的方法在一开始就判断k的合法性就需要知道结点的数量
  这就会遍历一遍,但我们要求总共最多遍历一遍

所以我们采取另一种方法来“曲线救国”

  • 首先我们要想清楚k什么时候不合法:k<0或者k>size(链表) 时不合法
  • 我们如何在不知道链表的size大小的情况下就可以判断是否合法呢
  • 我们先思考:如果k>size(链表)时,会出现什么情况?
  • 答案是,fast会走过头了,会出现空指针异常
  • 所以我们只要解决这个问题就可以了

我们的做法是:
在fast走 k-1步的过程中,每走一步就判断一下fast是否为null
从而避免fast为空时,fast.next出现空指针异常的问题

我们来看一下代码

/*
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){
            return null;
        }
        if(head == null){
            return null;
        }
        ListNode fast = head;
        ListNode slow = head;
        // fast先走 k-1 步
        while( k-1 >0){
            fast = fast.next;
            if(fast == null){
                return null;
            }
            k--;
        }
        //fast已经走了 k-1 步了,之后fast和slow一起走
        while(fast.next != null){
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }
}

 5.合并两个有序链表

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

力扣

示例: 

【数据结构】必掌握的链表面试题_第16张图片

输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

 我们需要借助一个虚拟节点,即 傀儡节点 来完成,定义它为newHead,同时定义一个tmp
记住当前的位置

【数据结构】必掌握的链表面试题_第17张图片

 当head1和head2都不为空时,将它们进行比较比较结果小的那一个作为newHead
节点的下一个节点
,我们演示几步给大家看看


【数据结构】必掌握的链表面试题_第18张图片 head2


【数据结构】必掌握的链表面试题_第19张图片 head1


【数据结构】必掌握的链表面试题_第20张图片

 串起来后,head1向后走,tmp走到当前节点的位置


 【数据结构】必掌握的链表面试题_第21张图片

 如果出现了这种情况,head2已经走完了,但head1还没有走完时,直接让tmp指向head1就可以了


最后要返回头节点时,我们返回 newHead.next就可以啦 

【总结】:

  1. 两个链表都有数据的时候,谁小就把谁先串起来
  2. 当其中一个走完时,tmp.next = 先走完的
  3. 最后返回的:return newHead.next;

 我们开始写代码吧!

/**
 * 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) {
        if(list1 ==null) return list2;
        if(list2 ==null) return list1;
        ListNode newHead = new ListNode(-1);
        ListNode tmp = newHead;
        while(list1 != null && list2 != null){
            if(list1.val < list2.val){
                tmp.next = list1;
                list1 = list1.next;
            }else{
                tmp.next = list2;
                list2 = list2.next;
            }
            tmp = tmp.next;
        }
        if(list1 != null){
            tmp.next = list1;
        }
        if(list2 != null){
            tmp.next = list2;
        }
        return newHead.next;
    }
}

6.链表的回文结构 

  • 对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。
  • 给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。

【数据结构】必掌握的链表面试题_第22张图片


  其实这道题我们要用到前面的 翻转链表找到链表中间节点 的思想 

【数据结构】必掌握的链表面试题_第23张图片  


   我们的思路是:先找到中间节点,然后从这个中间节点到最后一个节点,我们将这部分翻转
这样我们就得到了下面的结果

【数据结构】必掌握的链表面试题_第24张图片

我们再定义一个a和b, a和b比较,如果相同就向中间走
  如果不同就返回false 

【数据结构】必掌握的链表面试题_第25张图片 当a和b相遇时返回true


我们分析完整体的思路后,再来看一个问题 【数据结构】必掌握的链表面试题_第26张图片

 我们发现 当节点数为偶数时,最后,fast就变为空了
所以我们在翻转时和最后判断是否相同时,用head和slow操作,不用fast


【数据结构】必掌握的链表面试题_第27张图片

偶数的时候还有一个比较特殊的问题,这种情况下,head和slow相等,却不能相遇,所以我们需要加个判断
if(head.next ==slow){
      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 head) {
        // write code here
        if(head == null) return false;
        if(head.next== null) return true;//只有一个节点的情况
        //1.找到中间节点
        ListNode fast = head;
        ListNode slow = head;
        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.判断回文,此时slow已经走到了最后一个节点
        while(head != slow){
            if(head.val != slow.val){
                return false;
            }
            //偶数的情况
            if(head.next == slow){
                return true;
            }
            head = head.next;
            slow = slow.next;
        }
        return true;
    }
}

7.链表分割

链表分割_牛客题霸_牛客网

  现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。 

我们处理这道题的思路是:

  • 设置两个区间  [beforeStart , beforeEnd] 还有 [afterStart , afterEnd]
  • 我们把值小于x的节点全部放到第一个区间,把值大于x的节点放到第二个区间
  • 在把每个节点放到指定的区间时,我们采用尾插法,这样就能不改变链表原来的顺序 
  • 最后再把两个区间串起来

【数据结构】必掌握的链表面试题_第28张图片

还有一些小细节需要注意: 

  1. 别忘了处理 第一个区间没有数据的特殊情况!!!
  2. 还有,不要忘记把最后一个节点的 next 置为 null !!!

最后我们尝试动手写一下代码:

import java.util.*;

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

    ListNode(int val) {
        this.val = val;
    }
}*/
public class Partition {
    public ListNode partition(ListNode head, int x) {
        if(head == null) return null;
        ListNode bs = null;
        ListNode be = null;
        ListNode as = null;
        ListNode ae = null;
        ListNode cur = head;
        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,be】里没有数据
            return as;
        }
        
        be.next = as;
        if(as != null){
            //第二个区间 不为空
            ae.next = null; //把最后一个节点的next置为null
        }
        
        return bs;
    }
}

8.相交链表

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。

图示两个链表在节点 c1 开始相交

【数据结构】必掌握的链表面试题_第29张图片

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构 。

要求:时间复杂度 O(m + n) 、仅用 O(1) 内存 

示例一:

【数据结构】必掌握的链表面试题_第30张图片

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

示例二

【数据结构】必掌握的链表面试题_第31张图片

输入:intersectVal = 2, listA = [1,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [1,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。


示例三

【数据结构】必掌握的链表面试题_第32张图片

输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。

我们的思路是:

  1. 求这两个链表的长度
  2. 让较长的链表走这两个链表长度差值个步数
  3. 一起走,直到相遇 
/**
 * 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) {
        if(headA == null ||headB == null ) return null;
        int len1 = 0;
        int len2 = 0;
        ListNode pl = headA;
        ListNode ps = headB;
        while(pl != null){
            len1++;
            pl = pl.next;
        }
        while(ps != null){
            len2++;
            ps =ps.next;
        }
        pl = headA;
        ps = headB;
        int len = len1 - len2;
        if(len <0){
            pl = headB;
            ps = headA;
            len = len2-len1;
        }
        while(len != 0){
            pl = pl.next;
            len--;
        }
        while(pl != ps){
            pl = pl.next;
            ps = ps.next;
        }
        return pl;
    }
}

 9.环形链表

力扣

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false 。

 示例一:

【数据结构】必掌握的链表面试题_第33张图片

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。


示例二:

【数据结构】必掌握的链表面试题_第34张图片

输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

示例三

输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

【思路】
  这道题目我们又要用到”快慢指针“的思想,即慢指针一次走一步,快指针一次走两步,两个指针从链表起始位置开始运行,如果链表带环则一定会在环中相遇否则快指针率先走到链表的末尾
 

【一些思考】 

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

【数据结构】必掌握的链表面试题_第35张图片

如何判断没环的情况

当速度快的fast为null时,说明没有环

/**
 * 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) {
        if(head == null) return false;
        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.找环形链表开始入环的第一个节点

【数据结构】必掌握的链表面试题_第36张图片

  1. 起始点到入口点的距离为X
  2. 环的长度设为C
  3. 设相遇点到入口点的距离为Y

【几点说明】

1.因为fast的速度是slow速度的两倍,所以当它们相遇后,此时fast走的路程是slow走的路程的两倍

2.当slow指针入环时,fast可能已经在环中绕了n圈了,n至少为1

3.slow入环时,fast肯定会在slow走一圈之内追上slow指针,因为slow进环后,快慢指针之间的距离最大就是环的长度,而两个指针移动时,每次它们的距离都缩减一步,因此在慢指针移动一圈之前快指针一定能追上慢指针

4.两个指针的路程:

  • fast : X+nC+C-Y
  • slow: X+C-Y

fast路程是slow的两倍

所以有: X+nC+C-Y = X+X+C+C-Y-Y

化简得,X=(n-1)C+Y

极端情况下,假设n=1,此时 X = Y

即:把一个指针移到链表起始位置,从这里运行,另一个指针从相遇点位置绕环

每次都走一步,两个指针最终会在入口点的位置相遇,这就帮助我们找到了入环点

/**
 * 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) {
        if(head == null) return null;
        ListNode fast = head;
        ListNode slow = head;

        while(fast != null  &&  fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
            if(fast == slow){
                break;
            }
        }
        //到这里后,有两种情况:
        //1.不满足循环条件(其中一个为null,证明没有环) 
        //2.遇到break(有环,且相遇了)

        //判断是那种情况
        if(fast == null || fast.next == null){
            return null;
        }
        
        //把一个指针移到起始位置,另一个不变,一起每次走一步,再次相遇点就是入口点
        slow = head;
        while(slow != fast){
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }
}

你可能感兴趣的:(数据结构,链表,面试,数据结构)