概述
最近项目里面用到了排序相关功能,多数情况下,js用数组内置的sort函数就可以了,这里顺便总结下常见的几种排序算法js的实现,也算是对经典排序算法的更加深入理解。
对比各种排序算法
实现
注:文章实现的排序方法全部以升序排序。
插入排序
思路分析
- 从数组第二个元素开始,当前位置和前面位置比较,当小于前一个位置,进行交换,当前指针前移。
- 重复1的步骤,继续比较前面的位置和前面的前面的数值。
- i++,重复以上步骤,直到i=n-1结束,排序完成
动图演示:
代码实现:
function insertSort(arr) {
let len = arr.length;
for (let i = 1; i < len; i++) {
let prev = now = i - 1;
while (now >= 0) {
if (arr[i] < arr[now]) {
prev = now;
now --;
} else {
break;
}
}
if (prev >= 0) {
swap(arr, i, prev)
}
}
function swap(arr, i, j) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
return arr;
}
希尔排序(插入排序的升级版)
思路分析
- 在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式
- 这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2...1},称为增量序列。
- 对增量步骤的数值进行插入排序,知道最后gap为0
动图演示:
代码实现:
function shellSort(arr) {
let len = arr.length;
// 定义初始
let gap = Math.floor(len / 2);
while (gap > 0) {
for (let i = gap; i < len; i += gap) {
let now = i;
let prev = now - gap;
while (prev >= 0) {
if (arr[now] < arr[prev]) {
swap(arr, now, prev);
now -= gap;
prev -= gap;
} else {
break;
}
}
}
gap = Math.floor(gap / 2);
}
function swap(arr, i, j) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
return arr;
}
选择排序
思路分析
- 选择排序在开始的时候,先扫描整个列表,以找到列表中的最小元素,然后将这个元素与第一个元素进行交换。这样最小元素就放到它的最终位置上。
- 然后,从第二个元素开始扫描,找到n-1个元素中的最小元素,然后再与第二个元素进行交换。
- 以此类推,直到第n-1个元素(如果前n-1个元素都已在最终位置,则最后一个元素也将在最终位置上)。
动图演示:
代码实现:
function selectSort(arr) {
let len = arr.length;
for (let i = 0; i < len; i++) {
let minIndex = i;
for (let j = i + 1; j < len; j++) {
if(arr[j] < arr[minIndex]) {
minIndex = j;
}
}
swap(arr, i, minIndex)
}
function swap(arr, i, j) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
冒泡排序
思路分析
- 相邻两个数两两相比,n[i]跟n[j+1]比,如果n[i]>n[j+1],则将连个数进行交换
- j++, 重复以上步骤,第一趟结束后,最大数就会被确定在最后一位,这就是冒泡排序又称大(小)数沉底
- i++,重复以上步骤,直到i=n-1结束,排序完成
动图演示:
代码实现:
function bubbleSort(arr) {
let len = arr.length;
for (let i = len - 1; i >= 0; i--) {
for (let j = 0; j <= i; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1);
}
}
}
function swap(arr, i, j) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
return arr;
}
快速排序
思路分析
- 从数列中挑出一个元素,作为基准值,这里实现选择第一个元素;
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。
- 递归地把小于基准值元素的子数列和大于基准值元素的子数列排序,最后拼接(回溯)。
动图演示:
代码实现:
function quickSort(arr) {
if (!arr.length) return [];
let leftArr = [];
let rightArr = [];
let cur = arr[0];
for (let i = 1; i < arr.length; i++) {
if(arr[i] > cur) {
rightArr.push(arr[i])
}else{
leftArr.push(arr[i])
}
}
return [...quickSort(leftArr),cur,...quickSort(rightArr)]
}
堆排序(分为大顶堆和小顶堆)
思路分析
- 堆它是选择排序的一种,它是通过堆来进行选择数据。(升序要建大堆,降序建小堆)
- 首先是创建堆,即把待排序的序列转换成堆的形式。
- 然后将根节点与最后一个节点交换。
- 交换之后会打乱堆的规律,需要对前(n-1)个节点进行调整,使之重新成为堆。
- 接下来交换根节点与倒数第二个节点 …… 重复上述调整,直到序列有序为止。(建堆、交换、调整)
动图演示:
代码实现:
// 堆排序(大顶堆)
function bigHeapSort(arr) {
let len = arr.length;
// 大顶堆序列化
function heapify(arr, len, index) {
let max = index;
let leftNodeIndex = index * 2 + 1;
let rightNodeIndex = index * 2 + 2;
if (leftNodeIndex < len && arr[leftNodeIndex] > arr[max]) {
max = leftNodeIndex;
}
if(rightNodeIndex < len && arr[rightNodeIndex] > arr[max]) {
max = rightNodeIndex;
}
// 父节点比两个子节点任意一个小,进行交换,递归维护大顶堆的特性
if (max != index) {
swap(arr, index, max);
heapify(arr, len, max);
}
}
// 构建大顶堆
for (let i = Math.floor((len - 1) / 2); i >= 0; i--) {
heapify(arr, len, i);
}
// 每次和后面一个元素交换,这样堆顶的元素就跑到后面去了
for (let j = len - 1; j > 0; j--) {
swap(arr, j, 0);
heapify(arr, j, 0);
}
function swap(arr, i, j) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
return arr;
}
// 堆排序(小顶堆)
function smallHeapSort(arr) {
let len = arr.length;
// 大顶堆序列化
function heapify(arr, len, index) {
let min = index;
let leftNodeIndex = index * 2 + 1;
let rightNodeIndex = index * 2 + 2;
if (leftNodeIndex < len && arr[leftNodeIndex] < arr[min]) {
min = leftNodeIndex;
}
if(rightNodeIndex < len && arr[rightNodeIndex] < arr[min]) {
min = rightNodeIndex;
}
// 父节点比两个子节点任意一个小,进行交换,递归维护大顶堆的特性
if (min != index) {
swap(arr, index, min);
heapify(arr, len, min);
}
}
// 构建小顶堆
for (let i = Math.floor((len - 1) / 2); i >= 0; i--) {
heapify(arr, len, i);
}
// 每次和后面一个元素交换,这样堆顶的元素就跑到后面去了
for (let j = len - 1; j > 0; j--) {
swap(arr, j, 0);
heapify(arr, j, 0);
}
function swap(arr, i, j) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
return arr;
}
归并排序
思路分析
- 思路和快速排序类似,采用分而治之思想,递归回溯
- 和快速排序不同在于,归并排序需要递归处理子问题的有序数组进行合并,最终返回
- 归并排序以空间换时间
动图演示:
代码实现:
function mergeSort(arr) {
// 递归函数
function merge(arr) {
if (arr.length <= 1) return arr;
let left = 0;
let middle = Math.floor((arr.length - 1) / 2);
let right = arr.length;
let leftArr = merge(arr.slice(left, middle + 1));
let rightArr = merge(arr.slice(middle + 1, right));
// 分而治之,进行回溯合并两个有序数据
return helper(leftArr, rightArr);
}
// 用于排列两个有序数组
function helper(arr1, arr2) {
let res = [];
let i = 0;
let j = 0;
while (i < arr1.length && j < arr2.length) {
if (arr1[i] <= arr2[j]) {
res.push(arr1[i]);
i++;
} else {
res.push(arr2[j]);
j++;
}
}
if (i != arr1.length) {
for (let m = i; m < arr1.length; m++) {
res.push(arr1[m])
}
}
if (j != arr2.length) {
for (let n = j; n < arr2.length; n++) {
res.push(arr2[n])
}
}
return res;
}
return merge(arr);
}
计数排序(最简单)
思路分析
1、找出待排序的数组中最大和最小的元素
2、统计数组中每个值为i的元素出现的次数,存入数组C的第i项
3、对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
4、反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
动图演示:
代码实现:
function countSort(arr) {
let res = [];
let len = arr.length;
let list = new Array(len);
for (let i = 0; i < len; i++) {
list[arr[i]] = list[arr[i]] != undefined ? list[arr[i]] + 1 : 1;
}
for (let j = 0; j < list.length; j++) {
if (list[j]) {
let count = list[j];
while (count > 0) {
res.push(j)
count--;
}
}
}
return res;
}
桶排序
思路分析
- 创建桶
- 将元素放入桶(桶可以使用链表或者数组)
- 每个桶里的元素,做好排序
- 遍历所有桶,放入到结果数组中
动图演示:
代码实现:
function bucketsort(arr) {
// 计算最大值,最小值
let max = min = arr[0];
let len = arr.length;
for (let i = 0; i < len; i++) {
max = Math.max(max, arr[i])
min = Math.min(min, arr[i])
}
// 计算桶的数量
// [3, 1, 7, 10, 2, 3, 6, 5, 4, 2];
let bucketNum = Math.floor((max - min) / arr.length) + 1;
// 通过二维数组保存桶元素
let bucketList = new Array(bucketNum).fill(0).map(item => []);
// 将数组元素放到对应的桶当中
for (let j = 0; j < len; j++) {
// 计算当前元素对应桶的索引位置,将当前元素放到正确的桶当中
let bucketIndex = Math.floor((arr[j] - min) / len);
bucketList[bucketIndex].push(arr[j]);
}
// 对桶中的元素进行排序
for (let k = 0; k < bucketList.length; k++) {
bubbleSort( bucketList[k])
}
// 拆分输出
return bucketList.flat(1);
}
基数排序
思路分析
- 基数排序(Radix Sort)是桶排序的扩展,它的基本思想是:将整数按位数切割成不同的数字,然后按每个位数分别比较。
- 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。
- 然后,从最低位开始,依次进行一次排序
- 这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
- ps:基数排序只可以应用于整数。
动图演示:
代码实现:
function radixSort(arr) {
// 初始化桶
let radixList = new Array(10).fill(0).map(item => []);
// 结果
let res = arr;
// 最大值
let max = Math.max(...arr);
// 最大值的位数
let maxLen = max.toString().length;
// 数组长度
let len = arr.length;
// 初始索引
let init = 1;
while (init <= maxLen) {
// 重置桶中数据
radixList = new Array(10).fill(0).map(item => []);
// 依次对个十百各个数位进行重新排列
for (let i = 0; i < len; i++) {
let currentDigitalString = res[i].toString();
let currentDigitalStringLen = currentDigitalString.length;
let currentDigitalNumber = currentDigitalStringLen >= init ? currentDigitalString[currentDigitalStringLen - init] * 1 : 0;
radixList[currentDigitalNumber].push(res[i]);
}
// 从新排列数组
res = [];
for (let i = 0; i < radixList.length; i++) {
let currentLen = radixList[i].length;
if (currentLen) {
for (let j = 0; j < currentLen; j++) {
res.push(radixList[i][j])
}
}
}
init++;
}
return res;
}