【发布时间】:2011-11-30 13:25:13
【问题描述】:
考虑一个函数minout :: [Int] -> Int,它接受一个不同的非负整数列表,并返回列表中不存在的最小非负整数。如果输入具有重复项,则函数的行为无关紧要。这可以在线性时间内实现吗,只使用列表(没有数组或向量或其他具有高效随机访问的数据结构)?
(这是here。)
【问题讨论】:
考虑一个函数minout :: [Int] -> Int,它接受一个不同的非负整数列表,并返回列表中不存在的最小非负整数。如果输入具有重复项,则函数的行为无关紧要。这可以在线性时间内实现吗,只使用列表(没有数组或向量或其他具有高效随机访问的数据结构)?
(这是here。)
【问题讨论】:
如果l 包含0 和(length l) - 1 之间的所有数字,则minout l 是length l,否则,它位于[0..(length l - 1)]。所以minout l 总是存在于[0..(length l)] 中,只有l 的元素在[0..(length l - 1)] 中是相关的。我们可以丢弃剩余的元素。使用这个想法,我们可以实现一个线性时间的分治法。与归并排序不同,在递归的每一步,我们只递归到两个子列表中的 一个,每个子列表的大小最多为原始大小的一半(在做一些线性工作之后)。这给出了线性时间复杂度。
minout :: [Int] -> Int
minout = minoutaux 0
where
minoutaux :: Int -> [Int] -> Int -- \list base -> smallest integer >= base not occuring in list
minoutaux base [] = base
minoutaux base [x] = base + (if x==base then 1 else 0)
minoutaux base xs = if (length smallpart == n2) then minoutaux (base+n2) bigpart else minoutaux base smallpart
where
n = (length xs)
n2 = n `div` 2
smallpart = [x | x <- xs , base <= x , x < base + n2]
bigpart = [x | x <- xs, base + n2 <= x, x < base + n]
在上面的代码中,minoutaux 是一个函数,它给定一个“基”整数和一个具有不同条目的列表,返回至少是基数且不在列表中的最小整数。为此,它会丢弃可以丢弃的“不相关”元素,如前所述,并生成两个列表,由位于 [base、base + n2)(称为smallpart)和 [ base + n2、base + n)(称为bigpart)。这些列表中的每一个的长度最多为n2。如果length smallpart == n2,那么smallpart的所有数字都在[base,base + n2)中,所以答案一定在bigpart中,否则smallpart本身就有一个“缺口”,所以答案在于smallpart。
为什么这会在线性时间内运行?首先,遍历长度为 N 的整个列表几次,比方说,这需要大约 10N 次操作。然后minoutaux 在一个较小的列表上被调用,最大大小为 N/2。所以我们(最多)有 10N/2 更多的操作。然后是 10N/4、10N/8 等等。将所有这些相加,我们得到 10(2N) = 20N 的界限。 (常数 10 只是作为例子)
这里我们多次遍历列表来计算它的长度,计算smallpart,计算bigpart,等等。通过一次完成所有这些,可以相当容易地优化它。然而这仍然是一个线性时间解决方案,我想让代码更清晰,而不是在常数因素上进行优化。
这个问题和解决方案不是我的原创;我在学习 Haskell 时在课堂上遇到过。
【讨论】:
length、smallpart 和bigpart 中遍历。 smallpart 和 bigpart 中的遍历由于惰性可能是线性的,但是 length 呢?
Richard Bird 在他的书 Pearls of Functional Algorithm Design 的第 1 章中描述了这个确切的问题。 (那一章恰好是亚马逊上的预览版。)
【讨论】: