十种排序算法详解&Java实现(Leetcode | 912. 排序数组 )

排序算法

  • 题目(Leetcode | 912. 排序数组)
  • 比较类排序
    • 交换排序
      • 一、冒泡排序
        • 原理详解
        • Java实现
      • 二、快速排序
        • 原理详解
        • Java实现
    • 插入排序
      • 三、直接插入排序
        • 原理详解
        • Java实现
      • 四、希尔排序
        • 原理详解
        • Java实现
    • 选择排序
      • 五、简单选择排序
        • 原理详解
        • Java实现
      • 六、堆排序
        • 原理详解
        • Java实现
    • 归并排序
      • 七、归并排序
        • 原理详解
        • Java实现
  • 非比较类排序
    • 八、计数排序
      • 原理详解
      • Java实现
    • 九、桶排序
      • 原理详解
      • Java实现
    • 十、基数排序
      • 原理详解
      • Java实现
  • 复杂度总结

题目(Leetcode | 912. 排序数组)

给你一个整数数组 nums,请你将该数组升序排列。
示例 :
 输入:nums = [5,2,3,1]
 输出:[1,2,3,5]
提示:
 1 <= nums.length <= 50000
 -50000 <= nums[i] <= 50000

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

  • 比较类排序: 通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
  • 非比较类排序: 不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
    十种排序算法详解&Java实现(Leetcode | 912. 排序数组 )_第1张图片

比较类排序

交换排序

一、冒泡排序

原理详解

 冒泡排序(Bubble Sort)是一种较简单的排序算法。它重复走访要排序的数组,一次比较相邻的两个元素,将较大的数通过交换放在后面,这样遍历一遍数组会将当前数组的最大值交换到最后;重复上述操作,直至所有元素排序完毕。
 这个算法名字的由来是因为通过这样的交换越小的元素会慢慢的“浮”在数组的一端。是一种稳定的排序。
 (通过两两比较交换每次遍历将当前最大值交换到序列的最后)

冒泡排序算法:
 1、对于有n个元素的初始无序数组,从第一个元素开始,比较与其相邻的元素,若第一个比第二个元素大,则交换两者位置,将较大的放在后面,然后比较第二个和第三个元素,依次类推,直到数组最后,这样最后的第n个数就是整个数组最大的数。
  2、再次从头开始重复第一步,这次遍历除去已经确定最大的最后一个元素,即遍历0到n-1个数
  3、重复上述两步,直到只剩第一个元素排序完成。

时间复杂度:O(n) ~ O(n2)
空间复杂度:O(1)

Java实现

常规写法:时间复杂度一直时O(n2),会超时,会出现很多无用的循环(比如第一遍排序已经排好了之后还会继续循环遍历n-1遍)

class Solution {
     
    public int[] sortArray(int[] nums) {
     
        //冒泡排序
        if(nums == null || nums.length == 0) return nums;
        int len = nums.length;
        for(int i = 0; i < len-1; i++){
     //i为已排序的元素,即已确定的放在最后的元素
            for(int j = 0; j < len-1-i; j++){
     
                if(nums[j]>nums[j+1]){
     
                    swap(nums,j,j+1);
                }  
            }
        }
        return nums;
    }
    public void swap(int[] arr, int p, int q){
     
        int tmp = arr[p];
        arr[p] = arr[q];
        arr[q] = tmp;
    }
}

第一次优化:每次遍历设置一个标记位,标记这次遍历中是否有交换发生,如果没有交换发生,说明整个数组的排序已经完成,就不需要下一次循环了,可以提前结束,这样时间复杂度最好的情况能到O(n),即初始数组为排序好的情况时遍历一遍即可。结果大case还是会超时。

class Solution {
     
    public int[] sortArray(int[] nums) {
     
        //冒泡排序
        if(nums == null || nums.length == 0) return nums;
        int len = nums.length;
        boolean flag = false;
        for(int i = 0; i < len-1; i++){
     //i为已排序的元素,即已确定的放在最后的元素
            if(i!= 0 && !flag) return nums;
            flag = false;//每次遍历开始前将标记位归位为false
            for(int j = 0; j < len-1-i; j++){
     
                if(nums[j]>nums[j+1]){
     
                    swap(nums,j,j+1);
                    flag = true;//在这次遍历里只要有一次交换标记为true
                }  
            }
        }
        return nums;
    }
    public void swap(int[] arr, int p, int q){
     
        int tmp = arr[p];
        arr[p] = arr[q];
        arr[q] = tmp;
    }
}

第二次优化:在第一次优化的基础上,再增加一个标记,记录每一次遍历时交换的最后一个元素的位置,表明这个元素后面的元素已经排序完成不需再进行判断与交换,这样下一次遍历时,只需遍历到标记的这个位置即可,减少了内循环的次数。在这题里大case还是会超时,嗐。

class Solution {
     
    public int[] sortArray(int[] nums) {
     
        //冒泡排序
        if(nums == null || nums.length == 0) return nums;
        int len = nums.length;
        boolean flag = false;
        int swapend = 0;
        for(int i = 0; i < nums.length-1; i++){
     //i为已排序的元素,即已确定的放在最后的元素
            flag = false;//每次遍历开始前将标记位归位为false
            for(int j = 0; j < len-1; j++){
     
                if(nums[j]>nums[j+1]){
     
                    swap(nums,j,j+1);
                    flag = true;//发生交换,标记位为true。在一次遍历里只要有一次交换标记为true
                    swapend = j+1;//将交换的位置记录下来
                }  
            }
            len = swapend;//将一次遍历中最后的交换位置赋值给len,下次遍历只需到len-1和len的交换就可以了
            if(!flag) return nums;
        }
        return nums;
    }
    public void swap(int[] arr, int p, int q){
     
        int tmp = arr[p];
        arr[p] = arr[q];
        arr[q] = tmp;
    }
}

二、快速排序

原理详解

 快速排序(Quick Sort)也是一种交换排序,是对冒泡排序的改进。 快速排序采用分治思想,将一个序列通过一个基准元素分为两个子序列,再递归进行操作。
 其基本思想是,给定一个基准元素,通过一趟遍历与交换将待排序列分为两部分,一部分是小于基准元素的,在基准元素左边,另一部分大于基准元素的,在基准元素右边。(分的方式是: 每个元素与pivot进行比较,在pivot前的数若比pivot大则两者交换位置;在pivot后的数若比pivot小则两者交换位置。)然后再分别对上述两部分继续重复上述操作(递归)进行排序最终使整个序列有序。
 简言之就是通过一次遍历将基准元素交换到其在整个序列里的正确索引位置。 快速排序不是一种稳定的算法,多个相同的值的相对位置在算法结束时可能会有所变动。
 (每次选择第一个数字作为基准与后面每个数字(从最后开始)比较交换位置,让小于它的都交换到左边,大于它的都交换到右边,即找到他在序列中正确的排序位置后,再将两边用同样的思想这么干)

一趟快速排序算法:
 1、设置两个变量第一个元素 i = 0,最后一个元素 j = n-1。
 2、挑选一个基准元素pivot,一般为第一个元素nums[i]。
 3、从最后一个元素j开始向前搜索(此时基准元素在搜索元素的左边),j–,依次与基准元素pivot = nums[i]比较,找到第一个比pivot小的数,将两者交换位置,这时基准元素到达 j 的位置。
 4、从i开始向后搜索(此时基准元素在搜索元素的右边),i++,依次与基准元素pivot = nums[j]比较,找到第一个比pivot大的元素,两者交换位置,此时基准元素在 i 的位置。
 5、重复3、4两步,直到i = j。

时间复杂度:O(nlog2n)~O(n2)。
 快速排序的一次划分算法从两头开始交替搜索直至i = j,因此其时间复杂度为O(n),所以整个快排的时间复杂度与进行的一次划分的次数有关。

  1. 在理想的情况下,每次划分所选择的基准元素刚好将整个序列几乎等分,经过logn次划分,使其得到长度为1的子表,所以此时复杂度为O(nlog2n)。
  2. 最坏的情况下,每次选择的pivot都是序列中最小或最大元素,这使得每次划分得到的两个子序列一个是0元素,一个是n-1个元素,长度为n的序列可能需要n次划分,此时整个快排算法的时间复杂度为O(n2)

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

Java实现

class Solution {
     
    public int[] sortArray(int[] nums) {
     
        //快速排序
        if(nums == null || nums.length == 0) return nums;
        int len = nums.length;
        quikSort(nums,0,len-1);
        return nums;
    }
    public void quikSort(int[] nums, int l, int r){
     
        //一趟划分算法
        int i = l, j = r;
        int pivot = nums[i];
        while(i<j){
     
            while(i<j && nums[j]>pivot){
     
                j--;
            }
            while(i<j && nums[i]<pivot){
     
                i++;
            }
            if(i<j && nums[i] == nums[j]){
     
                i++;
            }else{
     
                swap(nums,i,j);
            }
        }
        //递归
        if(i-1>l) quikSort(nums,l,i-1);
        if(j+1<r) quikSort(nums,j+1,r);
    }
    public void swap(int[] arr, int p, int q){
     
        int tmp = arr[p];
        arr[p] = arr[q];
        arr[q] = tmp;
    }
}

十种排序算法详解&Java实现(Leetcode | 912. 排序数组 )_第2张图片

插入排序

三、直接插入排序

原理详解

 直接插入排序(Insertion Sort)是最简单的排序算法,对于少量元素的排序很有效。其基本思想就是构建有序序列(初始为一个元素),对于其他未排序元素,从后往前遍历有序表将未排序序列中的元素依次插入到已经排好序的有序表中,从而产生一个新的、数量增加1的有序表。
 从第一个数字开始,每次选择序列中的一个数字与有序序列中元素依次比较插入有序序列中。

插入排序算法:
 1、从第一个元素开始,构建有序序列,初始为第一个元素,认为第一个元素已按升序排列
 2、选择下一个元素nums[i],在有序表中从后往前遍历,依次比较大小,若遍历到的元素比nums[i]大,将该元素后移,继续往前遍历,直到找到比nums[i]小或等的元素,即该元素应该插入的位置,将其插入到对应位置
 3、重复第二步。直至排序完成

时间复杂度:O(n)~O(n2)

  1. 最好的情况下,初始序列是有序的,只需当前数和前一个数比较一下就可以了,这是一共需要比较n-1次,时间复杂度为O(n);
  2. 最坏的情况下,初始序列是逆序的,每个元素都要从它的位置比较到0,因此一共需要1+2+3+…+n-1次,此时时间复杂度为O(n2)

空间复杂度:O(1)

Java实现

class Solution {
     
    public int[] sortArray(int[] nums) {
     
        //插入排序
        if(nums == null || nums.length == 0) return nums;
        int len = nums.length,current = 0;
        for(int i = 1; i < len; i++){
     //初始有序序列为第一个元素,需插入的元素从nums[1]开始  
            current = nums[i];
            int j = i-1;
            for(j = i-1; j >=0; j--){
     
                if(current<nums[j]){
     
                    nums[j+1] = nums[j];//这个元素后移,继续往前遍历
                }else{
     
                    break;
                }
            }
            //可以插入到当前位置
            nums[j+1] = current;
        }
        return nums;
    }
}

十种排序算法详解&Java实现(Leetcode | 912. 排序数组 )_第3张图片

四、希尔排序

原理详解

 希尔排序(Shell Sort)是直接插入排序的改进版,是第一个突破O(n2)的算法。它与插入排序不同的是会优先比较距离较远的元素,又叫缩小增量排序。(增量:相隔较远距离,可理解为间隔)
 希尔排序的核心在于间隔(增量)序列的设定。该方法实质上是一种分组插入排序方法。每次设置不同的增量/间隔分为不同的序列,子序列按增量从大到小依次进行插入排序,直至最后增量为1的子序列的插入排序完毕。
 虽然插入排序是稳定的,不会改变相同元素的相对顺序,但是由于多次插入在不同的插入排序过程中,相同元素在各自的增量序列的插入排序中移动,导致其相对顺序可能会变动,所以希尔排序是不稳定的。
 分组插入排序:设置不同的间隔,间隔从大到小,/2,按每个间隔组成的每个子序列为组别,每次依次进行插入排序,直到把间隔为1的序列也排完为止。

希尔排序算法:
 1、选择一系列增量g1、g2、…、gk,gi>gi+1,gk = 1,一般来说gi+1 = gi/2
 2、对每个增量gi来说,有gi个子序列需要排序,对这gi个子序列分别进行gi次插入排序
 3、最后当增量为1时,将整个序列当作一个表进行插入排序,排序结束。

时间复杂度:O(n1.3)~O(n2)
空间复杂度:O(1)

Java实现

class Solution {
     
    public int[] sortArray(int[] nums) {
     
        //希尔排序
        if(nums == null || nums.length == 0) return nums;
        int len = nums.length,current = 0,j = 0;
        for(int gap = len/2; gap>=1; gap /= 2){
     //增量每次减半
            // for(int i = 0; i < gap;i++){//对每个增量gap来说,能有gap个子序列需要进行排序
            //     //对每个子序列继续插入排序
            //     for(int j = i+gap; j < len; j+=gap){
     
            //         current = nums[j];//当前需插入的元素
            //         //向前面有序序列进行比较
            //         int k = j-gap;
            //         while(k>=i && nums[k]>current){
     
            //             nums[k+gap] = nums[k];//后移
            //             k -= gap;
            //         }
            //         nums[k+gap] = current; 
            //     }
            // }
            //上述两个循环可以直接合并为一个循环如下:
            for(int i = gap; i < len; i++){
     
                current = nums[i];
                j = i-gap;
                while(j>=0 && nums[j]>current){
     
                    nums[j+gap] = nums[j];//后移
                    j -= gap;
                }
                nums[j+gap] = current; 
            }
        }
        return nums;
    }
}

十种排序算法详解&Java实现(Leetcode | 912. 排序数组 )_第4张图片

选择排序

五、简单选择排序

原理详解

 选择排序(Selection-sort)是一种简单直观的排序算法。其基本原理是:每次遍历选出未排序序列中最大/小的元素,将其与第一个位置上的元素交换位置,即将其放在当前未排序序列的开头,然后再继续从剩下的未排序序列中重复上述操作,直至排序完成。
 由于在直接选择排序中存在着不相邻元素之间的互换,因此,直接选择排序是一种不稳定的排序方法。
 每次遍历选出最小/最大的值将其放在序列的最前面。

选择排序算法:
 n个元素的选择排序经过n-1趟排序算法即可得到结果。
 1、初始无序区{a1,a2,…,an},有序区为空。
 2、遍历无序区,找出无序区最小元素ai,将其放在无序区的开头(下一次有序区的末尾)即与a1交换位置,此时无序区为{a2,a3,…,an},有序区为{a1}。
 3、重复第二步,继续遍历,每次遍历一遍,有序区添加一个元素,无序区减少一个元素。n-1次之后,排序完成。

时间复杂度:一直O(n2)
空间复杂度:O(1)

Java实现

class Solution {
     
    public int[] sortArray(int[] nums) {
     
        //选择排序
        if(nums == null || nums.length == 0) return nums;
        for(int i= 0; i< nums.length; i++){
     //有序区的末尾的后一个元素
            int minindex = i;//记录最小值的索引,i为无序区的开头
            for(int j = i+1; j < nums.length; j++){
     
                if(nums[j] < nums[minindex]){
     
                    minindex = j;
                }            
            }
            //里层一次循环后找出当前无序区最小的元素的索引minindex,将其与无序区开头元素i交换位置
            int tmp = nums[i];
            nums[i] = nums[minindex];
            nums[minindex] = tmp;
        }
        return nums;
    }
}

十种排序算法详解&Java实现(Leetcode | 912. 排序数组 )_第5张图片

六、堆排序

原理详解

 堆排序(Heapsort)是利用堆这种数据结构来设计的一种排序算法。堆是一种以数组形式存储的满足子节点的值总是不小于/不大于它的父节点的完全二叉树结构的数据结构,故大顶堆的堆顶元素必定是所有元素中最大的,小顶堆的堆顶元素必定是所有元素中最小的。
 堆的特点:堆顶最大/最小,根据堆的特点,将序列建堆,每次将堆顶拿出来放在有序序列中,再将剩余元素调整建堆,再拿堆顶,依次类推。和选择排序不同的就是每次选择当前最大/最小的元素的方式不同,这里运用了堆的特点。选择排序就是很普通的通过遍历依次比较。

