荷兰旗问题及随机快排和bfprt算法

Posted 一个山里的少年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了荷兰旗问题及随机快排和bfprt算法相关的知识,希望对你有一定的参考价值。

1.荷兰旗问题

什么是荷兰期问题了?

荷兰国旗是由红白蓝3种颜色的条纹拼接而成,如下图所示

假设这样的条纹有多条,且各种颜色的数量不一,并且随机组成了一个新的图形,新的图形可能如下图所示,但是绝非只有这一种情况:

 把这些条纹按照颜色排好,红色的在上半部分,白色的在中间部分,蓝色的在下半部分,我们把这类问题称作荷兰国旗问题。

如何解决这个问题呢?我们之前在学习快排的时候我们会先选一个key出来然后通过单趟排序使得左边小于等它右边大于等于它。但是如果我们改成这样左边比它小 中间是等于key的 右边都是大于key的我们不就解决了这个问题了吗?

定一个整数数组,给定一个值k,这个值在原数组中一定存在,要求把数组中小于k的元素放到数组的左边,大于K的元素放到数组的右边,等于K的元素放到数组的中间,最终返回一个整数数组,其中只有两个值,分别是等于k的数组部分的左右两个下标值。

我们要如何来实现呢?首先我们先看下面这个步骤

1.我们首先定义小于区域,大于区域,以及划分值也就是key。

2.一开始小于区域的最右侧less在数组最左侧的前一个位置,大于区的最左侧more在数组最右边界的下一位置

3.定义一个变量cur变量数组如果遍历到的值等于key直接跳过继续遍历,如果小于key则将less的前一个位置和当前值交换cur++,less++.如果大于key则将more的前一个值和当前值进行交换,然后more--;即可

4.

结束条件当cur小于more循环就继续否则结束循环。也就是cur不能超过大于区域的左边界

对应图解:

我们假设数组为[1,3,2,4,6,2,0]

在这里我们选择2作为key

首先cur从下标为0的位置开始一看1比key要小less的前一个和当前值换less++,cur也++

 然后再继续看当前cur的值比key要大则将more前一个数和当前数交换然后more--。

 此时cur的值比key要小也就是将less的前一个数和当前数交换然后less++,cur++

此时我们发现cur的值刚好等于key的值直接跳过cur++;

 此时cur的值比key要大和more的前一个值进行交换,more--

此时cur的值和key相等直接跳过即可,cur++。

此时cur的值大于keymore的前一个位置的值和当前值进行交换,--more 

 

 此时more和cur重合了结束循环我们从图中可以看出此时已经是左边比key要小了 中间都是等于key的 右边都是大于key的。

 我们来看编程题也就是荷兰国旗的一道编程题

对应letecode链接:

75. 颜色分类 - 力扣(LeetCode) (leetcode-cn.com)

 题目描述:

给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。

示例 1:

输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
示例 2:

输入:nums = [2,0,1]
输出:[0,1,2]
示例 3:

输入:nums = [0]
输出:[0]
示例 4:

输入:nums = [1]
输出:[1]

提示:

n == nums.length
1 <= n <= 300
nums[i] 为 0、1 或 2

进阶:

你可以不使用代码库中的排序函数来解决这道题吗?
你能想出一个仅使用常数空间的一趟扫描算法吗

解题思路:

解题思路就是刚才介绍的荷兰国旗问题再本题中我们只需要选择1作为key即可达到左边比1小中间是等于1的右边是大于1的使数组有序。 

对应代码:

class Solution 
public:
    void sortColors(vector<int>& nums) 

          int less=-1;
          int more=nums.size();
          int cur=0;
          while(index<more)
              if(nums[cur]==0)//对应小于key
                  swap(nums[++less],nums[cur++]);
              
              else if(nums[cur]==1)//等于key
                  ++cur;
              
              else//大于key
                  swap(nums[cur],nums[--more]);
              
          
          
    
;

我们在来看一下类似的题目:

数组中第k大的数 

对应letecode链接:

剑指 Offer II 076. 数组中的第 k 大的数字 - 力扣(LeetCode) (leetcode-cn.com)

 题目描述:

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例 1:

输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
示例 2:

输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4

提示:

1 <= k <= nums.length <= 104
-104 <= nums[i] <= 104

方法一:快速排序。一个数如果是第k大那么对应的小标应该为数组的长度-k. 所以我们可以通过快速排序将整个数组排序然后返回数组中对应小标为数组长度-k的值即可。

对应代码:

 int findKthLargest(vector<int>& nums, int k) 
             sort(nums.begin(),nums.end());
             return nums[nums.size()-k];
        
    

 时间复杂度:O(N*logN)空间复杂度:O(1)但是如果在面试中出现了这一道题使用这种方法面试官是不可能让你过的

方法二:优先级队列 

我么可以使用优先级队列建一个k个数的小根堆将数组中的数一次入堆当堆中元素的个数大于k个数我们就弹出一个数遍历结束之后堆顶的数即为答案。

对应代码:

int findKthLargest(vector<int>& nums, int k) 
          priority_queue<int,vector<int>,greater<int>>q;//注意优先级队列默认为大根堆
          for(auto x:nums)
              q.push(x);
              if(q.size()>k)
                  q.pop();
              
          
          return q.top();
    

时间复杂度O(N*logk)空间复杂度O(K) 

方法三:快速选择算法

 利用快速排序每次选择的key单躺排序之后左边小于key右边大于key。那么key就到了正确的位置也就到了正确的下标。而最大的k个数对应的下标为数组长度-k所以我么每一次单趟排序之后返回key对应的下标和数组的长度-k比较如果等于就返回该小标所对应的值,如果小于则去key的左边去找如果比key大则去右边找这样时间复杂度为0(N)

但是当面对数组为 1 2 3 4 5 6 7 8 也就是有序的时候时间复杂度会退化为O(N^2) 如何解决这个问题了?我们可以采用在数组范围内随机选数或者三数取中做key保证key每次基本不会取到最大或者最小

对应代码:

class Solution 
public:
    int findKthLargest(vector<int>& nums, int k) 
               int pos=nums.size()-k;
               int start=0;
               int end=nums.size()-1;
               int index=partition(nums,start,end);
               while(pos!=index)

                   if(pos<index)//小于
                      end=index-1;
                   
                   else大于
                       start=index+1;
                   
                   
                   index=partition(nums,start,end);
               
               return nums[index];
    
     int partition(vector<int>&nums,int left,int right)
   
          int index=random()%(right-left+1)+left;//在left到right中随机选数
          swap(nums[left],nums[index]);

          int cur=left+1;
          int prev=left;
          int key=left;
          while(cur<=right)
              if(nums[cur]<nums[key]&&++prev!=cur)
                    swap(nums[cur],nums[prev]);
              
              ++cur;
          
          swap(nums[prev],nums[key]);
          return prev;
    
 
;

当然也可以使用上面说的荷兰国旗问题的单趟排序:

class Solution 
public:
    int findKthLargest(vector<int>& nums, int k) 
         return  quick_sort(nums,0,nums.size()-1,nums.size()-k);
        
    
    int quick_sort(vector<int>&nums,int left,int right,int index)
                     if(left==right)//只有一个数直接返回
                         return nums[left];
                     
                     int pivot=nums[left+random()%(right-left+1)];//随机选数
                     vector<int>tmp=partition(nums,left,right,pivot);
                     if(index>=tmp[0]&&index<=tmp[1])//命中等于区域
                         return nums[index];
                     
                     else if(index<tmp[0])//在等于区域的左边
                        return quick_sort(nums,left,tmp[0]-1,index);
                     
                     else右边
                            return  quick_sort(nums,tmp[1]+1,right,index);
                     
    
    
    vector<int> partition(vector<int>&nums,int left,int right,int pivot)

             int less=left-1;
             int more=right+1;
              int index=left;
              while(index<more)
                  if(nums[index]==pivot)
                      index++;
                  
                  else if(nums[index]<pivot)
                      swap(nums[++less],nums[index++]);
                  
                  else
                      swap(nums[index],nums[--more]);
                  
              

              return less+1,more-1;
    
    
