经典排序算法的实现

216 阅读2分钟

算法术语

  • 稳定: 排序前a在b的前面且a=b, 排序后a仍在b的前面;

  • 不稳定: 排序前a在b的前面且a=b, 排序后a可能在b的后面;

  • 内排序: 排序操作在内存中完成;

  • 外排序: 外部排序指的是大文件的排序,即待排序的记录存储在外存储器上,待排序的文件无法一次装入内存,需要在内存和外部存储器之间进行多次数据交换,以达到排序整个文件的目的。

  • 时间复杂度: 一个算法执行所耗费的时间。

  • 空间复杂度: 运行完一个程序所需内存的大小。

排序对比如下图

冒泡排序

  最简单的排序算法之一, 比较任何两个相邻的项并交换; 经过比较越小的元素慢慢"浮出"到顶端, 因此取名冒泡排序.

无优化

function bubbleSort(arr) {
    for (let end = arr.length - 1; end > 0; end--) {
        for (let begin = 1; begin <= end; begin++) {
            if (arr[begin] < arr[begin - 1]) {        //相邻元素两两对比
                [arr[begin - 1], arr[begin]] = [arr[begin], arr[begin - 1]];
            }
        }
    }
    return arr;
}

优化一

设置一个标志位,用来表示当前第 i 趟是否有交换,如果有则要进行 i+1 趟,如果没有,则说明当前数组已经完成排序,跳出循环。

function bubbleSort2(arr) {
    for (let end = arr.length - 1; end > 0; end--) {
        var sorted = true;
        for (let begin = 1; begin <= end; begin++) {
            if (arr[begin] < arr[begin - 1]) {        //相邻元素两两对比
                [arr[begin - 1], arr[begin]] = [arr[begin], arr[begin - 1]];
                sorted = false;
            }
        }
        if(sorted) break;
    }
    return arr;
}

优化二

设置一标志性变量soretdIndex, 用于记录每趟排序中最后一次进行交换的位置。由于soretdIndex位置之后的记录均已交换到位,故在进行下一趟排序时只要扫描到soretdIndex位置即可

function bubbleSort3(arr) {
    for (let end = arr.length - 1; end > 0; end--) {
        let soretdIndex = 1;
        for (let begin = 1; begin <= end; begin++) {
            if(arr[begin] < arr[begin - 1]) {
                [arr[begin - 1], arr[begin]] = [arr[begin], arr[begin - 1]];
                soretdIndex  = begin;
            }
        }
        end = soretdIndex;
    }
    return arr;
}

选择排序

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。 重复第二步,直到所有元素均排序完毕。

function selectionSort(arr) {
    let len = arr.length;
    let minIdx;
    for (let i = 0; i < len - 1; i++) {
        minIdx = i;
        for (let j = i + 1; j < len; j++) {
            if(arr[j] < arr[minIdx]) {
                minIdx = j;
            }
        }
        [arr[i], arr[minIdx]] = [arr[minIdx], arr[i]];
    }
    return arr;
}

插入排序

将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

for循环实现

function insertSort(arr) {
    for (let i = 1; i < arr.length; i++) {
        for (j = i; j > 0; j--) {
            if(arr[j] < arr[j - 1]) {
                [arr[j], arr[j - 1]] = [arr[j - 1], arr[j]];
            }
        }
    }
    return arr;
}

while循环实现

function insertSort(arr) {
    for (let begin = 1; begin < arr.length; begin++) {
        let cur = begin;
        while(cur > 0 && arr[cur] < arr[cur - 1]) {
            [arr[cur], arr[cur - 1]] = [arr[cur - 1], arr[cur]];
            cur--
        }
    }
    return arr;
}

插入优化一

将‘交换’转变为‘挪动’ ,交换代码执行行数比挪动多。

  • 先将待插入的元素备份
  • 头部有序元素数据中比待插入元素大的,都朝尾部方向挪动1个位置
  • 将待插入元素放到最终的合适位置

代码如下