堆排序算法:
1、将初始排序序列[a1,a2,…,an]构建为大顶堆,初始为无序的。
2、此时堆顶元素a1必定是序列中最大值,所以将a1与最后一个元素an交换,得到剩下的无序序列[a1,a2,…,an-1]与有序序列[an]。此时满足a[1,2,…,n-1]<=an; 其实就是根据大顶堆的性质每次把堆顶元素即最大值挑出来。
3、交换后剩下的无序序列中新的堆顶可能不是剩余最大值,所以需要重新将该序列调整为堆,然后在此将a1与无序序列中最后一个元素交换,得到新的无序序列[a1,a2,…,an-2]与有序序列[an-1,an].不断重复此步骤直至有序序列元素个数为n-1,整个排序完成。

时间复杂度:O(nlog2n)
 初始化建堆的时间复杂度为 O(n),建完堆以后需要进行 n-1 次调整,一次调整(即 heapify) 的时间复杂度为 O(log2n),那么 n-1 次调整即需要O(nlog2n) 的时间复杂度。因此,总时间复杂度为 O(n+nlog2n) = O(n+nlog2n) = O(nlog2n)
空间复杂度:O(1),只需常数空间存放临时变量。

Java实现

class Solution {
     
    public int[] sortArray(int[] nums) {
     
        //堆排序
        if(nums == null || nums.length == 0) return nums;
        buildMaxHeap(nums);//建堆
        //将堆顶元素即当前最大值拿出来,放在最后(从最后开始i遍历,与0进行交换),然后再将剩余的0到n-2个元素重新建堆
        for(int i = nums.length-1; i>0; i--){
     
            //将最大的堆顶元素nums[0]放在最后i
            swap(nums,0,i);
            //剩余元素重新建堆,剩余元素为数组中0到i的元素
            heapify(nums,0,i);//这里不能用Arrays.copyOf的原因是,这个方法传回的是新的数组对象,不会影响原来的数组,所以必须再原数组上进行操作,不能复制。
        }
        return nums;
    }
    public void buildMaxHeap(int[] arr){
     
        int len = arr.length;
        //根节点在前面,叶子节点在后面,从最后一个非叶子节点len/2-1来进行构建
        for(int i = len/2-1; i >=0; i--){
     
            heapify(arr,i,len);//维护堆的性质,父节点不小于左结点,不小于右节点
        }
    }
    //len为当前维护堆性质的堆的范围:在arr数组中从0到len
    public void heapify(int[] arr, int i,int len){
     
        //i的左右结点
        int left = (i+1)*2-1;
        int right = (i+1)*2;
        //将其中比i大的子节点放在堆顶,即与i交换位置,只要有一个比他大就行,无论谁
        int largest = i;
        if(left < len){
     
            if(right < len){
     
                if(arr[left]<arr[right]) left = right;
            }
            if(arr[left] > arr[largest]) largest = left;
        }
        //判断是否有比i大的,即largest是否还是i
        if(largest!=i){
     
            swap(arr,largest,i);//将较大的子节点放在堆顶
            heapify(arr,largest,len);//将换过来的原来的堆顶,现在作为子节点继续往下维护堆的性质
        }

    }
    public void swap(int[] arr, int p, int q){
     
        int tmp = arr[p];
        arr[p] = arr[q];
        arr[q] = tmp;
    }
}

十种排序算法详解&Java实现(Leetcode | 912. 排序数组 )_第6张图片

归并排序

七、归并排序

原理详解

 归并排序(Merge Sort)是典型的分治法的应用,运用归并操作进行排序。先将序列平分divide递归子序列,递归后结果子序列已有序,然后合并两个有序子序列merge。基本原理是将几个有序的子序列合并得到整体的有序的序列。即先使子序列有序,再合并有序子序列。若是两个有序子序列合并,就叫2-路归并。n个有序子序列合并,就是n-路归并。
 归并排序因为不涉及相同元素的相对位置变化,所以是稳定的排序。归并排序比较占用内存,但却是一种效率高且稳定的算法,速度仅次于快速排序。

