算法笔记(0)

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

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


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

“求最小可用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.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;
}

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

发布了2 篇原创文章 · 获赞 1 · 访问量 5040
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览