Bootstrap

Java算法OJ(7)随机快速排序

fb7f538c48644005a107eb6a3a760d38.jpeg


目录

1.前言

2.正文

1. 快速排序的基本原理

2. 随机快速排序的改进

3. 随机快速排序的步骤

3.小结


1.前言

哈喽大家好吖,今儿给大家带来算法—随机快速排序相关知识点,废话不多说让我们开始。

2.正文

在了解随机快排之前,先了解一下原始的快速排序是什么:

1. 快速排序的基本原理

快速排序是一种基于分治法的排序算法,主要步骤包括:

  1. 选取基准:从数组中选取一个基准元素,通常选择第一个元素、最后一个元素或中间元素。
  2. 分区:将数组分为两个子数组,使得左子数组的元素都小于基准,右子数组的元素都大于基准。
  3. 递归排序:分别对左、右子数组递归进行快速排序,直到子数组的长度为1。
public static void quickSort1(int[] arr, int l, int r) {
    if (l >= r) {
        return;
    }
    int x = arr[l + (int) (Math.random() * (r - l + 1))]; // 随机选择基准值
    int mid = partition1(arr, l, r, x); // 分区操作
    quickSort1(arr, l, mid - 1); // 递归排序左部分
    quickSort1(arr, mid + 1, r); // 递归排序右部分
}

 分区过程(partition1

  • 遍历 arr[l...r] 的所有元素,将小于等于 x 的元素放到左侧(<=x 区域),大于 x 的元素放到右侧。
  • 使用一个变量 a 来记录 <=x 区域的边界,并动态更新位置。
  • 遍历完成后,将等于 x 的最后一个元素放在 <=x 区域的最后位置,以确保分区后的基准值处于正确位置。
public static int partition1(int[] arr, int l, int r, int x) {
    int a = l, xi = 0; // a 记录 <=x 区域的边界,xi 用于记录 x 的位置
    for (int i = l; i <= r; i++) {
        if (arr[i] <= x) {
            swap(arr, a, i);
            if (arr[a] == x) {
                xi = a;
            }
            a++;
        }
    }
    swap(arr, xi, a - 1); // 将 x 放到 <=x 区域的末尾
    return a - 1; // 返回 x 的最终位置
}

2. 随机快速排序的改进

在传统的快速排序中,选择固定的基准可能导致较差的时间复杂度。比如,如果数组已经有序或逆序,选择第一个或最后一个元素作为基准会导致算法的性能退化到 O(n2)O(n^2)O(n2)。

改进版使用了“荷兰国旗问题”的分区方式,将数组分为小于基准值、等于基准值、大于基准值三部分,分别放在左中右,即不再是使用俩块的分区方式,而是在一个分区中将所有都等于基准值的数字都放到中间。这样在处理大量重复元素时效率更高。

3. 随机快速排序的步骤

public static void quickSort2(int[] arr, int l, int r) {
    if (l >= r) {
        return;
    }
    int x = arr[l + (int) (Math.random() * (r - l + 1))]; // 随机选择基准值
    partition2(arr, l, r, x); // 分区操作
    int left = first; // 左边界(小于 x 的部分的末尾)
    int right = last; // 右边界(等于 x 的部分的末尾)
    quickSort2(arr, l, left - 1); // 递归排序小于 x 的部分
    quickSort2(arr, right + 1, r); // 递归排序大于 x 的部分
}

分区过程(partition2

  • partition2 使用双指针方法实现了“荷兰国旗”分区。
  • 使用三个区域:<x(小于 x 的区域)、==x(等于 x 的区域)和 >x(大于 x 的区域)。
  • 通过遍历 arr[l...r],将小于 x 的元素放在左边,大于 x 的元素放在右边,等于 x 的元素放在中间。
  • 更新全局变量 firstlast,分别指向 ==x 区域的左右边界
public static int first, last;

public static void partition2(int[] arr, int l, int r, int x) {
    first = l;
    last = r;
    int i = l;
    while (i <= last) {
        if (arr[i] == x) {
            i++;
        } else if (arr[i] < x) {
            swap(arr, first++, i++);
        } else {
            swap(arr, i, last--);
        }
    }
}

逻辑

  • 初始时 first 指向 llast 指向 ri 指针用于遍历数组。
  • arr[i] == x 时,i 向右移动,继续检查下一个元素。
  • arr[i] < x 时,表示当前元素属于 <x 区域,将其交换到 first 所指位置,并同时增大 firsti
  • arr[i] > x 时,表示当前元素属于 >x 区域,将其交换到 last 所指位置,并减少 last
  • 这样在遍历完成后,<x 区域、==x 区域和 >x 区域的边界已经划分完毕,firstlast 分别指向 ==x 区域的左边界和右边界。
public static void swap(int[] nums, int i, int j) {
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }

这里再对比一下改进前和改进后的不同:

| 比较项       | quickSort1                                    | quickSort2                                                |
|--------------|----------------------------------------------|-----------------------------------------------------------|
| **效率**     | 适合普通数据集,但处理重复元素效率较低             | “荷兰国旗”分区方式更适合含有重复元素的数组,将等于基准值的部分集中在中间,减少重复元素的处理次数 |
| **递归调用** | 递归划分基于一个位置                             | 递归划分基于两个位置(`first` 和 `last`),有效减少递归深度 |
| **使用场景** | 适合一般数据集,重复元素较多时效率较低             | 推荐用于重复元素较多的数组,一般情况下也更高效                  |

下面这俩个是俩个排序的测试链接,一个是填函数风格,另一个是acm练习风格,本文仅将核心部分罗列出来,剩下的调试部分就交给大家了。

P1177 【模板】排序 - 洛谷 | 计算机科学教育新生态https://www.luogu.com.cn/problem/P1177912. 排序数组 - 力扣(LeetCode)https://leetcode.cn/problems/sort-an-array/description/

3.小结

今天的分享到这里就结束了,喜欢的小伙伴点点赞点点关注,你的支持就是对我最大的鼓励,大家加油!

 

 

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;