手记

基本的六种排序算法

本文将介绍六种基本排序算法思想及Java代码实现,它们分别是:

  • 插入排序
  • 选择排序
  • 冒泡排序
  • 希尔排序
  • 归并排序
  • 双向切分的快速排序

    本文所有排序算法都将使用泛型<T extends Comparable>
    这六个基本排序算法都放在了一个类里面,每个方法都是static的。

两个辅助方法

为了代码更清晰,在本类里面写了两个私有的辅助方法,分别是比较交换

/**
 * 交换两个元素
 * @param arr 数组
 * @param a 第一个位置
 * @param b 第二个位置
 * @param <T>
 */
private static <T> void exchange(T[] arr, int a, int b) {
    T temp = arr[a];
    arr[a] = arr[b];
    arr[b] = temp;
}

/**
 * 比较两个元素
 * @param arr 数组
 * @param a 第一个位置
 * @param b 第二个位置
 * @param <T>
 * @return arr[a]是否小于arr[b]
 */
private static <T extends Comparable> boolean less(T[] arr, int a, int b) {
    return arr[a].compareTo(arr[b]) < 0;
}
测试类代码:
public class SortTest {
    private Integer[] arr;
    private Integer[] sortedArr;

    /**
     * 初始化数组
     */
    @Before
    public void initArray() {
        arr = new Integer[200];
        Random random = new Random();
        for (int i = 0; i < arr.length; i++) {
            arr[i] = random.nextInt(10000);
        }
        sortedArr = new Integer[200];
        System.arraycopy(arr, 0, sortedArr, 0, arr.length);
        Arrays.sort(sortedArr);
    }

    /**
     * 检查数组是否排好序
     */
    @After
    public void checkArray() {
        assertArrayEquals(sortedArr, arr);
    }

    @Test
    public void insertSort() {
        Sort.insertSort(arr);
    }
    // 其它排序方法类似...
}
1. 插入排序

插入排序类似于每次从桌上的扑克牌中摸起一张,插入到手中的牌的合适位置,使手中的牌总是有序。可利用Java的System.arraycopy做适当优化。

/**
 * 插入排序
 * 1. 维护arr[0..i)为有序数组
 * 2. 取得arr[i], 把它插入到前面的有序数组中的合适的位置
 * 3. i从1遍历到n - 1
 * @param arr 要排序的数组
 * @param <T> 类型
 */
public static <T extends Comparable> void insertSort(T[] arr) {
    T temp;
    for (int i = 1; i < arr.length; i++) {
        int j = i;
        // 这里做了一个优化,先找到位置,再一次性交换.而不是一个个交换过去.
        while (j >0 && less(arr, i, j - 1))
            j--;
        temp = arr[i];
        System.arraycopy(arr, j, arr, j + 1, i - j);
        arr[j] = temp;
    }
}
2. 选择排序

选择排序类似于每次从桌上的牌堆中找出最小的那张牌,放到手中的牌的最后位置。

/**
 * 选择排序
 * 1. 维护arr[0..i)有序
 * 2. 每次从arr[i..n)中取得最小值arr[j]
 * 3. 交换arr[i]和arr[j]
 * 4. i从0到n - 1 遍历
 * @param arr 要排序的数组
 * @param <T> 类型
 */
public static <T extends Comparable> void selectSort(T[] arr) {
    for (int i = 0; i < arr.length; i++) {
        int min = i; // 最小值的索引
        for (int j = i; j < arr.length; j++) {
            if (less(arr, j, min)) {
                min = j;
            }
        }
        // 交换i和min位置的元素
        if (i != min) {
            exchange(arr, i, min);
        }
    }
}
3. 冒泡排序

冒泡排序是一种交换排序,从数组底部往顶部冒泡,如果一个元素比前一个元素小,就交换这两个元素。这里使用了flag变量来做简单的优化。
进一步的优化思路是:不必都要两两交换,可直接通过比较,找到需要发生交换的位置m,将arr[j]插入到m,然后利用System.arraycopy移动数组。这种思路与上述的插入排序优化思路一致。

/**
 * 冒泡排序
 * 1. 维持[0..i)有序
 * 2. 从数组末尾n - 1 位置到 i 位置, 每个元素与上一个元素比较,若小于,则两个元素交换.
 * 3. 经过一趟2之后,最后到了i出, arr[i]就是[i..n - 1]最小的元素
 * 优化:设置一个标记变量flag,当循环中没有交换数据时,算法将停止循环。
 * 冒泡排序是一种交换排序.
 * @param arr 要排序的数组
 * @param <T> 类型
 */
public static <T extends Comparable> void bubbleSort(T[] arr) {
    boolean flag = true; // 本趟冒泡是否发生了交换
    for (int i = 0; i < arr.length && flag; i++) {
        flag = false;
        for (int j = arr.length - 1; j > i; j--) {
            if (less(arr, j, j - 1)) {
                flag = true;
                exchange(arr, j, j - 1);
            }
        }
    }
}
4. 希尔排序

希尔排序是快速排序的改进。希尔排序的增量序列的选取比较关键。本文只是简单的使用的是2的幂的增量序列。读者可以换用其它增量序列。

/**
 * 希尔排序
 * 1. 修改插入排序算法,使数组每隔k个数的子数列有序
 * 2. 逐步缩小k,最后缩小到1,整个数组有序.
 * 3. 需要注意的是这时不能使用插入排序的优化方法,因为不能直接简单地arraycopy
 * @param arr 要排序的数组
 * @param <T> 类型
 */
public static <T extends Comparable> void shellSort(T[] arr) {
    for(int k = arr.length / 2; k > 0; k /= 2) {
        for (int i = k; i < arr.length; i += k) {
            for (int j = i; j >= k && less(arr, j, j - k); j -= k) {
                exchange(arr, j, j - k);
            }
        }
    }
}
5. 归并排序

归并排序利用了递归的思想。若左右两个子数组都排好序,那可以使用一次归并过程,将这两个有序子数组合并成一个有序数组。

/**
 * 归并排序
 * 1. 假设[lo..mid]与[mid+1..hi]均排好序,
 * 2. 合并上述排好序的数组,组成一个新的有序数组
 * 3. 当子数组长度为1时,自然是已经排好序的.
 * 4. 使用递归算法重复上述过程
 * @param arr 要排序的数组
 * @param <T> 类型
 */
public static <T extends Comparable> void mergeSort(T[] arr) {
    mergeSort(arr, 0, arr.length - 1);
}

/**
 * 归并排序的递归过程
 * @param arr  整个数组
 * @param lo 低位
 * @param hi 高位
 * @param <T> 类型
 */
private static <T extends Comparable> void mergeSort(T[] arr, int lo, int hi) {
    // 设置递归临界条件
    if (hi <= lo) return;
    int mid = lo + (hi - lo) / 2; // 防止边界溢出
    mergeSort(arr, lo, mid);
    mergeSort(arr, mid + 1, hi);
    merge(arr, lo, mid, hi); // 合并
}

/**
 * 归并排序的一趟合并过程.其中arr[lo..mid]和arr[mid + 1..hi]已经排好序
 * @param arr 数组
 * @param lo 低位
 * @param mid 中位
 * @param hi 高位
 * @param <T> 类型
 */
private static <T extends Comparable> void merge(T[] arr, int lo, int mid, int hi) {
    // 计算两边的长度和总长度
    int lLen = mid - lo + 1;
    int rLen = hi - mid;
    int n = lLen + rLen;
    Comparable[] temp = new Comparable[n];
    System.arraycopy(arr, lo, temp, 0, n);
    // 三个指针,i为左边数组当前位置,j为右边数组当前位置,k为最终数组当前位置
    for (int i = 0, j = lLen, k = lo; k <= hi; k++) {
        if (i >= lLen) arr[k] = (T) temp[j++];
        else if (j >= n ) arr[k] = (T) temp[i++];
        else if (less(temp, i, j)) arr[k] = (T) temp[i++];
        else arr[k] = (T) temp[j++];
    }
}
6. 快速排序

快速排序也是利用了递归的思想。它对一个数组arr[]排序时,选取某一个元素为支点,小于支点的放左边,大于支点的放右边,最后把支点放到中间合适的位置,返回这个位置。

/**
 * 快速排序
 * 1. 选取某一个元素为flag, 比flag小的放到左边,比flag大的放到右边.可以选取flag为第一个元素.
 * 2. 利用递归思想,对左边的数组和右边的数组分别重复上述过程,逐渐缩小待排序数组的范围,直到为1,此时整个数组已经排好序
 * @param arr 要排序的数组
 * @param <T> 类型
 */
public static <T extends Comparable> void quickSort(T[] arr) {
    quickSort(arr, 0, arr.length - 1);
}

/**
 * 快速排序归并方法
 * @param arr 要排序的数组
 * @param lo 低位
 * @param hi 高位
 * @param <T> 类型
 */
private static <T extends Comparable> void quickSort(T[] arr, int lo, int hi) {
    if (hi <= lo) return;
    int p = partition(arr, lo, hi); // 一趟切分
    quickSort(arr, lo, p - 1);
    quickSort(arr,p + 1, hi);
}

/**
 * 一趟快速排序的切分
 * 1. 设置支点为arr[lo]
 * 2. 设置两个指针i, j, 从两端到中间扫描
 * 3. 先从左边向右扫描,如果发现arr[i]比支点大,停下来.
 * 4. 再从右边向左边扫描,如果发现arr[j]比支点小,停下来.
 * 5. 交换arr[i]与arr[j],继续向后扫描.直到 i >= j
 * @param arr 整个数组
 * @param lo 低位
 * @param hi 高位
 * @return 支点的位置
 */
private static <T extends Comparable> int partition(T[] arr, int lo, int hi) {
    //左右扫描指针,注意初始化的条件,是因为下面的扫描是先移动再比较.比如++i --j
    int i = lo, j = hi + 1;
    while (true) {
        while (less(arr, ++i, lo))
            if (i == hi) break;
        while (less(arr, lo, --j))
            if (j == lo) break;
        if (i >= j) break;
        exchange(arr, i, j);
    }
    exchange(arr, lo, j); // 这里是j而不是i,因为到最后总是j <= i 的
    return j;
}
几种基本排序算法的对比(为美观,表头省略了'复杂度') 排序算法 最差时间 平均时间 空间 稳定
插入 O(n^2^) O(n^2^) O(1)
选择 O(n^2^) O(n^2^) O(1)
冒泡 O(n^2^) O(n^2^) O(1)
希尔 - - O(1)
归并 O(nlog~2~n) O(nlog~2~n) O(n)
快速 O(n^2^) O(nlog~2~n) O(1)

希尔排序的时间复杂度取决于增量序列的选取,一般位于O(nlog~2~n)到O(n^2^)之间。

其它排序

另外常见的排序算法包括:

  • 堆排序:将在介绍堆数据结构时介绍;
  • 基数排序:数组的每个元素在一个较小的取值范围时,很快。
  • TimSort:归并排序的改进,Python和Java默认的数组排序算法。有兴趣可以查看Java的Arrays.sort(T[] a, Comparator<? super T> c)源码。
  • ...

全部代码:

0人推荐
随时随地看视频
慕课网APP