【算法】一文带你搞定面试必会五大排序算法(java)

73 阅读8分钟

冒泡排序

冒泡排序(Bubble Sort)基本思想:

经过多次迭代,通过相邻元素之间的比较与交换,使值较小的元素逐步从后面移到前面,值较大的元素从前面移到后面。

  • 最佳时间复杂度O(n)O(n)。初始序列已经为有序序列。

  • 最坏时间复杂度O(n2)O(n^2)。初始时序列已经是降序排列,或者最小值元素处在序列的最后。

  • 冒泡排序适用情况:数据量较小的情况,尤其是当序列的初始状态为基本有序的情况。

  • 排序稳定性:交换相邻元素,不会改变相等元素的相对顺序,稳定排序算法

class Solution {
    public int[] sortArray(int[] nums) {
        int len =nums.length;
        for(int i=0;i<len-1;i++){
            boolean flag = true;
            for(int j=0;j<len-i-1;j++){
                if(nums[j]>nums[j+1]){
                    int temp = nums[j];
                    nums[j] = nums[j+1];
                    nums[j+1] = temp;
                    flag=false;
                }
            }
            if(flag){
                break; //没有发生交换,已经有序了
            }
        }
        return nums;
    }
}

快速排序

快速排序(Quick Sort)基本思想:

采用经典的分治策略,选择数组中某个元素作为基准数,通过一趟排序将数组分为独立的两个子数组,一个子数组中所有元素值都比基准数小,另一个子数组中所有元素值都比基准数大。然后再按照同样的方式递归的对两个子数组分别进行快速排序,以达到整个数组有序。

快速排序算法的时间复杂度主要跟基准数的选择有关。

  • 最佳时间复杂度O(n×logn)O(n \times \log n)。每一次选择的基准数都是当前数组的中位数,此时算法时间复杂度满足的递推式为 T(n)=2×T(n2)+Θ(n),由主定理可得 T(n)=O(n×log⁡n)。
  • 最坏时间复杂度O(n2)O(n^2)。每一次选择的基准数都是数组的最终位置上的值(初始时已经有序),此时算法时间复杂度满足的递推式为 T(n)=T(n−1)+Θ(n),累加可得 T(n)=O(n2)。
  • 平均时间复杂度O(n×logn)O(n \times \log n)。在平均情况下,每一次选择的基准数可以看做是等概率随机的。其期望时间复杂度为 O(n×log⁡n)。
  • 空间复杂度O(n)O(n)。无论快速排序算法递归与否,排序过程中都需要用到堆栈或其他结构的辅助空间来存放当前待排序数组的首、尾位置。最坏的情况下,空间复杂度为 O(n)。如果对算法进行一些改写,在一趟排序之后比较被划分所得到的两个子数组的长度,并且首先对长度较短的子数组进行快速排序,这时候需要的空间复杂度可以达到 O(log2n)。
  • 排序稳定性:在进行哨兵划分时,基准数可能会被交换至相等元素的右侧。因此,快速排序是一种 不稳定排序算法
class Solution {
    public int[] sortArray(int[] nums) {
        sort(nums,0,nums.length-1);
        return nums;
    }
    public void sort(int[] nums,int l,int r){
        if(l>=r){
            return;
        }
        int p = partition(nums,l,r);
        sort(nums, l, p-1); //基准数放到了正确的位置
        sort(nums, p+1, r);
    }

    int partition(int[] nums,int l,int r){
        int pivot = nums[l];
        int i=l+1,j=r;
        while(i<=j){
            while(i<r && nums[i]<=pivot) i++;
            while(j>l && nums[j]>pivot) j--;
            if(i>=j){  //直到ij相遇
                break;
            }
            swap(nums,i,j);
        }
        swap(nums,l,j);
        return j;
    }

    void swap(int[] nums,int l,int r){
        int temp=nums[l];
        nums[l]=nums[r];
        nums[r]=temp;
    }
}

归并排序

归并排序(Merge Sort)基本思想:

采用经典的分治策略,先递归地将当前数组平均分成两半,然后将有序数组两两合并,最终合并成一个有序数组。

  • 时间复杂度O(n×logn)O(n \times \log n)。归并排序算法的时间复杂度等于归并趟数与每一趟归并的时间复杂度乘积。子算法 merge 的时间复杂度是 O(n)。
  • 空间复杂度O(n)O(n)。归并排序方法需要用到与参加排序的数组同样大小的辅助空间。因此,算法的空间复杂度为 O(n)。
  • 排序稳定性:因为在两个有序子数组的归并过程中,如果两个有序数组中出现相等元素,merge(left_nums, right_nums): 算法能够使前一个数组中那个相等元素先被复制,从而确保这两个元素的相对顺序不发生改变。因此,归并排序算法是一种 稳定排序算法
class Solution {
    int[] temp;
    public int[] sortArray(int[] nums) {
        temp = new int[nums.length];
        sort(nums,0,nums.length-1);
        return nums;
    }
    public void sort(int[] nums,int l,int r){
        if(l==r){ //缩小到一个元素
            return;
        }
        int mid = l+(r-l)/2;
        sort(nums,l,mid);
        sort(nums,mid+1,r);
        merge(nums,l,mid,r);
    }

