排序算法总结与 Java 实现

Posted by Shane on October 7, 2019

排序算法总结与 Java 实现

O(n^2) 排序算法

  • 基础,编码简单,易于实现,是一些简单情景的首选

  • 在一些特殊情况下,简单的排序算法更有效

  • 简单的排序算法思想衍生出复杂的排序算法

  • 作为子进程,改进更复杂的排序算法

冒泡排序(Bubble Sort)

对相邻的元素进行两两比较,顺序相反则进行交换,这样每一趟会将最小或最大的元素「浮」到顶端,最终达到完全有序

Bubble Sort-2

Bubble Sort-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
 * 冒泡排序,不断找出最大值
 * @param arr
 */
public static void bubbleSort(int[] arr) {
    int len = arr.length;
    for (int i = 0; i < len - 1; i++) {
        for (int j = 0; j < len - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                swap(arr, j, j + 1);
            }
        }
    }
}

/**
 * 冒泡排序优化:假定有序,减少趟数
 * @param arr
 */
public static void bubbleSort2(int[] arr) {
    boolean isSorted = true; // 假定有序
    int len = arr.length;
    for (int i = 0; i < len - 1; i++) {
        isSorted = true;
        for (int j = 0; j < len - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                swap(arr, j, j + 1);
                isSorted = false; // 存在交换则无序
            }
        }
        if (isSorted) { // 假定有序,减少趟数
            break;
        }
    }
}

/**
 * 交换
 *
 * @param arr
 * @param i
 * @param j
 */
