时间复杂度介绍及其计算

简介: 时间复杂度介绍及其计算

1.算法效率


如何衡量一个算法的好坏呢?看这段代码:


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


这是斐波那契数列的递归代码,非常简洁,那么这就一定说明它好吗?答案显而易见。


2.算法的复杂度


算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。


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


3.算法的时间复杂度


概念:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,就是算法的时间复杂度。


也就是:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。


Test01

//计算Func1中的++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);
}


count++语句的执行次数:F(N) = N^2 + 2N +10


N=10时:F(N) = 130

N=100,F(N) = 10210

n = 1000,F(N) = 1002010

实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。


大O的渐进表示法

大O符号(Big O notation):是用于描述函数渐进行为的数学符号。


大O阶的推导方法:


  1. 若最高阶项不存在,则用常数1取代运行时间中所有的常数部分。
  2. 在修改后的运行函数次数中,只保留最高阶项。
  3. 如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。


那么在Test01中,使用大O表示法之后:时间复杂度为 O(N^2)


大O表示法实际上是去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。


另外有些算法的时间复杂度存在最好、平均和最坏情况:


最坏情况:任意输入规模的最大运行次数(上界)


平均情况:任意输入规模的期望运行次数


最好情况:任意输入规模的最小运行次数(下界)


例如:在一个不重复长度为N的数组中寻找一个值为x的下标。


最好情况:1次找到


最坏情况:N次找到


平均情况:N/2次找到


但是在实际操作中,一般关注的是算法的最坏运行情况所以数组中搜索数据时间复杂度为O(N)。


Test02:

//计算Func2中的++count语句执行了多少次?
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);
}


Test02的时间复杂度用大O的渐进表示法就为:O(N)。


原因解释:这里的++count语句严格计算的话:共执行了:2N+10次,但是根据大O的渐进表示规则:最高阶项是2N,这里将2去掉,剩下的部分就是时间复杂度。


Test03:

//计算Func3中的++count语句执行了多少次?
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);
}


Test03的时间复杂度为:


  • 若N与M接近:则O(N)或者O(M)都可以。(当N与M接近时,++count语句的执行次数接近于2N或2M,再去掉常数部分,即可得到答案)
  • 若M>>N,则为O(M)。(当M>>N时,N就可忽略不计。)
  • 若N>>M,则为O(N)。


Test04:

void Func4(int N)
{
  int count = 0;
  for (int k = 0; k < 100; ++k)
  {
  ++count;
  }
  printf("%d\n", count);
}


Test04的时间复杂度为:O(1)


解释:无论其他变量如何变化,++count语句始终会执行100次,始终为常数次,时间复杂度用大O的渐进表示法则为:O(1)。


Test05:

// 计算strchr的时间复杂度?
const char * strchr ( const char * str, int character );


strchr函数的功能是:在str字符串中寻找character字符的下标,若不存在则返回-1。这个函数查找的可以分为最好和最坏两种情况:


  • 最好情况:1次就找到
  • 最坏情况:搜完整个字符串才找到或者不存在。


而在大O的渐进表示法中,一般表示最坏的情况,假设字符串的长度为N,那么strchr函数的时间复杂度就是O(N)了。


Test06:

// 计算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;
  }
}


Test06的时间复杂度为:O(N2).


原因:冒泡排序是由两层循环嵌套实现的,数组长度为n。假设最坏情况:数组中的元素由大到小排列。外层循环要执行n-1次,内层循环会随着外层循环的增加而减少,所以整体的执行次数为:(N-1) + (N-2) + (N-3) + (N-4) + ……+1,这是一串等差数列,最高阶项就是N2,所以时间复杂度也就是O(N2)。


Test07:

// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
  assert(a);
  int begin = 0;
  int end = n - 1;
  // [begin, end]:begin和end是左闭右闭区间,因此有=号
  while (begin <= end)
  {
  int mid = begin + ((end - begin) >> 1);
  if (a[mid] < x)
    begin = mid + 1;
  else if (a[mid] > x)
    end = mid - 1;
  else
    return mid;
  }
  return -1;
}


Test07的时间复杂度是:O(logN),(以2为底,N的对数)。


解释:考虑最差情况:要寻找的数在边界上,即二分区间之内只有一个数。一个长度为N的数组,要执行多少次二分,才能让二分区间只有一个数字?答案是logN次。所以时间复杂度就为O(logN)。


