在实现过程中,参考了许多大佬的博客,不能全部列出,在此致谢,致歉。
本文不讲基础的原理过程,只贴JavaScript代码,供想使用JavaScript实现排序的读者参考。
排序的方法很重要,不同的排序方法效率差距还是很大的。打开控制台即可测试不同排序方法的效率如何。
// 我们用下面的方法测试函数执行时间
console.time("name"); // 开始计时
function(){}; // 你的函数
console.timeEnd("name"); // 结束计时
1. 冒泡排序
冒泡排序是最经典的排序方法。两两比较,每一轮找出一个最大的值,沉入最底。
function bubble(array){
for(let i = 0; i < array.length - 1; i ++){
for(let j = 0; j < array.length - 1 - i; j ++){
if(array[j] > array[j + 1]){
let temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
}
改进版的冒泡,在没有可交换的操作时,提前结束。
// 改进冒泡,任何一轮没有可交换的操作时,结束
function bubble(array){
for(let i = 0; i < array.length - 1; i ++){
console.log("------第" + i + "轮------")
let flag = 0;
for(let j = 0; j < array.length - 1 - i; j ++){
if(array[j] > array[j + 1]){
let temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
flag ++;
}
}
if(flag === 0){
break;
}
}
}
2. 选择排序法
选择排序,每次选择最小值。
function selection(a){
for(let i = 0; i < a.length; i++){
// 在剩余的元素中选择最小值
let min = i;
for(let j = i+1; j < a.length; j++){
if(a[j] < a[min]){
min = j;
}
}
// 交换
let temp = a[i];
a[i] = a[min];
a[min] = temp;
}
}
3. 插入排序法
插入排序将序列分成两组,一组已排序一组未排序,每次从未排序的之中拉一个到已排序的中,选择合适的位置***去,故叫插入法。但一般用数组实现时,没有直接的插入方法,故实际上我们使用的还是两两对比交换位置的方法实现插入。在由小到大排序时,我们经常从后往前判断插入位置。
// 最终的结果看起来是前插,实际上实现过程我们依然用的是交换
// 不停地交换,直到到达合适的位置,即看起来像是直接插到合适的位置。
function _insertion(a){
for(let i = 1; i < a.length; i ++){
let j = i; // i 代表现在是第几轮
console.log("------第" + j + "轮------")
while(j > 0 && a[j] < a[j - 1]){
let temp = a[j - 1];
a[j - 1] = a[j];
a[j] = temp; // 前插
j --;
}
console.log(a);
}
}
使用for循环实现:
// 插入
// i 是排序与未排序的分水岭,i从1移动到最后,已排序的吃掉所有未排序的
// i及以后是未排序的,i以前是已经排序的
function insertion(arr) {
for (let i = 1; i < arr.length; i++) { // 未排序
for (let j = i - 1; j >= 0; j--) { // 已排序,从后往前插入
if (arr[j + 1] < arr[j]) {
// 小了交换位置,往前移
swap(j, j + 1, arr)
} else {
// 大了结束
break;
}
}
}
}
另一种思路:把交换拆成两个步骤,先挖坑但不着急填坑,直到找到最后欲插入位置再结束循环填坑。
// 插入
function insertion(arr) {
for (let i = 1; i < arr.length; i++) { // i 就是想要插入到已排序的那组当中去的当前值
let currentWaitingToInsert = arr[i];
let j = 0; // 记录欲插入位置
for (j = i - 1; j >= 0; j--) { // j 依次遍历已排序的那组,从后往前,即从大到小
if (currentWaitingToInsert < arr[j]) {
arr[j + 1] = arr[j]; // 后移欲插入位置上的原有数据
} else {
// 找到了欲插入位置
break;
}
}
arr[j + 1] = currentWaitingToInsert; // 插入
}
}
4. 希尔排序
用一个gap打乱基本插入法的顺序,先进行粗排,减少交换次数,提升效率。
// shell排序法,缩小增量排序,减少数据搬移的次数
function shell(a){
let n = a.length;
let gap = n;
do{
gap = gap % 2 === 0 ? gap / 2 : (gap - 1) / 2; // 向下取整(c语言里直接int)
for(let i = gap; i < n; i ++){ // 从gap到length
let j = i;
while(j - gap >= 0 && a[j] < a[j - gap]){
let temp = a[j - gap];
a[j - gap] = a[j];
a[j] = temp; // 前插
j -= gap;
}
}
}while(gap > 1)
}
5. 快速排序
快速排序网上大概有三种方法:挖坑法、左右指针法、前后指针法。
挖坑法。挖新坑,用新坑的土填旧坑。
// 快速排序
// 挖坑法
function quick(a, l, r){
if(r > l){ // 截止条件,左右相逢
let _i = l;
let _j = r;
let key = a[_i]; // 挖坑
while(_i < _j){ // 萍水未相逢, 只有相逢才是出路
while(_i < _j && a[_j] >= key){ // 未相逢,位正前移
_j --;
} // 相逢 或 位不正
if(_i < _j){ // 确认是位不正
a[_i] = a[_j]; // 填旧坑,挖新坑
}
while(_i < _j && a[_i] <= key){
_i ++;
}
if(_i < _j){
a[_j] = a[_i];
}
}
a[_i] = key;
quick(a, l, _i - 1);
quick(a, _i + 1, r);
}
}
左右指针法。左右交换。
// 左右指针法
function quick(array, left, right){
if(left < right){
let i = left;
let j = right;
let key = array[i];
while(i !== j){
while(i < j && array[j] >= key){
--j;
}
while(i < j && array[i] <= key){
++i;
}
if(i < j){
swap(i, j);
}
}
swap(left, i);
quick(array, left, i - 1);
quick(array, i + 1, right);
}
function swap(x, y){
let temp = array[x];
array[x] = array[y];
array[y] = temp;
}
}
前后指针法。这个路子比较野,不好理解。
// 前后指针法
function quick(array, left, right){
if(left < right){
let cur = left;
let pre = left - 1;
let mid = getMid();
let key = array[mid];
swap(mid, right); // 将基准值放在最右
while(cur < right){
while(array[cur] < key && ++pre !== cur){
swap(pre, cur);
}
++cur;
}
swap(++pre, cur);
quick(array, left, pre - 1);
quick(array, pre + 1, right);
}
// 三数取中法
function getMid(){
let mid = (right - left) % 2 === 0 ? left + (right - left) / 2 : left + (right - left - 1) / 2;
if(array[mid] > array[left]){
if(array[mid] < array[right]){
return mid;
}else if(array[right] > array[left]){
return right;
}else{
return left;
}
}else{
if(array[mid] > array[right]){
return mid;
}else if(array[right] > array[left]){
return left;
}else{
return right;
}
}
}
function swap(x, y){
let temp = array[x];
array[x] = array[y];
array[y] = temp;
}
}
6. 堆排序
大根堆(大顶堆)是一颗完全二叉树,树的每一个节点的值要大于他的左右孩子的值。
// 大根堆
function heap_big(array){
function heap(array, parent, length){
let temp = array[parent]; // 把老子挂起来
let child = 2 * parent + 1;
while(child < length){
// 选出最能干的儿子
if(child + 1 < length && array[child + 1] > array[child]){
child = child + 1;
}
// 老子还是比儿子能干,老子继续干
if(array[parent] > array[child]){
break;
}
// 儿子比老子能干,子代父
array[parent] = array[child];
array[child] = temp;
// 更新,循环
parent = child;
child = 2 * parent + 1;
}
}
// Math.floor(array.length/2)是第一个叶子结点,减一就是非叶子节点
for(let i = Math.floor(array.length / 2) - 1; i > 0; i--){
// 由下至上的针对每一个非叶子节点调整
heap(array, i, array.length);
}
return array;
}
大根堆排序法。大根堆的根节点必然是序列中的最大值,所以每次求得序列最大值,最后挖去最大值再生成大根堆。
// 大根堆排序
function heap_big(array){
function heap(array, parent, length){
let temp = array[parent]; // 把老子挂起来
let child = 2 * parent + 1;
while(child < length){
// 选出最能干的儿子
if(child + 1 < length && array[child + 1] > array[child]){
child = child + 1;
}
// 老子还是比儿子能干,老子继续干
if(array[parent] > array[child]){
break;
}
// 儿子比老子能干,子代父
array[parent] = array[child];
array[child] = temp;
// 更新,循环
parent = child;
child = 2 * parent + 1;
}
}
// 初始化
for(let i = Math.floor(array.length / 2) - 1; i > 0; i--){
// 由下至上的针对每一个非叶子节点调整
heap(array, i, array.length);
}
for(let j = array.length - 1; j > 0; j--){
// 交换
let temp = array[0];
array[0] = array[j];
array[j] = temp;
// 对根节点 调整
heap(array, 0, j);
}
}
7. 基数排序法
基数排序法属于分配排序(distribution sort),又称桶子法(bucket sort)。通过装桶倒桶排序。
// 基数排序法
function radixSort(array){
function getDigit(num, radix){
let result = (num % (radix * 10) - num % radix) / radix;
return result;
}
let bucket = []; // 桶
let flag = true; // 是否继续
function initBucket(){
for(let i = 0; i < 10; i++){
bucket[i] = []; // 空桶
}
}
initBucket();
while(flag){
flag = false;
let radix = 10 ** i;
// 根据基数填进桶里
for(let i = 0; i < array.length; i ++){
let index = getDigit(array[i], radix);
if(index !== 0){
flag = true;
}
bucket[index].push(array[i]);
}
// 桶里倒出来
let k = 0;
for(let i = 0; i < 10; i ++){
for(let j = 0; j < bucket[i].length; j ++){
array[k] = bucket[i][j];
k++;
}
}
// 清空桶
initBucket();
// 更新基数
i ++;
}
}