1.插入排序
JS实现如下:
function insertSort(arr){ for(var i=1; i<arr.length; i++) for(var j=i-1;j>=0 && arr[j]>arr[j+1];j--) [arr[j], arr[j+1]] = [arr[j+1], arr[j]]; return arr; }
插入算法的核心就是‘交换’二字。也就是代码中的第4行。
所以第4行的意思就是,如果arr[j]大于arr[j+1],那么交换这两个变量的值。其余行就没什么可说的了,都是循环体,返回语句一类的必要的东西。
实现过程中出现过的bug:
1 一开始没有使用j变量,直接用的i-1,结果很明显了,每次内部循环执行之后i--, 然后外部循环又i++,所以无限循环……火狐奔溃了……
2 原先测试的时候使用的是while而不是for,当然这不是重点,重点是判断的条件不是j>=0,而是arr[j]。我当时下意识地想啊如果arr[j]有意义那就执行循环体,当j<0的时候arr[j]变成undifined了就不执行了。稍微一想这实在是有点缺心眼,直接判断j>0不就行了。而且真要判断的话也不应该写成while(arr[j]),而是while(arr[j]!=undefined), 不然碰到数组里的0就直接漏过去了。嗯所以下意识的想法往往禁不起推敲。最后为了缩短行数把while循环改写成了for循环。
2 循环不变式
书中在介绍插入排序的正确性时拿出了这么一个概念: 循环不变式,英文原文是loop invarition。判断循环不变式的成立分为3个阶段,若3个阶段都成立则循环不变式成立。这3个阶段就是:循环开始前,循环进行时,循环结束后。
循环不变式代表的是: 为了使循环保证算法的有效性,必须在循环执行中一直保持为真值的某个条件,就好像是这样子:
for(每个循环):
if(!循环不变式)
return 算法不成立
看起来就像是一个表达式,所以中文翻译为 循环不变‘式’我觉得是有一定道理的。
具体到插入排序这个例子里: 循环体的作用是每次循环,调整数组中第0个数到第j个数的子数组的有序状态,使得其中的元素严格升序或降序排列。那么为了保证循环不变式,就需要证明,在循环之前,arr.[0]到arr[j]是有序的;在循环的每个步骤结束后,arr[0]到arr[j]是有序的;循环终止时,保证循环终止条件正确,使得整个数组是有序的。
在第一次循环开始之前,子数组仅有一个数,因此是有序的,故阶段一成立。
在每次循环迭代的过程中,如果arr[0]到arr[j-1]是有序的,我们的代码使得新加入的a[j]会依次移动到符合条件的位置,因此arr[0]到arr[j]也是有序的。因此阶段二成立。
循环结束时,判断条件为i=arr.length-1,此时已对arr[0]到arr[arr.length-1]进行排序,而这正是整个数组,故阶段三成立。
3 选择排序
来自于第二章的课后题。描述是:首选选出数组A中最小的数,然后与A[0]交换;接着找出第二小的数,与A[1]交换;直到遍历结束。
伪代码实现:
Selection Sort Method(A): for i=0 to i=A.length-2: for j=i to j=A.length-1: let S = smallest variable among A[j] to A[A.length-1] swap A[i] and S return A
JS实现
function selectionSort(N){ for(var i=0; i<N.length-1; i++){ var smallest=N[i],position=i; for(var j=i; j<N.length; j++) if(N[j]<smallest) [smallest,position] = [N[j],j]; [N[i], N[position]] = [N[position], N[i]]; } return N; }
选择排序的每次循环要实现的目的是保证A[0]到A[i]之间的数有序(并且均小于后面的任何一个数)。同样可以套用上面的步骤证明循环不变式的正确性。
4 归并排序
归并排序的概念大致上就是:假设有两组已经排好序的序列,比如[1,3,5,7]和[2,4,6,8],我要将它们合并,形成一个新的有序序列[1,2,3,4,5,6,7,8]。怎么做呢?很简单,就像两堆扑克牌,平放在桌面上,正面朝上,这时只能看到每一组的第一张,选出其中较小的一张放入队列。接下来再从能看见的两张里选出一张,放入队列……直到某一堆为空。
当然现实中往往没有现成的两组已经排好序的序列,比如是[1,5,3,7]和[2,6,4,8],这样就不能直接套用上面的方法了。不过我们正好发现,给定的两组序列正好各自符合上面的条件,那么我们对每一组序列采用上面的方法排好序,再对这两组序列进行排序就完工了。
如果还不是呢?那就继续往下划分。好在这样的划分是肯定能分到符合条件的情况的,因为划分到最后序列里只包含一个数的情况下那是肯定符合条件的。
书中给的伪代码如下:
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表示将两组有序序列合并的方法。书中介绍的MERGE方法使用了一个‘哨兵牌’来简化代码,即在每个数组后面加上了一个无穷大的数,这样就不必检测两个序列的数是否有一个被取完了。
根据上面的伪代码,JS实现如下:
function merge(A, p, q, r) { var L = [], R = []; L = A.slice(p, q+1); R = A.slice(q+1, r+1); L.push(Infinity); R.push(Infinity); var i = 0, j = 0; for(var k=p ; k<=r; k++) { if(L[i] < R[j]) { A[k] = L[i]; i++; } else { A[k] = R[j]; j++; } } } function mergeSort(A, p, r) { if(p < r) { var q = (p+r)>>1; mergeSort(A, p, q); mergeSort(A, (q+1), r); merge(A, p, q, r); } }
实现过程中出现的一个严重的乌龙:在声明q的时候忘记使用var语句,导致q成了一个全局变量,每次递归调用时递归函数都会改变q的值而传入父级递归函数,导致排序过程出现混乱。在我加上了无数console.log调试之后发现q的值变化过程和草稿纸上写的不一样,才发现了这个显而易见的错误,真是哭笑不得。
上面的MERGE实现过程采用了‘哨兵牌’来减少了代码的复杂程度。然而这样无形中会增加了算法的计算量。原本在MERGE函数里,当发现其中一段序列已经遍历完毕之后,应当直接将另一端序列取出即可。而采用了哨兵牌之后,在其中一列取完之后,另一列剩下的每一个数都要与INFINITY进行比较。这样来说上面的这个算法是不完美的。
其次MERGESORT过程也存在一定的问题,MERGESORT采用了很简洁的递归形式,寥寥数行代码就完成了整个过程,然而简洁的背后往往是复杂的计算。
举例来说,对于序列[1,3,5,7,2,4,6,8],采用归并排序,正常情况应该是这样的:
第一步,将序列分成左右两部分[1,3,5,7]和[2,4,6,8]
第二步,因两部分有序,合并[1,3,5,7]和[2,4,6,8]得到结果
而在这个算法里,实际上是这样的:
第一步, 将序列分成[1,3,5,7]和[2,4,6,8]
第二步, 将[1,3,,5,7]分成[1,3]和[5,7]
第三步, 将[1,3]分成[1],[3]
第四步,将[1] [3]合并成[1,3]
第五步, 将[5,7]分成[5]和[7]
...
第N步,合并[1,3,5,7] [2,4,6,8]得到[1,2,3,4,5,6,7,8]
所以这里是强制将数组划分为单元数组后再进行合并的。所以我认为其实上这个过程不用递归反而更加合适,直接将数组分割为一个个单独的数再进行合并岂不是省了好多步骤么。
5 插入排序递归版
function insertSort_R(A, p) { if(p>0) { insertSort_R(A, p-1); for(var i=p- 1; i>=0; i--) { if(A[p] < A[i]) { [A[p], A[i]] = [A[i], A[p]]; current--; } } } }
6 冒泡排序
这是许多人学习的第一个排序算法吧。记得当初觉得它好复杂的说。
function bubbleSort(A){ for(var i=0; i<A.length; i++){ for(var j=A.length-1; j>=i; j--){ if(A[j] < A[j-1]) [A[j],A[j-1]]=[A[j-1],A[j]]; } } }
7 分治法的应用 - 最大连续子序列和
才发现这个经典的问题原来出自这里。
最大子序列和问题: 给定一个序列,求出该序列的所有连续子序列中和最大连续子序列,并求出这个子序列的和。
要应用分治法,首先要将子序列分类。这里我们从元序列中取一个坐标i,然后将所有 子序列分为3类: 第一类,所有元素的坐标均<=i; 第二类,所有元素的坐标均>i; 第三类,即存在<i又存在>i的元素的子序列。
分别求出这三类子序列中的最大成员,然后取其最大者即可得到答案。
光看代码可能有些不太好理解,这里举个实际的例子就好说多了:
给定序列A = [ -6,2,4,-7,5,3,2-1,6-9,10,-2]
选取A[5] = 3 作为中点,将序列分为两部分[ -6,2,4,-7,5,3,2-1,6,-9,10,-2]
第一类, 求序列[-6,2,4,-7,5,3]的最大子序列
选取4为分割点,将其分割为两个部分[-6,2,4,-7,5,3]
第一类, 求序列[-6,2,4]的最大子序列
严格来说还要继续往下分隔,不过这里已经足够简单了,可以目测出最大子序列为[2,4] = 6
第二类, 求序列[-7,5,3]的最大子序列
最大子序列为[5,3] = 8
第三类, 求包含两部分的最大子序列
最大子序列为[2,4,-7,5,3] = 7
综上可以得到左侧的最大子序列[5,3] = 8
第二类, 求序列[2,-1,6,-9,10,-2]的最大子序列
选取6为分割点,将其分为两列[2,-1,6,-9,10,-2]
第一类 求[2,-1,6]的最大子序列
可以看出结果为[2,-1,6] = 7
第二类 求[-9,10,-2]的最大子序列
可以看出结果为[10] = 10
第三类 求包含两部分的最大子序列
可以看出结果为[2,-1,6,-9,10] = 8
综上可得到右侧最大子序列[10] = 10
第三类, 求包含两部分的最大子序列
这个子序列的左边部分必须包含3,所以往左延伸就好了,不难看出3+5是最大值; 右边部分必须包含2,不难看出2-1+6-9+10是最大值
可以求出这个分类最大子序列为[3,5,2,-1,6,-9,10] = 16
最后,取三者中最大,得到最大子序列[3,5,2,-1,6,-9,10] = 16
下面是JS代码
function maxSubarrayCross(A, low, mid, high){ var leftSum = -Infinity, rightSum = -Infinity; var sum = 0; var left = mid, right = mid; for(var i=mid; i>=low; i--){ sum += A[i]; if(sum > leftSum){ leftSum = sum; left = i; } } sum = 0; for(var j=mid+1; j<=high; j++){ sum += A[j]; if(sum > rightSum){ rightSum = sum; right = j; } } return [left, right, leftSum+rightSum]; } function maxSubarray(A, low ,high){ if(high == low) return [low, high, A[low]]; else { var mid = low+high >> 1; var leftResult = maxSubarray(A, low, mid); var rightResult = maxSubarray(A, mid+1, high); var crossResult = maxSubarrayCross(A, low, mid, high); if(leftResult[2] > rightResult[2] && leftResult[2] > crossResult[2]) return leftResult; else if(rightResult[2] > leftResult[2] && rightResult[2] > crossResult[2]) return rightResult; else return crossResult; } }
--to be continue