最坏时间复杂度为O(n)的改进选择算法

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq_40692109/article/details/102696615

简介

在构造kd-tree中,我们需要对方差最大维度上的中位数进行计算。中位数计算方法的好坏一定程度上影响了树的构造时间。要计算中位数,我们一般想到的是将数据进行排序,然后取中间的数。排序算法最快的时间复杂度为O(nlogn)。但是实际上我们要求中位数,不需要将所有数据进行排序,有的排序步骤是无用的。所以有了选择算法,其平均时间复杂度为O(n),最坏时间复杂度为O(n2)。在Rob Hess 的sift源码中,kd-tree使用了一种快速选择算法,能使最坏时间复杂度为O(n)。下面我们将对快速排序算法、选择算法、快速选择算法进行对比介绍。

快速排序

快速排序步骤:
(1)首先设定一个分界值,通过该分界值将数组分成左右两部分。
(2)将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。
(3)重复(1)(2)步骤直至数据不能再划分为止。

第一次排序:在这里插入图片描述
以第一个数为分界值,从左往右找一个比分界值大的数,从右往左找一个比分界值小的数,如果比分界值大的在小的左边,则交换两个数位置。再记录下比分界值小的数的个数,最后将分界值放入对应位置,完成第一次排序。接着对分界值左边和右边的数分别进行递归操作直到完成排序。
递归排序:在这里插入图片描述

代码如下:

template<typename T>
static void Qsort(T arr[], int low, int high) {
 //c++;
 if (high <= low) return;
 int i = low;
 int j = high + 1;
 int key = arr[low];
 while (true)
 {
  /*从左向右找比key大的值*/
  while (arr[++i] < key)
  {
   if (i == high) {
    break;
   }
  }
  /*从右向左找比key小的值*/
  while (arr[--j] > key)
  {
   if (j == low) {
    break;
   }
  }
  if (i >= j) break;
  /*交换i,j对应的值*/
  int temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
 }
 /*中枢值与j对应值交换*/
 int temp = arr[low];
 arr[low] = arr[j];
 arr[j] = temp;
 Qsort(arr, low, j - 1);
 Qsort(arr, j + 1, high);
}

选择算法

在前面快速排序中,我们确定了6的位置之后,由于我们需要寻找中位数,完全不需要对6右边的数据进行排序,因此有了选择算法。与快速排序一样,我们将输入数组进行递归划分,与快速排序不同的是,快速排序会递归处理划分的两边,而选择只处理划分一边。
橙色为我们需要确定的中间位置的数,黄色为当前作分界值的数,也是一轮比较交换之后能确定位置的数。当我们每次确定位置的数确定的位置在中间位置,那么这个分界值数就是中位数。

代码如下:

template<typename T>
T Select(T arr[], int low, int high, int m) {
 //c++;
 if (high <= low)
 {
  return -1;
 }
 int i = low;
 int j = high + 1;
 int key = arr[low];
 while (true)
 {
  /*从左向右找比key大的值*/
  while (arr[++i] < key)
  {
   if (i == high) {
    break;
   }
  }
  /*从右向左找比key小的值*/
  while (arr[--j] > key)
  {
   if (j == low) {
    break;
   }
  }
  if (i >= j) break;
  /*交换i,j对应的值*/
  int temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
 }
 /*中枢值与j对应值交换*/
 int temp = arr[low];
 arr[low] = arr[j];
 arr[j] = temp;
 if (j > m) {
  mQsort(arr, low, j - 1, m);
 }
 if (j < m) {
  mQsort(arr, j + 1, high, m));
 }
 if (j = m) {
  return arr[j];
 }
}

快速选择算法

