算法基础(C语言描述)

200 阅读3分钟

什么是算法

  • 算法是为了解决问题需要遵守、被明确指定的指令的集合。
  • 很多人说程序的本质是算法,就是因为程序的本质也是为了解决问题而使用的需要遵守、被明确指定的指令的集合。

使用算法需要考虑的问题(时间&空间)

  • 如果一个算法是可行的,我们接下来要考虑的往往是算法的效率。比如说如果一个算法虽然可以解决问题,但是却要耗费很长的时间或不合理的内存空间,那这种算法其实也意义不大。

算法复杂度

时间复杂度

  • 大O表示法
/*
 * 这是一个简单的单循环算法
 * n 表示循环的规模 =〉问题规模
 */
void hello(int n){
    int i = 1; 
    //用 i 表示其与 n 的关系为 i<=n
    while(i<=n){
        printf("你叫什么名字?你玩过吃鸡吗?\n",i);
        i++;
    }
    printf("废话,I'm a box on the ground");
}
  • 假设 n 为 100,可以得到一个表达式为:T(100) = 1 + 101 + 2 * 100 + 1其中 T 表示单位时间成本 n 表示问题规模 T(n) = 时间复杂度第一个常数 1 表示 int i = 1;在程序中执行的次数。第二个常数 101 表示 while 判断执行的次数。后面依次类推.....最终得到一个详细的算法时间开销
  • 我想我已经可是使用上述方法去计算算法时间复杂度了,但是现在我遇到了一个问题:我可能需要写一段很长很长的代码,如果我还用这种逐行去数的方式去计算实在显得有点不切实际。所以现在我的脑袋里浮现出了两个问题。如果一个算法有上千行的代码,难不成我也要一行一行地去数?我是否可以忽略某些部分,只关注代码中影响时间复杂度的最重要部分?
  • 能!首先我们将 T(100) 中的 100 仍然使用代数 n(表示规模) ,可得 1 + 101 + 2 * 100 + 1 = 3n + 3对于程序而言,运算阶数低的部分的时间开销往往可以忽略不计,只保留阶数最高的部分(好读书不求甚解,可以先这么理解) ,于是乎哈哈得到 3n,最后还是由于好读书不求甚解的原因,当 n 变得足够大时,常数项往往可以忽略不计,遂得到 3n = n最后用大O表示法得到当前算法的时间复杂度为 T(n) = O(n)

算法的时间复杂度

O(1) < O(log2n) < O(n) < O(nlog2n) < O(n²) < O(n³) < O(2ⁿ) < O(n!) < O(nⁿ)
常 < 对 < 幂 < 指 < 阶

示例

  • 嵌套循环
    /*
     * 这是一个简单的嵌套循环算法
     * n 表示循环的规模 =〉问题规模
     */
    void hello(int n){
        int i = 1; 
        //用 i 表示其与 n 的关系为 i<=n
        while(i<=n){
            printf("你叫什么名字?你玩过吃鸡吗?\n",i);
            i++;
            //内层循环次数以及循环几轮取决于 n 的值,所以共执行n²次
            for(int j = 0; j < n; j++){
                printf("如果你玩吃鸡的话,我们可以组团吃鸡。\n");
            }
        }
        printf("废话,I'm a box on the ground");

得到 T(n) = O(n) + O(n²) 取最高阶的部分得出 T(n) = O(n²)

  • 单次循环、 i 倍次递增
    /*
     * 这是一个简单的循环算法
     * n 表示循环的规模 =〉问题规模
     */
    void hello(int n){
        int i = 1; 
        //用 i 表示其与 n 的关系为 i<=n
        while(i<=n){
            printf("你叫什么名字?你玩过吃鸡吗?\n",i);
            i = i * 2;
        }
        printf("废话,I'm a box on the ground");
    }

得到 T(n) = O(log2n)
log2n: 假设 n = 8 , 8的对数为3 ,2³ = 8。由于2³ + 1 > 8,所以循环退出,最终循环一共执行了 3 次,3 = log2(8),结果 T(n) = log2n

  • 遍历查找算法
    /*
     * 这是一个简单的遍历查询算法
     * n 表示循环的规模 =〉问题规模
     */
    void hello(int flag[],int n){
        int i = 0; 
        printf('find n\n')
        //用 i 表示其与 n 的关系为 i<=n
        while(i<n){
            if(flag[i]==n){
                printf("find it,厉害,值:%d\n",flag[i]);
                break;
            }
            i++;
        }
    }

最好的情况:数组中第一个元素就是要查找的值 T(n) = O(1) 一般不采用
最坏的情况:数组中最后一个元素才是目标元素 T(n) = O(n)
平均情况:假设n在任意一个位置的概率相同 T(n) = O(n)\

  • 平均情况复杂度计算公式\
  • 循环次数x = (1+2+3+4+...+n) * 1/n = (n(1+n)/2)*1/n = 1+n/2\
  • T(n) = O(x) = O(n)

空间复杂度

空间与问题规模的关系

示例

  • 循环递增
 void hello(int n){
        int i = 1;
        while(i<=n){
            printf("你见过凌晨4点的洛杉矶吗?%d\n",i);
            i++;
        }
        printf("我没见过,但是我见过凌晨4点网吧,凌晨4点烧烤摊,还有凌晨四点的召唤师峡谷\n");
    }

i&n都是内存中固定的常量,所以无论程序执行时i怎样变化,原地工作,复杂度都为O(1)
S(n) = O(1)

  • 单个数组
    // n 占 4个字节
    void hello(int n){
        //flag占4n个字节
        int flag[n];
        //i占4个字节
        int i;
    }

变量占用的空间相加 S(n) = 4 + 4n + 4 = 4n + 8 = O(n)

  • 二维数组
    // n 占 4个字节
    void hello(int n){
        //flag占4n²个字节
        int flag[n][n];
        //temp占4n个字节
        int temp[n];
        //i占4个字节
        int i;
    }

S(n) = 4 + 4n² + 4n + 4 = 4n² + 4n = O(n²)
取最高阶 4n²

  • 递归
    void hello(int n){
        int a,b,c;
        if(n>1){
            hello(n-1);
        }
        printf("%d\n",n)
    }

S(n) = O(n)
每次递归调用,都会为方法开辟一块内存空间,虽然每块内存空间中都有a,b,c三个整数,但是他们与问题规模没有关系,所以空间复杂度为O(n)

  • 递归&数组
    void hello(int n){
        int flag[n];
        if(n>1){
            hello(n-1);
        }
        printf("%d\n",n)
    }

与上面相反,此处会有一个与问题规模关联的数组,所以代入一个等差数列 1 + 2 + 3 +...+ n = n(1+n)/2 = 1/2 * n² + 1/2 * n
忽略常数项,取最高阶可得 T(n) = S(n²)