算法笔记(0)

来源:互联网 发布:手机淘宝5.9.0版本 编辑:程序博客网 时间:2024/06/05 04:44

    最近入手一本《算法新解(刘新宇 ◎ 著)》,单单看完前言小例子就让人大呼过瘾,将算法讲的很透彻。趁着最近闲暇之余,留个笔记,记录一下学习进程。

本文算法描述及代码实现来自《算法新解(刘新宇 ◎ 著)》,感谢作者


  这是书中的第一个算法小例子:

“求最小可用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)

search(A, l, u)

    其中: m = [n/2] + 1

    A’ = { x | ∀x∈A & x ≦ m}

    A” = { x | ∀x∈A & x > m}

函数式语言Haskell实现如下:

import Data.ListminFree 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;}

前言第一个小例子完毕,还需继续加油~

原创粉丝点击