当前位置:主页 > 查看内容

面试必会算法(1):排序算法

发布时间:2021-07-30 00:00| 位朋友查看

简介:面试必会算法1排序算法 面试必会算法系列之排序算法 前文推荐 【版权申明】未经博主同意谢绝转载请尊重原创博主保留追究权 本博客的内容来自于 面试必会算法1排序算法 学习、合作与交流联系q384660495 本博客的内容仅供学习与参考并非营利 文章目录 面试必会……

面试必会算法(1):排序算法

面试必会算法系列之排序算法
前文推荐:


【版权申明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权);
本博客的内容来自于;面试必会算法(1):排序算法
学习、合作与交流联系q384660495;
本博客的内容仅供学习与参考,并非营利;


一、排序算法综述

1、概念

将杂乱无章的数据元素,通过一定的方法按关键字顺序排列的过程叫做排序。

2、分类

十种常见排序算法可以分为两大类:

非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。

线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。

算法分类

3、算法比较

时间复杂度与空间复杂度比较

稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。

算法对比

n:数据规模
k:"桶"的个数
In-place:占用常数内存,不占用额外内存
Out-place:占用额外内存
稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同

  1. 平方阶 (O(n2)) 排序 各类简单排序:直接插入、直接选择和冒泡排序。
  2. 线性对数阶 (O(nlog2n)) 排序 快速排序、堆排序和归并排序;
  3. O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数。 希尔排序
  4. 线性阶 (O(n)) 排序 基数排序,此外还有桶、箱排序。

内部排序:若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序。
外部排序:若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。
就地排序:若排序算法所需的辅助空间并不依赖于问题的规模n,即辅助空间为O(1),称为就地排序。
稳定排序: 假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序后,这些记录的相对次序保持不变,即在原序列中 ri=rj, ri 在rj 之前,而在排序后的序列中,ri 仍在 rj 之前,则称这种排序算法是稳定的;否则称为不稳定的

二、冒泡排序

1、基本思想

基本思想: 冒泡排序,类似于水中冒泡,较大的数沉下去,较小的数慢慢冒起来,假设从小到大,即为较大的数慢慢往后排,较小的数慢慢往前排。

直观表达,每一趟遍历,将一个最大的数移到序列末尾。

2、算法描述

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

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

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

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

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

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

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

冒泡排序
(1)要排序数组:[10,1,35,61,89,36,55]

(2)第一趟排序:

第一次排序:10和1比较,10大于1,交换位置      [1,10,35,61,89,36,55]

第二趟排序:10和35比较,10小于35,不交换位置  [1,10,35,61,89,36,55]

第三趟排序:35和61比较,35小于61,不交换位置  [1,10,35,61,89,36,55]

第四趟排序:61和89比较,61小于89,不交换位置  [1,10,35,61,89,36,55]

第五趟排序:89和36比较,89大于36,交换位置   [1,10,35,61,36,89,55]

第六趟排序:89和55比较,89大于55,交换位置   [1,10,35,61,36,55,89]

第一趟总共进行了六次比较,排序结果:[1,10,35,61,36,55,89]
      
(3)第二趟排序:

第一次排序:1和10比较,1小于10,不交换位置  1,10,35,61,36,55,89

第二次排序:10和35比较,10小于35,不交换位置 1,10,35,61,36,55,89

第三次排序:35和61比较,35小于61,不交换位置 1,10,35,61,36,55,89

第四次排序:61和36比较,61大于36,交换位置   1,10,35,36,61,55,89

第五次排序:61和55比较,61大于55,交换位置   1,10,35,36,55,61,89

第二趟总共进行了5次比较,排序结果:1,10,35,36,55,61,89

(4)第三趟排序:

1和10比较,1小于10,不交换位置  1,10,35,36,55,61,89

第二次排序:10和35比较,10小于35,不交换位置 1,10,35,36,55,61,89

第三次排序:35和36比较,35小于36,不交换位置 1,10,35,36,55,61,89

第四次排序:36和61比较,36小于61,不交换位置   1,10,35,36,55,61,89

第三趟总共进行了4次比较,排序结果:1,10,35,36,55,61,89

到目前位置已经为有序的情形了

3、动图演示

冒泡排序动图演示

4、算法分析

(1)冒泡排序的优点:每进行一趟排序,就会少比较一次,因为每进行一趟排序都会找出一个较大值。如上例:第一趟比较之后,排在最后的一个数一定是最大的一个数,第二趟排序的时候,只需要比较除了最后一个数以外的其他的数,同样也能找出一个最大的数排在参与第二趟比较的数后面,第三趟比较的时候,只需要比较除了最后两个数以外的其他的数,以此类推……也就是说,没进行一趟比较,每一趟少比较一次,一定程度上减少了算法的量。

(3)时间复杂度

1.如果我们的数据正序,只需要走一趟即可完成排序。所需的比较次数C和记录移动次数M均达到最小值,即:Cmin=n-1;Mmin=0;所以,冒泡排序最好的时间复杂度为O(n)。

2.如果很不幸我们的数据是反序的,则需要进行n-1趟排序。每趟排序要进行n-i次比较(1≤i≤n-1),且每次比较都必须移动位置。在这种情况下,比较和移动次数均达到最大值:

4、代码实现

public static void main(String[] args) {
        int[] array = new int[]{1,2,5,2,1,2,3,4,5,1};
        array = bubbleSort(array);
        System.out.println("Arrays.toString(array) = " + Arrays.toString(array));
    }

    public static int[] bubbleSort(int[] array){
    	//一定要记住判断边界条件,很多人不注意这些细节,面试官看到你的代码的时候都懒得往下看。
        if(arr == null || arr.length < 2){
            return array;
        }
        for(int i = 0; i < array.length; i++){
            for(int j = 0; j < array.length - 1 - i; j++){//这边减1是防止数组越界
                if(array[j] > array[j+1]){
                    int temp = array[j+1];
                    array[j+1] = array[j];
                    array[j] = temp;
                }
            }
        }
        return array;
    }

还可以对冒泡排序进行优化,详细可参考冒泡排序的三种优化冒泡排序的三种优化

public static int[] bubbleSort(int[] array){
        //一定要记住判断边界条件,很多人不注意这些细节,面试官看到你的代码的时候都懒得往下看,你的代码哪个项目敢往里面加?
        if(array == null || array.length < 2){
            return array;
        }
        for(int i = 0; i < array.length-1; i++){
            int flag = 1;
            for(int j = 0; j < array.length - 1 - i; j++){
                if(array[j] > array[j+1]){
                    int temp = array[j+1];
                    array[j+1] = array[j];
                    array[j] = temp;
                    flag =0;
                }
            }
            if(flag == 1){
                return array;
            }
        }
        return array;
    }

三、选择排序

1、基本思想

选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

2、算法描述

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

初始状态:无序区为R[1…n],有序区为空;

第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1…i-1]和R(i…n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1…i]和R[i+1…n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;

n-1趟结束,数组有序化了

3、动图演示

在这里插入图片描述

4、算法分析

  • 复杂度

时间复杂度:O(n^2)

空间复杂度:O(1),在原数组空间内操作,需要的额外空间是指针空间

  • 稳定性

不稳定:

如果待排序列中有两个相同的数a、b,最右边的数b的右边所有的数都比ab大,则不会出现不稳定情况,如[3, 2-a, 2-b, 4, 5],第一次交换后[2-a, 3, 2-b, 4, 5],第二次交换后[2-a, 2-b, 3, 4, 5],最终结果a和b保持原来的相对顺序。

如果待排序列中的两个相同数a、b,最右边的数b的右边的所有数中有比ab小的数,在一次交换中可能会将a交换到b的右边。比如[3, 5-a, 7, 5-b, 2],第二次交换后[2, 3, 7, 5-b, 5-a],最终结果[2, 3, 5-b, 5-a, 7],a和b不能保持原来的相对顺序。

5、代码实现

 public static int[] selectionSort(int[] array){
        if(array == null || array.length<2){
            return array;
        }
        for(int i = 0; i < array.length-1; i++){
            int index = i;
            for(int j = i+1; j<array.length; j++){
                if(array[j] < array[i]){
                    index = j;
                }
            }
            if(index != i) {
                int temp = array[i];
                array[i] = array[index];
                array[index] = temp;
            }
        }
        return array;
    }

还可以对选择排序进行优化,即每次查找不仅找出最小值,还找出最大值,分别插到前面和后面,可以减少一半的查询时间选择排序优化选择排序优化

四、插入排序

1、基本思想

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

2、算法描述

从第一个元素开始,该元素可以认为已经被排序;

取出下一个元素,在已经排序的元素序列中从后向前扫描;

如果该元素(已排序)大于新元素,将该元素移到下一位置;

重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;

将新元素插入到该位置后;

重复步骤2~5。
插入排序
过程:

初始状态:设5为有序,其中i为1,即:5 2 0 6 9

第一趟排序:第i个元素2比5小,则插入到5前面,然后i自增,即 : 2 5 0 6 9

第二趟排序:第i个元素0比2,5小,则插入到2前面,然后i自增,即:0 2 5 6 9

第三趟排序:第i个元素6比5大,则插入到5后面,然后i自增,即:0 2 5 6 9

第四趟排序:第i个元素9比6大,则插入到6后面,然后i自增,即:0 2 5 6 9

最终的答案为:0 2 5 6 9

3、动图演示

插入排序

4、算法分析

1.在最好情况下,严格递增的数组,比较次数C和移动次数M为:

C = n - 1
M = 0
时间复杂度为O(n)。

2.在最坏情况下,严格递减的数组,比较次数C和移动次数M为:

C = n(n-1)/2
M = n(n-1)/2
时间复杂度为O(n2)。

综上,时间复杂度为:O(n2) 。

优缺点:

优点 : 稳定,相对于冒泡排序与选择排序更快;
缺点 : 比较次数不一定,比较次数越少,插入点后的数据移动越多,特别是当数据总量大的时候;

5、代码实现

public static int[] insertionSort(int[] array) {
        if (array == null || array.length < 2) {
            return array;
        }
        for (int i = 1; i < array.length; i++) {
            int current = array[i];
            int preIndex = i - 1;
            for (; preIndex >= 0; preIndex--) {
                if (current < array[preIndex]) {
                    array[preIndex + 1] = array[preIndex];
                }else{
                    break;
                }
            }
            array[preIndex + 1] = current;
        }
        return array;
    }

插入排序的优化方式可以参考插入排序优化方法

有兴趣的朋友可以了解一下为什么插入排序比冒泡排序更欢迎

五、希尔排序

1、基本思想

希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

2、算法描述

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

选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;

按增量序列个数k,对序列进行k 趟排序;

每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。在这里插入图片描述

3、动图演示

在这里插入图片描述

4、算法分析

希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。

while (gap < len / 3) {          // 动态定义间隔序列
        gap = gap * 3 + 1;
    }


5、代码实现

 public static int[] shellSort(int[] array){
        if(array == null || array.length < 2){
            return array;
        }
        int temp;
        int gap = array.length/2;
        while(gap > 0){
            for(int i = gap; i < array.length; i++){
                temp = array[i];
                int preIndex = i - gap;
                while(preIndex >= 0 && array[preIndex] > temp){
                    array[preIndex + gap] = array[preIndex];
                    preIndex -= gap;
                }
                array[preIndex + gap] = temp;
            }
            gap /= 2;
        }
        return array;
    }

希尔排序的优化方法可以参考希尔排序优化方法

六、归并排序

1、基本思想

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

2、算法描述

把长度为n的输入序列分成两个长度为n/2的子序列;

对这两个子序列分别采用归并排序;

将两个排序好的子序列合并成一个最终的排序序列
在这里插入图片描述
上图中首先把一个未排序的序列从中间分割成2部分,再把2部分分成4部分,依次分割下去,直到分割成一个一个的数据,再把这些数据两两归并到一起,使之有序,不停的归并,最后成为一个排好序的序列。

总的来说就是,将数组分成二组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的二个小组就可以了。这样通过先递归的分解数列,再合并数列就完成了归并排序。

3、动图演示

在这里插入图片描述

4、算法分析

归并排序的时间复杂度,在最坏,最好和平均都是O(nlogn),这是效率,性能非常好的排序算法。

只不过它需要占用 O(n)的内存空间,如果数据量一旦很大,内存可能吃不消,这是它的弱点和致命伤。而其他排序算法,比如快速排序,希尔排序,都是就地排序算法,它们不占用额外的内存空间。

5、代码实现

   public static void sort(int[] unsorted, int start, int end, int[] sorted){
        if(start < end){
            int middle = (start + end)/2;
            sort(unsorted, start, middle, sorted);
            sort(unsorted, middle+1, end, sorted);
            merge(unsorted, start, middle, end, sorted);
            System.out.println("区间["+start+","+end+"]:"+Arrays.toString(sorted));
        }
    }

    private static void merge(int[] unsorted, int start, int middle, int end, int[] sorted) {
        int num1 = start;
        int num2 = middle + 1;
        int k = start;
        while(num1 <= middle && num2 <= end){
            if(unsorted[num1] < unsorted[num2]){
                sorted[k++] = unsorted[num1++];
            }else{
                sorted[k++] = unsorted[num2++];
            }
        }
        while(num1 <= middle){
            sorted[k++] = unsorted[num1++];
        }
        while(num2 <= end){
            sorted[k++] = unsorted[num2++];
        }
        while (start <= end){
            unsorted[start] = sorted[start];
            start++;
        }

    }


归并排序的优化可以参考归并排序优化
归并排序优化大白话归并排序讲解

七、快速排序

1、基本思想

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

2、算法描述

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

从数列中挑出一个元素,称为 “基准”(pivot);

重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;

递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

3、动图演示

在这里插入图片描述

4、算法分析

快速排序的时间复杂度在最坏的情况下是O(N2),因为和冒泡排序一样,需要两两交换。平均情况下,快速排序的时间复杂度是O(NlogN)

5、代码实现

     private static int partition(int[] arr, int low, int high) {
                //指定左指针i和右指针j
                int i = low;
                int j= high;
                
                //将第一个数作为基准值。挖坑
                int x = arr[low];
                
                //使用循环实现分区操作
                while(i<j){//5  8
                        //1.从右向左移动j,找到第一个小于基准值的值 arr[j]
                        while(arr[j]>=x && i<j){
                                j--;
                        }
                        //2.将右侧找到小于基准数的值加入到左边的(坑)位置, 左指针想中间移动一个位置i++
                        if(i<j){
                                arr[i] = arr[j];
                                i++;
                        }
                        //3.从左向右移动i,找到第一个大于等于基准值的值 arr[i]
                        while(arr[i]<x && i<j){
                                i++;
                        }
                        //4.将左侧找到的打印等于基准值的值加入到右边的坑中,右指针向中间移动一个位置 j--
                        if(i<j){
                                arr[j] = arr[i];
                                j--;
                        }
                }
                
                //使用基准值填坑,这就是基准值的最终位置
                arr[i] = x;//arr[j] = y;
                //返回基准值的位置索引
                return i; //return j;
        }
        private static void quickSort(int[] arr, int low, int high) {//???递归何时结束
                if(low < high){
                        //分区操作,将一个数组分成两个分区,返回分区界限索引
                        int index = partition(arr,low,high);
                        //对左分区进行快排
                        quickSort(arr,low,index-1);
                        //对右分区进行快排
                        quickSort(arr,index+1,high);
                }
        
        }

快速排序的优化方法可以参考快速排序优化方法快速排序的多种实现方式
关于快速排序和归并排序的时间空间复杂度分析可以参考这一篇文章:排序(下):如何用快排思想在O(n)内查找第K个大元素?

八、堆排序

1、基本思想

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
在这里插入图片描述

2、算法描述

堆排序的基本思路:

a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;

b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;

c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
  
详细步骤可参考图文解释堆排序,写的非常详细。

3、动图演示

在这里插入图片描述

4、算法分析

堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)…1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)级。

关于堆排序和快速排序的比较可以参考这一篇文章:堆排序缺点何在

5、代码实现

void heapSort(int array[], int n)
{
    int i;
    for (i=n/2;i>0;i--)
    {
        HeapAdjust(array,i,n);//从下向上,从右向左调整
    }
    for( i=n;i>1;i--)
    {
        swap(array, 1, i);
        HeapAdjust(array, 1, i-1);//从上到下,从左向右调整
    }
}
void HeapAdjust(int array[], int s, int n )
{
    int i,temp;
    temp = array[s];
    for(i=2*s;i<=n;i*=2)
    {
        if(i<n&&array[i]<array[i+1])
        {
            i++;
        }
        if(temp>=array[i])
        {
            break;
        }
        array[s]=array[i];
        s=i;
    }
    array[s]=temp;
}
void swap(int array[], int i, int j)
{
    int temp;

    temp=array[i];
    array[i]=array[j];
    array[j]=temp;
}

九、计数排序

1、基本思想

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

2、算法描述

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

算法步骤为:

第一步:找出原数组中元素值最大的,记为max。

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

第三步:遍历原数组中的元素,以原数组中的元素作为count数组的索引,以原数组中的元素出现次数作为count数组的元素值。

第四步:创建结果数组result,起始索引index。

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

