1. 总览

六种常见排序算法的复杂度和稳定性:

稳定性指的是对于相等的元素,排序前后能够保证这些元素的相对次序不变,如[1-A, 2-B, 3-C, 2-D], 字母仅仅表示一个相对次序,稳定性的排序结果为[1-A, 2-B, 3-C, 2-D],非稳定性的排序结果为**[1-A, 2-D, 3-C, 2-B]
排序算法最坏时间平均时间稳定性
冒泡排序 (BubbleSort)O(N2)O(N2)稳定
选择排序 (SelectSort)O(N2)O(N2)不稳定
插入排序 (InsertSort)O(N2)O(N2)稳定
归并排序 (MergeSort)O(NlogN)O(NlogN稳定
快速排序 (quickSort)O(N2)O(NlogN)不稳定
堆排序 (heapSort)O(NlogN)O(NlogN)不稳定

2. 冒泡排序 BubbleSort

比较简单的一种排序算法,主要思想为:每次都从0位置开始,比较当前元素和下一个元素的大小,如果逆序则进行交换。每次比对都相当于遍历一次数组的无序部分,并将最大的元素传递到队尾,排序的过程很像冒泡一样。

public class sort_bubbleSort {
    public static void main(String[] args) {
        int[] arr = ArrayGenerator.array(20, 20);  // 随机数组生成器
        System.out.println(Arrays.toString(arr));
        bubbleSort(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void bubbleSort(int[] arr){
        for(int i = 0; i < arr.length; i++){
            boolean shutdown = true;   // 加状态位提前结束
            for(int j = 0; j < arr.length - i - 1; j++){
                if(arr[j] > arr[j+1]){
                    shutdown = false;
                    swap(arr, j, j + 1);
                }
            }
            if(shutdown){
                break;
            }
        }
    }

    public static void swap(int[] arr, int i, int j){
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
}

3. 选择排序 SelectSort

算法思想:每次从当前无序序列中选择最大值(最小值)交换到无序序列的队尾(队首)。每一次的选择最大值(最小值)需要遍历一次无序部分,要做n-1次选择,因此时间复杂度相当于1-n之间的连续数相加,自然为O(N2)。但与冒泡排序相比,在最坏情况,每一次的外循环,选择排序只会做一次交换和n次比较;而冒泡排序则要做n次比较和n次交换,因此时间复杂度虽然一个等级,选择排序的实际耗时要小于冒泡排序

因为该算法每次比对都需要遍历到无序部分的末尾,当存在多个最大值时,后面的最大值会覆盖前面的(即选最后的最大值进行交换),因此是不稳定的排序

public class sort_selectSort {
    public static void main(String[] args) {
        int[] arr = ArrayGenerator.array(20, 20);
        System.out.println(Arrays.toString(arr));
        selectSort(arr);
        System.out.println(Arrays.toString(arr));
    }
    
    public static void selectSort(int[] arr){
        for(int i = 0; i < arr.length; i++){
            int minIndex = i;
            for(int j = i; j < arr.length; j++){
                minIndex = arr[minIndex] > arr[j] ? j : minIndex;
            }
            swap(arr, minIndex, i);
        }
    }
    
    public static void swap(int[] arr, int i, int j){
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
}

4. 插入排序 InsertSort

算法思想:将数组分为有序部分(左边)和无序部分(右边),对于右边无序部分的元素,将其与有序部分进行依次比较,直到找到自己应该插入的位置进行插入。
插入排序每一次外循环最坏情况要进行的交换次数会大于选择排序,但在最好情况下,插入排序能实现O(N)的时间复杂度,因此平均来看,插入排序的性能是要优于选择排序的

public class sort_InsertSort {
    public static void main(String[] args) {
        int[] arr = ArrayGenerator.array(20, 20);
        System.out.println(Arrays.toString(arr));
        insertSort(arr);
        System.out.println(Arrays.toString(arr));
    }
    
    public static void insertSort(int[] arr){
        for(int i = 1; i < arr.length; i++){
            for (int j = i - 1; j >= 0 && arr[j + 1] < arr[j]; j--) {
                swap(arr, j, j + 1);
            }
            
    public static void swap(int[] arr, int i, int j){
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
}

PLUS:上面3中算法比较基础,都是单一的交换排序思想,比较low,不会涉及到与其他算法思想的结合。而下面三种算法,每种算法都是交换排序算法与其他算法思想或数据结构的结合,因此能够衍生出许多变种问题!

5. 归并排序 MergeSort

引入算法思想:二分思想
思路:将数组不断进行二分,直到不可分(单元素),分割的时间复杂度为O(logN),对分开的左右部分进行归并(merge)。因为分割的最小部分为单元素,而单元素自然有序,因此问题就成为了对两个有序序列的合并问题,合并过程的复杂度为O(N)。因此总复杂度就退化为O(NlogN)

归并排序降低排序时间复杂度需要付出空间复杂度的代价,在合并过程中,必须申请额外的数组空间。

public class sort_mergeSort {
    public static void main(String[] args) {
        int[] arr = ArrayGenerator.array(20, 20);
        System.out.println(Arrays.toString(arr));
        mergeSort(arr, 0, arr.length - 1);
        System.out.println(Arrays.toString(arr));
    }
    
    public static void mergeSort(int[] arr, int L, int R){
        if(L >= R){
            return;
        }
        int mid = L + (R - L) / 2;
        mergeSort(arr, L, mid);
        mergeSort(arr, mid + 1, R);
        merge(arr, L, mid, R);
    }
    
    public static void merge(int[] arr, int L, int mid, int R){
        int l = L;
        int r = mid + 1;
        int[] tmp = new int[R - L + 1];
        int i = 0;

        while(l <= mid && r <= R){
            tmp[i++] = arr[l] > arr[r] ? arr[r++] : arr[l++];
            // 这里可以加一些业务逻辑
        }

        while(l <= mid){
            tmp[i++] = arr[l++];
        }
        while(r <= R){
            tmp[i++] = arr[r++];
        }

        for(int k = 0; k <= R - L; k++){
            arr[L + k] = tmp[k];
        }
    }
    
    public static void swap(int[] arr, int i, int j){
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
}

归并排序会衍生出一些算法问题。因为归并排序时稳定的,且时间复杂度降到了O(NlogN), 因此一些需要进行对数组进行双层循环且对元素的次序性要求严格的问题都可以使用归并排序进行优化
衍生问题:无序数组的逆序对数量

6. 快速排序 QuickSort

引入的算法思想:分组思想 + 二分思想
分组思想就是对一个数组按target进行分组,数组中大于target的值放在左边,小于target的数放在右边。
快速排序的流程就是不停地对数组进行分组,第一次分组后,对大于target部分和小于target的部分继续进行分组,直到数组完全有序

public class sort_quickSort {
    public static void main(String[] args) {
        int[] arr = ArrayGenerator.array(20, 20);
        System.out.println(Arrays.toString(arr));
        quickSort(arr, 0, arr.length - 1, arr.length - 1);
        System.out.println(Arrays.toString(arr));
    }
    
    public static void quickSort(int[] arr, int L, int R, int x) {
        if( L >= R){
            return;
        }
        int[] info = partition(arr, L, R, x);
        quickSort(arr, L, info[0], info[0]);
        quickSort(arr, info[1], R, R);
    }
    
    public static int[] partition(int[] arr, int L, int R, int x){
        // 返回分组后左部分的尾索引和右部分的头索引
        int l = L - 1, r = R + 1;
        int i = L;
        int target = arr[x];
        while(i < r){
            if(arr[i] < target){
                swap(arr, i++, ++l);
            }else if(arr[i] > target){
                swap(arr, i, --r);
            }else{
                i++;
            }
        }
        return new int[]{l,r};
    }
    
    public static void swap(int[] arr, int i, int j){
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
}

衍生问题:荷兰国旗问题(实质上就是partition过程)、随机快排(随机选择target)

7. 堆排序 HeapSort

引入数据结构:堆(最大堆)
堆实质上是完全二叉树的一种结构,有最大堆和最小堆,以最大堆为例,最大堆的特点就是根节点的值一定大于左孩子和右孩子,因此一个最大堆结构中,节点的最大值就是根节点的值
堆排序实质上就是选择排序的思想,不同的是,选择的过程使用堆这个数据结构来进行优化。一个节点为N的堆插入一个新节点,维护的时间复杂度只需要O(logN),而对于一个有N个元素的有序数组,插入一个新元素,将要花费O(N)的时间

堆排序流程:

  • 首先所有元素入堆(最大堆),堆的根节点就是数组的最大值
  • 将堆的顶点和数组的末尾元素进行交换,并重新维护堆
  • 因为数组末尾已有序,因此堆的规模 - 1
  • 重复上述过程,直到堆的规模为 1

PLUS:堆作为一种完全二叉树,满足以下特性:

  • 索引为 i 的节点其如果有左孩子,则左孩子索引为 2 * i + 1
  • 索引为 i 的节点其如果有右孩子,则右孩子索引为 2 * i + 2
public class sort_heapSort {
    public static void main(String[] args) {
        int[] arr = ArrayGenerator.array(20, 20);
        System.out.println(Arrays.toString(arr));
        heapSort(arr, arr.length);
        System.out.println(Arrays.toString(arr));
    }

    public static void heapSort(int[] arr, int heapSize){
        for(int i = 0; i < heapSize; i++){
            buildHeap(arr, i);
        }
        while(heapSize > 0){
            swap(arr, 0, --heapSize);
            reHeap(arr, 0, heapSize);
        }
    }

    public static void buildHeap(int[] arr, int index){  
        // 每次加入一个节点到末尾
        while(arr[index] > arr[(index - 1) / 2]){
            swap(arr, index, (index - 1) / 2);
            index = (index - 1) / 2;
        }
    }

    public static void reHeap(int[] arr, int index, int heapSize){   // 当顶点变化时,重新维护堆
        int L ;
        while((L = index * 2 + 1) < heapSize ){
            int maxIndex = L + 1 < heapSize && arr[L + 1] > arr[L] ? L + 1 : L;
            if(arr[maxIndex] > arr[index]){
                swap(arr, index, maxIndex);
                index = maxIndex;
            }else{
                break;
            }

        }
    }

    public static void swap(int[] arr, int i, int j){
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
}

衍生问题:实质上应该反过来说,堆排序仅仅是堆结构的一个衍生应用而已,堆结构经常应用于维护一个动态序列的最大值和最小值,因为无论是插入还是取出,堆结构的reHeap过程都是O(logN)
常见需要用到堆的问题:合并K个有序数组或者链表、一些贪心问题、需要维护动态序列的最大最小值问题


Chord_Gll
51 声望74 粉丝

每一个领跑者,都曾是优秀而虔诚的追赶者


引用和评论

0 条评论