JavaScript算法-排序算法

Posted 奋飛

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript算法-排序算法相关的知识,希望对你有一定的参考价值。

​ 此生之路,我将走过;走过这一次,便再也无法重来。所有力所能及的善行,所有充盈于心的善意,我将毫不吝惜,即刻倾予。我将再不拖延,再不淡漠,只因此生之路,再也无法重来。

对计算机中存储的数据执行的两种最常见操作是排序和索引。下述阐述的排序方式,暂且都是用数组进行测试(从小到大)。

var dataAry = [5, 4, 3, 7, 1, 2, 8, 6, 9]; // 测试数组
/**
 *【工具方法】交换数组中两个值
 * @param ary 数组
 * @param i 下标i
 * @param j 下标j
 */
function swap(ary, i, j) {
    var temp = ary[i];
    ary[i] = ary[j];
    ary[j] = temp;
}

冒泡排序

之所以称为冒泡排序是因为使用这种排序算法时,数据值会像气泡一样从数组的一端漂浮到另一端。如,从小到大排序:其会比较相邻的数据,当左侧值大于右侧值时将它们进行交换。

冒泡排序算法的运作如下:(从小到大)

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
function bubbleSort(dataAry) {
	var len = dataAry.length;
	for(var i = 0; i < len - 1; i++) {
        for(var j = 0; j < len - i - 1; j++) {
            if(dataAry[j] > dataAry[j+1]) {
                swap(dataAry, j, j+1);
            }
        }
    }
    return dataAry;
}
// 测试
console.log(bubbleSort(dataAry)); // [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
543712869原始数组
435127689第1轮
341256789第2轮
312456789第3轮
123456789第4轮
123456789第5轮
123456789第6轮
123456789第7轮
123456789第8轮
123456789第9轮

通过上述可知,第4轮过后,排序就已完成(数组已经有序了),上述算法仍然兢兢业业的执行了9轮。所以,对于冒泡排序,我们可以做优化。

优化方式

第一种方式: 在内循环中添加标记,如果有交换,则数组无序;无交换,则数组已有序。

function bubbleSort(dataAry) {
	var len = dataAry.length;
 	for(var i = 0; i < len - 1; i++) {
         var isSorted = true;
         for(var j = 0; j < len - i - 1; j++) {
             if(dataAry[j] > dataAry[j+1]) {
                 swap(dataAry, j, j+1);
               	isSorted = false;
             }
         }
         if (isSorted) break;
     }
     return dataAry;
 }

上述优化结束后,4轮执行便可结束。但仔细查看,会发现 swap 仍然存在多余的比较(第二轮结束后,5、6、7、8、9均已就位,但是第三轮仍需要比较其中部分 – 8、9不会再比较)。

第二种方式: 对有序去界定,记录下最后一次元素交换的位置。

function bubbleSort(dataAry) {
	var len = dataAry.length;
  	// 记录最后一次交换的位置
  	var lastExchangeIndex = 0
  	// 无序边界(每次比较截止位置)
  	var sortBorder = len - 1
	for(var i = 0; i < len - 1; i++) {
        var isSorted = true;
        for(var j = 0; j < sortBorder; j++) {
            if(dataAry[j] > dataAry[j+1]) {
                swap(dataAry, j, j+1);
              	isSorted = false;
              	// 更新最后一次交换元素的位置
              	lastExchangeIndex = j
            }
        }
    	sortBorder = lastExchangeIndex
        if (isSorted) break;
    }
    return dataAry;
}

第三种方式: 鸡尾酒排序(双向交换),大家自行查阅!

选择排序

​ 从数组的第一个数据开始,将第一个数据和其他数据进行比较。它的工作原理是每一次从待排序的数据中选出最小(或最大)的一个数据,存放在序列的起始位置,直到全部待排序的数据元素排完。

function selectionSort(dataAry) {
    var len = dataAry.length;
    for(var i = 0; i < len - 1; i++) {
        for(var j = i + 1; j < len; j++) {
            if(dataAry[i] > dataAry[j]) {
                swap(dataAry, i , j);
            }
        }
    }
    return dataAry;
}
// 测试
console.log(selectionSort(dataAry)); // [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]

插入排序

​ 插入排序有两个循环,外循环将数组元素挨个移动,而内循环则对外循环中选定的元素及它后面的那个元素比较。如果外循环中选中元素小,那么数组元素会向右移动,为内循环中的这个元素腾出位置。每步将一个待排序的纪录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止。

直接插入排序算法的说明<有1-9张标有是数字的卡片>:

  1. 取出第一张卡片直接放到桌子最左侧
  2. 取出第二张卡片,如果该卡片标注的数字小于第一张,则将第一张卡片向右移动一格;否则放到右侧;
  3. 取出第n张卡片,将该卡片与第n-1、n-2···第1张对比,小于等于则右移继续对比,大于则停止对比将该值插入对应位置。