二分查找的效率是非常高的,但是由于被二分的数组必须有序,那么二分查找才能有效执行,这就导致了二分查找是不经常使用的。


注意:logN这种写法,如无特殊说明,底数都是2.


Test08:

// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
  if (0 == N)
  return 1;
  return Fac(N - 1) * N;
}


Test08的时间复杂度为:O(N)


解释:Fac是一个用于计算阶层的函数,这里的递归次数取决于参数N。递归调用的复杂度就是多次调用次数的累加,而在每一次的递归调用中,语句的执行次数为常数次,也就是O(1),这里的时间复杂度就是函数的调用次数了,也就是O(N)。


Test09:

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


Test09的时间复杂度为:O(N2)


解释:函数每调用一次,就会再次向下调用两次这个函数,直到一方调用到F(2)或F(1)为止。如图所示,如图的每一层,函数调用次数会随着函数调用深度而增加,由20 到 21 ,22,直到2 N-1 ,再对这些调用次数进行相加,再使用大O的渐进表示法,最后就能得到时间复杂度。调用到最后,会形成一个类似等腰三角形的形状。灰色部分是无函数调用,白色部分是函数调用。


7f4ff6bfa3a7a8a72b465f1d8684ecf5_6ab3dab006ce4c508387b5c48f3b4236.png


d8965531280e9f8b11a1c00f37f00e53_9318217815b1473cb3dd582b80f7d735.png


4.完结


时间复杂度的内容就到这里啦,若有不足,欢迎评论区指正,下期见!


相关文章
|
SQL Java 数据库连接
挺详细的spring+springmvc+mybatis配置整合|含源代码
挺详细的spring+springmvc+mybatis配置整合|含源代码
|
Dubbo Java 应用服务中间件
微服务框架Dubbo环境部署实战
微服务框架Dubbo环境部署的实战指南,涵盖了Dubbo的概述、服务部署、以及Dubbo web管理页面的部署,旨在指导读者如何搭建和使用Dubbo框架。
1123 17
微服务框架Dubbo环境部署实战
|
存储 消息中间件 缓存
Redis的高性能使得它非常适合用于实时分析场景
【5月更文挑战第15天】Redis在Python Web开发中扮演关键角色,常用于缓存系统,提高数据读取速度;会话管理,存储用户信息;分布式锁,确保数据一致性;排行榜和计数,利用有序集合和哈希结构;消息队列,基于列表结构实现异步处理;实时分析,高效处理实时数据。其丰富的数据结构和高性能使其在多种场景下应用广泛。
460 3
|
数据处理 Python
使用Pandas解决问题:对比两列数据取最大值的五种方法
​在数据处理和分析中,经常需要比较两个或多个列的值,并取其中的最大值。Pandas库作为Python中数据处理和分析的强大工具,提供了多种灵活的方法来实现这一需求。本文将详细介绍五种使用Pandas对比两列数据并取最大值的方法,通过代码示例和案例分析,帮助新手更好地理解并掌握这些技巧。
821 0
|
存储 SQL Oracle
关系型数据库Oracle归档日志备份
【7月更文挑战第19天】
375 5
|
供应链 监控 算法
ERP系统中的库存优化与库存周转率分析解析
【7月更文挑战第25天】 ERP系统中的库存优化与库存周转率分析解析
1307 1
|
机器学习/深度学习 算法 数据可视化
算法金 | 再见!!!K-means
**k-means 算法的简要总结:** - **k-means** 是一种非监督学习的聚类算法,用于将数据分为 k 个类别。 - **工作流程** 包括初始化 k 个中心点,分配数据点到最近的中心,更新中心点,然后迭代直到中心点稳定或达到最大迭代次数。 - **优点** 包括简单易懂、计算效率高,适合大规模数据,结果直观。 - **缺点** 包括需要预设 k 值,对初始中心点敏感,假设簇是凸形,受异常值影响大。
454 2
算法金 | 再见!!!K-means
|
API Python
实现缓冲区协议
实现缓冲区协议
117 0
|
前端开发 Java Spring
Spring Framework五大功能模块
Spring Framework五大功能模块
145 0
|
SQL 监控 关系型数据库
数据库日志解析:深入了解MySQL中的各类日志
数据库日志解析:深入了解MySQL中的各类日志
1202 0