几个时间复杂度O(logN)的算法

1 二分查找算法

int BinarySearch(const ElementType A[], ElementType X, int N)
{
    int mid, right, left;
    right = 0;
    left = N - 1;

    while(right <= left){           // 不断更新左右边界的索引(实质是每次循环将待查元素减半,实现了O(logN)时间复杂度),直到左索引大于右索引
        mid = (right + left)/2;
        if(A[mid] > X)
            left = mid - 1;
        else if(A[mid] < X)
            right = mid + 1;
        else
            return mid;
    }
    return -1;
}

二分查找算法适合:只需查找,不需要插入(O(N)复杂度?)和删除的情况。如查询元素周期表这种较稳定的数据。

2 欧几里德算法(求最大公因数)

 1 unsigned int Gcd(unsigned int M, unsigned int N)
 2 {
 3     unsigned int Rem;
 4      
 5     while(N > 0){
 6         Rem = M % N;
 7         M = N;
 8         N = Rem;
 9     }           
10     
11     return M;
12 }

若M > N,则第一次循环交换M和N。

若想分析其时间复杂度,则要求循环次数,即生成余数的次数。

可以证明: 当M > N, 则M % N < M / 2

证明:当N <= M/2 时,M % N < M / 2

         当N > M/2时,M - N < M / 2,那么也有M % N < M/2. 

   结论成立。

由此可得:有M、N(M>N)两个正整数,第一次循环后其余数小于M/2,第二次循环后其余数小于N/2,所以可以说,每两次循环后,余数最多为原值的一半。

所以最大的求余次数为2logN = O(logN).

2logN并不精确,即使在最坏的情况(如M、N是相邻的fibonacci数)下,仍然可被优化为1.44logN(每次M和余数的商为1.618,则求余的次数为N关于1.618的对数,即1.44logN)。欧几里德算法的平均性能需要大量篇幅的精确计算,迭代次数的平均值约为(12ln2 lnN) / π^2+1.47。

3 求幂

最简单的递推公式:Xn = Xn-1 * X;

         X0 = 1;

需要Θ(n)步和Θ(n)空间

还可以: Xn = X n/2 * X n/2 = (X * X)n/2    X is even

     X= X(n-1)/2 * X(n-1)/2  * X = (X * X)(n-1)/2 * X     X is odd

long int Pow(long int X,unsigned int N)
{
    if(N == 0)
        return 1;if(N % 2 == 1)
        return Pow(X * X, N/2);
    else
        return Pow(X * X, (N-1)/2) * X;
}

需要Θ(logN)步和Θ(logN)空间。

还可以:Xn = X n/2 * X n/2     X is even

    Xn = Xn-1 * X          X is odd

 
1 long int Pow(long int X, unsigned int N)
2 {
3     if(N == 0)
4         return 1;
5     if(N % 2 == 0)
6         return Pow(X*X, N/2);
7     else
8         return Pow(X, N-1) * X;
9 }   

 

需要Θ(logN)步和Θ(logN)空间。

在不影响正确性的前提下对代码进行微调是很有趣的。

如对第六行的修改:

1) return Pow(Pow(X,2), N/2);

看上去正确,但当N=2时,return Pow(Pow(X,2),1)调用Pow(X,2),Pow(X,2)继续调用Pow(X,2),每次递归没有向基准情况推进,无限循环直到崩溃。

2) return Pow(Pow(X, N/2), 2);

看上去正确,但当N = 2时, 调用Pow(X,2),再调用Pow(X,2)...,同(1).

3) return Pow(X, N/2) * Pow(X, N/2);

正确,但影响效率。

每次调用都有两次递归。则总步数为20 + 21 + 22 + ... 2logN  = 2logN+1 -1 = 2N-1

则需O(N)步和O(N)空间。(待验证)

 以及和上述算法相同的迭代算法。

long int Pow(long int X, unsigned N)
{
    long int a = 1;

    while(N > 0){
        if(N % 2 != 0)
            a *= X;
        N = N/2;
        X *= X;
    }
    return a;
}

例如 X23 = X * (X * X)11 = X * X2 * (X2 * X2)5 = X * X2 * X4 *(X4*X4)2 = X * X2 * X4 * (X8 * X8)1 = X * X2 * X4 * X16 = X23

该程序中定义了一个变量a,整个程序中保证a * XN不变,则当N = 0时,a *X= a * X0 = a,说明这是a的值即我们要求的值。

通常,定义一个不变量,使它在状态间保持不变,这种技术是构建迭代算法的强有力的方法。

posted on 2014-02-26 09:58  奇好猫  阅读(17186)  评论(3编辑  收藏  举报

导航