常见排序算法

非线性时间比较类排序 

算法时间复杂度空间复杂度稳定思想注意事项
冒泡O(n²)O(1)Y比较最好情况需要额外判断
选择O(n²)O(1)N比较交换次数一般少于冒泡
O(nlogn)O(1)N选择堆排序的辅助性较强,理解前先理解堆的数据结构
插入O(n²)O(1)Y比较插入排序对于近乎有序的数据处理速度比较快,复杂度有所下降,可以提前结束
希尔O(nlogn)O(1)N插入gap序列的构造有多种方式,不同方式处理的数据复杂度可能不同
归并O(nlogn)O(n)Y分治需要额外的O(n)的存储空间
快速O(nlogn)O(logn)N分治快排可能存在最坏情况,需要把枢轴值选取得尽量随机化来缓解最坏情况下的时间复杂度

稳定 vs 不稳定

我们说一个算法稳不稳定,也就是看排序前后会不会把一些相同元素的顺序给打乱。排序后取值相同的元素顺序没有被打乱,称之为稳定算法。排序后取值相同的元素顺序被打乱了,称之为不稳定算法

冒泡排序

排序思想

冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系的要求。如果不满足就让它俩互换位置。一次冒泡会让至少一个元素移动到它应该在的位置,重复n次,就完成了n个数据的排序工作。

  • 每轮冒泡不断比较相邻的两个元素,如果它们是逆序的,则交换它们的位置。
  • 下一轮冒泡,可以调整未排序的右边界,减少不必要的比较。

动画演示

优化:每次循环时,若能确定更合适的右边界,则可以减少冒泡轮数。

代码实现

    private static void bubble(int[] a) {
        // 初始右边界
        int j = a.length - 1;
        while (true) {
            // 记录右边界
            int x = 0;
            for (int i = 0; i < j; i++) {
                if (a[i] > a[i + 1]) {
                    int t = a[i];
                    a[i] = a[i + 1];
                    a[i + 1] = t;
                    x = i;
                }
            }
            // 每轮冒泡结束 将右边界赋值给j
            j = x;
            // 右边界为0时 排序结束
            if (j == 0) {
                break;
            }
        }
    }

快速排序

快速排序也是利用分治思想。如果要排序一组数据,我们先选择这组数据中任意一个数据作为分区点pivot,然后遍历这组数据,将小于分区点pivot的放到左边,大于分区点pivot的放到右边,将pivot放到中间。然后再分别对左右两部分进行排序。

快排在基于比较的算法里是速度最快的,而且数据量越大效果越明显。

单边快排

lomuto 洛穆托分区方案

排序思想

  • 每轮找到一个基准元素,把比它小的放到它左边,比它大的放到它右边,这称为分区
  • 选择最右侧元素作为基点元素
  • j 指针负责找比基准点小的,i 负责指针找比基准点大的,一旦找到,二者进行交换
    • 交换时机:j 找到小的,且与 i 不相等
    • i 找到 >= 基准点元素后,不应自增
  • 最后基准点与 i 交换,i 即为基准点最终索引

图例

i 和 j 都从左边出发向右查找,i 找到比基准点4大的5,j找到比基准点小的2,停下来交换

i 找到了比基准点大的5,j 找到比基准点小的3,停下来交换

j 到达right 处结束,right 与 i 交换,一轮分区结束

代码实现

public class QuickSortLomuto {

    private static void quick(int[] a, int left, int right) {
        if (left >= right) {
            return;
        }
        // p 代表基准点元素索引
        int p = partition(a, left, right);
        quick(a, left, p - 1);
        quick(a, p + 1, right);
    }

    private static int partition(int[] a, int left, int right) {
        // 基准点元素值
        int pv = a[right];
        int i = left;
        int j = left;
        while (j < right) {
            // j找到比基准点小的
            if (a[j] < pv) {
                // 若i和j指向同一个位置则不需要交换
                if (i != j) {
                    swap(a, i, j);
                }
                i++;
            }
            j++;
        }
        swap(a, i, right);
        return i;
    }

    private static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
}

双边快排

排序思想

  • 选择最左侧元素作为基准点
  • j 找比基准点小的,i 找比基准点大的,一旦找到,二者进行交换
    • i 从左向右
    • j 从右向左
  • 最后基准点与 i 交换,i 即为基准点最终索引

动画演示

双边快排 - 动画演示

随机基准点:使用随机数作为基准点,避免万一最大值或最小值作为基准点导致的分区不均衡

处理重复值:如果重复值较多,则原来算法中的分区效果也不好,如下图中左侧所示 ,需要想办法改为右侧的分区效果

代码实现

public class QuickSortHandleDuplicate {

    public static void sort(int[] a) {
        quick(a, 0, a.length - 1);
    }

    private static void quick(int[] a, int left, int right) {
        if (left >= right) {
            return;
        }
        int p = partition(a, left, right);
        quick(a, left, p - 1);
        quick(a, p + 1, right);
    }

    private static int partition(int[] a, int left, int right) {
        int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
        swap(a, left, idx);
        int pv = a[left];
        int i = left + 1;
        int j = right;
        while (i <= j) {
            // i 从左向右找大的或者相等的
            while (i <= j && a[i] < pv) {
                i++;
            }
            // j 从右向左找小的或者相等的
            while (i <= j && a[j] > pv) {
                j--;
            }
            if (i <= j) {
                swap(a, i, j);
                i++;
                j--;
            }
        }
        swap(a, j, left);
        return j;
    }

    private static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
}

选择排序

排序思想

  • 每一轮选择,找出最大(最小)的元素,并把它交换到合适的位置

动画演示

代码实现

   // 每轮选择出最大的元素
   public static void sort(int[] a) {
        // 1. 选择轮数 a.length - 1
        // 2. 交换的索引位置(right) 初始 a.length - 1, 每次递减
        for (int right = a.length - 1; right > 0 ; right--) {
            int max = right;
            for (int i = 0; i < right; i++) {
                if (a[i] > a[max]) {
                    max = i;
                }
            }
            // 每轮循环结束 将选择的值交换到合适的位置
            if(max != right) {
                swap(a, max, right);
            }
        }
    }

    private static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }

堆排序

选择排序是先选择最大值,然后再把它交换到合适的位置。其中选择最大值时间复杂度是O(n),交换的时间复杂度是O(n)。嵌套起来就是O(n²)。

堆排序就是对选择排序的一种优化,借助堆的数据结构将选择大值得时间复杂度变成O(㏒n),这样堆排序的时间复杂度就是O(n㏒n)。

什么是堆

堆是一种特殊的树,它满足需要满足两个条件:

  • 堆是一种完全二叉树,也就是除了最后一层,其他层的节点个数都是满的,最后一个节点都靠左排列。
  • 堆中每一个节点的值都必须大于等于(或小于等于)其左右子节点的值。

对于每个节点的值都大于等于子树中每个节点值的堆,我们叫作“大顶堆”。对于每个节点的值都小于等于子树中每个节点值的堆,我们叫作“小顶堆”。

排序思想

  • 建立大顶堆

  • 每次将堆顶元素(最大值)交换到末尾,调整堆顶元素,让它重新符合大顶堆特性

图例

建堆

交换,下潜调整

代码实现

public class HeapSort {
    public static void sort(int[] a) {
        // 建堆
        heapify(a, a.length);
        
        // 外层循环 O(n)
        for (int right = a.length - 1; right > 0; right--) {
            swap(a, 0, right);
            // 内层有循环 O(nlogn)
            down(a, 0, right);
        }
    }

    // 建堆 O(n)
    //     1.找到最后一个非叶子节点   (size << 1) - 1
    //     2.从后向前,对每个叶子节点执行下潜
    private static void heapify(int[] array, int size) {
        for (int i = size / 2 - 1; i >= 0; i--) {
            down(array, i, size);
        }
    }

    // 下潜
    // leetcode 上数组排序题目用堆排序求解,非递归实现比递归实现大约快 6ms
    private static void down(int[] array, int parent, int size) {
        while (true) {
            // 左子节点   2n + 1
            int left = parent * 2 + 1;
            // 右子节点   2n + 2
            int right = left + 1;
            // 记录较大值
            int max = parent;

            // 跟左子节点比较
            if (left < size && array[left] > array[max]) {
                max = left;
            }
            // 跟右子节点比较
            if (right < size && array[right] > array[max]) {
                max = right;
            }

            // 父节点比左右子节点都大 则不用下潜 跳出循环
            if (max == parent) {
                break;
            }    
            swap(array, max, parent);
            parent = max;
        }
    }

    // 交换
    private static void swap(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }
}

插入排序

排序思想

将数组的数据分为两个区间,已排序区间未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。算法的核心思想就是,取未排序区间中的元素,在已排序区间中找到合适的位置将其插入,并保证已排序区间的数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束。

动画演示

代码实现

public class InsertionSort {

    public static void sort(int[] a) {
        // 外层循环 O(n)
        for (int low = 1; low < a.length; low++) {
            // 将 low 位置的元素插入至 [0..low-1] 的已排序区域
            int t = a[low];
            int i = low - 1; // 已排序区域指针
            
            // 内层循环 O(n)
            while (i >= 0 && t < a[i]) {  // 没有找到插入位置
                a[i + 1] = a[i];          // 空出插入位置
                i--;
            }

            // 找到插入位置
            if (i != low - 1) {
                a[i + 1] = t;
            }
        }
    }
}

希尔排序

排序思想

希尔排序是简单插入排序的改进版。他与插入排序的不同之处在于,它会优先比较较远的元素。希尔排序又叫缩小增量排序。希尔排序的核心在于间隔序列的设定(也就是增量)。既可以提前设定好间隔序列,也可以动态定义间隔序列。

  • 分组实现插入,每组元素间隙称为 gap
  • 每轮排序后 gap 逐渐变小,直至 gap 为 1 完成排序
  • 对插入排序的优化,让元素更快速地交换到最终位置

动画演示

 代码实现

public class ShellSort {
    public static void sort(int[] a) {
        for (int gap = a.length >> 1; gap >= 1; gap = gap >> 1) {
            // 内层是插入排序
            for (int low = gap; low < a.length; low++) {
                int t = a[low];
                int i = low - gap; 
                while (i >= 0 && t < a[i]) { 
                    a[i + gap] = a[i];      
                    i -= gap;
                }
                if (i != low - gap) {
                    a[i + gap] = t;
                }
            }
        }
    }
}

归并排序

排序思想

归并排序的采用分治思想,如果要排序一个数组,我们先把数组从中间分成前后两个部分,然后对前后两个部分分别进行排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。

  • 分 - 每次从中间切一刀,处理的数据少一半
  • 治 - 当数据仅剩一个时可以认为有序
  • 合 - 两个有序的结果,可以进行两个有序数组的合并排序

动画演示

代码实现

递归 — 自顶至下

// 自顶至下
public class MergeSortTopDown {

    public static void sort(int[] a1) {
        int[] a2 = new int[a1.length];
        split(a1, 0, a1.length - 1, a2);
    }

    private static void split(int[] a1, int left, int right, int[] a2) {
        // 2.治
        // 递归结束条件
        if (left == right) {
            return;
        }
        // 1. 分
        int m = (left + right) >>> 1;
        split(a1, left, m, a2);
        split(a1, m + 1, right, a2);
        // 3. 合
        merge(a1, left, m, m + 1, right, a2);
        System.arraycopy(a2, left, a1, left, right - left + 1);
    }

    /**
     * 有序数组合并
     *
     * a1       原始数组
     * i ~ iEnd 第一个有序范围
     * j ~ jEnd 第二个有序范围
     * a2       临时数组
     */
    private static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {
        int k = i;
        while (i <= iEnd && j <= jEnd) {
            if (a1[i] < a1[j]) {
                a2[k] = a1[i];
                i++;
            } else {
                a2[k] = a1[j];
                j++;
            }
            k++;
        }
        if (i > iEnd) {
            System.arraycopy(a1, j, a2, k, jEnd - j + 1);
        }
        if (j > jEnd) {
            System.arraycopy(a1, i, a2, k, iEnd - i + 1);
        }
    }
}

非递归 — 自下至上

// 自下至上
public class MergeSortBottomUp {

    private static final int SIZE = 10000000;

    public static void main(String[] args) {
        int[] a = new int[SIZE];
        Random r = new Random();
        for (int i = 0; i < SIZE; i++) {
            a[i] = r.nextInt(SIZE);
        }
        long s = System.currentTimeMillis();
        sort(a);  // 1301
        System.out.println("归并-非递归:" + (System.currentTimeMillis() - s) + "毫秒");
    }

    public static void sort(int[] a1) {
        int n = a1.length;
        int[] a2 = new int[n];
        // width 代表有序宽度区间,取值依次是 1、2、4、8 ...
        for (int width = 1; width < n; width = width << 1) {
            // [left right] 分别代表待合并区的左右边界
            for (int left = 0; left < n; left += 2 * width) {
                // 右边界为下次左边界减1
                int right = Integer.min(left + 2 * width - 1, n - 1);  // 防止越界
                // 中间索引为左边界加上宽度减1
                int m = Integer.min(left + width - 1, n - 1);          // 防止越界
                merge(a1, left, m, m + 1, right, a2);
            }
            System.arraycopy(a2, 0, a1, 0, n);
        }
    }

