面试记录之合并K个有序链表

146 阅读6分钟

铺垫

想象一下,这次你是一名待业在家的苦逼的乏的程序员,你的前司把你炒了,你的头发还是秃的,我们还是管你叫“秃头大帅”(whatever, who cares)。

这一天,太阳落得有点儿早,17点多的时间,天气已经微微泛黄,你扫了一辆贴着“约吗”二维码的共享单车,你要去面试,到一家叫变态跳舞的公司。

0A14936E-93EE-4C32-B689-EBFEABB8F722.png

你赶到面试地点的时候,面试官已经早早等着你,并给你准备了一杯他们公司从长白山专门订制的有点儿甜的矿泉水。你心里默默想,这家伙真TM专业又周到,真好,你暗暗较劲一定好好面,要跟他成为同事。

Round One(两个有序链表)

面试官说,先来个开胃菜吧,缓解缓解气氛,最后不成也能叫个朋友。

你说,好的。

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 

你想了一下,这还不简单,于是奋笔疾书,记下五除二就写完了答案。并解释道:

  1. 维护head、tail来表示结果链表的头尾,这里可以哨兵节点的技巧统一首节点和中间节点插入
  2. 同时遍历两个链表,谁小就把谁的节点添加到结果里并后移,直到有一个链表到尾部
  3. 最后不要忘了两个链表中可能有一个尚未到达结尾,就把这个链表当前节点连在结果链表尾部

时间复杂度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)的过程,所以你还给出了另一种递归写法:

  1. 结束条件是链表为空,返回另一个链表
  2. 如果当前节点小,递归合并当前节点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

image.png

面试官听了听,并不在意,看起来一切都在他的掌握之中。他缓缓的问,有没有更高效的算法。

你挠了挠你仅剩的几根乌黑的发丝,不太确定地说是不是可以分而治之,两两合并,直到合并成一个链表。

面试官并没有表情,你不知道该说什么好,于是补充道我写一下试试,说着便奋然提笔。

面试官这时候打断说,等一下,你先按思路分析一下是不是更高效。

你说,好的。

分支合并

你一边分析一边伪代码说明着

第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还不行?你抓耳挠腮假装思索了一番,说想不出。

面试官这时候面带笑意,好像这就是他要的结果,他说好的,那今天先这样,咱们后面再聊。

A10B1B04-4CCE-4575-ABB0-A38FFFEC80EC.png