2-路归并排序算法:
 1、Divide:将长度为n序列平分为两个n/2的序列。
 2、递归:对两个子序列进行归并排序(默认升序)。
 3、Merge:对两个已经排序好的子序列进行合并,具体操作就是各自从第一个元素开始比较,将较小的挑出来,即当前合并序列中最小的,较大的那个与较小那个后面的元素继续比较,直到比较到子序列的末尾。

时间复杂度:O(nlog2n)~O(n)
空间复杂度:原地归并为O(1),利用临时数组存储归并结果为O(n)(临时的数组和递归时压入栈的数据占用的空间:n + logn)

Java实现

//这里用的是时间复杂度O(nlogn),空间复杂度O(n)的写法
class Solution {
     
    public int[] sortArray(int[] nums) {
     
        //归并排序
        if(nums == null || nums.length == 0) return nums;
        mergesort(nums, 0, nums.length-1);
        return nums;
    }
    public int[] mergesort(int[] nums, int l, int r){
     
        //divide
        int mid = l+(r-l)/2;
        if(l<r){
     
            mergesort(nums,l,mid);
            mergesort(nums,mid+1,r);
            merge(nums,l,mid,r);//表示将l到mid和mid+1到r两个有序子序列合并有序
        }
        
        return nums;
    }
    public void merge(int[] nums,int l, int mid, int r){
     
        //临时数组存储合并结果
        int[] tmp = new int[r-l+1];
        int k = 0;//临时数组的元素
        int i = l, j = mid+1;//两个子数组的起始指针
        while(i<=mid && j <= r){
     
            //两子序列从开头依次比较,将较小的取出放入合并结果tmp中
            if(nums[i]<nums[j]){
     
                tmp[k] = nums[i];
                i++;
            }else{
     
                tmp[k] = nums[j];
                j++;
            }
            k++;
        }
        while(i<=mid){
     //比较结束,左子序列还有剩余的,那么右子序列就没有剩余的,把左子序列剩余的加入结果
            tmp[k++] = nums[i++];//i++是先做别的事再++,++i是先++再做别的事,所以++i可以做左值

        }
        while(j<=r){
     //右子序列有剩余
            tmp[k++] = nums[j++];
        }
        //把临时数组即合并结果,加入原数组中,更改原数组
        for(i = 0; i < tmp.length; i++){
     
            nums[l+i] = tmp[i];
        }
    }
}

十种排序算法详解&Java实现(Leetcode | 912. 排序数组 )_第7张图片

非比较类排序

八、计数排序

原理详解

 计数排序(Counting Sort)不是基于比较的排序。它的优势在于对一定范围内的正数排序时,复杂度为O(n+k),k为整数范围,比任何比较排序算法都快,线性时间复杂度。基本思想是将输入的数据值转化为键存储在额外开辟的数组空间中。这也是牺牲空间换取时间的算法。而且当O(k)>O(nlog2n)时其效率并不如比较排序,因为比较排序的时间复杂度理论上下限为O(nlog2n)。

计数排序算法:
 1、确定范围k: 遍历一遍序列找出待排序序列中最大和最小的元素。(O(n))
 2、计数: 再遍历一遍序列,统计每个元素出现的次数,并存入临时数组C[]中,C[i]表示元素i在待排序序列中出现的次数。(因为C的下标是升序的且排序序列中元素是对应C的下标的,所以我们取出时自然也是排序的。)(O(k))
 3、累加: 对所有的计数累加,即从C的第一个元素开始,每一项与前一项相加,即C[i]存储待排序序列中比元素i小的元素个数。(计数排序的核心思想:对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数。此处不是通过比较,而是通过计数和计数累加获得)
 4、反向填充目标数组: 新建立一个数组B[]存储排序后序列,将每个元素放在B的C[i]处,即B[C[i]] = i,每放一个元素C[i]-1。因为元素i可能不止一个。(或者没有第三步的累加的话,从第二步已知各个元素的出现个数就可以开始按序填充数组了)

时间复杂度:O(n+k),k表示输入的元素是0到k之间的整数。
空间复杂度:O(n+k)

Java实现

class Solution {
     
