快速排序算法

一、算法概述

快速排序是一种不稳定的原地排序算法,其核心特点是"分治+分区":通过选择一个"基准元素"(pivot),将待排序序列分割成两个子序列,其中左子序列的所有元素均小于等于基准元素,右子序列的所有元素均大于等于基准元素(具体分区规则可灵活调整),然后分别对这两个子序列递归执行快速排序,直到所有子序列长度为1(此时子序列已天然有序),排序完成。

二、核心原理与分区逻辑

快速排序的整个执行流程可分为"分区"和"递归排序"两个核心步骤,其中"分区"是快速排序的灵魂,直接决定了算法的效率;"递归排序"则是对分区后的子序列重复执行相同的逻辑,直至排序完成。

1. 核心思想(分而治之)

快速排序的分而治之思想可拆解为以下三个步骤,形成一个递归闭环:

分解:选择一个基准元素,将待排序序列 ​ arr[low...high] 分区为左子序列 ​ arr[low...pivotIndex−1] 、基准元素 ​ arr[pivotIndex] 和右子序列 ​ arr[pivotIndex+1...high] ,满足左子序列所有元素≤基准元素,右子序列所有元素≥基准元素;​

解决:递归调用快速排序,分别对左子序列 ​ arr[low...pivotIndex−1] 和右子序列arr[pivotIndex+1...high] 进行排序;​

合并:由于左、右子序列的排序是在原数组上进行的,且基准元素已处于最终的正确位置,因此无需额外的合并操作,递归结束后整个序列即有序。

2. 分区操作(Partition)

分区操作是快速排序的核心,其目的是找到基准元素的最终位置,并将序列分割为符合要求的左右两个子序列。常用的分区方式有"霍尔分区法"(Hoare Partition Scheme)和" Lomuto 分区法"(Lomuto Partition Scheme),其中霍尔分区法是快速排序的原始分区方式,效率更高; Lomuto 分区法逻辑更简单,易于实现,但效率略低。本文重点讲解霍尔分区法,并简要介绍 Lomuto 分区法。

(1)霍尔分区法

霍尔分区法的核心逻辑的是:选择一个基准元素(通常选择序列的第一个元素、最后一个元素或中间元素),设置两个指针(left和right)分别指向序列的起始位置和终止位置,然后通过指针移动,将小于基准元素的元素移到左侧,大于基准元素的元素移到右侧,最终确定基准元素的位置。具体步骤如下:

  1. 选择基准元素pivot,本文暂选序列的第一个元素(arr[low]);

  2. 初始化指针left = low + 1(指向基准元素的下一个位置),right = high(指向序列的最后一个位置);

  3. 移动left指针,直到找到第一个大于pivot的元素(arr[left] > pivot),停止移动;

  4. 移动right指针,直到找到第一个小于pivot的元素(arr[right] < pivot),停止移动;

  5. 若left < right,交换arr[left]和arr[right],然后继续执行步骤3和步骤4;

  6. 若left ≥ right,交换arr[low](基准元素)和arr[right],此时基准元素pivot的位置即为right,分区完成。

需要注意的是,霍尔分区法中,基准元素最终会与right指针指向的元素交换,因为right指针停止时,指向的必然是小于基准元素的元素,交换后可保证基准元素左侧均为小于它的元素,右侧均为大于它的元素。

(2)Lomuto 分区法

Lomuto 分区法的逻辑更简洁,核心是选择一个基准元素(通常选择序列的最后一个元素),设置一个指针i(指向小于基准元素的区域的右边界),然后遍历序列从low到high-1,将小于基准元素的元素与i指针指向的元素交换,并移动i指针,遍历结束后,将基准元素与i指针指向的元素交换,确定基准元素的位置。具体步骤如下:

  1. 选择序列的最后一个元素作为基准元素pivot = arr[high];

  2. 初始化指针i = low - 1(表示小于基准元素的区域初始为空);

  3. 遍历j从low到high-1:

    1. 若arr[j] ≤ pivot,将i指针加1,交换arr[i]和arr[j];
  4. 遍历结束后,将i指针加1,交换arr[i]和arr[high](基准元素),此时基准元素的位置即为i,分区完成。

Lomuto 分区法的优势是逻辑简单,易于理解和实现,但由于每次都选择最后一个元素作为基准,在序列有序时会出现最坏情况,且交换次数略多于霍尔分区法。

三、算法实现(以Java为例)

本文分别实现霍尔分区法和 Lomuto 分区法的快速排序,均采用递归方式实现,同时补充非递归实现(避免递归栈溢出),并附带详细注释,便于读者理解和调试。

(1)霍尔分区法​

java 复制代码
/**
 * 快速排序(霍尔分区法)
 * @param arr 待排序数组
 * @param low 排序起始索引
 * @param high 排序终止索引
 */
