[算法总结] 十大排序算法

[算法总结] 十大排序算法

简介: 本文首发于我的个人博客:尾尾部落排序算法是最经典的算法知识。因为其实现代码短,应该广,在面试中经常会问到排序算法及其相关的问题。一般在面试中最常考的是快速排序和归并排序等基本的排序算法,并且经常要求现场手写基本的排序算法。

本文首发于我的个人博客:JavaGPT

排序算法是最经典的算法知识。因为其实现代码短,应该广,在面试中经常会问到排序算法及其相关的问题。一般在面试中最常考的是快速排序和归并排序等基本的排序算法,并且经常要求现场手写基本的排序算法。如果这些问题回答不好,估计面试就凉凉了。所以熟练掌握排序算法思想及其特点并能够熟练地手写代码至关重要。

下面介绍几种常见的排序算法:冒泡排序、选择排序、插入排序、归并排序、快速排序、希尔排序、堆排序、计数排序、桶排序、基数排序的思想,其代码均采用Java实现。

1. 冒泡排序外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

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

img_33a947c71ad62b254cab62e5364d2813.gif

冒泡排序

算法实现
public static void bubbleSort(int[] arr) {
    int temp = 0;
    for (int i = arr.length - 1; i > 0; i--) { 
        for (int j = 0; j < i; j++) { 
            if (arr[j] > arr[j + 1]) {
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}
稳定性

在相邻元素相等时,它们并不会交换位置,所以,冒泡排序是稳定排序。

适用场景

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

代码优化

在数据完全有序的时候展现出最优时间复杂度,为O(n)。其他情况下,几乎总是O( n^2 )。因此,算法在数据基本有序的情况下,性能最好。
要使算法在最佳情况下有O(n)复杂度,需要做一些改进,增加一个swap的标志,当前一轮没有进行交换时,说明数组已经有序,没有必要再进行下一轮的循环了,直接退出。

public static void bubbleSort(int[] arr) {
    int temp = 0;
    boolean swap;
    for (int i = arr.length - 1; i > 0; i--) { 
        swap=false;
        for (int j = 0; j < i; j++) { 
            if (arr[j] > arr[j + 1]) {
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                swap=true;
            }
        }
        if (swap==false){
            break;
        }
    }
}

2. 选择排序外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

选择排序是一种简单直观的排序算法,它也是一种交换排序算法,和冒泡排序有一定的相似度,可以认为选择排序是冒泡排序的一种改进。

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

img_1c7e20f306ddc02eb4e3a50fa7817ff4.gif

选择排序

算法实现
public static void selectionSort(int[] arr) {
    int temp, min = 0;
    for (int i = 0; i < arr.length - 1; i++) {
        min = i;
        
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[min] > arr[j]) {
                min = j;
            }
        }
        if (min != i) {
            temp = arr[i];
            arr[i] = arr[min];
            arr[min] = temp;
        }
    }
}
稳定性

用数组实现的选择排序是不稳定的,用链表实现的选择排序是稳定的。
不过,一般提到排序算法时,大家往往会默认是数组实现,所以选择排序是不稳定的。

适用场景

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

3. 插入排序外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

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

img_91b76e8e4dab9b0cad9a017d7dd431e2.gif

插入排序

算法实现
public static void insertionSort(int[] arr){
    for (int i=1; i<arr.length; ++i){
        int value = arr[i];
        int position=i;
        while (position>0 && arr[position-1]>value){
            arr[position] = arr[position-1];
            position--;
        }
        arr[position] = value;
    }
}
稳定性

由于只需要找到不大于当前数的位置而并不需要交换,因此,直接插入排序是稳定的排序方法。

适用场景

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

4. 归并排序外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

算法描述

两种方法

  • 递归法(Top-down)
  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
  4. 重复步骤3直到某一指针到达序列尾
  5. 将另一序列剩下的所有元素直接复制到合并序列尾
  • 迭代法(Bottom-up)

原理如下(假设序列共有n个元素):

  1. 将序列每相邻两个数字进行归并操作,形成ceil(n/2)个序列,排序后每个序列包含两/一个元素
  2. 若此时序列数不是1个则将上述序列再次归并,形成ceil(n/4)个序列,每个序列包含四/三个元素
  3. 重复步骤2,直到所有元素排序完毕,即序列数为1
动图演示

img_cdda3f11c6efbc01577f5c29a9066772.gif

归并排序

算法实现
public static void mergeSort(int[] arr){
    int[] temp =new int[arr.length];
    internalMergeSort(arr, temp, 0, arr.length-1);
}
private static void internalMergeSort(int[] arr, int[] temp, int left, int right){
    
    if (left<right){
        int middle = (left+right)/2;
        internalMergeSort(arr, temp, left, middle);          
        internalMergeSort(arr, temp, middle+1, right);       
        mergeSortedArray(arr, temp, left, middle, right);    
    }
}

private static void mergeSortedArray(int arr[], int temp[], int left, int middle, int right){
    int i=left;      
    int j=middle+1;
    int k=0;
    while (i<=middle && j<=right){
        temp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
    }
    while (i <=middle){
        temp[k++] = arr[i++];
    }
    while ( j<=right){
        temp[k++] = arr[j++];
    }
    
    for (i=0; i<k; ++i){
        arr[left+i] = temp[i];
    }
}
稳定性

因为我们在遇到相等的数据的时候必然是按顺序“抄写”到辅助数组上的,所以,归并排序同样是稳定算法。

适用场景

归并排序在数据量比较大的时候也有较为出色的表现(效率上),但是,其空间复杂度O(n)使得在数据量特别大的时候(例如,1千万数据)几乎不可接受。而且,考虑到有的机器内存本身就比较小,因此,采用归并排序一定要注意。

5. 快速排序外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

快速排序是一个知名度极高的排序算法,其对于大数据的优秀排序性能和相同复杂度算法中相对简单的实现使它注定得到比其他算法更多的宠爱。

算法描述
  1. 从数列中挑出一个元素,称为"基准"(pivot),
  2. 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任何一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  3. 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
动图演示

img_c411339b79f92499dcb7b5f304c826f4.gif

快速排序

算法实现
public static void quickSort(int[] arr){
    qsort(arr, 0, arr.length-1);
}
private static void qsort(int[] arr, int low, int high){
    if (low >= high)
        return;
    int pivot = partition(arr, low, high);        
    qsort(arr, low, pivot-1);                   
    qsort(arr, pivot+1, high);                  
}
private static int partition(int[] arr, int low, int high){
    int pivot = arr[low];     
    while (low < high){
        while (low < high && arr[high] >= pivot) --high;
        arr[low]=arr[high];             
        while (low < high && arr[low] <= pivot) ++low;
        arr[high] = arr[low];           
    }
    
    arr[low] = pivot;
    
    return low;
}
稳定性

快速排序并不是稳定的。这是因为我们无法保证相等的数据按顺序被扫描到和按顺序存放。

适用场景

快速排序在大多数情况下都是适用的,尤其在数据量大的时候性能优越性更加明显。但是在必要的时候,需要考虑下优化以提高其在最坏情况下的性能。

6. 堆排序外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。堆排序就是把最大堆堆顶的最大数取出,将剩余的堆继续调整为最大堆,再次将堆顶的最大数取出,这个过程持续到剩余数只有一个时结束。

树的概念

关于树的概念请参考:[算法总结] 二叉树

堆的概念

堆是一种特殊的完全二叉树(complete binary tree)。完全二叉树的一个“优秀”的性质是,除了最底层之外,每一层都是满的,这使得堆可以利用数组来表示(普通的一般的二叉树通常用链表作为基本容器表示),每一个结点对应数组中的一个元素。
如下图,是一个堆和数组的相互关系:

img_168fa8c66f8401f274dbe0a25460cb3d.jpe

image

对于给定的某个结点的下标 i,可以很容易的计算出这个结点的父结点、孩子结点的下标:

  • Parent(i) = floor(i/2),i 的父节点下标
  • Left(i) = 2i,i 的左子节点下标
  • Right(i) = 2i + 1,i 的右子节点下标

二叉堆一般分为两种:最大堆和最小堆。
最大堆:
最大堆中的最大元素值出现在根结点(堆顶)
堆中每个父节点的元素值都大于等于其孩子结点(如果存在)

img_9190216ef9dffcada41c116c13ae6581.jpe

image

最小堆:
最小堆中的最小元素值出现在根结点(堆顶)
堆中每个父节点的元素值都小于等于其孩子结点(如果存在)

img_b80ed2ae5af5b32a80852bdbe34b403f.jpe

image

堆排序原理

堆排序就是把最大堆堆顶的最大数取出,将剩余的堆继续调整为最大堆,再次将堆顶的最大数取出,这个过程持续到剩余数只有一个时结束。在堆中定义以下几种操作:

  • 最大堆调整(Max-Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
  • 创建最大堆(Build-Max-Heap):将堆所有数据重新排序,使其成为最大堆
  • 堆排序(Heap-Sort):移除位在第一个数据的根节点,并做最大堆调整的递归运算
    继续进行下面的讨论前,需要注意的一个问题是:数组都是 Zero-Based,这就意味着我们的堆数据结构模型要发生改变

img_51f44b1483c84530130c4a0150bd42e6.jpe

image

相应的,几个计算公式也要作出相应调整:

  • Parent(i) = floor((i-1)/2),i 的父节点下标
  • Left(i) = 2i + 1,i 的左子节点下标
  • Right(i) = 2(i + 1),i 的右子节点下标
堆的建立和维护

堆可以支持多种操作,但现在我们关心的只有两个问题:

  1. 给定一个无序数组,如何建立为堆?
  2. 删除堆顶元素后,如何调整数组成为新堆?

先看第二个问题。假定我们已经有一个现成的大根堆。现在我们删除了根元素,但并没有移动别的元素。想想发生了什么:根元素空了,但其它元素还保持着堆的性质。我们可以把最后一个元素(代号A)移动到根元素的位置。如果不是特殊情况,则堆的性质被破坏。但这仅仅是由于A小于其某个子元素。于是,我们可以把A和这个子元素调换位置。如果A大于其所有子元素,则堆调整好了;否则,重复上述过程,A元素在树形结构中不断“下沉”,直到合适的位置,数组重新恢复堆的性质。上述过程一般称为“筛选”,方向显然是自上而下。

删除后的调整,是把最后一个元素放到堆顶,自上而下比较

删除一个元素是如此,插入一个新元素也是如此。不同的是,我们把新元素放在末尾,然后和其父节点做比较,即自下而上筛选。

插入是把新元素放在末尾,自下而上比较

那么,第一个问题怎么解决呢?

常规方法是从第一个非叶子结点向下筛选,直到根元素筛选完毕。这个方法叫“筛选法”,需要循环筛选n/2个元素。

但我们还可以借鉴“插入排序”的思路。我们可以视第一个元素为一个堆,然后不断向其中添加新元素。这个方法叫做“插入法”,需要循环插入(n-1)个元素。

由于筛选法和插入法的方式不同,所以,相同的数据,它们建立的堆一般不同。大致了解堆之后,堆排序就是水到渠成的事情了。

动图演示

img_c66a7e83189427b6a5a5c378f73c17ca.gif

image

算法描述

我们需要一个升序的序列,怎么办呢?我们可以建立一个最小堆,然后每次输出根元素。但是,这个方法需要额外的空间(否则将造成大量的元素移动,其复杂度会飙升到O(n^2) )。如果我们需要就地排序(即不允许有O(n)空间复杂度),怎么办?

有办法。我们可以建立最大堆,然后我们倒着输出,在最后一个位置输出最大值,次末位置输出次大值……由于每次输出的最大元素会腾出第一个空间,因此,我们恰好可以放置这样的元素而不需要额外空间。很漂亮的想法,是不是?

算法实现
public class ArrayHeap {
    private int[] arr;
    public ArrayHeap(int[] arr) {
        this.arr = arr;
    }
    private int getParentIndex(int child) {
        return (child - 1) / 2;
    }
    private int getLeftChildIndex(int parent) {
        return 2 * parent + 1;
    }
    private void swap(int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
    
    private void adjustHeap(int i, int len) {
        int left, right, j;
        left = getLeftChildIndex(i);
        while (left <= len) {
            right = left + 1;
            j = left;
            if (j < len && arr[left] < arr[right]) {
                j++;
            }
            if (arr[i] < arr[j]) {
                swap(array, i, j);
                i = j;
                left = getLeftChildIndex(i);
            } else {
                break; 
            }
        }
    }
    
    public void sort() {
        int last = arr.length - 1;
        
        for (int i = getParentIndex(last); i >= 0; --i) {
            adjustHeap(i, last);
        }
        
        while (last >= 0) {
            swap(0, last--);
            adjustHeap(0, last);
        }
    }

}
稳定性

堆排序存在大量的筛选和移动过程,属于不稳定的排序算法。

适用场景

堆排序在建立堆和调整堆的过程中会产生比较大的开销,在元素少的时候并不适用。但是,在元素比较多的情况下,还是不错的一个选择。尤其是在解决诸如“前n大的数”一类问题时,几乎是首选算法。

7. 希尔排序(插入排序的改良版)外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在希尔排序出现之前,计算机界普遍存在“排序算法不可能突破O(n2)”的观点。希尔排序是第一个突破O(n2)的排序算法,它是简单插入排序的改进版。希尔排序的提出,主要基于以下两点:

  1. 插入排序算法在数组基本有序的情况下,可以近似达到O(n)复杂度,效率极高。
  2. 但插入排序每次只能将数据移动一位,在数组较大且基本无序的情况下性能会迅速恶化。
算法描述

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

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

img_f14e4169ff39bad42c3dd6c385ad9c72.gif

image

算法实现

Donald Shell增量

public static void shellSort(int[] arr){
    int temp;
    for (int delta = arr.length/2; delta>=1; delta/=2){                              
        for (int i=delta; i<arr.length; i++){              
            for (int j=i; j>=delta && arr[j]<arr[j-delta]; j-=delta){ 
                temp = arr[j-delta];
                arr[j-delta] = arr[j];
                arr[j] = temp;
            }
        }
    }
}

O(n^(3/2)) by Knuth

public static void shellSort2(int[] arr){
    int delta = 1;
    while (delta < arr.length/3){
        delta=delta*3+1;    
    }         
    int temp;
    for (; delta>=1; delta/=3){
        for (int i=delta; i<arr.length; i++){              
            for (int j=i; j>=delta && arr[j]<arr[j-delta]; j-=delta){
                temp = arr[j-delta];
                arr[j-delta] = arr[j];
                arr[j] = temp;
            }
        }
    }
}
希尔排序的增量

希尔排序的增量数列可以任取,需要的唯一条件是最后一个一定为1(因为要保证按1有序)。但是,不同的数列选取会对算法的性能造成极大的影响。上面的代码演示了两种增量。
切记:增量序列中每两个元素最好不要出现1以外的公因子!(很显然,按4有序的数列再去按2排序意义并不大)。
下面是一些常见的增量序列。

  • 第一种增量是最初Donald Shell提出的增量,即折半降低直到1。据研究,使用希尔增量,其时间复杂度还是O(n2)。

第二种增量Hibbard:{1, 3, …, 2k-1}。该增量序列的时间复杂度大约是O(n1.5)。

第三种增量Sedgewick增量:(1, 5, 19, 41, 109,…),其生成序列或者是94^i - 92^i + 1或者是4^i - 3*2^i + 1。

稳定性

我们都知道插入排序是稳定算法。但是,Shell排序是一个多次插入的过程。在一次插入中我们能确保不移动相同元素的顺序,但在多次的插入中,相同元素完全有可能在不同的插入轮次被移动,最后稳定性被破坏,因此,Shell排序不是一个稳定的算法。

适用场景

Shell排序虽然快,但是毕竟是插入排序,其数量级并没有后起之秀–快速排序O(n㏒n)快。在大量数据面前,Shell排序不是一个好的算法。但是,中小型规模的数据完全可以使用它。

计数排序外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

算法描述
  1. 找出待排序的数组中最大和最小的元素;
  2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
  3. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
  4. 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
动图演示

img_3c7ddb59df2d21b287e42a7b908409cb.gif

image

算法实现
public static void countSort(int[] a, int max, int min) {
     int[] b = new int[a.length];
     int[] count = new int[max - min + 1];

     for (int num = min; num <= max; num++) {
        
        count[num - min] = 0;
     }

     for (int i = 0; i < a.length; i++) {
        int num = a[i];
        count[num - min]++;
     }

     for (int num = min + 1; num <= max; num++) {
        
        count[num - min] += sum[num - min - 1]
     }

     for (int i = 0; i < a.length; i++) {
          int num = a[i];
          int index = count[num - min] - 1;
          b[index] = num;
          count[num - min]--;
     }

     
     for(int i=0;i<a.length;i++){
         a[i] = b[i];
     }
}
稳定性

最后给 b 数组赋值是倒着遍历的,而且放进去一个就将C数组对应的值(表示前面有多少元素小于或等于A[i])减去一。如果有相同的数x1,x2,那么相对位置后面那个元素x2放在(比如下标为4的位置),相对位置前面那个元素x1下次进循环就会被放在x2前面的位置3。从而保证了稳定性。

适用场景

排序目标要能够映射到整数域,其最大值最小值应当容易辨别。例如高中生考试的总分数,显然用0-750就OK啦;又比如一群人的年龄,用个0-150应该就可以了,再不济就用0-200喽。另外,计数排序需要占用大量空间,它比较适用于数据比较集中的情况。

桶排序

桶排序又叫箱排序,是计数排序的升级版,它的工作原理是将数组分到有限数量的桶子里,然后对每个桶子再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后将各个桶中的数据有序的合并起来。

计数排序是桶排序的一种特殊情况,可以把计数排序当成每个桶里只有一个元素的情况。网络中很多博文写的桶排序实际上都是计数排序,并非标准的桶排序,要注意辨别。

算法描述
  1. 找出待排序数组中的最大值max、最小值min
  2. 我们使用 动态数组ArrayList 作为桶,桶里放的元素也用 ArrayList 存储。桶的数量为(max-min)/arr.length+1
  3. 遍历数组 arr,计算每个元素 arr[i] 放的桶
  4. 每个桶各自排序
  5. 遍历桶数组,把排序好的元素放进输出数组
图片演示

img_ee2ed2ea8e4428888e08a8e34c6cdf45.jpe

image

算法实现
public static void bucketSort(int[] arr){
    int max = Integer.MIN_VALUE;
    int min = Integer.MAX_VALUE;
    for(int i = 0; i < arr.length; i++){
        max = Math.max(max, arr[i]);
        min = Math.min(min, arr[i]);
    }
    
    int bucketNum = (max - min) / arr.length + 1;
    ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
    for(int i = 0; i < bucketNum; i++){
        bucketArr.add(new ArrayList<Integer>());
    }
    
    for(int i = 0; i < arr.length; i++){
        int num = (arr[i] - min) / (arr.length);
        bucketArr.get(num).add(arr[i]);
    }
    
    for(int i = 0; i < bucketArr.size(); i++){
        Collections.sort(bucketArr.get(i));
    }
    System.out.println(bucketArr.toString());
}
稳定性

可以看出,在分桶和从桶依次输出的过程是稳定的。但是,由于我们在对每个桶进行排序时使用了其他算法,所以,桶排序的稳定性依赖于这一步。如果我们使用了快排,显然,算法是不稳定的。

适用场景

桶排序可用于最大最小值相差较大的数据情况,但桶排序要求数据的分布必须均匀,否则可能导致数据都集中到一个桶中。比如[104,150,123,132,20000], 这种数据会导致前4个数都集中到同一个桶中。导致桶排序失效。

基数排序

基数排序(Radix Sort)是桶排序的扩展,它的基本思想是:将整数按位数切割成不同的数字,然后按每个位数分别比较。
排序过程:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

算法描述
  1. 取得数组中的最大数,并取得位数;
  2. arr为原始数组,从最低位开始取每个位组成radix数组;
  3. 对radix进行计数排序(利用计数排序适用于小范围数的特点);
动图

img_3a6f1e5059386523ed941f0d6c3a136e.gif

image

算法实现
public abstract class Sorter {
     public abstract void sort(int[] array);
}
 
public class RadixSorter extends Sorter {
     
     private int radix;
     
     public RadixSorter() {
          radix = 10;
     }
     
     @Override
     public void sort(int[] array) {
          
          
          int[][] bucket = new int[radix][array.length];
          int distance = getDistance(array); 
          int temp = 1;
          int round = 1; 
          while (round <= distance) {
               
               int[] counter = new int[radix];
               
               for (int i = 0; i < array.length; i++) {
                    int which = (array[i] / temp) % radix;
                    bucket[which][counter[which]] = array[i];
                    counter[which]++;
               }
               int index = 0;
               
               for (int i = 0; i < radix; i++) {
                    if (counter[i] != 0)
                         for (int j = 0; j < counter[i]; j++) {
                              array[index] = bucket[i][j];
                              index++;
                         }
                    counter[i] = 0;
               }
               temp *= radix;
               round++;
          }
     }
     
     private int getDistance(int[] array) {
          int max = computeMax(array);
          int digits = 0;
          int temp = max / radix;
          while(temp != 0) {
               digits++;
               temp = temp / radix;
          }
          return digits + 1;
     }
     
     private int computeMax(int[] array) {
          int max = array[0];
          for(int i=1; i<array.length; i++) {
               if(array[i]>max) {
                    max = array[i];
               }
          }
          return max;
     }
}
稳定性

通过上面的排序过程,我们可以看到,每一轮映射和收集操作,都保持从左到右的顺序进行,如果出现相同的元素,则保持他们在原始数组中的顺序。可见,基数排序是一种稳定的排序。

适用场景

基数排序要求较高,元素必须是整数,整数时长度10W以上,最大值100W以下效率较好,但是基数排序比其他排序好在可以适用字符串,或者其他需要根据多个条件进行排序的场景,例如日期,先排序日,再排序月,最后排序年 ,其它排序算法可是做不了的。

总结

img_446032f3223d0307a09207e351dafd2e.jpe

写在最后

如果这篇【文章】有帮助到你,希望可以给【JavaGPT】点个赞👍,创作不易,如果有对【后端技术】、【前端领域】感兴趣的小可爱,也欢迎关注❤️❤️❤️ 【JavaGPT】❤️❤️❤️,我将会给你带来巨大的【收获与惊喜】💝💝💝!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/247754.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

代码随想录算法训练营 | day48 动态规划 198.打家劫舍,213.打家劫舍Ⅱ,337.打家劫舍Ⅲ

刷题 198.打家劫舍 题目链接 | 文章讲解 | 视频讲解 题目&#xff1a;你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的房屋在同一晚上被…

c YUV 转 JPEG(准备霍夫曼编码)

先取yuv 文件中一个168的块&#xff0c;跑通全流程 理解与思路&#xff1a; 1.块分割 YUV 文件分为&#xff1a;YUV444 YUV 422 YUV420。444:就是&#xff1a;12个char 有4个Y&#xff0c;4个U&#xff0c;4个 U&#xff0c;422&#xff1a;8个char 中有4个Y &#x…

Oracle MongoDB

听课的时候第一次碰到&#xff0c;可以了解一下吧&#xff0c;就直接开了墨者学院的靶场 #oracle数据库 Oracle数据库注入全方位利用 - 先知社区 这篇写的真的很好 1.判断注入点 当时找了半天没找到 看样子是找到了&#xff0c;测试一下看看 id1 and 11 时没有报错 2.判断字段…

开发人员必须掌握的几个高级命令

xargs命令 在平时的使用中,我认为 xargs 这个命令还是较为重要和方便的。我们可以通过使用这个命令,将命令输出的结果作为参数传递给另一个命令。 比如说我们想找出某个路径下以 .conf 结尾的文件,并将这些文件进行分类,那么普通的做法就是先将以 .conf 结尾的文件先找出…

linux(centos7)mysql8.0主从集群搭建(两台机器)

docker安装:&#xff08;转载&#xff09;centos7安装Docker详细步骤&#xff08;无坑版教程&#xff09;-CSDN博客 环境信息 主数据库服务器&#xff1a;192.168.1.10 从数据库服务器&#xff1a;192.168.1.11 1. mysql8.0镜像下载 docker pull mysql:8.0.23 2.创建docke…

SaaS 电商设计 (五) 私有化部署-实现 binlog 中间件适配

一、 背景 具体的中间件私有化背景在上文 SaaS 电商设计 (二) 私有化部署-缓存中间件适配 已有做相关介绍.这里具体讨论的场景是通过解析mysql binlog 来实现mysql到其他数据源的同步.具体比如:在电商的解决方案业务流中经常有 ES 的使用场景,用以解决一些复杂的查询和搜索商品…

17--异常处理

1、异常概述 1.1 什么是异常 异常&#xff1a;指的是程序在执行过程中&#xff0c;出现的非正常情况&#xff0c;如果不处理最终会导致JVM的非正常停止。 异常指的并不是语法错误和逻辑错误。语法错了&#xff0c;编译不通过&#xff0c;不会产生字节码文件&#xff0c;根本运…

01.前言

前言 1.什么是前端开发 前端开发是创建 Web 页面或 app 等前端界面呈现给用户的过程核心技术&#xff1a;HTML&#xff0c;CSS&#xff0c;JavaScript 以及衍生出的各种技术&#xff0c;框架等 2.前端开发应用场景 3.前端职业路线 4.什么是CS架构与BS架构 介绍 应用软件&a…

SuperMap iClient3D for Cesium 实现鼠标移动选中模型并显示模型对应字段

SuperMap iClient3D for cesium 实现鼠标移动选中模型并显示模型对应字段 一、实现思路二、数据制作1. 计算出模型中心点并保存到属性表中2. 计算出模型顶部高程3. 模型数据切缓存4. 发布三维服务. 三、代码编写 作者&#xff1a;xkf 一、实现思路 将模型属性数据存储到前端&a…

c++面经总结

C基础语法 C和c的区别 c中new和delete是对内存分配的运算符&#xff0c;取代了c中的malloc和free 标准c中的字符串类取代了标准c函数库头文件中的字符数组处理函数(c中没有字符串类型). 在c中&#xff0c;允许有相同的函数名&#xff0c;不过他们的参数类型不能完全相同&…

JAVA:深入探讨Java 8 Stream的强大功能与用法

1、简述 Java 8引入了Stream API&#xff0c;为处理集合数据提供了一种更为强大和灵活的方式。Stream是一种抽象的数据结构&#xff0c;它允许你以一种声明性的方式处理数据集合。与传统的集合操作不同&#xff0c;Stream并不是一个存储数据的数据结构&#xff0c;而是在源数据…

Leetcode—78.子集【中等】

2023每日刷题&#xff08;五十九&#xff09; Leetcode—78.子集 算法思想 实现代码 class Solution { public:vector<vector<int>> subsets(vector<int>& nums) {int len nums.size();vector<int> path;vector<vector<int>> ans;f…

20231215给AIO-3399J适配Rockchip的原始Andoroid10的挖掘机开发板01

20231215给AIO-3399J适配Rockchip的原始Andoroid10的挖掘机开发板01 2023/12/15 10:49 【请严重注意&#xff1a;】如果刷不适配的SDK&#xff0c;可能会引起您的开发板【硬件发生物理】损坏&#xff01; 如果您按照本步骤刷机引起的一切后果&#xff0c;请自行承担责任&#x…

状态的一致性和FlinkSQL

状态一致性 一致性其实就是结果的正确性。精确一次是指数据有可能被处理多次&#xff0c;但是结果只有一个。 三个级别&#xff1a; 最多一次&#xff1a;1次或0次&#xff0c;有可能丢数据至少一次&#xff1a;1次或n次&#xff0c;出错可能会重试 输入端只要可以做到数据重…

vue3 添加编辑页使用 cron 表达式生成

示例效果图 1、添加组件 <template><div class"v3c"><ul class"v3c-tab"><li class"v3c-tab-item" :class"{ v3c-active: tabActive 1 }" click"onHandleTab(1)">秒</li><li class&qu…

【Linux系统编程二十一】:(进程通信3)--消息队列/信号量(system v标准的内核数据结构的设计模式)

【Linux系统编程二十】&#xff1a;消息队列/信号量(system v标准的内核数据结构的设计模式&#xff09; 一.消息队列二.system v标准的内核数据结构的设计三.四个概念(互斥/临界)四.信号量1.多线程并发访问2.计数器3.原子的4.总结 一.消息队列 一个叫做a进程啊&#xff0c;一个…

乐益达教育网页

目录 一、网页效果 二、html代码 三、CSS代码 四、JS代码 一、网页效果 二、html代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, in…

Milesight VPN server.js 任意文件读取漏洞(CVE-2023-23907)

0x01 产品简介 MilesightVPN 是一款软件&#xff0c;一个 Milesight 产品的 VPN 通道设置过程更加完善&#xff0c;并可通过网络服务器界面连接状态。 0x02 漏洞概述 MilesightVPN server.js接口处存在文件读取漏洞&#xff0c;攻击者可通过该漏洞读取系统重要文件&#xff…

LeetCode-42. 接雨水【栈 数组 双指针 动态规划 单调栈】

LeetCode-42. 接雨水【栈 数组 双指针 动态规划 单调栈】 题目描述&#xff1a;解题思路一&#xff1a;单调栈&#xff0c;维护一个单调递减栈。每当遇到当前元素大于栈顶元素就出栈&#xff0c;在出栈时更新答案。当遇到出栈的情况&#xff0c;若单调栈栈左边有一个元素则必有…

Java医院信息化建设云HIS系统源码

云HIS提供标准化、信息化、可共享的医疗信息管理系统&#xff0c;实现医患事务管理和临床诊疗管理等标准医疗管理信息系统的功能。优化就医、管理流程&#xff0c;提升患者满意度、基层首诊率&#xff0c;通过信息共享、辅助诊疗等手段&#xff0c;提高基层医生的服务能力构建和…