排序

219 阅读17分钟

记录一下前端常见排序,学习~~~

冒泡排序

简介:比较两个相邻的元素,将值大的元素交换到右边;

2.思路:依次比较相邻的两个数,将比较小的数放在前面,比较大的数放在后面。

    (1)第一次比较:首先比较第一和第二个数,将小数放在前面,将大数放在后面。

    (2)比较第2和第3个数,将小数 放在前面,大数放在后面。

    ......

    (3)如此继续,知道比较到最后的两个数,将小数放在前面,大数放在后面,重复步骤,直至全部排序完成

    (4)在上面一趟比较完成后,最后一个数一定是数组中最大的一个数,所以在比较第二趟的时候,最后一个数是不参加比较的。

    (5)在第二趟比较完成后,倒数第二个数也一定是数组中倒数第二大数,所以在第三趟的比较中,最后两个数是不参与比较的。

    (6)依次类推,每一趟比较次数减少依次

现在这样的一个数组:[4,5,6,3,2,1],用冒泡的思路去解决,思考一下怎么写?

const bubbleSort = (arr: Array) => {

    if(!arr.length) return;

    const len = arr.length;

    for(let i=0; i<len-1;i++){

        for(let j=i;j<len-1-i;j++){

            let item = null;

            if(arr[j] > arr[j+1]){ // 相邻两元素比较,这里是从小到大的顺序

                item = arr[j+1]; // 元素交换

                arr[j+1] = arr[j];

                arr[j] = item;

            }

        }

    }

    return arr;

}
冒泡次数和冒泡结果

第一次冒泡:4和5比较,5比4大,位置不变,5和6比较,6比5大,位置不变,6和3比较,交换位置,6和3比较,交换位置,6和1比较,交换位置,交换位置,最后为 [4,5,3,2,1,6];

第二次冒泡:4比5小,位置不变,5和3比,交换位置,5和2比,交换位置,5和1比,交换位置,最后为 [4,3,2,1,5,6];

第三次冒泡:4和3比较,交换位置,4和2比较,交换位置,4和1比较,交换位置,4和5比较,位置不变,最后为 [3,2,1,4,5,6];

第四次冒泡:3和2比较,交换位置,3和1比较,交换位置,3和4比较,位置不变,最后为 [2,1,3,4,5,6];

第五次冒泡:2和1比较,交换位置,2和3比较,位置不变,最后为 [1,2,3,4,5,6];

第六次冒泡:1和2比较,位置不变,最后为 [1,2,3,4,5,6]

总共进行了6次冒泡,排序结果:[1,2,3,4,5,6]

1635779306(1).jpg

分析:

n的元素要想排序完成,需要n-1次排序,i次的排序为n-i次。

原地排序:冒泡的过程只涉及相邻数据的交换操作,只需要常量级的临时空间,所以它的空间复杂度为O(1),是一个原地排序算法。

稳定排序:在冒泡排序中,只有交换才可以改变两个元素的前后顺序。为了保证冒泡排序算法的稳定性,当有相邻的两个元素大小相等的时候,我们不做交换,相同大小的 数据在排序前后不会改变顺序,所以冒泡排序是稳定的排序算法。

时间复杂度:最好情况下,要排序的数据已经是有序的了,我们只需要进行一次冒泡操作,就可以结束了,所以最好情况时间复杂度是O(n)。而最坏的情况是,要排序的数据 刚好是倒序排列的,我们需要进行n次冒泡操作,所以最坏情况时间复杂度为O(n2)。

快速排序

快排思想:如果要排序数组中下标从p到r之间的一组数据,我们选择p到r之间的任意一个数据作为pivot(分区点)。

我们遍历p到r之间的数据,将小于pivot的放到左边,将大于pivot的放到右边,将pivot放到中间。经过这一步骤之后,数组p到r之间的数据就被分成了三个部分,前 面p到q-1之间都是小于pivot的,中间是pivot,后面的q+1到r之间是大于pivot的。

1.png

可以用递归排序下标从p到q-1之间的数据和下标从q+1到r之间的数据,直到区间缩小为1,就说明所有的数据都有序了。

我们申请两个临时数组X和Y,遍历A[p…r],将小于pivot的元素都拷贝到临时数组X,将大 于pivot的元素都拷贝到临时数组Y,最后再将数组X和数组Y中数据顺序拷贝到A[p…r]。

