十大排序算法JS实现以及复杂度分析

文章目录

  • 十大排序算法概述
  • 应用场景
  • 代码实现
    • 一、冒泡排序
    • 二、选择排序
    • 三、插入排序
    • 四、希尔排序
    • 五、归并排序
    • 六、快速排序
    • 七、堆排序
    • 八、计数排序
    • 九、桶排序
    • 十、基数排序

十大排序算法概述

(图片来源于网络)
十大排序算法JS实现以及复杂度分析_第1张图片
十大排序算法JS实现以及复杂度分析_第2张图片

注意

  • 原地快排的空间占用是递归造成的栈空间的使用,最好情况下是递归log2n次,所以空间复杂度为O(log2n),最坏情况下是递归n-1次,所以空间复杂度是O(n)。
  • 非原地排序每次递归都要声明一个总数为n的额外空间,所以空间复杂度变为原地排序的n倍,即最好情况下O(nlog2n),最差情况下O(n的平方)

应用场景

  1. 从平均时间来看,快速排序是效率最高的,但快速排序在最坏情况下的时间性能不如堆排序和归并排序。而后者相比较的结果是,在n较大时归并排序使用时间较少,但使用辅助空间较多。

  2. 简单排序包括除希尔排序之外的所有冒泡排序、插入排序、简单选择排序。其中直接插入排序最简单,但序列基本有序或者n较小时,直接插入排序是好的方法,因此常将它和其他的排序方法,如快速排序、归并排序等结合在一起使用。

  3. 基数排序的时间复杂度也可以写成O(d*n)。因此它最使用于n值很大而关键字较小的的序列。若关键字也很大,而序列中大多数记录的最高关键字均不同,则亦可先按最高关键字不同,将序列分成若干小的子序列,而后进行直接插入排序。

  4. 从方法的稳定性来比较,基数排序是稳定的内排方法,所有时间复杂度为O(n^2)的简单排序也是稳定的。但是快速排序、堆排序、希尔排序等时间性能较好的排序方法都是不稳定的。稳定性需要根据具体需求选择。

  5. 上面的算法实现大多数是使用线性存储结构,像插入排序这种算法用链表实现更好,省去了移动元素的时间。具体的存储结构在具体的实现版本中也是不同的。

代码实现

一、冒泡排序

  1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  3. 针对所有的元素重复以上的步骤,除了最后一个;
  4. 重复步骤1~3,直到排序完成。

应用场景:
冒泡排序思路简单,代码也简单,特别适合小数据的排序。但是,由于算法复杂度较高,在数据量大的时候不适合使用。

//时间复杂度(平均):O(n^2)
//时间复杂度(最坏):O(n^2)
//时间复杂度(最好):O(n)
//空间复杂度():O(1)
//稳定性:稳定
function betterBubbleSort(arr) {
    const len = arr.length  
    
    for(let i=0;i arr[j+1]) {
                [arr[j], arr[j+1]] = [arr[j+1], arr[j]]
                // 只要发生了一次交换,就修改标志位
                flag = true
            }
        }
        // 若一次交换也没发生,则说明数组有序,直接放过
        if(flag == false)  return arr;
    }
    return arr
}
console.log(bubbleSort([6,5,3,2,22,56]));

二、选择排序

  1. 在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
  2. 从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
  3. 重复第二步,直到所有元素均排序完毕。

适用场景
选择排序实现也比较简单,并且由于在各种情况下复杂度波动小,因此一般是优于冒泡排序的。在所有的完全交换排序中,选择排序也是比较不错的一种算法。但是,由于固有的O(n2)复杂度,选择排序在海量数据面前显得力不从心。因此,它适用于简单数据排序。

//经过n-1趟直接选择
//选择最小的交换到前面

//时间复杂度(平均):O(n^2)
//时间复杂度(最坏):O(n^2)
//时间复杂度(最好):O(n^2)
//空间复杂度():O(1)
//稳定性:不稳定

function selectSort(arr)  {
  // 缓存数组长度
  const len = arr.length 
  // 定义 minIndex,缓存当前区间最小值的索引,注意是索引
  let minIndex  
  // i 是当前排序区间的起点
  for(let i = 0; i < len - 1; i++) { 
    // 初始化 minIndex 为当前区间第一个元素
    minIndex = i  
    // i、j分别定义当前区间的上下界,i是左边界,j是右边界
    for(let j = i; j < len; j++) {  
      // 若 j 处的数据项比当前最小值还要小,则更新最小值索引为 j
      if(arr[j] < arr[minIndex]) {  
        minIndex = j
      }
    }
    // 如果 minIndex 对应元素不是目前的头部元素,则交换两者
    if(minIndex !== i) {
      [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]
    }
  }
  return arr
}
console.log(selectionSort([6,5,3,2,22,56]));

三、插入排序

  1. 把待排序的数组分成已排序和未排序两部分,初始的时候把第一个元素认为是已排好序的。
  2. 从第二个元素开始,在已排好序的子数组中寻找到该元素合适的位置并插入该位置。
  3. 重复上述过程直到最后一个元素被插入有序子数组中。

适用场景
插入排序由于O( n2 )的复杂度,在数组较大的时候不适用。但是,在数据比较少的时候,是一个不错的选择,一般做为快速排序的扩充。例如,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序。又如,在JDK 7 java.util.Arrays所用的sort方法的实现中,当待排数组长度小于47时,会使用插入排序。

//时间复杂度(平均):O(n^2)
//时间复杂度(最坏):O(n^2)
//时间复杂度(最好):O(n)
//空间复杂度():O(1)
//稳定性:稳定

// 插入排序的时间复杂度分析。在最坏情况下,数组完全逆序,插入第2个元素时要考察前1个元素,插入第3个元素时,要考虑前2个元素,……,插入第N个元素,要考虑前 N - 1 个元素。
// 因此,最坏情况下的比较次数是 1 + 2 + 3 + ... + (N - 1),等差数列求和,结果为 N^2 / 2,所以最坏情况下的复杂度为 O(N^2)。
// 最好情况下,数组已经是有序的,每插入一个元素,只需要考查前一个元素,因此最好情况下,插入排序的时间复杂度为O(N)。


// 从第一个元素开始,该元素可以认为已经被排序;
// 取出下一个元素,在已经排序的元素序列中从后向前扫描;
// 如果该元素(已排序)大于新元素,将该元素移到下一位置;
// 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
// 将新元素插入到该位置后;
// 重复步骤2~5。

function insertSort(arr) {
  // 缓存数组长度
  const len = arr.length
  // temp 用来保存当前需要插入的元素
  let temp  
  // i用于标识每次被插入的元素的索引
  for(let i = 1;i < len; i++) {
    // j用于帮助 temp 寻找自己应该有的定位
    let j = i
    temp = arr[i]  
    // 判断 j 前面一个元素是否比 temp 大
    while(j > 0 && arr[j-1] > temp) {
      // 如果是,则将 j 前面的一个元素后移一位,为 temp 让出位置
      arr[j] = arr[j-1]   
      j--
    }
    // 循环让位,最后得到的 j 就是 temp 的正确索引
    arr[j] = temp
  }
  return arr
}
console.log(insertionSort([6,5,3,2,22,56]));

四、希尔排序

  1. 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
  2. 按增量序列个数k,对序列进行 k 趟排序;
  3. 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

适用场景
插入排序在基本有序或者规模较小时十分高效,较大规模且无序时,可以用用希尔排序:相当于是将数据分组,再每组进行插入排序;

//时间复杂度(平均):O(n^1.3)
//时间复杂度(最坏):O(n^2)
//时间复杂度(最好):O(n)
//空间复杂度():O(1)
//稳定性:不稳定

// 缩小增量排序
// 最后成为增量为1的插入排序

function shellSort(arr) {
    var len = arr.length,
        temp;
    for (var gap=Math.floor(len/2);gap>0;gap=Math.floor(gap/2)) {//Math.floor(gap / 2),返回小于等于(gap/2)的最大整数
        for (var i = gap; i < len; i++) {//循环小组
            //每组进行插入排序, 前gap—1个相当于是每组插入排序的第一个,所以从gap开始;
            temp = arr[i];
            for (var j = i-gap; j >= 0 && arr[j]> temp; j-=gap) {
                arr[j + gap] = arr[j];
            }
            arr[j + gap] = temp;
        }
    }
    return arr;
}

console.log(shellSort([6,5,3,2,22,56]));

五、归并排序

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
  4. 重复步骤3直到某一指针到达序列尾
  5. 将另一序列剩下的所有元素直接复制到合并序列尾
    //时间复杂度(平均):O(nlog2 n)
    //时间复杂度(最坏):O(nlog2 n)
    //时间复杂度(最好):O(nlog2 n)
    //空间复杂度():O(n)
    //稳定性:稳定
    
    function mergeSort(arr) {  // 采用自上而下的递归方法
        var len = arr.length;
        if (len < 2) {
            return arr;
        }       
        var middle = Math.floor(len / 2),//向下取整
            left = arr.slice(0, middle),
            right = arr.slice(middle);
        return merge(mergeSort(left), mergeSort(right));
    }
     
    function merge(left, right) {
        var result = [];
     
        while (left.length>0 && right.length>0) {
            if (left[0] <= right[0]) {//<=为了稳定性
                result.push(left.shift());
            } else {
                result.push(right.shift());
            }
        }
     
        while (left.length)
            result.push(left.shift());
     
        while (right.length)
            result.push(right.shift());
     
        return result;
    }
    
    console.log(mergeSort([6,5,3,2,22,56]));

六、快速排序

  • 原地快排的空间占用是递归造成的栈空间的使用,最好情况下是递归log2n次,所以空间复杂度为O(log2n),最坏情况下是递归n-1次,所以空间复杂度是O(n)。
  • 非原地排序每次递归都要声明一个总数为n的额外空间,所以空间复杂度变为原地排序的n倍,即最好情况下O(nlog2n),最差情况下O(n的平方)

原地快排

    //时间复杂度(平均):O(nlog2 n)
    //时间复杂度(最坏):O(n^2)
    //时间复杂度(最好):O(nlog2 n)
    //空间复杂度():O(log2 n)
    //稳定性:不稳定
    //思路:两个哨兵,i,j,j从右边找比基数小的,
    //i从左边找比基数大的,然后交换两个目标元素的位置,
    //直到i=j,然后交换i和基数的位置,递归处理。
   // 快速排序入口
function quickSort(arr, left = 0, right = arr.length - 1) {
  // 定义递归边界,若数组只有一个元素,则没有排序必要
  if(arr.length > 1) {
      // lineIndex表示下一次划分左右子数组的索引位
      const lineIndex = partition(arr, left, right)
      // 如果左边子数组的长度不小于1,则递归快排这个子数组
      if(left < lineIndex-1) {
        // 左子数组以 lineIndex-1 为右边界
        quickSort(arr, left, lineIndex-1)
      }
      // 如果右边子数组的长度不小于1,则递归快排这个子数组
      if(lineIndex pivotValue) {
          j--
      }

      // 若i<=j,则意味着基准值左边存在较大元素或右边存在较小元素,交换两个元素确保左右两侧有序
      if(i<=j) {
          swap(arr, i, j)
          i++
          j--
      }

  }
  // 返回左指针索引作为下一次划分左右子数组的依据
  return i
}

// 快速排序中使用 swap 的地方比较多,我们提取成一个独立的函数
function swap(arr, i, j) {
  [arr[i], arr[j]] = [arr[j], arr[i]]
}

    console.log(quickSort([22,14,3,61,9,8,41,13]));

非原地快排:

function quickSort(arr) {
	//检查数组的元素个数,如果小于等于1,就返回
  if (arr.length <= 1) { return arr; }
	//选择"基准"(pivot),并将其与原数组分离
  var pivot = arr.pop();
	//定义两个空数组,用来存放一左一右的两个子集。
  var left = [];

  var right = [];
	//遍历数组,小于"基准"的元素放入左边的子集,大于基准的元素放入右边的子集。
  for (var i = 0; i < arr.length; i++){

    if (arr[i] < pivot) {

      left.push(arr[i]);

    } else {

      right.push(arr[i]);

    }

  }
	//用递归不断重复这个过程
  return quickSort(left).concat([pivot], quickSort(right));

};

七、堆排序

//时间复杂度(平均):O(nlog2 n)
//时间复杂度(最坏):O(n^2)
//时间复杂度(最好):O(nlog2 n)
//空间复杂度():O(1)
//稳定性:不稳定

// 初始化化堆 是从非叶子结点从后往前进行调整为堆,而后序的堆调整 都是从下标为0处开始调整为堆。
// 因为起初的完全二叉树是完全无效的,所有只能从后往前调整。在初始化完堆之后,
// 尽管将最值移动到尾部,打乱了堆,因为原来的堆结构已经基本形成,

//1、调整为大顶堆或者小顶堆
//2、将堆的第一个和最后一个互换
//3、调整打乱后的堆
//重复1、2、3


var len;    // 因为声明的多个函数都需要数据长度,所以把len设置成为全局变量
 
function buildMaxHeap(arr) {   // 建立大顶堆
    len = arr.length;
    for (var i = Math.floor(len/2); i >= 0; i--) {
        heapify(arr, i);
    }
}
 
function heapify(arr, i) {     // 堆调整
    var left = 2 * i + 1,
        right = 2 * i + 2,
        largest = i;
 
    if (left < len && arr[left] > arr[largest]) {
        largest = left;
    }
 
    if (right < len && arr[right] > arr[largest]) {
        largest = right;
    }
 
    if (largest != i) {
        swap(arr, i, largest);
        heapify(arr, largest);
    }
}
 
function swap(arr, i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}
 
function heapSort(arr) {
    buildMaxHeap(arr);
 
    for (var i = arr.length - 1; i > 0; i--) {
        swap(arr, 0, i);
        len--;
        heapify(arr, 0);
    }
    return arr;
}

八、计数排序

//时间复杂度(平均):O(n+k)
//时间复杂度(最坏):O(n+k)
//时间复杂度(最好):O(n+k)
//空间复杂度():O(n+k)
//稳定性:稳定

//适用于整数

// 基本思想为一组数在排序之前先统计这组数中其他数小于这个数的个数,则可以确定这个数的位置。
// 例如要排序的数为 7 4 2 1 5 3 1 5;则比7小的有7个数,所有7应该在排序好的数列的第八位,
// 同理3在第四位,对于重复的数字,1在1位和2位(暂且认为第一个1比第二个1小),5和1一样位于6位和7位。

function countingSort(arr){
    var maxValue = Math.min.apply(this,arr);
    var bucket = new Array(maxValue+1);
    var sortIndex = 0;
    for(var i =0;i0){
            arr[sortIndex++] = j;
            bucket[j]--;
        }
    }
    return arr;
}
console.log(countingSort([6,5,3,2,22,56]));

九、桶排序

当输入的数据可以均匀的分配到每一个桶中时最快

当输入的数据被分配到了同一个桶中时最慢

//时间复杂度(平均):O(n+k)
//时间复杂度(最坏):O(n^2)
//时间复杂度(最好):O(n)
//空间复杂度():O(n+k)
//稳定性:稳定


// 桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。
// 桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,
// 每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