    void merge(int[] nums,int l,int mid,int r){
        for(int i=l;i<=r;i++){ //复制一份
            temp[i]=nums[i];
        }
        int i=l,j=mid+1;
        for(int k=l;k<=r;k++){ //整理nums的[l,r]部分
            if(i==mid+1){ //[l,mid]部分整理完了
                nums[k]=temp[j++];
            }else if(j==r+1){ //[mid,r]部分整理完了
                nums[k]=temp[i++];
            }else if(temp[i]>temp[j]){ //都没完,放入小的
                nums[k]=temp[j++];
            }else{
                nums[k]=temp[i++];
            }

        }
    }
}

插入排序

插入排序(Insertion Sort)基本思想:

将数组分为两个区间:左侧为有序区间,右侧为无序区间。每趟从无序区间取出一个元素,然后将其插入到有序区间的适当位置。

插入排序在每次插入一个元素时,该元素会在有序区间找到合适的位置,因此每次插入后,有序区间都会保持有序。

  • 最佳时间复杂度O(n)O(n)。最好的情况下(初始时区间已经是升序排列),每个元素只进行一次元素之间的比较,因而总的比较次数最少,为 ∑i=2n1=n−1,并不需要移动元素(记录),这是最好的情况。
  • 最差时间复杂度O(n2)O(n^2)。最差的情况下(初始时区间已经是降序排列),每个元素 nums[i] 都要进行 i−1 次元素之间的比较,元素之间总的比较次数达到最大值,为 ∑i=2n(i−1)=n(n−1)2。
  • 平均时间复杂度O(n2)O(n^2)。如果区间的初始情况是随机的,即参加排序的区间中元素可能出现的各种排列的概率相同,则可取上述最小值和最大值的平均值作为插入排序时所进行的元素之间的比较次数。
  • 空间复杂度O(1)O(1)。插入排序算法为原地排序算法,只用到指针变量 i、jj 以及表示无序区间中第 1 个元素的变量等常数项的变量。
  • 排序稳定性:在插入操作过程中,每次都讲元素插入到相等元素的右侧,并不会改变相等元素的相对顺序。因此,插入排序方法是一种 稳定排序算法
class Solution {
    public int[] sortArray(int[] nums) {
        for(int i=1;i<nums.length;i++){
            int temp = nums[i];
            int j=i;
            while(j>0 && nums[j-1]>temp){ //从右到左便利有序区间
                nums[j]=nums[j-1]; //给temp腾位置
                j--;
            }
            nums[j] = temp;
        }
        return nums;
    }
}

希尔排序

希尔排序(Shell Sort)基本思想:

将整个数组切按照一定的间隔取值划分为若干个子数组,每个子数组分别进行插入排序。然后逐渐缩>小间隔进行下一轮划分子数组和对子数组进行插入排序。直至最后一轮排序间隔为 1,对整个数组进行插入排序。

  • 时间复杂度:介于 O(n×log2⁡n) 与 O(n2) 之间。

    • 希尔排序方法的速度是一系列间隔数 gapi 的函数,而比较次数与 gapi 之间的依赖关系比较复杂,不太容易给出完整的数学分析。
    • 本文采用 gapi=⌊gapi−1/2⌋ 的方法缩小间隔数,对于具有 n 个元素的数组,如果 gap1=⌊n/2⌋,则经过 p=⌊log2⁡n⌋ 趟排序后就有 gapp=1,因此,希尔排序方法的排序总躺数为 ⌊log2⁡n⌋。
    • 从算法中也可以看到,外层 while gap > 0 的循环次数为 log⁡n 数量级,内层插入排序算法循环次数为 n 数量级。当子数组分得越多时,子数组内的元素就越少,内层循环的次数也就越少;反之,当所分的子数组个数减少时,子数组内的元素也随之增多,但整个数组也逐步接近有序,而循环次数却不会随之增加。因此,希尔排序算法的时间复杂度在 O(n×log2⁡n) 与 O(n2) 之间。
  • 空间复杂度O(1)O(1)。希尔排序中用到的插入排序算法为原地排序算法,只用到指针变量 i、jj 以及表示无序区间中第 1 个元素的变量、间隔数 gap 等常数项的变量。

  • 适用场景:希尔排序时直接插入排序的优化版,解决了直接插入排序在面对大量数据时的效率低问题。希尔排序适用于大规模无序数组的排序,且相对于直接插入排序数组越大优势越大。

  • 排序稳定性:在一次插入排序是稳定的,不会改变相等元素的相对顺序,但是在不同的插入排序中,相等元素可能在各自的插入排序中移动。因此,希尔排序方法是一种 不稳定排序算法

class Solution {
    public int[] sortArray(int[] nums) {
        for(int gap=nums.length/2;gap>0;gap/=2){
            for(int i=gap;i<nums.length;i++){
                int temp=nums[i];
                int j;
                for(j=i-gap;j>=0 && nums[j]>temp;j-=gap){ //-gap才是一组的
                    nums[j+gap] = nums[j];
                }
                nums[j+gap]=temp;
            }
        }
        return nums;
    }
}