最近入手一本《算法新解(刘新宇 ◎ 著)》,单单看完前言小例子就让人大呼过瘾,将算法讲的很透彻。趁着最近闲暇之余,留个笔记,记录一下学习进程。
本文算法描述及代码实现来自《算法新解(刘新宇 ◎ 著)》,感谢作者
这是书中的第一个算法小例子:
“求最小可用ID”
找到最小可分配的ID,例如:当前已使用ID:
[18, 4, 8, 9, 16, 1, 14, 7, 19, 3, 0, 5, 2, 11, 6];
求不在该列表中的最小非负整数, 即 10 .
由于数据较少,只有15个,直接数就可以数出来答案为 10 。
乍一看似乎不难,直接从0到最大值遍历一遍,看当前值是否已经使用,算法描述如下:
function Min-Free(A)
x ← 0
loop
if x ∉ A then
return x
else
x ← x + 1
其中 ∉ 符号的实现如下:
function '∉' (x, X)
for i ←1 to |X| do
if x == X[i] then
return False
return True
但是对于长度为n的ID列表,该算法的时间复杂度为O(n²),当n的值为10万、100万时,这个算法的性能就不敢恭维了
改进一
改进这一解法的关键基于这一事实:对于任何n个非负整数x1,x2,···,xn ,如果存在小于n的可用整数,必然存在某个xi不在[0,n)范围内。否则这些整数一定是 0,1,···,n-1的某个排列,在这种情况下,最小可用非负整数为n。于是有如下结论:
minfree(x1,x2,···,xn) <= n
根据这一结论,我们可以使用一个长度为n+1的数组,来标记区间[0, n]内的某个整数是否可用:
function Min-Free(A)
F ← [False, False, ···,False] where |F| = n + 1
for ∀x ∈ A do
if x < n then
F[x] ← True
for i ← 0 to n do
if F[i] = False
return i
其中步骤2将标志数组中所有值初始化为False, 需要O(n)时间,接着步骤6遍历A中的所有元素,只要小于n就将对应标志位置为True, 这一步也需要O(n)时间,故整个算法是线性时间O(0)。
以下为C语言实现代码
#include<stdio.h>
#define N 1000000 //100万
#define WORD_LENGTH sizeof(int) * 8
void setbit(unsigned int *bits, unsigned int i) {
bits[i / WORD_LENGTH] |= 1<<(i % WORD_LENGTH);
}
int testbit(unsigned int *bits, unsigned int i) {
return bits[i / WORD_LENGTH] & (1 << (i % WORD_LENGTH));
}
unsigned int bits[N / WORD_LENGTH + 1];
int min_free(int *xs, int n) {
int i, len =N / WORD_LENGTH +1;
for(i = 0; i < len; i++) {
bits[i] = 0;
}
for(i = 0; i < n; i++) {
if(xs[i] < n)
setbit(bits, xs[i]);
}
for(i = 0; i <= n; i++) {
if(!testbit(bits, i))
return i;
}
}
int main() {
int test[15] = {18, 4, 8, 9, 16, 1, 14, 7, 19, 3, 0, 5, 2, 11, 6};
printf("%d", min_free(test, 15));
}
在上面min_free()函数中,最后一个for循环可以进一步优化,从数组第一个int开始,以int为单位(每次检查32个位)检查该int的比特位是否全为1,若不等于0xffffffff,则说明最小的可分配ID在该int范围内,再遍历该int值得32个比特位。如下:
if((!bits[i]) != 0){
for(j = 0; ; ++j)
if(!testbit(bits, i*WORD_LENGTH + j))
return i*WORD_LENGTH + j;
}
以上在我看来O(n)已经是最为快速的算法了,然而大神还有另一种方法,那就是分治策略。
改进二
在改进一种以空间的消耗为代价做了速度上的改进,由于维护一个长度为n+1的标志数组,当n很大时,空间上的性能就成了新的瓶颈。
分而治之的典型策略是将问题分解为若干规模较小的子问题,然后逐步解决它们以得到最终的结果。
思路:
将所有满足xi ≦ [n/2] 的整数放入子序列A’,并将剩余的整数放入另一个序列A’中,如果| A’| == [n/2], 说明前一半整数已满,为 0 ~ [n/2] 的一个排列,故最小可分配整数一定可以在A”中递归找到;否则可以在A’中递归找到。通过如此划分,问题的规模减小了。
需要注意的是在A”中递归查找时,边界情况发生了一些变化:不在是从0开始寻找,寻找的下界为[n/2]+1,因此算法定义为minfree(A,l,u),其中l为下界,u为上界。
minfree(A) = search(A,0,|A|-1)
其中: m = [n/2] + 1
A’ = { x | ∀x∈A & x ≦ m}
A” = { x | ∀x∈A & x > m}
函数式语言Haskell实现如下:
import Data.List
minFree xs = bsearch xs 0 (length xs - 1)
bsearch xs l u | xs == [] = l
| length as == m - l + 1 = bsearch bs (m + 1) u
| otherwise = bsearch as l m
where
m = (l + u) `div` 2
(as, bs) = partitiopn (<= m) xs
哈哈,是不是很神奇,跟天书似得,然而这就是函数式语言,和我们以前学过的C++、Java等都截然不同,有兴趣的可以推敲推敲,我也是特地找了些Haskell的资料才看懂上面的代码,这种迥异于命令式语言的函数式语言就像另一种思维方式,别有一番风趣。
别急,下面贴出将递归转换为迭代的C语言代码:
int min_free(int *xs, int n) {
int l = 0;
int u = n - 1;
while(n) {
int m = (l + u) / 2;
int right, left = 0;
//将小于m的值放入left左部
for(right = 0; right < n; ++ right)
if(xs[right] <= m) {
swap(xs[left], xs[right]);
++left
}
if(left == m - l + 1) { //前半个数组满
xs = xs + left; //寻找的下界变为 [n/2] + 1
n = n - left;
l = m + 1;
}
else { //寻找的值在left左部
n = left;
u = m;
}
}
return l;
}