跳转至

快速排序

一、算法描述

1.1 核心思想

  • 二分,选取一个分区值,将数据分割成 2 部分:小于和大于
  • 递归,对小于大于两部分再排序,不断选点分割数据,直到无法分割为止
  • 整个二分过程类似于一棵二叉树,从上往下排序,先确定根节点位置,再处理子树排序

1.2 细节解释

比如说, 数组 [5, 7, 2, 4, 3, 6, 1]。

首先选取一个分区值(随意选一个),比如说 5。

以 5 基准,将其他数据分割成 2 部分:一部分小于 5,一部分大于 5。

      小于5                     大于5
 ___ ___ ___ ___               ___ ___
| 2 | 4 | 3 | 1 |             | 7 | 6 |

然后将 5 插入 2 部分的中间:

      小于5                     大于5
 ___ ___ ___ ___      ___      ___ ___
| 2 | 4 | 3 | 1 |    | 5 |    | 7 | 6 |

此时 5 就已经排好了。

下面接着对左右 2 部分进行递归排序。

(1) 先对左边小于 5 的部分排序

首先选取一个分区值 2,然后以 2 为基准分割成 2 部分:

小于2           大于2
 ___           ___ ___
| 1 |         | 4 | 3 |

将 2 插入中间位置:

小于2           大于2
 ___    ___    ___ ___
| 1 |  | 2 |  | 4 | 3 |

此时 2 就排好了。

接着不断往下递归,直到无法分割为止,最后可以得到:

 ___    ___    ___    ___
| 1 |  | 2 |  | 3 |  | 4 |

(2) 再对右边大于 5 的部分排序

同理,也是递归分割数据,最后可以得到:

 ___    ___
| 6 |  | 7 |

(3) 最后完整的序列结果

 ___    ___    ___    ___    ___    ___    ___
| 1 |  | 2 |  | 3 |  | 4 |  | 5 |  | 6 |  | 7 |

至此排序就完成了,快速排序本质上是一种分治算法,将大排序变成小排序来做。

二、算法实现

2.1 递归二分排序

void partSort(int[] arr, int start, int end) {
    if (start < end) {
        // 分区
        int m = partition(arr, start, end);
        // 左边排序
        partSort(arr, start, m);
        // 右边排序
        partSort(arr, m + 1, end);
    }
}

2.2 对数据进行分区

int partition(int[] arr, int start, int end) {
    // 选取分区点
    int p = pivot(arr, start, end - 1);
    // 将分区点放到最前面
    swap(arr, start, p);
    int ref = arr[start];
    int lp = start;
    // 将数据与分区点对比,分成小于和大于2部分
    for (int i = lp + 1; i < end; i++) {
        if (arr[i] <= ref) {
            swap(arr, ++lp, i);
        }
    }
    // 将分区点放到它最终的位置
    swap(arr, start, lp);
    return lp;
}

2.3 分区点的选择

// 采用左中右三点取中值的方式
int pivot(int[] arr, int i, int j) {
    int mid = i + (j - i) / 2;
    if (arr[i] < arr[j]) {
        return arr[mid] > arr[i] ? mid : j;
    } else {
        return arr[mid] < arr[i] ? mid : i;
    }
}

三、算法分析

3.1 时间复杂度

  • 最好时间复杂度:O(nlogn)
  • 最坏时间复杂度:O(n^2)
  • 平均时间复杂度:O(nlogn)

3.2 空间复杂度

  • 空间复杂度:O(logn)
  • 原地算法

3.3 稳定性

  • 不稳定排序算法

四、适用场景

  • 数据量比较大
  • 数据有序性比较差

附录

/**
 * 快速排序
 * <p>
 * 时间复杂度:最好 O(nlogn) 最差 O(n^2) 平均 O(nlogn)
 * <p>
 * 空间复杂度:O(logn)
 * <p>
 * 稳定性:不稳定
 *
 * @author weijiaduo
 * @since 2022/7/16
 */
public class QuickSort implements Sort {

    /**
     * 排序
     *
     * @param arr 数组
     */
    @Override
    public void sort(int[] arr) {
        partSort(arr, 0, arr.length);
    }

    /**
     * 递归排序
     *
     * @param arr   数组
     * @param start [start, end)
     * @param end   [start, end)
     */
    private void partSort(int[] arr, int start, int end) {
        if (start < end) {
            // 分区
            int m = partition(arr, start, end);
            // 左边排序
            partSort(arr, start, m);
            // 右边排序
            partSort(arr, m + 1, end);
        }
    }

    /**
     * 二分数组
     *
     * @param arr   数组
     * @param start [start, end)
     * @param end   [start, end)
     * @return 分隔点索引
     */
    private int partition(int[] arr, int start, int end) {
        // 选取分区点
        int p = pivot(arr, start, end - 1);
        // 将分区点放到最前面
        swap(arr, start, p);
        int ref = arr[start];
        int lp = start;
        // 将数据与分区点对比,分成小于和大于2部分
        for (int i = lp + 1; i < end; i++) {
            if (arr[i] <= ref) {
                swap(arr, ++lp, i);
            }
        }
        // 将分区点放到它最终的位置
        swap(arr, start, lp);
        return lp;
    }

    /**
     * 选择分区点(选择三个点的中值位置)
     *
     * @param arr 数组
     * @param i   [i, j]
     * @param j   [i, j]
     * @return 分区点索引
     */
    private int pivot(int[] arr, int i, int j) {
        int mid = i + (j - i) / 2;
        if (arr[i] < arr[j]) {
            return arr[mid] > arr[i] ? mid : j;
        } else {
            return arr[mid] < arr[i] ? mid : i;
        }
    }

}