排序算法总结

冒泡排序

import java.util.Arrays;

import static sort.Logarithm.*;

//冒泡排序:时间复杂度O(N^2),额外空间复杂度O(1)
public class BubbleSort {

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

    //交换arr的i和j位置上的值
    private static void swap(int[] arr, int i, int j) {
        arr[i] = arr[i] ^ arr[j];
        arr[j] = arr[i] ^ arr[j];
        arr[i] = arr[i] ^ arr[j];
    }

}

 选择排序

import java.util.Arrays;

import static sort.Logarithm.*;

//选择排序:时间复杂度O(N^2),额外空间复杂度O(1)
public class SelectionSort {

    public static void selectionSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        for (int i = 0; i < arr.length - 1; i++) {  // i ~ N - 1
            int minIndex = i;
            for (int j = i + 1; j < arr.length; j++) {  // i ! N - 1上找最小值的下标
                minIndex = arr[j] < arr[minIndex] ? j : minIndex;
            }
            swap(arr, i, minIndex);
        }
    }

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

}

       冒泡排序和选择排序是较为基础的排序算法,容易理解,每次挑选出一个最大(最小)的数进行交换,使得数组中第 0 ~ i 位置上的元素有序。因其每次循环都会进行 n - i 次比较(n为数组内元素个数,i为循环次数),且不会保留之前的比较结果,导致算法效率较低。时间复杂度为O(N^2)。冒泡和选择排序不需要开辟额外的空间进行辅助计算,故空间复杂度为O(1)。

插入排序

import java.util.Arrays;

import static sort.Logarithm.*;

//插入排序:时间复杂度O(N^2),额外空间复杂度O(1)
//算法流程按照最差情况来估计时间复杂度
public class InsertionSort {

    public static void insertionSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        for (int i = 1; i < arr.length; i++) {  // 0 ~ i 做到有序
            for (int j = i; j > 0; j--) {
                if (arr[j] < arr[j - 1]) {
                    swap(arr, j, j - 1);
                }
            }
        }
    }

    //i和j是一个位置的话,会出错
    private static void swap(int[] arr, int i, int j) {
        arr[i] = arr[i] ^ arr[j];
        arr[j] = arr[i] ^ arr[j];
        arr[i] = arr[i] ^ arr[j];
    }

}

       插入排序使得数组在 0 ~ i 范围上保持有序,将数组中 i 位置后面的元素与前面的有序序列进行比较,逐个插入到合适的位置上。实现思路与冒泡和选择排序类似,但实现细节略有不同。时间复杂度为O(N^2),空间复杂度为O(1)。

归并排序

import java.util.Arrays;

import static sort.Logarithm.*;

//归并排序:时间复杂度O(N*logN),额外空间复杂度O(N)
public class MergeSort {

    public static void mergeSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        process(arr, 0, arr.length - 1);
    }

    public static void process(int[] arr, int L, int R) {
        if (L == R) {
            return;
        }
        int mid = L + ((R - L) >> 1);
        process(arr, L, mid);
        process(arr, mid + 1, R);
        merge(arr, L, mid, R);
    }

    public static void merge(int[] arr, int L, int M, int R) {
        int[] help = new int[R - L + 1];
        int i = 0;
        int p1 = L;
        int p2 = M + 1;
        while (p1 <= M && p2 <= R) {
            help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
        }
        while (p1 <= M) {
            help[i++] = arr[p1++];
        }
        while (p2 <= R) {
            help[i++] = arr[p2++];
        }
        for (i = 0; i < help.length; i++) {
            arr[L + i] = help[i];
        }
    }

}

       归并排序使用递归的方式不断将数组分为大小相同的两部分,分别进行排序合并,最终使得整个数组有序。其时间复杂度可以使用master公式 T(N) = a * T(b / N) + O(N ^ d) 来计算,其中a为调用次数,b为子问题规模,O(N ^ d)表示除了子问题调用,剩下的时间复杂度。该算法中调用次数为两次,子问题规模为数组总长度的一半,额外的时间复杂度为O(N),故a = 2,b = 2,d = 1。根据log(b, a) = d,可以得出归并排序的时间复杂度为O(N*logN)。为了合并数组,归并排序需要新建一个长度为数组长度的辅助数组,故空间复杂度为O(N)。

快速排序

import java.util.Arrays;

import static sort.Logarithm.*;