    public int[] sortArray(int[] nums) {
     
        //计数排序
        if(nums == null || nums.length == 0) return nums;
        //1、确定范围
        int min = Integer.MAX_VALUE;
        int max = Integer.MIN_VALUE;
        for(int i = 0; i < nums.length; i++){
     
            min = nums[i]<min?nums[i]:min;
            max = nums[i]>max?nums[i]:max;
        }
        //2、计数 k = max-min+1;
        int[] c = new int[max-min+1];
        for(int i = 0; i < nums.length; i++){
     
            c[nums[i]-min] +=1;//优化了数组c的大小  以min作为0下标,一次类推,最大值min+k-1 = max
        }
        //没有第三步的写法
        //4、填数 c[i]存储的是序列中i元素的个数,直接依次填充
        int j = 0;
        for(int i = 0; i < c.length; i++){
     
            while(c[i]>0){
     
                nums[j++] = i+min;//这里nums[j] = i+min后再j++;
                c[i]--;
            }
        }
        return nums;
        //有第三步的写法
        // //3、累加
        // for(int i = 1; i < c.length; i++){
     
        //     c[i] = c[i]+c[i-1];
        // }
        // //4、填数 c[i]存储的是序列中小于等于i元素的个数,所以从后往前填
        // int[] b = new int[nums.length];
        // for(int i = nums.length-1; i >= 0; i--){
     
        //     b[--c[nums[i]-min]] = nums[i];
        // }
        // return b;
    }

}

在这里插入图片描述

九、桶排序

原理详解

 桶排序(Bucket Sort)是计数排序的升级版,利用了函数的映射关系,高效与否取决于映射关系。其基本原理是:将数组分到有限数量的桶中,每个桶进行排序(可能使用别的排序算法或是递归使用桶排序)。当待排序序列的元素数值为均匀分配的时候,桶排序使用线性时间O(n)。(源自百度百科:桶排序是鸽巢排序的一种归纳结果。鸽巢排序就是计数排序的低配版?计数排序的计数数组是min到max,鸽巢排序的计数数组是0到max,所以当排序序列中出现很多不相等元素时,鸽巢排序效率很低)
桶排序其实就是将元素分为不同的子序列即桶,这里的子序列和归并排序的子序列不同的是,这里的子序列在划分的时候已经按范围排序了,比如第一个子序列的最大值是比第二个子序列最小值要小。然后再对子序列即桶内进行排序,因为分子序列时已经按范围排序分好,所以子序列排好序之后将排序后的子序列拼接起来就是最终排序序列了。

桶排序算法:
 1、划分子序列即空桶: 设置一个定量的数组当作空桶,即把待排序序列的范围平分为n份,放在空桶数组的0到n-1位置上。比如0-9放在0位置,10-19放在1,20-29放在2…依次类推
 2、映射: 遍历输入数据,将那个数据一个个放到对应的桶里即对应的范围内。
 3、对桶排序: 对每个不空的桶进行排序。
 4、拼接: 从不空的桶里将排好序的数据拿出来拼接一起。

时间复杂度:平均O(N+C),C = N*(logN-logM),M为桶数量。
 O(N)+O(M*(N/M)* log(N/M))=O(N+N*(logN-logM))=O(N+N*logN-N*logM)
 桶排序最好情况下是O(N),取决于各个桶内数据进行排序的时间复杂度。桶划分的越小,各个桶内数据越少,排序时间越少,但是相应空间消耗也会增大。
空间复杂度:O(N+M)

Java实现

class Solution {
     
    public int[] sortArray(int[] nums) {
     
        //桶排序
        if(nums == null || nums.length == 0) return nums;
        //1、确定范围
        int min = Integer.MAX_VALUE;
        int max = Integer.MIN_VALUE;
        for(int i = 0; i < nums.length; i++){
     
            min = nums[i]<min?nums[i]:min;
            max = nums[i]>max?nums[i]:max;
        }
        //2、设置桶,计算桶数量M 桶间隔为nums.length = N/M
        int bucketcount = (max-min)/nums.length+1;
        ArrayList<ArrayList<Integer>> bucketarr = new ArrayList<>();
        for(int i = 0; i < bucketcount; i++){
     
            bucketarr.add(new ArrayList<Integer>());
        }
        //3、遍历数组,将对应的数据放入对应的桶中 O(N) 利用映射函数将数据分配到各个桶中
        for(int i = 0; i < nums.length; i++){
     
            bucketarr.get((nums[i]-min)/nums.length).add(nums[i]);
        }
        //4、对每个桶进行排序 O(N/M*log(N/M)) * M
        int index = 0;
        for(int i = 0; i < bucketarr.size();i++){
     
            if(bucketarr.get(i).size()==0) continue;
            Collections.sort(bucketarr.get(i));
            //5、并依次取出 拼接
            for(int j = 0; j < bucketarr.get(i).size();j++){
     
                nums[index++] = bucketarr.get(i).get(j);
            }
        }
        return nums;
    }

}

