🧑💻作者: @情话0.0
📝专栏:《数据结构》
👦个人简介:一名双非研究生的编程菜鸟,在这里分享自己的编程学习笔记,欢迎大家的指正与点赞,谢谢!
算法与算法评价
在数据结构中,有着众多的算法,比如查找算法,排序算法等。在查找算法中有顺序查找、折半查找、分块查找等,排序算法中有冒泡排序、快速排序、希尔排序等,而面对这么多的算法,是怎样去衡量算法的执行效率呢?而这也就是此篇文章的重点:时间复杂度和空间复杂度。
算法是对特定问题求解步骤的一种描述,它是指令的有限序列,其中的每条指令表示一个或多个操作。此外,一个算法还具有下列5个重要特性:
有穷性:一个算法必须总在执行有穷步之后结束,并且每一步都可在又穷时间内完成。
确定性:算法中每条指令必须有确切的含义,对于相同的输入只能得出相同的结果。
可行性:算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现。
输入:一个算法有零个或多个输入,这些输入来自于某些特定对象的集合。
输出:一个算法有一个或多个输出,这些输出是与输入存在某种特定关系的量
而一个算法被认定为 “好” 算法应该考虑到以下目标:
正确性:算法能够正确地解决问题。
可读性:算法应具有良好的可读性,以帮助读者理解。
健壮性:输入非法数据时,算法能够及时地做出回应或进行处理,而不会产生莫名其妙的输出结果。
效率与敌存储空间:效率是指算法的执行时间,存储量需求是指算法执行过程中所需要的最大存储空间,这两个都与问题的规模有关。
算法效率的度量是通过时间复杂度与空间复杂度来描述的。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算机发展初期,计算机的存储容量很小,所以对空间复杂度很是在乎。但是经过这几十年计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。
时间复杂度的定义:一个语句的 频度 是指该语句在算法中被重复执行的次数,而算法中的语句频度之和表示了算法问题规模的函数。时间复杂度就是分析频度之和的数量级。在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。
对于一个算法执行所耗费的时间,从理论上说是不能算出来的。为什么呢?只有把你的程序放在机器上跑起来,才能知道改程序所耗费的时间,这只是其中一点,还有就是相同的程序放在不同的机器上,不同的编译器上都会有不同的执行时间。所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
算法的时间复杂度不仅依赖于问题的规模,也取决于待输入数据的性质。 例如,在数组 arr[0...n-1]中,查找数值 k 的大概算法如下:
i = n - 1;
while(i >= 0 && (arr[i]) != k)
i--;
return i;
该算法中第三条语句 i-- 的频度不仅与问题规模有关,而且与输入实例中 arr 中的个元素的取值及 k 的取值有关:
若
arr中没有与k相等的元素,则第三条语句的频度为 n。
若arr的最后一个元素等于k,则第三条语句的频度为常数 0。
对于这样的算法,出现了最坏和最优的情况。
最坏时间复杂度是指在需要执行最多的次数才能完成算法的执行而算出的时间复杂度。
平均时间复杂度是指所有可能输入实例在等概率出现的情况下,算法的期望运行时间。
最好时间复杂度是指执行最少的次数完成算法的执行而算出的时间复杂度。
一般总是考虑在最坏情况下的时间复杂度,以保证算法的执行时间不会比它更长。
// 请计算一下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);
}
Func1 执行的基本操作次数 : F(N) = N^2 + 2*N + 10
在计算时间复杂度时,我们不需要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。
大O符号:是用于描述函数渐进行为的数学符号。
推导大O阶方法:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
使用大O的渐进表示法以后,Func1的时间复杂度为:O(N^2)
常见的渐进时间复杂度为:
O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(2^n) < O(n!) < O(n^n)
案例一
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-1 次,然后依次递减,最终的总次数为 n*(n-1) / 2,最终的时间复杂度为O(n^2)
案例二
int BinarySearch(int* a, int n, int x)
{
assert(a);
int begin = 0;
int end = n-1;
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(logn)
案例三
// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}

从上图可以看到,关于斐波那契函数的递归调用,没递归一次都会是上次调用个数的二倍,是以2为底数的指数级增长,时间复杂度为O(2^n)
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 。
一个程序在执行时除需要存储空间来存放本身所用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为实现计算机所需信息的辅助空间。
空间复杂度不是程序占用了多少bytes的空间,算的是变量的个数,为这些变量所开辟的空间多少。空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。
注意: 函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因
此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
案例一
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;
}
}
在这到冒泡排序算法中,总共创建了三个变量:
end、exchange、i,使用了常数个额外空间,所以空间复杂度为 O(1)
对于exchange重复创建,计算结果只算一个
案例二
//返回斐波那契数列的前n项
long long* Fibonacci(size_t n)
{
if(n==0)
return NULL;
long long * fibArray = (long long *)malloc((n+1) * sizeof(long long));
fibArray[0] = 0;
fibArray[1] = 1;
for (int i = 2; i <= n ; ++i)
{
fibArray[i] = fibArray[i - 1] + fibArray [i - 2];
}
return fibArray;
}
在斐波那契数列的算法当中,动态开辟了
n+1个内存空间,创建了一个变量,所以空间复杂度为 O(N)
案例三
// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
if(N == 0)
return 1;
return Fac(N-1)*N;
}
该算法使用递归调用了N次,开辟了N个栈帧,每个栈帧使用了常数个空间。空间复杂度为O(N)
大家可以看看下面这几个算法的时间复杂度是多少?
例一:
count = 0;
for(k = 1;k <= n;k *= 2)
{
for(j=1;j<=n;j++)
{
count++;
}
}
例二:
int func(int n)
{
int i=0,sum=0;
while(sum<n)
{
sum += ++i;
}
return i;
}
例三:
void func(int n)
{
int i=0;
while(i*i*i<=n)
{
i++;
}
}

类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h
我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%
我主要使用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
为什么4.1%2返回0.0999999999999996?但是4.2%2==0.2。 最佳答案 参见此处:WhatEveryProgrammerShouldKnowAboutFloating-PointArithmetic实数是无限的。计算机使用的位数有限(今天是32位、64位)。因此计算机进行的浮点运算不能代表所有的实数。0.1是这些数字之一。请注意,这不是与Ruby相关的问题,而是与所有编程语言相关的问题,因为它来自计算机表示实数的方式。 关于ruby-为什么4.1%2使用Ruby返
它不等于主线程的binding,这个toplevel作用域是什么?此作用域与主线程中的binding有何不同?>ruby-e'putsTOPLEVEL_BINDING===binding'false 最佳答案 事实是,TOPLEVEL_BINDING始终引用Binding的预定义全局实例,而Kernel#binding创建的新实例>Binding每次封装当前执行上下文。在顶层,它们都包含相同的绑定(bind),但它们不是同一个对象,您无法使用==或===测试它们的绑定(bind)相等性。putsTOPLEVEL_BINDINGput
我可以得到Infinity和NaNn=9.0/0#=>Infinityn.class#=>Floatm=0/0.0#=>NaNm.class#=>Float但是当我想直接访问Infinity或NaN时:Infinity#=>uninitializedconstantInfinity(NameError)NaN#=>uninitializedconstantNaN(NameError)什么是Infinity和NaN?它们是对象、关键字还是其他东西? 最佳答案 您看到打印为Infinity和NaN的只是Float类的两个特殊实例的字符串
如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象
关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?