Acwing学习——快速排序

133 阅读4分钟

1. 概念

快速排序的核心是分治思想,且还利用到了递归思想,每次确定原数组中的一个中间数,使得其左边数全小于右边数,依次再对左右两边进行递归,直到原数组完全有序为止。

后续介绍均采用递增排序

2. 步骤

  1. 确定分界点:确定一个中间数(记为X),这个数可以是数组首元素,也可以是数组末元素,以及数组中的任意一个元素都可以。
  2. 调整区间:对数组进行局部排序,即将所有小于X的元素放在X的左边,所有大于等于X的元素放在元素右边。那么此时也使得X位于最终位置,并且X左边的元素全部小于XX右边的元素全部大于等于X
  3. 递归处理:对X左边的子数组和X右边的子数组分别重复上述操作。
  4. 将所有子数组进行排序后以及回归,即可确定原数组整体有序。

3. 重难点

不难发现,上述步骤中的步骤一、步骤三、步骤四其实都非常简单,主要重难点是步骤二,如何将比X小的数放到X的左边以及将比X大的数放在X的右边。

这里介绍两种方法:

3.1. 暴力法

利用两个额外数组,分别记为a数组b数组,对原数组进行遍历,将小于X的数存入a数组,将大于等于X的数存入b数组。从原数组首地址位置开始,对a数组进行遍历,开始依次存入原数组,再将X存入原数组,最后再将b数组进行遍历依次填入原数组。此时便符合步骤二的最终结果。

但是这个方法也很容易发现,在每一趟子排序中,需要利用到O(n)的空间复杂度,并且需要对原数组进行两次遍历,即O(2n)的时间复杂度,效率极其之低。

3.2. 双指针

令X为原数组首元素,利用两个额外指针头指针i指向原数组首元素,尾指针j指向数组尾元素,从原数组的尾部开始遍历,找到第一个小于X的元素,将该元素存入i所指的位置,然后i依次向后遍历,找到第一个大于等于X的元素;重复此操作,直到i等于j为止,此时将X存入ij所占的位置即可。

在每一趟子排序中,利用双指针仅需要O(1)的空间复杂度,并且只需要O(n)的时间复杂度,相比于暴力法效率快了很多。

4. 代码模板

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) swap(q[i], q[j]);
    }
    quick_sort(q, l, j), quick_sort(q, j + 1, r);
}

5. 例题

5.1. 785. 快速排序 - AcWing题库

#include <iostream>

using namespace std;

const int N = 1e6 + 10;

int n;
int q[N];

void quick_sort(int q[], int l, int r) 
{
    if(l >= r) return;
    
    int x = q[l];
    // 或 int x = q[(l + r) / 2];
    int i = l - 1;
    int 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]); // swap是交换两个数的函数
    }
    
    quick_sort(q, l, j);
    // 或 quick_sort(q, l, i - 1);
    quick_sort(q, j + 1, r);
    // 或 quick_sort(q, i, r);
}

int main() 
{
    scanf("%d", &n);
    for(int i = 0; i < n; i++) scanf("%d", &q[i]);
    
    quick_sort(q, 0, n - 1);
    
    for(int i = 0; i < n; i++) printf("%d", q[i]);
    
    return 0;
}

5.2. 786. 第k个数 - AcWing题库

#include <iostream>

const int N = 100010;
int k;
int n;

int q[N];

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

int main()
{
    scanf("%d", &n);
    scanf("%d", &k);
    
    for(int i = 0; i < n; i++) scanf("%d", &q[i]);
    
    printf("%d", quick_sort(q, 0, n - 1));
    
    return 0;
}

6. 注意点

当使用quick_sort(q, l, j); quick_sort(q, j + 1, r);时,x不能取int x = q[r];,否则会导致边界问题,使得递归一直重复下去;

同理,当使用quick_sort(q, l, i - 1); quick_sort(q, i, r);时,x不能取int x = q[l];,否则会导致边界问题,也会使得递归一直重复下去。

例如:区间为0-1时,如果x取0(即最左端点).那么后续每次右递归都是每次0-1,因此会陷入无限的死循环。

7. 复杂度分析

元素组长度为N的情况下,当利用到了递归操作,需要考虑由长度为N的元素,所产生的二叉树的高度即lognN,空间复杂度为O(logN),而时间复杂度为O(NlogN),共进行logN趟排序,而每趟排序的时间复杂度为O(N)