第六步:返回结果数组result。

详细步骤可以参考这篇文章:深入浅出计数排序

3、动图演示

基数排序

4、算法分析

计数排序的空间复杂度为O(n+k),其中n为数据规模,k为数据范围。根据这个空间复杂度,我们可以得出以下结论:

在大规模数据的情况下,内存根本无法一口气存下这么大规模的数据,这个时候就要使用归并排序这种天然适合外部排序的算法了。

再者,计数排序只适用于对数据范围比较集中的数据集合进行排序,如果数据的值非常分散,需要额外申请的辅助数组就更不可能存储了。虽然可以通过离散化或者bitmap等方式来进行优化,但会增加算法实现的复杂性。

而且,计数排序只能直接应用于非负整数的排序中,如果需要排序的数据含有负数,或者是其他类型的值,那么,还需要在不改变相对大小的情况下映射成非负整数,使整个排序逻辑变得复杂。

计数排序速度这么快,为什么使用范围这么小?

5、代码实现

/**
     * 计数排序
     *
     * @param array
     * @return
     */
    public static int[] CountingSort(int[] array) {
        if (array.length == 0) return array;
        int bias, min = array[0], max = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i] > max)
                max = array[i];
            if (array[i] < min)
                min = array[i];
        }
        bias = 0 - min;
        int[] bucket = new int[max - min + 1];
        Arrays.fill(bucket, 0);
        for (int i = 0; i < array.length; i++) {
            bucket[array[i] + bias]++;
        }
        int index = 0, i = 0;
        while (index < array.length) {
            if (bucket[i] != 0) {
                array[index] = i - bias;
                bucket[i]--;
                index++;
            } else
                i++;
        }
        return array;
    }

十、桶排序

1、基本思想

桶排序是计数排序的变种,把计数排序中相邻的m个”小桶”放到一个”大桶”中,在分完桶后,对每个桶进行排序(一般用快排),然后合并成最后的结果。

2、算法描述

元素值域的划分,也就是元素到桶的映射规则。映射规则需要根据待排序集合的元素分布特性进行选择,若规则设计的过于模糊、宽泛,则可能导致待排序集合中所有元素全部映射到一个桶上,则桶排序向比较性质排序算法演变。若映射规则设计的过于具体、严苛,则可能导致待排序集合中每一个元素值映射到一个桶上,则桶排序向计数排序方式演化。

排序算法的选择,从待排序集合中元素映射到各个桶上的过程,并不存在元素的比较和交换操作,在对各个桶中元素进行排序时,可以自主选择合适的排序算法,桶排序算法的复杂度和稳定性,都根据选择的排序算法不同而不同。

1、设置一个定量的数组当作空桶;

2、遍历输入数据,并且把数据一个一个放到对应的桶里去;

3、 对每个不是空的桶进行排序;

4、从不是空的桶里把排好序的数据拼接起来。

3、动图演示

4、算法分析

快速排序是将集合拆分为两个值域,这里称为两个桶,再分别对两个桶进行排序,最终完成排序。桶排序则是将集合拆分为多个桶,对每个桶进行排序,则完成排序过程。两者不同之处在于,快排是在集合本身上进行排序,属于原地排序方式,且对每个桶的排序方式也是快排。桶排序则是提供了额外的操作空间,在额外空间上对桶进行排序,避免了构成桶过程的元素比较和交换操作,同时可以自主选择恰当的排序算法对桶进行排序。