function insertionSort(dataAry) {
    var temp, inner;
  	// 外循环(跳过第一个值,因为第一个直接放到最左侧)
    for(var outer = 1, len = dataAry.length; outer < len; outer++) {
        temp = dataAry[outer];
        inner = outer;
        // 内循环,比左侧值小就移动让位
      	while(inner > 0 && (dataAry[inner - 1] >= temp)) {
            dataAry[inner] = dataAry[inner - 1];
            inner--;
        }
        dataAry[inner] = temp;
    }
  	return dataAry
}
// 测试
console.log(insertionSort(dataAry)); // [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]

希尔排序

​ 该算法在插入排序的基础上做了相应改善。其需要预先(或动态)定义一个间隔序列来表示在排序过程中进行比较的元素之间的间隔。对被间隔的每组元素使用直接插入排序算法排序;随着间隔序列中的数逐渐减小,每组包含的元素越来越多,当间隔减至1时,整个文件恰被分成一组,算法便终止。这确保了在开始最后一次处理时,大部分元素都已在正确位置,必须再进行多次数据交换,这就是希尔排序比插入排序更高效的地方。

希尔排序算法说明:

  1. 先取一个小于n的整数d1作为第一个间隔,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;
  2. 取第二个间隔值d2<d1重复上述的分组和排序;
  3. 直至所取的间隔为1,即所有记录放在同一组中进行直接插入排序为止。
    希尔排序
/**
 * 希尔排序
 * @param dataAry 数据数组
 * @param gaps    间隔数组
 */
function shellSort(dataAry, gaps) {
    var temp, inner, currentGap;
    // 遍历间隔
    for(var g = 0, gLen = gaps.length; g < gLen; g++) {
        // 当前间隔
        currentGap = gaps[g];
        // 直接插入法外循环
        for(var outer = currentGap, len = dataAry.length; outer < len; outer++) {
           temp = dataAry[outer];
           inner = outer;
           // 直接插入法内循环(注意对比间隔值)
           while(inner >= currentGap && (dataAry[inner - currentGap] >= temp)) {
               dataAry[inner] = dataAry[inner - currentGap];
               inner = inner - currentGap;
           }
           dataAry[inner] = temp;
        }
    }
    return dataAry;
}
// 测试
console.log(shellSort(dataAry, [3, 2, 1])); // [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]

归并排序

把一系列排好序的子序列合并成一个大的完整有序序列。归并排序通常使用递归来实现。

自顶向下的归并排序(递归)

自顶向下的归并排序(递归)

function mergeSort(dataAry) {
    if(dataAry.length === 1) {
        return dataAry;
    }
    var mid = Math.floor(dataAry.length/2);
    var leftAry = dataAry.slice(0, mid);
    var rightAry = dataAry.slice(mid);
    return merge(mergeSort(leftAry), mergeSort(rightAry));
}
/**
 * 合并数组
 * @param dataAry
 */
function merge(leftAry, rightAry) {
    var result = [];
    while(leftAry.length > 0 && rightAry.length > 0) {
        if(leftAry[0] < rightAry[0]) {
            result.push(leftAry.shift());
        }else {
            result.push(rightAry.shift());
        }
    }
    return result.concat(leftAry).concat(rightAry);
}
// 测试
console.log(mergeSort(dataAry)); // [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]

​ 上述mergeSort()被执行了2n-1次(上述被执行了17次),一旦数组元素个数增多,采用递归的方式将不再可取,元素个数超过1500在Firfox下就会造成栈溢出!

自底向上的归并排序(迭代)

​ 首先将数据集分解为一组只有一个元素的数组,然后通过创建一组左右数组将其合并,每次合并确保数据已排好序,直到最后所有数据排序完成。

这里写图片描述

function mergeSort(dataAry) {
    if(dataAry.length < 2) {
        return dataAry;
    }
    var step = 1; // 控制子序列大小
    var left, right; // 左、右下标
    while(step < dataAry.length) {
        left = 0;
        right = step;

        while((right + step) <= dataAry.length) {
            mergeArrays(dataAry, left, left+step, right, right+step);
            left = right + step;
            right = left + step;
        }
        // 不能被分组的元素
        if(right < dataAry.length) {
            mergeArrays(dataAry, left, left+step, right, dataAry.length);
        }
        step = step * 2;
    }
    return dataAry;
}

/**
 * 合并数组<包含头,不包含尾>
 * @param ary
 * @param startLeft 
 * @param endLeft
 * @param startRight
 * @param endRight
 */