    /**
     * 有序数组合并
     *
     * a1       原始数组
     * i ~ iEnd 第一个有序范围
     * j ~ jEnd 第二个有序范围
     * a2       临时数组
     */
    public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {
        int k = i;
        while (i <= iEnd && j <= jEnd) {
            if (a1[i] < a1[j]) {
                a2[k] = a1[i];
                i++;
            } else {
                a2[k] = a1[j];
                j++;
            }
            k++;
        }
        if (i > iEnd) {
            System.arraycopy(a1, j, a2, k, jEnd - j + 1);
        }
        if (j > jEnd) {
            System.arraycopy(a1, i, a2, k, iEnd - i + 1);
        }
    }
}

归并 + 插入

小数据量且有序度高时,插入排序效果高,大数据量用归并效果好,可以结合二者。

public class MergeInsertionSort {

    private static final int SIZE = 10000000;

    public static void main(String[] args) {
        int[] a = new int[SIZE];
        Random r = new Random();
        for (int i = 0; i < SIZE; i++) {
            a[i] = r.nextInt(SIZE);
        }
        long s = System.currentTimeMillis();
        sort(a);   // 968毫秒
        System.out.println("归并+插入:" + (System.currentTimeMillis() - s) + "毫秒");
    }

    public static void sort(int[] a1) {
        int[] a2 = new int[a1.length];
        split(a1, 0, a1.length - 1, a2);
    }

    private static void split(int[] a1, int left, int right, int[] a2) {
        // 2. 治
        if (right == left) {
            return;
        }
        // 当分到小于128时 使用插入排序
        if (right - left <= 128) {
            insertion(a1, left, right);
            return;
        }
        // 1. 分
        int m = (left + right) >>> 1;
        split(a1, left, m, a2);
        split(a1, m + 1, right, a2);
        // 3. 合
        merge(a1, left, m, m + 1, right, a2);
        System.arraycopy(a2, left, a1, left, right - left + 1);
    }

    /**
     * 有序数组合并
     *
     * a1       原始数组
     * i ~ iEnd 第一个有序范围
     * j ~ jEnd 第二个有序范围
     * a2       临时数组
     */
    public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {
        int k = i;
        while (i <= iEnd && j <= jEnd) {
            if (a1[i] < a1[j]) {
                a2[k] = a1[i];
                i++;
            } else {
                a2[k] = a1[j];
                j++;
            }
            k++;
        }
        if (i > iEnd) {
            System.arraycopy(a1, j, a2, k, jEnd - j + 1);
        }
        if (j > jEnd) {
            System.arraycopy(a1, i, a2, k, iEnd - i + 1);
        }
    }

    // 插入排序
    private static void insertion(int[] a, int left, int right) {
        for (int low = left + 1; low <= right; low++) {
            int t = a[low];
            int i = low - 1;
            while (i >= left && t < a[i]) {
                a[i + 1] = a[i];
                i--;
            }
            if (i != low - 1) {
                a[i + 1] = t;
            }
        }
    }
}

线性时间非比较类排序

非比较排序算法时间复杂度空间复杂度稳定性
计数排序O(n+k)O(n+k)稳定
桶排序O(n+k)O(n+k)稳定
基数排序O(d*(n+k))O(n+k)稳定
注:n 是数组长度、  k 是桶长度、  d 是基数位数 

计数排序

排序思想

  • 找到原数组中的最大值和最小值,创建一个长度为(最大值减最小值+1)的 count 数组
  • 让原始数组的最小值映射到 count[0],最大值映射到 count 最右侧
  • 原始数组元素 - 最小值 = count 索引
  • count 索引 + 最小值 = 原始数组元素

动画演示

       计数排序 - 动画演示

代码实现

public class CountingSort {

    public static void sort2(int[] a) {
        int min = a[0];
        int max = a[0];
        // 找到数组中的最大值和最小值
        for (int i : a) {
            if (i > max) {
                max = i;
            } else if (i < min) {
                min = i;
            }
        }
        // 创建Counting数组
        int[] counting = new int[max - min + 1];
        // 将数组中的元素映射到Counting数组中
        for (int i : a) {
            counting[i - min]++;
        }
        // 基于Counting数组排序
        int k = 0;
        for (int i = 0; i < counting.length; i++) {
            while (counting[i] > 0) {
                a[k] = i + min;
                counting[i]--;
                k++;
            }
        }
    }
}

针对 byte [],因为数据范围已知,省去了求最大、最小值的过程,java 中对 char[]、short[]、byte[] 的排序都可能采用 counting 排序。

桶排序

排序思想 

桶排序的核心思想就是将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。桶排序完之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。

图例

代码实现

public class BucketSortGeneric {
    public static void main(String[] args) {
        int[] ages = {20, 10, 28, 66, 25, 31, 67, 30, 70};
        sort(ages, 3);
        System.out.println(Arrays.toString(ages));
    }