当然桶排序更是对计数排序的改进,计数排序申请的额外空间跨度从最小元素值到最大元素值,若待排序集合中元素不是依次递增的,则必然有空间浪费情况。桶排序则是弱化了这种浪费情况,将最小值到最大值之间的每一个位置申请空间,更新为最小值到最大值之间每一个固定区域申请空间,尽量减少了元素值大小不连续情况下的空间浪费情况。

具体可以参考这篇文章排序算法(九):桶排序

5、代码实现

/**
     * 桶排序
     * 
     * @param array
     * @param bucketSize
     * @return
     */
    public static ArrayList<Integer> BucketSort(ArrayList<Integer> array, int bucketSize) {
        if (array == null || array.size() < 2)
            return array;
        int max = array.get(0), min = array.get(0);
        // 找到最大值最小值
        for (int i = 0; i < array.size(); i++) {
            if (array.get(i) > max)
                max = array.get(i);
            if (array.get(i) < min)
                min = array.get(i);
        }
        int bucketCount = (max - min) / bucketSize + 1;
        ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketCount);
        ArrayList<Integer> resultArr = new ArrayList<>();
        for (int i = 0; i < bucketCount; i++) {
            bucketArr.add(new ArrayList<Integer>());
        }
        for (int i = 0; i < array.size(); i++) {
            bucketArr.get((array.get(i) - min) / bucketSize).add(array.get(i));
        }
        for (int i = 0; i < bucketCount; i++) {
            if (bucketSize == 1) { // 如果带排序数组中有重复数字时  感谢 @见风任然是风 朋友指出错误
                for (int j = 0; j < bucketArr.get(i).size(); j++)
                    resultArr.add(bucketArr.get(i).get(j));
            } else {
                if (bucketCount == 1)
                    bucketSize--;
                ArrayList<Integer> temp = BucketSort(bucketArr.get(i), bucketSize);
                for (int j = 0; j < temp.size(); j++)
                    resultArr.add(temp.get(j));
            }
        }
        return resultArr;
    }


十一、基数排序

1、基本思想

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

2、算法描述

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

3、动图演示

在这里插入图片描述

4、算法分析

基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。

这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
基数排序:根据键值的每位数字来分配桶
计数排序:每个桶只存储单一键值
桶排序:每个桶存储一定范围的数值

5、代码实现

/**
     * 基数排序
     * @param array
     * @return
     */
    public static int[] RadixSort(int[] array) {
        if (array == null || array.length < 2)
            return array;
        // 1.先算出最大数的位数;
        int max = array[0];
        for (int i = 1; i < array.length; i++) {
            max = Math.max(max, array[i]);
        }
        int maxDigit = 0;
        while (max != 0) {
            max /= 10;
            maxDigit++;
        }
        int mod = 10, div = 1;
        ArrayList<ArrayList<Integer>> bucketList = new ArrayList<ArrayList<Integer>>();
        for (int i = 0; i < 10; i++)
            bucketList.add(new ArrayList<Integer>());
        for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
            for (int j = 0; j < array.length; j++) {
                int num = (array[j] % mod) / div;
                bucketList.get(num).add(array[j]);
            }
            int index = 0;
            for (int j = 0; j < bucketList.size(); j++) {
                for (int k = 0; k < bucketList.get(j).size(); k++)
                    array[index++] = bucketList.get(j).get(k);
                bucketList.get(j).clear();
            }
        }
        return array;
    }


总结

搞到这里,这样一篇长篇博客终于弄完了。写一篇长文博客真的很不容易,虽然很大一部分内容都是直接参考了优质博主的文章,但是汇总这么多文章真是一件很不容易的事情。希望以后自己也可以发一个原创的文章。看完这篇文章的你会不会有所收获了,希望大家指出文章中的错误,我会及时修正,也会陆续增添文章的内容。点赞,关注,大家一起进步。

参考资料

前端面试十大经典排序算法(动画演示)
十大经典排序算法总结(Java语言实现)
十大经典排序算法动图图解
十大经典排序算法

;原文链接:https://blog.csdn.net/qq_44159782/article/details/115631663
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!
上一篇:51单片机数码管显示历史键值 下一篇:没有了

推荐图文


随机推荐