在这里插入图片描述

十、基数排序

原理详解

 基数排序(radix sort)属于分配式排序,又称桶子法。基本原理是按低位先排序,然后收集(比如先按个位数排序);再按照高位排序,再收集(再按十位数排序,以此类推),直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。(比如先按个位数排序,再按十位数排序,最后的结果就是十位数小的在前,十位数相同的个位数小的在前)
 基数排序基于分别排序,分别收集,所以是稳定排序。是一种多关键字排序算法,就是基于多个关键字进行多次排序,我们可以把关键字理解为位数。

基数排序算法:
 1、确定范围:取得待排序序列中最大数并获取其位数,即最高位。
 2、对原始序列从最低位开始,依次对整个数组按照当前位进行一次排序(采用计数排序,因为计数排序适用于小范围数)。直到排序到最高位。

 基数排序因为按照位数进行分词排序,所以存在无法处理负数排序的情况,解决方法有,将所有的数加上一个正数,使得所有数都是正数之后进行基数排序,最后输出的时候所有元素再减去这个正数即可。

时间复杂度:O(d*2n),d为位数,即关键字。详解见注释。
空间复杂度:O(n+k),k为桶的数量,一般来说n>>k。

Java实现

class Solution {
     
    public int[] sortArray(int[] nums) {
     
        //基数排序
        if(nums == null || nums.length == 0) return nums;
        //1、确定范围 最高位
        int min = Integer.MAX_VALUE;
        int max = Integer.MIN_VALUE;
        for(int i = 0; i < nums.length; i++){
     
            max = nums[i]>max?nums[i]:max;
            min = nums[i]<min?nums[i]:min;
        }
        //判断有负数情况,如果有负数,将所有数据加上一个正数使得最小的负数也变成正数
        if(min<0){
     
            for(int i = 0; i < nums.length; i++){
     
                nums[i] = nums[i]-min;
            }
            max = max-min;
        }
        int mod = 1;//位数从小到大,取1,10,100...
        int[][] bucket = new int[10][nums.length];//桶,用来存放当前位为i时的元素,下标为出现次数,bucket[i]为当前位为i时的元素序列
        int[] c = new int[10];//用来记录每个桶中有几个数据 计数
        //对每位数mod来计算,依次进行排序 比如有d个位数,需要d次排序
        while(mod<=max){
     
            //把当前数据按照顺序存到桶中 计数排序
            //每次排序,O(n)
            for(int num : nums){
     
                int digit = (num/mod)%10;//当前位的数字
                bucket[digit][c[digit]] = num;
                c[digit]++;
            }
            //一次计数排序结束,将数据从桶中放入数组,即收集
            //排序完收集O(n)
            int k = 0;
            for(int i = 0; i < c.length; i++){
     
                if(c[i]!=0){
     
                    for(int j = 0; j < c[i]; j++){
     
                        nums[k++] = bucket[i][j];
                    }
                }
                //遍历完一个桶,计数器清零
                c[i] = 0;
            }
            mod *= 10;//比较下一位 再进行一次排序
        }
        //将所有数再恢复原状
        if(min < 0){
     
            for(int i = 0; i < nums.length; i++){
     
                nums[i] += min;
            }
        }
        return nums;
    }
}

在这里插入图片描述

复杂度总结

图片来自维基百科
十种排序算法详解&Java实现(Leetcode | 912. 排序数组 )_第8张图片
比较类排序中
时间复杂度好的:快排、归并、堆排序
占额外空间的:归并

参考:
[1] 十大经典排序算法(动图演示)
[2] 十大经典排序算法的java实现以及原理讲解
[3] 冒泡排序及优化详解
[4] 各个排序的百度百科

你可能感兴趣的:(Java,Leetcode,java,排序算法)