近期除了做专题以外,每天都要抽出时间学习算法和计算机网络的知识,因为感觉这2个方面比较薄弱。
算法上除了通过google以外,主要就是看《学习javascript数据结构与算法》和《javascript高级程序设计》。
这篇文章主要就是归纳了5种常见的javascript的排序算法。
本来书中是有6种排序算法,但是堆排序我并没有很理解。
打算等专题做完后再花时间好好的学习基础的算法。
1.冒泡排序
冒泡排序是所有排序算法种最简单的一个,但是性能是最差的。
下面是初步的冒泡排序方法。
javascript代码
var arr = [1,5,4,3,2], // [n1,n2,n3,n4,n5]
len = arr.length;
for (let i = 0; i < len; i++) { // 外循环
for (let j = 0; j < len-1; j++) { // 内循环
if (arr[j] > arr[j+1]) { // 如果前一项大于后一项,就调换位置。
var n = arr[j];
arr[j] = arr[j+1];
arr[j+1] = n;
}
};
};
console.log(arr); // [1,2,3,4,5]
冒泡排序原理:
1. 外循环是根据数组长度,比如数组长度为5,就表示外循环有5个迭代,每个迭代都包含一个内循环。即执行5次内循环。
一共进行了5次内循环,第一轮内循环过程分析:
1. 首先arr的第一项和第二项比较,如果第一项大于第二项,调换位置。即这2项的最大值放在了第二项的位置。
2. 接下来第二项和第三项进行比较,如果第二项大于第三项,调换位置。即第二项和第三项的最大值放在第三项位置,这就能得出前三项的最大值,并放在第三的位置上。
3. 接下来是第三项和第四项......以此类推,就能得出[*,*,*,*,5](*代表不确定的数字)。最大的一项,也就是5放在了最后一个位置。
然后是第二次内循环,重复上面的步骤,就能够得出[*,*,*,4,5]的结果。每一次内循环就能得出一个数字在arr中的位置。
所以我们根据arr的长度进行循环,就能得出[1,2,3,4,5]的结果。
原理很简单,但其实这是有缺陷的,因为有些步骤是没必要进行的。
以下是改进后的冒泡排序
var arr = [1,5,4,3,2],
len = arr.length;
for (let i = 0; i < len; i++) { // 外循环
for (let j = 0; j < len-1-i; j++) { // 内循环
if (arr[j] > arr[j+1]) { // 如果前一项大于后一项,就调换位置
// var n = arr[j];
// arr[j] = arr[j+1];
// arr[j+1] = n;
[arr[j],arr[j+1]] = [arr[j+1],arr[j]]; // ES6语法
}
};
};
console.log(arr); // [1,2,3,4,5]
主要有2个不同,第一是运用了ES6的结构赋值,简化了代码。
第二是将len-1改成了len-1-i,我们来分析为什么这样做。
上面一种方法,一共数组有5项。在第一次内循环的时候,两两比较,共进行了4次比较,才得出了[*,*,*,*,5]的结论。
然后进行第二次内循环,还是进行了4次比较,得出[*,*,*,4,5]。
这不是最优解,当第四项和第五项进行比较,得出4 < 5,最后将4放在了第四项的位置上。
这一步完全可以忽略。因为在第一次内循环中,我们早已经得出5是最大值,放在最后一项的结论。
我们只要比较前四项,两两比较,进行3次。就能得出4的位置。
同理在第三次内循环中,我们因为在前2次内循环得到了4和5分别在最后2项的结论,所以只要比较1,2,3就够了。也就是2次比较。
我们发现每进行一次内循环,比较的次数就-1。
所以我们在内循环len-1的基础上,添加为len-1- i(因为每个迭代 i 都会比上一个迭代+1)。
2.选择排序
选择排序和冒泡排序一样,复杂度为O(n^2),都包含有嵌套的2个循环。
javascript代码
var arr = [5,4,1,3,2],
len = arr.length,
minIndex;
for (var i = 0; i < len-1; i++) { // 外循环
minIndex = i;
for (var j = i; j < len; j++) { // 内循环
// 如果原来的最小值比j位置的值小,就将原来的最小值转换为j位置的值,即新的最小值,继续比较。
if (arr[minIndex] > arr[j]) {
minIndex = j;
}
}
if (i !== minIndex) {
// var n = arr[minIndex]; // 最小值
// arr[minIndex] = arr[i];
// arr[i] = n;
[ arr[i] , arr[minIndex] ] = [ arr[minIndex] , arr[i] ]; // ES6语法
}
}
console.log(arr) // [1,2,3,4,5]
选择排序原理:找到数组中的最小值并将它放在第一位,接着找到第二小的放在第二位,以此类推。
外循环表示内部代码执行次数为数组长度-1次。因为当得出前面数字的位置时,我们自然知道最后一项要放在哪里。
当第一次外循环的时候,我们比较数组中的最小值。
通过minIndex = i,我们得到arr[0],也就是第一项的值,如上例,就是5。
然后我们将它和数组内的元素进行比较,如果碰到比它小的,就 minIndex = j。
如上例,当5碰到4的时候,发现它更小,通过minIndex = j,这样arr[minIndex] = 4。
这样就将原来的最小值转换为新的最小值(比原来更小)。
然后以此类推,继续比较。只要碰到更小的,就通过minIndex = j,获得那个更小的值的位置 j 和值。
当第一次内循环结束,我们就得到最小值的位置和值。
然后接下来判断这个最小值的位置是不是和原来的最小值arr[0]一样,如果arr[0]为最小值,我们什么都不用做。
如果不是的话,就将它们调换一下位置,这样数组中的最小值就放在了第一项。
然后i=1,开始第二次外循环,我们获取整个数组中的第二小值放在第二项,以此类推。
3.插入排序
插入排序在理解上会比上面2个难点。
javascript代码
var arr = [3,5,1,4,2],
len = arr.length,
j,
temp;
for (let i = 1; i < len; i++) { // 外循环
j = i;
temp = arr[j];
while (j > 0 && arr[j-1] > temp) { // 内循环
arr[j] = arr[j-1];
j--;
}
arr[j] = temp;
}
console.log(arr); // [1,2,3,4,5]
插入排序原理:假定第一项已经排序,接着,它和第二项进行比较。然后条件判断,第二项是应该呆在原地还是插入到第一项之前?这样就能正确排序前两项了。接着再和第三项比较(判断它是该插入第一、第二还是第三的位置)。
1. 首先我们假设第一项已经排列好了,然后开始第一次外循环。这时候 j = 1,我们获取数组中第二项的值5,将它和第一项3进行比较。
2. [3,5,1,4,2],我们很容易就得出3 > 5 === false,跳过了这次内循环,位置不变。
3. 然后开始第二次外循环,这时候 j = 2。我们获取第三项的值1,将它和第二项进行比较,发现5 > 1成立。所以我们将arr[1]的值,赋给了arr[2],也就是第三项变成了5。[3,*,5,4,2]。
4. 其实这时候*是5,但因为我们已经通过temp记录下1的值,所以在最后arr[ j ] = temp就行了。
5. 接下来我们将1的值和3进行比较。因为 j--,这时候j=1。我们比较arr[j-1]和temp的大小(其实就是3和1)。因为3 > 1成立,所以我们将3放在了2的位置上,内循环结束。[3,3,5,4,2]
5. 最后一步arr[ j ] = temp。因为3已经找到了新的位置,所以我们将原来的位置给了1。这样前三项就能很好的排序了。然后再执行下一次外循环,以此类推。
插入排序的算法比选择排序和冒泡排序性能更好,但是还是太复杂了。
4.归并排序
归并算法是一种分治算法,有不错的性能。火狐浏览器就是使用归并排序来实现sort方法。
其思想是将原始的数组切分成较小的数组,直到每个数组只有一个位置。通过比较,将小数组归并成大数组,直到最后一个排序完成的大数组。
因为是分治算法,所以归并算法需要用到递归。
javascript代码
var arr = [4,1,3,2];
console.log(mergeSort(arr)); // [1,2,3,4]
function mergeSort (arr) { // 将数组拆分成一个位置的小数组
var len = arr.length;
if (len === 1) {
return arr;
}
var mid = Math.floor(len/2),
left = arr.slice(0,mid),
right = arr.slice(mid,len);
return merge(mergeSort(left),mergeSort(right));
};
function merge(left,right) { // 将多个一个位置的小数组合并成一个大数组
var result = [],
n1 = 0,
n2 = 0;
while (n1 < left.length && n2 < right.length) {
if (left[n1] < right[n2]) {
result.push(left[n1++]); // push进left[n1],n1 = n1 + 1;
} else {
result.push(right[n2++]);
}
}
while (n1 < left.length) {
result.push(left[n1++]);
}
while (n2 < right.length) {
result.push(right[n2++]);
}
return result;
}
归并排序主要分成2部分,一个是拆分,一个是合并。这2个过程分别在2个函数中。
首先来分析怎么拆分。
javascipt代码
var arr = [4,1,3,2];
function mergeSort (arr) { // 将数组拆分成一个位置的小数组
var len = arr.length;
if (len === 1) { // 递归终止条件
return arr;
}
// 将大数组拆分成2个小数组
var mid = Math.floor(len/2),
left = arr.slice(0,mid),
right = arr.slice(mid,len);
return merge(mergeSort(left),mergeSort(right));
};
我们通过mergeSort函数进行拆分,因为是递归,首先设定递归终止条件,即数组长度为1,这表示大数组已经拆分完毕。
如果长度不为1,我们就将大数组依照中间位置,拆分成left和right两个小数组。直到数组只剩下一个位置为止。
分析上面例子:
1. 传入数组[4,1,3,2],再判断终止条件,数组长度不为1,继续执行。
2. 依照中间位置拆分成left,right两个小数组 left = [4,1]和right = [3,2],然后return。
3. 这是函数的返回结果:merge( mergeSort( [4,1] ) , mergeSort( [3,2] ) ),将大数组拆分成2个单位的小数组,然后接着执行mergeSort函数。====> mergeSort([4,1])和mergeSort([3,2])。
4. mergeSort([4,1]):因为[4,1]还是不满足长度为1的终止条件,会被拆分成merge( mergeSort( [4] ) , mergeSort( [1] ) )。然后接着执行mergeSort( [4],因为数组length为1,所以直接返回数组[4]。所以merge( mergeSort( [4] ) , mergeSort( [1] ) )的结果就是:merge( [4],[1] )。也就是说mergeSort( [4,1] )的最终结果是:merge([4],[1])。
5. 同理,mergeSort([3,2])也是一样。所以传入[4,1,3,2],就会返回merge( merge( [4],[1] ) , merge( [3],[2] ) )
这时候我们已经将它拆分成一个单位的小数组,并传入merge函数,在这个函数内部完成合并的过程。
javascript代码
var result = merge( merge([4],[1]) , merge([3],[2]) );
console.log(result); // [1,2,3,4]
function merge(left,right) { // 将多个一个位置的小数组合并成一个大数组
var result = [],
n1 = 0,
n2 = 0;
while (n1 < left.length && n2 < right.length) { // 控制2个长度为循环次数
if (left[n1] < right[n2]) {
result.push(left[n1++]); // push进left[n1],n1 = n1 + 1;
} else {
result.push(right[n2++]);
}
}
while (n1 < left.length) {
result.push(left[n1++]);
}
while (n2 < right.length) {
result.push(right[n2++]);
}
return result;
}
首先我们执行merge函数:merge( merge( [4],[1] ) , merge( [3],[2] ) ),merge( [4],[1] )其实就是大的merge的参数,所以越内部就会先执行。
过程分析:
1. 首先会执行merge( [4],[1] )函数。我们的目的是合并成内部元素从小到大排序的数组[1,4]。
2. left = [4],right = [1],因为都只有一项,所以第一个while循环只执行一次。然后我们比较left的第一项和right的第一项,如果left的小,就将它添加进数组result中。然后控制循环的变量n1++。
3. 接下来因为n1 = left.length,不满足要求,就没有继续循环,就执行后面的内容。然后因为n2 < right.length,将right[0]添加进result数组中。这样就完成了我们的目的,将小数组合并成[1,4]。
其实merge函数的原理就是通过比较left和right项的大小,将小的一项添加进result中。直到left和right中有一个数组的项全部添加进去。如上例就是将通过循环将left的所有项添加进result数组中,然后right数组直接将剩余项添加到result中就好了。
所以merge( [4],[1] )的返回值就是[1,4],同理,merge( [3],[2] )的返回值是[2,3]。完成了第一步归并的结果就是merge( [1,4],[2,3] )
接下来继续执行第二步的归并,再次通过merge函数将两个数组合并成一个大数组。然后返回归并后的结果就行了。
5.快速排序
快速排序是最常用的排序算法,并且性能也很好。
var arr = [5,4,2,3,1];
function arrSort(arr) { // 快速排序
var len = arr.length;
if (len <= 1) { // 递归终止条件
return arr;
}
var index = Math.floor(len/2),
num = arr.splice(index,1), // 选择的基准值
leftArr = [],
rightArr = [];
arr.forEach((val)=>{ // 和基准值进行比较
if (val <= num) { // [2]和数字进行比较,会转化为数字再进行比较
leftArr.push(val);
} else {
rightArr.push(val);
}
});
return arguments.callee(leftArr).concat(num,arguments.callee(rightArr)); // 数组合并
}
console.log(arrSort(arr)); // [1,2,3,4,5]
快速排序原理:
1. 在数组中选择中间元素作为基准值。上例为[2]
2. 将数组的其他项和这个基准值进行比较,形成2个子集,一个是大于2的,一个是小于2的。leftArr = [1],rightArr = [3,4,5]。
3. 对2个子集重复1、2步骤,直到这2个子集只剩下一个元素为止。
我在可能有8个月前就接触过快速排序,当时因为对递归没有足够了解,所以一直不懂它。需要注意的是,arguments.callee()这个方法是函数调用自身函数。
目前app的商品发布快做完了,下一次博客会总结图片上传功能的各种实现方式。