链表经典面试题(反转链表,中间节点,倒数第k个节点,合并分割链表,删除重复节点)

Posted Ischanged

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了链表经典面试题(反转链表,中间节点,倒数第k个节点,合并分割链表,删除重复节点)相关的知识,希望对你有一定的参考价值。

🎓反转一个单链表。

题目描述
反转很好理解,就是我们生活中所说的颠倒,反转后原来链表的头节点变成尾结点,尾结点变成头节点,链表内容的顺序相反就行了。
题目链接

基本思路
好理解的双指针(c里面的是指针,java里面是引用变量)

  1. 定义两个指针: pre和 cur ;pre在前 cur在后,pre的初始值为null,用它记录要反转节点的前一个节点的地址,cur里面开始存头节点的地址,指向头节点。
  2. cur之后向后移动遍历整个链表,每次让cur的next指向pre ,实现一次局部反转,局部反转完成之后,pre 和 cur同时往后移动一个位置循环上述过程,直至 pre 到达链表尾部。
    3.但在cur的next指向pre之前,要定义一个临时变量存储cur后面节点的地址,如果没有提前存储cur后面节点的地址,cur的next指向了pre,cur后面所有的节点都将找不到了。

代码实现:

//声明一个类
class ListNode 
int val;
ListNode next;
public ListNode(int val) 
this.val=val;



class Solution 
	public ListNode reverseList(ListNode head) 
		//申请节点,pre和 cur,pre指向null
		ListNode pre = null;
		ListNode cur = head;
		ListNode tmp = null;
		while(cur!=null) 
			//记录当前节点的下一个节点
			tmp = cur.next;
			//然后将当前节点指向pre
			cur.next = pre;
			//pre和cur节点都前进一位
			pre = cur;
			cur = tmp;
		
		return pre;
	


动图演示

🎓返回链表的中间结点

题目描述:
题目很好理解,只是注意下,当链表元素个数为偶数的时候,中间节点有两个,题目说的是返回第二个中间节点,还有有时候可能遇到叫返回第一个节点的情况。
题目连接

基本思路:
📝比较经典的做法是:快慢指针
首先如果链表没有节点或者只有一个节点,直接返回null,什么都不做。如果有两个以上的节点,使用两个指针变量,刚开始都位于链表的第 1 个结点,一个永远一次只走 1 步,一个永远一次只走 2 步,一个在前,一个在后,同时走。循环走向去,这样当快指针走完的时候,慢指针就来到了链表的中间位置。这也很好理解就像跑步一样,甲乙两人一起跑步,甲的速度是乙的两倍,当他们走相同的时间的时候,乙走的路程是甲的一半,乙在甲走的路程中点处。
对于循环的条件fast!=null&&fast.next!=null,自己画个偶数,画个奇数的图,就明白了不能单独的是fast.next!=null为判断条件,不然偶数那里会发生空指针异常。

class Solution 
    public ListNode middleNode(ListNode head) 
        if(head==null||head.next==null) //如果链表为空直接返回null
            return head;
        
        ListNode fast=head;
        ListNode slow=head;
        while (fast!=null&&fast.next!=null) 
slow=slow.next;//走一步
fast=fast.next.next;//走两步

        
        return slow;


    

动图演示:
偶数情况:

奇数情况:

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

题目描述:
题目连接

基本思路:
方法一:

  1. 先遍历统计链表长度,记为 n ;
  2. 设置一个指针走 (n-k) 步,即可找到链表倒数第 k个节点。
    分析如图:

    代码实现:
class Solution 
    public ListNode getKthFromEnd(ListNode head, int k) 

        ListNode cur=head;
        int count=0;
         if(head==null) //空链表
        return head;
    
    if(k<=0) //输入的节点位置不合法,k是从1开始的
        return null;
    
        while(cur!=null) //求链表的长度
count++;
cur=cur.next;
        


        while((count-k)!=0) //找倒数第k个节点
head=head.next;
count--;

        
      return head;  

    

这种思路的时间复杂度为o(n),要遍历链表求链表的长度,至于说要遍历链表两次,没有双指针好,我觉得差不多,双指针也要遍历两次链表。
方法二:双指针法,该方法只用一次循环不用求链表的长度。
基本思路:

1.初始化: 定义快指针fast,慢指针slow(这里是引用变量),双指针都指向头节点 head​ 。
2.构建双指针距离:快指针fast先向前走 k步(结束后,快指针fast,慢指针slow 间相距 k 步)。
3.双指针共同移动:在 循环中,快指针fast,慢指针slow 每轮都向前走一步,直至 fast 走到链表的 尾节点 时跳出(跳出后, slow与尾节点距离为 k-1步,这时fast 指向的就是倒数第 k 个节点)。
4.返回值: 返回 slow 即可。 时间复杂度为O(n),空间复杂度为O(1),快慢指针法不用求链表的长度。

图示分析:

代码实现:

class Solution 
    public ListNode getKthFromEnd(ListNode head, int k) 
        ListNode fast=head;
        ListNode slow=head;
        if(head==null) 
            return head;
        
        if(k<=0)
            return null;
        while(k-1!=0) 
            if(fast.next==null)
                return null;
            else 
fast=fast.next; 
k--;
            
                
        
        while(fast.next!=null) 
            fast=fast.next;
            slow=slow.next;
        
        
return slow;
    

    

动图分析:

🎓将两个有序链表合并为一个新的有序链表并返回。

题目描述:
理解题目:注意,两个升序链表合并为一个新的链表后任然是升序的,不是简单的拼接,可以存在相同的值。
题目链接

基本思路:
方法一:迭代
📝我们可以用迭代的方法来实现上述算法。当 链表A 和 链表B 都不是空链表时,同时从链表A 和 链表B的头节点出发,两两比较,哪一个链表的头节点的值更小,将较小值的节点添加到新的链表里面,当一个节点被添加到新的链表之后,将对应链表中的节点向后移一位。如果比较后链表中的结点值没有被添加到新的链表,则节点值位置不变和另一个链表中下一个节点比较,在比较值的时候小于和等于的情况可以归为一类,小于的节点先接在新的链表后面,两个链表的节点值相等的时候先接哪个节点都一样,如此循环下去直到其中一个链表遍历完(循环结束的条件为其中一个链表为空,两个链表不可能同时为空)。因为是两两比较和升序链表,所以两个链表中当一个先遍历完,把没有遍历完的链表剩余的节点接在新链表的节点之后。之后返回新链表的头节点即可。不咋清楚的再看看图。时间复杂度:O(n + m),空间复杂度:O(1)O(1)
图示分析:

代码实现:

class Solution 
    public ListNode mergeTwoLists(ListNode headA, ListNode headB) 
        if( headA==null) return headB;
        if( headB==null) return headA;
        if( headB==null&&headA==null) return null;
        ListNode NewHead=new ListNode(-1);
        ListNode tem=NewHead;
        while (headA!=null&&headB!=null) 
            if(headA.val<headB.val) 
tem.next=headA;
headA=headA.next;
            
             else  
tem.next=headB;
headB=headB.next;
        
   //     else 

//tem.next=headB;
//headB=headB.next;
//headA=headA.next;
    //    
tem=tem.next;
        
 //最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可      
if(headB==null) tem.next=headA;
if(headA==null) tem.next=headB;
return NewHead.next;
    

动图分析:

方法二:递归法:
递归法通常将一个大型复杂的问题层层转化为一个与原问题相似的较小的问题来求解,递归只需要少量几步就可以解决复杂的问题,求解递归如果找到公式很容易的决解问题,如果没有公式则难以入手,对于这道题的求解公式大概如下:

基本思路:
如果 l1 或者 l2 一开始就是空链表 ,那么没有任何操作需要合并,所以我们只需要返回非空链表。否则,我们要判断 l1 和 l2 哪一个链表的头节点的值更小,然后递归地决定下一个添加到结果里的节点。如果两个链表有一个为空,递归结束。时间复杂度:O(n + m),空间复杂度为 O(n+m)。
代码实现:

class Solution 
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) 
        if(l1 == null) 
            return l2;
        
        if(l2 == null) 
            return l1;
        

        if(l1.val < l2.val) 
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
         else 
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;
        
    



✏️以给定值x为基准将链表分割成两部分

题目描述:
题目也好理解以给定值x,将链表分成两部分,但要注意在分割的时候不需要排序,保持原有的顺序,就比如示例的4,3分割后为4,3不要写成3,4。
题目链接