function insertSort2(arr) {
  for (let begin = 1; begin < arr.length; begin++) {
    let cur = begin;
    let tmp = arr[cur];
    while(cur > 0 && tmp < arr[cur - 1]) {
      arr[cur] = arr[cur - 1];
      cur--;
    }
    arr[cur] = tmp;
  }
  return arr;
}

插入优化二

此优化基于优化一,不同的是采用二分搜索法查找要插入的位置。

function insertSort2(arr) {
    for (let i = 1; i < arr.length; i++) {
        let tmp = arr[i];
        let j = search(i, arr);
        for (let m = i; m > j; m--) {
            arr[m] = arr[m - 1];
        }
        arr[j] = tmp      
    }
    return arr;
}

function search(index, arr) {
    let begin = 0;
    let end = index;
    while(begin <  end) {
        let mid = (begin + end) >> 1;
        if(arr[index] < arr[mid]) {
            end = mid;
        } else {
            begin = mid + 1;
        }
    }
    return begin;
}

希尔排序

1,将数组拆分为若干个子分组, 每个分组由相距一定"增量"的元素组成. 比方说将[0,1,2,3,4,5,6,7,8,9,10]的数组拆分为"增量"为5的分组, 那么子分组分别为 [0,5], [1,6], [2,7], [3,8], [4,9] 和 [5,10].
2,然后对每个子分组应用直接插入排序.
3,逐步减小"增量", 重复步骤1,2.
4,直至"增量"为1, 这是最后一个排序, 此时的排序, 也就是对全数组进行直接插入排序.

function shellSort(arr) {
    let len = arr.length, gap = len >> 1, current, i, j;
    while(gap > 0) {
        insertSort(arr, gap);
        gap = gap >> 1;
    }
    return arr;
}
function insertSort(arr, gap) {
    for (let i = gap; i < arr.length; i++) {
        for (let j = i; j > 0; j -= gap) {
            if(arr[j] < arr[j - gap]) {
                [arr[j], arr[j - gap]] = [arr[j - gap], arr[j]];
            }
        }
    }
    return arr;
}

计数排序

计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数

**简单实现
**简单写法有以下缺点:无法对负整数进行排序,及其浪费内存空间,是个不稳定排序

function countingSort(arr) {
    if(arr.length < 2) return arr;
    const maxVal = Math.max(...arr);
    let counts = new Array(maxVal + 1).fill(0);
    arr.forEach(ele => {
        counts[ele]++;
    });
    let sortIdx = 0;
    counts.forEach((count, i) => {
        while(count--) {
            arr[sortIdx++] = i;
        }
    });
    return arr;
}

优化

function countingSort(arr) {
    if(arr.length < 2) return arr;
    let max = Math.max(...arr);
    let min = Math.min(...arr);
    let counts = new Array(max - min + 1).fill(0);
    // 统计每个整数出现的次数
    for (let i = 0; i < arr.length; i++) {
        counts[arr[i] - min]++;
    }
    // 累加次数
    for (let i = 1; i < counts.length; i++) {
        counts[i] += counts[i - 1];
    }
    let newArr = [];
    // 从后往前遍历元素,将它放在有序数组中的合适位置
    for (let i = arr.length - 1; i >= 0; i--) {
        newArr[--counts[arr[i] - min]] = arr[i];
    }
    return newArr;
}

桶排序

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点: 在额外空间充足的情况下,尽量增大桶的数量 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中 同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要 当输入的数据可以均匀的分配到每一个桶中, 最快 当输入的数据被分配到了同一个桶中, 最慢

