前言
我们先要知道什么是算法
算法(Algorithm)是指用来操作数据、解决程序问题的一组方法。对于同一个问题,使用不同的算法,也许最终得到的结果是一样的,但在过程中消耗的资源和时间却会有很大的区别。
如何衡量一个算法的好坏呢,一般是从时间和空间两个维度来衡量,即时间复杂度和空间复杂度。
通俗一点的说,
我的代码实现这个功能使用的时间是30毫秒,占用内存20M
别人是1秒,占用内存40M
什么是时间复杂度
时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
假设算法的问题规模为n,那么操作单元数量便用函数f(n)来表示
随着数据规模n的增大,算法执行时间的增长率和f(n)的增长率相同,这称作为算法的渐近时间复杂度,简称时间复杂度,记为 O(f(n))
大O符号法
大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
我们来计算一下下面代码的时间复杂度
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);
}
这个函数在调用的过程中使用了三个for循环和一个while循环,每循环一次我们说它进行了一次基本操作。那么这个函数执行基本操作的次数为F(N)=N²+2*N+10
那么我们如何用大O的线性表示法来表示这个函数的时间复杂度呢?
推导大O阶方法:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
按照上面的规则,那么上述代码的时间复杂度就为O(N²)。
我们发现,通过上面的规则,我们就使用N²来代替了N²+2*N+10,我们为什么要这样规定呢,我们以上面的表达式为例,当N为不同的值时,表达式的结果为多少
N=100 F(N)=10210
N=1000 F(N)=1002010
N=10000 F(N)=100020010
我们发现,当N不断变大时,表达式的值也不断变大,而对表达式的结果影响最大的一项就是这个表达式中阶数最高的那一项。
为什么可以这么去简化呢,因为大O符号表示法并不是用于来真实代表算法的执行时间的,它是用来表示代码执行时间的增长变化趋势的。
常见时间复杂度
常数阶 对数阶 线性阶 线性对数阶 平方阶 立方阶 k次方阶 指数阶
常数阶O(1)
无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1),如:
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
此代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。
线性阶O(N)
int f ( unsigned int n ) {
if (n == 0 || n==1)
return 1;
else
return n * f(n-1);
}
此函数会被递归调用n - 1次,每次操作都是一次,所以时间复杂度为n
对数阶(logN)
void fun(int n) {
int i=l;
while(i<=n)
i=i*2;
}
此函数有一个循环,但是循环没有被执行n次,i每次都是2倍进行递增,所以循环只会被执行log2(n)次。
线性对数阶O(nlogN)
for(m=1; m<n; m++)
{
i = 1;
while(i<n)
{
i = i * 2;
}
}
将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)。
平方阶O(n^2)
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
a[i][j]=i*j;
程序有两次循环,每个循环都有n次操作,所以时间复杂度为n^2
立方阶O(n³)、K次方阶O(n^k)
和O(n^2)同理,相当于套了三层n循环
什么是空间复杂度
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 。
空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。
空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
通俗的说空间复杂度就是算法需要多少内存,占用了多少空间
常见空间复杂度
O(1)、O(n)、O(n²)
O(1)
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1)
O(N)
int[] m = new int[n]
for(i=1; i<=n; ++i)
{
j = i;
j++;
}
这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,这段代码虽然有循环,但是没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即O(n)。
O(N^2)
int** fun(int n) {
int ** s = (int **)malloc(n * sizeof(int *));
while(n--)
s[n] = (int *)malloc(n * sizeof(int));
return s;
}
此处开辟的是一个二维数组,数组有n行,每行分别有1,2,3,…n列,所以是n(n + 1)/2个元素空间,空间复杂度为n^2
复杂度练习
轮转数组
思路一:
一个接一个,挪到最前面
那么时间复杂度就是O(K*N)
那如果我旋转10次,旋转14次呢?
14为7的倍数,相当于没旋转
10相当于只旋转3次
所以,真实的旋转次数应该为:
K %= N
最坏情况: K % N = N - 1
最好情况: K % N = 0
那么真实的复杂度就是F(N) = (N-1)*(N-1),简化一下就等于O(N^2)
代码实现:
思路二
三段逆置
第一步逆置前n-k个
第二步后k个逆置
第三步整体逆置
代码实现:
void reverse(int* a, int left, int right)
{
while(left<right)
{
int tmp = a[left];
a[left] = a[right];
a[right] = tmp;
left++;
right--;
}
}
void rotate(int* nums, int numsSize, int k) {
k %= numsSize;
reverse(nums,numsSize - k,numsSize-1);
reverse(nums,0,numsSize-k-1);
reverse(nums,0,numsSize - 1);
}
这个思路的时间复杂是O(n),跟第一种思路比,这个明显更好
——————————————————————————————————————————
希望这篇博客对你有所帮助!!!