算法的复杂度
算法在编写可执行程序后,运行时需要耗费时间资源和空间资源。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外的空间。在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度,所以我们如今已经不需要再特别关注一个算法的空间复杂度。
摩尔定律:每十八个月 半导体晶体管数量会增加一倍,内存越来越偏大和便宜,程序多占一点内存也无所谓
但是对于时间还是有追求的,效率最重要。
时间复杂度
时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法执行所消耗的时间,从理论上来说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度
上述所说的的函数,并不是我们所调用的函数,而是数学里带有未知数的函数表达式
一个程序它所运行的时间与机器和环境有关系,所以我们无法拿时间来比较。
我们用程序执行次数来进行比较
请计算一下Fun1中++count语句总共执行了多少次?
void Func1(int N)
{
int count = 0;
for (int i = 0; i < N; ++i)
{
for (int j = 0; j < N; ++j)
{
++count;
}
}
for (int k = 0; k < 2 * N; ++k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
上述代码时间复杂度的函数式 :F(N) = N*N + 2*N + 10
我们取对F(N)影响最大的那部分,不需要将每一部分都带进去
实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。
大O的渐进表示法:估算!
大O符号:是用于描述函数渐进行为的数学符号 学过高数的同学应该知道
推导大O阶方法:
1、用常数1取代运行时间中的所有加法常数
2、在修改后的运行次数函数中,只保留最高阶项
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶
所以上面代码的时间复杂度是O(N^2)
void Func2(int N)
{
int count = 0;
for (int k = 0; k < 2 * N; ++k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
经过我们了解学习后,来算算这段代码的时间复杂度
时间复杂度函数表达式F(N) = 2N +10 ---> 时间复杂度O(N)
其实在经历上述学习后我觉得时间复杂度与高数中的极限有异曲同工之妙,这个N是趋近于无穷大的,所以所有的常数都可以忽略不计,N的系数也可以忽略不计,只求最高阶,毕竟这是一个估算。
void Func3(int N, int M)
{
int count = 0;
for (int k = 0; k < M; ++k)
{
++count;
}
for (int k = 0; k < N; ++k)
{
++count;
}
printf("%d\n", count);
}
这段代码的时间复杂度分为四个情况,根据给的条件作答
没有说明M和N的大小关系 O(M+N)
M远大于N O(M)
N远大于M O(N)
N与M差不多大 O(M) O(N)
注意!!!!!
假如你计算的时间复杂函数中只有常数,那么结果就是O(1) !
并不是你只能运算一次,而是你只能运算常数次!
悲观预期
假如让你查找一个字符串 hello world
假如查找的是h 1 最好情况:任意输入规模的最小运行次数(下界)
假如查找的是w N/2 平均情况:任意输入规模的期望运行次数
假如查找的是d N 最差情况:任意输入规模的最大运行次数(上界)
我们用N代表字符串长度大小
当一个算法随着输入不同,时间复杂度不同,时间复杂度做悲观预期,看最坏情况
当我们遇到二分查找的时候
int BinarySearch(int* a, int n, int x)
{
assert(a);
int begin = 0;
int end = n;
while (begin < end)
{
int mid = begin + (end - begin) / 2;
if (a[mid] < x)
{
begin = mid + 1;
}
else if (a[mid] > x)
{
end = mid;
}
else
return mid;
}
return -1;
}
可能很多人上来就看这是几层循环,直接得出O(N)的结果
但是!算时间复杂度不能只去看几层循环,而是要去看他的思想!
二分查找的时间复杂度是O()
最好情况:O(1)
最坏情况:O(
)
二分查找就好比折纸一样,如果找不到就会折一半,所以他所能查找的空间每次就少一半
所以是2 ^ X = N 这个X就是O()中的数
一个2的次方,增长起来是十分快的。
假如有一百万个数,大概只需要20次就能查到!
中国有十四亿人,假如要查找到一个人,最多只需要31次!
由此可以看出来二分查找的牛逼之处!
不仅二分查找能体现,递归方法的Fibonacci数列也可以体现
如果对他感兴趣的话,可以看这篇博客 点我!点我!
空间复杂度
空间复杂度也是一个数学函数表达式,是对一个算法在运行过程中临时占用存储空间大小的量度,也就是额外的存储空间。
空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则跟时间复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的占空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显示申请的额外空间来确定
int Fib(int n){if (n < 3)
{
return 1;
}else{
return Fib(n - 1) + Fib(n - 2);
还是斐波那契额数列,它的空间复杂度是多少呢?
答案是O(N),很多小伙伴以为是O(2^N),我当时也是这么以为的,因为他需要额外大概O(2^N)个变量。
但是我们要记住一句话,空间是可以重复利用的,时间则不能!
在上面代码中当走进Fib(n - 1)时,会以n -1 为n进入下一个Fib(n - 1)直到最后一个,然后释放空间,进入Fib(n - 2)。
所以,空间复杂度就是递归的深度!
接下来整两道练练手
思路1:排序 利用qsort快排->时间复杂度O(n*
)
思路2:先把0-n个数字的和算出来,再求出数组中所有数的和,一减就出来了->时间复杂度O(N)
思路3:创建一个数组,初始化为0,把每个数放到数组对应下标的位置,假如哪个位置是0。->时间复杂度O(N)
思路4:异或 给一个值x = 0
x先跟[0,n]所有值异或 在跟数组里所有值异或 最后x就是缺的那个数字
一道题有多种方法,那么我们不用实现,只需要分析每种方法的时间复杂度,选择复杂度优的方式,这就是复杂度存在的意义
我做了一个方法3:
int missingNumber(int* nums, int numsSize){
int sum = (numsSize)*(numsSize+1)/2;
int i = 0;
for(i = 0;i<numsSize;i++)
{
sum -= nums[i];
}
return sum;
}
其他方法自己可以试试
第二题:
思路1:将最后一个数存放到临时变量中,之后每一位数保存到下一位数,再将第最后一位数放到第一位,循环K次
思路2:这个方法很巧妙,只需要三次逆转
因为是右移
先将从后往前k位之后的数逆序,再将k位前面的数逆序,最后将整个数组逆序
比如 1,2,3,4,5 k = 2
1->1,2,3,5,4
2->3,2,1,5,4
3->4,5,1,2,3
void reverse(int* nums,int left,int right)
{
int temp = 0;
while(left < right)
{
temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
left++;
right--;
}
}
void rotate(int* nums, int numsSize, int k){
k = k % numsSize;
reverse(nums,0,numsSize-k-1);
reverse(nums,numsSize-k,numsSize-1);
reverse(nums,0,numsSize-1);
}
k = k % numsSize;
一定要记得对numsSize取余,不然会越界,而且每旋转numsSize次,数组不变,所以进行取余就ok了
今天所有的内容就这些了,感谢你能看到这里。
三连必回!!!