外星人源码论坛 首页 编程经验 算法基础 第〇章 通论

算法基础 第〇章 通论

2018-3-5 06:54
原作者: 外星人源码网 来自: 外星人源码网 收藏 分享 邀请

参考书就是那本算法导论(别问我是哪本). 有人说,程序就是数据结构加上算法. 照这么看, 算法应该是定义在数据结构上的操作. 算法接受一系列的数据作为参数, 并输出一些我们想要的数据, 这便是算法的意义. 算法的度 ...

参考书就是那本算法导论(别问我是哪本).

有人说,程序就是数据结构加上算法. 照这么看, 算法应该是定义在数据结构上的操作. 算法接受一系列的数据作为参数, 并输出一些我们想要的数据, 这便是算法的意义.

算法的度量

标准计算模型

很多情况下, 我们研究算法不只是解决从0到1的问题, 而是解决从 1 到 100 的问题. 比如, 很多情况下我们需要一些优秀的算法来减少某些资源的使用, 比如减少内存的使用, 更多情况下我们考虑的是算法的时间性能. 也就是说, 很多情况下, 我们希望拿到处理同样的数据而使用时间短一些的算法. 当然, 你也可以花更多钱购置更多性能更高的机器, 加速自己手头的工作. 但更经济有效的方法, 是研究出更优秀的算法.

我们假定我们的优化目标是运行时间, 在保证结果正确的前提下, 我们更青睐运行时间短的算法. 这就涉及到一套公正的测试标准来衡量算法的性能.

在哪一台机器上跑作为标准呢? 难道我们计算机科学也要想 SI 学习, 搞一套标准服务器放在博物馆里, 供大家测性能吗? 这显然是不现实的. 因此, 我们虚拟一台计算机, 以在它上面算法"运行时间"作为标准. 这台计算模型叫做随机访存机(RAM). 在他上面支持的运算有: +, -, *, /, 2的幂, 取余, 取整, 位运算等基本算术运算; 条件判断, 循环, 函数调用等结构; 以及随机地址访存, 数据移动语句. 每条语句用伪代码表示, 每条语句执行时间为某一常数. 你可能觉得乘法和除法怎么能跟加减法相提并论? 或者有人故意在一条语句中写了特别多的操作, 这条语句还是常数时间吗? 当然这些问题很有道理, 而我们不会再继续严格约束伪代码的写法, 但写伪代码的时候自己应该注意. 什么样的语句可以单独拿出来作为一条常数时间指令自己心里应该清楚.

具体度量方法

我们以一段简单的代码为例:

//A SIMPLE DEMO     // 执行时间
i = 1               // c1
while i <= n       // c2*(n+1)
    A[i] += B[i]    // c3*(n)
    B[i] /= C[i]    // c4*(n)
    if A[i] > C[i]  // c5*(n)
    then ++ B[i]    // ti*c6*(n)
    ++ i            // c7*(n)

每条语句后面是它的执行时间, 其中需要注意的几个地方. while 这条语句要比循环内的其他语句多执行一次, 因为最后有一次离开循环体的判断(即 i == n+1). 判断语句 if 的执行时间是不确定的, 因此前面乘了一个系数 ti, ti 的取值与 i 有关(因为是 i 的循环内), 只能取 0 或 1. 这个 ti 和具体执行情况有关, 我们无法预测.

于是总的运行时间 $$ T = c_1 + c_2*(n+1) + (c_3+c_4+c_5+c_7)*n + ti*c_6*n $$

由于 T 的取值是不确定的, 所以如果我们要分析这个算法还要做进一步的近似.

最好的情况便是判断语句总不成立, 这样就会避免执行 if 块内的语句. 这个情况我们称之为最佳运行时间

$$ T_Best = c_1 + c_2*(n+1) + (c_3+c_4+c_5+c_7)*n $$

同样还有最差运行时间

$$ T_Worst = c_1 + c_2*(n+1) + (c_3+c_4+c_5+c_6+c_7)*n $$

如果我们对判断情况做粗略的平均, 即我们假定判断成功和失败是等概率的, 那么有平均运行时间

$$ T_Average = c_1 + c_2*(n+1) + (c_3+c_4+c_5+c_6/2+c_7)*n $$

在这个例子中, 这三个时间其实相差不大, 整理一下得到:

$$ T = A*n + B$$

A, B 是常数. 将那么多的 ci 加到一起粗略的得到某个常数, 是因为我们并不关心是 2n 还是 200n,
我们只关心数量级 n, 将其记作:

$$ T = \Theta (n)$$

粗略的意思是: T 与 n 同量级.

渐进记号

我们在前面知道了, 再研究算法的性能时我们考虑的是运行时间 T 关于输入规模 n 的量级, 表示为:

$$ T = \Theta (f(n)) $$

各个算法进行性能比较的时候, 我们就看 f(n) 以及 n 的取值范围. 除了这个记号, 通常还有另外几个记号.

$$ T = O(f(n)), T = \Omega(f(n)) $$

前者读做作"大O", 意思是存在一个常数 C, 使得当 n 足够大以后, C*f(n) 总比 T(n) 的值要大; 后者恰恰相反. 也就是说, O(f(n)) 描述了算法再差不会超过这个量级, 而后者描述了算法最好也就是这个量级. 一般的, O(f(n)) 是最常用的记号, 因为平均运行时间往往和最差运行时间同量级. 用 f(n) 更有意义.

到这里你可能有些疑惑, 如果我把 f(n) 定义成这样:

$$ T(n) = O(n^n) $$

我们通常见到的算法, 在 n 很大以后, 基本上不会超过这个量级. 如果是考试, 我全写这样的 f(n), 岂不是躺着就能及格吗? 因此我们虽然在定义上不做限制, 但在具体分析的时候, 还是要保证 f(n) 尽可能准确地表述 T(n) 的量级.

递归式的分析

根据定义我们可以轻松地计算出串行程序, 或者中间夹杂着循环的程序的运行时间. 那如果程序中含有递归调用怎么办呢?

function foo (val, m, n)
    if m-n <= 1
    then return val
    
    val_next = foo(val, m, (m+n)/2) + foo(val, (m+n)/2+1, n)
    return val+val_next

这样的递归代码, 假设输入规模是 n , 我们可以将其时间写作

$$ T(n) = C_0, (n <= 1) $$
$$ T(n) = C_1*T(n/2)+C_2*T(n/2)+C_0, (n > 1)$$

这便是递归形式的运行时间, 我们再用一些数学技巧将其非递归化. 观察得知, 从 n 一直除以 2, 分解到最后的 1. 这个过程可以近似的看成一棵 n 个叶的完全二叉树, 这个树的高度就是运行时间(如果能完全并行化).

$$ T(n) = O(log_2(n)) $$

记为:

$$ T(n) = O(lg(n)) $$

2017.9.14
Osinovsky


*有一个好玩的地方在于, Omega 这个字母的名称, 本意就是"大O".


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

粉丝 阅读111 回复0
上一篇:
vue webpack多页配置发布时间:2018-03-05
下一篇:
内存泄露实例发布时间:2018-03-05
推荐资讯
阅读排行
国内最专业的源码技术交流社区
全国免费热线电话

0373-5171417

周一至周日9:00-23:00

反馈建议

admin@eenot.com 在线QQ咨询

扫描二维码关注我们