    public static void sort(int[] a, int range) {
        int max = a[0];
        int min = a[0];
        for (int i = 1; i < a.length; i++) {
            if (a[i] > max) {
                max = a[i];
            }
            if (a[i] < min) {
                min = a[i];
            }
        }
        // 1. 准备桶
        List<Integer>[] buckets = new ArrayList[(max - min) / range + 1];
        for (int i = 0; i < buckets.length; i++) {
            buckets[i] = new ArrayList<>();
        }
        // 2. 放入年龄数据
        for (int age : a) {
            buckets[(age - min) / range].add(age);
        }
        int k = 0;
        for (List<Integer> bucket : buckets) {
            // 3. 排序桶内元素
            int[] array = new int[bucket.size()];
            for (int i = 0; i < bucket.size(); i++) {
                array[i] = bucket.get(i);
            }
            InsertionSort.sort(array);
            // 4. 把每个桶排序好的内容,依次放入原始数组
            for (int v : array) {
                a[k++] = v;
            }
        }
    }
}

基数排序

排序思想

基数排序与前面的排序算法不一样,它不基于比较和移动元素来进行排序,而是基于多关键字排序的思想,将一个逻辑关键字分为多个关键字,它是基于关键字各位的大小进行排序的。基数排序有两种实现方式:最高位优先法最低位优先法,分别是按关键字高次位排序和低次位排序。

图例

下面通过最低位优先法,对给定的关键字序列 {110,119,007,911,114,120,122} 进行排序

1、该序列的链式结构如下:

2、首先按照关键字的个位数字大小进行第一趟基数排序:

3、根据第一趟的顺序,按照关键字的十位数字大小进行第二趟基数排序: 

4、根据第二趟的顺序,按照关键字的百位数字大小进行第三趟基数排序:

代码实现

public class RadixSort {
    public static void radixSort(String[] a, int length) {
        ArrayList<String>[] buckets = new ArrayList[10];
        for (int i = 0; i < buckets.length; i++) {
            buckets[i] = new ArrayList<>();
        }
        for (int i = length - 1; i >= 0; i--) {
            for (String s : a) {
                buckets[s.charAt(i)].add(s);
            }
            int k = 0;
            for (ArrayList<String> bucket : buckets) {
                for (String s : bucket) {
                    a[k++] = s;
                }
                bucket.clear();
            }
        }
    }

    public static void main(String[] args) {
        String[] phoneNumbers = new String[10];
        phoneNumbers[0] = "138";
        phoneNumbers[1] = "139";
        phoneNumbers[2] = "136";
        phoneNumbers[3] = "137";
        phoneNumbers[4] = "135";
        phoneNumbers[5] = "134";
        phoneNumbers[6] = "150";
        phoneNumbers[7] = "151";
        phoneNumbers[8] = "152";
        phoneNumbers[9] = "157";
        RadixSort.radixSort(phoneNumbers, 3);
        for (String phoneNumber : phoneNumbers) {
            System.out.println(phoneNumber);
        }
    }
}

Java中的排序

 java中的排序主要是 Arrays.sort 方法

JDK 7~13 中的排序实现

排序目标条件采用算法
int[] long[] float[] double[]size < 47混合插入排序 (pair)
size < 286双基准点快排
有序度高归并排序
有序度低双基准点快排
byte[]size > 29计数排序
size <= 29插入排序
char[] short[]size > 3200计数排序
size < 47插入排序
size < 286双基准点快排
有序度高归并排序
有序度低双基准点快排
Object[]-Djava.util.Arrays.useLegacyMergeSort=true传统归并排序
TimSort

JDK 14~20 中的排序实现

排序目标条件采用算法
int[] long[] float[] double[]size < 65 并不是最左侧混合插入排序 (pin)
size < 44 并位于最左侧插入排序
递归次数超过 384堆排序
对于整个数组或非最左侧 size > 4096,有序度高归并排序
有序度低双基准点快排
byte[]size > 64计数排序
size <= 64插入排序
char[] short[]size > 1750计数排序
size < 44插入排序
递归次数超过 384计数排序
不是上面情况双基准点快排
Object[]-Djava.util.Arrays.useLegacyMergeSort = true传统归并排序
TimSort
  • 其中 TimSort 是用 归并 + 二分插入排序 的混合排序算法

  • 值得注意的是从 JDK 8 开始支持 Arrays.parallelSort 并行排序

  • 根据最新的提交记录来看 JDK 21 可能会引入基数排序等优化

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值