【算法系列】快速排序详解

快速排序的多种实现方式

快速排序(Quick Sort)是一种高效的排序算法,采用分治法策略。它通过选择一个“基准”元素,将数组分割成两个子数组,并递归地对这两个子数组进行排序。本文将详细介绍几种常见的快速排序实现方式,并讨论它们的特点和适用场景。
在这里插入图片描述

1. 基本快速排序(Lomuto 分区方案)

1.1 基本原理

Lomuto 分区方案是最常见的快速排序实现之一。它选择数组的最后一个元素作为基准(pivot),然后重新排列数组,使得所有小于基准的元素位于基准的左侧,所有大于基准的元素位于基准的右侧。

1.2 步骤

  1. 选择基准:通常选择数组的最后一个元素作为基准。
  2. 初始化指针:设置一个指针 i,用于追踪当前小于基准的最后一个元素的位置。
  3. 遍历数组
    • 遍历数组中的每个元素(除了基准元素),如果当前元素小于等于基准,则将该元素与 i 指针所指向的元素交换,并将 i 向右移动一位。
  4. 放置基准:最后将基准元素与 i + 1 位置的元素交换,使得基准元素处于正确的位置。
  5. 递归排序:对基准两侧的子数组分别递归执行上述过程,直到每个子数组只剩下一个元素或为空。

1.3 Java 实现示例

public static void quickSort(int[] arr) {
    quickSort(arr, 0, arr.length - 1);
}

private static void quickSort(int[] arr, int low, int high) {
    if(low >= high) {
        return;
    }
    int pivotIndex = lomuto(arr, low, high);
    quickSort(arr, low, pivotIndex - 1);
    quickSort(arr, pivotIndex + 1, high);
}

private static int lomuto(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 ++;
            if(i != j) {
                swap(arr, i, j);
                System.out.println(low + "|" + high + " " + i + "|" + j + " " + Arrays.toString(arr));
            }
        }
    }

    // 将基准元素放回数组正确位置
    swap(arr, i + 1, high);
    System.out.println(low + "|" + high + "\t \t" + Arrays.toString(arr));
    return i + 1;
}

/**
 * 交换数组索引为i和j的两个元素
 * @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;
}

2. Hoare 分区方案

2.1 基本原理

Hoare 分区方案由 C.A.R. Hoare 提出,是另一种常见的快速排序实现。它通过选择一个基准元素(通常是第一个或最后一个元素),然后重新排列数组,使得所有小于基准的元素位于基准的左侧,所有大于基准的元素位于基准的右侧。

2.2 步骤

  1. 选择基准:通常选择数组的第一个元素作为基准。
  2. 初始化双指针:设置两个指针,分别指向数组的起始位置和结束位置。
  3. 移动指针:
    • 左指针向右移动,直到找到一个大于基准的元素。
    • 右指针向左移动,直到找到一个小于基准的元素。
  4. 交换元素:当左右指针都停止时,交换这两个元素的位置。
  5. 重复步骤3和4:继续移动指针并交换元素,直到左右指针相遇。
  6. 放置基准:最后将基准元素与右指针的位置交换,使得基准元素处于正确的位置。

2.3 Java 实现示例

public static void quickSort(int[] arr) {
    quickSort(arr, 0, arr.length - 1);
}

private static void quickSort(int[] arr, int low, int high) {
    if(low >= high) {
        return;
    }

    int pivotIndex = hoare(arr, low, high);
    quickSort(arr, low, pivotIndex - 1);
    quickSort(arr, pivotIndex + 1, high);
}

private static int hoare(int[] arr, int low, int high) {
    int pivot = arr[low]; // 选择一个元素作为基准
    int l = low; // 左索引
    int r = high; // 右索引
    while(l < r) {
        while(l < r && arr[r] > pivot) {
            r --;
        }
        while(l < r && arr[l] < pivot) {
            l ++;
        }

        if(l < r) {
            swap(arr, l, r);
            System.out.println(l + "|" + r + "\t\t" + Arrays.toString(arr));
        }
    }
    return l;
}

/**
 * 交换数组索引为i和j的两个元素
 * @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;
}

3. 三数取中法

3.1 基本原理

为了减少快速排序在最坏情况下的时间复杂度(即 O(n^2)),可以使用“三数取中法”来选择基准元素。这种方法通过选择数组的第一个、中间和最后一个元素中的中位数作为基准,从而减少最坏情况的发生概率。

3.2 步骤

  1. 选择基准:选择数组的第一个、中间和最后一个元素中的中位数作为基准。
  2. 分区操作:根据选择的基准进行分区操作,可以使用 Lomuto 或 Hoare 分区方案。
  3. 递归排序:对基准两侧的子数组分别递归执行上述过程,直到每个子数组只剩下一个元素或为空。

3.3 Java 实现示例

public static void quickSort(int[] arr) {
    quickSort(arr, 0, arr.length - 1);
}

private static void quickSort(int[] arr, int low, int high) {
    if (low >= high) {
        return;
    }
    int pivotIndex = lomuto(arr, low, high);
    quickSort(arr, low, pivotIndex - 1);
    quickSort(arr, pivotIndex + 1, high);
}

private static int lomuto(int[] arr, int low, int high) {
    int pivotIndex = medianOfThree(arr, low, high); // 使用三数取中法选择基准
    int pivot = arr[pivotIndex];
    int i = low - 1; // 指向当前小于基准的最后一个元素
    if (pivotIndex != high) {
        swap(arr, pivotIndex, high); // 将基准元素移到最后
        System.out.println("pivot:" + pivotIndex + "|" + high + "\t" + Arrays.toString(arr));
    }

    for (int j = low; j < high; j++) {
        if (arr[j] <= pivot) {
            i++;
            if (i != j) {
                swap(arr, i, j);
                System.out.println(low + "|" + high + " \t" + i + "|" + j + "\t" + Arrays.toString(arr));
            }
        }
    }

    // 将基准元素放回数组正确位置
    if (i + 1 != high) {
        swap(arr, i + 1, high);
        System.out.println("back:" + (i + 1) + "|" + high + "\t" + Arrays.toString(arr));
    }
    return i + 1;
}

/**
 * 选择数组的第一个、中间和最后一个元素中的中位数作为基准,返回其下标
 *
 * @param arr
 * @param low
 * @param high
 * @return
 */