2.png 我们通过游标i把A[p…r-1]分成两部分。A[p…i-1]的元素都是小于pivot的,我们暂且叫它“已处理区间”,A[i…r-1]是“未处理区 间”。我们每次都从未处理的区间A[i…r-1]中取一个元素A[j],与pivot对比,如果小于pivot,则将其加入到已处理区间的尾部,也就是A[i]的位置。

数组的插入操作还记得吗?在数组某个位置插入元素,需要搬移数据,非常耗时。当时我们也讲了一种处理技巧,就是交换,在O(1)的时间复杂度内完成插入操 作。这里我们也借助这个思想,只需要将A[i]与A[j]交换,就可以在O(1)时间复杂度内将A[j]放到下标为i的位置。

如果数组中有两个相同的元素,比如序列6,8,7,6,3,5,9,4,在经过第一次分区操作之后,两个6的相对先后顺序就会改 变。所以,快速排序并不是一个稳定的排序算法。

快排总结如下图

image.png

它的处理过程是由上到下的,先分区,然后再处理子问题。

代码如下:

function quickSort(arr, left, right) {
    var len = arr.length,
        partitionIndex,
        left = typeof left != 'number' ? 0 : left,
        right = typeof right != 'number' ? len - 1 : right;
 
    if (left < right) {
        partitionIndex = partition(arr, left, right);
        quickSort(arr, left, partitionIndex-1);
        quickSort(arr, partitionIndex+1, right);
    }
    return arr;
}
 
function partition(arr, left ,right) {     // 分区操作
    var pivot = left,                      // 设定基准值(pivot)
        index = pivot + 1;
    for (var i = index; i <= right; i++) {
        if (arr[i] < arr[pivot]) {
            swap(arr, i, index);
            index++;
        }       
    }
    swap(arr, pivot, index - 1);
    return index-1;
}
 
