算法导论 第二章-算法基础
插入排序
插入排序,对有限元素比较合适的算法。和打牌差不多。
伪代码
INSERTION-SORT[A]
for j ← 2 to length[A] // for 循环 j = 2, j < 数组A的length
do key ← A[j] // A[j] 的值 给变量 key
i ← j-1 // i = j - 1
while i > 0 and A[i] >key // 内层循环
do A[i+1] ← A[i]
i ← i-1
A[i+1] ← key
循环不变式用来理解算法的正确性
1.初始化:循环的第一次迭代之前,它为真。
2.保持:如果循环的某次迭代之前它为真,那么下次迭代之前它仍为真。
3.终止:在循环终止时,不变式为我们提供了一个有用的性质,该性质有助于证明算法是正确的
比如一个数组 [1, 22, 3, 4, 44, 55], 跟打牌一样,元素 A[1, j-1] 是手里的牌,[j, n] 是桌上的牌
数学归纳法,要证明某一性质正确,必须首先证明基本情况和一个归纳步骤都是正确的。
归纳法中,归纳步骤是无穷使用的。
就插入排序而言,证明一下。
初始化,当j=2,数组[1..j-1],就1个元素,显然是正确的
保持,证明没一次循环都能让循环不变式成立,在外层循环中,让A[j-1], A[j-2],A[j-3]向右移动,找到A[j]的合适位置。
终止,在j>n时,循环结束了,把j替换为n+1,子数组A[1, n]包含了原来A[1,n]的元素,现在排序好了,他就是整个数组,所以这是正确的。
算法的运行时间,是指在特定的输入时,所执行的基本操作数。
// 插入排序 js版本实现
function insertSort(arr) {
let len = arr.length;
for (let a = 1; a < len; a++) {
// 当前值,当前牌
let cur = arr[a];
let b = a - 1;
// 找到一个合适的位置,把cur插进去
// arr[b] 是排序好的最大的一个,如果这个不大于cur,那就不用排了
while (b > 0 && cur < arr[b]) {
arr[b + 1] = arr[b];
b--;
}
arr[b + 1] = cur;
}
return arr;
}
分析算法
- 分析算法,通常我们想度量的是时间。
- 我们一般假定是单处理器计算模型 RAM作为实现技术,指令一条一条执行,没有并发操作。
- RAM模型,包含真实计算机的指令,如算数指令(加减乘除等)、数据移动指令、控制指令,每条这样的指令需要的时间都是常数。
- 一般来说,算法的时间与输入规模同步增长。所以通常把一个算法的运行时间,描述成输入规模的函数。
- 输入规模的最佳概念依赖于研究的问题。
- 一个算法在特定输入上的运行时间,是执行的基本操作数或者步数。
最坏情况和平均情况分析
对于插入排序的分析。通过具体分析,在最优情况,即数组是排序好的情况下,运行时间可表示为
a*n+b
对于最差的情况,是
a * n的平方 + b * n + c
在一般情况下,我们是考察算法的最坏运行情况。
增长的量级
运行时间的增长率,或者叫做增长的量级,这样我们只考虑公式的最高次项,以为当n很大,低阶项就不重要了
另外,还要忽略最高次项的常数次数。例如,插入排序的最差时间代价是 Θ(n2)
如果一个算法的最坏运行时间比另外一个低,那么我们认为它的效率更高。
算法设计
插入排序使用的是,增量方法,在排好序的子数组插入元素。
分治法
分治策略是,把原问题划分为n个规模较小,结构与原问题相似的子问题。
递归解决子问题,然后合并结果得到原问题的解。
分治模式,在每一层递归上,都有3个步骤。
- 分解,把原问题划分为子问题。
- 解决,递归解决子问题,子问题足够小则直接解决。
- 合并,子问题的解合并为原问题的解。
以下是归并排序的伪代码
MERGE(A, p, q, r)
n1 = q-p+1
n2 = r-q
//let L[1....n1+1] and R[1....n2+1] be new arrays
for i =1 to n1
L[i] = A[p+i-1]
for j=1 to n2
R[j] = A[q+j]
L[n1+1] = ∞
L[n2+1] = ∞
i=1
j=1
for k =p to r
if L[i]<=R[j]
A[k] = L[i]
i = i + 1
else
A[k]=R[j]
j = j+1
MERGE-SORT(A,p,r)
if p < r
q =(p+r)/2 //向下取整,不会打那个符号
MERGE-SORT(A, p, q)
MERGE-SORT(A, q+1, r)
MERGE(A, p, q, r)
归并排序的关键是,合并两个已排序好的子序列。通过一个 MERGE(A,p,q,r)
来实现 p <= q <= r
前提是,子数组 A[p, q]
和 A[q+1, r]
是排序好的数组。通过合并这两个排序好的子数组来代替当前
的子数组 A[p,r]
;
以下是JavaScript的实现,开始并不好理解,可以打开debugger,一步一步的去看,就比较清晰了
function mergeSort(arr, start, end) {
if (start < end) {
let m = Math.floor((start + end) / 2);
mergeSort(arr, start, m);
mergeSort(arr, m + 1, end);
return merge(arr, start, m, end);
}
}
function merge(arr, start, mid, end) {
/*
这是原书的代码
// 创建左子数组
let n1 = mid - start + 1;
// 创建一个数组,size是子数组的长度+1
let L = new Array(n1 + 1);
for (let i = 0; i < n1; i++) {
L[i] = arr[start + i];
}
// 创建右子数组
let n2 = end - mid;
// 创建一个数组,size是子数组的长度+1
let R = new Array(n2 + 1);
for (let j = 0; j < n2; j++) {
R[j] = arr[mid + j + 1];
}
*/
// js代码可以很简单的实现上边的功能
let L = [...arr.slice(start, mid + 1), Infinity];
let R = [...arr.slice(mid + 1, end + 1), Infinity];
let i = 0, j = 0;
for (let k = 0; k < end - start + 1; k++) {
// debugger
if (L[i] <= R[j]) {
arr[start + k] = L[i];
i++;
} else {
arr[start + k] = R[j];
j++;
}
}
return arr;
}
分治法分析
当一个算法中含有递归调用时,运行时间可用一个递归方程表示。T(n)
如果问题规模足够小,如 n <= c(常量),那么表示为Θ(1)
把原问题分解为a个子问题,每个问题规模是原问题的 1/b, 分解问题是D(n) ,合并问题是C(n)
那么T(n) = aT(n/b) + D(n) + C(n)
归并算法的分析
分解,需要常量时间 D(n) = Θ(1)
解决, 递归解决两个规模是n/2的子问题,时间是2T(n/2)
合并,合并n的元素的子数组,merge时间是Θ(n), 所以C(n) = O(n)
所以带入 T(n) = aT(n/b) + D(n) + C(n) 就是
T(n) = 2T(n/2) + Θ(n) + Θ(n)
T(n) = 2T(n/2) + Θ(n)
我们用常数c代表Θ(1),于是得到 T(n) = 2T(n/2) + cn
递归树总的层数是 lgn+1
,每一层的代价是 cn
所以 T(n) = cn(lgn+1) = nlgn
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。