链表专题是最开始学算法的时候写的,很多代码都写得很烂,目前正在慢慢的重写,u1s1 链表的题还是很考验细心的,稍不注意就连错了
给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。 如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。 您可以假设除了数字 0 之外,这两个数都不会以 0 开头。
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807
解法一
public ListNode addTwoNumbers (ListNode l1, ListNode l2) { ListNode dummyNode=new ListNode (-1 ); ListNode temp=dummyNode; dummyNode.next=temp; int carry=0 ; while (l1!=null || l2!=null ){ int sum= (l1!=null ?l1.val:0 ) + (l2!=null ?l2.val:0 )+ carry; temp.next=new ListNode (sum%10 ); temp=temp.next; carry=sum/10 ; l1=l1!=null ?l1.next:null ; l2=l2!=null ?l2.next:null ; } if (carry!=0 ) temp.next=new ListNode (1 ); return dummyNode.next; }
很久之前写的代码了,代码很乱,用 0 补齐短的那个然后对应相加注意进位就行了
2020.3.22 把之前的代码删了,一年前的代码,写的太丑了
解法二
2020.3.22 新增了一个解法,有点偏,没啥意思,不过熟悉下链表还是可以
public ListNode addTwoNumbers (ListNode l1, ListNode l2) { ListNode dummyNode=new ListNode (-1 ); dummyNode.next=l1; int carry=0 ; ListNode last=l1; while (l1!=null && l2!=null ){ int sum= l1.val + l2.val+ carry; l1.val=sum%10 ; carry=sum/10 ; last=l1; l1=l1.next; l2=l2.next; } while (l1!=null && carry!=0 ){ int sum = l1.val + carry; l1.val=sum%10 ; carry=sum/10 ; last=l1; l1=l1.next; } if (l2!=null ){ last.next=l2; while (l2!=null && carry!=0 ){ int sum = l2.val + carry; l2.val=sum%10 ; carry=sum/10 ; last=l2; l2=l2.next; } } if (carry!=0 ) last.next=new ListNode (1 ); return dummyNode.next; }
给定两个非空 链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储单个数字。将这两数相加会返回一个新的链表。 你可以假设除了数字 0 之外,这两个数字都不会以零开头。
进阶: 如果输入链表不能修改该如何处理?换句话说,你不能对列表中的节点进行翻转。
输入:(7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 8 -> 0 -> 7
public static ListNode addTwoNumbers (ListNode l1, ListNode l2) { BigInteger b1 = new BigInteger (list2num(l1)); BigInteger b2 = new BigInteger (list2num(l2)); String resStr=b1.add(b2).toString(); ListNode res=new ListNode (1 ); ListNode real=res; for (int i=0 ;i<resStr.length();i++) { real.next=new ListNode (Integer.valueOf(resStr.charAt(i)-48 )); real=real.next; } return res.next; } public static String list2num (ListNode l) { String num="" ; while (l!=null ){ num=num+l.val; l=l.next; } return num; }
这两题方法很多,下面那题实际上是上面那一题反过来的,但是题目要求不改变链表所以可以利用栈来反转,然后就跟上面的类似了,然后这里我偷了个懒用的BigInteger
搞的速度也还行 77%beat。
解法二
(update: 2020.4.14)上面的解法笔试这样写倒是无所谓,面试这样写肯定是不行的,咱还是得规规矩矩的写
public ListNode addTwoNumbers (ListNode l1, ListNode l2) { Deque<Integer> stack1=new ArrayDeque <>(); Deque<Integer> stack2=new ArrayDeque <>(); ListNode res=new ListNode (-1 ); while (l1!=null ){ stack1.push(l1.val); l1=l1.next; } while (l2!=null ){ stack2.push(l2.val); l2=l2.next; } int carry=0 ; while (!stack1.isEmpty() || !stack2.isEmpty() || carry>0 ){ int temp=(stack1.isEmpty()?0 :stack1.pop())+(stack2.isEmpty()?0 :stack2.pop())+carry; ListNode next=res.next; ListNode newNode=new ListNode (temp%10 ); res.next=newNode; newNode.next=next; carry=temp/10 ; } return res.next; }
最后的头插法还是挺好的,我开始还想着翻转一下的,我看见评论区有人用递归写,我试了下,还是算了,太麻烦了,还没上面的简洁,写一大坨初始化,然后再递归,代码一点都不简洁,没啥意义
public static ListNode middleNode (ListNode head) { if (head==null ||head.next==null )return head; ListNode fast=head; ListNode slow=head; while (fast!=null &&fast.next!=null ){ fast=fast.next.next; slow=slow.next; } return slow; }
快慢指针
,很常见很经典的做法后面很多题会用到这个。
反转一个单链表。
解法一
public static ListNode reverseList (ListNode head) { if (head == null || head.next == null ) { return head; } ListNode newHead = reverseList(head.next); head.next.next = head; head.next = null ; return newHead; }
解法二
public static ListNode reverseList2 (ListNode head) { if (head==null )return head; ListNode pre=head; ListNode cur=head.next; ListNode temp; while (cur!=null ){ temp=cur.next; cur.next=pre; pre=cur; cur=temp; } head.next=null ; return pre; }
反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。
解法一
public static ListNode reverseBetween (ListNode head, int m, int n) { if (m==n) return head; ListNode pre=head; ListNode mid=head.next; ListNode rear=null ; ListNode preM = null ; ListNode valM=head; int count = 1 ; while (count <=n-1 ){ rear=mid.next; if (count==m-1 ){ preM=pre; valM=mid; } if (count==n-1 ){ valM.next=rear; if (m==1 ){ head=mid; } else { preM.next=mid; } } if (count >= m && count <=n-1 ){ mid.next=pre; } pre=mid; mid=rear; count++; } return head; }
代码写的比较烂但是思路还是比较清晰,只扫描了一遍链表 2ms beat 100%,但是创建的指针有点多,抠边界要细心。
解法二
被本来是像把上面的解法删掉的,想了一下还是留着,提醒下自己,上面的解法不够通用,属于一次性的解法,细节也很多,当初可能是追求在一个循环内写完,所以可能写的比较难看
func reverseBetween2 (head *ListNode, m int , n int ) *ListNode { dummyNode := &ListNode{ Val: -1 , Next: head, } mpre := dummyNode for i := m; i > 1 ; i-- { mpre = mpre.Next } pre := mpre cur := mpre.Next for i := m; i <= n; i++ { next := cur.Next cur.Next = pre pre = cur cur = next } mpre.Next.Next = cur mpre.Next = pre return dummyNode.Next }
这个解法实际上就是普通一个翻转,然后处理翻转部分的头尾节点连接,但是代码比上面的简洁多了
解法三
头插法的应用,将翻转部分节点用头插法插入 pre 后,也挺不错
func reverseBetween (head *ListNode, m int , n int ) *ListNode { dummyNode := &ListNode{ Val: -1 , Next: head, } mpre := dummyNode for i := m; i > 1 ; i-- { mpre = mpre.Next } cur := mpre.Next for i := m; i < n; i++ { next := cur.Next cur.Next = next.Next next.Next = mpre.Next mpre.Next = next } return dummyNode.Next }
Given a (singly) linked list with head node root
, write a function to split the linked list into k
consecutive linked list “parts”.
The length of each part should be as equal as possible: no two parts should have a size differing by more than 1. This may lead to some parts being null.
The parts should be in order of occurrence in the input list, and parts occurring earlier should always have a size greater than or equal parts occurring later.
Return a List of ListNode’s representing the linked list parts that are formed.
Examples 1->2->3->4, k = 5 // 5 equal parts [ [1], [2], [3], [4], null ]
Example 1:
Input: root = [1 , 2 , 3 ], k = 5 Output: [[1 ],[2 ],[3 ],[],[]] Explanation: The input and each element of the output are ListNodes, not arrays. For example, the input root has root.val = 1 , root.next.val = 2 , \root.next.next.val = 3 , and root.next.next.next = null . The first element output[0 ] has output[0 ].val = 1 , output[0 ].next = null . The last element output[4 ] is null , but it's string representation as a ListNode is [].
Example 2:
Input: root = [1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ], k = 3 Output: [[1 , 2 , 3 , 4 ], [5 , 6 , 7 ], [8 , 9 , 10 ]] Explanation: The input has been split into consecutive parts with size difference at most 1 , and earlier parts are a larger size than the later parts.
Note:
The length of root
will be in the range [0, 1000]
.
Each value of a node in the input will be an integer in the range [0, 999]
.
k
will be an integer in the range [1, 50]
.
解法一
public static ListNode[] splitListToParts(ListNode root, int k) { ListNode temp=root; ListNode next=root; ListNode [] result=new ListNode [k]; int count=0 ; while (temp!=null ){ temp=temp.next; count++; } temp=root; int size=count/k; int num=count%k; result[0 ]=root; int index=1 ; if (k<=count){ for (int i=1 ;temp!=null && index < k;i++){ next=temp.next; if (i<=(size+1 )*num && i%(size+1 )==0 ){ result[index++]=next; temp.next=null ; } else if (i>(size+1 )*num && (i-num)%size==0 ){ result[index++]=next; temp.next=null ; } temp=next; } } else { for (int i=1 ;i<k;i++){ if (temp==null ){ result[i]=null ; } else { next=temp.next; result[i]=next; temp.next=null ; temp=next; } } } return result; }
3ms beat 89% 这题也比较简单主要是边界要抠好
给定一个链表和一个特定值x
,对链表进行分隔,使得所有小于x
的节点都在大于或等于x
的节点之前。
你应当保留两个分区中每个节点的初始相对位置。
输入: head = 1->4->3->2->5->2, x = 3
输出: 1->2->2->4->3->5
解法一
public ListNode partition (ListNode head, int x) { ListNode dummyNode=new ListNode (-1 ); dummyNode.next=head; ListNode pre=cutNode=dummyNode; ListNode cur=head; int cut=0 ; while (cur!=null ){ if (cur.val>=x&&cut==0 ){ cutNode=pre; cut=1 ; } else if (cur.val<x && cut==1 ){ pre.next=cur.next; cur.next=cutNode.next; cutNode.next=cur; cutNode=cur; } pre=cur; cur=cur.next; } return dummyNode.next }
这题也挺简单和上面的那题名字一样是在整理这篇博客的时候现场做的 (2019.2.27) 前后大概半个小时 orz。比较菜,但是这个我没有在本地跑直接在 LeetCode 上提交的然后就过了 1ms beat84% 感觉思路比较清晰就没有本地跑,提交记录上最快的居然是用了额外空间 new 了两个链表然后连起来的。醉了可能是测试用例太少了。
编写一个程序,找到两个单链表相交的起始节点。 如下面的两个链表:
在节点 c1 开始相交。
示例 1:
输入 : intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3输出 : Reference of the node with value = 8输入解释 : 相交节点的值为 8 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
示例 2:
输入 :intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1输出 :Reference of the node with value = 2输入解释 : 相交节点的值为 2 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例 3:
输入 :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。
注意:
如果两个链表没有交点,返回 null
.
在返回结果后,两个链表仍须保持原有的结构。
可假定整个链表结构中没有循环。
程序尽量满足 O(n ) 时间复杂度,且仅用 O(1 ) 内存。
解法一:
public static ListNode getIntersectionNode (ListNode headA, ListNode headB) { ListNode pA=headA; ListNode pB=headB; int lenA=0 ; int lenB=0 ; while (pA!=null ){ pA=pA.next; lenA++; } while (pB!=null ){ pB=pB.next; lenB++; } int dis=lenB>lenA?lenB-lenA:lenA-lenB; if (lenB>lenA){ while (dis-->0 ){ headB=headB.next; } } else { while (dis-->0 ){ headA=headA.next; } } while (headA!=headB){ if (headA.next==null ){ return null ; } headA=headA.next; headB=headB.next; } return headA; }
这种方法比较直接直接计算两个链表的长度然后计算差值然后将长的那个移动到对应的位置让两条链表尾对齐
然后一起向后移动解法二:
public static ListNode getIntersectionNode2 (ListNode headA, ListNode headB) { ListNode pA=headA; ListNode pB=headB; if (headB==null || headA==null ) return null ; while (pA!=pB){ pA=pA==null ?headB:pA.next; pB=pB==null ?headA:pB.next; } return pA; }
同时遍历两个链表,当一个指针到结尾时转到另一个链表头再向后移动 ,这样做的目的和就是可以直接消除链表之间的长度差,使之尾对齐,方法还是很巧妙的,代码也比较简洁。
请判断一个链表是否为回文链表。
示例 1:
输入: 1->2输出: false
示例 2:
输入: 1->2->2->1输出: true
进阶: 你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
public boolean isPalindrome (ListNode head) { if (head==null || head.next==null ){ return true ; } ListNode fast = head; ListNode slow = head; while (fast.next != null ) { fast = fast.next.next == null ? fast.next : fast.next.next; slow = slow.next; } resverList(slow); while (fast != null && head != null ) { if (fast.val != head.val) { return false ; } fast = fast.next; head = head.next; } return true ; } public static void resverList (ListNode node) { ListNode cur = node; ListNode pre = null ; ListNode next = node; while (cur != null ) { next = next.next; cur.next = pre; pre = cur; cur = next; } }
这题就用到了上面的翻转链表的方法,不过这里只翻转了一半,翻转了后半段然后从两边到中间逐个节点对比
请编写一个函数,使其可以删除某个链表中给定的(非末尾)节点,你将只被给定要求被删除的节点。
现有一个链表 – head = [4,5,1,9],它可以表示为:
示例 1:
输入: head = [4,5,1,9], node = 5输出: [4,1,9]解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.
示例 2:
输入: head = [4,5,1,9], node = 1输出: [4,5,9]解释: 给定你链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9.
说明:
链表至少包含两个节点。
链表中所有节点的值都是唯一的。
给定的节点为非末尾节点并且一定是链表中的一个有效节点。
不要从你的函数中返回任何结果。
二货做法
public static void deleteNodelow (ListNode node) { ListNode next; ListNode temp=new ListNode (0 ); ListNode pre=temp; while (node.next!=null ){ next=node.next; if (node.next.next==null ){ pre=node; } temp.val=node.val; node.val=next.val; next.val=temp.val; node=next; } pre.next=null ; }
首先想到的愚蠢的做法,怎么这么蠢????
正确做法
public static void deleteNode (ListNode node) { node.val=node.next.val; node.next=node.next.next; }
删除链表中等于给定值 val 的所有节点。
示例:
输入: 1->2->6->3->4->5->6, val = 6输出: 1->2->3->4->5
解法一: 双指针 + 虚拟头节点
public static ListNode removeElements (ListNode head, int val) { if (head == null ) return null ; ListNode dummyHead = new ListNode (0 ); dummyHead.next = head; ListNode pre = dummyHead, cur = head; while (cur != null ) { if (cur.val == val) { pre.next = cur.next; } else { pre = cur; } cur = cur.next; } return dummyHead.next; }
解法二:
递归方法比较简洁
public static ListNode removeElements2 (ListNode head, int val) { if (head == null ) return null ; head.next = removeElements2(head.next, val); return head.val == val ? head.next : head; }
给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。 示例: 给定一个链表:1->2->3->4->5, 和 n = 2. 当删除了倒数第二个节点后,链表变为 1->2->3->5. 说明: 给定的 n 保证是有效的。 进阶: 你能尝试使用一趟扫描实现吗?
解法一
public ListNode removeNthFromEnd (ListNode head, int n) { if (head==null &&head.next==null ) return null ; ListNode fast=head; ListNode dummyNode=new ListNode (-1 ); dummyNode.next=head; ListNode slow=dummyNode; int count=1 ; while (fast!=null ){ fast=fast.next; slow=count<n?slow:slow.next; count++; if (fast.next==null ){ slow.next=slow.next.next; return dummyNode.next; } } return dummyNode.next; }
看了评论才知道咋一遍循环,主要就是控制 slow 指针走length-n
步,让快指针先走 n 步,然后快慢一起走,快指针到头时慢指针就到length-n
的位置了,这题也可以用 List 保存每个节点让然后把待删除的节点的前一个拿出来操作,遍历两遍的方法比较简单就不写了,感觉这种方法比较好 , 这题的 OJ case 比较少所以没什么可比性 , 前几个都是跑了两遍的,我把最快的拷过来跑的比我还慢。然后我又提交了一次 beat 90%…….
给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中没有重复出现 的数字。
示例 1:
输入:1->2->3->3->4->4->5 输出:1->2->5
示例 2:
输入:1->1->1->2->3 输出:2->3
解法一
乍一看跟上面那一题一样?这题是排序链表上面那题是无序的,而且这题不给定元素
public static ListNode deleteDuplicates (ListNode head) { if (head==null )return null ; ListNode dummyNode=new ListNode (-1 ); dummyNode.next=head; ListNode cur=head; ListNode next=head.next; ListNode pre=dummyNode; while (next!=null ){ while (next.val==cur.val){ next=next.next; if (next==null ){ pre.next=null ; return dummyNode.next; } if (next.val!=cur.val){ pre.next=next; cur=next; break ; } } pre=cur.next!=null &&cur.val==cur.next.val?pre:cur; cur=next; next=next.next; } return dummyNode.next; }
整体来说还是挺简单的只跑了一趟 1ms beta98%,评论里面大都只用了两个指针我用了三个这样感觉比较清晰 怎么好理解怎么来。貌似最快的是一个递归的,递归写起来确实玄学还要多练练啊
解法二
2020.4.2 重写了一个递归的,感觉良好
public ListNode deleteDuplicates (ListNode head) { if (head==null || head.next==null ) return head; if (head.val==head.next.val){ while (head!=null && head.next!=null && head.val==head.next.val){ head=head.next; } return deleteDuplicates(head.next); } head.next=deleteDuplicates(head.next); return head; }
给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
示例 1:
示例 2:
输入:1 ->1 ->2 ->3 ->3 输出:1 ->2 ->3
解法一
老题新写
public ListNode deleteDuplicates (ListNode head) { if (head==null || head.next==null ) return head; ListNode node=deleteDuplicates(head.next); if (head.val==node.val) { head.next=node.next; } return head; }
解法二
public ListNode deleteDuplicates (ListNode head) { ListNode temp = head; while (temp != null ){ if (temp.next == null ){ break ; } if (temp.next.val == temp.val){ temp.next = temp.next.next; }else { temp = temp.next; } } return head; }
编写代码,移除未排序链表中的重复节点。保留最开始出现的节点。
示例 1:
输入:[1 , 2 , 3 , 3 , 2 , 1 ] 输出:[1 , 2 , 3 ]
示例 2:
输入:[1 , 1 , 1 , 1 , 2 ] 输出:[1 , 2 ]
提示:
链表长度在 [0, 20000] 范围内。
链表元素在 [0, 20000] 范围内。
进阶:
如果不得使用临时缓冲区,该怎么解决?
解法一
无序链表,和前面不太一样,开个 20000 的数组判断是否重复就行了
func removeDuplicateNodes (head *ListNode) *ListNode { var set = make ([]bool ,20001 ) var dummyNode = &ListNode{Next:head} var pre = dummyNode for head!=nil { if !set[head.Val]{ set[head.Val]=true pre=head }else { pre.Next=head.Next } head=head.Next } return dummyNode.Next }
这个进阶或许应该称之为退阶。进阶的应该只能暴力 O(N^2) 而且这个数据量肯定会 T,排序的话并没有合适的排序方法
给定一个单链表 _L_:L0→L1→…→Ln-1→Ln 将其重新排列后变为: L0→Ln→L1→Ln-1→L2→Ln-2→…
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例 1:
给定链表 1->2->3->4, 重新排列为 1->4->2->3.
示例 2:
给定链表 1->2->3->4->5, 重新排列为 1->5->2->4->3.
这题和上面的回文链表有点类似,都是快慢指针不过这题稍微复杂点
解法一
public static void reorderList (ListNode head) { if (head==null ){ return ; } ListNode right = head; ListNode slow = head; while (right.next != null ) { right = right.next.next != null ? right.next.next : right.next; slow = slow.next; } res(slow); ListNode left = head; ListNode rnext = right; ListNode lnext = left; while (right != null && left != null ) { lnext = lnext.next; rnext = rnext.next; if (right.next==null ){ lnext=null ; } left.next = right; right.next = lnext; left = lnext; right = rnext; } } public static void res (ListNode node) { ListNode pre = null ; ListNode cur = node; ListNode nex = node; while (cur != null ) { nex = nex.next; cur.next = pre; pre = cur; cur = nex; } }
也是之前的代码了,写的比较烂,但是思路还是比较清晰的,边界需要注意,速度还行 4ms 77% 。
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入: 1 ->2 ->4 , 1 ->3 ->4 输出: 1 ->1 ->2 ->3 ->4 ->4
解法一
常规迭代的做法
public ListNode mergeTwoLists (ListNode l1, ListNode l2) { ListNode temp=new ListNode (0 ); ListNode res=temp; while (l1!=null &&l2!=null ){ if (l2.val<l1.val){ temp.next=l2; l2=l2.next; temp=temp.next; }else { temp.next=l1; l1=l1.next; temp=temp.next; } } temp.next=l1==null ?l2:l1; return res.next; }
归并分治的思想,期末考试的一道题
解法二
递归的做法
public ListNode mergeTwoLists (ListNode l1, ListNode l2) { ListNode dummyNode=new ListNode (-1 ); mergeTwoLists(l1,l2,dummyNode); return dummyNode.next; } public void mergeTwoLists (ListNode l1, ListNode l2,ListNode res) { if (l1==null ){ res.next=l2; return ; } if (l2==null ){ res.next=l1; return ; } if (l1.val>l2.val){ res.next=l2; mergeTwoLists(l1,l2.next,res.next); }else { res.next=l1; mergeTwoLists(l1.next,l2,res.next); } }
上面是我一开始自己写的,一点也不递归
public ListNode mergeTwoLists (ListNode l1, ListNode l2) { if (l1==null ) return l2; if (l2==null ) return l1; ListNode dummyNode=new ListNode (-1 ); if (l1.val<l2.val){ l1.next=mergeTwoLists(l1.next,l2); return l1; }else { l2.next=mergeTwoLists(l1,l2.next); return l2; } }
这种看着就很简洁
合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。
示例:
输入: [ 1 ->4 ->5 , 1 ->3 ->4 , 2 ->6 ] 输出:1 ->1 ->2 ->3 ->4 ->4 ->5 ->6
解法一:
public ListNode mergeKLists (ListNode[] lists) { if (lists.length==0 )return null ; ListNode temp=lists[0 ]; for (int i=0 ;i<lists.length-1 ;i++){ temp=merge2List(temp,lists[i+1 ]); } return temp; } public static ListNode merge2List (ListNode headA,ListNode headB) { ListNode dummyNode=new ListNode (-1 ); ListNode res=dummyNode; ListNode tempA=headA; ListNode tempB=headB; while (tempB!=null &&tempA!=null ){ if (tempB.val>tempA.val){ res.next=tempA; tempA=tempA.next; } else { res.next=tempB; tempB=tempB.next; } res=res.next; } res.next=tempA==null ?tempB:tempA; return dummyNode.next; }
最先想到的方法,和前面的二路归并一样,把前两个归并的结果和后面的继续归并。速度太慢了 200ms 左右。…. 时间复杂度是O(N^2)
.解法二:
public ListNode mergeKLists2 (ListNode[] lists) { if (lists.length==0 )return null ; return divide(lists,0 ,lists.length-1 ); } public static ListNode divide (ListNode[] lists,int left,int right) { if (left>=right)return lists[left]; int mid=left+((right-left)>>1 ); ListNode l = divide(lists,left,mid); ListNode r = divide(lists,mid+1 ,right); return merge2List(l,r); } public static ListNode merge2List (ListNode headA,ListNode headB) { if (headA==null )return headB; if (headB==null )return headA; ListNode dummyNode=new ListNode (-1 ); ListNode res=dummyNode; ListNode tempA=headA; ListNode tempB=headB; while (tempB!=null &&tempA!=null ){ if (tempB.val>tempA.val){ res.next=tempA; tempA=tempA.next; } else { res.next=tempB; tempB=tempB.next; } res=res.next; } res.next=tempA==null ?tempB:tempA; return dummyNode.next; }
看起来很眼熟?没错就是归并排序的思路,利用分治的思想,先归并左边,再归并右边,然后 merge 左右的结果,时间复杂度为O(NlogK)
(递归树深度为 logK,归并每一层时间复杂度都是 N), 10ms 左右,N 是所有链表的元素个数,K 是链表个数。而且因为是链表空间复杂度也不高。另外这题也可以改成非递归的方式如下:
public ListNode mergeKLists3 (ListNode [] lists) { if (lists.length == 0 ) { return null ; } int k = lists.length; while (k > 1 ) { for (int i = 0 ; i < k / 2 ; i++) { lists[i] = merge2Lists(lists[i], lists[i + (k + 1 ) / 2 ]); } k = (k + 1 ) / 2 ; } return lists[0 ]; }
解法三:
public ListNode mergeKLists4 (ListNode[] lists) { if (lists.length < 1 ) return null ; Queue<ListNode> pq = new PriorityQueue <>((a, b) -> (a.val - b.val)); ListNode head = new ListNode (0 ); ListNode cur = head; for (ListNode p : lists){ if (p != null ) pq.offer(p); } while (!pq.isEmpty()) { cur.next = pq.poll(); cur = cur.next; if (cur.next != null ) pq.offer(cur.next); } return head.next; }
利用了小根堆,Java 里面有小根堆可以直接用,思路就是每次把每条链表的头元素都放进小根堆里面然后找出最小的加到新链表中然后,最小的那个节点的链表向后移再加到小根堆里面,方法还是相当简洁的。但是用了 90ms 左右比较慢,时间复杂度O(NlogK)
和上面是一样的,每次调整时间复杂度都是logK
,需要调整N
次 (K 为链表数量,N 为所有链表的元素个数),空间复杂度是 O(K)
设计一个简化版的推特 (Twitter),可以让用户实现发送推文,关注/取消关注其他用户,能够看见关注人(包括自己)的最近十条推文。你的设计需要支持以下的几个功能:
postTweet(userId, tweetId): 创建一条新的推文
getNewsFeed(userId): 检索最近的十条推文。每个推文都必须是由此用户关注的人或者是用户自己发出的。推文必须按照时间顺序由最近的开始排序。
follow(followerId, followeeId): 关注一个用户
unfollow(followerId, followeeId): 取消关注一个用户
实例
Twitter twitter = new Twitter ();twitter.postTweet(1 , 5 ); twitter.getNewsFeed(1 ); twitter.follow(1 , 2 ); twitter.postTweet(2 , 6 ); twitter.getNewsFeed(1 ); twitter.unfollow(1 , 2 ); twitter.getNewsFeed(1 );
解法一
其实核心就是一个 k 路链表的归并,这里直接用的优先级队列,然后用 java8 的新特性简化了代码
class Twitter { private int timeStamp=0 ; private Map<Integer,Tweet> userTweetMap=new HashMap <>(); private Map<Integer,Set<Integer>> userFollowMap=new HashMap <>();; public Twitter () {} public void postTweet (int userId, int tweetId) { Tweet oldHead=userTweetMap.get(userId); userTweetMap.compute(userId,(k,v)->new Tweet (tweetId,++timeStamp)).next=oldHead; } public List<Integer> getNewsFeed (int userId) { PriorityQueue<Tweet> pq=new PriorityQueue <>((t1,t2)->t2.time-t1.time); List<Integer> feed=new ArrayList <>(); follow(userId,userId); userFollowMap.get(userId).forEach(followerId->Optional.ofNullable(userTweetMap.get(followerId)).ifPresent(tw->pq.offer(tw))); int count=0 ; while (!pq.isEmpty() && count<10 ){ Tweet tw=pq.poll(); feed.add(tw.twId); if (tw.next!=null ){ pq.offer(tw.next); } count++; } return feed; } public void follow (int followerId, int followeeId) { userFollowMap.computeIfAbsent(followerId,k->new HashSet <>()).add(followeeId); } public void unfollow (int followerId, int followeeId) { Optional.ofNullable(userFollowMap.get(followerId)).ifPresent(set->set.remove(followeeId)); } } class Tweet { int twId; int time; Tweet next; public Tweet (int twId,int time) { this .twId=twId; this .time=time; } }
您将获得一个双向链表,除了下一个和前一个指针之外,它还有一个子指针,可能指向单独的双向链表。这些子列表可能有一个或多个自己的子项,依此类推,生成多级数据结构,如下面的示例所示。
扁平化列表,使所有结点出现在单级双链表中。您将获得列表第一级的头部。
示例:
输入: 1—2—3—4—5—6–NULL | 7—8—9—10–NULL | 11–12–NULL
输出: 1-2-3-7-8-11-12-9-10-4-5-6-NULL
说明:
给出以下多级双向链表:
我们应该返回如下所示的扁平双向链表:
解法一:
public static Node flatten1 (Node head) { if (head==null || head.next==null &&head.child==null ){ return head; } Node cur=head; Node nNext; Node child; while (cur!=null ){ if (cur.child!=null ){ child=cur.child; nNext=cur.next; cur.next=child; child.prev=cur; cur.child=null ; if (nNext==null ){ continue ; } while (child.next!=null ){ child=child.next; } child.next=nNext; nNext.prev=child; } cur=cur.next; } return head; }
解法二:
public static Node flatten2 (Node node) { if (node==null ){ return node; } Node head = node; while (head!=null ){ Node next = head.next; if (head.child!=null ){ next = head.next; Node nextLayer = flatten2(head.child); head.next = nextLayer; nextLayer.prev = head; head.child = null ; while (nextLayer.next!=null ){ nextLayer = nextLayer.next; } nextLayer.next = next; if (next!=null ){ next.prev = nextLayer; } } head = next; } return node; }
两种方法都是看的评论里面的第二种我稍微改了下,直接跳过子链表效率会高很多,不过这题 case 也比较少看不出差异。
给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos
是 -1
,则在该链表中没有环。
示例 1:
输入:head = [3 ,2 ,0 ,-4 ], pos = 1 输出:true 解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入: head = [1 ,2 ], pos = 0 输出: true 解释: 链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入: head = [1 ], pos = -1 输出: false 解释: 链表中没有环。
进阶:
你能用 O(1)(即,常量)内存解决此问题吗?
解法一
有一点需要注意的是只有一个节点的情况应该是不考虑的直接 false
public static Boolean hasCycle (ListNode head) { if (head == null || head.next == null ) return false ; ListNode slow=head; ListNode fast=head.next; while (slow!=fast){ if (fast.next==null || fast.next.next==null ){ return false ; } fast=fast.next.next; slow=slow.next; } return true ; }
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
为了表示给定链表中的环,我们使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos
是 -1
,则在该链表中没有环。
说明: 不允许修改给定的链表。 ps: 上题的基础上返回入环的第一个节点
func detectCycle (head *ListNode) *ListNode { var fast, slow = head, head for fast!=nil && fast.Next!=nil { fast = fast.Next.Next slow = slow.Next if fast == nil { return nil } if fast == slow { for head!=fast { head = head.Next fast = fast.Next } return head } } return nil }
这种解法还是挺有意思的快慢指针
,快指针一次走两步慢指针一次走一步在环上相遇的时候快指针回到头节点步数调整为 1,再>次相遇的>时候(这里有可能重合,当头节点就是入环节点的时候)就是入环节点。 原理 : A—->B—->C 分别为头节点
,入环节点
,第一次相遇的节点
分析第一次相遇时快慢指针走过的路径可得 AB+BC+CB+BC=2(AB+BC) 快指针走过的路程肯定是慢指针的两倍 化简最后就得到 AB=CB 所以他们再次相遇
就是入环的节点
给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。
示例 1:
输入:1 ->2 ->3 ->4 ->5 ->NULL, k = 2 输出:4 ->5 ->1 ->2 ->3 ->NULL 解释: 向右旋转 1 步:5 ->1 ->2 ->3 ->4 ->NULL 向右旋转 2 步:4 ->5 ->1 ->2 ->3 ->NULL
示例 2:
输入:0 ->1 ->2 ->NULL, k = 4 输出:2 ->0 ->1 ->NULL 解释: 向右旋转 1 步:2 ->0 ->1 ->NULL 向右旋转 2 步:1 ->2 ->0 ->NULL 向右旋转 3 步:0 ->1 ->2 ->NULL 向右旋转 4 步:2 ->0 ->1 ->NULL
解法一
public static ListNode rotateRight (ListNode head, int k) { if (head==null ||head.next==null ){ return head; } ListNode temp=head; ListNode tail=head; int length=0 ; while (temp!=null ){ length++; if (temp.next==null ){ tail=temp; break ; } temp=temp.next; } k=k%length; if (k==0 ) return head; temp=head; int count=0 ; while (temp!=null ){ if (count==(length-k-1 )){ tail.next=head; head=temp.next; temp.next=null ; return head; } count++; temp=temp.next; } return head; }
虽然难度是 mid,但是感觉这题还是比较简单,我看见有一种比较好点的方法是在第一遍循环完之后将链表转换为双向链表
然后再移动还是比较有意思的
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。
示例 1:
输入:1->2->3->4->5->NULL
输出:1->3->5->2->4->NULL
示例 2:
输入:2->1->3->5->6->4->7->NULL
输出:2->3->6->7->1->5->4->NULL
说明:
应当保持奇数节点和偶数节点的相对顺序。 链表的第一个节点视为奇数节点,第二个节点视为偶数节点,以此类推。
解法一
public static ListNode oddEvenList (ListNode head) { if (head==null ||head.next==null ||head.next.next==null )return head; ListNode pOdd=head; ListNode pEven=head.next; ListNode temp=pEven; while (pEven!=null &&pEven.next!=null ){ pOdd.next=pEven.next; pOdd=pOdd.next; pEven.next=pOdd.next; pEven=pEven.next; } pOdd.next=temp; return head; }
很像踩石头过河的游戏,一道很简单的 mid,不知道为啥一开始抠了半天的边界。果然
还是不熟悉啊。Add oil ! ! !
插入排序的动画演示如上篇文章。 每次迭代时,从输入数据中移除一个元素(用红色表示),并原地将其插入到已排好序的链表中。 插入排序算法: 插入排序是迭代的,每次只移动一个元素,直到所有元素可以形成一个有序的输出列表。 每次迭代中,插入排序只从输入数据中移除一个待排序的元素,找到它在序列中适当的位置,并将其插入。 重复直到所有输入数据插入完为止。
示例 1:
输入:4 ->2 ->1 ->3 输出:1 ->2 ->3 ->4
示例 2:
输入:-1 ->5 ->3 ->4 ->0 输出:-1 ->0 ->3 ->4 ->5
解法一
public static ListNode insertionSortList (ListNode head) { if (head==null ||head.next==null )return head; ListNode dummyNode=new ListNode (Integer.MIN_VALUE); System.out.println(dummyNode.val); dummyNode.next=head; ListNode tempNode=head; ListNode loopVariable=head.next; ListNode loopPre=head; ListNode tempPre=dummyNode; while (loopVariable!=null ){ for (tempNode=dummyNode;tempNode!=loopVariable;tempNode=tempNode.next){ if (tempNode.val>loopVariable.val){ loopPre.next=loopVariable.next; loopVariable.next=tempNode; tempPre.next=loopVariable; loopVariable=loopPre; break ; } tempPre=tempNode; } loopPre=loopVariable; loopVariable=loopVariable.next; } return dummyNode.next; }
上面的是我开始自己写的,但是提交后发现速度有点慢 40ms 50%左右 然后我有点不信把比较靠前的拷了一个 10ms😂前几名 10ms 以内的都是用的方法不是插入。… 在研究别人 10ms 的代码时突然意识到了问题所在 我在进行插入的时候没有判断就时没有关心是不是应该进行插入操作,对于数组的插入排序是不用关心这个问题的,因为是反向遍历的 而这里是链表只能正向的遍历如果不判断就会浪费很多时间
public static ListNode insertionSortList (ListNode head) { if (head==null ||head.next==null )return head; ListNode dummyNode=new ListNode (Integer.MIN_VALUE); System.out.println(dummyNode.val); dummyNode.next=head; ListNode tempNode=head; ListNode loopVariable=head.next; ListNode loopPre=head; ListNode tempPre=dummyNode; while (loopVariable!=null ){ if (loopVariable.val<loopPre.val){ for (tempNode=dummyNode;tempNode!=loopVariable;tempNode=tempNode.next){ if (tempNode.val>loopVariable.val){ loopPre.next=loopVariable.next; loopVariable.next=tempNode; tempPre.next=loopVariable; loopVariable=loopPre; break ; } tempPre=tempNode; } } loopPre=loopVariable; loopVariable=loopVariable.next; } return dummyNode.next; }
这题整体思路就是按照插入排序的思路来的,值得注意的就是链表只能正向遍历,而且需要考虑保存的节点有两个,插入位置的前一个,以及待插入的前一个(头插法)。
在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。
示例 1:
输入:4->2->1->3 输出:1->2->3->4 示例 2:
输入:-1->5->3->4->0 输出:-1->0->3->4->5
上面那题时间复杂度明显是 O(n2) 最坏,这题要求是 O(nlogn) 和常数空间上面的插入肯定不适合了
解法一
public static ListNode sortList (ListNode head) { if (head==null ||head.next==null )return null ; return mergeSort(head); } public static ListNode mergeSort (ListNode head) { if (head.next==null ){ return head; } ListNode fast=head; ListNode slow=head; ListNode pre=head; while (fast!=null &&fast.next!=null ){ pre=slow; fast=fast.next.next; slow=slow.next; } pre.next=null ; ListNode left = mergeSort(head); ListNode right = mergeSort(slow); return merge2list(left,right); } public static ListNode merge2list (ListNode headA,ListNode headB) { if (headA==null )return headB; if (headB==null )return headA; ListNode dummyNode=new ListNode (-1 ); dummyNode.next=headA; ListNode temp=dummyNode; while (headA!=null &&headB!=null ){ if (headA.val>headB.val){ temp.next=headB; headB=headB.next; } else { temp.next=headA; headA=headA.next; } temp=temp.next; } temp.next=headA==null ?headB:headA; return dummyNode.next; }
4ms 85% 标准的归并操作
分治的思想,但是我还是扣了好长时间,最后还是看了别人的代码才知道,其实一开始就想到了快慢指针找中点
但是感觉时间复杂度可能会变得更高就没那样做。还是太菜了时间复杂度都不会分析。这里有一个小地方就是找到中点之后要记得断开中点和后面链表的连接,这样会方便后面归并,不然就需要传递一个边界的指针那样又会有很多问题(没错我开始就是这么做的😭)
下面这种是后来又写的经典快排
,400ms,12% 我都怀疑我到底写了个啥?后来把插入拿来试了下 884+ms 然后又看了一遍才相信我写的是快排。
public static ListNode sortList4 (ListNode head) { if (head==null ||head.next==null )return head; sortList(head,null ); return head; } public static void sortList (ListNode head,ListNode tail) { if (tail==head){ return ; } ListNode base=partion(head,tail); sortList(head,base); sortList(base.next,tail); } public static ListNode partion (ListNode head,ListNode tail) { ListNode base=head; ListNode fast=head.next; ListNode slow=head; while (fast!=tail){ if (fast.val<=base.val){ swap(fast,slow.next); slow=slow.next; } fast=fast.next; } swap(head,slow); return slow; } public static void swap (ListNode a,ListNode b) { int temp=a.val; a.val=b.val; b.val=temp; }
实际上快排确实不适合链表(下面光速打脸)因为毕竟不是数组可以从两边开始遍历,链表每次都需要遍历整个链表才能划分好基准位置。
看了下前几的代码发现了这个,非标准的三向切分的快排
,为啥说是非标准呢?看下面代码就知道了,我给加了注释
public static ListNode sortList3 (ListNode head) { ListNode node = new ListNode (0 ); node.next = head; sort(node, null ); return node.next; } private static void sort (ListNode from, ListNode to) { if (from == null || from == to || from.next == to || from.next.next == to)return ; int v = from.next.val; ListNode mid = from; ListNode equal = from.next; ListNode node = from.next; while (node.next != to) { if (node.next.val < v) { ListNode currentNext = node.next.next; ListNode midNext = mid.next; mid.next = node.next; node.next.next = midNext; node.next = currentNext; mid = mid.next; } else if (node.next.val == v) { if (equal == node) { equal = node.next; node = node.next; } else { ListNode nodeNext = node.next.next; ListNode equalNext = equal.next; equal.next = node.next; node.next.next = equalNext; node.next = nodeNext; equal = equal.next; } } else { node = node.next; } } sort(from, mid.next); sort(equal, to); }
整体思路就是一共有三个指针,mid
(切分点) equal
(等于区) node
(遍历指针) node 从 from 遍历到 to,注意这里是左开右开区间
就是说头 from 和尾 to 都取不到,然后将小与 base 的节点插入到 mid 的后面,然后 mid 后移,等于区插入到 equal 的后面,最后形成的就是[mid.next---equal]
为等于 val 的节点,然后对子区域递归就 ok 了,这个用时 4ms
。还要继续加油啊!!!
给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。
要求返回这个链表的深拷贝。
示例:
输入: `{"$id" :"1" ,"next" :{"$id" :"2" ,"next" :null,"random" :{"$ref" :"2" },"val" :2 },"random" :{"$ref" :"2" },"val" :1 }` 解释: 节点 1 的值是 1 ,它的下一个指针和随机指针都指向节点 2 。 节点 2 的值是 2 ,它的下一个指针指向 null,随机指针指向它自己。
提示:
你必须返回给定头的拷贝作为对克隆列表的引用。
解法一
利用 Map 保存原链表和复制链表的对应关系
public static Node copyRandomList (Node head) { Map<Node,Node> copNode =new HashMap <>(); Node temp=head; while (temp!=null ){ copNode.put(temp,new Node (temp.val,null ,null )); temp=temp.next; } temp=head; while (temp!=null ){ copNode.get(temp).next=copNode.get(temp.next); copNode.get(temp).random=copNode.get(temp.random); temp=temp.next; } return copNode.get(head); }
第一个循环利用 Map 将原链表和拷贝链表形成对应关系,第二个循环就是直接给拷贝链表的 next 域和 random 域赋值。
解法二
奥义 影分身
public static Node copyRandomList2 (Node head) { if (head==null )return head; Node temp=head; while (temp!=null ){ temp.next=new Node (temp.val,temp.next,null ); temp=temp.next.next; } temp=head; while (temp!=null ){ if (temp.random!=null ){ temp.next.random=temp.random.next; } temp=temp.next.next; } temp=head; Node newHead=head.next; Node next=null ; while (temp!=null ){ next=temp.next; if (next!=null ){ temp.next=next.next; } temp=next; } return newHead; }
为啥要叫影分身?因为帅。.. 这种方法比上面的要快一点可能是创建 hashMap 比较耗时间,其实分析这两种方法其实都是先把链表拷贝了一份,然后通过对应关系来连接拷贝链表的 next 和 random 域,map 是通过键值对的方式对应拷贝链表,这样可以方便的通过原链表找到拷贝链表的 random. 然后上面这种方法也是一样,在原链表每个节点后面 copy 一个节点,然后根据前一个节点的 random 来找拷贝节点的 random(前一个节点的 random 的 next) 主要就是找到一个对应关系。
tips: 这题 OJ 上的 0ms 是有问题的,这题本意肯定也不是这个
最开始能通过主要是 OJ 后台只判断了 val 的值,可以看出现在题目已经改了。现在肯定是跑不过的,可能是判断了 random 是不是 new 出来的(我试了下看了下返回这个) 输入{"$id":"1","next":{"$id":"2","next":null,"random":{"$ref":"2"},"val":2},"random":{"$ref":"2"},"val":1}
输出{"$id":"1","next":{"$id":"2","next":null,"random":
{"$id":"3","next":null,"random":null,"val":2},"val":2},
"random":{"$id":"4","next":null,"random":null,"val":2},"val":1}
预期结果{"$id":"1","next":{"$id":"2","next":null,"random":{"$ref":"2"},"val":2},"random":{"$ref":"2"},"val":1}
解法三
重写了一遍,题目改了一点
public Node copyRandomList (Node head) { if (head==null ) return null ; Node temp=head; while (temp!=null ){ Node next=temp.next; Node newNode=new Node (temp.val); temp.next=newNode; newNode.next=next; temp=next; } temp=head; while (temp!=null ){ Node next=temp.next.next; if (temp.random!=null ){ temp.next.random=temp.random.next; } temp=next; } Node newHead=head.next; temp=head; while (temp!=null ){ Node next=temp.next; if (next!=null ){ temp.next=next.next; } temp=next; } return newHead; }
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例:
给定 1->2->3->4, 你应该返回 2->1->4->3.
解法一
public ListNode swapPairs (ListNode head) { if (head==null ||head.next==null )return head; ListNode dummyNode=new ListNode (-1 ); dummyNode.next=head; ListNode nex=head.next; ListNode cur=head; ListNode pre=dummyNode; while (nex!=null ){ pre.next=nex; cur.next=nex.next; nex.next=cur; pre=cur; if (cur.next==null ){ return dummyNode.next; } cur=cur.next; nex=cur.next; } return dummyNode.next; }
其实跟反转链表是一样的,三个指针分别记录前 中 后三个节点然后逆序,只不过步长不一样,这里 step 为 2,一次走两步, 我上面的代码可能写的有些乱,思路还是一样的
解法二
public ListNode swapPairs (ListNode head) { if (head==null ||head.next==null ){ return head; } ListNode next=head.next; head.next=swapPairs(next.next); next.next=head; return next; }
递归是真的简洁,我最开始写反转链表的递归就是这么写的😂
给出一个链表,每 k 个节点一组进行翻转,并返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么将最后剩余节点保持原有顺序。
示例 :
给定这个链表:1 ->2 ->3 ->4 ->5 当 k = 2 时,应当返回:2 ->1 ->4 ->3 ->5 当 k = 3 时,应当返回:3 ->2 ->1 ->4 ->5
说明 :
你的算法只能使用常数的额外空间。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
解法一
public static ListNode reverseKGroup (ListNode head,int k) { if (head==null ||head.next==null )return head; ListNode dummyNode=new ListNode (-1 ); dummyNode.next=head; ListNode pre=dummyNode; ListNode cur=head; ListNode next=head; ListNode temp; while (next!=null ){ temp=cur; int step=k; while (step>0 && next!=null ){ next=next.next; step--; if (next==null && step!=0 ){ return dummyNode.next; } } pre.next=reverse(cur,k); temp.next=next; pre=temp; cur=next; } return dummyNode.next; } public static ListNode reverse (ListNode node,int k) { ListNode pre=null ; ListNode cur=node; ListNode next=node; while (k>0 &&next!=null ){ next=next.next; cur.next=pre; pre=cur; cur=next; k--; } System.out.println("头" +pre.val); return pre; }
7ms 74% 也是我第一道做出来的困难题,上一道困难题超时了
这题虽然说是困难题但是其实也不难,感觉就 mid 左右的水平,确实比上一题要复杂一点,但是只要思路理清楚其实也挺简单的,下面是我用 OneNote 画的一张图片。
4 个指针分别对应 K 链表的 前一个 (pre) K 链表的头节点 (cur) 没有翻转前的 K 链表的头节点 and 翻转后的尾节点 (temp) K 链表的后一个节点 (next)。然后其实就简单了,写一个翻转链表的函数然后返回头节点(也可以多加一个指针指向翻转前的头节点 ),然后就简单了,next 指针一次走 K 步,走到 K+1 位置 同时也是下一次 K 链表的头节点 ,而 temp 则为下一次 K 链表的 pre… 然后循环这个过程就行了,其实写成递归会很简洁,但是我是真的不会写递归,太菜了 Orz
解法二
2020.2.23 时隔多年现在回头重新写了一个递归的写法,还是比较简洁的
public ListNode reverseKGroup (ListNode head, int k) { if (head==null || k==1 ) return head; int sum=0 ; ListNode temp=head; while (temp!=null ){ temp=temp.next; sum++; } return reverse(head,k,sum); } public ListNode reverse (ListNode head, int k,int remain) { if (remain<k) return head; if (head==null ) return head; ListNode cur=head,pre=null ,last=head; int count=k; while (count-- >0 ){ last=cur.next; cur.next=pre; pre=cur; cur=last; } head.next=reverse(last,k,remain-k); return pre; }
解法三
这题递归明显是不太符合要求的,空间复杂度不是常数的,如果有要求还是要写下面的解法
func reverseKGroup (head *ListNode, k int ) *ListNode { dummyNode := &ListNode{ Next: head, Val: -1 , } pre := dummyNode cur := head for cur != nil { for i := 0 ; i < k-1 && cur != nil ; i++ { cur = cur.Next } if cur == nil { break } next := cur.Next cur.Next = nil start := pre.Next pre.Next = reverse(start) start.Next = next pre = start cur = next } return dummyNode.Next } func reverse (head *ListNode) *ListNode { var pre *ListNode var cur = head for cur != nil { next := cur.Next cur.Next = pre pre = cur cur = next } return pre }
隔一段时间就会重新写一遍,写肯定写的出来,就是要想清楚,最好搞个 case 在上面,一边模拟一边写
做链表的题就是得细心啊,容易把自己绕进去,上面的解法就是看了题解才写出来的
给定一个链表(链表结点包含一个整型值)的头结点 head。 同时给定列表 G,该列表是上述链表中整型值的一个子集。 返回列表 G 中组件的个数,这里对组件的定义为:链表中一段最长连续结点的值(该值必须在列表 G 中)构成的集合。
示例 1:
输入: head: 0 ->1 ->2 ->3 G = [0 , 1 , 3 ] 输出:2 解释: 链表中,0 和 1 是相连接的,且 G 中不包含 2 ,所以 [0 , 1 ] 是 G 的一个组件,同理 [3 ] 也是一个组件,故返回 2 。
示例 2:
输入: head: 0 ->1 ->2 ->3 ->4 G = [0 , 3 , 1 , 4 ] 输出:2 解释: 链表中,0 和 1 是相连接的,3 和 4 是相连接的,所以 [0 , 1 ] 和 [3 , 4 ] 是两个组件,故返回 2 。
注意:
如果 N 是给定链表 head 的长度,1 <= N <= 10000。
链表中每个结点的值所在范围为 [0, N - 1]。
1 <= G.length <= 10000
G 是链表中所有结点的值的一个子集。
解法一
public int numComponents (ListNode head, int [] G) { if (head==null ||head.next==null ) return head; ListNode temp=head; int res=0 ; while (temp!=null ){ if (isInG(temp.val,G)){ while (temp!=null &&isInG(temp.val,G)){ temp=temp.next; } res++; if (temp==null ){ return res; } } temp=temp.next; } return res; } public static Boolean isInG (int val,int [] G) { for (int i=0 ;i<G.length;i++){ if (G[i]==val){ return true ; } } return false ; }
首先想到的方法 91ms 19%….. 有点慢了,然后我稍微改了下。
public int numComponents2 (ListNode head, int [] G) { ListNode temp=head; int res=0 ; Boolean [] isInG=new Boolean [10000 ]; int j=0 ; while (temp!=null ){ for (int i=0 ;i<G.length;i++){ if (G[i]==temp.val){ isInG[j]=true ; break ; } } j++; temp=temp.next; } temp=head; for (int i=0 ;temp!=null ;temp=temp.next,i++){ if (isInG[i]){ while (temp!=null &&isInG[i]){ temp=temp.next; i++; } res++; if (temp==null ){ return res; } } } return res; }
用了一个数组保存了每个位置的状态速度跟前面的差不多。主要问题就是那个数组的创建,这种创建方式用连续的下标来对应连续的链表的每个元素,每次都要遍历 G 才知道当前位置是不是在 G 中。
其实可以直接把当前节点的 val 作为数组的下标这样既有了对应关系也不用遍历 G. 可以说是很优秀了,但是实际上这样做是有前提条件的那就是链表中的元素值应该没有负数
,还是题做少了啊 Orz。
public int numComponents3 (ListNode head, int [] G) { ListNode temp=head; int res=0 ; Boolean [] isInG=new Boolean [10000 ]; int j=0 ; for (int i:G){ isInG[i]=true ; } while (temp!=null ){ if (isInG[temp.val]){ while (temp!=null &&isInG[temp.val]){ temp=temp.next; } res++; if (temp==null ){ return res; } } temp = temp.next; } return res; }
给出一个以头节点 head
作为第一个节点的链表。链表中的节点分别编号为:node_1, node_2, node_3, ...
。
每个节点都可能有下一个更大值(next larger value ):对于 node_i
,如果其 next_larger(node_i)
是 node_j.val
,那么就有 j > i
且 node_j.val > node_i.val
,而 j
是可能的选项中最小的那个。如果不存在这样的 j
,那么下一个更大值为 0
。
返回整数答案数组 answer
,其中 answer[i] = next_larger(node_{i+1})
。
注意: 在下面的示例中,诸如 [2,1,5]
这样的输入 (不是输出)是链表的序列化表示,其头节点的值为 2,第二个节点值为 1,第三个节点值为 5 。
示例 1:
示例 2:
输入:[2 ,7 ,4 ,3 ,5 ] 输出:[7 ,0 ,5 ,5 ,0 ]
示例 3:
输入:[1 ,7 ,5 ,1 ,9 ,2 ,5 ,1 ] 输出:[7 ,9 ,9 ,9 ,0 ,5 ,0 ,0 ]
提示:
对于链表中的每个节点,1 <= node.val <= 10^9
给定列表的长度在 [0, 10000]
范围内
解法一
public static int [] nextLargerNodes(ListNode head) { ArrayList<Integer> A = new ArrayList <>(); for (ListNode node = head; node != null ; node = node.next) A.add(node.val); int [] res = new int [A.size()]; Stack<Integer> stack = new Stack <>(); for (int i = 0 ; i < A.size(); ++i) { while (!stack.isEmpty() && A.get(stack.peek()) < A.get(i)) res[stack.pop()] = A.get(i); stack.push(i); } return res; }
新题,磨了好长时间,没做出来。真是菜啊 Orz,70ms,因为跑两遍。下面这个14ms
可以说是相当快了,但,时间可能耗费在建立栈和 list 上了,看了下提交上的前几个都是用的数组,用数组模拟的栈。
public static int [] nextLargerNodes3(ListNode head) { int [] stack = new int [10000 ]; int [] res = new int [10000 ]; int [] temp = new int [10000 ]; int top = -1 , i = 0 ; ListNode node = head; while (node != null ) { while (top != -1 && temp[stack[top]] < node.val){ res[stack[top--]] = node.val; } stack[++top] = i; temp[i++] = node.val; node = node.next; } return Arrays.copyOf(res, i); }
思路就是利用单调栈
,栈里面存的索引对应的元素都是单调递减的,遇到不递减的就会一直 pop() 直到再次单调递减。这样很容易就找到了每个元素的下一个最大元素了。
19.7.21 重新做了一遍这道题,第一遍还是没想出来,还是看了之前的代码
public static int [] nextLargerNodes5(ListNode head) { List<Integer> list=new ArrayList <>(); ListNode temp=head; while (temp!=null ){ list.add(temp.val); temp=temp.next; } int [] stack=new int [10000 ]; int stackIndex=-1 ; int [] res=new int [10000 ]; for (int i=0 ;i<list.size();i++) { while (stackIndex!=-1 && list.get(i)>list.get(stack[stackIndex])){ res[stack[stackIndex--]]=list.get(i); } stack[++stackIndex]=i; } return Arrays.copyOf(res, list.size()); }
相比上面的方法一,采用了数组模拟队列(数据范围已经给定了),30ms,80% 。仔细看看代码发现其实第一个循环完全没有必要,可以一边遍历一边存进去。
一次遍历
public static int [] nextLargerNodes6(ListNode head) { List<Integer> list=new ArrayList <>(); ListNode temp=head; int [] stack=new int [10000 ]; int stackIndex=-1 ; int [] res=new int [10000 ]; for (int i=0 ;temp!=null ;i++) { list.add(temp.val); while (stackIndex!=-1 && list.get(i)>list.get(stack[stackIndex])){ res[stack[stackIndex--]]=list.get(i); } stack[++stackIndex]=i; temp=temp.next; } return Arrays.copyOf(res, list.size()); }
优化后发现比之前还慢了。
数组模拟链表
public static int [] nextLargerNodes7(ListNode head) { int [] list=new int [10000 ]; ListNode temp=head; int [] stack=new int [10000 ]; int stackIndex=-1 ; int [] res=new int [10000 ]; int i; for ( i=0 ;temp!=null ;i++) { list[i]=temp.val; while (stackIndex!=-1 && list[i]>list[stack[stackIndex]]){ res[stack[stackIndex--]]=list[i]; } stack[++stackIndex]=i; temp=temp.next; } return Arrays.copyOf(res, i); }
这次提交了几次直接 8ms 100%了。