原理部分是参考一文搞定十大排序算法 的这篇文章, 该文只是借鉴学习使用, 并用js的方法做了还原.
-
冒泡排序
冒泡排序(Bubble Sort),顾名思义类似于水中冒泡,较大的数沉下去,较小的数慢慢冒起来。通过交换元素位置,达到排序目的,是一种交换排序。
步骤:
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 重复步骤1~3,直到排序完成。
图解:
代码实现一: 快慢指针
let nums = [33, 4, 64, 234, 23, 2, 15, 56, 1, 170, 70]
function bubbleSort(arr){
// 建立快指针
let fast = 1
// 建立慢指针
let slow = 0
for (let i = 0; i < arr.length; i ++) { // 确定完成排序的长度
// 如果快指针没有触碰到完成排序边界则继续
while(fast < arr.length - i) {
// 如果慢指针指向的元素更大则交换元素
if (arr[slow] > arr[fast]) {
let num = arr[slow]
arr[slow] = arr[fast]
arr[fast] = num
}
// 无论是否交换, 快慢指针都要前进
fast ++;
slow ++;
console.log("第"+i+"次排序"+arr)
}
// 重置快慢指针
slow = 0
fast = 1
}
}
console.log(bubbleSort(nums))
代码实现二:
let nums = [33, 4, 64, 234, 23, 2, 15, 56, 1, 170, 70]
function bubbleSort(arr){
for (let i = 1; i < arr.length; i ++) { // 确定需要执行轮次
let flag = true // 添加轮次执行是否完成标记
for (let j = 0; j < arr.length - i; j++) { //比较, 每次比较完, 将较大数放后一位
if (arr[j] > arr[j + 1]) {
let num = arr[j]
arr[j] = arr[j + 1]
arr[j + 1] = num
flag = false
}
console.log("第"+ i + "次排序结果" + j + ":" +arr)
}
if (flag) {
break
}
}
}
console.log(bubbleSort(nums))
-
选择排序
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
步骤
- 比较所有相邻元素,如果第一个比第二个大,则交换它们
- 一轮下来保证可以找到一个数是最大的
- 执行n-1轮,就可以完成排序
图解:
代码实现:
let nums = [33, 4, 64, 234, 23, 2, 15, 56, 1, 170, 70]
function selectionSort(arr){
for (let i = 0; i < arr.length; i ++) {
// 假设最小值是数组第一个
let minIndex = i
// 依次比较, 找到数组中的最小值, 并记录最小值位置, 以此类推, 在剩余项中找到最小值
for (let j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j
}
}
// 找到最小值后, 将它与所比较元素位置互换
if (minIndex != i) {
let num = arr[i]
arr[i] = arr[minIndex]
arr[minIndex] = num
}
}
}
console.log(selectionSort(nums))
-
插入排序
插入法排序通过构建有序数组元素的存储,对未排序的数组元素,在已排序的数组中从最后一个元素向第一个元素遍历,找到相应位置并插入。
步骤
- 假设第一个数是有序的,剩余的数是无序的
- 选择第二个数与第一个数进行比较,如果第二个数比第一个数小,则交换两个数的位置,如果第二个数比第一个数大,则位置不发生变动
- 选择第三个数与第二个数进行比较,如果第三个数小于第二个数,则交换两个数的位置,然后用第二个数与第一个数进行比较,如果第二个数小于第一个数,交换两个数的位置,反之不发生变动;如果第三个数大于第二个数,则不发生变动。
- 按上述方法依此类推,直到将最后一个数比较完为止。
图解:
代码实现:
let nums = [33, 4, 64, 234, 23, 2, 15, 56, 1, 170, 70]
function insertionSort(arr){
//设置排序区, 假设第一条数据顺序为正位, 从数组的第二条数据开始插入
for ( let i = 1; i < arr.length; i++) {
//保存待插入的值
let compare = arr[i]
//反向遍历排序区
let j = i - 1
//如果大于待插入的值, 则将其向后排
while( j >= 0 && arr[j] > compare) {
arr[j + 1] = arr[j]
j--;
}
// 或者用for循环
// for ( j >= 0; j--) {
// if (compare > arr[j]) break;
// arr[j + 1] = arr[j];
// }
//插入
arr[j + 1] = compare;
}
}
console.log(insertionSort(nums))
-
希尔排序
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录 “基本有序” 时,再对全体记录进行依次直接插入排序。
步骤
在此我们选择增量 gap=length/2,缩小增量继续以 gap = gap/2 的方式,这种增量选择我们可以用一个序列来表示,{n/2, (n/2)/2, ..., 1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
- 选择一个增量序列 {t1, t2, …, tk},其中 (ti>tj, i<j, tk=1);
- 按增量序列个数 k,对序列进行 k 趟排序;
- 每趟排序,根据对应的增量 t,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
图解:
一开始结合图文并没有太理解什么意思, 后面去搜逐步推导的步骤, 才算明白. 具体请看这位大佬的文章js实现希尔排序
逐步推导过程:
let nums = [33, 4, 64, 234, 23, 2, 15, 56, 1, 170, 70]
function shellSort(arr) {
// 第一轮排序将11个数据分成了11/2=5组
for(let i=5;i<arr.length;i++){
// 遍历各组中所有的元素,共有5组,每组2个元素,步长为5
for(let j = i-5;j >= 0;j -= 5){
//如果当前元素大于加上步长后的那个元素,则交换(从小到大排序)
if(arr[j] > arr[j+5]){
let tmp = arr[j];
arr[j] = arr[j+5];
arr[j+5] = tmp;
}
// console.log("第"+ i + "次排序结果" + j + ":" +arr)
}
}
// 排序后结果: [2,4,56,1,23,33,15,64,234,170,70]
// 第一轮排序将11个数据分成了5/2=2组
for(let i=2;i<arr.length;i++){
for(let j = i-2;j >= 0;j -= 2){
//如果当前元素大于加上步长后的那个元素,则交换(从小到大排序)
if(arr[j] > arr[j+2]){
let tmp = arr[j];
arr[j] = arr[j+2];
arr[j+2] = tmp;
}
console.log("第"+ i + "次排序结果" + j + ":" +arr)
}
}
// 排序后结果: [2,1,15,4,23,33,56,64,70,170,234]
// 最后一轮排序将10个数据分成了2/2 = 1组
for(let i=1;i<arr.length;i++){
for(let j = i-1;j >= 0;j -= 1){
//如果当前元素大于加上步长后的那个元素,则交换(从小到大排序)
if(arr[j] > arr[j+1]){
let tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
console.log("第"+ i + "次排序结果" + j + ":" +arr)
}
}
}
console.log(shellSort(nums))
总结后算法是交换法实现的, 效率很低
let nums = [33, 4, 64, 234, 23, 2, 15, 56, 1, 170, 70]
function shellSort(arr){
for(let gap = arr.length / 2;gap > 0;gap =Math.floor( gap/2)){
for(let i=gap;i<arr.length;i++){
for(let j = i - gap;j >= 0;j -= gap){
if(arr[j] > arr[j+gap]){
let tmp = arr[j];
arr[j] = arr[j + gap];
arr[j + gap] = tmp;
}
}
}
}
return arr;
}
shellSort(nums)
优化后使用插入法:
let nums = [];
for(let i=0;i<5000000;i++){
let num = Math.floor(Math.random() * 5000000);
nums.push(num)
}
let t1 = new Date().getTime();
function shellSort(arr){
// 计算增量, 循环到增量为1
for(let gap = Math.floor(arr.length/2);gap > 0;gap = Math.floor(gap/2)){
// 遍历各组中的所有元素
for(let i = gap;i < arr.length;i++){
let j = i;
let tmp = arr[j];
// 如果当前元素大于加上步长后的那个元素,则交换
if(arr[j] < arr[j-gap]){
// 如果同一组中 前数大于后数,则交换他们
while(j - gap >= 0 && arr[j-gap] > tmp){
arr[j] = arr[j-gap];
j = j-gap;
}
arr[j] = tmp;
}
}
}
let t2 = new Date().getTime();
// 计算完成排序所需时间
console.log((t2 - t1)/1000 + "秒");
return arr;
}
shellSort(nums);
-
归并排序
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法 (Divide and Conquer) 的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为 2 - 路归并。
该排序算法的主体思想:
1. “分治”:不断将当前数组从中点处分割(直至末尾被分割的数组只有1个元素),不断将复杂的大问题拆解为易于处理的小问题;
2. “合并”:当拆分后的子数组长度为1时,开始由底向上进行合并操作(将当前子数组排序后合并),直至合并至原数组,此时完成了排序;
步骤
- 如果输入内只有一个元素,则直接返回,否则将长度为 n 的输入序列分成两个长度为 n/2 的子序列;
- 分别对这两个子序列进行归并排序,使子序列变为有序状态;
- 设定两个指针,分别指向两个已经排序子序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间(用于存放排序结果),并移动指针到下一位置;
- 重复步骤 3 ~4 直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。
图解:
代码实现:
let nums = [33, 4, 64, 234, 23, 2, 15, 56, 1, 170, 70]
function mergeSort(arr) {
if (arr.length < 2) return arr;
// --- “分”
// 设置分割点
let division = Math.floor(arr.length / 2);
const left = arr.slice(0, division);
const right = arr.slice(division);
// --- “合”
return this.merge(this.mergeSort(left), this.mergeSort(right));
}
function merge(left, right) {
// 创建额外数组用来保存每次排序后的情况
const merged = [];
// 当分割完左右两边都有至少一条数据的时, 比较最小值,放入数组
while (left.length > 0 && right.length > 0) {
if (left[0] <= right[0]) {
merged.push(left.shift());
}
else {
merged.push(right.shift());
}
}
// 当分割完只有左边或右边有数据, 直接放入数组
while (left.length) {
merged.push(left.shift());
}
while (right.length) {
merged.push(right.shift());
}
return merged;
}
console.log(mergeSort(nums))
-
快速排序
快速排序跟归并排序一样用到了分治思想。乍看起来快速排序和归并排序非常相似,都是将问题变小,先排序子串,最后合并。不同的是快速排序在划分子问题的时候经过多一步处理,将划分的两组数据划分为一大一小,这样在最后合并的时候就不必像归并排序那样再进行比较。但也正因为如此,划分的不定性使得快速排序的时间复杂度并不稳定。
快速排序的基本思想:通过一趟排序将待排序列分隔成独立的两部分,其中一部分记录的元素均比另一部分的元素小,则可分别对这两部分子序列继续进行排序,以达到整个序列有序。
步骤
- 从序列中随机挑出一个元素,做为 “基准”(pivot);
- 重新排列序列,将所有比基准值小的元素摆放在基准前面,所有比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个操作结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地把小于基准值元素的子序列和大于基准值元素的子序列进行快速排序。
代码参考博文: js 实现快速排序
图解:
代码实现: 左右指针交换法
let nums = [33, 4, 64, 234, 23, 2, 15, 56, 1, 170, 70]
// 左右指针对向前进,当遇到左侧指针所指的数小于等于基准值,或右侧指针指向的
// 数大于基准值,则循环会停止,交换左右指针对应的的数,否则指针继续向前,最
// 后两个指针会指向同一个位置,将基准值与重合位置交换
function quickSort(arr, left, right) {
// 当左右指针索引值相减<=0,说明左右指针相遇, 数组只有一个值了
if (right - left <= 0) return;
let leftPoint = left; // 定义左指针
let rightPoint = right; // 定义右指针
let base = leftPoint; // 以左边第一个为基准值的索引值
let center = arr[base]; // 定义基准值
// 循环条件,就是左指针小于右指针,当等于或大于时,即停止
while (leftPoint < rightPoint) {
// 如果右侧指针指向的数小于基准值,则停止循环,否则继续向左移动一位
while (leftPoint < rightPoint && center < arr[rightPoint]) {
// 右侧指针继续向左移动一位
rightPoint--;
}
// 如果左侧指针指向的数大于基准值,则停止循环,否则继续向右移动一位
// 注意,左右指针遍历条件中至少有一个写=,否则会陷入死循环
while (leftPoint < rightPoint && center >= arr[leftPoint]) {
leftPoint++;
}
// 当上面两个循环停止,此时交换满足左指针指向的元素大于基准值,
// 右指针指向的元素小于基准值,则将左右元素交换
let temp = arr[rightPoint];
arr[rightPoint] = arr[leftPoint];
arr[leftPoint] = temp;
}
// 整体遍历结束后,说明 left=right,这时候左右指针相遇,则相遇的位置即为基
// 准值应该被排好序的位置,这时候将基准值与相遇位置的元素进行交换
let temp = arr[base];
arr[base] = arr[leftPoint];
arr[leftPoint] = temp;
// 递归,这时候相遇位置的索引等于 left=right
//分别递归左右两侧
// left=right 位置已经是排好序的了
this.quickSort(arr, left, leftPoint - 1);
this.quickSort(arr, leftPoint + 1, right);
return arr;
}
console.time('"quick"Sort');
console.log(quickSort(nums, 0, nums.length - 1))
console.timeEnd('"quick"Sort');
代码实现二: 额外定义两个数组实现
最简单,空间复杂度最高
let nums = [33, 4, 64, 234, 23, 2, 15, 56, 1, 170, 70]
function quickSort(arr) {
if (arr.length <= 1) return arr;
// 选择数组中的第一个值作为基准
const num = arr[0];
// 定义两个额外数组用
let left = [], right = [];
// 将数组中小于该值的数置于该数之前,大于该值的数置于该数之后,
// 并保存到刚才定义的两个数组中
for (let i = 1; i < arr.length; i++) {
if (arr[i] <= num) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
// 重复操作直至排序完成, 并连接左边数组、基准值及右边数组
return quickSort(left).concat([num], quickSort(right));
}
console.time('"quick"Sort');
console.log(quickSort(nums))
console.timeEnd('"quick"Sort');
-
堆排序
堆排序是指利用堆这种数据结构所设计的一种排序算法。参考博文: 前端学习数据结构与算法系列(七):堆排序与归并排序
堆属性:
堆分为两种: 最大堆和最小堆,两者的差别在于节点的排序方式。
在不同类型的堆中,每一个节点都遵循堆的属性,下方所述内容即为堆的属性。
- 最大堆: 父节点的值大于子节点的值
- 最小堆: 父节点的值小于子节点的值
由一个完全二叉树组成,且树中的所有节点都满足堆属性,这个完全二叉树就是堆。
根据堆的属性可知:
堆的根节点中存放的是最大或最小元素,但是其他节点的排列顺序是未知的。例如,在一个最大堆中,最大的那一个元素总是位于index0的位置,但是最小的元素则未必是最后一个元素,唯一能够保证的是最小的元素是一个叶节点,但是不确定是哪一个。
- 最大堆根节点中的元素一定是树中的最大值
- 最小堆根节点中的元素一定是树中的最小值
实现思路:
实现堆排序之前,我们需要先将即将排序的数据构建成一个最大堆,构建完成后,根据最大堆的属性可知,堆顶部的值最大,我们将它取出,然后重新构建堆,直到堆中的所有数据被取出,堆排序也就完成了。
步骤:
调整完全二叉树中的树为一个最大堆
在一个完全二叉树中,从一个节点出发,找到它的左子树和右子树,将当前节点与它的两颗子树进行大小比较,找到两颗树中较大的一方,将其与当前节点进行交换,交换完毕后,当前节点所在的树就是一个最大堆。我们称这个交换操作为heapify
- 将数据先构建成一个最大堆
- 从堆的最后一个节点出发,将其与树的根节点进行位置交换
- 交换完毕后,调用heapify重新调整堆。
- 排序好一个数后,我们的数组长度就会-1,则调用swap和heapify时,树的高度就是当前循环到的i的值。
代码实现:
/*
* 1. 从一个节点出发
* 2. 从它的左子树和右子树中选择一个较大值
* 3. 将较大值与这个节点进行位置交换
* 上述步骤,就是一次heapify的操作
* */
// n为树的节点数,i为当前操作的节点 (找到这颗树里的最大节点)
function heapify (tree, n, i) {
if(i >= n){
// 结束递归
return;
}
// 找到左子树的位置
let leftNode = 2 * i + 1;
// 找到右子树的位置
let rightNode = 2 * i +2;
/*
1. 找到左子树和右子树位置后,必须确保它小于树的总节点数
2. 已知当前节点与它的左子树与右子树的位置,找到最大值
*/
// 设最大值的位置为i
let max = i;
// 如果左子树的值大于当前节点的值则最大值的位置就为左子树的位置
if(leftNode < n && tree[leftNode] > tree[max]){
max = leftNode;
}
// 如果右子树的值大于当前节点的值则最大值的位置就为右子树的位置
if(rightNode < n && tree[rightNode] > tree[max]){
max = rightNode;
}
/*
* 1. 进行大小比较后,如果最大值的位置不是刚开始设的i,则将最大值与当前节点进行位置互换
* */
if(max !== i){
// 交换位置
swap(tree,max,i);
// 递归调用,继续进行heapify操作
heapify(tree,n,max)
}
};
// 交换数组位置函数
function swap (arr,max,i) {
[arr[max],arr[i]] = [arr[i],arr[max]];
};
/*
* 将完全二叉树构建成堆
* 1. 从树的最后一个父节点开始进行heapify操作
* 2. 树的最后一个父节点 = 树的最后一个子结点的父节点
* */
function buildHeap (tree,n) {
// 最后一个节点的位置 = 数组的长度-1
const lastNode = n -1;
// 最后一个节点的父节点
const parentNode = Math.floor((lastNode - 1) / 2);
// 从最后一个父节点开始进行heapify操作
for (let i = parentNode; i >= 0; i--){
heapify(tree, n, i);
}
};
// 堆排序函数
function heapSort (tree,n) {
// 构建堆
buildHeap(tree,n);
// 从最后一个节点出发
for(let i = n - 1; i >= 0; i--){
// 交换根节点和最后一个节点的位置
swap(tree,i,0);
// 重新调整堆
heapify(tree,i,0);
}
};
let nums = [33, 4, 64, 234, 23, 2, 15, 56, 1, 170, 70];
heapSort(nums, nums.length);
console.log(nums);
-
计数排序
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
计数排序 (Counting sort) 是一种稳定的排序算法。计数排序使用一个额外的数组 C,其中第 i 个元素是待排序数组 A 中值等于 i 的元素的个数。然后根据数组 C 来将 A 中的元素排到正确的位置。它只能对整数进行排序。
有两种方法:
方法一步骤:
- 创建计数数组nums
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为i的元素出现的次数,存入计数数组nums的第i项;
- 对所有的计数累加(从计数数组nums中的第一个元素开始,每一项和前一项相加);
- 反向填充目标数组:将每个元素i放在新数组的第nums(i)项,每放一个元素就将C(i)减去1。
方法一代码实现:
let numArr = [33, 4, 64, 234, 23, 2, 15, 56, 1, 170, 70]
function countingSort(arr) {
const n = arr.length;
// 找到数组中的最大值和最小值
let max = arr[0];
let min = arr[0];
for (let i = 1; i < n; i++) {
if (arr[i] > max) {
max = arr[i];
}
if (arr[i] < min) {
min = arr[i];
}
}
// 计算元素的频次
const countArray = Array(max - min + 1).fill(0);
for (let i = 0; i < n; i++) {
countArray[arr[i] - min]++;
}
// 根据频次生成排序后的数组
let index = 0;
for (let i = 0; i < countArray.length; i++) {
while (countArray[i] > 0) {
arr[index++] = i + min;
countArray[i]--;
}
}
return arr;
}
console.time('countingSort');
countingSort(numArr)
console.timeEnd('countingSort');
方法二步骤:
- 找到范围: 遍历整个数组,找到数组中的最大值和最小值。
- 统计频次: 创建一个计数数组,计数数组的长度,是最大值与最小值差值+1, 记录每个元素出现的频次。
- 生成排序数组: 根据频次信息,生成排序后的数组。
let numArr = [33, 4, 64, 234, 23, 2, 15, 56, 1, 170, 70]
function countingSort(nums) {
let arr = []; // 定义一个空数组
let max = Math.max(...nums); // 找到这个数组的 最大 最小值
let min = Math.min(...nums);
// 找到数组中的最大值和最小值
// let max = arr[0];
// let min = arr[0];
// for (let i = 1; i < n; i++) {
// if (arr[i] > max) {
// max = arr[i];
// }
// if (arr[i] < min) {
// min = arr[i];
// }
// }
// 把传入数组的值作为下标 装入一个长度为数组最
for(let i = 0, i < nums.length; i ++) {
// 比如 第一位是5 传入arr后 arr为[,,,,,1] => [empty × 5, 1]
let temp = nums[i];
arr[temp] = arr[temp] + 1 || 1;
}
// 定义一个索引 这个索引是 后面用来改变原数组的
let index = 0;
// 还原原数组
// 写一个for循环 i=原数组最小值 i>原数组最大值的时候跳出循环
for(let i = min; i <= max; i ++) {
// 从arr[i]开始 如果他>0
while(arr[i] > 0) {
// 就把原数组的第index位赋值为 i, 这个其实是 nums[index] = i;
// 和 index++的缩写
nums[index++] = i;
// 每赋值完一次后 临时数组当前位的值就--,如果=0 就说明这位上没有值了
arr[i]--;
}
}
return nums
}
console.time('countingSort');
countingSort(numArr)
console.timeEnd('countingSort');
区别:
方法一的计数数组的下标,是待排序数组的值, 值是该数在待排序数组中出现的次数。
方法二的计数数组的下标,是待排序数组最大值与最小值的差值, 并记录该差值出现的次数。
-
桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
- 在额外空间充足的情况下,尽量增大桶的数量
- 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
桶排序的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行。
步骤
- 设置一个定量的数组当作空桶;
- 遍历输入数据,并且把数据一个一个放到对应的桶里去;
- 对每个不是空的桶进行排序;
- 从不是空的桶里把排好序的数据拼接起来。
图解:
元素分布在桶中:
然后,元素在每个桶中排序:
现在我们要确定桶的数量,怎么确定?这里面有一个计算公式,
桶的数量 = (最大值 - 最小值)/ 数组长度 + 1。 数组长度 可以 是 任意自定义的有意义的正整数。
那这里面我们可以确定桶的数量为 46 / 8 +1 = 4;
现在我们再用另一个公式确定每个元素在哪个桶中
元素位置 =( 元素大小 - 最小值)/ 数组长度
根据这个公式我们可以确定每个元素都在第几个桶里面。
代码实现:
let numArr = [33, 4, 64, 234, 23, 2, 15, 56, 1, 170, 70]
function bucketSort(arr) {
if (arr.length === 0) {
return arr;
}
// 求最大值和最小值
let maxValue = Math.max(...arr);
let minValue = Math.min(...arr);
//桶的初始化
let DEFAULT_BUCKET_SIZE = 5; // 设置桶的默认数量为5
let bucketCount = Math.floor((maxValue - minValue) / DEFAULT_BUCKET_SIZE) + 1;
let 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) / bucketCount)].push(arr[i]);
}
arr.length = 0;
// 对每个桶进行排序,这里使用了插入排序,这里也可以直接用js的数组sort方法,很直接
for (i = 0; i < buckets.length; i++) {
insertionSort(buckets[i]);
// 合并桶
for (var j = 0; j < buckets[i].length; j++) {
arr.push(buckets[i][j]);
}
}
return arr;
}
function insertionSort(arr){
for ( let i = 1; i < arr.length; i++) {
let compare = arr[i]
let j = i - 1
while( j >= 0 && arr[j] > compare) {
arr[j + 1] = arr[j]
j--;
}
arr[j + 1] = compare;
}
}
console.time('countingSort');
console.log(bucketSort(numArr))
console.timeEnd('countingSort');
计数排序VS桶排序:
与桶排序类似, 计数排序用到了“桶”的概念(同样的还有基数排序), 不同的是计数排序每个桶只存储单一键值,而桶排序是每个桶存储一定范围的数值。
-
基数排序
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
步骤
- 取得数组中的最大数,并取得位数,即为迭代次数 N(例如:数组中最大数值为 1000,则 N=4);
- A 为原始数组,从最低位开始取每个位组成 radix 数组;
- 对 radix 进行计数排序(利用计数排序适用于小范围数的特点);
- 将 radix 依次赋值给原数组;
- 重复 2~4 步骤 N 次
图解:
代码实现:
function radixSort(arr) {
//定义一个二维数组,表示10个桶,每个桶就是一个一维数组
//说明
//1,二维数组包含10个一维数组,
//2.为了防止在放入数的时候,数据溢出,则每个一维数组(桶)
//大小定为arr.length
//3.很明确,基数排序是使用空间换时间的经典算法
let bucket = new Array(10);
for (let i = 0; i < bucket.length; i++) {
bucket[i] = new Array(arr.length);
}
//为了记录每个桶中,实际存了多少个数据,我们定义一个
//一维数组来记录每个桶的每次放入的数据个数
//可以这里理解
//比如:bucketElementCounts[0],记录的就是bucket[0]桶的放入数据个数
let buckeElementCounts = new Array(10).fill(0);
//1.得到数组中最大的位数
let max = arr[0];
for (let i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i]
}
}
//得到最大是几位数
let maxLength = (max + '').length;
for (let i = 0, n = 1; i < maxLength; i++, n = n * 10) {
//每一轮,对每个元素的各个位数进行排序处理,
//第一次是个位,第二次是十位,第三次是百位
for (let j = 0; j < arr.length; j++) {
//取出每个元素的各位的值
let digitOfElement = Math.floor(arr[j] / n) % 10;
bucket[digitOfElement][buckeElementCounts[digitOfElement]] = arr[j];
buckeElementCounts[digitOfElement]++;
}
//按照这个桶的顺序(以为数组的下标依次取出数据,放入原来数组)
let index = 0;
//遍历每一桶,并将桶中的数据,放入原数组
for (let k = 0; k < buckeElementCounts.length; k++) {
//如果桶中有数据,我们才放入原数组
if (buckeElementCounts[k] !== 0) {
//循环该桶即第k个桶,即第k个一维数组,放入
for (let l = 0; l < buckeElementCounts[k]; l++) {
//取出元素放入arr
arr[index] = bucket[k][l];
//arr下标后移
index++;
}
//每轮处理后,下标要清0
buckeElementCounts[k] = 0;
}
}
}
}
基数排序与桶排序与计数排序不同点在于,基数排序是根据键值的每位数字来分配桶。
-
总结
-
使用场景
数据量小,要求稳定: 插入排序, 冒泡排序
数据量中等,要求最快速度: 快速排序
数据量大,要求稳定: 归并排序, 计数排序, 桶排序
浮点数排序: 插入排序, 归并排序, 堆排序
针对特定数据分布: 计数排序, 桶排序, 基数排
对于百万以上级别的数据,推荐使用时间复杂度为O(nlogn)的排序,如 快速排序, 堆排序 和 归并排序。
如果数据分布较集中的话,可以考虑线性时间的 计数排序 和 桶排序。
所以具体使用哪种排序算法,需要根据数据量的大小,稳定性的要求,数据的分布特点以及其他要素来综合判断选用。一般来说,对小数据量使用简单排序,大数据量使用时间复杂度低的稳定排序,特殊数据使用对应分布的排序。