function swap(arr, i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

插入排序

简介:插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

一个有序的数组,我们往里面添加一个新的数据后,如何继续保持数据有序呢?很简单,我们只要遍历数组,找到数据应该插入的位置将 其插入即可。

image.png

这是一个动态排序的过程,即动态地往有序集合中添加数据,我们可以通过这种方法保持集合中的数据一直有序。而对于一组静态数据,我们也可以借鉴上面讲 的插入方法,来进行排序,于是就有了插入排序算法。

那插入排序具体是如何借助上面的思想来实现排序的呢?

首先,我们将数组中的数据分为两个区间,已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束。 如图所示,要排序的数据是4,5,6,1,3,2,其中左侧为已排序区间,右侧是未排序区间。

image.png

插入排序也包含两种操作,一种是元素的比较,一种是元素的移动。当我们需要将一个数据a插入到已排序区间时,需要拿a与已排序区间的元素依次比较大小,找到合适的插入位置。找到插入点之后,我们还需要将插入点之后的元素顺序往后移动一位,这样才能腾出位置给元素 插入。

对于不同的查找插入点方法(从头到尾、从尾到头),元素的比较次数是有区别的。但对于一个给定的初始序列,移动操作的次数总是固定的,就等于逆序度。

为什么说移动次数就等于逆序度呢?我拿刚才的例子画了一个图表,你一看就明白了。满有序度是n*(n-1)/2=15,初始序列的有序度是5,所以逆序度是10。插入排 序中,数据移动的个数总和也等于10=3+3+4。

image.png

代码实现:

function insertionSort(arr) {
    var len = arr.length;
    var preIndex, current;
    for (var i = 1; i < len; i++) {
        preIndex = i - 1;
        current = arr[i];
        while (preIndex >= 0 && arr[preIndex] > current) {
            arr[preIndex + 1] = arr[preIndex];
            preIndex--;
        }
        arr[preIndex + 1] = current;
    }
    return arr;
}

那么,插入排序是原地排序算法吗?

从实现过程可以很明显地看出,插入排序算法的运行并不需要额外的存储空间,所以空间复杂度是O(1),也就是说,这是一个原地排序算法。

插入排序是稳定的排序算法吗?

在插入排序中,对于值相同的元素,我们可以选择将后面出现的元素,插入到前面出现元素的后面,这样就可以保持原有的前后顺序不变,所以插入排序是稳定 的排序算法。

插入排序的时间复杂度是多少?

如果要排序的数据已经是有序的,我们并不需要搬移任何数据。如果我们从尾到头在有序数据组里面查找插入位置,每次只需要比较一个数据就能确定插入的位 置。所以这种情况下,最好是时间复杂度为O(n)。注意,这里是从尾到头遍历已经有序的数据。

如果数组是倒序的,每次插入都相当于在数组的第一个位置插入新的数据,所以需要移动大量的数据,所以最坏情况时间复杂度为O(n2)。

对于插入排序来说,每次插入操作都相当于在数组中插入一个数据,循环 执行n次插入操作,所以平均时间复杂度为O(n2)。

归并排序

归并排序是一种分治算法。其思想是将原始数组切分成较小的数组,直到每个小数组只有一

个位置,接着将小数组归并成较大的数组,直到最后只有一个排序完毕的大数组。

由于是分治法,归并排序也是递归的:

this.mergeSort = function(){

array = mergeSortRec(array);

};

我们将声明mergeSort方法以供随后使用,而mergeSort方法将

会调用mergeSortRec,该函数是一个递归函数:

var mergeSortRec = function(array){ 
 var length = array.length; 
 if(length === 1) { //{1} 
 return array; //{2} 
 } 
 var mid = Math.floor(length / 2), //{3} 
 left = array.slice(0, mid), //{4} 
 right = array.slice(mid, length); //{5} 
 return merge(mergeSortRec(left), mergeSortRec(right)); //{6} 
};
var merge = function(left, right){ 
 var result = [], // {7} 
 il = 0,
 ir = 0; 
 while(il < left.length && ir < right.length) { // {8} 
 if(left[il] < right[ir]) { 
 result.push(left[il++]); // {9} 
 } else{ 
 result.push(right[ir++]); // {10} 
 } 
 } 
 while (il < left.length){ // {11} 
 result.push(left[il++]); 
 } 
 while (ir < right.length){ // {12} 
 result.push(right[ir++]); 
 } 
 return result; // {13} 
};

归并排序将一个大数组转化为多个小数组直到只有一个项。由于算法是递归的,我们需要一

个停止条件,在这里此条件是判断数组的长度是否为1(行{1})。如果是,则直接返回这个长度

为1的数组(行{2}),因为它已排序了。

如果数组长度比1大,那么我们得将其分成小数组。为此,首先得找到数组的中间位(行{3}),

找到后我们将数组分成两个小数组,分别叫作left(行{4})和right(行{5})。left数组由

索引0至中间索引的元素组成,而right数组由中间索引至原始数组最后一个位置的元素组成。

下面的步骤是调用merge函数(行{6}),它负责合并和排序小数组来产生大数组,直到回到

原始数组并已排序完成。为了不断将原始数组分成小数组,我们得再次对left数组和right数组

递归调用mergeSortRec,并同时作为参数传递给merge函数。

merge函数接受两个数组作为参数,并将它们归并至一个大数组。排序发生在归并过程中。

首先,需要声明归并过程要创建的新数组以及用来迭代两个数组(left和right数组)所需的两

个变量(行{7})。迭代两个数组的过程中(行{8}),我们比较来自left数组的项是否比来自right

数组的项小。如果是,将该项从left数组添加至归并结果数组,并递增迭代数组的控制变量(行

{9});否则,从right数组添加项并递增相应的迭代数组的控制变量(行{10})。接下来,将left

数组或者right数组所有剩余的项添加到归并数组中。最后,将归并数组作为结果返回。

如果执行mergeSort函数,下图是具体的执行过程:

image.png

可以看到,算法首先将原始数组分割直至只有一个元素的子数组,然后开始归并。归并过程

也会完成排序,直至原始数组完全合并并完成排序。

选择排序

选择排序算法是一种原址比较排序算法。选择排序大致的思路是找到数据结构中的最小值并

将其放置在第一位,接着找到第二小的值并将其放在第二位,以此类推。

this.selectionSort = function(){
    var length = array.length, //{1}
    indexMin;
    for (var i=0; i<length-1; i++){ //{2}
        indexMin = i; //{3}
        for (var j=i; j<length; j++){ //{4}
            if(array[indexMin]>array[j]){ //{5}
                indexMin = j; //{6}
            }
        }

        if (i !== indexMin){ //{7}
            swap(i, indexMin);
        }
    }
};

首先声明一些将在算法内使用的变量(行{1})。接着,外循环(行{2})迭代数组,并控制

迭代轮次(数组的第n个值——下一个最小值)。我们假设本迭代轮次的第一个值为数组最小值(行

{3})。然后,从当前i的值开始至数组结束(行{4}),我们比较是否位置j的值比当前最小值小

(行{5});如果是,则改变最小值至新最小值(行{6})。当内循环结束(行{4}),将得出数组第

n小的值。最后,如果该最小值和原最小值不同(行{7}),则交换其值。

用以下代码段来测试选择排序算法:

array = createNonSortedArray(5);

console.log(array.toString());

array.selectionSort();

console.log(array.toString());

image.png

数组底部的箭头指示出当前迭代轮寻找最小值的数组范围(内循环{4}),示意图中的每一 步则表示外循环。

选择排序同样也是一个复杂度为O(n2)的算法。和冒泡排序一样,它包含有嵌套的两个循环,这导致了二次方的复杂度。然而,接下来要学的插入排序比选择排序性能要好。

总的来说,选择排序是一种很容易理解和实现的简单排序算法,它有两个很鲜明的特点。 运行时间和输入无关 为了找出最小元素而扫描一遍数组并不能为下一遍扫描提供什么信息。这种性质在某些情况下是缺点,因为使用选择排序的人可能会惊讶的发现,一个已经有序的数组或是主键全部相等的数组和一个元素随机排列的数组所用的排序时间竟然一样长!我们将会看到,其他算法会更善于利用输入的初始状态。

数据移动是最少的。 每次交换都会改变两个数组元素的值,因此选择排序用了N次交换-----交换次数和数组的大小是线性关系。我们将研究的其他任何算法都不具备这个特征(大部分的增长数量级都是线性对数或者平方级别)。

堆排序

是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

堆排序具有空间原址性:任何时候都只需要常数个额外的元素空间存储临时数据。因此,堆排序集合了我们目前已经讨论的两种排序算法优点的一种排序算法。

堆排序引入了另一种算法设计技巧:使用一种我们称之为“堆”的数据结构来进行信息管理。堆不仅用在堆排序中,而且它也可以构造一种有效的优先队列。

(二叉)堆是一个数组,它可以被看成一个近似的完全二叉树。树上的每一个节点对应数组中的一个元素。除了最底层,该树是完全充满的,而且是从左向右填充。表示堆的数组A包括两个属性:A.length 通常给出数组元素的个数,A.heap-size表示有多少个堆元素存储在该数组中。也就是说,虽然A[1..A.length]可能都存有数据,但只有A[1..A.heap-size]中存放的是堆的有效元素,这里,0<= A.heap-size<=A.length。树的根节点是A[1],这样给定一个结点的下标i,我们很容易计算它的父节点、左孩子和右孩子的下标:

image.png

  • 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
  • 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
  • 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
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;
}