//快速排序:时间复杂度O(N*logN),额外空间复杂度O(logN)
public class quickSort {

    public static void quickSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        quickSort(arr, 0, arr.length - 1);
    }

    // arr[L..R]排好序
    public static void quickSort(int[] arr, int L, int R) {
        if (L < R) {
            swap(arr, L + (int) (Math.random() * (R - L + 1)), R);
            int[] p = partition(arr, L, R);
            quickSort(arr, L, p[0] - 1);  // < 区
            quickSort(arr, p[1] + 1, R);  // > 区
        }
    }

    // 这是一个处理arr[L..R]的函数
    // 默认以arr[R]做划分, arr[r] -> p    <p    ==p    >p
    // 返回等于区域(左边界,右边界),所以分会一个长度为2的数组res, res[0]为左边界,res[1]为右边界
    public static int[] partition(int[] arr, int L, int R) {
        int less = L - 1;  // < 区右边界
        int more = R;  // > 区左边界
        while (L < more) {  // L表示当前数的位置 arr[R] -> 划分值
             if (arr[L] < arr[R]) {  // 当前数 < 划分值
                swap(arr, ++less, L++);
            } else if (arr[L] > arr[R]) {  // 当前数 > 划分值
                swap(arr, --more, L);
            } else {
                L++;
            }
        }
        swap(arr, more, R);
        return new int[] {less + 1, more};
    }

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

}

       快速排序通过在数组中随机选择一个元素作为划分值,将数组划分为小于区域,等于区域和大于区域三部分,每次划分可以确定等于划分值元素的位置,达到一次处理一批元素的效果。小于区域和大于区域则递归进行同样的操作,优化前的快速排序在最差情况下时间复杂度为O(N^2),优化后因划分值在数组中随机选择,所以不可能每次都是最差情况,其时间复杂度可以收敛于O(N*logN),每次划分需要记录划分的位置,划分值的位置决定了使用空间数量的多少,概率累加后空间复杂度可以收敛到O(logN)的水平。

堆排序

import java.util.Arrays;

import static sort.Logarithm.*;

//快速排序:时间复杂度O(N*logN),额外空间复杂度O(1)
public class HeapSort {

    public static void heapSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        for (int i = 0; i < arr.length; i++) {  // O(n)
            heapInsert(arr, i);  //O(logN)
        }
//        for (int i = (arr.length - 1) / 2; i >= 0; i--) {
//            heapify(arr, i, arr.length);
//        }
        int heapSize = arr.length;
        swap(arr, 0, --heapSize);
        while (heapSize > 0) {  // O(N)
            heapify(arr, 0, heapSize);  // O(logN)
            swap(arr, 0, --heapSize);  // O(1)
        }
    }

    // 某个数现在处在index位置,往上继续移动
    public static void heapInsert(int[] arr, int index) {
        while (arr[index] > arr[(index - 1) / 2]) {
            swap(arr, index, (index - 1) / 2);
            index = (index - 1) / 2;
        }
    }

    // 某个数在index位置,能否往下移动
    public static void heapify(int[] arr, int index, int heapSize) {
        int left = index * 2 + 1;
        while (left < heapSize) {
            int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
            largest = arr[index] > arr[largest] ? index : largest;
            if (index == largest) {
                break;
            }
            swap(arr, largest, index);
            index = largest;
            left = index * 2 + 1;
        }
    }

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

}

       堆排序中通过构建大根堆(小根堆)来对数组进行排序,其中最重要的两个操作就是heapInsert(向堆中插入元素)和heapify(堆化),这两个操作是形成堆结构的核心操作。堆排序通过将数组构建成堆,从堆中逐个弹出元素对数组进行排序。时间复杂度O(N*logN)。堆操作的方法中都没有申请额外变量,故空间复杂度为O(1),在时间复杂度为O(N*logN)的排序中,只有堆排序可以做到。

基数排序(桶排序)

import java.util.Arrays;

import static sort.Logarithm.*;

public class RadixSort {

