算法分析-分治 归并排序,递归插入排序,二分查找
反正分治的套路就是 相同子问题,递归做,我之前有介绍express源码,其中的中间件使用就是用next()函数一直递归,想看的看我的express源码分析:
分治3步骤:
- 分解
- 处理
- 归并
下面给出归并排序的js代码:
1 var A = [5, 2, 4, 6, 1, 3]; 2 var len = A.length - 1; 3 4 MERGE_SORT(A, 0, len); 5 6 A.forEach(function (element, index, arr) { 7 console.log(index, "-----------", element); 8 }); 9 10 function MERGE_SORT(A, start, end) { 11 if (start < end) { 12 var middle = Math.floor((start + end) / 2); //向下去整 13 MERGE_SORT(A, start, middle); //左边递归 14 MERGE_SORT(A, middle + 1, end); //右边递归 15 MERGE(A, start, middle, end); 16 } 17 } 18 19 function MERGE(A, start, middle, end) { 20 var Arr1 = A.slice(start, middle + 1), 21 Arr2 = A.slice(middle + 1, end + 1),//slice(start,end) end不包括,所以加1 22 len1 = Arr1.length, 23 len2 = Arr2.length, 24 i = 0, 25 j = 0; 26 for (i, j; i < len1 && j < len2;) { 27 (Arr1[i] < Arr2[j]) ? (A[start++] = Arr1[i++]) : (A[start++] = Arr2[j++]); 28 } 29 30 while (i == len1 && j < len2) { 31 32 A[start++] = Arr2[j++]; 33 } 34 while (j == len2 && i < len1) { 35 A[start++] = Arr1[i++]; 36 } 37 38 }
像这种分治递归的,你需要找到一个出口来结束递归。
还记得插入排序吗,我们换成递归的写法,怎么写呢,思路是这样的,将A[n],插入A[0...n-1]内,而A[0..n-1]是已经排序好的。上代码:
1 var A = [5, 2, 4, 6, 1, 3]; 2 3 RECURSIVE_INSERT_SORT(A, 6); 4 A.forEach(function (element, index, arr) { 5 console.log(index, "-----------", element); 6 }); 7 8 function RECURSIVE_INSERT_SORT(A, len) { 9 if (len > 1) { 10 var last_var = A[len - 1]; 11 A.splice(--len, 1); 12 RECURSIVE_INSERT_SORT(A, len); 13 INSERT(A, last_var); 14 } 15 } 16 17 function INSERT(A, last_var) { 18 var len = A.length, 19 k = len - 1; 20 while (A[k] > last_var && k >= 0) { 21 22 A[k + 1] = A[k]; 23 k--; 24 } 25 A[k + 1] = last_var; 26 27 }
那思考一下,如果A=[1,2,3,5,6,7]是有序的,用二分查找去查找v=3,v=4,怎么写,有思路吗?
1 var result = {index: false}, 2 A = [1, 2, 3, 5, 6, 7]; 3 function BINARY_SEARCH(A, start, end, val) { 4 if (end >= start) { 5 var middle = Math.floor((start + end) / 2); 6 if (A[middle] == val) { 7 result.index = middle; 8 console.log("找到了~"); 9 return; 10 } 11 if (A[middle] < val) { 12 13 BINARY_SEARCH(A, middle + 1, end, val); 14 } 15 if (A[middle] > val) { 16 BINARY_SEARCH(A, start, middle - 1, val); 17 } 18 } 19 } 20 21 BINARY_SEARCH(A, 0, 5, 7); 22 console.log(result.index);
既然我们已经学会了二分查找策略,我们能不能将插入排序再改一改,将while循环改成二分查找策略呢?
1 var A = [99, 12, 77, 103, 1000, 3, 11, 4324, 3, 321, 545, 65, 76765, 78, 889, 98, 324, 23, 4, 544, 6, 2]; 2 3 BINARY_INSERT_SROT(A); 4 A.forEach(function (element, index, arr) { 5 console.log(index, "-----------", element); 6 }); 7 8 function BINARY_INSERT_SROT(A) { 9 var len = A.length; 10 for (var i = 1; i < len; i++) { 11 var key = A[i]; 12 var j = i - 1; 13 14 /* 15 * 16 * A是要修改的数组对象 17 * 0表示已经排号序列的start下标, 18 * j表示已经排号序列的end下标, 19 * key 表示需要插入的值,也就是第五个参数need_insert_index下标对应的值 20 * need_insert_index 就是我们要插入的key值的下标。也就是j+1; 21 * 22 * */ 23 BINARY_SEARCH(A, 0, j, key, j + 1); //这里替换掉while(key<A[j]&&j>=0) 24 } 25 } 26 function BINARY_SEARCH(A, start, end, val, need_insert_index) { 27 28 /* 29 * 30 * 我们知道二分查找是查找中间下标对于的值是否为所求,是就找到,不是,就对半再递归查找。 31 * 32 * 这一步在我们这个程序里还是需要的,毕竟可能会直接找到相同的,找到后,我们默认将值插在后面, 33 * 34 * 这里有个关键步骤,也就是第5个参数的用处,我们是通过对比查找的。我们如果往后移动,也一定要在 35 * 36 * 这个middle下标到need_insert_index下标之间全部移动,不然可能只移动了一部分。 37 * 38 * 当start==end的时候,或者start + 1 ==end的时候,middle都等于stsrt,这时候,其实都是比较一个数, 39 * 40 * 下标的移动和middle这个找到的特殊情况一样,当找不到的时候根据条件去移动。 41 * 42 * */ 43 44 45 if (end >= start) { 46 var middle = Math.floor((start + end) / 2); 47 if (A[middle] == val) { 48 var j = middle; 49 while (need_insert_index > middle) { 50 A[need_insert_index] = A[need_insert_index - 1]; 51 need_insert_index--; 52 } 53 A[middle + 1] = val; //默认是插后的 54 return; 55 } 56 if (middle == start) { 57 if (A[start] <= val && A[end] >= val) { 58 while (need_insert_index > start) { 59 A[need_insert_index] = A[need_insert_index - 1]; 60 need_insert_index--; 61 } 62 A[start + 1] = val; 63 64 } else if (A[start] <= val && A[end] <= val) { 65 while (need_insert_index > end) { 66 A[need_insert_index] = A[need_insert_index - 1]; 67 need_insert_index--; 68 } 69 A[end + 1] = val; 70 } else { 71 while (need_insert_index > start) { 72 A[need_insert_index] = A[need_insert_index - 1]; 73 need_insert_index--; 74 } 75 A[start] = val; 76 } 77 return 78 } 79 (A[middle] > val) ? (BINARY_SEARCH(A, start, middle - 1, val, need_insert_index)) : (BINARY_SEARCH(A, middle + 1, end, val, need_insert_index)); 80 } 81 }
思考:请给出一个运行时间为O(nlgn)的算法,使之能在给定一个由n个整数构成的集合S和另一个整数x时,判断出S中是否存在有两个其和等于x的元素。
分析:
若要整个算法的时间复杂度为O(nlgn),那么只要算法中最复杂的模块的复杂度为O(nlgn)就可以了。
1 var A = [99, 12, 77, 103, 1000, 3, 11, 4324, 3, 321, 545, 65, 76765, 78, 889, 98, 324, 23, 4, 544, 6, 2], 2 result = {index: false}; 3 var isfind = isExisted(A, 89); 4 console.log(isfind); 5 6 function isExisted(A, val) { 7 var len = A.length; 8 //先给集合排序 9 MERGE_SORT(A, 0, len - 1); 10 11 //循环次数最多为n 12 for (var i = 0; i < len; i++) { 13 //每次次二分搜索,时间消耗lgn 14 BINARY_SEARCH(A, 0, len - 1, val - A[i]); 15 if (result.index) { 16 //下面这个判断是考虑到了x的值是序列中某个元素的2倍的情况 17 if (result.index != i) { 18 result.index = true; 19 break; 20 }else { 21 result.index = false; 22 } 23 } 24 } 25 return result.index; 26 } 27 28 29 //复制粘贴 30 function BINARY_SEARCH(A, start, end, val) { 31 if (end >= start) { 32 var middle = Math.floor((start + end) / 2); 33 if (A[middle] == val) { 34 result.index = middle; 35 console.log("找到了~"); 36 return; 37 } 38 if (A[middle] < val) { 39 40 BINARY_SEARCH(A, middle + 1, end, val); 41 } 42 if (A[middle] > val) { 43 BINARY_SEARCH(A, start, middle - 1, val); 44 } 45 } 46 } 47 48 //复制粘贴 49 function MERGE_SORT(A, start, end) { 50 if (start < end) { 51 let middle = Math.floor((end + start) / 2); 52 MERGE_SORT(A, start, middle); 53 MERGE_SORT(A, middle + 1, end); 54 MERGE(A, start, middle, end); 55 } 56 } 57 58 //复制粘贴 59 function MERGE(A, start, middle, end) { 60 var arr1 = A.slice(start, middle + 1); 61 var arr2 = A.slice(middle + 1, end + 1); 62 var len1 = arr1.length; 63 var len2 = arr2.length; 64 var i = 0; 65 var j = 0; 66 for (i, j; i < len1 && j < len2;) { 67 (arr1[i] < arr2[j]) ? (A[start++] = arr1[i++]) : (A[start++] = arr2[j++]); 68 } 69 while (i == len1 && j < len2) { 70 A[start++] = arr2[j++]; 71 } 72 while (j == len2 && i < len1) { 73 A[start++] = arr1[i++]; 74 } 75 }
总结:
merge_sort这个函数是归并排序算法,它的时间复杂度是O(nlgn)。
第12行处,这个for循环中,有一个二分查找算法。它的时间复杂度是O(lgn),所以整个for循环模块的时间复杂度为O(nlgn)。
霍纳规则的正确性:
公式的简单推理:
a0+a1*x+a2*x^2+a3*x^3+a4*x^4+…+ak*x^k+…+an*x^n
计算机的循环计算。
1 y = 0 时间花费 1
2 for i=n down to 0 n+1
3 y = ai + x*y n
总时间花费 2n+2
这样循环计算出来的y就是上面汇总的值。
a)、Θ(n), 推理过程看上面。
b)、伪代码实现的朴素的多项式求值算法。
下面是一个取巧的算法,时间消耗是 3n, 在n >2 时 时间消耗大于 2n+2
void Ploynomial() 时间消耗 = 3n
{
int t; 1
sum = a[0]; 1
for (i = 1; i < n; i++) n
{
sum += a[i]*x; n-1
x = x*x; n-1
}
}
c)、
初始化: 有 y = 0, i = n , 这样 计算 下面公式的右边 为 0 , 所以初试化满足循环不变式。
保持:假设当第i=j满足时,考察i=j-1。
终止: 当循环结束时候,有 i= -1,
------------
由于0从0到n-(i+1),因此有:
y = Σ ak+i+1 * x^k
= ak+i+1 + ak+i+2 * x + ... + an * x^(n-(i+1))
霍纳规则代码段循环不变式证明如下:
初始:
i=n,y[n] = 0,迭代开始时,循环后有y[n] = a[n]。
保持:
对于任意 0 ≤ i ≤ n,循环后有:
y[i] = a[i] + y[i+1] * x = a[i] + (a[i+1] * x + a[i+2] * x + ... + a[n] * x^(n-(i+1))) * x
= a[i] + a[i+1] * x + a[i+2] * x^2 + ... + a[n] * x^(n-i)
终止:
i小于0时终止,此时有 y[0] = a[0] + a[1] * x + a[2] * x^2 + a[n] * x^n
证明和y = Σ a[k+i+1] * x^k的关系:
k 从0到n-(i+1),等价于 0 ≤ k ≤ n-(i+1)。因此
y = Σ a[k+i+1] * x^k
= a[i+1] + a[i+2] * x + ... + a[n-(i+1)+i+1] * x^(n-i)
= a[i+1] + a[i+2] * x + ... + a[n] * x^(n-i)
由于i+1循环之后和i循环之前的值相等,用y'[i]表示i循环之前的值,则有:
y'[i] = y[i+1]
霍纳规则循环不变式的结果表明:
y[i] = a[i] + a[i+1] * x + a[i+2] * x^2 + ... + a[n] * x^(n-i)
因此有:
y'[i] = y[i+1] = a[i+1] + a[i+2] * x + ... + a[n] * x^(n-(i+1))
令k=n-(i+1),则n=k+i+1,所以:
y'[i] = a[i+1] + a[i+2] * x + ... + a[k+i+1] * x^(k+i+1-(i+1))
= a[i+1] + a[i+2] * x + ... + a[k+i+1] * x^k
用y表示y'[i],则有:
y = a[i+1] + a[i+2] * x + ... + a[k+i+1] * x^k
= Σ a[k+i+1] * x^k
其中 k从0到n-(i+1)
证毕。
思考题:逆序对
设A[1..n]是一个包含n个不同数的数组。如果i<j且A[i]>A[j],则(i,j)就称为A中的一个逆序对(inversion)。
a)列出数组〈2,3,8,6,1〉的5个逆序。
b)如果数组的元素取自集合{1, 2, ..., n},那么,怎样的数组含有最多的逆序对?它包含多少个逆序对?
c)插入排序的运行时间与输入数组中逆序对的数量之间有怎样的关系?说明你的理由。
d)给出一个算法,它能用Θ(nlgn)的最坏情况运行时间,确定n个元素的任何排列中逆序对的数目。(提示:修改合并排序)
a) (2,1) (3,1) (8,1) (6,1),(8,6)
b) 数组从大到小有序排列时,逆序对最多,为n(n-1)/2个。
c) 逆序对增加时,插入排序时间增加。
没有逆序对时,插入排序时间最少,为Θ(n)。
逆序对最多时,插入排序时间最多,为Θ(n^2)。
d) 归并算法, 每次移动牌,次数加1, 合计的次数就是逆序对的个数。
给出修改后的程序:
1 var num = 0,A = [5,2,4,6,1,3]; 2 MERGE_SORT(A, 0, A.length - 1); 3 console.log("最终结果是:",num); 4 5 function MERGE_SORT(A, start, end) { 6 if (start < end) { 7 let middle = Math.floor((end + start) / 2); 8 MERGE_SORT(A, start, middle); 9 MERGE_SORT(A, middle + 1, end); 10 MERGE(A, start, middle, end); 11 } 12 } 13 14 function MERGE(A, start, middle, end) { 15 var arr1 = A.slice(start, middle + 1); 16 var arr2 = A.slice(middle + 1, end + 1); 17 var len1 = arr1.length; 18 var len2 = arr2.length; 19 var i = 0; 20 var j = 0; 21 for (i, j; i < len1 && j < len2;) { 22 /* (arr1[i] < arr2[j]) ? (A[start++] = arr1[i++]) : (A[start++] = arr2[j++]);*/ 23 if (arr1[i] > arr2[j]) { 24 num += (len1 - i); 25 A[start++] = arr2[j++]; 26 27 } else { 28 A[start++] = arr1[i++] 29 } 30 } 31 while (i == len1 && j < len2) { 32 A[start++] = arr2[j++]; 33 34 } 35 while (j == len2 && i < len1) { 36 A[start++] = arr1[i++]; 37 38 } 39 console.log(A, "对应的num数目:",num); 40 41 }
分析:
我们使用归并排序,其实递归执行的时候 ,是从左往右的,大家可以画图:
[5,2,4,6,1,3]
[5,2,4] [6,1,3]
[5,2] [4] [6,1] [3]
[5] [2] [6] [1]
我们发现,当归并[5] [2] 的时候,因为左边大于右边,所以数目加1,归并后变成[2,5],[2,5]和[4]归并,因为5大于4,数目加1变为2,
归并后为[2,4,5],同理分析右边,最后归并[2,4,5] [1,3,6] 这里是关键,也就是为什么代码中是绿色的部分,因为2大于1,所以2后面的所有都大于1
4大于3,4后面的全大于3.