JavaSE-八大经典排序算法及优化算法思路与实现

2023-05-16


目录


1.冒泡排序

1.1算法思想

1.2 算法实现

1.3 算法优化

1.4 算法分析

2.简单选择排序

2.1 算法思想

2.2 算法实现

2.3 算法优化

2.4 算法分析

3.直接插入排序

3.1 算法思想

3.2 算法实现

3.3 算法优化 

3.4 算法分析

4.希尔排序

4.1 算法思想

4.2 算法实现

4.3 算法分析

5.快速排序

5.1算法思想

5.2算法实现

5.3算法分析

6.归并排序

6.1算法思想

6.2 算法实现

6.3算法分析

7.堆排序

7.1算法思想

7.2 算法实现

7.3 算法分析

8.基数排序

8.1 算法思想

8.2算法实现

8.3算法分析

9.总结与思考

9.1时间、空间复杂度及算法稳定性对比

9.2 算法特点总结

9.3 应用范围


1.冒泡排序

1.1算法思想

冒泡排序的过程就如同它的名字一样,每次冒泡的过程会将元素中最大/小的一个数冒出来, 这样最后的一个元素就会是最大/小的元素,下一次冒泡过程就可以对前n-1个再进行排序,n趟过程下来整个序列就变成有序的了。

 以上图为例,它的过程如下:

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。 

  2. 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对,最后的元素将会是最大的数。 

  3. 之后针对所有的元素重复以上的步骤,除了最后一个。

  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较,排序就完成了。

第一趟:5和8比,5小不用动;8和6比,6小,进行交换;8和1比,1小进行交换;8和9比,8小不用动,第一趟排序结束。

第二趟:9已经是选出的最大元素,对剩下的元素进行冒泡,5和6比不用动;6和1比交换,6和8比不用动。

以此类推,最后排序结束。

动图示例:

1.2 算法实现

外层循环控制趟数,内层循环用来进行两两元素的交换。

  public static void bubbleSort(int[] arr){
        if(arr == null || arr.length <2)
            return;
        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j < arr.length-i-1; j++) {
                if(arr[j]>arr[j+1]){
                    int temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
        }
    }

测试:

  public static void main(String[] args) {
        int[] arr1 = {2,3,1,0,2,5,3};
        int[] arr2 = {6,2,5,-1,-3,0,-1};
        int[] arr3 = {1,19,-20,20,20,20,14};
        bubbleSort(arr1);
        bubbleSort(arr2);
        bubbleSort(arr3);

        System.out.println(Arrays.toString(arr1));
        System.out.println(Arrays.toString(arr2));
        System.out.println(Arrays.toString(arr3));
    }

打印结果:

1.3 算法优化

普通的冒泡排序存在着一个问题,数据的顺序排好之后,冒泡算法仍然会继续进行下一轮的比较,直到arr.length-1次,后面的比较没有意义的,因此可以通过优化减少它的比较次数。

改进方法:设置标志位flag,如果发生了交换flag设置为true;如果没有交换就设置为false。

这样当一轮比较结束后如果flag仍为false,即:这一轮没有发生交换,说明数据的顺序已经排好,没有必要继续进行下去。

  public static void optimizeBubbleSort(int[] arr){
        if(arr == null || arr.length <2)
            return;
        boolean flag = false;
        for (int i = 0; i < arr.length; i++) {
            for (int j = 0; j < arr.length-i-1; j++) {
                if(arr[j]>arr[j+1]){
                    int temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                    flag = true;
                }
            }
            if(!flag)
                break;
        }
    }

1.4 算法分析

时间复杂度:

若元素序列的初始状态已经是正序的,一趟扫描即可完成排序。所需的关键字比较次数C 和记录移动次数M 均达到最小值:Cmin=n-1,Mmin=0。

因此冒泡排序最好情况下的时间复杂度为O(n)

若初始元素序列是逆序的,需要进行n-1趟排序。每趟排序要进行n-i次关键字的比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值。

因此冒泡排序最好情况下的时间复杂度为O(n^2)。

综上,冒泡排序的平均时间复杂度为:O(n^2)

空间复杂度:

冒泡排序的空间复杂度为常数阶O(1)。

算法稳定性:

首先算法稳定性的定义:我的理解是在整个排序过程中,如果任意两个相等的元素在排序之前和排序之后的相对位置顺序没有发生改变,那么该算法就是稳定的,反之就是不稳定的。

比如arr[i]=arr[j],i<j,整个过程中arr[i]都在arr[j]的前面没有发生改变,那么就是稳定的排序算法。

冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,是不会再交换的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定的排序算法。

2.简单选择排序

