算法导论实践——选择第i大的数
算法的魅力就在于,明明感觉思路很简单,想要实现却要花上一阵不小的功夫,今天给大家表演的是,第i大数的选取,可以利用这个函数进行中位数的选择。
理论
啰嗦一句“第i大的数选取”的背景,直接扣PPT了,研究来研究去,最后还是为了降低时间复杂度,让我们来看看它是怎么降低时间复杂度的把
一些约定
- 数组长度用n表示
- 算法定义为Select_i_th_Data(A, p, q, k)
- A是输入数组
- p是起始下标
- q是结束下标
- k表示选择A[p:q]中第k大的数
- 不会显式地写取整符号,意思到了即可
正式开始
将数组分组,按照每五个一组划分
可以划分为[n/5](向上取整👆)组
找出每一组的中位数。
原理是排序+简单计算,这里因为数据基数小,只有5个数据,因此无论用什么排序,排序时间都是O(1)。这里假设用插入排序。当然,这里有一个技巧:就是,由于后面会利用到这些中位数,因此,我们直接会将这些中位数交换到数组的最前面
选择中位数中的中位数(MoM)。
这里可以再次调用我们的算法Select_i_th_Data,去选择MoM。Select_i_th_Data(A,0,n/5,10/n)
利用选择的MoM划分
将比MoM小的分到左边,比MoM大的分到右边,获取MoM的下标indexOfMoM,并同时获取MoM的名词rankOfMoM,这里用到的技巧是快排里的划分技巧
递归搜索即可
比较rankOfMoM和k,递归搜索即可
- rankOfMoM == k, 则MoM是第k大的数
- rankOfMoM > k,则在大于MoM的部分递归调用函数
- rankOfMoM < k,则在小于MoM的部分递归调用函数
复杂度分析
事实上,数组划分求中位数部分的时间复杂度为O(n),MoM的搜索复杂度为T(n/5),再来,根据下图有,因此最后的递归部分时间复杂度不超过T(7n/10+6)
综上所述:
实践
BigDataMedianUtil.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace BigInteger
{
public static class BigDataMedianUtil
{
//在数组A[p:q]中找到第k大的数
public static int Select_i_th_Data(int[] A, int p, int q, int k)
{
//初值条件,如果A的有效长度小于等于5,则直接返回中位数
//默认若是偶数,则返回前一个数;若是奇数,则返回中间的数;
if (q - p <= 4)
{
InsertSort(A, p, q);
return A[p + k - 1];
}
int effectiveLength = q - p + 1;//选取A[p:q]部分的长度
int effectiveEndIndex = p + effectiveLength / 5 - 1;//选取A[p:q]能够被5划分的末尾下标
//以五个为一组,挑选中位数,把中位数放到数组的前A[p:effectiveEndIndex]部分,方便后面再次调用
//注意:挑选中位数时,不需要多余的部分,因此直接effectiveLength / 5即可;
for (int i = 0; i < effectiveLength / 5; i++)
{
InsertSort(A, i * 5 + p, i * 5 + 4 + p);
Swap(A, i + p, i * 5 + 2 + p);
}
//对数组的前A[p:effectiveEndIndex]部分,也就是所有组的中位数递归调用Select_i_th_Data,
//这次需要选出中位数的中位数,因此,k = effectiveLength / 10(向上取整)
int MoM = Select_i_th_Data(A, p, effectiveEndIndex, (effectiveLength / 10) % 2 == 0 ? (effectiveLength / 10) : (effectiveLength / 10 + 1));
//利用MoM对A[p:q]部分进行划分,划分过程类似快速排序的划分过程,时间复杂度是O(n)
//注意:这里进行划分时,需要把多余的部分(不是Partition(A,p,effectiveEndIndex,MoM))基于MoM一起划分,不然就会缺少元素。
int indexOfMoM = Partition(A, p, q, MoM);//得到MoM的绝对下标
int rankOfMoM = indexOfMoM + 1 - p;//得到相对A[p:q]来说,第rankOfMoM大的数
//分解+解决,不用合并
//如果MoM刚好是A[p:q]中第k大的数,则返回MoM;
if (k == rankOfMoM) return MoM;
else if (k < rankOfMoM)
{
//在数组A[p:indexOfMoM - 1]中找到第k大的数
return Select_i_th_Data(A, p, indexOfMoM - 1, k);
}
else
{
//在数组A[indexOfMoM + 1, q]中找到第k - indexOfMoM - 1大的数
return Select_i_th_Data(A, indexOfMoM + 1, q, k - indexOfMoM - 1);
}
}
public static int Partition(int[] A, int p, int q, int Value)
{
int i = p - 1;
int key = Value;
for(int j = p; j < q; j++)
{
if(A[j] < key)
{
i++;
Swap(A, i, j);
}
}
Swap(A, FindIndexByValue(A, key), i + 1);
return i + 1;
}
public static void Swap(int[] A,int p, int q)
{
int mid = A[p];
A[p] = A[q];
A[q] = mid;
}
public static void InsertSort(int[] A, int p, int q)
{
for(int i = p; i <= q - 1; i++)
{
int j = i + 1;
int key = A[j];
while (key < A[j - 1])
{
A[j] = A[j - 1];
j--;
if(j == p)
{
break;
}
}
A[j] = key;
}
}
public static void GetResult(int[] A)
{
for (int i = 0; i < A.Length; i++)
{
Console.Write(A[i]);
Console.Write(" ");
}
Console.WriteLine(" ");
}
public static int FindIndexByValue(int[] A,int a)
{
for(int i = 0; i < A.Length; i++)
{
if (A[i] == a)
{
return i;
}
}
return -1;
}
}
}
Program.cs
using System;
using BigInteger;
namespace BigInteger
{
class Program
{
static void Main(string[] args)
{
int[] A = { 11, 12, 14, 13, 15, 5, 6, 7, 8, 9, 1, 2, 3, 4, 10, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26 };
int Element = BigDataMedianUtil.Select_i_th_Data(A, 0, A.Length - 1, A.Length/2);
Console.WriteLine(Element);
}
}
}