当输入为逆序是,快速排序和选择算法的时间复杂度都会降为O(n2),为了防止选择算法最坏情况的产生,我们需要使每次确定的数尽可能等分数据,即尽可能接近中位数(在选择排序中,我们选择的为第一个数进行判断)。所以我们不在随机选择一个数据进行划分,而是先寻找一个接近中位数的数据进行划分。
快速选择算法对选择算法在选择划分数据时进行了修改。将输入数组的n个元素划分为n/5组,每组5个元素。首先对每组元素进行插入排序,然后找每组中位数的中位数k,以k对数据进行划分。这样可是划分值k更接近中位数,防止最坏情况发生。

中位数的选取如图所示,黄色为每组选出的中位数,选出之后再对黄色的这些中位数递归调用快速选择算法选择中位数。在这里插入图片描述
伪代码如下:
Select(数组,数组起点,数组终点,中点位置m){
将输入数组的n个元素划分为n/5组,每组5个元素,对每组元素进行插入排序;
Select()递归调用求每组数据中位数的中位数k;
数组内所有点与k比较,小于放它左边,大于放它右边;此时k所在位置为 j;
if(j > m){
return Select(数组,数组起点,j - 1,m);
}
if (j < m){
return Select(数组,j +1,数组终点,m);
}
return arr[m] ;
}
代码如下:

emplate<typename T>
static void insertion_sort(T* array, int n)
{
 T k;
 int i, j;
 for (i = 1; i < n; i++)
 {
  k = array[i];
  j = i - 1;
  while (j >= 0 && array[j] > k)
  {
   array[j + 1] = array[j];
   j -= 1;
  }
  array[j + 1] = k;
 }
}

template<typename T>
static int partition_array(T* array, int n, T pivot)
{
 T tmp;
 int p, i, j;
 i = -1;
 for (j = 0; j < n; j++)
  if (array[j] <= pivot)
  {
   tmp = array[++i];
   array[i] = array[j];
   array[j] = tmp;
   if (array[i] == pivot)
   {
    p = i;
   }
  }
 array[p] = array[i];
 array[i] = pivot;
 return i;
}

template<typename T>
T rank_select(T* array, int n, int r)
{
 // c++;
 T* tmp, med;
 int gr_5, gr_tot, rem_elts, i, j;
 /* base case */
 if (n == 1)
  return array[0];
 /* divide array into groups of 5 and sort them */
 gr_5 = n / 5;
 gr_tot = cvCeil(n / 5.0);
 rem_elts = n % 5;
 tmp = array;
 for (i = 0; i < gr_5; i++)
 {
  insertion_sort(tmp, 5);
  tmp += 5;
 }
 insertion_sort(tmp, rem_elts);
 /* recursively find the median of the medians of the groups of 5 */
 tmp = (T*)calloc(gr_tot, sizeof(T));
 for (i = 0, j = 2; i < gr_5; i++, j += 5)
  tmp[i] = array[j];
 if (rem_elts)
  tmp[i++] = array[n - 1 - rem_elts / 2];
 med = rank_select(tmp, i, (i - 1) / 2);
 free(tmp);
 /* partition around median of medians and recursively select if necessary */
 int i2 = 0;
 j = partition_array(array, n, med);
 if (r == j)
  return med;
 else if (r < j)
  return rank_select(array, j, r);
 else
 {
  array += j + 1;
  return rank_select(array, (n - j - 1), (r - j - 1));
 }
}

速度比较

横坐标为数据量
纵坐标为计算时间
粉色-冒泡排序 O(N2)
红色-快速排序 O(NlogN)
绿色-快速选择算法 O(N)
蓝色-选择算法 O(N)
通过斜率可以清楚看到这些算法的时间复杂度。
在这里插入图片描述
从图中可看到选择算法比快速选择算法更快,但是在最坏情况下(逆序输入)可看到如下结果:
在这里插入图片描述
这时快速排序和选择算法时间复杂度都变为了O(N2)。(其实逆序输入并不是快速选择算法的最坏情况)

参考:
Rob Hess sift源码

猜你喜欢

转载自blog.csdn.net/qq_40692109/article/details/102696615
今日推荐