首页 > 其他分享 >时间复杂度

时间复杂度

时间:2024-10-09 17:36:39浏览次数:11  
标签:复杂度 算法 时间 字符串 常数 排序

时间复杂度总结

什么是时间复杂度

  • 时间复杂度是一个函数,它定性描述了算法的运行时间

  • 开发者通过时间复杂度估算程序的运行时间。通常以算法的操作单元数量来代表程序消耗的时间,这里默认算法的每个操作单元运行所消耗的时间都是相同的

  • 假设算法的数据规模为n,操作单元数量用函数f[n]表示,随着数据规模n的增大,算法执行时间的增长率和f[n]的增长率相同,这个增长趋势称为算法的渐近时间复杂度,简称时间复杂度。

O是什么

  • 大家都知道O(n),O(n^2),却说不清什么是O。

  • 《算法导论》给出的解释是:O用来表示上界,当用它作为算法在最坏情况下运行时间的上界时,就是对任意数据输入的运行时间的上界。

  • 《算法导论》给出了例子

    • 以插入排序为例,插入排序的时间复杂度是O(n^2)。

      • 输入数据的形式对程序运行时间有很大影晌,在数据有序的情况下,时间复杂度是O(n)。如果
        数据是逆序的,那么插入排序的时间复杂度就是O(n^2), 也就是对于所有输入情况来说,最差情况下的时间复杂度是O(n^2), 所以称插入排序的时间复杂度为O(n^2)

      • 补充插入排序相关内容

        • 插入排序原理如下:

          • 从第一个元素开始,该元素可以认为已经被排序;

          • 取出下一个元素,在已经排序的元素序列中从后向前扫描;

          • 如果该元素(已排序)大于新元素,将该元素移到下一位置;

          • 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;

        • 插入排序代码如下:

          void InsertSort(int A[], int n)
          {
              for(int i = 1; i < n; i++)
              {
                  int key = A[i];
                  int j = i - 1;
                  while(j >= 0 && A[j] > key)
                  {
                      A[j + 1] = A[j];
                      j--;
                  }
                  A[j + 1] = key;
              }
          }
          
        • 附上Acwing的题目链接,5333 插入排序

    • 快速排序的时间复杂度是O(nlogn),但是在数据有序的情况下,快速排序的时间复杂度是O(n^2), 所以从O的定义来说,快速排序的时间复杂度应该是O(n^2)。但是我们依然说快速排序的时间复杂度是O(nlogn),这就是业内的一个默认规定,这里的O代表的就是一般情况,而不是严格意义上的上界。

      • 补充快速排序相关内容:

        • 快速排序原理如下:

          • 从数列中挑出一个元素,称为 “基准”(pivot);

          • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;

          • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;

        • 快速排序代码如下:

          void QuickSort(int A[], int left, int right)
          {
              if(left >= right)
                  return;
              int i = left, j = right;
              int key = A[left];
              while(i < j)
              {
                  while(i < j && A[j] >= key)
                      j--;
                  A[i] = A[j];
                  while(i < j && A[i] <= key)
                      i++;
                  A[j] = A[i];
              }
              A[i] = key;
              QuickSort(A, left, i - 1);
              QuickSort(A, i + 1, right);
          }
          
        • 附上Acwing的题目链接,785 快速排序

    • 给出图片总结:
      img

  • 我们主要关心的是一般情况下的数据形式,面试中涉及的算法的时间复杂度指的都是—般情况。如果面试宫和我们深入探讨一个算法的实现及性能的时候,就要时刻想着数据用例不—样,时间复杂度也是不同的,这一点一定要注意。

不同数据规模的差异

  • 给出一张图:
    img

    • 如图2-2所示,我们可以看出不同算法的时间复杂度在不同数据输入规模下的差异。

    • 图2-2中,O(5n^2) 和 O(100n)在n为20之前,时间复杂度为O(5n^2)的算法是更优的,所花费的时间也是更少的。

  • 此图更全面:
    img

  • 在决定使用哪些算法时,不是时间复杂度越低越好(因为简化后的时间复杂度忽略了常数顶等),还要考虑数据规模,如果数据规模很小,那么可能出现时间复杂度为0(n^2)的算法比时间复杂度为O(n)的算法更合适的情况(在有常数项的时候)。

为什么在计算时间复杂度的时候要忽略常数项系数

  • 比如O(100n) 就是 O(n),O(5n^2) 就是 O(n^2),而且默认O(n) 要优于 O(n^2)。

  • 这涉及到O的定义,因为O就是在数据量级突破一个点且数据量级非常大的情况下所表现出的时间复杂度!这个数据量就是常数项系数已经不起决定性作用的数据量。

  • 例如,图2-2中的20就是那个点! n只要大于20,常数项系数就不起决定性作用了。所以我们说的时间复杂度都是省略常数项系数的,一般情况下默认数据规模足够大。

算法时间复杂度的排行

基于上述事实,给出算法时间复杂度的排行:

\[O(1)常数阶 < O(logn)对数阶 < O(n)线性阶 < O(n^2)平方阶 < O(n^3)立方阶 < O(2^n)指数阶 \]

但也要注意大常数,如果这个常数非常大,比如\(10^7\),\(10^9\),那么常数就是不得不考虑的因素了。

复杂表达式的简化

  • 如果计算时间复杂度的时候发现不是一个简单的O(n)或者O(n^2),而是一个复杂的表达式,如

    O(2*n^2 + 10*n + 1000)
    

    那么如何描述这个算法的时间复杂度呢?介绍其中的一种方法——简化法

    • 去掉表达式中的加法常数项(因为常数项并不会因为n的增大而增加计算机的操作次数):

      O(2*n^2 + 10*n)
      
    • 去掉常数系数 (上文部分已经讲了为什么可以去掉常数系数的原因)

      O(n^2 + n)
      
    • 只保留最高项去掉数量级小一级的n(因为n^2的数据规模远大于n),最终简化为:

      O(n^2)
      
    • 如果这一步理解有困难,那也可以做提取n的操作,变成O(n(n+1)) ,省略加法常数项后也就别变成了:

      O(n^2)
      
  • 也可以用另一种简化的思路,其实当n大于40的时候, 这个复杂度会恒小于O(3 × n^2), O(2 × n^2 + 10 × n + 1000) < O(3 × n^2), 所以说最后省略掉常数项系数最终时间复杂度也是O(n^2)。

O(logn)中的log以什么为底

  • 我们平时说这个算法的时间复杂度是logn的,那么一定是log以2为底n的对数么(即\(\log_2 n\))?
    其实不然,也可以是以10为底n的对数,也可以是以20为底n的对数,但我们统一说 logn,也就是忽略底数的描述

  • 为什么可以这样做?如下图:
    img

    假如有两个算法的时间复杂度,分别是log以2为底n的对数和log以10为底n的对数,那么这里如果还记得高中数学的话,应该不难理解以2为底n的对数 = 以2为底10的对数 * 以10为底n的对数。(补充,换底公式:\(\frac{log_c b}{log_c a} = log_a b\))

    而以2为底10的对数是一个常数,在上文已经讲述了我们计算时间复杂度是忽略常数项系数的。

    抽象一下就是在时间复杂度的计算过程中,log以i为底n的对数等于log 以j为底n的对数,所以忽略了i,直接说是logn。

    这样就应该不难理解为什么忽略底数了。

面试题:找出n个字符串中相同的两个字符串

通过下面这道面试题,我们分析一下时间复杂度。

题目描述:找出n个字符串中相同的两个字符串(假设这里只有两个相同的字符串)。

  • 如果是暴力枚举的话,时间复杂度是多少呢,是O(n^2)么?

    这里一些同学会忽略了字符串比较的时间消耗,这里并不像int 型数字做比较那么简单,除了n^2 次的遍历次数外,字符串比较依然要消耗m次操作(m也就是字母串的长度),所以时间复杂度是O(m × n × n)。

  • 接下来再想一下其他解题思路。

    先将n个字符串按字典序来排序,排序后n个字符串就是有序的,意味着两个相同的字符串就是挨在一起,然后在遍历一遍n个字符串,这样就找到两个相同的字符串了。

    那看看这种算法的时间复杂度,快速排序时间复杂度为O(nlogn),依然要考虑字符串的长度是m,那么快速排序每次的比较都要有m次的字符比较的操作,就是O(m × n × log n) 。

    之后还要遍历一遍这n个字符串找出两个相同的字符串,别忘了遍历的时候依然要比较字符串,所以总共的时间复杂度是 O(m × n × logn + n × m)。

    我们对O(m × n × log n + n × m) 进行简化操作,把m × n提取出来变成 O(m × n × (logn + 1)),再省略常数项最后的时间复杂度是 O(m × n × log n)。

  • 最后很明显O(m × n × logn) 要优于O(m × n × n)!

    所以先把字符串集合排序再遍历一遍找到两个相同字符串的方法要比直接暴力枚举的方式更快。

这就是我们通过分析两种算法的时间复杂度得来的。

当然这不是这道题目的最优解,仅仅是用这道题目来讲解一下时间复杂度

补充:对于上述面试题,用chatgpt生成的代码(供参考)

  • 使用哈希表
#include <iostream>
#include <vector>
#include <unordered_map>

std::string findDuplicate(const std::vector<std::string>& strings) {
    std::unordered_map<std::string, int> stringCount;
    
    for (const auto& str : strings) {
        stringCount[str]++;
        if (stringCount[str] == 2) {
            return str; // 找到第二次出现的字符串
        }
    }
    
    return ""; // 如果没有找到
}

int main() {
    std::vector<std::string> strings = {"apple", "banana", "orange", "apple"};
    std::string duplicate = findDuplicate(strings);
    
    if (!duplicate.empty()) {
        std::cout << "找到重复字符串: " << duplicate << std::endl;
    } else {
        std::cout << "没有找到重复字符串" << std::endl;
    }
    
    return 0;
}

  • 先快排,再遍历
