链表的提升学习Java语言(在线OJ面试题)

题目前提

class ListNode {
    int val;
    ListNode next;

    ListNode() {
    }

    ListNode(int val) {
        this.val = val;
    }

    ListNode(int val, ListNode next) {
        this.val = val;
        this.next = next;
    }
}

做这些题目之前会先给出一个节点类,有节点的val和节点的next,构造方法有三个:第一个没有参数:不做任何操作;有一个参数的:实例化一个节点的同时,将节点的val赋值;有二个参数的:实例化一个节点的同时,将节点的val和next赋值。

第一道题

题目

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例一
在这里插入图片描述

代码

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

解题思路

假设有这样一个链表:
在这里插入图片描述
我们需要将他反转,我们再上一篇博客中讲过尾插法,我们可以利用尾插法实现,我们先将23插到12的前面,往后移将34插到23前面,以此类推,直到节点遍历为null,跳出循环,如何用代码实现呢?首先实例化一个节点prev,这个节点为null,再实例化一个节点指向head,我们将prev的值赋值给cur的next,那么12的next就改为null,也就是尾节点,然后到cur往后移到23,prev往后移到head,再将prev的值赋值给cur的next,此时23就与12相连,直到cur为null,循环结束。但是上述过程中有一个致命的错误,也是非常易错的地方,当我们将prev赋值给cur的next,后cur就无法往后移动,因为cur.next已经被覆盖掉了,所以无法用cur=cur.next,因此我们需要再引进一个节点(curNext)实例用来记录cur后面一个节点的位置。 像这样画图理解:
准备工作:
在这里插入图片描述

第一次移动:
在这里插入图片描述
第二次移动
在这里插入图片描述
以此类推直到cur为null,跳出循环,这就实现了链表的反转,当然最后别忘了返回链表的头,此时的头应该为prev(cur为null),head是原来链表的头,是反转链表的尾。问题解决!

第二道题

题目

给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
示例1:

输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。

示例2:

输入:[1,2,3,4,5,6]
输出:此列表中的结点 4 (序列化形式:[4,5,6])
由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。

代码

public ListNode middleNode(ListNode head) {
        if (head == null) {
            return null;
        }
        ListNode fast = head;
        ListNode slow = head;
        while (fast != null && fast.next != null) {
            fast = fast.next;
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }

解题思路

刚接触的这道题目时,第一反应是,求链表的长度,然后除以2,再把该链表的对应节点打印出来,这种算法当然可以解决,但是时间复杂度相对于遍历了两遍链表,有没有遍历一次的方法呢?当然有,这里就介绍一种双指针的方法。
假设有这样一种链表:
在这里插入图片描述

我们实例化两个节点(fast,slow),这两个节点都指向head,之后fast往后移动两个节点,slow移动一个节点,直到链表遍历完(fast为null或者fast的nest为null)像这样:
在这里插入图片描述

如果链表的长度为奇数,那么fast的next就为null,跳出循环,slow走了2步也就是34。
在这里插入图片描述

当链表的长度为偶数时,fast走了2步,slow走了1步,此时fast不为null,fast的next不为null,fast继续走2步,slow继续走一步。
在这里插入图片描述
fast此时为null,所以跳出循环,返回slow,当然还有一种情况不能忘记,当链表为null时,返回null。问题解决!

第三道题

题目

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

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

代码

   public ListNode FindKthToTail(ListNode head, int k) {
        if (k <= 0) {
            return null;
        }
        if (head == null) {
            return null;
        }
        ListNode fast = head;
        ListNode slow = head;
        while (k - 1 != 0) {
            fast = fast.next;
            if (fast == null) {
                return null;
            }
            k--;
        }
        while (fast.next != null) {
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }

解题思路

这种问题,大部分人想到的肯定还是求链表的长度,而后由倒数求正数,这种算法固然能解决这个问题,但是同样时间复杂度比较大,那么有没有简单的方法呢?还是用双指针解决问题。
在这里插入图片描述
假设有这样一个链表,求倒数第二个节点(0x456),我们可以实例化两个节点(fast,slow)都指向head,我们让fast先走一步,然后fast和slow都往后以相同的速度移动,直到fast的next为null,这样返回slow节点,这个节点就是倒数第二个。像这样:
fast先走一步:
在这里插入图片描述
slow和fast以相同的速度往后走:
在这里插入图片描述
这样就完成了输出倒数第n个节点,当然还有漏洞需要我们完善。第一,k(倒数第几个)是否合法,首先k不能为负数,同样k不能大于链表的长度,但是有同学会说不能大于链表的长度,链表的长度还是要求出来,那还不如用由倒数求正数,的确但是也可以不用求出链表的长度,我们第一步是让fast往后先走k-1,因此我们只需要判断k先走的过程中,是否出现null的情况,如果出现就返回null,这样就避免了求整个链表的长度。第二,当head为null时,同样返回null。问题解决!

第四题

题目

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
实例1:
在这里插入图片描述

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

代码

    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if (l1 == null && l2 == null) {
            return null;
        }
        ListNode newHead = new ListNode();
        ListNode tmp = newHead;
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                tmp.next = l1;
                l1 = l1.next;
                tmp = tmp.next;
            } else {
                tmp.next = l2;
                l2 = l2.next;
                tmp = tmp.next;
            }
        }
        if (l1 == null) {
            tmp.next = l2;
        } else {
            tmp.next = l1;
        }
        return newHead.next;
    }