private static void swap(int[] arr, int i, int j) {
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

选择排序(Selection Sort)

不断在后面的数组找到最小的元素,放在前面排序好的数组,依次类推,最终达到完全有序

Selection Sort-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * 选择排序
 * @param arr
 */
public static void selectionSort(int[] arr) {
    int len = arr.length;
    for (int i = 0; i < len - 1; i++) {
        // 假定当前为最小值,减少趟数
        int min = i;
        for (int j = i + 1; j < len; j++) {
            if (arr[j] < arr[min]) {
                min = j;
            }
        }
        if (min != i) {
            swap(arr, i, min);
        }
    }
}

插入排序(Insertion Sort)

依次遍历后面的数组,与前面的数组进行比较,直到找到比前面数组中元素大的位置,插入即可

Insertion Sort-2

Insertion Sort-1

优化

不用每次比较时都交换,暂时保存后面数组的数据,前面数组在往前遍历时,前一个值赋值给后一个,直到后面数组的数据找到合适的位置(即前面数组中前一个值 <= 当前值)

对于近乎有序的数组,速度非常快,应用:系统日志

对于有序数组,复杂度为 O(n)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
 * 插入排序
 * @param arr
 */
public static void insertionSort(int[] arr) {
    int len = arr.length;
    for (int i = 1; i < len; i++) {
        for (int j = i; j > 0; j--) {
            if (arr[j] < arr[j - 1]) {
                swap(arr, j, j - 1);
            } else {
                break;
            }
        }
    }
}

/**
 * 插入排序优化:不用每次比较时都交换,暂时保存后面数组的数据,前面数组在往前遍历时,前一个值赋值给后一个,直到后面数组的数据找到合适的位置(即前面数组中前一个值 <= 当前值)
 * @param arr
 */
public static void insertionSort2(int[] arr) {
    int len = arr.length;
    for (int i = 1; i < len; i++) {
        int temp = arr[i];
        int j = i - 1;
        for (; j >= 0 && arr[j] > temp; j--) {
            arr[j + 1] = arr[j];
        }
        arr[j + 1] = temp;
    }
}

希尔排序(Shell Sort)

希尔排序(Shellsort),也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位

插入排序的延伸,插入排序每次都和之前的一个元素进行比较。希尔排序每一次都和之前第 h 元素进行比较,将 h -> 1,即排序完成

时间复杂度:O(n^3/2)

Shell Sort-2

Shell Sort-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
 * 希尔排序
 * @param arr
 */
public static void shellSort(int[] arr) {
    int len = arr.length;
    for (int step = len / 2; step >= 1; step /= 2) {
        for (int i = step; i < len; i++) {
            int temp = arr[i];
            int j = i - step;
            // 插入排序
            for (; j >= 0 && arr[j] > temp; j -= step) {
                arr[j + step] = arr[j];
            }
            arr[j+ step] = temp;
        }
    }
}

O(nlogn) 排序算法

归并排序(Merge Sort)

自顶向下的归并排序

先把数组以一半依次划分,直到不能划分为止,然后依次向上进行归并,归并成一个数组时,即排序完成

Merge Sort-1

归并过程(Merge)

  • 使用临时空间 T,待归并数组 AB(逻辑上分为 A、B,其实是一个数组)
  • 界限:l(left)m(middle)r(right)A[l...m]B[m+1...r]
  • A[i]B[j] 依次比较大小,小的元素添加到 T[k],添加完成即排序完成

Merge Sort-2

优化

  • 归并操作之前判断 arr[mid] > arr[mid+1]mid 之前的数组和 mid 之后的数组都是有序的,如果 arr[mid] <= arr[mid+1],说明当前是有序的
  • 递归操作到底时,在数据量较小时,可以选择插入排序,数据量越小,有序的可能性越大
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
 * 归并排序,递归版
 *
 * @param arr
 */
public static void mergeSort(int[] arr) {
    int len = arr.length;
    mergeSort(arr, 0, len - 1);
}

// 递归使用归并排序,对 arr[l...r] 的范围进行排序
private static void mergeSort(int[] arr, int l, int r) {
    if (l >= r) {
        return;
    }
    int mid = (l + r) / 2;
    mergeSort(arr, l, mid);
    mergeSort(arr, mid + 1, r);
    // 对于arr[mid] <= arr[mid+1]的情况,不进行merge
    // 对于近乎有序的数组非常有效,但是对于一般情况,有一定的性能损失
    if (arr[mid] > arr[mid + 1]) {
        merge(arr, l, mid, r);
    }
}

// 将 arr[l...mid] 和 arr[mid+1...r] 进行归并
private static void merge(int[] arr, int l, int mid, int r) {
    int[] aux = Arrays.copyOfRange(arr, l, r + 1);
    // 初始化,i 指向左半部分的起始索引位置 l,j 指向右半部分起始索引位置 mid+1
    int i = l, j = mid + 1;
    for (int k = l; k <= r; k++) {
        if (i > mid) { // 如果左半部分元素已经全部处理完毕
            arr[k] = aux[j - l];
            j++;
        } else if (j > r) { // 如果右半部分元素已经全部处理完毕
            arr[k] = aux[i - l];
            i++;
        } else if (aux[i - l] < aux[j - l]) { // 左半部分所指元素 < 右半部分所指元素
            arr[k] = aux[i - l];
            i++;
        } else { // 左半部分所指元素 >= 右半部分所指元素
            arr[k] = aux[j - l];
            j++;
        }
    }
}

自底向上的归并排序

  • 从左到右依次把数组分为小段,然后进行归并操作
  • 没有使用数组直接访问索引的特性,可以对链表进行 O(nlogn) 排序

逆序对

  • 一个数组抽取两个数字,如果有序,则为顺序对,否则为逆序对
  • 衡量数组的有序程度
  • 考察每一个数对。暴力解法算法复杂度:O(n^2),使用归并排序达到 O(nlogn) 复杂度

快速排序(Quick Sort)

Quick Sort-2

Quick Sort-1

Partition

选出元素 Varr[l+1...j]<V[j+1...i-1]>V

Quick Sort-Partition-1

Quick Sort-Partition-2

Quick Sort-Partition-3

Quick Sort-Partition-4

Quick Sort-Partition-5

优化

  • Partition 递归操作递归到底时,在数据量较小时,可以选择插入排序。数据量越小,有序的可能性越大
  • Partition 操作时随机选出元素 V(随机算法)。如果是有序或近乎有序的数组,Partition 操作选出的两个数组有可能退化为链表
  • 有大量重复元素的数组,Partition 操作时把 =V 的元素,尽量平均分配在 <V>V 的部分

Quick Sort-optimize-1

Quick Sort-optimize-2

Quick Sort-optimize-3

Quick Sort-optimize-4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
 * 快速排序
 *
 * @param arr
 */
public static void quickSort(int[] arr) {
    int len = arr.length;
    quickSort(arr, 0, len - 1);
}

// 递归使用快速排序,对 arr[l...r] 的范围进行排序
private static void quickSort(int[] arr, int l, int r) {
    if (l >= r) {
        return;
    }
    int p = partition(arr, l, r);
    quickSort(arr, l, p - 1);
    quickSort(arr, p + 1, r);
}

// 对 arr[l...r] 进行 partition 操作
// 返回 p, 使得 arr[l...p-1] < arr[p]; arr[p+1...r] > arr[p]
private static int partition(int[] arr, int l, int r) {
    // 随机在 arr[l...r] 的范围中, 选择一个数值作为标定点 pivot
    swap( arr, l , (int)(Math.random()*(r-l+1))+l );
    int v = arr[l];
    int j = l; // arr[l+1...j] < v ; arr[j+1...i) > v
    for (int i = l + 1; i <= r; i++) {
        if (arr[i] < v) {
            j++;
            swap(arr, j, i);
        }
    }
    swap(arr, l, j);
    return j;
}

三路快速排序(Quick Sort 3 Ways)

适合有大量重复元素的数组

Quick Sort 3 Ways-1

Quick Sort 3 Ways-2

Quick Sort 3 Ways-3

Quick Sort 3 Ways-4

Quick Sort 3 Ways-5

Quick Sort 3 Ways-6

Quick Sort 3 Ways-7

Quick Sort 3 Ways-8

Quick Sort 3 Ways-9

Quick Sort 3 Ways-10

应用

取出数组中第 n 大的元素

  • 取出数组中的最大值、最大值。遍历,算法复杂度:O(n)
  • 排序,算法复杂度:O(nlogn)
  • 快速排序,算法复杂度:O(n)

堆排序(Heap Sort)

运用最大堆的特性,插入待排序数组的数据,然后取出数据 extractMax()

Heap Sort-3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * 堆排序
 *
 * @param arr
 */
public static void heapSort(int[] arr) {
    int len = arr.length;

    for (int i = (len - 1 - 1) / 2; i >= 0; i--) {
        shiftDown(arr, len, i);
    }
    for (int i = len - 1; i > 0; i--) {
        swap(arr, 0, i);
        shiftDown(arr, i, 0);
    }
}

private static void shiftDown(int[] arr, int len, int k) {
    while (2 * k + 1 < len) {
        int j = 2 * k + 1;
        if (j + 1 < len && arr[j + 1] > arr[j]) {
            j += 1;
        }
        if (arr[k] >= arr[j]) {
            break;
        }
        swap(arr, k, j);
        k = j;
    }
}

原地堆排序

  • heapify 操作,选出最大元素(数组首元素),与数组末尾元素交换
  • 前半部分数组首元素 Shift Down 操作,选出最大元素(数组首元素),与前半部分数组末尾元素交换
  • 依次类推,直到前半部分数组个数为 1,排序完成

Heap Sort-1

空间复杂度为 O(1)

多路归并排序

  • 一般归并排序是分为两个数组,然后分别对两个数组中的两个元素进行比较
  • 多路则把数组分为多个数组,然后对多个元素放入堆中进行比较
  • 如果路数变为元素的个数,只需要递归一次即排序完成,此时退化为堆排序
  • 多路归并排序是归并排序和堆排序的组合

Heap Sort-2

排序算法的稳定性

  • 稳定排序:对于相等的元素,在排序后,原来靠前的元素依然靠前,相等元素的相对位置没有发生改变
  • 插入排序:「后面数组的元素 < 前面数组的元素」,则交换相等的元素,则会依次排列
  • 归并排序:归并过程中,「后面数组的元素 < 前面数组的元素」,才会归并后面数组的元素,相等时前面数组的元素先归并
  • 快速排序:在 Partition 操作中,选出元素 V 时,可能破坏稳定性
  • 堆排序:在 heapify 过程中,可能破坏稳定性
  • 可以通过自定义比较函数,让排序算法不存在稳定性的问题

算法复杂度分析中的符号

  • Θ,读音:theta、西塔;既是上界也是下界(tight),等于的意思。
  • Ο,读音:big-oh、Omicron(欧米可荣,大写);表示上界(tightness unknown),小于等于的意思。
  • ο,读音:small-oh、Omicron(欧米可荣,小写);表示上界(not tight),小于的意思。
  • Ω,读音:big omega、欧米伽(大写);表示下界(tightness unknown),大于等于的意思。
  • ω,读音:small omega、欧米伽(小写);表示下界(not tight),大于的意思。

排序算法的复杂度

Array Sorting Algorithms

Algorithm Time Complexity     Space Complexity
  Best Average Worst Worst
Quicksort Ω(n log(n)) Θ(n log(n)) O(n^2) O(log(n))
Mergesort Ω(n log(n)) Θ(n log(n)) O(n log(n)) O(n)
Timsort Ω(n) Θ(n log(n)) O(n log(n)) O(n)
Heapsort Ω(n log(n)) Θ(n log(n)) O(n log(n)) O(1)
Bubble Sort Ω(n) Θ(n^2) O(n^2) O(1)
Insertion Sort Ω(n) Θ(n^2) O(n^2) O(1)
Selection Sort Ω(n^2) Θ(n^2) O(n^2) O(1)
Tree Sort Ω(n log(n)) Θ(n log(n)) O(n^2) O(n)
Shell Sort Ω(n log(n)) Θ(n(log(n))^2) O(n(log(n))^2) O(1)
Bucket Sort Ω(n+k) Θ(n+k) O(n^2) O(n)
Radix Sort Ω(nk) Θ(nk) O(nk) O(n+k)
Counting Sort Ω(n+k) Θ(n+k) O(n+k) O(k)
Cubesort Ω(n) Θ(n log(n)) O(n log(n)) O(n)

算法设计

  • 分治算法:分而治之,将原问题分割成同等结构的子问题,之后将子问题逐一解决后,原问题也得到了解决
  • 分治:归并排序、快速排序、树结构
  • 贪心:选择排序、堆、Kruskal、Prim、Dijkstra
  • 递归回溯:树的遍历、图的遍历
  • 动态规划:Prim、Dijkstra