// 按照函数的映射关系设置一个定量的数组当作空桶;
// 遍历输入数据,并且把数据一个一个放到对应的桶里去;
// 对每个不是空的桶进行排序;
// 从不是空的桶里把排好序的数据拼接起来。 

function bucketSort(arr, bucketSize) {//数组、桶的大小(可不输入,有默认值5)
    if (arr.length === 0) {
      return arr;
    }
 
    var i;
    var minValue = arr[0];
    var maxValue = arr[0];
    for (i = 1; i < arr.length; i++) { 
      if (arr[i] < minValue) {
          minValue = arr[i];                // 输入数据的最小值
      } else if (arr[i] > maxValue) {
          maxValue = arr[i];                // 输入数据的最大值
      }
    }
 
    // 桶的初始化
    var DEFAULT_BUCKET_SIZE = 5;            // 设置桶的默认大小为5
    bucketSize = typeof bucketSize != 'number' ? DEFAULT_BUCKET_SIZE : bucketSize;
    var bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;  //Math.floor():向下舍入;取得桶的数量
    var 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) / bucketSize)].push(arr[i]);
    }
 
    arr.length = 0;
    for (i = 0; i < buckets.length; i++) {
        console.log(buckets[i]);
        insertionSort(buckets[i]);                      // 对每个桶进行排序,这里使用了插入排序
        for (var j = 0; j < buckets[i].length; j++) {
            arr.push(buckets[i][j]);                     
        }
    }
 
    return arr;
}


function insertionSort(arr) {
    var len  = arr.length;
    var preIndex, //前面有序队列中用以比较的下标
     current;//后面乱序中提出来,用以比较的数
    for(var i = 1; i= 0 && arr[preIndex] > current){
            arr[preIndex + 1] =arr[preIndex]; //向右移动
            preIndex--;
        }
        arr[preIndex + 1] = current;
    }
    return arr;
}


console.log(bucketSort([6,5,3,2,22,56]));

十、基数排序

//时间复杂度(平均):O(n*k)
//时间复杂度(最坏):O(n*k)
//时间复杂度(最好):O(n*k)
//空间复杂度():O(n+k)   k为桶的数量
//稳定性:稳定

// 取得数组中的最大数,并取得位数;
// arr为原始数组,从最低位开始取每个位组成radix数组;
// 对radix进行计数排序(利用计数排序适用于小范围数的特点);


// 正是因为高位的数值决定数字的大小,所以应该后排,否则高位排好再排低位,高位不就乱了?
// 因为基数排序是稳定的,所以可以这么做,即高位如果相等的,还是保持低位排好的顺序

// LSD Radix Sort
var counter = [];
function radixSort(arr) {//数组

    var maxValue = arr[0];
    for (i = 1; i < arr.length; i++) { 
        if (arr[i] > maxValue) {
            maxValue = arr[i];       
        }
    }
    var maxDigit = getMaxDigit(maxValue);//最高位数
 
    var mod = 10;
    var dev = 1;
    for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
        for(var j = 0; j < arr.length; j++) {
            var bucket = parseInt((arr[j] % mod) / dev); //个位、十位......的数
            if(counter[bucket]==null) {
                counter[bucket] = [];
            }
            counter[bucket].push(arr[j]);
        }
        var pos = 0;
        for(var j = 0; j < counter.length; j++) {
            var value = null;
            if(counter[j]!=null) {
                while ((value = counter[j].shift()) != null) { //shift(),把数组的第一个元素从其中删除,并返回第一个元素的值。
                      arr[pos++] = value;
                }
          }
        }
    }
    return arr;
}
function getMaxDigit(maxValue){//求整数的位数
    var maxDigit = 1;
while(maxValue/10>=1){
    maxValue=maxValue/10;
    maxDigit++;
}
return maxDigit;
}

参考:
https://zhuanlan.zhihu.com/p/42586566

本文链接https://blog.csdn.net/qq_39903567/article/details/115638973

你可能感兴趣的:(数据结构与算法,前端,排序算法,JavaScript,面试)