解题思路

这道题相对比较简单,那么如何解决呢?首先题目告诉我们是升序链表,我们可以重新创建一个头节点newHead接收合并后的所有节点,因此我们只需要遍历l1和l2链表,在遍历的过程中比较l1.val和l2.val的值,如果l1.val<=l2.val的值,那么就将l1连接到newHead上,反之亦然。直到l1或者l2有一个遍历完后(l1或者l2you一个为null),将l2或者l1剩下的连接到newHead新链表上。最后返回newHead.next的值。
假设有这样两个链表:
在这里插入图片描述

我们创建一个新的头newHead,并用tmp指向它(用来遍历新的链表)。
在这里插入图片描述
首先,我们用l1和l2遍历整个链表,比较l1和l2的大小,我们可以看到l1的val值小于等于l2的val(1<=1),因此将l1连接到tmp的后面,并且tmp往后移一个,l1也往后移一个。
在这里插入图片描述
再比较l1与l2的val值,直到l1和l2有一个链表为null停止,此时另一个链表不一定为null,因此将另一个链表的剩下的所有节点都连接到tmp上,所有工作完成。返回newHead.next。但是还是有细节需要注意的地方。第一,当l1和l2都为null时要考虑,如果l1和l2为null,那么返回null;第二循环的结束条件,什么时候结束循环,如果是(l1 .next!= null && l2 .next!= null)跳出循环,那么会发生最后一个节点的丢失,导致bug。

第五题

题目

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

举例:[3,1,6,6,11,8,5],x=6
结果:[3,1,5,6,6,11,8]

代码

    public ListNode partition(ListNode head, int x) {
        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) {
            return as;
        }
        be.next = as;
        if (as != null) {
            ae.next = null;
        }
        return bs;
    }

解题思路

这一题听起来简单,做起来也很简单,但是细节方面会比较多,容易出错,如何解决,首先我们实例化一个节点cur,指向链表的头,用cur遍历整个链表,当cur.val值小于指定x值时,放进新链表l1中,否则放进另一个链表l2中,当cur遍历完整个链表,最后将链表l1和l2连接起来。整体实现过程时非常简单的。我们以[3,6,6,5,8,7]为例。
在这里插入图片描述

我们定义4个节点bs(l1的头),be(l1的遍历节点),as(l2的头),ae(l2的遍历节点),再定义一个节点cur指向head(用于遍历原链表),首先遍历第一个节点的val值3(小于x),因为bs为null,我们将bs和be都指向cur,遍历第二个节点的val值6(大于等于x),因为as为null,我们将as和ae都指向cur,接着cur往下遍历,遍历第三个节点的val值6(大于等于x),此时as不在为null,因此只需要ae往后移动,cur往后移动即可。
在这里插入图片描述

因此最后就是这种情况,最后我们将两个链表相连接就可以完成,即be.next=as,但是我们还需要注意一些细节方面的问题,第一,原链表都不小于指定的x,这会导致第一个链表为null,也就是说bs和be为null,因此不能用be.next=as,这种情况下,我们就直接返回as就可以了。第二,最后一个元素不一定时第二个链表的最后一个元素,什么意思呢?也就是第二个链表的最后一个元素的next不一定时null,这就会发生链表无法遍历完导致溢出,出现bug,因此只要第二个数组不为null,我们就需要手动将ae的next置为null,这两个是非常容易出错的地方,需要注意。

第六题

题目

在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表 1->2->3->3->4->4->5 处理后为 1->2->5。

