基于Partition函数实现快排、超过一半数字、最小K个数

本文参考了《算法导论》及如下博客:
http://blog.jobbole.com/105219/
https://blog.csdn.net/AA2519556/article/details/77884962
https://www.cnblogs.com/zuilehongdou/p/6197716.html
http://haoyuanliu.github.io/2016/12/18/Partition%E7%AE%97%E6%B3%95%E5%89%96%E6%9E%90/
注:上述博客在考虑end是元素的个数n,留意end对应的元素是否被访问,避免漏掉。
Partition函数的枢纽取值有多种取法(取头、取尾等),最好是取元素随机值,时间复杂度低。

一、二分Partition函数详解

如果你学习过算法,那么肯定听说过快速排序的大名,但是对于快速排序中用到的 partition 算法,你了解的够多吗?或许是快速排序太过于光芒四射,使得我们往往会忽视掉同样重要的 partition 算法。
partition算法从字面上就非常好理解,就是分割算法嘛!简单讲就是可以把数组按照一定的分成几个部分,其中最常见的就是快速排序中使用的partition算法,这是一个二分partition算法,将整个数组分解为小于某个数和大于某个数的两个部分,然后递归进行排序算法。

思路I

算法思路

  • 使用第一个数组元素作为枢轴点,即为pivot;
  • 使用一个指针去扫描整个数组,凡是小于pivot的全部放到数组左端;
  • 最后将pivot放到数组中间的位置,pivot左边全部都是小于它的数字,右边反之,最后返回pivot的位置信息;
    这里写图片描述
    代码:
void swap(int &x, int &y)
{
    int t = x;
    x = y;
    y = t;

}
int partition(vector<int> &nums, int begin, int end)
{
    int pivot = nums[begin];//枢轴(也可以是在begin和end之间的随机数)
    // Last position where puts the no_larger element.
    //凡是小于pivot的全部放到数组左端,pos指向<枢轴值的最后一个
    //pos++指向不满足条件的(用于交换,将满足条件的换过来)
    int pos = begin;
    for (int i = begin + 1; i < end; ++i)
    {
        if (nums[i] < pivot)
        {
            pos++;
            if (i != pos) //避免自身交换
                swap(nums[pos], nums[i]);
        }

    }
    swap(nums[pos], nums[begin]);
    return pos;
}

注意:i < end表示并没有访问end值。此时若快排,end=元素个数。

