【算法学习笔记】8:快速排序中的边界问题

1 基本思路

快排的思想是基于分治法,其思路是:

  1. 确定分界点:给定要排序的数组q,先在数组中找一个数作为分界点值x,分界点可以取左边界值x = q[l],可以取右边界值x = q[r],可以取数组中间的数x = q[l + r >> 1],也可以随机取一个。
  2. 调整区间:将数组划分成两个区间,使得左半部分所有的数都<= x,右半部分所有的数都>= x
  3. 递归处理左右两个区间。

快排有多种实现方法,在y总的模板里,分界点的位置不一定是x,因为x参与交换之后仍然会被留在左右区间中的一个里。

2 指针移动的判断不带等号

使用两个指针ij分别指向要处理的区间的左右两侧,每次向中间移动。只要q[i] < x成立就说明i位置的数在左侧区间是合理的,所以i ++,直到q[i] >= x停下来。接下来去移动j,只要q[j] > x成立就说明j位置的数在右侧区间是合理的,所以j --,直到q[j] <= x停下来。

这里考虑一个边界问题,为什么移动ij指针的条件是q[i] < xq[j] > x,而不是q[i] <= xq[j] >= x?因为如果选取的x是数组里最大的数,那么一直都满足q[i] <= x,所以i会一直++发生越界都不会停下来。同理,如果选取的x是数组里最小的数,那么一直都满足q[j] >= x,所以j会一直--发生越界都不会停下来。

反例数据:

3
1 3 2

说明:

第一轮时选取的3是数组里最大的,所以在第一轮时i就会一直++发生越界。

3 在交换前检查指针相对位置

当两个指针都停下来之后,这一对数都是错位的,所以把它们交换一下,交换完成之后q[i] < x并且q[j] > x,下一轮就可以让ij(只要满足i < j)继续对向移动了。

这里考虑一个边界问题,试想q[i]q[j]i == j - 1时停下来做交换的场景,交换完成之后ij会各自前进(i ++, j --)一步,形成i > j(具体是i == j + 1)的不合法局面,这时候就不应该做交换了,所以在swap之前需要再判断一次i < j

反例数据:

3
3 1 2

说明:

交换完31后形成1 3 2的局面,i ++变成1指向元素3j --变成0指向元素1,没有做检查指针相对位置的判断,又发生了一次交换,恢复成3 1 2的局面。

4 使用do-while而不是while循环

指针ij初始化为数组两侧外一个元素,即i = l - 1j = r + 1,然后在数组中使用do-while循环每次先进行一次指针的移动,再去看循环条件。

这里考虑一个边界问题,为什么不能让i = lj = r然后使用while循环代替do-while循环?因为如果数组中存在重复的数字,那么某一轮可能存在ij都指向重复的数字,并且划分数字x也是这个数字,那么while (q[i] < x)while (q[j] > x)判断不成立不会进入,又因为q[i] = q[j] = x,交换它们之后这个局面仍然不会改变,所以要使用do-while循环,确保每次两个指针都至少会移动一步,以保证上一次交换的结果能被走掉。

反例数据:

3
2 1 2

说明:

i指向2j指向2并且x = 2的一次处理中,一直重复发生这两个2的交换。

5 选取数组中间的数字作为划分值

与其选取左右两端的数作为划分值,不妨选取数组中间的数字。

这里考虑一个边界问题,如果数组已经是有序的了,那么选取数组开头或者结尾的数字就意味着每次将长度为 n n n的数组划分成了长度为 1 1 1 n − 1 n-1 n1的两段,这时候快速排序递归处理的子问题规模还是这么大,总的时间复杂度就达到了 O ( n 2 ) O(n^2) O(n2),为了避免这种情况,要选取数组中间的数字作为划分值。

反例数据:

任意已排序的数组

说明:

只影响时间,不影响正确性。

6 使用[l, j]作为区间左半边而不是[l, i]

在快排一轮的处理结束后,递归处理的两个子区间应该是[l, j][j + 1, r]而不是[l, i][i + 1, r]

这里考虑一个边界问题,试想q[i]q[j]i == j - 1时停下来做交换的场景,交换完成之后ij会各自前进(i ++, j --)一步,形成i > j(具体是i == j + 1)的不合法局面。在这个局面下,满足性质<= x的区间是[l, j]而不是[l, i],因此划分的两个区间是[l, j][j + 1, r]

反例数据:

3
3 1 2

说明:

第一轮时选取的x = 1,在31发生交换后,i ++变成1j --变成0,满足性质<= 1的区间应当是[0, j = 0],而不是[0, i = 1]

7 模板题

AcWing 785 快速排序

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int n;
int q[N];

void quick_sort(int q[], int l, int r) {
    if (l >= r)
        return ;

    int x = q[l + r >> 1];
    int i = l - 1, j = r + 1;
    while (i < j) {
        do i ++ ; while (q[i] < x);
        do j -- ; while (q[j] > x);
        if (i < j)
            swap(q[i], q[j]);
    }
    
    quick_sort(q, l, j);
    quick_sort(q, j + 1, r);
}

int main() {
    cin >> n;
    for (int i = 0; i < n; i ++ ) cin >> q[i];
    
    quick_sort(q, 0, n - 1);
    
    for (int i = 0; i < n; i ++ ) cout << q[i] << ' ';
    
    return 0;
}

AcWing 786 第k个数(快速选择,k缩减版)

#include <iostream>

using namespace std;

const int N = 1e5 + 10;

int a[N];

int quick_sort(int q[], int l, int r, int k) {
    if (l >= r) return q[l];
    int i = l - 1, j = r + 1;
    int x = q[i + j >> 1];
    while (i < j) {
        do i ++; while (q[i] < x);
        do j --; while (q[j] > x);
        if (i < j)
            swap(q[i], q[j]);
    }
    if (j - l + 1 >= k)
        return quick_sort(q, l, j, k);
    else
        return quick_sort(q, j + 1, r, k - (j - l + 1));
}


int main() {
    int n, k;
    cin >> n >> k;
    
    for (int i = 0; i < n; i ++)
        cin >> a[i];
    
    cout << quick_sort(a, 0, n - 1, k);
    
    return 0;
}

LeetCode 215 数组中的第k个最大元素(快速选择,k不变版)

class Solution {
public:
    int quick_sort(vector<int>& nums, int l, int r, int k) {
        if (l >= r) return nums[k]; // 因为这种写法k在子问题里不变所以这里还是nums[k]
        int x = nums[l + r >> 1];
        int i = l - 1, j = r + 1;
        while (i < j) {
            do i ++ ; while (nums[i] > x);
            do j -- ; while (nums[j] < x);
            if (i < j) swap(nums[i], nums[j]);
        }
        // 注意因为这种写法k在子问题里不变,这里的k要直接和j比较,与l无关
        if (k <= j) return quick_sort(nums, l, j, k);
        else return quick_sort(nums, j + 1, r, k);
    }

    int findKthLargest(vector<int>& nums, int k) {
        return quick_sort(nums, 0, nums.size() - 1, k - 1); // 第k个=>有序时下标为k-1
    }
};

注意,k不变版和k缩减版的选择,与使用的是数组还是vector无关,只和在子问题里是否选择去改变k有关系,这个完全看个人喜好了,我喜欢k缩减版。

8 Java实现

import java.util.Scanner;


public class Main {
    static int[] a;

    public static void quick_sort(int[] q, int l, int r) {
        if (l >= r) return;
        int i = l - 1, j = r + 1, x = q[l + r >> 1];
        while (i < j) {
            do i++; while (q[i] < x);
            do j--; while (q[j] > x);
            if (i < j) {
                // 注意Java没法实现swap函数
                int t = q[i];
                q[i] = q[j];
                q[j] = t;
            }
        }
        quick_sort(q, l, j);
        quick_sort(q, j + 1, r);
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        a = new int[n];
        for (int i = 0; i < n; i++)
            a[i] = scanner.nextInt();
        quick_sort(a, 0, n - 1);
        for (int i = 0; i < n; i++)
            System.out.print(a[i] + " ");
        System.out.println();
    }
}
  • 65
    点赞
  • 122
    收藏
    觉得还不错? 一键收藏
  • 22
    评论
快速排序是一种常用的排序算法,它通过分治法的思想将一个大问题分解为多个小问题来解决。在快速排序,我们选择一个基准元素,将数组分为两部分,一部分是小于基准元素的,另一部分是大于基准元素的。然后对这两部分分别进行递归排序,最终得到有序的数组。 下面是C++实现的快速排序代码,其包括了边界问题的处理[^1][^2]: ```cpp #include <iostream> using namespace std; // 交换两个元素的值 void swap(int* a, int* b) { int temp = *a; *a = *b; *b = temp; } // 将数组分为两部分,并返回基准元素的索引 int partition(int arr[], int low, int high) { int pivot = arr[high]; // 选择最后一个元素作为基准元素 int i = (low - 1); // i指向小于基准元素的位置 for (int j = low; j <= high - 1; j++) { // 如果当前元素小于等于基准元素,则将其放到左边 if (arr] <= pivot) { i++; swap(&arr[i], &arr[j]); } } swap(&arr[i + 1], &arr[high]); // 将基准元素放到正确的位置 return (i + 1); } // 快速排序函数 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[] = {64, 25, 12, 22, 11}; int n = sizeof(arr) / sizeof(arr[0]); quickSort(arr, 0, n - 1); cout << "Sorted array: "; for (int i = 0; i < n; i++) { cout << arr[i] << " "; } cout << endl; return 0; } ``` 这段代码实现了快速排序算法,并对一个示例数组进行排序。你可以根据需要修改数组的内容和大小,然后运行代码查看排序结果。
评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值