希尔排序

是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

  • 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
  • 按增量序列个数k,对序列进行k 趟排序;
  • 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
function shellSort(arr) {
    var len = arr.length;
    for (var gap = Math.floor(len / 2); gap > 0; gap = Math.floor(gap / 2)) {
        // 注意:这里和动图演示的不一样,动图是分组执行,实际操作是多个分组交替执行
        for (var i = gap; i < len; i++) {
            var j = i;
            var current = arr[i];
            while (j - gap >= 0 && current < arr[j - gap]) {
                 arr[j] = arr[j - gap];
                 j = j - gap;
            }
            arr[j] = current;
        }
    }
    return arr;
}

稳定性: 由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的。

优劣: 不需要大量的辅助空间,和归并排序一样容易实现。希尔排序是基于插入排序的一种算法, 在此算法基础之上增加了一个新的特性,提高了效率。希尔排序的时间的时间复杂度为O(

 ),希尔排序时间复杂度的下界是n*log2n。希尔排序没有快速排序算法快 O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。但是比O(

 )复杂度的算法快得多。并且希尔排序非常容易实现,算法代码短而简单。 此外,希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差。专家们提倡,几乎任何排序工作在开始时都可以用希尔排序,若在实际使用中证明它不够快,再改成快速排序这样更高级的排序算法. 本质上讲,希尔排序算法是直接插入排序算法的一种改进,减少了其复制的次数,速度要快很多。 原因是,当n值很大时数据项每一趟排序需要移动的个数很少,但数据项的距离很长。当n值减小时每一趟需要移动的数据增多,此时已经接近于它们排序后的最终位置。 正是这两种情况的结合才使希尔排序效率比插入排序高很多。Shell算法的性能与所选取的分组长度序列有很大关系。只对特定的待排序记录序列,可以准确地估算关键词的比较次数和对象移动次数。想要弄清关键词比较次数和记录移动次数与增量选择之间的关系,并给出完整的数学分析,今仍然是数学难题。

