计数排序、基数排序、归并排序、桶排序、堆排序思路,代码实现,及各算法之间的比较……
一 计数排序
计数排序的基本思想是,计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
计数排序,是一种牺牲空间换取时间的排序算法,一定范围内整数排序时,快于任何比较排序算法。
步骤:
- 找出乱序数组中最大的元素,建立一个最大元素 + 1长度的新数组;新数组的下标对应乱序数组的值。
- 统计乱序数组中每个值出现的次数,依次放入新数组中;
- 遍历新数组,依次去除新数组中的元素。
计数排序的代码实现
// 计数排序方法
function countingSort(arr, max) {
// 创建一个新数组,用来统计数组中每个元素出现的次数
let middle = new Array(max + 1);
// 遍历数组,把每个元素出现的次数记录在新数组的相应位置
for(let i = 0,len = arr.length;i < len;i ++) {
// 如果元素未出现过,则置为1
if (!middle[arr[i]]) {
middle[arr[i]] = 1;
} else {
// 已经出现过的元素次数 + 1
middle[arr[i]] ++;
}
}
// 从第几个元素开始排序
let startIndex = 0;
// 遍历新数组,依次取出元素(新数组的下标对应乱序数组的值)
for(let j = 0;j <= max;j ++) {
while (middle[j] > 0) {
arr[startIndex] = j;
// 对应元素的次数 - 1
middle[j] --;
// 排序索引增加
startIndex ++;
}
}
return arr;
}
// 验证
countingSort([9, 9, 3, 3, 7, 7, 4, 8, 1, 8, 1, 7, 8, 4, 7, 2, 0, 4], 9);
计数排序改进:
/**
* 如果元素数组是这样的:[90, 99, 99, 90, 91, 91, 96, 96, 98, 98, 93, 93, 92, 92]
* 那么上述方法会创建一个长度为 100 的数组,消耗较多的内存
*/
function countingSort(arr) {
// 定义最大值和最小值为数组第一个元素
let max = arr[0], min = arr[0];
// 查找数组中的最大值和最小值
for (let i = 0,len = arr.length; i < len; i ++) {
if (arr[i] > max) {
max = arr[i]
}
if (arr[i] < min) {
min = arr[i]
}
}
// 根据最大值和最小值创建新的数组 新数组长度为 max - min + 1
let middle = new Array(max - min + 1);
let startIndex = 0;
for(let i = 0,len = arr.length; i < len; i ++) {
// 新添加,我们要处理的索引为当期值减去最小值之后的索引。之后的步骤类似
let index = arr[i] - min;
if (!middle[index]) {
middle[index] = 1
} else {
middle[index]++
}
}
for(let j = 0; j <= max - min; j ++) {
while (middle[j] > 0) {
arr[startIndex] = j + min;
middle[j] --;
startIndex++
}
}
return arr;
}
countingSort([90, 99, 99, 90, 91, 91, 96, 96, 98, 98, 93, 93, 92, 92]);
二 基数排序
基数排序,是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
基数排序属于稳定的排序,在某些时候,基数排序效率高于其他稳定性排序方法。
基数排序的两种实现方式:
- LSD: 从元素的最低位开始排序;
- MSD: 从元素的最高位开始排序。
LSD 的实现及图解
- 第一次循环,根据个位排序。
- 第二次循环,根据十位排序。
- 第三次循环,根据百位排序
function radixSort (arr) {
// 在数组中找最大值
let max = arr[0];
let len = arr.length;
for(let i = 0; i < len; i ++) {
if (max < arr[i]) {
max = arr[i]
}
}
// 获取最大值的位数,如:10 两位数 100 三位数 1000四位数
let digits = `${max}`.length;
// 根据位数来决定循环次数
for (let i = 0; i < digits; i ++) {
sort(arr, i)
}
console.log(arr)
}
function sort(arr, round) {
// 创建一个元素容器 (桶)
let result = [];
// 根据位数创建对应的桶
for (let i = 0; i < 10; i++) {
result[i] = [];
}
// 将数组中的数放进对应的桶子中
for (let i = 0; i < arr.length; i++) {
let middle = arr[i] / (Math.pow(10, round));
// 得到目标桶的坐标
let index = Math.floor(middle % 10);
// 把元素放入到对应的桶中
result[index].push(arr[i]);
}
let index = 0;
// 在桶中将元素取出,放入原数组
for (let i = 0; i < result.length; i++) {
for (let j = 0; j < result[i].length; j++) {
// 从每个桶中取元素
arr[index++] = result[i][j];
}
}
}
radixSort([10, 5, 5, 50, 0, 155, 422, 5, 1, 4, 254]);
MSD 的实现及图解
function radixSort (arr) {
let max = arr[0];
let len = arr.length;
for(let i = 0;i < len;i ++) {
if (max < arr[i]) {
max = arr[i]
}
}
let digits = `${max}`.length;
sort(arr, digits - 1);
// 得到有序数组
console.log(arr)//[ 0, 1, 4, 5, 5, 5, 10, 50, 155, 254, 422 ]
}
function sort(arr, round) {
if (round < 0) {
return;
}
let result = [];
// 根据位数创建对应的桶
for (let i = 0; i < 10; i++) {
result[i] = [];
}
// 将数组中的数放进对应的桶子中
for (let i = 0; i < arr.length; i++) {
let middle = arr[i] / (Math.pow(10, round));
// 得到目标桶的坐标
let index = Math.floor(middle % 10);
// 把元素放入到对应的桶中
result[index].push(arr[i]);
}
// 如果存在某一位未排序,递归排序
for (let j = 0,len = result.length;j < len;j ++) {
if (result[j].length > 1) {
sort(result[j], --round)
}
}
let index = 0;
// 直到左右元素都排序结束,将桶中将元素取出,放入原数组
for (let i = 0; i < result.length; i++) {
for (let j = 0; j < result[i].length; j++) {
arr[index++] = result[i][j];
}
}
}
radixSort([10, 5, 5, 50, 0, 155, 422, 5, 1, 4, 254]);
三 归并排序
归并排序,采用分治法(Divide and Conquer)先使子序列有序,再将已有序的子序列合并,得到完全有序的序列。
实现步骤:
- 把长度为 n 的数组分成两个长度为 n/2 的子数组。
- 以相同方法继续拆分子数组,直至最后的子数组长度为 1。
- 子数组间两两排序,合并,直到得到一个有序数组。
function mergeSort(arr) {
const len = arr.length;
// 如果数组长度为1,则递归终止
if (len === 1) {
return arr;
}
let left = [];
let right = [];
const middle = Math.floor(len / 2);
for (let i = 0;i < len;i ++) {
i < middle ? left.push(arr[i]) : right.push(arr[i])
}
// 将数组拆分到只有单元素的时候才开始合并, 注意递归
return merge(mergeSort(left), mergeSort(right));
}
// 合并子数组的函数
function merge(left, right) {
let result = [];
let l = 0;
let r = 0;
// 根据left和right中元素的大小排序
while(l < left.length && r < right.length) {
// 如果 左 < 右 将左数组中的元素放入结果数组
if (left[l] < right[r]) {
result.push(left[l]);
l++;
} else {
// 如果 左 > 右 将右数组中的元素放入结果数组
result.push(right[r]);
r++;
}
}
/*
* 在上面循环只处理了 左/右 中的一个
* 下面的循环处理另一个
*/
while (l < left.length) {
result.push(left[l]);
l++;
}
while(r < right.length) {
result.push(right[r]);
r++;
}
return result;
}
// 排序好的数组
mergeSort([10, 5, 4, 50, 0, 155, 422, 90]); // [ 0, 4, 5, 10, 50, 90, 155, 422]
四 桶排序
桶排序,是计数排序的升级版,先将数组元素分发到有限数量的桶里,每个桶分别排序,最后合并得到一个有序数组的过程。
实现步骤:
- 设置空桶
- 将数据放到对应的空桶中
- 将每个不为空的桶进行排序
- 拼接不为空的桶
function bucketSort (arr) {
if (arr.length <= 1) {
return arr;
}
// 默认创建 5 个桶容器
const bucketCount = 5;
// 初始化需要的参数
let len = arr.length;
// 用来排序的桶
let barrel = [];
// 用来存放排序结果
let result = [];
let max = arr[0];
let min = arr[0];
// 寻找到数组中的最大值和最小值
for (let i = 1; i < len; i++) {
arr[i] >= max ? max = arr[i] : arr[i] <= min ? min = arr[i] : '';
}
// 求出每一个桶的数值范围
let space = (max - min + 1) / bucketCount;
// 将数值装入桶中
for (let i = 0; i < len; i++) {
// 找到相应的桶序列
let index = Math.floor((arr[i] - min) / space);
// 判断是否桶中已经有数值
if (barrel[index]) {
let bucket = barrel[index];
let k = bucket.length - 1;
// 使用插入排序方法将数组从小到大排列
while (k >= 0 && barrel[index][k] > arr[i]) {
barrel[index][k + 1] = barrel[index][k];
k--
}
barrel[index][k + 1] = arr[i];
} else {
// 否则,新建容器并添加数据
barrel[index] = [];
barrel[index].push(arr[i]);
}
}
// 开始合并数组
let n = 0;
while (n < bucketCount) {
// 将不为空的数组合并
if (barrel[n]) {
result = result.concat(barrel[n]);
}
n++;
}
return result;
}
//开始排序
bucketSort([12, 4, 3, 2, 5, 84, 34, 52, 42, 45, 6, 7, 86, 68, 67]);
五 堆排序
堆排序,是用堆这种数据结构来实现排序的过程,根据 小根堆(大根堆) 移除堆顶元素的过程使得数组有序。
实现步骤
- 创建一个最大堆。
- 将堆顶元素放到数组的末尾
- 将剩下的元素再次调整为一个最大堆。
- 重复上述步骤,直至数组有序。
// 排序方法
function heapSort(arr) {
// 获得数组的长度
let len = arr.length;
// 首先创建一个最大堆
createHeap(arr, len);
while(len > 0) {
let last = len - 1;
// 将最大元素保存到数组末尾
swap(arr, 0, last);
len--;
// 然后将剩下的元素继续构建为一个新的最大堆
adjustHeap(arr, 0, len);
}
// 返回排序好的数组
return arr;
}
function createHeap(arr, len) {
// 构建最大堆。。Math.floor(len / 2) => 数组后半部分的元素都是叶节点,无需进行大小判断
for (let i = Math.floor(len / 2); i >= 0; i--) {
adjustHeap(arr, i, len);
}
}
// 调整堆
function adjustHeap(arr, i, len) {
// 获得根元素的左子元素坐标
let left = 2 * i + 1;
// 获得根元素的右子元素坐标
let right = 2 * i + 2;
// 保存做大值坐标
let largest = i;
if (left < len && arr[left] > arr[largest]) {
largest = left;
}
if (right < len && arr[right] > arr[largest]) {
largest = right;
}
// 如果 i 不是最大值
if (largest !== i) {
// 将最大值放到父节点
swap(arr, i, largest);
// 递归调整。一步步将堆调整到最大堆
adjustHeap(arr, largest, len);
}
}
// 交换函数
function swap(arr, i, j) {
[arr[i], arr[j]] = [arr[j], arr[i]];
}
六 算法比较
说明
-
稳定:如果 a 原本在 b 前面,而 a = b,排序之后 a 仍然在 b 的前面;
-
不稳定:如果 a 原本在 b 的前面,而 a = b,排序之后 a 可能会出现在 b 的后面;
-
内排序(in-place):所有排序操作都在内存中完成;
-
外排序(out-place):由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;