1、冒泡排序
人们开始学习排序算法时,通常都先学冒泡算法,因为它在所有排序算法中最简单。然而,从运行时间的角度来看,冒泡排序是最差的一个。
冒泡排序过程:比较任何两个相邻的项,如果第一个比第二个大,则交换它们。元素项向上移动至正确的顺序,就好像气泡升至表面一样,冒泡排序因此得名。代码实现如下:
function bubbleSort(arr){
var len = arr.length;
for(var i=0;i<len;i++){
for(var j=0;j<=len-1-i;j++){
var temp = 0;
if(arr[j]>arr[j+1]){
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
return arr;
}
下图展示了冒泡排序是如何执行的:
2、选择排序
选择排序同样也是一个复杂度为O(n^2)的算法。和冒泡排序一样,它包含有嵌套的两个循环,这导致了二次方的复杂度。
选择排序大致的思路是找到数据结构中的最小值并将其放置在第一位,接着找到第二小的值并将其放在第二位,以此类推。代码实现如下:
function selectionSort(arr) {
var len = arr.length;
var minIndex, temp;
for (var i = 0; i < len - 1; i++) {
minIndex = i;//假设本迭代轮次的第一个值为数组最小值
for (var j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) { //寻找最小的数
minIndex = j; //将最小数的索引保存
}
}
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
return arr;
}
下图展示了选择排序是怎样执行的:
3、插入排序
插入排序的思想:假定第一项已经排序了,接着,它和第二项进行比较,第二项是应该待在原位还是插到第一项之前呢?这样,头两项就已正确排序,接着和第三项比较(它是该插入到第一、第二还是第三的位置呢?),以此类推。JS实现代码如下:
function insertSort(array) {
var length = array.length,
j,temp;
for (var i = 1;i<length;i++){//算法是从第二个位置(索引 1 )而不是 0 位置开始的(我们认为第一项已排序了)。
j=i;
temp = array[i];//用 i 的值来初始化一个辅助变量,并将其值存储于一临时变量中,便于之后将其插入到正确的位置上。
while(j>0&&temp<array[j-1]){//如果待比较的临时变量temp比前面的项小,则往前移。
array[j] = array[j-1];
j--;
}
array[j] = temp;
}
return array;
}
下图展示了插入排序是怎样执行的:
排序小型数组时,此算法比选择排序和冒泡排序性能要好。
4、归并排序
归并排序是第一个可以被实际使用的排序算法。前三个排序算法性能不好,但归并排序性能不错,其复杂度为O(nlog(n))。
归并排序是一种分治算法。其思想是将原始数组切分成较小的数组,直到每个小数组只有一个位置,接着将小数组归并成较大的数组,直到最后只有一个排序完毕的大数组。JS实现如下:
function mergeSort(arr) { //采用递归方法
var len = arr.length;
if(len === 1) {//由于算法是递归的,我们需要一个停止条件,在这里此条件是判断数组的长度是否为 1 。如果是,则直接返回这个长度为 1 的数组,因为它已排序。
return arr;
}
var middle = Math.floor(len / 2),//中间位
left = arr.slice(0, middle),
right = arr.slice(middle);
return merge(mergeSort(left), mergeSort(right));//为了不断将原始数组分成小数组,我们得再次对 left 数组和 right 数组递归调用 mergeSort
}
function merge(left, right){//merge 函数,它负责合并和排序小数组来产生大数组,直到回到原始数组并已排序完成。
var result = [];
while (left.length && right.length) {
if (left[0] <= right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}
while (left.length){
result.push(left.shift());
}
while (right.length){
result.push(right.shift());
}
return result;
}
下图展示了归并排序的执行过程:
可以看到,算法首先将原始数组分割直至只有一个元素的子数组,然后开始归并。归并过程也会完成排序,直至原始数组完全合并并完成排序。
5、快速排序
快速排序也许是最常用的排序算法了。它的复杂度为O(nlog(n)),且它的性能通常比其他的复杂度为O(nlog(n))的排序算法要好。和归并排序一样,快速排序也使用分治的方法,将原始数组分为较小的数组(但它没有像归并排序那样将它们分割开)。
快速排序的思想:
(1)在数据集之中,选择一个元素作为”基准”(pivot)。
(2)所有小于”基准”的元素,都移到”基准”的左边;所有大于”基准”的元素,都移到”基准”的右边。
(3)对”基准”左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个元素为止。
现在先说说普遍的实现方法(没有用到原地算法)
function quickSort(arr){
//如果数组<=1,则直接返回
if(arr.length<=1){return arr;}
var pivotIndex=Math.floor(arr.length/2);
//找基准,并把基准从原数组删除
var pivot=arr.splice(pivotIndex,1)[0];
//定义左右数组
var left=[];
var right=[];
//比基准小的放在left,比基准大的放在right
for(var i=0;i<arr.length;i++){
if(arr[i]<=pivot){
left.push(arr[i]);
}else{
right.push(arr[i]);
}
}
//递归
return quickSort(left).concat([pivot],quickSort(right));
}
上述实现方法的弊端:它需要Ω(n)的额外存储空间,跟归并排序一样不好。在生产环境中需要额外的内存空间,影响性能。但实际上这一点是完全可以克服的,对于不用额外空间(即常数大小的额外空间)的算法,有一个通用的名字叫做In-place Algorithms(原地算法)。
常用排序算法总结如下:
快速排序算法的in-place实现:
(1) 首先,从数组中选择中间一项作为主元。
(2) 创建两个指针,左边一个指向数组第一个项,右边一个指向数组最后一个项。移动左指针直到我们找到一个比主元大的元素,接着,移动右指针直到找到一个比主元小的元素,然后交换它们,重复这个过程,直到左指针超过了右指针。这个过程将使得比主元小的值都排在主元之前,而比主元大的值都排在主元之后。这一步叫作划分操作。
(3) 接着,算法对划分后的小数组(较主元小的值组成的子数组,以及较主元大的值组成的
子数组)重复之前的两个步骤,直至数组已完全排序。
function quick (array, left, right){//left初始化为数组第一个元素的索引值:0和right初始化为数组最后一个元素的索引值:arr.length-1;
var index;
if (array.length > 1) {
index = partition(array, left, right); //将子数组分离为较小值数组和较大值数组
if (left < index - 1) { //如果子数组存在较小值的元素,则对该数组重复这个过程
quick(array, left, index - 1);
}
if (index < right) { //对存在较大值得子数组也是如此,如果存在子数组存在较大值,我们也将重复快速排序过程
quick(array, index, right);
}
}
return array;
}
//划分过程
function partition (array, left, right) {
var pivot = array[Math.floor((right + left) / 2)], //选择中间项作为主元
i = left,
j = right;
while (i <= j) { //只要 left 和 right 指针没有相互交错,就执行划分操作
while (array[i] < pivot) { //移动 left 指针直到找到一个元素比主元大
i++;
}
while (array[j] > pivot) { //移动 right 指针直到找到一个元素比主元小
j--;
}
if (i <= j) { //当左指针指向的元素比主元大且右指针指向的元素比主元小,并且此时左指针索引没有右指针索引大,交换它们,然后移动两个指针,并重复此过程
var temp = array[i];
array[i] = array[j];
array[j] = temp;
i++;
j--;
}
}
return i; //在划分操作结束后,返回左指针的索引,用来创建子数组。
};
对本文介绍的5种排序算法的时间复杂度做一个总结,如下表所示:
结语:JavaScript的 Array 类定义了一个 sort 函数( Array.prototype.sort )用以排序JavaScript数组(我们不必自己实现这个算法,直接拿来用就好了)。例如用sort函数实现数组内元素从小到大排序:array.sort((a,b)=>a-b)
。
ECMAScript没有定义用哪个排序算法,所以浏览器厂商可以自行去实现算法。例如,Mozilla Firefox使用归并排序作为 Array.prototype.sort 的实现,而Chrome使用了一个快速排序的变体。