草庐IT

数据结构——时间复杂度和算法复杂度

头发没有代码多 2023-07-15 原文

目录

时间复杂度

 计算下列函数的时间复杂度

冒泡排序时间复杂度 

 大O的渐进表示法

旋转数组 

空间复杂度


时间复杂度

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

 计算下列函数的时间复杂度

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

 时间复杂度函数式:N*N+2*N+10

Func1 执行的基本操作次数 :


N = 10 F(N) = 130
N = 100 F(N) = 10210
N = 1000 F(N) = 1002010

这里计算清楚该函数在那个量级就行,不必求具体值,上式中后俩项对F(N)的影响很小,

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

这里时间复杂度:O(N^2)

// 计算Func2的时间复杂度?
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)=2*N+10,2和10对F(N)的影响不大,所以是O(N)

// 计算Func3的时间复杂度?
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);
}

F(N)=M+N,

M远大于N,时间复杂度就是O(M)

N远大于M,时间复杂度就是O(N)

N和M差不多大,时间复杂度就是O(M)或O(N)

N=M,时间复杂度O(2M)或O(2N)

冒泡排序时间复杂度 

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

N个元素, 冒泡排序第一轮比较N-1次

                                第二轮N-2次

                                         .

                                         .

                                         .

                          最后一轮1次

1+2+3……+N-1=N*(N-1)/2=F(N)

对F(N)影响最大的一项是N^2/2,所以时间复杂度:O(N^2)

 大O的渐进表示法

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

推导大O阶方法(量级的估算):
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项
(对结果产生决定性影响)。
3、如果最高阶项系数存在且不是1,则去除与这个项目相乘的常数(参考上面的冒泡排序和M+N那道题)。得到的结果就是大O阶。

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

如这道题,用常数1取代运行时间中的所有加法常数,这道题算法复杂度为O(1),O(1)不代表一次,代表常数次

通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。

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

strchar函数遍历一个字符串,找某一个字符

另外有些算法的时间复杂度存在最好、平均和最坏情况:
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
例如:在一个长度为N数组中搜索一个数据x
最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)

这道题最好情况:O(1)

平均情况:O(N/2)

最坏情况:O(N)

对于冒泡排序

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

时间复杂度最坏:O(N^2),

时间复杂度最好:O(N) 这种情况是数组排好序了,遍历了N个元素

时间复杂度不能去数循环,而是要对程序进行分析

这是希尔排序,有三层循环,不能把这个时间复杂度认为是O(N^3),这个排序要比冒泡排序快,这个排序平均下来O(N^1.3),因此我们不能通过循环去确定时间复杂度,要看算法的逻辑进行时间复杂度计算

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

 计算二分查找的时间复杂度,二分查找数组必须是有序的

最好的情况是O(1)

最坏的情况,当找完所有的数后没有找到某元素

二分查找每次会缩放一半的区间,若区间大小为N,第一次在N/2个元素查找,第二次N/2/2,第三次N/2/2/2……=1,以此类推

每查找一次,查找区间的个数减少一班,最后就剩一个值了

假设查找了X次,N=2^X,X=log 2 N(以二为底,N的对数)

X就是时间复杂度O(log 2 N)

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

时间复杂度为:O(N)

Fac(N)>Fac(N-1)>Fac(N-2)……F(0)

这里总共要调用N次,这里递归了N次,每次是O(1)

// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
int i=0;
for(i=0;i<N;i++)
printf("%d",i);
if(0 == N)
return 1;
return Fac(N-1)*N;
}

对上题做修改后,时间复杂度变为O(N^2)

N+N-1+N-2+N-3......+1

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

2^0+2^1+2^2+2^3.... +2^(n-1)=2^N-1

 时间复杂度:O(2^N)

我们可以看到左边缺了一块,说明少了一些调用次数,但是少的这些调用次数和2^N相比可以忽略不计 。

优化斐波那契数列(把递归改循环)

long long Fib(size_t N)
{
long long f1=1,f2=1,f3;
for(size_t i=0;i<=N;i++)
{
f3=f2+f1;
f1=f2;
f2=f3;
}
return f3;
}

旋转数组 

给你一个数组,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]

 方法1:

void rotate(int* nums, int numsSize, int k) {
    int newArr[numsSize];
    for (int i = 0; i < numsSize; ++i) {
        newArr[(i + k) % numsSize] = nums[i];
    }
    for (int i = 0; i < numsSize; ++i) {
        nums[i] = newArr[i];
    }
}

 方法二:

以空间换时间

新开辟一块空间把后K个拷贝到数组前面,前N-K个拷贝到数组后面 ,之后将TMP数组拷贝回原数组,然后释放tmp数组

方法二:

 

 

 

空间复杂度

空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度

空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。

空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。

注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因 此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。

 

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

冒泡循环空间复杂度:O(1),这里数组是创建好的不是临时占用的,这里只有三个变量end i 和exchang,每次循环当中end i exchang都各自占用一个空间,循环了N次,end只占了一个空间,i和exchange也一样,所以空间复杂度是O(1),

旋转数组

这个空间复杂度是O(N),因为中间有创建一个临时数组来接收旋转的结果 

时间可以累计,空间可以重复利用

如果是结构体,还是O(1),不用考虑里面的成员个数,把结构体当成一个整体

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

空间复杂度为O(N),因为函数调用会涉及到栈帧的创建与销毁,每次在调用完一个函数之后,会把这块空间还给操作系统,这里对一个函数进行了调用,无论调用了多少次,函数所占的空间都一样,只不过是每次调用完之后会还给操作系统,每次调用的时候空间复杂度为O(1),调用N次就是O(N)

不管调用多少次,函数仍然占据 

 

 

有关数据结构——时间复杂度和算法复杂度的更多相关文章

  1. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

    我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby-on-rails - Ruby 检查日期时间是否为 iso8601 并保存 - 2

    我需要检查DateTime是否采用有效的ISO8601格式。喜欢:#iso8601?我检查了ruby​​是否有特定方法,但没有找到。目前我正在使用date.iso8601==date来检查这个。有什么好的方法吗?编辑解释我的环境,并改变问题的范围。因此,我的项目将使用jsapiFullCalendar,这就是我需要iso8601字符串格式的原因。我想知道更好或正确的方法是什么,以正确的格式将日期保存在数据库中,或者让ActiveRecord完成它们的工作并在我需要时间信息时对其进行操作。 最佳答案 我不太明白你的问题。我假设您想检查

  4. ruby - Ruby 有 `Pair` 数据类型吗? - 2

    有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳

  5. ruby-on-rails - 将 Ruby 中的日期/时间格式化为 YYYY-MM-DD HH :MM:SS - 2

    这个问题在这里已经有了答案:Railsformattingdate(4个答案)关闭4年前。我想格式化Time.Now函数以显示YYYY-MM-DDHH:MM:SS而不是:“2018-03-0909:47:19+0000”该函数需要放在时间中.现在功能。require‘roo’require‘roo-xls’require‘byebug’file_name=ARGV.first||“Template.xlsx”excel_file=Roo::Spreadsheet.open(“./#{file_name}“,extension::xlsx)xml=Nokogiri::XML::Build

  6. ruby - 查找字符串中的内容类型(数字、日期、时间、字符串等) - 2

    我正在尝试解析一个CSV文件并使用SQL命令自动为其创建一个表。CSV中的第一行给出了列标题。但我需要推断每个列的类型。Ruby中是否有任何函数可以找到每个字段中内容的类型。例如,CSV行:"12012","Test","1233.22","12:21:22","10/10/2009"应该产生像这样的类型['integer','string','float','time','date']谢谢! 最佳答案 require'time'defto_something(str)if(num=Integer(str)rescueFloat(s

  7. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  8. ruby - 我如何添加二进制数据来遏制 POST - 2

    我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_

  9. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  10. 区块链之加解密算法&数字证书 - 2

    目录一.加解密算法数字签名对称加密DES(DataEncryptionStandard)3DES(TripleDES)AES(AdvancedEncryptionStandard)RSA加密法DSA(DigitalSignatureAlgorithm)ECC(EllipticCurvesCryptography)非对称加密签名与加密过程非对称加密的应用对称加密与非对称加密的结合二.数字证书图解一.加解密算法加密简单而言就是通过一种算法将明文信息转换成密文信息,信息的的接收方能够通过密钥对密文信息进行解密获得明文信息的过程。根据加解密的密钥是否相同,算法可以分为对称加密、非对称加密、对称加密和非

随机推荐