算法分析
这种实现思路比较直观,但是其实并不高效。从直观上来分析一下,每个小于pivot的值基本上(除非到现在为止还没有遇见大于pivot的值)都需要一次交换,大于pivot的值(有可能需要被交换多次才能到达最终的位置。

思路II

算法思路

  • 就如快速排序中最常使用的那样,使用两个指针分别从头部和尾部进行扫描,头部遇到大于pivot的数和尾部遇到小于pivot的数进行交换;
  • 使用了两个指针,效率更高一点;避免使用swap函数

如果我们考虑用 Two Pointers 的思想,保持头尾两个指针向中间扫描,每次在头部找到大于pivot的值,同时在尾部找到小于pivot的值,然后将它们做一个交换,就可以一次把这两个数字放到最终的位置。一种比较明智的写法如下:

//Two Pointers思想的分割函数(begin0,end为n-1)
int Partition(vector<int> &nums, int begin, int end)
{
    int pivot = nums[begin];//第一个记录作为枢轴(也可是在beginend之间的随机数)
    while (begin < end)
    {
        while (begin < end && nums[end] >= pivot)
        {
            end--;
        }
        nums[begin] = nums[end];//尾部找到小于pivot的值,移到低端

        while (begin < end && nums[begin] <= pivot)
        {
            begin++;
        }
        nums[end] = nums[begin];//头部找到大于pivot的值,移到高端
    }

    nums[begin] = pivot;//枢轴基准归位

    return begin;
}

注意:这里访问了end值,此时若快排,end=最大下标n-1。

算法分析
直观上来看,赋值操作的次数不多,比前面单向扫描的swap次数都少,效率应该会更高。


二、二分Partition函数的应用

快速排序算法
经典的快速排序算法,直接上代码:

void quickSort(vector<int> &nums, int begin, int end)
{

    if (begin >= end) return;

    int index = partition(nums, begin, end);
    if (index>begin)
        quickSort(nums, begin, index-1);
    if (index<end)
        quickSort(nums, index+1, end);
}
调用:
quickSort(vec, 0, vec.size()-1); //end为n-1

数组中出现次数超过一半的数字
数组的特性 :数组中有一个数字出现的次数超过了数组长度的一半。如果我把这个数组排序,那么排序之后位于数组中间的数字一定就是那个出现次数超过数组一半的数字。也就是说,这个数字就是统计学上的中位数,即长度为n的数组中第n/2的数字。 我们有成熟的O(n)的算法得到数组中任意第K大的数字 。
这种算法是受快速排序算法的启发。在随机快速排序算法中,我们现在数组中随机选择一个数字,然后调整数组中数字的顺序,使得比选中的数字小的数字都排在它的左边,比选中的数字大的数字都排在它的右边。如果这个选中的数字的下标刚好是n/2,那么这个数字就是数组的中位数。如果它的下标大于n/2,那么中位数应该位于它的左边,我们可以接着在它的左边部分的数组中查找。如果它的下标小于n/2,那么中位数应该位于它的右边,我们可以接着在它的右边部分的数组中查找。这是一个典型的递归过程,实现代码如下:

扫描二维码关注公众号,回复: 1504415 查看本文章
int MoreThanHalfNum_Solution(vector<int> numbers) 
{
    int length = numbers.size();
    if (numbers.empty() || length<0)
        return 0;

    int begin = 0;
    int end = length - 1;
    int middle = length >> 1;
    int index = 0; 
    //利用
    while (begin<end)
    {
        index = Partition(numbers, begin, end);
        if (index == middle)
        {
            break;
        }
        else if (index > middle)
        {
            end = index - 1; //则值在左边部分的数组。
        }
        else
        {
            begin = index + 1;//则值在右边部分的数组。
        }
    }
    //检查该值是否超过数组长度的一半
    int cnt = 0;
    for (int i = 0; i < length; ++i)
    {
        if (numbers[index] == numbers[i])
            cnt++;
    }
    if (cnt * 2 > length) return numbers[index];

    return 0;
}

最小的K个数
基于数组的第k个数字来调整,使得比第k个数字小的所有数字都位于数组的左边,比第k个数字大的所有数字都位于数组的右边。调整之后,位于数组左边的k个数字就是最小的k个数字(这k个数字不一定是排序的)。时间复杂度O(N)

int Partition(vector<int> &nums, int begin, int end)
{
    int pivot = nums[begin];//第一个记录作为枢轴(也可是在begin和end之间的随机数)
    while (begin < end)
    {
        while (begin < end && nums[end] >= pivot)
        {
            end--;
        }
        nums[begin] = nums[end];//尾部找到小于pivot的值,移到低端

        while (begin < end && nums[begin] <= pivot)
        {
            begin++;
        }
        nums[end] = nums[begin];//头部找到大于pivot的值,移到高端
    }

    nums[begin] = pivot;//枢轴基准归位

    return begin;
}


vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {

    int len=input.size();
    if(len==0||k>len ||k<0) return vector<int>();
    if(len==k) return input;

    int start=0;
    int end=len-1;
    int index=Partition(input,start,end);
    while(index!=(k-1))
    {
        if(index>k-1)
        {
            end=index-1;
            index=Partition(input,start,end);
        }
        else
        {
            start=index+1;
            index=Partition(input,start,end);
        }
    }

    vector<int> res(input.begin(), input.begin() + k);

    return res;
}

猜你喜欢

转载自blog.csdn.net/u013457167/article/details/79749882