参考书就是那本算法导论(别问我是哪本).算法
有人说,程序就是数据结构加上算法. 照这么看, 算法应该是定义在数据结构上的操做. 算法接受一系列的数据做为参数, 并输出一些咱们想要的数据, 这即是算法的意义.服务器
不少状况下, 咱们研究算法不仅是解决从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 的取值是不肯定的, 因此若是咱们要分析这个算法还要作进一步的近似. code
最好的状况即是判断语句总不成立, 这样就会避免执行 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".