基本思路:
只需要遍历链表的所有节点,小于x的放到一个小的链表中,大于等于x的放到一个大的链表中,最后再把这两个链表串起来即可。
图示分析:


代码实现,带哨兵节点(省掉一些特殊条件的判断,使代码更加简洁,如本题中不需要判断第一次添加节点时头结点的情况)

 public ListNode partition(ListNode head, int x) 
        //小链表的头
        ListNode smallHead = new ListNode(0);
        //大链表的头
        ListNode bigHead = new ListNode(0);
        //小链表的尾
        ListNode smallTail = smallHead;
        //大链表的尾
        ListNode bigTail = bigHead;
        //遍历head链表
        while (head != null) 
            if (head.val < x) 
                //如果当前节点的值小于x,则把当前节点挂到小链表的后面
                smallTail = smallTail.next = head;
             else //否则挂到大链表的后面
                bigTail = bigTail.next = head;
            

            //继续循环下一个结点
            head = head.next;
        
        //最后再把大小链表拼接在一块即可。
        smallTail.next = bigHead.next;
        bigTail.next = null;
        return smallHead.next;
    

✏️在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点i

题目描述:
题目链接

基本思路:
首先一个比较直观且通用的思路是,采用边遍历边构造的方式:

  1. 定义和构造新的节点,通过类的实例化产生一个虚拟节点newHead,以减少边界判断,用于之后接收不重复的节点,构造新的链表并返回新链表,定义一个临时节点cur指向需要删除的链表进行遍历链表节点,tem节点开始指向虚拟机节点之后代替虚拟节点移动接收不重复的节点(虚拟节点不能移动)
  2. 从头节点一直遍历元素直到把链表元素遍历完,遍历时如果从某一位置起有节点和后面的一个或多个节点相等,就一步一步的往后走跳过重复的元素,如果不是重复的元素就一个一个按顺序接在虚拟节点的后面,形成新的链表,之后返回虚拟节点的下一个位置位置即可以得到不重复的链表了。
    一些详细要拿捏的细节我们一起来看图示分析吧!


动图分析:

✏️删除链表中重复的元素ii

题目描述:
题目链接
下面的这道题和上面的有一些相似,所以我也把它总结在一起了,下面的这道题要求重复的节点出现一次,比上面的题简单一些,从头的位置和后面的节点比较,后面和前面相同就删除,删完了就移动。

基本思路:

  1. 指定 cur 指针指向头部 head
  2. 当 cur 和 cur.next的存在为循环结束条件,当二者有一个不存在时说明链表没有去重复的必要了 ,也就是说最少要有两个节点。
  3. 当 cur.val 和 cur.next.val相等时说明需要去重,则将 cur 的下一个指针指向下一个的下一个,这样就能达到去重复的效果
  4. 如果不相等则 cur移动到下一个位置继续循环 时间复杂度:O(n)

代码实现:

class Solution 
    public ListNode deleteDuplicates(ListNode head) 
        ListNode cur = head;
        while(cur != null && cur.next != null) 
            if(cur.val == cur.next.val) 
                cur.next = cur.next.next;
             else 
                cur = cur.next;
            
        
        return head;
    

✏️总结

链表题目形式多样,解法也很多,也有很多的技巧,需要我们多刷题,多画图,多动手,慢慢体会最终才能正真明白。**纸上得来终觉浅,绝知此事要躬行。*文章如有问题,请大佬指正。
🛣️过🉐小🧑🏽‍🤝‍🧑🏼如果觉🉐🦟🐙✔️你🈶帮助请点👍🏻🍹持👇🏻,🦀🦀了
如果对于顺序表及链表的基础知识不了解的老铁,请移步看一下我写的有关顺序表,链表的其他文章。
*旋转链表

链表的概念和结构及基本功能函数的实现(单链表的实现)
顺序表基本功能函数的实现

以上是关于链表经典面试题(反转链表,中间节点,倒数第k个节点,合并分割链表,删除重复节点)的主要内容,如果未能解决你的问题,请参考以下文章

最强解析面试题:链表中倒数第 k 个节点「建议收藏!」

最强解析面试题:链表中倒数第 k 个节点「建议收藏!」

面试题22:链表中倒数第k个节点

看了有助于你面试的单链表的OJ面试题

力扣题解-面试题22. 链表中倒数第K个节点

LeetCode 面试题22. 链表中倒数第k个节点