JavaScript数据结构与算法 - 排序算法
Posted 友人A ㅤ
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript数据结构与算法 - 排序算法相关的知识,希望对你有一定的参考价值。
1. 冒泡排序
冒泡排序比较所有相邻的两个项,如果第一个比第二个大,则交换它们。元素项向上移动至正确的顺序,就好像气泡升至表面一样。但是,从运行时间的角度来看,冒泡排序是最差的一个。
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 bubbleSort(array, compareFn = defaultCompare) {
// 存储数组长度
const { length } = array;
// 从数组第一位迭代到最后一位,控制了在数组中经过了多少轮排序
for (let i = 0; i < length; i++) {
// 从第一位迭代到倒数第二位,从内循环中减去外循环中已跑过的轮数,可以避免内循环中所有不必要的比较
for (let j = 0; j < length - 1 - i; j++) {
// 当前项比下一项大,交换顺序
if (compareFn(array[j], array[j + 1]) === Compare.BIGGER_THAN) {
swap(array, j, j + 1);
}
}
}
}
function swap(array, a, b) {
[array[a], array[b]] = [array[b], array[a]];
}
冒泡排序工作过程:
改进后的冒泡排序:如果从内循环减去外循环中已跑过的轮数,就可以避免内循环中所有不必要的比较
时间复杂度: O(n²)
2. 选择排序
思路:找到数据结构中的最小值并将其放置在第一位,接着找到第二小的值并将其放在第二位,以此类推。
时间复杂度: O(n²)
3. 插入排序
插入排序:每次排一个数组项,以此方式构建最后的排序数组。
- 假定第一项已经排序了
- 接着,它和第二项进行比较——第二项是应该待在原位还是插到第一项之前
- 接着和第三项比较(它是该插入到第一、第二还是第三的位置)
- 以此类推
function insertionSort(array, compareFn = defaultCompare) {
const { length } = array;
let temp;
// 迭代数组给第i项找到正确的位置
for (let i = 1; i < length; i++) {
// 用i的值来初始化一个辅助变量
let j = i;
// 将i的值存储在临时变量中
temp = array[i];
// 只要j比0大,并且数组中前面的值比待比较的值大
while (j > 0 && compareFn(array[j - 1], temp) === Compare.BIGGER_THAN) {
// 将这个值移到当前位置上并减小j
array[j] = array[j - 1];
j--;
}
array[j] = temp;
}
return array;
}
时间复杂度: O(n²)
4. 归并排序
归并排序是可以实际使用的排序算法。
归并排序是一种分而治之算法。
算法思想:将原始数组切分成较小的数组,直到每个小数组只有一个位置,接着将小数组归并成较大的数组,直到最后只有一个排序完毕的大数组。
function mergeSort(array, compareFn = defaultCompare) {
// 算法是递归的,需要一个停止条件:判断数组长度是否为1
if (array.length > 1) {
const { length } = array;
// 找到数组的中间位
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;
}
// merge函数,负责合并和排序小数组来产生大数组,直到回到原始数组并已排序完成
function merge(left, right, compareFn) {
// 声明归并过程要创建的新数组和用来迭代两个数组所需的两个变量
let i = 0;
let j = 0;
const result = [];
// 比较来自left数组的项是否比来自right数组的项小
while (i < left.length && j < right.length) {
// 如果是,将该项从left数组添加至归并结果数组,并递增用于迭代数组的控制变量
// 如果不是,从right数组添加项并递增用于迭代数组的控制变量
result.push(compareFn(left[i], right[i]) === Compare.LESS_THAN ? left[i++] : right[j++]);
}
// 将left数组所有剩余的项添加到归并数组中,right数组也一样
return result.concat(i < left.length ? left.slice(i) : right.slice(i));
}
可以看到,算法首先将原始数组分割直至只有一个元素的子数组,然后开始归并。
时间复杂度: O(nlog(n))
5. 快速排序
常用算法之一。
快速排序使用分而治之的方法,将原始数组分为较小的数组。
算法思想:
- 从数组中选择一个值作为主元(pivot),也就是数组中间的那个值
- 创建两个指针(引用),左边一个指向数组第一个值,右边一个指向数组最后一个值
- 划分操作:移动左指针直到找到一个比主元大的值,接着,移动右指针直到找到一个比主元小的值,然后交换它们。重复这个过程,直到左指针超过了右指针。这个过程将使得比主元小的值都排在主元之前,而比主元大的值都排在主元之后
- 算法对划分后的小数组(较主元小的值组成的子数组,以及较主元大的值组成的子数组)重复之前的步骤,直至数组已完全排序
// 声明一个主方法来调用递归函数,传递待排序数组、索引0及其最末的位置作为参数
function quickSort(array, compareFn = defaultCompare) {
return quick(array, 0, array.length - 1, compareFn);
}
// 创建quick函数
function quick(array, left, right, compareFn) {
// index能帮助将子数组分离为较小值和较大值数组
let index;
// 如果数组长度比1大,就执行partition操作
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;
};
// 划分过程
function partition(array, left, right, compareFn) {
// 选择中间值作为主元
const pivot = array[Math.floor((right + left) / 2)];
// 初始化两个指针,为数组第一个元素和最后一个元素
let i = left;
let j = 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);
// 移动指针,并从外层while处开始重复此过程
i++;
j--;
}
}
// 划分操作结束后,返回左指针索引,用来在index = partition(array, left, right, compareFn)处创建子数组
return i;
}
实现图例:
给定数组[3, 5, 1, 6, 4, 7, 2],划分操作第一次执行:
对有较小值的子数组执行的划分操作:
有较大值的子数组的划分:
对子数组[2, 3, 5, 4]中的较小子数组[2, 3]继续进行划分:
子数组[2, 3, 5, 4]中的较大子数组[5, 4]也继续进行划分:
最终,较大子数组[6, 7]也会进行划分操作,快速排序算法的操作执行完成。
时间复杂度:O(nlog(n)),性能要比其他O(nlog(n))的好。
6. 计数排序
计数排序是分布式排序,是一个整数排序算法。
分布式排序使用已组织好的辅助数据结构(称为桶),然后进行合并,得到排好序的数组。
思路:使用一个用来存储每个元素在原始数组中出现次数的临时数组。在所有元素都计数完成后,临时数组已排好序并可迭代以构建排序后的结果数组。
function countingSort(array) {
// 如果数组为空或只有一个元素,不需要运行排序算法
if (array.length < 2) {
return array;
}
// 找到数组中的最大值
const maxValue = findMaxValue(array);
// 创建计数数组,从索引0开始直到最大值索引value+1
const counts = new Array(maxValue + 1);
// 迭代数组中的每个位置
array.forEach(element => {
// 确保递增操作成功。如果counts数组中用来计数某个元素的位置一开始没有用0初始化的话,将其值赋值为0
if (!counts[element]) {
counts[element] = 0;
}
// 在counts数组中增加元素计数值
counts[element]++;
});
let sortedIndex = 0;
counts.forEach((count, i) => {
// 减少计数值直到它为0
while (count > 0) {
array[sortedIndex++] = i;
count--;
}
});
return array;
}
function findMaxValue(array) {
let max = array[0];
for (let i = 1; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
}
return max;
}
时间复杂度:O(n+k),其中k是临时计数数组的大小。
7. 桶排序
桶排序(也被称为箱排序),分布式排序算法。
思路:将元素分为不同的桶(较小的数组),再使用一个简单的排序算法,例如插入排序(用来排序小数组的不错的算法),来对每个桶进行排序。然后,它将所有的桶合并为结果数组。
// 指定需要多少桶来排序数组
function bucketSort(array, bucketSize = 5) {
if (array.length < 2) {
return array;
}
// 创建桶并将元素分不到不同的桶中
const buckets = createBuckets(array, bucketSize);
// 对每个桶执行插入排序算法和将所有的桶合并为排序后的结果数组
return sortBuckets(buckets);
}
function createBuckets(array, bucketSize) {
let minValue = array[0];
let maxValue = array[0];
// 迭代原数组找到最小值和最大值
for(let i = 1; i < array.length; i++) {
if (array[i] < minValue) {
minValue = array[i];
} else if (array[i] > maxValue) {
maxValue = array[i];
}
}
// 计算每个桶中需要分布的元素个数
const bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
const buckets = [];
// 初始化每个桶
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;
}
function sortBuckets(buckets) {
// 创建一个用做结果数组的新数组,表示数组不会被修改,会返回一个新数组
const sortedArray = [];
// 迭代每个可迭代的桶并应用插入排序
for (let i = 0; i < buckets.length; i++) {
if (buckets[i] != null) {
insertionSort(buckets[i]);
// 将排好序的桶中的所有元素加入结果数组
sortedArray.push(...buckets[i]);
}
}
return sortedArray;
}
8. 基数排序
基数排序是一个分布式排序算法。
思想:根据数字的有效位或基数将整数分布到桶中。基数是基于数组中值的记数制的。
function radixSort(array, radixBase = 10) {
if (array.length < 2) {
return array;
}
const minValue = findMinValue(array);
const maxValue = findMaxValue(array);
// 从最后一位开始排序所有的数
let significantDigit = 1;
// 首先基于最后一位有效位对数字进行排序
// 在下次迭代时,会基于第二个有效位进行排序,然后第三位,以此类推,直到没有待排序的有效位
while ((maxValue - minValue) / significantDigit >= 1) {
array = countingSortForRadix(array, radixBase, significantDigit, minValue);
significantDigit *= radixBase;
}
return array;
}
// 基于有效位(基数)排序
function countingSortForRadix(array, radixBase, significantDigit, minValue) {
let bucketsIndex = [];
const buckets = [];
const aux = [];
// 基于基数初始化桶(基于十进制就需要十个桶)
for (let i = 0; i < radixBase; i++) {
buckets[i] = 0;
}
// 基于数组中数的有效位进行计数排序
for (let i = 0; i < array.length; i++) {
bucketsIndex = Math.floor(((array[i] - minValue) / significantDigit) % radixBase);
buckets[bucketsIndex]++;
}
// 计算累积结果来得到正确的计数值
for (let i = 1; i < radixBase; i++) {
buckets[i] += buckets[i - 1];
}
// 计数完成后,要开始将值移回原始数组中
for (let i = array.length - 1; i >= 0; i--) {
// 对原始数组中的每个值,再次获取它的有效位并将它的值移到aux数组中
bucketsIndex = Math.floor(((array[i] - minValue) / significantDigit) % radixBase);
aux[--buckets[bucketsIndex]] = array[i];
}
// 这步可选。将aux数组中的每个值转移到原始数组中
for (let i = 0; i < array.length; i++) {
array[i] = aux[i];
}
// 除了返回array,也可以直接返回aux数组而不需要赋值它的值
return array;
}
以上是关于JavaScript数据结构与算法 - 排序算法的主要内容,如果未能解决你的问题,请参考以下文章