计数排序

计数排序的基本思想是对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数(此处并非比较各元素的大小,而是通过对元素值的计数和计数值的累加来确定)。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。例如,如果输入序列中只有17个元素的值小于x的值,则x可以直接存放在输出序列的第18个位置上。当然,如果有多个元素具有相同的值时,我们不能将这些元素放在输出序列的同一个位置上,因此,上述方案还要作适当的修改。

步骤:

1、找出原数组中元素值最大的,记为max

2、创建一个新数组count,其长度是max加1,其元素默认值都为0。

3、遍历原数组中的元素,以原数组中的元素作为count数组的索引,以原数组中的元素出现次数作为count数组的元素值。

4、创建结果数组result,起始索引index

5、遍历count数组,找出其中元素值大于0的元素,将其对应的索引作为元素值填充到result数组中去,每处理一次,count中的该元素值减1,直到该元素值不大于0,依次处理count中剩下的元素。

6、返回结果数组result

function countSort(ary) {
        let newAry = new Array(ary.length).fill(0);
        for (const value of ary) {
            newAry[value]++;
        }
        ary = [];
        // 给ary重新赋值
        for(var i =0; i<newAry.length; i++) {
            // 循环数字次数
            for(var k = newAry[i]; k>0; k--) {
                ary.push(i);
            }
        }
        newAry = null;
        return ary;
    }

小结:

计数排序算法没有用到元素间的比较,它利用元素的实际值来确定它们在输出数组中的位置。因此,计数排序算法不是一个基于比较的排序算法,从而它的计算时间下界不再是O(nlogn)。另一方面,计数排序算法之所以能取得线性计算时间的上界是因为对元素的取值范围作了一定限制,即k=O(n)。如果k=n^2,n^3,..,就得不到线性时间的上界。此外,我们还看到,由于算法第4行使用了downto语句,经计数排序,输出序列中值相同的元素之间的相对次序与他们在输入序列中的相对次序相同,换句话说,计数排序算法是一个稳定的排序算法。

桶排序

桶排序的大体思路就是先将数组分到有限个桶中,再对每个桶中的数据进行排序(对每个桶中数据的排序可以是桶排序的递归,或其他算法,在桶中数据较少的时候用插入排序最为理想)。

// 桶排序
function bucketSort(nums) {
  // 桶的个数,只要是正数即可
  let num = 5;
  let max = Math.max(...nums);
  let min = Math.min(...nums);
  // 计算每个桶存放的数值范围,至少为1,
  let range = Math.ceil((max - min) / num) || 1;
  // 创建二维数组,第一维表示第几个桶,第二维表示该桶里存放的数
  let arr = Array.from(Array(num)).map(() => Array().fill(0));
  nums.forEach(val => {
      // 计算元素应该分布在哪个桶
      let index = parseInt((val - min) / range);
      // 防止index越界,例如当[5,1,1,2,0,0]时index会出现5
      index = index >= num ? num - 1 : index;
      let temp = arr[index];
      // 插入排序,将元素有序插入到桶中
      let j = temp.length - 1;
      while (j >= 0 && val < temp[j]) {
          temp[j + 1] = temp[j];
          j--;
      }
      temp[j + 1] = val;
      console.log(temp);
  });
  // 修改回原数组
  let res = [].concat.apply([], arr);
  nums.forEach((val, i) => {
      nums[i] = res[i];
  });
  return nums;
}

