排序算法是每个程序员必备的技能,最近发现自己贪图数组的sort方法,已经把排序算法遗忘的差不多了,就来整理一下。
PS:ES并没有规定数组的sort函数如何实现,所以每个厂商实现不一样,Firefox使用归并排序,而Chrome(V8)使用了快排的变体。
首先是各个排序都会用到的一些方法
//交换指定索引的数组元素
function swap(arr,i,j) {
[arr[i], arr[j]] = [arr[j],arr[i]];
}
-
冒泡排序
冒泡排序比较相邻的两个项,如果第一个比第二个大(小)就交换他们。代码如下:
function bubbleSort(arr){
const {len} = arr;
for(let i=0; i<len; i++){
for(let j=0; j<len-1; j++){
if(arr[j]>arr[j+1]){
swap(arr, j, j+1);
}
}
}
}
但实际上我们知道,冒泡排序每一趟至少能完成一个元素的排序,即是最大的那个元素。因此我们第二重循环可以不必进行到数组的最后一个元素。可以对冒泡排序进行改进,代码如下(注意第二重循环结束条件):
function bubbleSort(arr){
const {len} = arr;
for(let i=0; i<len; i++){
for(let j=0; j<len-1-i; j++){
if(arr[j]>arr[j+1]){
swap(arr, j, j+1);
}
}
}
}
即使如此,它的时间复杂度还是O(),空间复杂度O(1)
-
选择排序
选择排序是找到数据中最小值,把它放到第一位;再找到第二小的放到第二位,以此类推,代码如下:
function selectionSort(arr){
const { len } = arr;
for(let i=0; i<len; i++){
let index = i;
for(let j=i; j<len; j++){
if(arr[index]>arr[j]){
index = j;
}
}
if(index != i){
swap(arr,index,i);
}
}
}
时间复杂度:O(),空间复杂度O(1)
-
插入排序
插入排序意思是每次对当前的数据,向前比较已经排序好了的数据,选择它合适的位置进行插入。
function insertionSort(arr){
const {length} = arr;
let temp;
for(let i=1; i<length; i++){
temp = arr[i];
let j=i;
while(j >0 && arr[j]>=temp){
arr[j] = arr[j-1];
j--;
}
arr[j] = temp;
}
}
插入排序时间复杂度:O(),空间复杂度O(1)。排序小型数据时,插入排序要比冒泡和选择排序好。
-
归并排序
归并排序是一种分而治之的算法,它主要思想是将数组分解为较小的数组,直到每个小数组只有一个位置,再讲小数组合并为大数组。这个方法是递归的,代码如下:
//这个方法需要有两个函数,mergeSort负责将数组分为多个数组, merge负责对小数组进行排序和归并
function mergeSort(arr){
const {length} = arr;
if(length> 1){
const middle = Math.floor(length/2);
const left = mergeSort( arr.slice(0, middle) );
const right = mergeSort( arr.slice(middle) );
arr = merge(left, right);
}
return arr;
}
function merge(left, right) {
let i=0, j = 0;
const res = [];
while(i<left.length && j<right.length){
res.push(
left[i]<right[j]? left[i++]:right[j++]
)
}
return res.concat(i<left.length? left.slice(i):right.slice(j) );
}
时间复杂度为:O(),空间复杂度O(n)
-
快速排序
快排基本上是首屈一指的排序方法了,它比其他 nlogn 的方法要快一点。它和归并排序一样使用分而治之的思想,但它不把数组分开。使用快排首先需要选定主元(也就是中间数),然后以主元为标准,将数组分为两边,代码如下:
//快排需要两个函数,一个partition用于获取主元并划分数组,quick进行递归调用
function quick(arr, left, right){
let index;
if(arr.length>1) {
index = partition(arr, left, right);
if(left < index-1){
quick(arr, left, index-1);
}
if(right > index + 1){
quick(arr, index, right);
}
}
return arr;
}
function partition(arr, left, right){
const mid = arr[ Math.floor( (right+left)/2 ) ];
let i = left,
j = right;
while(i <= j) {
while(arr[i] < mid){
i++;
}
while(arr[j] > mid){
j--;
}
if(i <= j){
swap(arr, i, j);
i++;j--;
}
}
return i;
}
时间复杂度:O(),空间复杂度O()
-
计数排序
计数排序使用一个用来存储每个元素在原始数组中出现的次数的临时数组。在所有元素都计数完成后,临时数组已排好序并可迭代以构建排序后的结果数组。它是用来排序整数的数组。
function countingSort(arr){
if(arr.length < 2) {
return arr;
}
const max = findMaxValue(arr);
const counts = new Array(max+1);
arr.forEach(val => {
if(!counts[val]){
counts[val] = 0;
}
counts[val] ++;
});
let index = 0;
counts.forEach( (val,i) => {
while(val>0){
arr[index++] = i;
val--;
}
});
return arr;
}
function findMax(arr){
let max = arr[0];
arr.forEach(val => {
if(val>max){
max=val;
}
});
return max;
}
时间复杂度:O(n+k),k为计数数组的大小。此算法适用于正整数数据
-
桶排序(箱排序)
桶排序也是用到了分而治之的思想,不过的思想是,根据传入的数据跨度对数据进行分组,分成一个一个的桶,然后再对桶使用简单的排序算法进行排序,最终将桶合并,该算法适用于数据密集的情况。
function bucketSort(arr, size=5){
if(arr.length<2){
return arr;
}
const buckets = createBuckets(arr, size);
return sortBuckets(buckets);
}
function createBuckets(arr, size){
let min = arr[0],
max = arr[0];
for(let i=1; i<arr.length; i++){
if(min>arr[i]) min = arr[i];
if(max<arr[i]) max = arr[i];
}
const bucketCount = Math.floor( (max-min)/size) + 1;
const buckets = [];
for(let i=0; i<bucketCount; i++){
buckets[i] = [];
}
arr.forEach(val=>{
const bucketIndex = Math.floor((arr[i]-min)/size);
buckets[bucketIndex].push(val);
}
return buckets;
}
function sortBuckets(buckets){
let res = [];
for(let i=0; i<buckets.length; i++){
if(buckets[i].length != 0){
insertSort(buckets[i]);
res.push(...buckets[i]);
}
}
return res;
}
-
基数排序
基数排序也是一个分布式排序算法,它根据数字的有效位或基数(这也是它为什么叫基数排 序)将整数分布到桶中。基数是基于数组中值的记数制的。比如,对于十进制数,使用的基数是 10。因此,算法将会使用 10 个桶用来分布元素并且首先基于个位数字进行排序,然后基于十位数字,然后基于百位数字,以此类推。