代码

    public ListNode deleteDuplication(ListNode pHead) {
        ListNode cur = head;
        ListNode newHead = new ListNode();
        ListNode tmp = newHead;
        while (cur != null) {
            if (cur.next != null && cur.val == cur.next.val) {
                while (cur.next != null && cur.val == cur.next.val) {
                    cur = cur.next;
                }
                cur = cur.next;
            } else {
                tmp.next = cur;
                tmp = tmp.next;
                cur = cur.next;
            }
        }
        tmp.next = null;
        return newHead.next;
    }
}

解题思路

这一题从题目的理解方面来时是比较简单易懂的,题目中告诉我们是排序好的链表,这就意味着如果是重复的节点,那么一定是连在一起的,但是在代码书写方面并不是很简单,如何实现呢?首先我们需要实例化一个节点用于链表的遍历,同时在实例化一个新的链表,用于连接符合要求的节点。在遍历的过程中,如果发现cur.val与cur.next.val的值相等时,则cur往后移动,否则,将节点连接到新的链表上。返回新的头节点的next值。我们举个例子:[1,3,3,4,4,5]

在这里插入图片描述
我们实例化节点cur指向head,同时实例化newHead节点,并用tmp指向它。
在这里插入图片描述
当cur.val(1)与cur.next.val(3)不相等时,将cur赋值给tmp.next。此时0x123节点连接到newHead节点上。tmp往后移,cur往后移,当cur.val(3)与cur.next.val(3)相等时,cur往后移。当cur.val(3)与cur.next.val(4)不相等时,跳出循环,此时cur往后移(这一步往后移动的目的:前面都是相等的val值
,最后一个与前面的val值相等,因此我们也需要跳过),同理后面也是如此下去,直到链表遍历结束。
在这里插入图片描述
但是,其中有一些细节问题,当链表的最后是重复的节点就会出现问题,哪个时候的tmp.next不为null,所以如果返回newHead.next就会发生溢出,出现bug,因此我们需要手动将tmp.next赋值为null,最后在返回newHead.next。

第七题

题目

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

举例:1->2->2->1
返回:true

代码

public boolean chkPalindrome(ListNode A) {
        ListNode fast = A;
        ListNode slow = A;
        while (fast != null && fast.next != null) {
            fast = fast.next;
            fast = fast.next;
            slow = slow.next;
        }
        ListNode cur = slow.next;
        slow.next = null;
        while (cur != null) {
            ListNode curNext = cur.next;
            cur.next = slow;
            slow = cur;
            cur = curNext;
        }
        boolean flag = true;
        while (slow != null) {
            if (slow.val != A.val) {
                flag = false;
                break;
            }
            slow = slow.next;
            A = A.next;
        }
        return flag;
    }

解题思路

判断是否为回文是一类经典的提醒,首先他需要O(n)的时间复杂度和O(1)的空间复杂度,类似将链表的val放进数组里面再进行回文判断是不可以的,那么如何解决呢?主要分三步,第一步:寻找中间节点;第二步,逆置链表;第三步,比较各个val值是否相等。也就是说,我们将一个链表对半分,前面一半不动,后面一半链表逆置,然后一个一个比较是否一样。先看第一步:寻找中间节点。这个我们上面讲过用快慢指针就能解决。第二步逆置链表,我们将实例化两个个节点cur和curNext,用这两个节点进行逆置,第三步比较大小,从最后一个节点和第一个节点开始比较,直到不满足某个循环条件,跳出循环。我们以[12,23,34,23,12]为例。
在这里插入图片描述
第一步:找到中间节点,我们用快慢指针解决,快指针一次移动两次,慢指针一次移动一次。最后达到这种情况。
在这里插入图片描述
此时,我们实例化cur节点,指向slow.next,如果cur不为null,那么就将cur.next赋值为slow。slow往后移动,cur往后移动,因为cur.next已经被覆盖,因此无法找到cur后面一个节点,因此我们用curNext记录cur后面一个节点的位置,直到循环结束。逆置完成。
在这里插入图片描述
但是细心的同学会发现我做了一个动作,我在34的地方,将next赋值成null,至于干什么?我们后面介绍。完成逆置后,slow就来到了最后一个节点,此时比较slow.val与head.val的大小,如果相同,head往后移动,slow往前移动,直到slow为null,跳出循环,问题解决。那么为什么要将34的位置next置为null呢?我们发现是循环条件用到了slow!=null,有同学会说我把head!=slow作为循环条件不是也可以嘛?当然可以,但是这是针对的奇数,对于偶数来说,并不会出现head==slow的情况,因此循环不会跳出,但是只要我们将第一步完成后slow的位置置为null,一旦slow回到这个位置,那么循环就会结束。

