我们看看js最常用的排序算法:
- 冒泡排序
- 选择排序
- 插入排序
- 归并排序
- 快速排序
- 计数排序
- 桶排序
下面,我们来看看这些排序是怎么实现的:
冒泡排序
相比于其他排序方式,冒泡排序是最简单的一种排序算法。从运行时间角度考虑,冒泡排序的的性能最差。
冒泡排序通过比较相邻的两项,若第一个比第二个大,就交换它们。这样元素项小的就会像冒泡一样跑到比它大的元素项的前面。
图示:
代码实现:
const Compare = {
LESS_THAN: -1,
BIGGER_THAN: 1,
EQUALS: 0,
};
function defaultCompare(a, b) {
if (a === b) {
return Compare.EQUALS;
}
return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}
function swap(array, a, b) {
[array[a], array[b]] = [array[b], array[a]];
}
/**
* 冒泡排序
* @param {array} array 排序数组
* @param {*} compareFn 比较函数
* @returns
*/
function bubbleSort(array, compareFn = defaultCompare) {
const length = array?.length || 0;
// 外循环,控制趟数,每一次找到一个最大值
for (let i = 0; i < length; i++) {
// 内循环,控制比较的次数,并且判断两个数的大小
for (let j = 0; j < length - 1; j++) {
// 如果前面的数大,放到后面(当然是从小到大的冒泡排序)
if (compareFn(array[j], array[j + 1]) === Compare.BIGGER_THAN) {
swap(array, j, j + 1);
}
}
}
return array; // 执行完的结果返回
}
const list = [2, 4, 5, 1, 3];
console.log(list);
console.log("sort-end", bubbleSort(list));
结果:
选择排序
选择排序是一种原址比较排序算法。
思路是找到数据结构中的最小值并将其放到第一位,接着找到第二小的值并将其放到第二位,以此类推。
图示:
代码实现:
const Compare = {
LESS_THAN: -1,
BIGGER_THAN: 1,
EQUALS: 0,
};
function defaultCompare(a, b) {
if (a === b) {
return Compare.EQUALS;
}
return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}
function swap(array, a, b) {
[array[a], array[b]] = [array[b], array[a]];
}
/**
* 选择排序
* @param {array} array 排序数组
* @param {*} compareFn 比较函数
* @returns
*/
function selectionSort(array, compareFn = defaultCompare) {
const length = array?.length || 0;
let indexMin;
// 控制循环次数
for (let i = 0; i < length - 1; i++) {
// 假设本次循环的第一个值为数组的最小值
indexMin = i;
// 从当前位置寻找最小值,找到则交换
for (let j = i+1; j < length-1; j++) {
if (
compareFn(array[indexMin], array[j + 1]) === Compare.BIGGER_THAN
) {
// 改变最小值至新最小值
indexMin = j+1;
}
}
// 若该最小值和原最小值不同,则交换其值
if (i !== indexMin) {
swap(array, i, indexMin);
}
}
return array;
}
const list = [2, 4, 5, 1, 3];
console.log(list);
console.log("sort-end", selectionSort(list));
输出结果:
插入排序
插入排序每次排一个数组项,以此方式构建最后的排序数组。假定第一项已经排序了。接着,他和第二项进行比较————第二项是应该呆在原位还是插入第一项之前呢?如此,头两项就以正确排序,接着和第三项比较,以此类推。
图示:
代码实现:
const Compare = {
LESS_THAN: -1,
BIGGER_THAN: 1,
EQUALS: 0,
};
function defaultCompare(a, b) {
if (a === b) {
return Compare.EQUALS;
}
return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}
function swap(array, a, b) {
[array[a], array[b]] = [array[b], array[a]];
}
/**
* 插入排序
* @param {array} array 排序数组
* @param {*} compareFn 比较函数
* @returns
*/
function insertionSort(array, compareFn = defaultCompare) {
const length = array?.length || 0;
let temp;
// 循环,给i项寻找正确位置
for (let i = 1; i < length; i++) {
temp = array[i];
let j = i;
while (j > 0 && compareFn(array[j - 1], temp) === Compare.BIGGER_THAN) {
array[j] = array[j - 1];
j--;
}
array[j] = temp;
}
return array;
}
const list = [2, 4, 5, 1, 3];
console.log(list);
console.log("sort-end", insertionSort(list));
输出结果:
应用情景: 排序小型数组时,比选择排序和冒泡排序性能好。
归并排序
归并排序是一种分而治之算法。其思想是将原始数组切分成较小的数组,直到每个小数组只有一个位置,接着将小数组归并成较大数组,直到最后只有一个排序完毕的大数组。
图示:
鉴于是分治法,这里是递归的。
代码实现:
const Compare = {
LESS_THAN: -1,
BIGGER_THAN: 1,
EQUALS: 0,
};
function defaultCompare(a, b) {
if (a === b) {
return Compare.EQUALS;
}
return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}
function swap(array, a, b) {
[array[a], array[b]] = [array[b], array[a]];
}
function merge(left, right, compareFn) {
let i = 0,
j = 0;
const result = [];
while (i < left.length && j < right.length) {
// 比较来自left数组的项是否比来自right数组的项小。如果是,将该项从left数组添加至归并结果数组,并递增用于迭代数组的控制变量;否则从right数组添加项并递增用于迭代数组的控制变量
result.push(
compareFn(left[i], right[j]) === Compare.LESS_THAN
? left[i++]
: right[j++]
);
}
// 将left、right数组所剩余项添加添加到归并数组中
return result.concat(i < left.length ? left.slice(i) : right.slice(j));
}
/**
* 归并排序
* @param {array} array 排序数组
* @param {*} compareFn 比较函数
* @returns
*/
function mergeSort(array, compareFn = defaultCompare) {
const length = array?.length || 0;
if (length > 1) {
// 数组的中间位置
const middle = Math.floor(length / 2);
// 将数组分成两个小数组
const left = mergeSort(array.slice(0, middle), compareFn);
const right = mergeSort(array.slice(middle, length), compareFn);
// 合并和排序小数组产生大数组
array = merge(left, right, compareFn);
}
return array;
}
const list = [2, 4, 5, 1, 3];
console.log(list);
console.log("sort-end", mergeSort(list));
输出结果:
快速排序
快速排序使用分而治之的方法将原始数组分为较小的数组。
图示:
算法步骤:
- 从数组中选择一个值作为主元,也就是数组中间的那个值。
- 创建两个指针,左边一个指向数组第一个值,右边一个指向数组最后一个值。移动左指针直到找到一个比主元大的值,接着,移动右指针直到找到一个比主元小的值,然后交换他们,重复这个过程,直到左指针超过了右指针。这个过程将使得比主元小的值都排在主元之前,而比主元大的值都排在主元之后。这一步叫作划分操作。
- 算法对划分后的小数组(较主元小的值组成的子数组,以及较主元大的值组成的子数组)重复之前的两个步骤,直至数组已完全排序。
代码实现:
const Compare = {
LESS_THAN: -1,
BIGGER_THAN: 1,
EQUALS: 0,
};
function defaultCompare(a, b) {
if (a === b) {
return Compare.EQUALS;
}
return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}
function swap(array, a, b) {
[array[a], array[b]] = [array[b], array[a]];
}
// 划分
function partition(array, left, right, compareFn) {
// 选择中间值作为主元
const pivot = array[Math.floor((left + right) / 2)];
// 初始化两个指针
let i = left,
j = right;
// left、right没有交错,执行划分操作
while (i <= j) {
// 移动left指针,直到找到一个比主元大的元素
while (compareFn(array[i], pivot) === Compare.LESS_THAN) {
i++;
}
// 移动right指针,直到找到一个比主元小的元素
while (compareFn(array[j], pivot) === Compare.BIGGER_THAN) {
j--;
}
// 当左指针指向的元素比主元大且右指针指向的元素比主元小,并且此时左指针索引没有右指针索引大时
if (i <= j) {
// 左项比右项大,我们交换他们
swap(array, i, j);
// 移动两个指针,并重复此过程
i++;
j--;
}
}
// 划分操作结束,返回左指针索引
return i;
}
function quick(array, left, right, compareFn) {
let index;
if (array.length > 1) {
// 对给定子数组执行划分操作
index = partition(array, left, right, compareFn);
// 子数组存在较小值元素
if (left < index - 1) {
quick(array, left, index - 1, compareFn);
}
// 子数组存在较大值元素
if (index < right) {
quick(array, index, right, compareFn);
}
}
return array;
}
/**
* 快速排序
* @param {array} array 排序数组
* @param {*} compareFn 比较函数
* @returns
*/
function quickSort(array, compareFn = defaultCompare) {
return quick(array, 0, array.length - 1, compareFn);
}
const list = [2, 4, 5, 1, 3];
console.log(list);
console.log("sort-end", quickSort(list));
输出结果:
计数排序
计数排序是一个分布式排序。
分布式排序:使用已组织好的辅助数据结构(称为桶),然后进行合并,得到排序好的数组。
计数排序使用一个用来存储每个元素在原始数组中出现次数的临时数组。在所有元素都计数完成后,临时数组已排好序并可迭代已构建排序后的结果数组。
图示:
代码实现:
// 数组中的最大值
function findMaxValue(array) {
let max = array[0];
for (let i = 0; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
}
return max;
}
/**
* 快速排序
* @param {array} array 排序数组
* @param {*} compareFn 比较函数
* @returns
*/
function countingSort(array, compareFn = defaultCompare) {
if (array.length < 2) return array;
const maxValue = findMaxValue(array);
const counts = new Array(maxValue);
array.forEach((v) => {
if (!counts[v]) {
counts[v] = 0;
}
counts[v]++;
});
let sortIndex = 0;
counts.forEach((v, i) => {
while (v > 0) {
array[sortIndex++] = i;
v--;
}
});
return array;
}
const list = [2, 4, 5, 3, 1, 3];
console.log(list);
console.log("sort-end", countingSort(list));
应用:
是用来排序整数的优秀算法。缺点:需要更多的内存来存放临时数组。
桶排序
桶排序(也称箱排序)也是分布式排序算法,其将元素分为不同的桶(较小数组),再使用一个简单的排序算法,如插入排序,来对每个桶进行排序,然后将所有的桶合并为结果数组。
图示:
代码实现:
const Compare = {
LESS_THAN: -1,
BIGGER_THAN: 1,
EQUALS: 0,
};
function defaultCompare(a, b) {
if (a === b) {
return Compare.EQUALS;
}
return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}
function swap(array, a, b) {
[array[a], array[b]] = [array[b], array[a]];
}
/**
* 插入排序
* @param {array} array 排序数组
* @param {*} compareFn 比较函数
* @returns
*/
function insertionSort(array, compareFn = defaultCompare) {
const length = array?.length || 0;
let temp;
// 循环,给i项寻找正确位置
for (let i = 1; i < length; i++) {
temp = array[i];
let j = i;
while (j > 0 && compareFn(array[j - 1], temp) === Compare.BIGGER_THAN) {
array[j] = array[j - 1];
j--;
}
array[j] = temp;
}
return array;
}
/**
*创建桶
* @param {*} array
* @param {*} bucketSize
*/
function createBuckets(array, bucketSize) {
let minValue = array[0];
let maxValue = array[0];
// 找到最大值、最小值
for (let i = 0; i < array.length; i++) {
const element = array[i];
if (element < minValue) {
minValue = element;
} else if (element > maxValue) {
maxValue = element;
}
}
// 计算每个桶中要分布的元素个数
const bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
const buckets = new Array(bucketCount);
// 初始化每个桶,数据结构是一个矩阵
for (let i = 0; i < bucketCount; i++) {
buckets[i] = [];
}
for (let i = 0; i < array.length; i++) {
// 计算元素放入那个桶
const bucketIndex = Math.floor((array[i] - minValue) / bucketSize);
buckets[bucketIndex].push(array[i]);
}
return buckets;
}
/**
* 排序
* @param {*} buckets
*/
function sortBuckets(buckets) {
const sortedArray = [];
for (let i = 0; i < buckets.length; i++) {
// 每个桶插入排序
insertionSort(buckets[i]);
sortedArray.push(...buckets[i]);
}
return sortedArray;
}
/**
* 桶排序
* @param {array} array
* @param {number} bucketSize 多少个桶
* @returns
*/
function bucketSort(array, bucketSize = 5) {
if (array.length < 2) return array;
// 创建桶并将元素分布到不同的桶中
const buckets = createBuckets(array, bucketSize);
// 对每个桶执行排序算法和将所有桶合并为排序后的结果数组
return sortBuckets(buckets);
}
const list = [2, 4, 5, 3, 1, 3, 10, 9, 8];
console.log(list);
console.log("sort-end", bucketSort(list));
输出结果: