铺垫
想象一下,这次你是一名待业在家的苦逼的乏的程序员,你的前司把你炒了,你的头发还是秃的,我们还是管你叫“秃头大帅”(whatever, who cares)。
这一天,太阳落得有点儿早,17点多的时间,天气已经微微泛黄,你扫了一辆贴着“约吗”二维码的共享单车,你要去面试,到一家叫变态跳舞的公司。
你赶到面试地点的时候,面试官已经早早等着你,并给你准备了一杯他们公司从长白山专门订制的有点儿甜的矿泉水。你心里默默想,这家伙真TM专业又周到,真好,你暗暗较劲一定好好面,要跟他成为同事。
Round One(两个有序链表)
面试官说,先来个开胃菜吧,缓解缓解气氛,最后不成也能叫个朋友。
你说,好的。
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
你想了一下,这还不简单,于是奋笔疾书,记下五除二就写完了答案。并解释道:
- 维护head、tail来表示结果链表的头尾,这里可以哨兵节点的技巧统一首节点和中间节点插入
- 同时遍历两个链表,谁小就把谁的节点添加到结果里并后移,直到有一个链表到尾部
- 最后不要忘了两个链表中可能有一个尚未到达结尾,就把这个链表当前节点连在结果链表尾部
时间复杂度O(M+N),M、N分别为两个链表的长度。
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func mergeTwoLists(list1 *ListNode, list2 *ListNode) *ListNode {
head := &ListNode{Next: nil} // dummy head
tail := head
// 谁小就右移谁
for list1 != nil && list2 != nil {
if list1.Val < list2.Val {
tail.Next = list1
list1 = list1.Next
} else {
tail.Next = list2
list2 = list2.Next
}
tail = tail.Next
}
// 判断谁还有剩余
if list1 != nil {
tail.Next = list1
}
if list2 != nil {
tail.Next = list2
}
return head.Next
}
随后你说这个合并过程本质上是l1.next=merge(l1.next, l2)的过程,所以你还给出了另一种递归写法:
- 结束条件是链表为空,返回另一个链表
- 如果当前节点小,递归合并当前节点next指向的链表和另一个链表
时间复杂度O(M+N),M、N分别为两个链表的长度。递归函数每次去掉一个元素,直到两个链表都为空,因此需要调用 R=O(M+N) 次。而在递归函数中我们只进行了 next 指针的赋值操作,复杂度为
O(1),故递归的总时间复杂度为 O(T)=R∗O(1)=O(M+N)。
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func mergeTwoLists(list1 *ListNode, list2 *ListNode) *ListNode {
// 结束条件链表到尾,返回另一个链表
if list1 == nil {
return list2
}
if list2 == nil {
return list1
}
// 递归合并l1.next链表和l2
if list1.Val <= list2.Val {
list1.Next = mergeTwoLists(list1.Next, list2)
return list1
} else { // 递归合并l2.next链表和l1
list2.Next = mergeTwoLists(list1, list2.Next)
return list2
}
}
Round Two(k个有序链表)
面试官听着你的解释,看了看你写的代码,眼神里微微一笑,你不知道这个笑是什么意思,只是顿感不妙。接着你收到了这样一道题:
给你一个链表数组(k个),每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
你思考了一下,又想了半天,然后支支吾吾说,可以利用上面两个合并的代码,第1个跟第2个合并,之后再跟第3个合并,这样一直到第k个。
循环合并
面试官说,写一下,于是你又抬起手,唰唰唰敲了起来
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func mergeKLists(lists []*ListNode) *ListNode {
if len(lists) < 1 {
return nil
}
var res *ListNode = lists[0]
for i := 1; i < len(lists); i++ { // 逐个合并
res = mergeTwoLists(res, lists[i])
}
return res
}
你解释道,如果每个链表平均长度为N。
那合并第2个链表后res长度为2N,合并第k个时候长度为(k-1)N;
所以总共代价为:2N+3N+...+(k-1)N=(k-2)(k+1)N/2
面试官听了听,并不在意,看起来一切都在他的掌握之中。他缓缓的问,有没有更高效的算法。
你挠了挠你仅剩的几根乌黑的发丝,不太确定地说是不是可以分而治之,两两合并,直到合并成一个链表。
面试官并没有表情,你不知道该说什么好,于是补充道我写一下试试,说着便奋然提笔。
面试官这时候打断说,等一下,你先按思路分析一下是不是更高效。
你说,好的。
分支合并
你一边分析一边伪代码说明着
第1轮大合并,两两合并k/2次,每次2N,复杂度O(k/2*2N)=O(kN);
第2轮大合并,两两合并k/4次,每次4N,复杂度O(k/2*2N)=O(kN);
第3轮大合并,两两合并k/8次,每次8N,复杂度O(k/2*2N)=O(kN);
共logk轮,每轮kN,所以总的时间代价为knlogk,即时间复杂度为O(knlogk)
func mergeKLists(lists []*ListNode) *ListNode {
m := len(lists)
if m == 0 { // 输入的 lists 可能是空的
return nil
}
if m == 1 { // 无需合并,直接返回
return lists[0]
}
left := mergeKLists(lists[:m/2]) // 合并左半部分
right := mergeKLists(lists[m/2:]) // 合并右半部分
return mergeTwoLists(left, right) // 最后把左半和右半合并
}
面试官看着你说得口水飞溅,却依然不冷不淡,用一种极其机器似的口吻继续问道,还有其它方法吗?
你瞬间紧张起来,你知道来者不善,你望着键盘上被你无情薅掉的头发,顾不上心疼。不停运转着大脑,你搜索大脑里存储的有关于算法方面的所有扇区,只是好像什么也加载不到,你只感觉到大脑一片空旷,嗡嗡声也随之而来,你已经不知道如何思考。
优先级队列
这时候面试官面漏笑意,说是不是可以多指针的方式指向k个链表头部,谁小合并谁,然后指针后移。
你想了一想,顺路拍了个马屁,说妙啊,于是写了起来,难点就在于用什么结构创建k个指针,你思考了一下使用了优先级队列。
func mergeKLists(lists []*ListNode) *ListNode {
q := make(PriorityQueue, 0)
heap.Init(&q)
for _, node := range lists {
if node != nil {
heap.Push(&q, Status{node.Val, node})
}
}
head := &ListNode{}
tail := head
for len(q) > 0 {
f := heap.Pop(&q).(Status) // 最小的出队
tail.Next = f.Ptr
tail = tail.Next
if f.Ptr.Next != nil { // 后移
heap.Push(&q, Status{f.Ptr.Next.Val, f.Ptr.Next})
}
}
return head.Next
}
这种方式,需要循环kn次,每次后移(插入队列)复杂度为logk,所以总时间复杂度O(kNlogk)
还有吗?
面试官瞥了一眼你的代码,听着你自以为头头是道,收起表情,说到这也没比分治高效啊,有没有更高效的方法?
你优点茫然,有点懵,这TM还不行?你抓耳挠腮假装思索了一番,说想不出。
面试官这时候面带笑意,好像这就是他要的结果,他说好的,那今天先这样,咱们后面再聊。