第八题

题目

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

代码

    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) {
            return null;
        }
        ListNode pl = headA;
        ListNode ps = headB;
        int lenA = 0;
        int lenB = 0;
        while (pl != null) {
            pl = pl.next;
            lenA++;
        }
        pl = headA;
        while (ps != null) {
            ps = ps.next;
            lenB++;
        }
        ps = headB;
        int len = lenA - lenB;//差值步
        if (len < 0) {
            pl = headB;
            ps = headA;
            len = lenB - lenA;
        }
        while (len > 0) {
            pl = pl.next;
            len--;
        }
        while (pl != null) {
            if (pl.next != ps.next) {
                pl = pl.next;
                ps = ps.next;
            } else {
                break;
            }
        }
        if (pl == null) {
            return null;
        }
        return pl;
    }

题目解析

这个题目并不难,首先我们需要了解,这个问题中交叉是类似“Y”还是“X”,当两个节点相交后,后面的部分应该是重叠的,就像这样。
在这里插入图片描述
那么该如何解决这个问题呢?首先我们需要找到最长的链表,然后实例化两个节点分别指向这两个链表,让最长的链表先走差值步,然后这两个节点同时往后遍历,直到相遇或者为null,问题解决。举个例子:
在这里插入图片描述
我们实例化两个节点,分别遍历两个链表,并将链表的长度记录为lenA和lenB,然后让pl指向最长的链表(首个节点val值为4的链表),ps指向最短的链表(首个节点val值为2的链表),让pl走lenA与lenB的差值(要为正数),然后分别往后走,直到相遇返回pl,或者走完整个链表返回pl(pl此时为null),问题解决。当然不能忘记当两个链表任意一个为null时的情况。

第九题

题目

给你一个链表的头节点 head ,判断链表中是否有环。
示例1:
在这里插入图片描述

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

代码

    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;
    }

解题思路

这一道题是比较简单的,我们举一个比较常见的例子,不带环的链表就像一条长跑道,因此跑的慢的永远不能与跑的快的相遇,我们将带环的链表想象成一个环形跑道,因此慢的是可以和跑的快的相遇,只要跑得快的跑完一圈与慢的相遇就可以了。同样,我们也可以用这种算法解决问题,但是需要注意的是:实际生活中,我们是不停的往前走,总有某一个时刻是相遇的,但是在代码中我们不能实现,因为我们是按次数来进行比较,也就是一次循环中,跑的快的先运动,跑的慢的再运动,然后再比较,这样有可能导致会两个节点错过,但是我们只需要让快的走2步,慢的走1步就可以实现了,问题就解决了。这道问题比较简单,我们就不举具体的例子了。

第十题

题目

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
示例1:
在这里插入图片描述

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

代码

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) {
                fast = head;
                break;
            }
        }
        if (fast==null||fast.next==null){
            return null;
        }
        while (fast != slow) {
            fast=fast.next;
            slow=slow.next;
        }
        return fast;
    }

解题思路

这道题中是跟上一题类似,但是难度不是一个等级的,有同学这个好办,我们只需要找到原链表最后一个节点的next指向的节点就可以了,那么怎么直到原链表的最后一个节点呢?所以这种方法不太可行,还有点同学说,只需要找到像重复的next,就可以找到入环的第一个节点,这种方法是可行的,但是如何在空间复杂度为O(1)的情况下,完成解题呢?我们来看:
在这里插入图片描述
我们由所有的条件可以得到slow走过的距离为x+C-y(slow不可能会在很多圈后与fast相遇,因为fast与slow速度相差只有1步,因此只能在slow一圈内相遇),而fast走过的距离为x+nC+C-y,又因为fast速度是slow速度两倍,因此2(x+C-y)=x+nC+C-y,所以得到x=(N-1)×C+y。而有这个又可以得到,从相遇点到入环点的距离加上 N-1 圈的环长,恰好等于从链表头部到入环点的距离。因此如果从相遇点和起点开始,同时同速度运动,那么相遇点的位置为入口点。
好了!关于链表的几道练习题就是这么多了,如果有什么意见或者想法欢迎私信,谢谢你的点赞支持!附上LeetCode和牛客链表的链接:
LeetCode:LeetCode链表
牛客网:牛客网链表
再次感谢各位!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Solitudefire

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值