;

 非递归:

class Solution 
public:
    int findKthLargest(vector<int>& nums, int k) 
         return  quick_sort(nums,0,nums.size()-1,nums.size()-k);
        
    
    int quick_sort(vector<int>&nums,int left,int right,int index)
                    
                    
                     vector<int>tmp;
                      while(left<right)
                           int pivot=nums[left+random()%(right-left+1)];
                           tmp=partition(nums,left,right,pivot);
                           if(index>=tmp[0]&&index<=tmp[1])//命中等于区域
                               return nums[index];
                           
                           else if(index<tmp[0])//在左边
                               right=tmp[0]-1;
                           
                           else
                               left=tmp[1]+1;
                           
                      
                      return nums[left];//只有一个数
    
    
    vector<int> partition(vector<int>&nums,int left,int right,int pivot)

             int less=left-1;
             int more=right+1;
              int index=left;
              while(index<more)
                  if(nums[index]==pivot)
                      index++;
                  
                  else if(nums[index]<pivot)
                      swap(nums[++less],nums[index++]);
                  
                  else
                      swap(nums[index],nums[--more]);
                  
              

              return less+1,more-1;
    
    
;

但是这样也是有概率选到最坏的情况单最好的情况时O(N)根据概率的数学期望我们可以证明他是收敛于O(N)的。 

bfprt算法

BFPRT这一名称来源于该算法的五位作者的首字母,在维基百科上,该算法被称为Median of medians,因为中位数在这里起到了至关重要的角色。

在快速选择算法中,我们分析了最佳和最坏情况的时间复杂度。如果每次选择主元,都恰好选中中位数,那么自然就会落入最佳情况。BFPRT正是利用了这一点,在快速选择算法的基础上,额外增加了计算近似中位数的步骤。

之所以计算近似中位数,是因为计算准确的中位数显然具有和Top k问题相同的时间复杂度,得不偿失。我们需要以较小的代价得到一个比较接近中位数的数。

首先,将N个数据每五个分为一组,共得到 N/5  组。注意,这里为了方便起见,我们不去考虑余数的问题,那些常数项在分析时间复杂度时并不重要。使用排序对每一组排序,找出每一组的中位数,共得到 N/5 个中位数。接下来这步很奇幻,我们递归调用BFPRT算法计算这 个数组的中位数。没错,调用我们正在描述的这个算法本身,因为中位数就是第  大的数,BFPRT的目标就是计算这个。虽然目前还看不出递归调用到底如何起作用,不过不必担心,递归算法的神奇之处正在于此。假设这个中位数真的计算出来了,那么这个数就可以称为中位数的中位数(Median of medians),也是整个数据的近似中位数。

 具体步骤:

1.特别讲究的选出一个key

2.单趟排序大于key的放在左边小于key的放在右边等于key的放在中间

3.检测命没命中

而在第一步中

1. 5个数一组如果最后一个组不足5个数不用管。时间复杂度为O(1)

2. 将每一个小组排序。时间复杂度为O(N),为什么是0(N)?首先我们让一个小组有序的代价是O(1)应为他是常数个数,一共有N/5个小组所以时间复杂度为O(1)*N/5所以时间复杂度为O(N)

3.将每个小组的中位数取出来。如果最后一组不足5个数,如果是偶数个则取出它的上中位数。然后再取出这个数组中的中位数就是我们要找的key我们如何求了?显然中位数就是数组中第N/2小的数所以我们可以递归调用前面的函数求出中位数