private static int medianOfThree(int[] arr, int low, int high) {
    int mid = (low + high) / 2;

    if ((arr[low] <= arr[mid] && arr[mid] <= arr[high]) || (arr[low] >= arr[mid] && arr[mid] >= arr[high])) {
        return mid;
    }
    if ((arr[mid] <= arr[low] && arr[low] <= arr[high]) || (arr[mid] >= arr[low] && arr[low] >= arr[high])) {
        return low;
    }
    return high;
}

/**
 * 交换数组索引为i和j的两个元素
 *
 * @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;
}

4. 尾递归优化

4.1 基本原理

快速排序的递归实现可能导致栈溢出问题,特别是在处理大规模数据集时。为了避免这种情况,可以使用尾递归优化技术来减少递归调用栈的深度。

4.2 步骤

  1. 选择基准:选择数组的某个元素作为基准。
  2. 分区操作:根据选择的基准进行分区操作,可以使用 Lomuto 或 Hoare 分区方案。
  3. 尾递归优化:每次递归只对较小的子数组进行递归调用,较大的子数组则通过循环继续处理,从而减少递归调用栈的深度。

4.3 Java 实现示例

public static void quickSort(int[] arr) {
    quickSort(arr, 0, arr.length - 1);
}

private static void quickSort(int[] arr, int low, int high) {
    // 使用循环代替递归
    while (low < high) {
        int pivotIndex = hoare(arr, low, high);

        // 对较小的分区进行递归调用
        if (pivotIndex - low < high - pivotIndex) {
            quickSort(arr, low, pivotIndex - 1);
            low = pivotIndex + 1;
        } else {
            quickSort(arr, pivotIndex + 1, high);
            high = pivotIndex - 1;
        }
    }
}

private static int hoare(int[] arr, int low, int high) {
    int pivot = arr[low]; // 选择一个元素作为基准
    int l = low; // 左索引
    int r = high; // 右索引
    while (l < r) {
        while (l < r && arr[r] > pivot) {
            r--;
        }
        while (l < r && arr[l] < pivot) {
            l++;
        }

        if (l < r) {
            swap(arr, l, r);
            System.out.println(l + "|" + r + "\t\t" + Arrays.toString(arr));
        }
    }
    return l;
}

/**
 * 交换数组索引为i和j的两个元素
 *
 * @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;
}

总结

快速排序有多种实现方式,每种实现方式在不同的应用场景下可能有不同的性能表现:

  • Lomuto 分区方案:简单易懂,但相比 Hoare 分区方案,在某些情况下可能会有更多的交换操作,导致效率稍低。
  • Hoare 分区方案:通常比 Lomuto 分区更高效,因为它减少了不必要的交换操作,从而减少了时间复杂度。
  • 三数取中法:通过选择数组的第一个、中间和最后一个元素中的中位数作为基准,减少最坏情况的发生概率。
  • 尾递归优化:通过优化递归调用栈的深度,避免栈溢出问题,特别适合处理大规模数据集。
Lomuto分区算法是一种用于快速排序的简单直观的实现方法。它并不直接涉及数组的划分,而是作为快速排序的核心部分,在每一轮递归过程中完成元素的移动。以下是Lomuto划分的基本步骤: 1. **选择基准** (pivot): 通常选择待排序数组的第一个元素作为基准。 2. **设置两个指针** (indices): 分别从数组的起始位置i和结束位置j开始。 3. **分区过程**: - 当i小于等于j时: a. 如果当前指针i指向的元素大于或等于基准,则将i向右移动一位,并将比基准大的元素放在新位置上。 b. 否则,将i向右移动一位,继续比较。 4. **交换元素**: 当i > j时,说明已经找到了所有小于基准的元素的边界,这时将基准值与i+1位置的元素交换,使得基准值处于正确的位置。 5. **递归处理**: 对基准左边的子数组(索引范围[0, i])和右边的子数组(索引范围[i+1, n])分别进行同样的操作,直到整个序列有序。 下面是Lomuto划分的一个简化版的C语言实现示例: ```c #include <stdio.h> void swap(int* a, int* b) { int temp = *a; *a = *b; *b = temp; } int partition(int arr[], int low, int high) { int pivot = arr[low]; // 选择第一个元素作为基准 int i = low + 1; for (int j = low + 1; j <= high; j++) { if (arr[j] <= pivot) { i++; swap(&arr[i], &arr[j]); } } swap(&arr[low], &arr[i]); // 将基准放到正确的位置 return i; } void quicksort(int arr[], int low, int high) { if (low < high) { int pi = partition(arr, low, high); quicksort(arr, low, pi - 1); quicksort(arr, pi + 1, high); } } // 示例 int main() { int arr[] = {9, 7, 5, 11, 12, 2, 14, 3}; int n = sizeof(arr) / sizeof(arr[0]); quicksort(arr, 0, n - 1); printf("Sorted array: "); for (int i = 0; i < n; i++) printf("%d ", arr[i]); return 0; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值