Thanks to visit codestin.com
Credit goes to github.com

Skip to content

十大排序算法 #56

@nmsn

Description

@nmsn

参考: https://github.com/damonare/Sorts

交换函数

function swap(arr, i, j) {
  temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}

// es6
function swap(arr, i, j) {
  [a[i], a[j]] = [a[j], a[i]];
}

比较排序

冒泡排序

概念

比较前后两位数字的大小,如果前小后大,就交换位置,所以每次循环都能找出当前循环中最大的数字,推到最后面,然后每次循环减少最后一位(已经找出上一次循环中最大的数字,下一次就不用循环这个数字了)

代码实现

for (let i = 0; i < arr.length - 1; i++) {
  for (let j = 0; j < arr.length - 1; j++) {
    if (arr[j] > arr[j + 1]) {
      swap(arr, j, j+1);
    }
  }
}

优化方案

  1. 标记是否有序
  2. 有序区优化
  3. 鸡尾酒优化

https://juejin.cn/post/6956624975600025613

选择排序

循环中找到最小的数字,跟最左边的数字进行交换

代码实现

for (let i = 0; i < arr.length - 1; i++) {
  for (let j = i + 1; j < arr.length; j++) {
    if (arr[i] > arr[j]) {
      swap(arr, i, j);
    }
  }
}

插入排序

概念

维护一个有序区,把元素一个一个插入到有序区的适当位置,直到所有元素有序

代码实现

for (var i = 0; i < arr.length; i++) {
  var n = i;
  while (arr[n] > arr[n + 1] && n >= 0) {
    swap(arr, n , n+1)
    n--;
  }
}

总结

这三者属于简单排序,平均时间复杂度都是 O(n2)

接近有序数组 插入排序 性能最好

无序时 选择排序性能最优

归并排序

概念

归并排序是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并

代码实现

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 = [];
    console.time('归并排序耗时');
    while (left.length && right.length) {
        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());
    console.timeEnd('归并排序耗时');
    return result;
}

总结

  1. 归并排序是稳定的排序
  2. 速度仅次于快速排序归并排序的比较次数小于快速排序的比较次数,移动次数一般多于快速排序的移动次数

把数组拆成两两一组,有序合并,并递归,经典的分治思想

快速排序

快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

  1. 从数列中挑出一个元素,称为 "基准"(pivot)
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序
/*
 * 易于理解概念的版本
 * 但是占用了额外的储存空间
 * concat 操作也会增加复杂度
*/
var quickSort = function(arr) {
  if (arr.length <= 1) { return arr; }
       
  const midIndex = Math.floor(arr.length / 2);
  const mid = arr.splice(pivotIndex, 1)[0];
  const left = [];
  const right = [];
  for (let i = 0; i < arr.length; i++){
    if (arr[i] < mid) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }
  return [...quickSort(left), mid, ...quickSort(right)];
};

// 不占用额外空间的版本
function partition(array, start, end) {
  let j = start
  let pivot = array[end]
  for (let i = start; i <= end; i++) {
    if (array[i] <= pivot) {
      swap(array, i, j++)
    }
  }
  return j - 1
}

function quickSort(array, start = 0, end = array.length -1) {
  let pivotIndex = partition(array, start, end)
  quickSort(array, start, pivotIndex - 1)
  quickSort(array, pivotIndex + 1, end)
  return array
}

希尔排序

概念

希尔排序(Shell's Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因 D.L.Shell 于 1959 年提出而得名。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。

代码实现

function shellSort(arr) {
    var len = arr.length,
        temp,
        gap = 1;
    console.time('希尔排序耗时:');
    while(gap < len/5) {          //动态定义间隔序列
        gap =gap*5+1;
    }
    for (gap; gap > 0; gap = Math.floor(gap/5)) {
        for (var i = gap; i < len; i++) {
            temp = arr[i];
            for (var j = i-gap; j >= 0 && arr[j] > temp; j-=gap) {
                arr[j+gap] = arr[j];
            }
            arr[j+gap] = temp;
        }
    }
    console.timeEnd('希尔排序耗时:');
    return arr;
}

总结

  1. 不需要大量的辅助空间,和归并排序一样容易实现
  2. 希尔排序是基于插入排序的一种算法, 在此算法基础之上增加了一个新的特性,提高了效率
  3. 希尔排序的时间的时间复杂度为O( n3/2),希尔排序时间复杂度的下界是n*log2n
  4. 希尔排序没有快速排序算法快 O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择,但是比O( )复杂度的算法快得多

堆排序

概念

堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了

代码实现

let 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) {
  // 左节点
  let left = 2 * i + 1;
  // 右节点
  let right = 2 * i + 2;
  let 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 heapSort(arr) {
  // 先建立顶堆
  buildMaxHeap(arr);

  for (var i = arr.length - 1; i > 0; i--) {
    // 交换首尾节点
    swap(arr, 0, i);
    // len-- 代表堆尾为排序完成的节点
    len--;
    // 对新堆进行排序
    heapify(arr, 0);
  }
  return arr;
}

总结

  1. 堆排序不是稳定排序

非比较排序

计数排序,基数排序,桶排序等非比较排序算法,平均时间复杂度都是O(n)。这些排序因为其待排序元素本身就含有了定位特征,因而不需要比较就可以确定其前后位置,从而可以突破比较排序算法时间复杂度O(nlgn)的理论下限。

计数排序

计数排序(Counting sort)是一种稳定的排序算法。计数排序是最简单的特例,由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存,适用性不高。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)

桶排序

桶排序假设输入是由一个随机过程产生,该过程将元素均匀、独立地分布在[0,1)区间上。

我们将[0,1)区间划分为n个相同大小的子区间,称为桶。然后将输入数据分别放到各个桶中。如果数据分布得很均匀,每个桶中的数据就不会太多,都会维持在常数量级。

基数排序

基数排序改善了计数排序,简单来说,基数排序算法就是将整数或字符串切分成不同的数字或字符,然后按对应位置的数或字符分别进行比较,这样就能将辅助数组或桶的数量降低到一个较小的值,经过多轮排序后得到最终的排序结果。

复杂度

sort.png

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions