基于快速排序的快速选择

基于快速排序的快速选择

         ——《数据结构与算法分析——C语言描述》

         快速选择有很多方法,这里不做一一介绍。《重谈快速排序》中我们介绍了快速排序的相关实现细节。这里我们介绍一种基于快速排序的快速选择方法。

         其实现原理和快速排序类似。

         给出一个序列,我们不知道该序列是否已经排好序,如果我们想从中选择第k小的数,该怎么做?最简单最直观的方法是对这个序列进行排序,然后依据k选择索引为k-1数即可。由于排序的时间复杂度为O(NlogN)。所有这种选择第k小的数的时间复杂度是O(NlogN)。

         这样会造成一定的浪费,因为我们仅仅是想从中选择第k个小的数,而对序列进行排序导致前k-1个数和后面的n-k个数都是有序的,这种有序对选择第k个小的数没有什么意义。所以,排序造成了一定的浪费。

         利用快速排序原理选择第k小的数,时间复杂度平均情况下可以达到O(N),最差的时间复杂度为O(N*N)。

         其原理是:每次对序列按照枢纽元进行左右风格,分割后,i指向枢纽元。如果k=i+1,则arr[i]就是序列中第k小的数;如果k<i+1,则第k小的数在左半部分,继续对左半部分进行分割,选择左半部分中第k个小的数;如果k>i+1,则第k小的数在右半部分,继续对右半部分进行分割,选择右半部分中第k-i-1小的数。

         如果我们直接在原序列上进行快速选择,则会打乱原来序列中元素的位置。如果不想打乱元素的位置,可以拷贝临时的一份作为快速选择的序列。

         下面我们对基于快速排序的快速选择进行实现如下:

// 基于快速排序的快速选择
#include <iostream>
using namespace std;

// 交换函数
void swap(int& a, int& b)
{
    int t = a;
    a = b;
    b = t;
}

// 该函数用于选择合适的枢纽元
// 具体做法是:选取三个元素,这三个元素为待排序序列中的第一个、最后一个、中间一个
// 去三个元素的中位数,作为快排的枢纽元
// 三个元素的选取也可以完全随机,但是没必要完全随机
// 该函数有两个功能:1.选择合适的枢纽元;2.将枢纽元交换到right-1位置
int Median3(int arr[], int left, int right)
{
    // 取中间的坐标
    int center = (left + right) / 2;

    // 对三个数进行排序
    if (arr[left] > arr[center])
    {
        swap(arr[left], arr[center]);
    }
    if (arr[left] > arr[right])
    {
        swap(arr[left], arr[right]);
    }
    if (arr[center] > arr[right])
    {
        swap(arr[center], arr[right]);
    }

    // 我们选arr[center]作为枢纽元
    // arr[right]本身会大于等于枢纽元
    // arr[left]本身会小于等于枢纽元
    // 所以,我们不再将枢纽元置坐标置为right,而是置为right-1
    // 在后续操作中,i的初始值设置为left+1,j的初始值设置为right-2

    // 将枢纽元置于序列的后面,但不是最后一个——right,而是right-1
    swap(arr[center], arr[right - 1]);

    // 返回枢纽元
    return arr[right - 1];
}

// 打印序列
void PrintArr(int arr[], int n)
{
    for (int i = 0; i < n; ++i)
    {
        cout << arr[i] << ' ';
    }
    cout << endl;
}

// 插入排序
// 时间复杂度O(N*N)
void InsertionSort(int arr[], int n)
{
    if (n <= 1)
    {
        return;
    }
    for (int i = 1; i < n; ++i)
    {
        int tmp = arr[i];
        int p   = i - 1;
        while (p >= 0 && arr[p] > arr[p + 1])
        {
            arr[p + 1] = arr[p];
            --p;
        }
        arr[p + 1] = tmp;
    }
}

// 用于定义调用快速排序的最低元素个数:Cutoff+1
const int Cutoff = 3;

// 实现基于快速排序的快速查找
void QSelect(int arr[], int k, int left, int right)
{
    int i = 0, j = 0;
    int pivot = 0;

    if (left + Cutoff <= right)
    {
        pivot = Median3(arr, left, right);
        i = left;
        j = right - 1;

        for (;;)
        {
            while (arr[++i] < pivot);
            while (arr[--j] > pivot);

            if (i < j)
            {
                swap(arr[i], arr[j]);
            }
            else
            {
                break;
            }
        }
        swap(arr[i], arr[right - 1]);

        if (k < i + 1)
        {
            QSelect(arr, k, left, i - 1);
        }
        else if (k > i + 1)
        {
            QSelect(arr, k, i + 1, right);
            // QSelect(arr + i + 1, k - i - 1, 0, right - i - 1);
        }
        else // k = i + 1
        {
            // 找到了第k小的数
            ;
        }
    }
    else
    {
        InsertionSort(arr + left, right - left + 1);
    }
}

// 对QSelect封装
int QuickSelect(int arr[], int k, int n)
{
    // 开辟一个新的副本,以免打乱原来的序列
    int* tmp = new int[n];
    memcpy(tmp, arr, sizeof (*tmp) * n);

    QSelect(tmp, k, 0, n - 1);

    cout << "tmp:";
    PrintArr(tmp, n);

    int t = tmp[k - 1]; // 第k小的数,其索引为k-1

    delete [] tmp;

    return t;
}

// 实现基于快速排序的快速查找2
int QSelect2(int arr[], int k, int left, int right)
{
    int i = 0, j = 0;
    int pivot = 0;

    if (left + Cutoff <= right)
    {
        pivot = Median3(arr, left, right);
        i = left;
        j = right - 1;

        for (;;)
        {
            while (arr[++i] < pivot);
            while (arr[--j] > pivot);

            if (i < j)
            {
                swap(arr[i], arr[j]);
            }
            else
            {
                break;
            }
        }
        swap(arr[i], arr[right - 1]);

        if (k < i + 1)
        {
            return QSelect2(arr, k, left, i - 1);
        }
        else if (k > i + 1)
        {
            return QSelect2(arr, k, i + 1, right);
            // QSelect(arr + i + 1, k - i - 1, 0, right - i - 1);
        }
        else // k = i + 1
        {
            // 找到了第k小的数
            return arr[i]; // 索引k-1
        }
    }
    else
    {
        InsertionSort(arr + left, right - left + 1);
        return arr[k - 1]; // 索引k-1
        // return (arr + left)[k - left - 1];
    }
}

// 对QSelect2封装
int QuickSelect2(int arr[], int k, int n)
{
    // 开辟一个新的副本,以免打乱原来的序列
    int* tmp = new int[n];
    memcpy(tmp, arr, sizeof (*tmp) * n);

    int t = QSelect2(tmp, k, 0, n - 1);

    cout << "tmp:";
    PrintArr(tmp, n);

    delete [] tmp;

    return t;
}

int main()
{
    int arr[] = {8, 1, 4, 9, 6, 3, 5, 2, 7, 10};

    PrintArr(arr, sizeof (arr) / sizeof (*arr));

    cout << QuickSelect(arr, 5, sizeof (arr) / sizeof (*arr)) << endl;
    PrintArr(arr, sizeof (arr) / sizeof (*arr));

    cout << QuickSelect2(arr, 5, sizeof (arr) / sizeof (*arr)) << endl;
    PrintArr(arr, sizeof (arr) / sizeof (*arr));

    cout << endl;

    int arr2[] = {9, 9, 9, 9, 9, 9, 9, 9, 9, 9};

    PrintArr(arr2, sizeof (arr2) / sizeof (*arr2));

    cout << QuickSelect(arr2, 8, sizeof (arr2) / sizeof (*arr2)) << endl;
    PrintArr(arr2, sizeof (arr2) / sizeof (*arr2));

    cout << QuickSelect2(arr2, 8, sizeof (arr2) / sizeof (*arr2)) << endl;
    PrintArr(arr2, sizeof (arr2) / sizeof (*arr2));

    return 0;
}

         其他快速选择算法将在后续介绍。

原文地址:https://www.cnblogs.com/unixfy/p/3483048.html