关于时间复杂度的一点简单总结
算法的复杂度分为时间复杂度和空间复杂度,今天在这里主要讲一下时间复杂度。
一.什么是时间复杂度:
时间复杂度,简单来说,就是通过程序语句的执行次数来估计程序运行时间的一个函数,用O表示。(在相同硬件和软件的情况下);可以把它认为就是花费时间的函数的数量级。
二.为什么要注意时间复杂度?
简单来说,就是为了提高计算机工作的效率,因为现实生活中,通常处理的是大量的数据,所以需要通过优化程序,使得提升工作效率。以及为分析程序效率提供了便利;
三.时间复杂度的估算:*
一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n))为算法的渐进时间复杂度(O是数量级的符号 ),简称时间复杂度。
以上是时间复杂的数学定义:
在这里f(n)代表的是算法需要的增长速度,O(n)就是把f(n)的最高项系数去掉之后,保留的最高项次数,就是表示的是一个数量级。简单来说就是我们估算出f(n)的函数,然后再对它进行O运算的结果。
几个简单的判定方法:
1.常数级是O(1):就是不管n多大,始终是一个常数;
比如说执行一条语句,a+b;
2.通常循环是O(n)(一层);(因为执行n次常数级操作)
嵌套通常情况是O(n^m);
3.采用二分策略可以降到log2(n);
四.几个简单降低时间复杂度的例子:
e.g.1(一个简单的例子)
计算(a^b)%mod;
通常我们会这样做:
#include<stdio.h>
#include<time.h>
const long long mo = 500;
#define ll long long
int main() {
ll a, b, tmp;
scanf("%lld%lld", &a, &b);
clock_t start = clock();
tmp = a;
b--;
while (b) {a = a * tmp % mo; b--;};
clock_t end = clock();
printf("%f", (double)(end - start) / CLOCKS_PER_SEC);
printf("%lld\n", a);
return 0;
}
通过简单的估算我们可以得到时间复杂度是O(n);
这个程序在一定数据范围下的运行时间我们是可以接受的,但是想想如果b很大的情况下呢?
比如b=10^17呢?(通常1s约等于O(10^8));意思就是说在b很大的情况下,我们要等很久才能出结果;
那么这种情况我们该怎么解决呢?
其实刚刚的过程就像是我们一个个的枚举计算,就像用手搬砖,一次能做的拿的东西就只有一个。
我们其实可以换种思路来运算:
比如我们要求2^64
我们可以把它看成(4)^32,(16)^16…..,就是每次给降幂的一半,把底数上升到它的平方;
这样我们的计算次数就会减少;
但是对于2^65来说,我们不能直接分解,我们可以把它看成2^64*2,设置一个临时变量来储存结果,当指数市集是奇数的时候就用临时变量乘一下,然后继续分解;
#include<stdio.h>
#include<time.h>
#define ll long long
const ll mo = 500;
ll quick_pow(ll a, ll b)
{
ll ans = 1;
while (b)
{
if (b % 2)ans = (ans * a) % mo;
b >>= 1;
a = (a * a) % mo;
}
return ans;
}
int main() {
ll a, b;
scanf("%lld%lld", &a, &b);
time_t start = clock() / 1000;
time_t end = clock() / 1000;
printf("%lld\n", quick_pow(a, b));
printf("%f", (double)(end - start) / CLOCKS_PER_SEC);
return 0;
}
这里我用了同一组数据来测试程序
2 100000000000000000(17个0)
对于第一个程序运行的时间是:。。。。。。
很久 ,我用手机测得时间,10分钟都没出来结果
而对于第二个运行的时间就少的多:
分析:因为每次都将指数除二,比如我们计算2^64,我们最多计算几次(2^64约等于10^18);
我们就可以把它转换成log2(n)的算法;
总结:计算机虽然计算速度比较快,但是我们也得让它有效率,有些情况下我们可以借助数学性质来降低时间复杂度;
e.g.2
* 关于排序:
就是想到排序,我们大一的可能在c语言课上接触到的有冒泡,选择排序等,这里我主要讲两个排序:
冒泡排序,归并排序(降序):
先看冒泡排序:
for(int i=0;i<n-1;i++)
{
for(int j=0;j<n-1-i;j++)
{
if(a[j]>a[j+1])
{
int tmp=a[j];
a[j]=a[j+1];
a[j+1]=tmp;
}
}
}
这个算法的时间复杂度就是O(N^2);
还是刚刚那个问题,如果数据范围很大呢?
这里我们介绍的是归并排序:
归并归并,就是先归再并,简单来说,就是我们把一个数列一分为二,然后再把子区间一分为二,直到整个区间只有一个数字(构造一个树形结构),然后我们合并两个区间的时候就可以利用树的后序遍历合并,就是每次将两个区间的首位数进行比较,把数字大的放在新数组的前面,对与有一个区间没有元素了,就把另一个区间的数依次加入队尾,再从小区间合并到大区间,最后完成排序。
代码实现如下:
void sort_x(int l,int r)
{
int mid=(l+r)/2,i,j,tmp;
if(r>l)
{
sort_x(l,mid);
sort_x(mid+1,r);
tmp=l;
for(i=l,j=mid+1;i<=mid&&j<=r;)
{
if(a[i]>a[j])
{
c[tmp++]=a[j++];
}
else c[tmp++]=a[i++];
}
if(i<=mid) for(;i<=mid;) c[tmp++]=a[i++];
if(j<=r) for(;j<=r;) c[tmp++]=a[j++];
for(i=l;i<=r;i++) a[i]=c[i];
}
}
实例对比:
对于这样一组数据:
程序一:
程序二
:
可以看到:
下面那个程序运行时间远远比第一个快;
e.g.3;
判断素数:
思路一:试除法:
bool is(int x)
{
if(x<=1)return false;
for(int i=2;i<sqrt(x);i++)
{
if(x%i==0)return false;
}
return true;
}
时间复杂度:O(sqrt(n))
对于这个程序有两个问题,第一个就是无法在短时间内判断一个大数为素数,第二个问题就是无法灵活的处理多次询问的问题。
我们先来看第二个问题:(下面的范围:n<=1^6);
如果我们要查询很多个数是不是素数,最暴力的方法就是每个数都判断一下,假设有m次询问(m<10^6),
那么每次的时间复杂度就是O(sqrt(n)),当然对于一定范围内我们可以利用数组打表,询问就只用一次就可以解决:
bool ok[1000006]={0];
for(int i=1;i<=1000000;i++)
if(is(i))ok[i]=1;
再分析一下,打表的话就有一个固定的开销,;好像并没有优化的样子;
其实我们可以利用一下素数的性质,就是除了二以外,每个数的只能被1和它本身整除,意思就是说,任何数的倍数都是合数,还有一个定理是任何合数都可以分解成几个素数的积,这样的目的就是为了避免数字重复被处理;
int tot=1;
bool ok[1000006];
int prime[1000006];
ok[0]=0;ok[1]=0;
for(int i=2;i<=1000000;i++)
{
if(!ok[i])
{
prime[tot++]=i;
for(int j=1;j<=tot&&i*prime[j]<=1000000;j++)
{
ok[i*prime[j]]=0;
if(!(i%prime[j]))break;
}
}
}
分析:虽然是一个二重循环,但是我们每次都会把后面的数据,给筛选掉,并且内层循环里面的次数取决于素数的个数,而素数的又比较少,所以该执行次数的数量级就是O(n)级别的,这样就比上面的程序快了很多。
第一个问题的话有兴趣的读者可以去了解一下米勒拉宾算法。这个算法可以在短时间内判断一个大数是不是素数。
总结:
解决问题的思路有很多,我们解决问题的思路决定了我们解决问题的时间。从上面的例子我们可以看出 ,通常情况下我们可以借助数学解决问题,比如说数列求和。。。也可以借助一些数据结构:比如说我们的排序还有一种堆排序,也有利用二进制性质的树状数组可以快速求出前缀和,解决集合关系的并查集。同时我们可以借助一些算法思想,比说分治。当然我们也可以借助一下计算机本身的运算机制(二进制),位运算。
以上就是我的简单总结,总结内容偏简单,有什么不足请大家指出。