    // only for no-negative value
    public static void radixSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        radixSort(arr, 0, arr.length - 1, maxbits(arr));
    }

    public static int maxbits(int[] arr) {
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < arr.length; i++) {
            max = Math.max(max, arr[i]);
        }
        int res = 0;
        while (max != 0) {
            res++;
            max /= 10;
        }
        return res;
    }

    // arr[begin..end]排序
    public static void radixSort(int[] arr, int L, int R, int digit) {
        final int radix = 10;
        int i = 0, j = 0;
        // 有多少个数准备多少个辅助空间
        int[] bucket = new int[R - L + 1];
        for (int d = 1; d <= digit; d++) {  //有多少位就救出多少次
            // 10个空间
            // count[0] 当前位(d位)是0的数字有多少个
            // count[1] 当前位(d位)是(0和1)的数字有多少个
            // count[2] 当前位(d位)是(0、1和2)的数字有多少个
            // count[i] 当前位(d位)是(0~i)的数字有多少个
            int[] count = new int[radix];  // count[0..9]
            for (i = L; i <= R; i++) {
                j = getDigit(arr[i], d);
                count[j]++;
            }
            for (i = 1; i < radix; i++) {
                count[i] = count[i] + count[i - 1];
            }
            for (i = R; i >= L; i--) {
                j = getDigit(arr[i], d);
                bucket[count[j] - 1] = arr[i];
                count[j]--;
            }
            for (i = L, j = 0; i <= R; i++, j++) {
                arr[i] = bucket[j];
            }
        }
    }

    public static int getDigit(int x, int d) {
        return ((x / ((int) Math.pow(10, d - 1))) % 10);
    }

}

       桶排序思想下的排序都是不基于比较的排序,应用范围有限,需要样本的数据状况满足桶的划分。先找出数组内元素的最大位数,确定需要循环(入桶出桶)的次数,每次通过getDigit方法得到不同位数上的值,统计每个值的元素个数,并进行累加计算前缀和。通过从后往前的顺序依次出桶,根据前缀和确定其在数组中的位置,完成数组排序。时间复杂度为O(N),额外空间负载度O(M)。

排序算法的稳定性

同样值的个体之间,如果不因为排序而改变相对次序,就说这个排序是有稳定性的。

不具备稳定性的排序:选择排序、快速排序、堆排序

具备稳定性的排序:冒泡排序、插入排序、归并排序、一起桶排序思想下的排序

排序算法比较
时间复杂度空间复杂度稳定性
选择排序O(N^2)O(1)×
冒泡排序O(N^2)O(1)
插入排序O(N^2)O(1)
归并排序O(N*logN)O(N)
快速排序O(N*logN)O(logN)×
堆排序O(N*logN)O(1)×

排序算法研究现状      

       在基于比较的排序算法中,目前没有找到时间复杂度O(N*logN),额外空间复杂度O(N),又稳定的排序,很多人致力于证明不存在这样的排序算法。一般来说排序会选择快速排序,根据实验结果,快排是最快的,能用快排的时候就用快排。如果有空间的限制可以使用堆排序,需要用到稳定性时可以使用归并排序。根据实际的情况选择排序算法。

排序算法常见的坑

  1. 通过内部缓存法可以使归并排序的额外空间复杂度变成O(1),但是非常难实现,且会丧失稳定性。
  2. “原地归并排序”的帖子都是垃圾,会让归并排序的时间复杂度变为O(N^2).
  3. 快速排序可以做到稳定性,但会让空间复杂度变为O(N),且非常难实现。“01 stable sort”。
  4. 所有的改进都不重要,因为目前没有找到时间复杂度O(N*logN),额外空间复杂度O(N),又稳定的排序。
  5. 面试大坑题:奇数放在数组左边,偶数放在数组右边,还要求原始的相对次序不变,时间复杂度O(N),空间复杂度O(1)。碰到这个问题,可以怼面试官。经典快排做不到稳定性,但是经典快排的partition是0 1标准,和奇偶问题是一种调整策略。

工程上对排序的改进

  • 充分利用O(N*logN)和O(N^2)排序各自的优势,大样本下利用快排调度上时间复杂度的优势,小样本下利用插入排序常数项低的优势(综合排序)
  • 稳定性的考虑。系统Arrays.sort方法的内部实现:基础类型会用快排,非基础类型会用归并。基础类型可以认为稳定性是没有用的,使用常数时间比较低的快排,非基础类型保持稳定性。

参考

        左程云算法与数据结构课程

所有算法源码都发布在gitee个人仓库中,仅可作学习使用,欢迎点赞收藏。

仓库地址:仓库 - jkyrie (jkyrie) - Gitee.com

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值