#include <iostream>
#include <vector>
#include <algorithm>

std::string findDuplicate(std::vector<std::string>& strings) {
    std::sort(strings.begin(), strings.end()); // 快速排序
    
    for (size_t i = 1; i < strings.size(); ++i) {
        if (strings[i] == strings[i - 1]) {
            return strings[i]; // 找到重复字符串
        }
    }
    
    return ""; // 如果没有找到
}

int main() {
    std::vector<std::string> strings = {"apple", "banana", "orange", "apple"};
    std::string duplicate = findDuplicate(strings);
    
    if (!duplicate.empty()) {
        std::cout << "找到重复字符串: " << duplicate << std::endl;
    } else {
        std::cout << "没有找到重复字符串" << std::endl;
    }
    
    return 0;
}

标签:复杂度,算法,时间,字符串,常数,排序
From: https://www.cnblogs.com/hisun9/p/18453344

相关文章

  • 创建一个类实现一个打字小游戏(利用time.time()计算参赛者的打字时间,进而利用时间计算
    #打字小游戏面向过程,面向对象#游戏类==》属性、行为##1.外部传入一些数据,数据单词列表[movies,add,open]#2.随机的抽取这个单词列表中的一个单词,作为本局游戏的单词!#3.游戏开始,游戏的展示,游戏的结束,游戏时间,游戏的正确率等importrandom#随机给出列表......
  • Pandas时间序列 处理方法
    当然可以,以下是Pandas处理时间序列的常用方法总结,代码和文字说明均使用Markdown格式。1.日期解析将字符串日期转换为Pandas的datetime对象。importpandasaspd#假设有一个包含日期字符串的DataFramedf=pd.DataFrame({'date':['2021-01-01','2021-01-02','2021-01-03......
  • 一文通Java 锁:锁机制及锁常见问题的深入解析(Java 并发编程(偏向、轻/重量级、读写、可
    在并发编程中,锁机制是保障线程安全的核心工具。锁的类型、使用场景、以及锁引发的种种问题都是开发者在设计高并发系统时必须应对的挑战。本篇博客将围绕锁的类型、应用场景、以及常见的锁问题展开详细讨论,帮助大家深入理解Java锁机制的优缺点与其适用场景。文章目录......
  • C语言 实现 unix时间戳转换到自定义 tm结构体
    之前使用ESP32写了一个闹钟,免得我老是把手机闹钟给滑了就不知该起床了原本想用标准库解决的,但是这个时间一直不准,逼得用SNTP获取了步进单位为(second)的时间戳,然后使用GPtimer来维持时间戳才算是把精准计时给解决了废话不多说,直接上代码typedefstruct{intyear;......
  • 基于GA遗传优化的GroupCNN分组卷积网络时间序列预测算法matlab仿真
    1.算法运行效果图预览(完整程序运行后无水印)  2.算法运行软件版本MATLAB2022A 3.部分核心程序(完整版代码包含详细中文注释和操作步骤视频)figureplot(Error2,'linewidth',2);gridonxlabel('迭代次数');ylabel('遗传算法优化过程');legend('Averagefitness'......
  • 一个后端接口响应时间太长,可能有哪些原因?
    1.网络问题:网络延迟:网络拥堵、带宽限制和网络故障都可能导致请求在传输过程中延迟,影响响应时间网络不稳定:网络波动或者网络中断2.服务器性能问题服务器过度负载:在使用过程中,服务器在处理大量并发请求时,可能因为资源占用(cpu、内存、磁盘)空间不足,导致响应时间变长服务器配置不......
  • Go时间轮
    packagemainimport( "fmt" "github.com/rfyiamcool/go-timewheel" "sync" "sync/atomic" "time")var( counterint64=0 loopNum=50 tw=newTimeWheel() wg1=sync.Wait......
  • 适应性推理时间计算:大型语言模型的自我评估能力
    随着大型语言模型(LLMs)的不断发展,提升其在多种应用中的响应质量显得愈发重要。本文探讨了一种新的推理时间计算方法,旨在提高LLMs的效率和性能,尤其是在生成响应的过程中能够自我评估其能力,从而实现更为智能的计算资源分配。......
  • 网站的时间使用dayjs.js格式化
    1.为什么要使用dayjs在mysql数据库中,datetime字段从数据库读取后,在前台显示出来会带有TZ字母,这是格林尼治时间,如:2024-10-07T12:02:00Z,而我们一般需要2024-10-0712:02:00这种格式。另外,需要在不同地区显示本地时间,那么dayjs会将格林尼时间转换为本地时间。dayjs是一个轻量级的......
  • FredNormer: 非平稳时间序列预测的频域正则化方法
    时间序列预测是一个具有挑战性的任务,尤其是在处理非平稳数据时。现有的基于正则化的方法虽然在解决分布偏移问题上取得了一定成功但仍存在局限性。这些方法主要在时间域进行操作,可能无法充分捕捉在频域中更明显的动态模式,从而导致次优的结果。FredNormer论文的研究目的主要包......