function mergeArrays(ary, startLeft, endLeft, startRight, endRight) {
    var leftAry = new Array(endLeft - startLeft + 1),
        rightAry = new Array(endRight - startRight + 1);

    var k = startLeft;
    for(var i = 0; i < (leftAry.length - 1); i++) {
        leftAry[i] = ary[k];
        k++;
    }
    leftAry[leftAry.length - 1] = Infinity; // 哨兵值

    k = startRight;
    for(var j = 0; j < (rightAry.length - 1); j++) {
        rightAry[j] = ary[k];
        k++;
    }
    rightAry[rightAry.length - 1] = Infinity; // 哨兵值

    // 对比左右元素大小
    var m = 0, n = 0;
    for(var k = startLeft; k < endRight; k++) {
        if(leftAry[m] <= rightAry[n]) {
            ary[k] = leftAry[m];
            m++;
        }else {
            ary[k] = rightAry[n];
            n++;
        }
    }
}
// 测试
console.log(quickSort(dataAry)); // [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]

快速排序

​ 快速排序是处理大数据集最快的排序算法之一。其核心思想为“分而治之”。设立基值,通过递归的方式将数据一次分解为包含较小元素和较大元素的不同子序列,然后不断重复,直到所有数据变为有序。

快速排序算法说明:

  1. 选择一个基准元素,将列表分隔为两个子序列;
  2. 将所有小于基准元素的数据放在基准值的左侧,大于基准值元素的数据放在基准值右侧;
  3. 分别对较小元素的子序列和较大元素的子序列重复步骤1和2。

快速排序<方式一>:单边。从左侧起开始循环数组与基值进行对比

function quickSort(dataAry) {
    if(dataAry.length === 0) {
        return [];
    }
    var left = [], right = [];
    var pivot = dataAry[0];
    for(var i = 1; i < dataAry.length; i++) {
        dataAry[i] < pivot ? left.push(dataAry[i]) : right.push(dataAry[i]);
    }
    return quickSort(left).concat(pivot, quickSort(right));
}
// 测试
console.log(quickSort(dataAry)); // [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]

快速排序<方式二>:双边。从左、右两侧一起移动,与基值进行对比

这里写图片描述

function quickSort(arr, left, right) {
    if (left > right) return;
    var temp,
        i = left,
        j = right;
    // 设基准值
    var pivot = arr[left];
    while (i != j) {
        // 先从右侧开始(找到小于基值的第一个数据)
        while (i < j && arr[j] >= pivot) {
            j--;
        }
        // 然后从左侧开始(找到大于基值的第一个数据)
        while (i < j && arr[i] <= pivot) {
            i++;
        }
        // 交换数据上述两个元素,继续查找,直到i和j相遇
        if (i < j) {
            temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }
  	// 将基值与相遇数据进行交换<确保了基值位于元素中央位置>
    temp = arr[i];
    arr[i] = arr[left];
    arr[left] = temp;
  	// 基值左侧数组按上述方式递归执行
    quickSort(arr, left, i - 1);
 	// 基值右侧数组按上述方式递归执行
    quickSort(arr, i + 1, right);
    return arr;
}
// 测试
console.log(quickSort(dataAry, 0, dataAry.length - 1)); // [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]

计数排序

计数排序利用数组下标来确定元素的位置。适用于一定范围内的整数排序。
以上述提供的测试数组为例 var dataAry = [5, 4, 3, 7, 1, 2, 8, 6, 9]; // 测试数组 其满足计数排序的场景。

计数排序说明:

  1. 确定数组的取值范围为 0 ~ 10
  2. 创建一个长度为11的统计数组countArray,下标从0开始,到10;设置初始值均为0
  3. 循环排序数组按照值对号入座,同时对应数组下标的元素进行加1操作
  4. 遍历countArray数组,输出数组的下标值,元素是几就输出几次
function countSort(dataAry) {
    // 获取最大数,并创建数组
    let maxNumber = Math.max.call(undefined, ...dataAry)
    let countArray = new Array(maxNumber+1).fill(0)
    // 填充统计数组
    for (let data of dataAry) {
        countArray[data]++
    }
    // 遍历统计结果数组
    let sortedArray = new Array(dataAry.length)
    let index = 0
    for (let i = 0, len = countArray.length;i < len; i++) {
        for (let j = 0; j < countArray[i]; j++) {
            sortedArray[index++] = i
        }
    }
    return sortedArray
}
/ 测试
console.log(countSort(dataAry)); // [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]

优化

[ 91 , 98 , 97 , 92 , 93 , 96 , 95 ] [91,98,97,92,93,96,95] [91,98,用JavaScript实现排序算法

JavaScript算法(冒泡排序选择排序与插入排序)

JavaScript之算法设计思想

7种基本排序算法的Java实现

JavaScript 数据结构与算法之美 - 归并排序快速排序希尔排序堆排序

总结:大厂面试常考手撕代码 —— JavaScript排序算法(冒泡排序选择排序插入排序快速排序)