对N个数据进行桶排序的时间复杂度分为两部分:

  1、对每一个数据进行映射函数的计算(映射函数确定了数据将被分到哪个桶),时间复杂度为O(N)。

  2、对桶内数据的排序,时间复杂度为∑ O(Ni*logNi) ,其中Ni 为第i个桶的数据量。

  对于N个待排数据,M个桶,平均每个桶[N/M]个数据的桶排序平均时间复杂度为:O(N)+O(M*(N/M)log(N/M))=O(N+N(logN-logM))=O(N+NlogN-NlogM),当N=M时,即极限情况下每个桶只有一个数据时。桶排序的最好效率能够达到O(N)。

  对于相同数量的数据,桶的数量越多,数据分散得越平均,桶排序的效率越高,可以说,桶排序的效率是空间的牺牲换来的。由此可见,桶排序对待排序数据的要求是非常苛刻的,适用场景是在数据分布相对比较均匀或者数据跨度范围并不是很大时。如果数据跨度非常大,空间消耗就会很大。所以桶排序很少使用。

空间复杂度:数据规模为n,划分到k个桶中,总空间复杂度O(n + k)。

时间复杂度:1.获取最大值和最小值,遍历数组,操作次数为n。2.初始化所有桶,操作次数为k。3.将所有待排序数据放入到对应的桶中操作次数为n。4.每个非空桶进行子排序使用时间复杂度为O(nlogn)的快速排序总操作次数为(k/n)log(k/n)k。5.按顺序将每个桶中数据进行合并操作次数n。所以总的时间复杂度为:3n+k+(k/n)log(k/n)k,因此时间复杂度为:O(n+k+logn-logk)。

稳定性:稳定

基数排序(Radix sort)

假设我们有10万个手机号码,希望将这10万个手机号码从小到大排序,你有什么比较快速的排序方法呢?

基数排序是一种线性排序,线性排序算法的时间复杂度为 O(n)。线性排序算法除了基数排序还包括桶排序、计数排序,这3种排序算法都不涉及元素之间的比较操作,是基于非比较的排序算法。

算法原理(以排序10万个手机号为例来说明)

  1. 比较两个手机号码a,b的大小,如果在前面几位中a已经比b大了,那后面几位就不用看了。
  2. 借助稳定排序算法的思想,可以先按照最后一位来排序手机号码,然后再按照倒数第二位来重新排序,以此类推,最后按照第一个位重新排序。
  3. 经过11次排序后,手机号码就变为有序的了。
  4. 每次排序有序数据范围较小,可以使用桶排序或计数排序来完成。

使用条件

  1. 要求数据可以分割独立的“位”来比较;
  2. 位之间由递进关系,如果a数据的高位比b数据大,那么剩下的地位就不用比较了;
  3. 每一位的数据范围不能太大,要可以用线性排序,否则基数排序的时间复杂度无法做到O(n)。

现在再来看10万个手机号的问题,假设要比较两个手机号码a,b的大小,如果在前面几位中,a手机号码已经比b手机号码大了,那后面的几位就不用看了。先按照最后一位来排序手机号码,然后,再按照倒数第二位重新排序,以此类推,最后按照第一位重新排序。经过11次排序之后,手机号码就都有序了。

例如这样一组数据:2345、1234、4567、2354 在对十进制整数进行基数排序的时候,个<十<百<千,通用比较规则:0<1<2<3<4<5<6<7<8<9

  1. 第一次按照个位排序为 1234|2354 2345 4567
  2. 第二次按照十位排序为 1234 2345 2354 4567
  3. 第三次按照百位排序为 1234 2345|2354 4567
  4. 第四次按照千位排序为 1234 2345 2354 4567 处理完毕
const radioSort = (arrs) => {
    let len = arrs.length
    if (len < 2) {
        return
    }
    let counter = []
    let mod = 10
    let dev = 1
    // 找出最大位数
    let maxNum
    for (let i = 0; i < len-1; i++) {
        if (arrs[i] > arrs[i+1]) {
            maxNum = arrs[i]
        } else {
            maxNum = arrs[i+1]
        }
    }
    let maxDigit = 0
    while (maxNum != 0) {
        maxNum = parseInt(maxNum/10)
        maxDigit++;
    }
     
    for (let i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
        for(let j = 0; j < arrs.length; j++) {
            let bucket = parseInt((arrs[j] % mod) / dev)
            if(counter[bucket]==null) {
                counter[bucket] = []
            }
            counter[bucket].push(arrs[j])
        }
        let pos = 0
        for(let j = 0; j < counter.length; j++) {
            let value = null
            if(counter[j]!=null) {
                while ((value = counter[j].shift()) != null) {
                  arrs[pos++] = value
                }
          }
        }
    }
}