为什么bfprt算法要这样选择key了?

 假设a b c d e 分别是每一个小组的中位数并且将设c为这一小组的中位数这一个小组一共有N/5个数那么有N/10的数比它小,有N/10个数比它大。然而我们会到数组中我们发现比d大的多了两个数比e大的也多了两个数那么回到数组中比c大的至少有3*N/10同理可证比c小的至少有3*N/10个数所以如果我们选择c作为中位数我们每次最多要付出7*N/10的代价。

所以总的时间复杂度T(N)=T(7*N/10)+N+T(N/5)最终我们根据数学方法可以得出时间复杂度是0(N)。

求中位数:

   int medianOfMedians(vector<int>&nums,int left,int right)
        int size=right-left+1;
        int offset=size%5==0?0:1;//判断最后一组是否有5个数
        vector<int>tmp(size/5+offset);
        for(int i=0;i<tmp.size();i++)
            int start=left+5*i;
            tmp[i]=getMedianNum(nums,start,min(start+4,right));
        
        return quick_sort(tmp,0,tmp.size()-1,tmp.size()/2);//递归调用返回中位数
    

获取每个小组的中位数:

   int getMedianNum(vector<int>&nums,int left,int right)
        for(int i=left;i<right;i++)//插入排序
            int end=i;
            int tmp=nums[end+1];
            while(end>=0)
                if(tmp<nums[end])
                    nums[end+1]=nums[end];
                    --end;
                else
                
                    break;
                
            
            nums[end+1]=tmp;
        
        return nums[(left+right)/2];//返回中位数
    

对应总代码:

class Solution 
public:
    int findKthLargest(vector<int>& nums, int k) 
         return  quick_sort(nums,0,nums.size()-1,nums.size()-k);
        
    
    int quick_sort(vector<int>&nums,int left,int right,int index)
                    
                    
                     vector<int>tmp;
                      while(left<right)
                           int pivot=medianOfMedians(nums,left,right);
                           tmp=partition(nums,left,right,pivot);
                           if(index>=tmp[0]&&index<=tmp[1])
                               return nums[index];
                           
                           else if(index<tmp[0])
                               right=tmp[0]-1;
                           
                           else
                               left=tmp[1]+1;
                           
                      
                      return nums[left];
    
    
    vector<int> partition(vector<int>&nums,int left,int right,int pivot)

             int less=left-1;
             int more=right+1;
              int index=left;
              while(index<more)
                  if(nums[index]==pivot)
                      index++;
                  
                  else if(nums[index]<pivot)
                      swap(nums[++less],nums[index++]);
                  
                  else
                      swap(nums[index],nums[--more]);
                  
              

              return less+1,more-1;
    
    int medianOfMedians(vector<int>&nums,int left,int right)
        int size=right-left+1;
        int offset=size%5==0?0:1;
        vector<int>tmp(size/5+offset);
        for(int i=0;i<tmp.size();i++)
            int start=left+5*i;
            tmp[i]=getMedianNum(nums,start,min(start+4,right));
        
        return quick_sort(tmp,0,tmp.size()-1,tmp.size()/2);
    
    int getMedianNum(vector<int>&nums,int left,int right)
        for(int i=left;i<right;i++)
            int end=i;
            int tmp=nums[end+1];
            while(end>=0)
                if(tmp<nums[end])
                    nums[end+1]=nums[end];
                    --end;
                else
                
                    break;
                

            
            nums[end+1]=tmp;
        
        return nums[(left+right)/2];
    
;

最后如果觉得有帮助的老铁可以动动您的小手点个赞谢谢! 

以上是关于荷兰旗问题及随机快排和bfprt算法的主要内容,如果未能解决你的问题,请参考以下文章

排序算法专题:快排和归并排序

经典面试题无序数组中,求第K大的数(堆荷兰国旗问题bfprt算法)

经典面试题无序数组中,求第K大的数(堆荷兰国旗问题bfprt算法)

经典面试题无序数组中,求第K大的数(堆荷兰国旗问题bfprt算法)

分治算法之快排和归并

排序算法之冒泡插入快排和选择排序