链表的提升学习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链表
牛客网:牛客网链表
再次感谢各位!