public static void quickSortHoare(int[] arr, int low, int high) {
    // 递归终止条件:当起始索引大于等于终止索引时,子序列长度为1或0,已有序
    if (low < high) {
        // 执行分区操作,获取基准元素的最终位置
        int pivotIndex = partitionHoare(arr, low, high);
        // 递归排序左子序列(基准元素左侧)
        quickSortHoare(arr, low, pivotIndex - 1);
        // 递归排序右子序列(基准元素右侧)
        quickSortHoare(arr, pivotIndex + 1, high);
    }
}

/**
 * 霍尔分区法:找到基准元素的最终位置,分割数组
 * @param arr 待分区数组
 * @param low 分区起始索引
 * @param high 分区终止索引
 * @return 基准元素的最终索引
 */
private static int partitionHoare(int[] arr, int low, int high) {
    // 选择序列第一个元素作为基准元素(可优化,后续讲解)
    int pivot = arr[low];
    int left = low + 1; // 左指针,从基准元素下一个位置开始
    int right = high; // 右指针,从序列末尾开始

    while (true) {
        // 移动左指针,找到第一个大于基准元素的元素
        while (left <= right && arr[left] <= pivot) {
            left++;
        }
        // 移动右指针,找到第一个小于基准元素的元素
        while (left <= right && arr[right] > pivot) {
            right--;
        }
        // 若左指针大于右指针,分区结束
        if (left > right) {
            break;
        }
        // 交换左、右指针指向的元素
        swap(arr, left, right);
        // 交换后,移动指针继续遍历
        left++;
        right--;
    }
    // 交换基准元素和右指针指向的元素,确定基准元素最终位置
    swap(arr, low, right);
    return right; // 返回基准元素索引
}

/**
 * 辅助方法:交换数组中两个索引位置的元素
 * @param arr 数组
 * @param i 索引1
 * @param j 索引2
 */
private static void swap(int[] arr, int i, int j) {
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

// 快速排序调用入口(简化用户调用)
public static void quickSortHoare(int[] arr) {
    // 边界校验:数组为空或长度小于2,无需排序
    if (arr == null || arr.length < 2) {
        return;
    }
    quickSortHoare(arr, 0, arr.length - 1);
}

(2)Lomuto 分区法​

java 复制代码
/**
 * 快速排序(Lomuto 分区法)
 * @param arr 待排序数组
 * @param low 排序起始索引
 * @param high 排序终止索引
 */
public static void quickSortLomuto(int[] arr, int low, int high) {
    if (low < high) {
        int pivotIndex = partitionLomuto(arr, low, high);
        quickSortLomuto(arr, low, pivotIndex - 1);
        quickSortLomuto(arr, pivotIndex + 1, high);
    }
}

/**
 * Lomuto 分区法:找到基准元素的最终位置,分割数组
 * @param arr 待分区数组
 * @param low 分区起始索引
 * @param high 分区终止索引
 * @return 基准元素的最终索引
 */
private static int partitionLomuto(int[] arr, int low, int high) {
    // 选择序列最后一个元素作为基准元素
    int pivot = arr[high];
    int i = low - 1; // 小于基准元素的区域右边界

    // 遍历序列,将小于等于基准元素的元素移到左侧区域
    for (int j = low; j < high; j++) {
        if (arr[j] <= pivot) {
            i++;
            swap(arr, i, j);
        }
    }
    // 交换基准元素和左侧区域的下一个元素,确定基准元素位置
    swap(arr, i + 1, high);
    return i + 1; // 返回基准元素索引
}

// 快速排序调用入口
public static void quickSortLomuto(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    quickSortLomuto(arr, 0, arr.length - 1);
}
相关推荐
机器视觉的发动机1 小时前
图像处理-机器视觉算法中的数学基础
开发语言·人工智能·算法·决策树·机器学习·视觉检测·机器视觉
Darkwanderor2 小时前
离散化思维的应用
数据结构·c++·算法·哈希算法
健忘的派大星3 小时前
需求激增800%!2025年第一硬通货:懂大模型、云计算和硬件的“前沿部署工程师”!
人工智能·算法·架构·langchain·云计算·大模型学习·大模型教程
代码改善世界10 小时前
【数据结构与算法】栈和队列题解
数据结构
ShineWinsu10 小时前
对于C++:继承的解析—上
开发语言·数据结构·c++·算法·面试·笔试·继承
pp起床10 小时前
动态规划 | part05
算法·动态规划
GuangHeAI_ATing10 小时前
国密算法SSD怎么选?这3款国产固态硬盘安全又高速
算法
雨泪丶11 小时前
代码随想录算法训练营-Day34
算法
Yzzz-F11 小时前
牛客寒假算法训练营2
算法