function bucketSort(arr, bucketSize) {
    if (arr.length === 0) {
      return arr;
    }

    var i;
    var minValue = Math.min(...arr);
    var maxValue = Math.max(...arr);

    //桶的初始化
    var DEFAULT_BUCKET_SIZE = 5;    // 设置桶的默认数量为5
    bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
    var bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;   
    var buckets = new Array(bucketCount);
    for (i = 0; i < buckets.length; i++) {
        buckets[i] = [];
    }

    //利用映射函数将数据分配到各个桶中
    for (i = 0; i < arr.length; i++) {
        buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i]);
    }

    arr.length = 0;
    for (i = 0; i < buckets.length; i++) {
        // 对每个桶进行插入排序
        insertSort(buckets[i]); 
        for (var j = 0; j < buckets[i].length; j++) {
            arr.push(buckets[i][j]);                      
        }
    }

    return arr;
}
function insertSort(arr) {
    for (let i = 1; i < arr.length; i++) {
        for (j = i; j > 0; j--) {
            if(arr[j] < arr[j - 1]) {
                [arr[j], arr[j - 1]] = [arr[j - 1], arr[j]];
            } else {
                break;
            }
        }
    }
    return arr;
}

基数排序

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。 

基数排序 vs 计数排序 vs 桶排序 这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异;
基数排序:根据键值的每位数字来分配桶;
计数排序:每个桶只存储单一键值; 
桶排序:每个桶存储一定范围的数值;

function radixSort(arr, maxDigit) {
    var mod = 10;
    var dev = 1;
    var counter = [];
    for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
        for(var j = 0; j < arr.length; j++) {
            var bucket = parseInt((arr[j] % mod) / dev);
            if(counter[bucket]==null) {
                counter[bucket] = [];
            }
            counter[bucket].push(arr[j]);
        }
        var pos = 0;
        for(var j = 0; j < counter.length; j++) {
            var value = null;
            if(counter[j]!=null) {
                while ((value = counter[j].shift()) != null) {
                      arr[pos++] = value;
                }
          }
        }
    }
    return arr;
}

归并排序

该算法采用分治的思想,把长度为n的输入序列分成两个长度为n/2的子序列; 对这两个子序列分别采用归并排序; 将两个排序好的子序列合并成一个最终的排序序列。

function mergeSort(arr) {
    let len = arr.length;
    if(len < 2) return arr;
    let m = len >> 1, 
        left = arr.slice(0, m),
        right = arr.slice(m);
    return merge(mergeSort(left), mergeSort(right));
}
function merge(left, right) {
    let res = [];
    while(left.length && right.length) {
        let item = left[0] < right[0] ? left.shift() : right.shift();
        res.push(item);
    }
    return res.concat(left.length ? left : right);
}

快速排序

快速排序采用了分治思想, 且基于冒泡排序做了改进.  它将数组拆分为两个子数组, 其中一个子数组的所有元素都比另一个子数组的元素小, 然后对这两个子数组再重复进行上述操作, 直到数组不可拆分, 排序完成.

实现一:浪费大量存储空间,写法简单

function quickSort(arr) {
    if(arr.length < 2) return arr;
    const target = arr[0],
          left = [],
          right = [];
    for (let i = 1; i < arr.length; i++) {
        if(arr[i] < target) {
            left.push(arr[i]);
        } else {
            right.push(arr[i]);
        }
    }
    return quickSort(left).concat([target], quickSort(right));
}

实现二: 双指针思想

记录一个索引low从数组最左侧开始,记录一个索引high从数组右侧开始
在low<high的条件下,找到右侧小于target的值arr[high], 并将其赋值到arr[low];
在low<high的条件下,找到左侧大于target的值arr[low],并将其赋值到arr[high];
这样让low=high时,左侧的值全部小于target,右侧的值全部大于target,将target放到该位置;不需要额外存储空间,写法思路稍复杂

function quickSort(arr, begin, end) {
    if(end - begin < 2) return;
    let mid = pivotIndex(begin, end, arr);
    quickSort(arr, begin, mid);
    quickSort(arr, mid + 1, end);
    return arr;
}
function pivotIndex(begin, end, arr) {
    let pivot = arr[begin];
    end--;
    while(begin < end) {
        while(begin < end) {
            if(pivot < arr[end]) {
                end--;
            } else {
                arr[begin++] = arr[end];
                break;
            }
        }
        while(begin < end) {
            if(pivot > arr[begin]) {
                begin++;
            } else {
                arr[end--] = arr[begin];
                break;
            }
        }
    }
    arr[begin] = pivot;
    return begin;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
quickSort(arr, 0, arr.length);
//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]