一.算法的定义

算法是一系列用于解决问题的明确、有限的指令集合,它能够在有限的时间内产生所要求的输出。具有有穷性、确定性、可行性、输入和输出这五大特性。这些性质确保了算法能够有效且准确地执行预定任务。

时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。

 算法优劣的关键就在于:其解决问题的时间和空间损耗的多少。

二.时间复杂度

1.斐波那契数列

long long Fib(int N)
{
 if(N < 3)
 return 1;
 
 return Fib(N-1) + Fib(N-2);
}

该算法通过使用递归的方式令代码复杂度大大减小,但也存在可读性较差,难以理解的问题。同时,当n较大时,每次往下反复递归所需要消耗的时间也是巨大的。

2.定义与常见表示方法

定义:

我们一般把算法运行时在最坏情况下所需要消耗的时间称为时间复杂度O一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。

常见表示方法:

1. 如果该算法基本操作的执行次数为常数次,我们称其时间复杂度为O(1).

示例如下:

for (int i = 0; i < 5; i++)
{
	cout << i << " " << endl;
}

在该程序中,输出i的值的操作执行了5次,为常数次,因此时间复杂度为O(1).

以此类推,总结如下:

常数时间复杂度:O(1),表示算法的执行时间不随输入规模的增长而变化,是最理想的情况。


对数时间复杂度:O(log n),通常出现在二分查找等分治算法中。 


线性时间复杂度:O(n),表示算法的执行时间与输入规模成正比。


线性对数时间复杂度:O(n log n),通常出现在快速排序、归并排序等分治算法中。


平方时间复杂度:O(n2),通常出现在嵌套循环的算法中。

指数时间复杂度:O(2n),通常出现在递归算法中。

多项式时间复杂度:O(nk),k可能是大于 2 的正整数,这意味着算法在大规模数据上的性能下降较快。

需要注意的是,我们对时间复杂度的表示是一个大致的计算,例如,假设我们计算的结果为n^2-2n+1,那么我们在表示时只采取最高次数的项数,即时间复杂度为O(n^2)。

3.例题计算分析

1.冒泡排序法

// 计算BubbleSort的时间复杂度?
void BubbleSort(int* a, int n)
{
     assert(a);
      for (size_t end = n; end > 0; --end)
     {
         int exchange = 0;
     for (size_t i = 1; i < end; ++i)
         {
             if (a[i-1] > a[i])
             {
             Swap(&a[i-1], &a[i]);
             exchange = 1;
             }
         }
     if (exchange == 0)
     break;
     }
}

由于时间复杂度默认是最坏情况下的大小,即数组与要求的顺序反序。

2.二分查找

// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
    assert(a);
 
    int begin = 0;
    int end = n - 1;
    while (begin < end)
    {
        int mid = begin + ((end - begin) >> 1);
        if (a[mid] < x)
            begin = mid + 1;
        else if (a[mid] > x)
            end = mid;
        else
            return mid;
    }
 
    return -1;
}

最好情况为O(1),一次直接找到,而最坏情况下,则需要k次,且2^k>=n,则时间复杂度为logn。

一次对半筛选,当数据很多时筛选k次才找到,2k=N,对数函数增长规律一样,为了保持统一性,下标可以忽略,建议写法即为logN。

3.等差数列

int fun(int n){
int i=1,s=1;
while(s<n){
	s+=++i;
}
return i;
}

循环内的执行次数为1+2+3+...+k的一个等差数列,即(1+k)*k/2,同上简化为k^2=n,因此时间复杂度为根号n。

三. 空间复杂度

1.定义

空间复杂度也是一个数学表达式,是对一个算法在运行过程中 临时占用存储空间大小的量度
空间复杂度不是程序占用了多少 bytes 的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。
空间复杂度计算规则基本跟实践复杂度类似,也使用 O 渐进表示法
注意: 函数运行时所需要的栈空间 ( 存储参数、局部变量、一些寄存器信息等 ) 在编译期间已经确定好了,因此 空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。

2.例题计算分析

(1)冒泡排序

// 计算BubbleSort的空间复杂度?
void BubbleSort(int* a, int n)
{
     assert(a);
      for (size_t end = n; end > 0; --end)
     {
         int exchange = 0;
     for (size_t i = 1; i < end; ++i)
         {
             if (a[i-1] > a[i])
             {
             Swap(&a[i-1], &a[i]);
             exchange = 1;
             }
         }
     if (exchange == 0)
     break;
     }
}

由于我们只申请了常数个变量,并且并未再额外动态开辟空间,因此空间复杂度为O(1).

(2)斐波那契数列(递归法)

// 计算斐波那契递归Fib的空间复杂度?
long long Fib(size_t N)
{
    if (N < 3)
        return 1;
 
    return Fib(N - 1) + Fib(N - 2);
}

空间复杂度为O(N)。

当传入参数N时,会不断向下递归进行逐次减去1和减去2的操作,直到递归至参数为1和2为止。每一次递归都需要调用Fib函数,而函数的调用又涉及到栈帧的销毁和创建。创建所开辟空间的次数即为函数向下递归的深度,因此空间复杂度为O(N)。

四.常见复杂度对比

一般算法常见的复杂度如下:

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