2.1 算法思想

 第一次从待排序的元素中选出最小(大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。

以上图为例:

第一趟:选出最小的元素1,与第一位元素5进行交换。

第二趟:选出最小的元素5,和第二位的元素8进行交换。

第三趟:最小的元素就在第三位,第四趟,第五趟都是,排序结束。

动图示例:

2.2 算法实现

外层循环控制趟数,内层循环用来寻找每一趟中最小元素的下标,内层循环结束进行最小元素与趟数对应位置的交换。

    public static void selsectSort(int[] arr){
        if(arr == null || arr.length <2)
            return;
        for (int i = 0; i < arr.length; i++) {
            int minIndex = i;
            for (int j = i+1; j < arr.length; j++) {
                if(arr[j] < arr[minIndex]){
                    minIndex = j;
                }
            }
            if(minIndex != i){
                int temp = arr[minIndex];
                arr[minIndex] = arr[i];
                arr[i] = temp;
            }
        }
    }

 测试:

    public static void main(String[] args) {
        int[] arr1 = {2,3,1,0,2,5,3};
        int[] arr2 = {6,2,5,-1,-3,0,-1};
        int[] arr3 = {1,19,-20,20,20,20,14};
        selsectSort(arr1);
        selsectSort(arr2);
        selsectSort(arr3);

        System.out.println(Arrays.toString(arr1));
        System.out.println(Arrays.toString(arr2));
        System.out.println(Arrays.toString(arr3));
    }

打印输出: 

2.3 算法优化

普通的选择排序算法在每一次交换中仅仅是选择出了最小的一个元素放在了第i个坐标,优化后可以加一个将最大元素放在最大下标的操作,这样可以减少整个排序的趟数,只需进行n/2趟即可。

用minIndex和maxIndex分别记录每一趟最小和最大元素的下标,找到之后与对应的i,j位置进行交换。

完整代码:

  public static void optimizeSelsectSort(int[] arr){
        if(arr == null || arr.length <2)
            return;
        for (int i = 0; i < arr.length/2; i++) {
            int minIndex = i;
            int maxIndex = i;
            int j = 0;
            for (j = i+1; j < arr.length-i-1; j++) {
                if(arr[j] < arr[minIndex]){
                    minIndex = j;
                }
                if(arr[j] > arr[maxIndex]){
                    maxIndex = j;
                }
            }
            if(minIndex != i){
                int temp = arr[minIndex];
                arr[minIndex] = arr[i];
                arr[i] = temp;
            }
            if(maxIndex == i){
                maxIndex = minIndex;
            }
            if(maxIndex != j){
                int temp = arr[maxIndex];
                arr[maxIndex] = arr[j];
                arr[j] = temp;
            }
        }
    }

对于其中的一段代码:

if(maxIndex == i){
   maxIndex = minIndex;
   }

 为什么要加个这个呢,因为优化后的算法存在一种情况,当maxIndex的下标恰好为i的时候,在上一步minIndex和i交换的过程中把arr[minIndex]和arr[i]的值给互换了,所以要将maxIndex改为minIndex。

2.4 算法分析

时间复杂度:

选择排序的交换操作介于 0 和 (n - 1)次之间。选择排序的比较操作为 n (n - 1) / 2 次之间。选择排序的赋值操作介于 0 和 3 (n - 1) 次之间。

比较次数O(n^2),比较次数与关键字的初始状态无关,总的比较次数N=(n-1)+(n-2)+...+1=n*(n-1)/2。

交换次数O(n),最好情况是,已经有序,交换0次;最坏情况交换n-1次,逆序交换n/2次。

交换次数比冒泡排序少,由于交换所需CPU时间比比较所需的CPU时间多,n值较小时,选择排序比冒泡排序快。

综上所述,选择排序的平均时间复杂度为:O(n^2)。

空间复杂度:

选择排序的空间复杂度为常数阶O(1)。

算法稳定性:

选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果一个元素比当前元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。举个例子,序列5 8 5 2 9,如果选择第二个5和第一个5交换,那么原序列中两个5的相对前后顺序就被破坏了,所以选择排序是一个不稳定的排序算法。

3.直接插入排序

3.1 算法思想

插入排序思想是将数组R划分成两个子区间R[1..i-1](已排好序的有序区)和R[i..n](当前未排序的部分,可称无序区)。插入排序的基本操作是将当前无序区的第1个记录R[i]插人到有序区R[1..i-1]中适当的位置上,使R[1..i]变为新的有序区。

如上图,第一趟-1小于3,将-1放在3的位置,3后移;第二趟2小于3但大于-1,将2插入3前面,3后移;第三趟8大于3不用动;第四趟5小于8但大于3,将5插入到8前面,8后移。

动图示例:

3.2 算法实现

 外层循环控制趟数,内层循环寻找到要插入到有序序列的无序序列下标,然后将有序序列后移,将temp放到找到的位置下标。

  public static void insertSort(int[] arr){
        if(arr == null || arr.length < 2){
            return;
        }
        for (int i = 0; i < arr.length; i++) {
            int temp = arr[i];
            int j = 0;
            for (; j <= i-1; j++) {
                if(arr[j] > temp){
                    break;
                }
            }
            for (int k = i-1; k >= j; k--) {
                arr[k+1] = arr[k];
            }
            arr[j] = temp;
        }
    }

测试:

    public static void main(String[] args) {
        int[] arr1 = {2,3,1,0,2,5,3};
        int[] arr2 = {6,2,5,-1,-3,0,-1};
        int[] arr3 = {1,19,-20,20,20,20,14};
        insertSort(arr1);
        insertSort(arr2);
        insertSort(arr3);

        System.out.println(Arrays.toString(arr1));
        System.out.println(Arrays.toString(arr2));
        System.out.println(Arrays.toString(arr3));
    }

打印输出:

3.3 算法优化 

普通插入是先找到位置然后移动,优化后可以将两个操作放在一个循环中,减少一个for循环的使用,从而减少了比较次数。

   public static void optimizeInsertSort(int[] arr){
        if(arr == null || arr.length < 2){
            return;
        }
        for (int i = 1; i < arr.length; i++) {
            int temp = arr[i];
            int j = i-1;
            for (; j >= 0; j--) {
                if(arr[j] < temp){
                    break;
                }
                arr[j+1] = arr[j];
            }
            arr[j+1] = temp;
        }
    }

3.4 算法分析

时间复杂度: 

在插入排序中,当待排序数组是有序时,是最优的情况,只需当前数跟前一个数比较一下就可以了,这时一共需要比较N- 1次,时间复杂度为O(n)。

最坏的情况是待排序数组是逆序的,此时需要比较次数最多,总次数记为:1+2+3+…+N-1,所以,插入排序最坏情况下的时间复杂度为(On^2)。

平均来说,A[1..j-1]中的一半元素小于A[j],一半元素大于A[j]。插入排序在平均情况运行时间与最坏情况运行时间一样,是输入规模的二次函数 。

综上所述,插入排序的平均时间复杂度为:O(n^2)。

空间复杂度:

插入排序的空间复杂度为常数阶O(1)。

算法稳定性:

排序前后相等元素的相对位置没有发生改变,所以该算法是稳定的。

4.希尔排序

4.1 算法思想

希尔排序(Shell's Sort)是插入排序的一种又称“缩小增量排序”(,是直接插入排序算法的更高效的改进版本。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  1. 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。

  2. 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。

先取一个小于n的整数d1作为第一个增量,把所有元素按增量进行分组,所有距离为d1的倍数的元素放在同一个组中,然后各组内进行直接插入排序;接下来取第二个增量d2 ,重复上述操作,直至增量为1时,最后进行一次排序即可。

如上图,开始增量为length/2=5时,有[8,0],[5,3],[11,9],[3,13],[-1,4]五组,对五组进行以此排序,以此类推,再减少增量为3然后排序,最后减少增量为1进行排序。

4.2 算法实现

有两种方式,一种是直接定义gap每次就除以2,也可以自定义增量数组,循环调用shellSort函数进行排序。

完整代码(自定义增量):

    public static void shellSort(int[] arr, int gap){
        for (int i = gap; i < arr.length; i++) {
            int temp = arr[i];
            int j = i-gap;
            for (; j >= 0; j-=gap) {
                if(arr[j] <= temp){
                    break;
                }
                else{
                    arr[j+gap] = arr[j];
                }
            }
            arr[j+gap] = temp;
        }
    }

    public static void shell(int[] arr){
    if(arr == null || arr.length < 2){
            return;
        }
        int[] gap = {5,3,2,1};
        for (int i = 0; i < gap.length; i++) {
            shellSort(arr,gap[i]);
        }
    }

或者(自除2): 

    private static void shellSort(int[] arr) {
        for (int gap = arr.length / 2; gap > 0; gap /= 2) {
            for (int i = gap; i < arr.length; i++) {
                int value = arr[i];
                int j = i-gap;
                for (j = i - gap; j >= 0 && arr[j] > value; j -= gap) {
                    arr[j + gap] = arr[j];
                }
                arr[j + gap] = value;
            }
        }
    }

测试:

 public static void main(String[] args) {
        int[] arr1 = {2,3,1,0,2,5,3};
        int[] arr2 = {6,2,5,-1,-3,0,-1};
        int[] arr3 = {1,19,-20,20,20,20,14};
        shell(arr1);
        shell(arr2);
        shell(arr3);

        System.out.println(Arrays.toString(arr1));
        System.out.println(Arrays.toString(arr2));
        System.out.println(Arrays.toString(arr3));
    }

打印输出:

4.3 算法分析

时间复杂度:

希尔排序的时间复杂度与增量的选取有关:

如希尔自己提出的每次将增量除以2向下取整的选择方法,时间复杂度为O(n^2)

增量的选取有两个要点:

  • 增量序列的最后一个值一定取1
  • 增量序列中的值应该尽量没有除1意外的公因子

空间复杂度:

希尔排序的空间复杂度为常数阶O(1)。

算法稳定性:

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

5.快速排序

5.1算法思想

每一趟的排序过程中,先选定序列中的一个元素作为中心枢纽,选取是随意的,因此一般选择第一个元素,将子序列中比枢纽元素小的移动到它的前面,比它大的移动到它的后面,根据需求将相等的元素划分到前面一部分还是后面的部分。当这一趟结束后,该序列以枢纽为中心被划分为两部分,一部分元素都比它小,一部分元素都比它大,然后分别对这两部分重复上述操作,不断的递归,当序列长度还剩一个元素时排序就完成了。

动图示例:

5.2算法实现

实现过程:定义两个引用start和end,分别指向待排序列的第一个元素和最后一个元素,  定义一个基准,一般来说取待排序列的第一个元素,从后往前比较end与基准的大小,  如果end小于基准,则end往前走,从前往后比较start和基准的大小,如果start大于  基准,则start往后走,start和end相对位置发生改变,此时结束,start=end=基准,  此时会发现,基准的左边都是比基准小的数据,基准的右边都是比基准大的数据,接下来针对左右两边的序列继续进行以上过程,最终可以得到一个完全有序的序列。
 

  public int[] MySort (int[] arr) {
        quickSort(arr, 0, arr.length-1);
        return arr;
    }
    
    public void quickSort(int[] arr,int low, int high){
         if(arr == null || arr.length == 0){
            return;
        }
        if(low < high) {
            int pos = partition(arr, low, high);
            quickSort(arr, low, pos - 1);
            quickSort(arr, pos + 1, high);
        }
    }
    
    public int partition(int[] arr, int low, int high){
        int i=low, j=high, tmp = arr[low];
        while(i<j){
            while(i<j && arr[j] >= tmp){
                j--;
            }
            swap(arr, i, j);
            while(i<j && arr[i] <= tmp){
                i++;
            }
            swap(arr, i, j);
        }
        arr[j] = tmp;
        return i;
    }
    
    public void swap(int[] arr, int low, int high){
        int tmp = arr[low];
        arr[low] = arr[high];
        arr[high] = tmp;
    }
}

测试:

  public static void main(String[] args) {
        int[] arr = new int[]{3,2,6,4,4,1,9};
        System.out.println(Arrays.toString(arr));
        rotate(arr,0,arr.length-1);
        System.out.println(Arrays.toString(arr));
    }

 打印输出:

5.3算法分析

时间复杂度:

快速排序最好情况下的时间复杂度为O(nlogn),即待排序序列越接近无序,该算法效率越高。

最坏情况下时间复杂度为O(n^2),待排序序列越接近有序,该算法效率越低。

空间复杂度:

从空间性能上看,尽管快速排序只需要一个元素的辅助空间,但快速排序需要一个栈空间来实现递归。最好的情况下,即快速排序的每一趟排序都将元素序列均匀地分割成长度相近的两个子表,所需栈的最大深度为log2(n+1);但最坏的情况下,栈的最大深度为n。这样,快速排序的空间复杂度为O(log2n)。

算法稳定性:

举个例子,3,2,2,5,4,如果选择arr[1]作为枢纽,取小于等于它的移动到前面,该算法就是不稳定的;或者取arr[2]为枢纽,大于等于它的移动到后面,这样来看也是稳定的。

综上所述,快速排序是一种不稳定的排序算法。

6.归并排序

6.1算法思想

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

简单来说,就是讲初始序列进行重复的对半分组,知道每组只剩一个元素或者两个元素时进行排序,按照指定的顺序排好序后,再合并两个小组进行排序,以此类推,直至最后将整个序列排序完毕后就完成了。

例如:

设有数列{6,202,100,301,38,8,1}

初始状态:6,202,100,301,38,8,1

第一次归并后:{6,202},{100,301},{8,38},{1},比较次数:3;

第二次归并后:{6,100,202,301},{1,8,38},比较次数:4;

第三次归并后:{1,6,8,38,100,202,301},比较次数:4;

总的比较次数为:3+4+4=11;

动图示例:

6.2 算法实现

整体思想是二分法将序列分成两部分,用四个下标记录两部分的起始与终止位置,循环比较两部分中,将较小的加入到temp数组中,一次比较完成后,至少有一部分已经归并完成,对可能存在的另一部分没有归并完的情况继续进行归并,然后进行位置的更新,当划分只有一部分的时候退出循环。退出时将临时数组拷贝至原数组即可。

package Sort;

import java.util.Arrays;

/**
 * Description :
 * Created by Resumebb
 * Date :2020/12/21
 */
public class mergeSort {

    public static void merge(int[] arr, int gap){
        // 四个变量表示有序序列的起始结束位置
        int left1 = 0;
        int left2 = left1+gap-1;
        int right1 = left2+1;
        int right2 = (right1+gap-1 > arr.length-1) ? arr.length-1 : right1+gap-1;
        // 临时数组,用于存储归并后的元素
        int[] temp = new int[100];
        // 临时指针
        int index = 0;
        // while①循环,该循环条件为归并时有两个归并段,如果只有一个归并段就把它放在temp中直接返回。
        while(right1 < arr.length) {
            // 对当前两个归并段进行合并的过程,while②循环
            while (left1 <= left2 && right1 <= right2) {
                if (arr[left1] <= arr[right1]) {
                    temp[index++] = arr[left1++];
                } else {
                    temp[index++] = arr[right1++];
                }
            }
            // 退出while②循环表示其中至少有一个归并段已经完成
            // 归并完成为1号归并段,2号没有完成时,对2号归并段继续进行归并操作
            while(left1 <= left2){
                temp[index++] = arr[left1++];
            }
            //归并完成为2号归并段,1号没有完成时,对1号归并段继续进行归并操作
            while(right1 <= right2){
                temp[index++] = arr[right1++];
            }

            // 一次归并完成后,对下标位置进行更新
            left1 = right2+1;
            left2 = left1+gap-1;
            right1 = left2+1;
            right2 = (right1+gap-1 > arr.length-1) ? arr.length-1 : right1+gap-1;
        }
            // 只有一个归并段的情况
            while(left1 < arr.length){
                temp[index++] = arr[left1++];
            }
            // 将归并后的元素拷贝至原数组
            for (int i = 0; i < arr.length; i++) {
                arr[i] = temp[i];
            }
    }

    public static void mergeSort(int[] arr){
        if(arr == null || arr.length == 0){
            return;
        }
        for (int i = 1; i < arr.length; i *= 2) {
            merge(arr, i);
        }
    }
    public static void main(String[] args) {
        int[] arr = {6, 9, 2, 1, 19, 5, 4, 3, 10, 5, 8, 18, 20, 24, 0};
        mergeSort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

 测试:

6.3算法分析

归并排序最好,最坏,平均时间复杂度都为O(nlogn),空间复杂度为O(n),是一种稳定算法。 

7.堆排序

7.1算法思想

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

   在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:

  • 最大堆调整(Max Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点

  • 创建最大堆(Build Max Heap):将堆中的所有数据重新排序

  • 堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算

堆排序的树结构:

动图示例:

7.2 算法实现

首先根据序列建堆,也就是一个二叉树,从上到下,从左到右依次填充。adjust函数用于调整,步长为2n+1,当结点下标超过end时终止循环,在循环中,判断如果i结点的右边结点存在并且arr[i]小于arr[i+1]的话,将i++,让i指向左右孩子结点中较大的右孩子结点,再与开始tmp存储的arr[start]值进行比较,如果比tmp要大,就用该值对arr[start]进行覆盖,然后start指向i,也就是当前最大孩子的下标。循环结束后,再将tmp赋值给arr[start]。

headSort中通过多次调用调整函数,进行大根堆的建造。

package sort;

import java.util.Arrays;

/**
 * 堆排序
 * 思想:利用堆(基于完全二叉树),堆分为大根堆和小根堆
 * 从小到大排序则利用大根堆
 * 从大到小排序则利用小根堆
 *
 */
public class test{
    //时间复杂度 O(N * log2N) 空间复杂度 O(1)
    //不稳定
    //调整堆的过程
    public static void adjust(int[] array, int start, int end){
        //start表示要调整树的根节点
        int tmp = array[start];
        //找左右 孩子的最大值
        for(int i=2*start+1; i<=end; i=2*i+1){
            //判断是否有右孩子
            if(i+1 <= end && array[i] < array[i+1]){
                i++;//i指向左右孩子的最大值
            }
            if(array[i] > tmp){
                //需要将array[i]换到start位置
                array[start] = array[i];
                start = i; //更新start,保存当前最大孩子的下标
            }else{
                break;
            }
        }
        array[start] = tmp;
    }

    public static void headSort(int[] array){
        if(array == null || array.length == 0){
            return;
        }
        //i初始化为最后一个叶子节点所在子树的根节点
        //经过for循环之后发现当前序列所对应的堆是大根堆
        for(int i=(array.length-1-1)/2; i>=0 ;i--){
            adjust(array, i, array.length-1);
        }
        for(int i=0; i<array.length; i++){
            //相当于将根节点(待排序列的最大值)换到待排序列的最后
            int tmp = array[0];
            array[0] = array[array.length-1-i];
            array[array.length-1-i] = tmp;

            adjust(array, 0, array.length-1-i-1);
        }
    }
    public static void main(String[] args) {
        int[] array = {2, 9, 10, 11, 29, 3, 6, 100, 50, 67, 30, 8};
        headSort(array);
        System.out.println(Arrays.toString(array));
    }
}

测试:

7.3 算法分析

堆排序的最好,最坏和平均时间复杂度都为O(nlogn),空间复杂度为O(1),因为发生了元素位置顺序的交换,所以是一种不稳定的排序算法。 

8.基数排序

8.1 算法思想

基数排序又称为,属于“分配式排序”,它通过元素的各个位的值,将元素放置对应的“桶”中。首先把元素统一为同样长度的数组长度元素较短的数前面补0,比如(1 15 336   看成  001 015 336),然后从最低位开始,以此进行排序。

动图示例:

8.2算法实现

package Sort;

import java.util.Arrays;

/**
 * Description :
 * Created by Resumebb
 * Date :2020/12/21
 */
public class Radix {

    public static void radixSort(int[] arr) {

        // 桶  10个桶  每个桶的最大容量默认为数组长度
        int[][] bucket = new int[10][arr.length];
        // 每个桶的当前容量
        int[] capacity = new int[10];

        //元素求出最大数                                                                                                                                                                                                                                                                                                                                                                                                                                          
        int max = arr[0];
        for (int r = 0; r < arr.length; r++) {
            if (arr[r] > max) {
                max = arr[r];
            }
        }
        //求出最大长度 用于判断循环几大轮 
        int length = (max + "").length();

        //取得(个位 十位 百位。。。。)基数     
        for (int b= 0,u=1; b < length; b++,u*=10) {
            for (int i = 0; i < arr.length; i++) {
                int base = arr[i] /u % 10;  //比如基数为 4
                //将基数按照规则放进桶中
                bucket[base][capacity[base]] = arr[i];     //放进第四个桶中 的第一几个当前容量位置
                capacity[base]++;   //容量增加
            }

            // 取出数据
            int d = 0;
            for (int k = 0; k < capacity.length; k++) {
                if (capacity[k] != 0) {
                    for (int p = 0; p < capacity[k]; p++) {
                        arr[d] = bucket[k][p];
                        d++;
                    }
                }
                //注意:清零
                capacity[k] = 0;
            }
        }
    }

    public static void main(String[] args) {
        int[] arr = {3,8,9,11,4,15,2,17,9,1};
        radixSort(arr);
        System.out.println(Arrays.toString(arr));
    }

}

 测试:

8.3算法分析

基数排序法是属于稳定性的排序,其时间复杂度为O (nlog(r)m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。 

9.总结与思考

9.1时间、空间复杂度及算法稳定性对比

平均情况下,快速排序、希尔排序、归并排序和堆排序的时间复杂度均为O(nlog2n),其他都是O(n^2),比较特殊的是基数排序,它的时间复杂度为O(d(r+rd))。

最坏情况下,快速排序的时间复杂度为O(n^2),其他都和平均情况相同,具体参照下表。

9.2 算法特点总结

  • 经过一趟排序,能够保证有一个元素到达最终位置的排序算法有:交换排序类(冒泡排序,快速排序),选择排序类(简单选择排序,堆排序)。
  • 排序算法的关键字比较次数与原始序列无关的排序算法:简单选择排序,折半插入排序。
  • 排序算法的排序趟数和原始序列有关:交换排序类(冒泡排序,快速排序)。

9.3 应用范围

  • 原始序列接近有序的情况,适合使用直接插入排序或冒泡排序。
  • 原始序列规模非常大的情况,适合使用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序。
  • 原始序列规模较小的情况,适合使用直接插入排序或简单选择排序。
  • 插入排序适用于部分有序以及小规模序列。
  • 希尔排序的优点是比插入排序和选择排序要快,且序列规模越大,它的优势越明显。
  • 快速排序的优点是原地排序,它只需要一个很小的辅助栈,被认为是内部排序中最好的排序算法,序列越接近无序,规模越大越适合快速排序。
  • 归并排序可以处理数百万甚至更大规模的数组,这是插入排序和选择排序做不到的,但归并排序的缺点是辅助数组所使用的额外空间和n的大小成正比,这也成为了使用它的一个限制条件。
  • 堆排序的优点是在排序时可以将需要排序的数组本身作为堆,无需任何额外空间,与选择排序有些类似,但所需的比较要少得多,堆排序适合例如嵌入式系统或低成本移动设备中容量有限的场景。   

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

JavaSE-八大经典排序算法及优化算法思路与实现 的相关文章

  • Python学习-多线程

    1 线程 首先区分线程和进程两个概念 xff1a 进程是资源 xff08 CPU 内存等 xff09 分配的基本单位 xff0c 它是程序执行时的一个实例 程序运行时系统就会创建一个进程 xff0c 并为它分配资源 xff0c 然后把该进程
  • Spring MVC controller出错后进入了不该进入的拦截器

    拦截器对一些接口进行了排除 xff0c 使这些接口不用进入拦截器的验证 xff0c 但是出现了一个现象 xff0c 例如 api v1 auth register 这个接口 xff0c 如果因为某些原因 xff0c 出现异常了 xff0c
  • Git学习-本地仓库与版本管理

    一 创建仓库 1 创建空仓库 在合适的位置创建一个新目录 xff0c Git Bash支持的是linux命令操作 第一种方法 xff1a mkdir mygit cd mygit pwd 用mkdir创建目录 xff0c pwd查看当前路径
  • STM32F407用wk2124芯片编写SPI转四路串口驱动

    目录 引言 一 SPI通信配置 1 GPIO初始化设置 2 SPI参数配置 3 读写函数 4 速度设置 二 WK2124逻辑代码编写 1 片选初始化 2 写寄存器函数 3 读寄存器函数 4 写FIFO函数 5 读FIFO函数 6 WK212
  • Git学习-远程仓库

    创建远程仓库 本地现在有一个仓库git xff0c 同时可以在github上创建一个同名的git仓库 xff0c 可以供自己和他人协同操作 1 在github电机new repository创建一个新仓库 xff0c 名称保持与本地一致 R
  • Python学习-SQLite

    SQLite是一种嵌入式数据库 xff0c 它的数据库就是一个文件 由于SQLite本身是C写的 xff0c 而且体积很小 xff0c 所以 xff0c 经常被集成到各种应用程序中 xff0c 甚至在iOS和Android的App中都可以集
  • Python学习-TCP网络编程

    1 客户端 Socket是网络编程的一个抽象概念 通常我们用一个Socket表示 打开了一个网络链接 xff0c 而打开一个Socket需要知道目标计算机的IP地址和端口号 xff0c 再指定协议类型即可 大多数连接都是可靠的TCP连接 创
  • Git学习-分支

    在Git里 xff0c 主分支为master分支 HEAD也不是指向提交 xff0c 而是指向master xff0c master才是指向提交的 xff0c 所以 xff0c HEAD指向的就是当前分支 一开始的时候 xff0c mast
  • Matplotlib绘制各类图像(折线图,曲线图...)-画图的神

    Matplotlib简介 Matplotlib是一个Python工具箱 xff0c 用于科学计算的数据可视化 借助它 xff0c Python可以绘制如Matlab和Octave多种多样的数据图形 最初是模仿了Matlab图形命令 但是与M
  • STM32F407多路串口通信进行数据收发

    一直被说是就不能把几个串口放在一起 xff0c 写个标准例程直接用 xff0c 非要每次用哪个串口才现场改程序 xff0c 被迫把usart1 usart2 usart3进行了资源整合 xff0c 挂在这以备不时之需 功能简述 xff1a
  • 基于Keras实现电影评论文本分类与RNN实现

    notebook使用评论文本将影评分为积极 xff08 positive xff09 或消极 xff08 nagetive xff09 两类 这是一个二元 xff08 binary xff09 或者二分类问题 xff0c 一种重要且应用广泛
  • 基于STM32F407的七要素气象站(气象传感器)CR-WS数据处理实现

    一 七要素气象站介绍 1 七要素气象站介绍 开发板还是采用STM32F407 485连线 xff1a 如果买了变送器就按照下图连线 xff1a 没有买变送器的话 xff0c 直接从气象站上拉线 xff0c 红正黑负 xff0c 黄485 A
  • MyBatis Plus 分页查询,total字段为0,分页未生效

    1 未配置 MybatisPlusInterceptor 64 Bean public MybatisPlusInterceptor mybatisPlusInterceptor MybatisPlusInterceptor interce
  • JavaSE学习记录-整数逆序+数组删除元素

    数组定义方法 int arr 61 new int 10 定义同时进行赋值 xff1a int arr 61 new int 1 2 3 4 5 数组打印方法 1 for循环打印 for int i 61 0 i lt arr length
  • FAFTS文件系统常用函数学习

    一 FATFS文件系统基础知识 1 简介 文件系统可以从官网进行下载 官网地址 xff1a http elm chan org fsw ff 00index e html FATFS是一个完全免费开源的FAT 文件系统模块 xff0c Fa
  • Leetcode-旋转数组+最后一个单词长度

    给定一个数组 xff0c 将数组中的元素向右移动 k 个位置 xff0c 其中 k 是非负数 示例 1 输入 1 2 3 4 5 6 7 和 k 61 3 输出 5 6 7 1 2 3 4 解释 向右旋转 1 步 7 1 2 3 4 5 6
  • Leetcode-最短路径和+最大子串和(动态规划)

    给定一个包含非负整数的 m x n 网格 xff0c 请找出一条从左上角到右下角的路径 xff0c 使得路径上的数字总和为最小 说明 xff1a 每次只能向下或者向右移动一步 示例 输入 1 3 1 1 5 1 4 2 1 输出 7 解释
  • LeetCode-二进制串和+宝石与石头

    给你两个二进制字符串 xff0c 返回它们的和 xff08 用二进制表示 xff09 输入为 非空 字符串且只包含数字 1 和 0 示例 1 输入 a 61 34 11 34 b 61 34 1 34 输出 34 100 34 示例 2 输
  • JavaSE数组练习-句子翻转+字符替换+打印特殊三角

    1 句子翻转 要求 xff1a 给定字符串如 34 hello i am a student 34 xff0c 对英语句子进行翻转 xff0c 并保持英语单词的顺序不变 xff0c 对标点符号当成字母处理 代码实现 xff1a import
  • 视觉SLAM学习--基础篇(SLAM框架及相机模型)

    一 例子 如上图的小萝卜机器人 xff0c 要使其具有自主运动能力至少需要两个条件 xff1a 1 我在什么地方 xff1f 定位 2 周围环境是什么样 xff1f 建图 因此它既需要知道自身的状态 位置 xff0c 也要了解所在的环境 地

随机推荐

  • Linux各类软件安装配置问题记录

    1 Ubuntu侧边栏和顶部栏消失不见 解决方法 xff1a 鼠标右键或者快捷键打开终端输入命令 dconf reset f org compiz 输入命令 setsid unity 一般到这一步侧边栏就会出现了 xff0c 如果没有出现就
  • 代码模拟确定有限自动机(DFA)执行过程

    一个确定有限自动机 xff08 DFA xff09 M是一个五元组 xff1a M 61 xff08 K xff0c xff0c f xff0c S xff0c Z xff09 其中 K是一个有穷集 xff0c 它的每个元素称为一个状态 x
  • 视觉SLAM-Eigen学习实践

    1 Eigen库介绍 Eigen 是一个 C 43 43 开源线性代数库 它提供了快速的有关矩阵的线性代数运算 xff0c 还包括解方程等功能 可以通过sudo apt install libeigen3 dev命令进行安装 xff0c 也
  • 苹果手机存储空间(或称为内存)满了导致黑屏转圈白苹果

    没刷机 xff0c 啥也没干 xff0c 发现把SIM卡拔了再开机就好了 xff0c 然后赶紧去卸载一些软件腾出空间
  • Arrays的toString方法和deepToString方法比较

    因为打印二维数组时用错了方法 xff0c 一般是用Arrays deppToString或者遍历使用toString xff0c 我直接用Arrays toString去打印了二维数组 xff0c 没有打印出正常二维数组的内容 xff0c
  • JavaSE-类与对象+单例模式

    1 类与对象的引用 概念 xff1a 如果一个变量的类型是类类型 xff0c 而非基本类型 xff0c 那么该变量又叫做引用 new testClass 该操作表示创建了一个testClass对象 xff0c 但没有办法访问这个对象 tes
  • JavaSE-类与对象-ATM自主操作系统实现

    学完类与对象的练习小作业 xff0c 主要有三个类 xff1a 银行卡类包含银行卡的相关信息如卡号 xff0c 密码 xff0c 姓名 xff0c 余额 xff1b 银行类中主要定义了一个银行卡数组 xff0c 用来存储当前用户的银行卡信息
  • JavaSE-基于回溯法用类+栈实现迷宫问题

    目录 1 问题描述 2 自定义类栈 3 结点类 4 操作类 5 函数讲解 6 测试类及结果 1 问题描述 输入迷宫大小 xff0c 以及路径 xff0c 0表示可走路径 xff0c 1表示死路 xff0c 从输入矩阵的左上角起点到右下角终口
  • Leetcode-234,844,19

    234 回文链表 请判断一个链表是否为回文链表 示例 1 输入 1 gt 2 输出 false 示例 2 输入 1 gt 2 gt 2 gt 1 输出 true 思路 xff1a 本想将链表逆置然后进行比较 xff0c 后来想了想用栈去做更
  • JavaSE-回溯+自定义类栈实现Puzzle问题

    Puzzle问题描述 如图有一个一维数组 xff0c 上面的数字表示可以移动的步数 xff0c 每个结点都有左右两个方向可以移动 xff0c 例如第一个结点4 xff0c 它只能往右移动4格到3的位置 xff0c 而3左右都可以移动 xff
  • JavaSE-泛型类、接口、方法、擦除机制

    1 泛型定义 泛型是JavaSE 1 5的新特性 xff0c 本质是参数化类型 xff0c 也就是所操作的数据类型被指定为一个参数 xff0c 将类型由原来的具体的参数类型化 xff0c 类似于方法中的变量参数 xff0c 此时类型也定义成
  • JavaSE-十分钟写个五子棋

    1 设计说明 1 1 简介 其实很久之前就写过五子棋 xff0c 当时拿AWT写的界面 xff0c 后面通过socket通信实现了联机对战操作 xff0c 当时写五子棋写的可费劲了 xff0c 现在又要写一次五子棋 xff0c 不过是简单版
  • JavaSE-类加载过程及反射

    目录 一 类加载过程 1 装载阶段 1 1执行过程 1 2 类加载器 1 3 双亲委派模型 1 4 类加载时机 2 链接阶段 2 1验证阶段 2 2准备阶段 2 3解析阶段 3 初始化阶段 二 反射 1 定义 2 用途 3 步骤 4 代码实
  • STM32F407-基于AD7606进行多路数据采集

    1 原理图 2 管脚定义 2 1 OS2 OS1 OS0 查阅数据手册 这三个管脚组合控制过采样模式 000 表示无过采样 xff0c 最大 200Ksps 采样速率 001 表示 2 倍过采样 xff0c 也就是硬件内部采集 2 个样本求
  • caffeine 与 reactor mono 一起使用产生的缓存错误问题

    现象 与reactor mono一起使用 xff0c 发现get key时 xff0c 返回的一直都是抛出的错误信息 xff0c 没有预期中的如果cache loader 返回null 或 错误时 xff0c caffeine自动剔除key
  • EBYTE E103-W02 WIFI模块配置总结(TCP+UDP+HTTP+云透传)

    目录 1 硬件配置 1 1 原理图 1 2 管脚配置 2 AT指令集 3 AP模式配置 3 1AP介绍 3 2 AP配置TCP通信 3 3 AP配置UDP通信 4 STA模式配置 4 1STA介绍 4 2配置过程 4 3网页配置 5 基于亿
  • JavaSE-自定义单链表

    目录 1 自定义链表实现 2 基础操作 2 1 链表打印操作 2 2 链表逆序打印 2 3 链表逆置 3 进阶操作 3 1查找倒数第K个结点 3 2 不允许遍历链表 xff0c 在pos结点之前插入 3 3两个链表相交 xff0c 输出相交
  • SRGAN实现超分辨率图像重建之模型复现

    1 论文介绍 1 1简介 论文名称 Photo Realistic Single Image Super Resolution Using a Generative Adversarial Ledig C Theis L Huszar F
  • JavaSE-自定义队列+两栈实现队列+两队列实现栈

    1 顺序队列实现 与栈一样 xff0c 队列也是一种操作受限制的线性表 xff0c 但与栈不同的是 xff0c 栈是后进先出 xff0c 队列的特点是先进先出 实现与栈类似 xff0c 队列有一个队头指针和一个队尾指针 xff0c 入队的时
  • JavaSE-八大经典排序算法及优化算法思路与实现

    目录 1 冒泡排序 1 1算法思想 1 2 算法实现 1 3 算法优化 1 4 算法分析 2 简单选择排序 2 1 算法思想 2 2 算法实现 2 3 算法优化 2 4 算法分析 3 直接插入排序 3 1 算法思想 3 2 算法实现 3 3