`
frank-liu
  • 浏览: 1665586 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

求最大(小)k个元素的分析

 
阅读更多

简介

    求最大(小)k个元素的问题已经在很多书或者文章上进行了大量的讨论。它有多种解决办法,每一种办法有它所独特适用的一面。同时,这个问题也和求第k大(小)的元素有很紧密的关系。这里,我们以取最小k个元素为例。主要按照《编程之美》这本书上讨论的思路作了一个详细的实现。并对每一种方法作了简单的讨论。

排序方法

    这是我们所能想到的比较直接的办法。首先对所有的元素排序,然后取第k个元素。这样这个元素及之前的那些元素就是我们所需要寻找的。具体的实现我们可以考虑用到常用的几种排序方法,比如归并排序和快速排序。另外,还有一种就是采用选择排序作一点修改。

方法一、快速排序的实现

public static void quickSort(int[] a, int l, int r)
{
	if(l < r)
	{
		int q = partition(a, l, r);
		quickSort(a, l, q - 1);
		quickSort(a, q + 1, r);
	}
}

public static int partition(int[] a, int l, int r)
{
	int x = a[r];
	int i = l - 1;
	for(int j = l; j < r; j++)
	{
		if(a[j] <= x)
		{
			i++;
			swap(a, i, j);
		}
	}
	swap(a, i + 1, r);
	return i + 1;
}

public static void swap(int[] a, int i, int j)
{
	int temp = a[i];
	a[i] = a[j];
	a[j] = temp;
}

     这部分主要是quicksort的实现。在通过quicksort得到结果之后。取数组中索引为k的那个元素。然后将元素从头到k的输出就可以了。这部分的伪代码实现如下:

for(int i = 0; i < k; i++)
    System.out.print(a[i] + " ");

 因为quicksort修改了原来排序的元素,只要取第k个的索引就找到结果了,结果数组是已经完全排序的。

和quicksort类似,归并排序的过程基本一样,就不赘述。这两种方法的时间复杂度为nlgn。

选择排序实现:

    我们如果注意到选择排序的算法的话,就会发现,它的过程比较符合我们这个问题的期望。因为它的过程是首先找到数组里最小的元素,然后放到数组的第一个位置,然后在剩下的元素里再找最小的,依次从前往后放。我们需要取k个最小的元素,那么按照这个要求,只要找到最小的K个元素就可以了。具体的实现如下:

public static void sort(int[] a, int k)
{
	for(int i = 0; i < k; i++)
	{
		int min = i;
		for(int j = i + 1; j < a.length; j++)
		{
			if(a[j] < a[min])
				min = j;
		}
		if(i != min)
		{
			swap(a, i, min);
		}
	}
}

public static void swap(int[] a, int i, int j)
{
	int temp = a[i];
	a[i] = a[j];
	a[j] = temp;
}

     这部分代码和纯粹的selection sort的区别就在于原来的selection sort方法要在外面的循环一直遍历到倒数第二个元素,现在只需要遍历到第k个元素。这种方法的时间复杂度为n*k。和前面那种排序的方法比起来,前者为nlgn,两者的差别在于lgn和k。如果k < lgn的话,则第二种选择排序的方法更加理想。

适用场景:

    我们这里采用的都是排序的过程,比如前面的快速排序的方式,每次划分要遍历一次整个数组。采用选择排序也类似,需要从数组里挑选最小的元素。这样做就有一个限制,如果要处理的数据量很大,不能一次将所有的数据都读到内存里面来的话,这种排序的方法就不适用了。

 

方法二、借鉴快速排序的思路

    这种方法借鉴了快速排序的思路。在快速排序中,每次我们要对数组进行partition,分组的结果是使得在我们指定的元素左边的元素都小于它,在它右边的元素都大于它。那么,假设我们指定一个元素s,通过partition方法,我们可以得到划分后s所在的索引值。这个索引值假设为i,则表示元素s是集合里第i大的元素。通过和我们所期望的k比较,如果i < k,说明需要在大于s的元素里找出k - i个元素。否则,只需要在i索引的范围内继续寻找k。具体的实现代码如下:

public static int kSmall(int[] a, int start, int end, int i)
{
	int q = partition(a, start, end);
	int k = q - start + 1;
	if(i == k)
		return q;
	else if(i < k)
		return kSmall(a, start, q - 1, i);
	else
		return kSmall(a, q + 1, end, i - k);
}

public static int partition(int[] a, int l, int r)
{
	int x = a[r];
	int i = l - 1;
	for(int j = l; j < r; j++)
	{
		if(a[j] <= x)
		{
			i++;
			swap(a, i, j);
		}
	}
	swap(a, i + 1, r);
	return i + 1;
}

public static void swap(int[] a, int i, int j)
{
	int temp = a[i];
	a[i] = a[j];
	a[j] = temp;
}

    我们这里用到分划的方法,每次将元素划分为一部分大于指定元素一部分小于指定元素,然后再在缩小的范围内继续调整。这种方法和我们随机分划来求第k大的元素有很类似的思路。它的时间复杂度在理想的情况下可以达到O(N),在最坏的情况下,时间复杂度为O(N * N)。

适用场景:

    和前面的方法类似,这里的分划和调整也要每次都遍历数组元素。只是每次在分划确定了范围之后要遍历的范围缩小了。对于数据量很大的集合还是存在可能内存不够的问题。

 

方法三、借鉴二分搜索策略

    这个方法和前面的类似快速排序比较类似,不过细节稍微有点不一样。既然我们是要找第k小的元素,那么,如果我们找到最大的元素max和最小的元素min,则这个第k小的元素在这个[min, max]的区间内。我们可以用二分搜索的方法,每次取min, max的中间值,然后计算小于这个中间值的元素个数。这样就相当于求出它是第几小的元素。如果这个元素的排名比我们所期望的k小,则说明我们要在这个元素的位置以后来搜索,也就是说要在大于(min+max) / 2的值域内。

    查找小于某个值的元素个数的方法实现如下:

public static int f(int[] a, int n, int m)
{
	int count = 0;
	for(int i = 0; i < n; i++)
	{
		if(a[i] <= m)
			count++;
	}
	return count;
}

 

    通过前面这么不断的调整,我们必然得到一个元素,它正好是第k小的。然后,我们再利用分划的思路,以该元素为界,将集合分成两部分。这样,这个元素及左边的部分就是所要求的结果。

查找和比对第k大元素的部分代码如下:

while(max - min > 0.5)
{
	int mid = min + (max - min) / 2;
	if(f(a, n, mid) >= k)
		max = mid - 1;
	else
		min = mid + 1;
}

   前面这部分返回一个最终的max或者min都可以。用来作为后面分划的值。 

   最后进行分划的代码如下:

public static int partial(int[] a, int l, int r, int key)
{
	int i = l - 1;
	for(int j = l; j <= r; j++)
	{
		if(a[j] <= key)
		{
			i++;
			swap(a, i, j);
		}
	}
	return i;
}

     它和partition的方法很相似,只是做了一点点的修改。因为它是要将指定的元素来进行划分,而不是partition中拿数组中最后的那个元素作为划分的标杆。中间还有一些实现求最大值和最小值的过程代码就不在这里赘述了。只是贴上一部分作为参考:

int max, min;
if(a[0] >= a[1])
{
	max = a[0];
	min = a[1];
}
else
{
	min = a[0];
	max = a[1];
}

int i;
for(i = 2; i < n; i += 2)
{
	if(i + 1 < n)
	{
		if(a[i] >= a[i + 1])
		{
			if(a[i] > max)
				max = a[i];
			if(a[i + 1] < min)
				min = a[i + 1];
		}
		else
		{
			if(a[i + 1] > max)
				max = a[i + 1];
			if(a[i] < min)
				min = a[i];
		}
	}
	else
	{
		if(a[i] > max)
			max = a[i];
		else if(a[i] < min)
			min = a[i];
	}
}

 

    总的来说,这种方法的过程如下:1. 首先找到最大最小值,时间复杂度为O(N),更精确的说是O(3/2N)。2. 查找第K大的元素,每次需要根据折半的部分计数,每次计数的时间复杂度为O(N)。总共需要计数的次数为lgN,那么总的复杂度为NlgN。3. 根据最后的结果进行分划,这部分的时间复杂度为O(N)。所以该方法总体的时间复杂度为O(NlgN)。

适用场景:

    和我们前面的方法类似,在数据量大的时候,我们希望尽可能少的去读数据。这里却需要反复的进行查找计数,后面还要进行划分。所以在大数据量的情况下,这并不是一种理想的选择。

 

方法四、堆排序思路

    这里借鉴了堆排序的思想。我们再回顾一下堆排序里面的过程。首先包括建堆,然后每次移除最上面的元素,移除之后再进行调整。我们每次调整所需要的花费为O(lgN)。而建一个堆的时间复杂度为O(N)。在这里,假如我们要求最小的k个元素,我们可以先建立一个k个元素的最大堆。这个堆里头的根结点是k个元素最大的那个。这样,我们针对后面的每个元素和堆顶的元素比较,如果比较元素比堆顶的元素大,我们可以直接忽略,而比堆顶的元素小,我们用这个元素替换堆顶的元素并进行调整。

    有了这个思路,我们实现的代码就很好办了。以下这部分是实现建堆的部分:

public static int left(int i)
{
	return i * 2 + 1;
}

public static int right(int i)
{
	return i * 2 + 2;
}

public static void maxHeapify(int[] a, int i, int length)
{
	int l = left(i);
	int r = right(i);
	int largest = i;
	while(true)
	{
		if(l < length && a[l] > a[i])
			largest = l;
		if(r < length && a[r] > a[largest])
			largest = r;
		if(i != largest)
			swap(a, i, largest);
		else
			break;
		i = largest;
		l = left(largest);
		r = right(largest);
	}
}

public static void buildMaxHeap(int[] a)
{
	for(int i = a.length / 2; i >= 0; i--)
		maxHeapify(a, i, a.length);
}

public static void swap(int[] a, int i, int j)
{
	int temp = a[i];
	a[i] = a[j];
	a[j] = temp;
}

     后面部分的实现代码如下:

for(int i = k; i < a.length; i++)
{
    if(a[i] < a[k - 1])
    {
        a[k - 1] = a[0];
        maxHeapify(a, 0, k);
    }
}

    这个方法的整体执行步骤有如下几个部分:1. 建一个k个元素大小的堆,时间复杂度为O(K)。2. 针对后面的所有元素,每次比较并调整堆。时间复杂度相当于(N - K)lgK。那么这个方法的整体时间复杂度为O(NlgK)。

适用场景:

    借鉴堆排序的过程可以说是一个比较理想的解决方法。如果k不是特别大的情况,我们可以直接在内存里建一个堆,然后每次读取一个后面的元素来比较和调整。因此对于大数据量来说,之需要读取一遍就可以实现所需要的结果。

 

方法五、借鉴计数排序

    在我前面讨论计数排序的文章里有专门提到过这种排序方法的特殊性。它要保证数据的分布在一个不是太大的范围,这样我们可以申请一个这么大范围的数组。然后在这个数组中每个元素保存对应的集合中元素出现的次数。当我们遍历整个数组得到这么一个统计结果的数组时,我们也就很容易得到第几个元素及之前元素的个数。这样再来找第k个元素就很容易了。建立这么一个统计数组的代码比较简单:

int[] c = new int[k];
for(int i = 0; i < a.length; i++)
	c[a[i]] = c[a[i]] + 1;

 后面我们来判断的代码如下:

int count = 0;
int i;
for(i = 0; i < k; i++)
{
    count += c[i];
    if(count >= k)
        break;
}
return i;

    这里是返回了i这个第k大的元素。如果要输出前面的部分就很简单了:

for(int j = 0; j < i; j++)
{
    for(int l = 0; l < c[j]; l++)
        System.out.print(j);
}

适用场景:

    这种方法采用了计数排序的思路,所以它本身就受到计数排序的两个条件限制:1. 数据分布范围不能太大。2. 数据都要大于等于0. 对于符合这种条件的大数据集合,采用这种办法排序也是一个可行的办法。 

总结

    各种求最大(小)k个元素的方法可以通过这个查找的过程取点巧。如果能找到第K小的元素并且这个元素前面所有的元素都是比该元素小的,那么我们直接在数组里输出这一个数据集合。否则,我们可以根据这个元素对所有数据做一次划分,然后取得所需要的的部分。

参考资料

编程之美

Introduction to algorithms

分享到:
评论

相关推荐

    Python实现从N个数中找到最大的K个数

    找出最大或最下的K个元素,可以使用Python库中的heapq模块,该模块提供两个函数nlargest()求最大K个和nsmallest()求最小K个。 下面我们举例说明: import heapq nums=[12,-9,-3,32,9,56,23,0,11,34] print(heapq....

    代码c++ 最大堆最小堆

    给定k个排好序的序列S1,S2…,Sk,用2路合并算法将这k个序列合并成一个序列。假设所采用的2路合并算法合并2个长度分别为m和n的序列需要m+n-1次比较。试设计一个算法确定合并这个序列的最优合并顺序,使所需的总比较...

    计算机算法设计与分析实验报告

    给定线性序集中n个元素和一个整数k,1&lt;=k,要求找出这n个元素中第k小的元素,即如果将这n个元素依其线性续排列时,排在第k个位置的元素即为要找的元素。当k=1时,就是要找的的最小元素,当k=n时,就要找到最大的元素...

    code4EE#yun-notes#215-数组中的第K个最大元素1

    输入: [3,2,1,5,6,4] 和 k = 2输出: 5分析解答func findKthLargest(nums []int, k int) int {fu

    算法设计与分析 java(包含几种经典算法)

    1.设a[0:n-1]是一个有n个元素的数组,k(0&lt;=k)是一个非负整数。 试设计一个算法将子数组a[0:k]与a[k+1,n-1]换位。要求算法在最坏情况下耗时O(n),且只用到O(1)的辅助空间。 2.在一个圆形操场的四周摆放着n堆石子。现要...

    煤中伴生金属元素对煤低温氧化特性的影响

    研究结果表明,Mg、Zn、Ca、Al、K、Na等元素阻化率在9.5%~57.9%,在60~180 ℃的低温氧化活化能最大增加29.6%,上述金属无机盐与煤分子结合生成稳定的络合物,增强了煤中活性结构的稳定性,也可捕捉煤中羟基...

    算法分析与设计习题集答案

    15、 利用分治策略,在n个不同元素中找出第k个最小元素。 16、 设有n个运动员要进行网球循环赛。设计一个满足以下要求的比赛日程表。 (1)每个选手必须与其它n-1选手各赛一次; (2)每个选手一天只能赛一次。 17、...

    数据结构(Java)复习题

    分析算法的效率以求改进 D. 分析算法的易懂性和文档性 ② A. 空间复杂性和时间复杂性 B. 正确性和简明性 C. 可读性和文档性 D. 数据复杂性和程序复杂性 5. 计算机算法指的是① ,它必具备输入、输出和② 等五个特性...

    上海电机学院C语言实训答案

    (12)编写程序验证以下说法:输入一个4位数,该数个、十、百、千位上的数互不相等,由个、十、百、千位上的数组成一个最大数和一个最小数,最大数-最小数,构成一个新的4位数。反复以上运算,使其最终结果为:6174...

    二级C上机习题

    二级C上机习题1(1)【参考答案】 ...该程序考查求最大值,需要掌握以下语句: for(i=0;i;i++) /*将最大的元素放入指针max所指的单元,最大元素的下标放入指针d所指的单元*/ if(*max[i]) {*max=a[i];*d=i;}

    数据关联分析.pptx

    第7章 数据关联分析 7.1 基 本 概 念 7.2 频繁项集产生 7.3 规则产生 7.4 频繁项集的紧凑表示 7.5 产生频繁项集的 其他方法 7.6 FP-growth算法 7.7 关 联 评 估...其中Lk-1的元素是可连接的,如它们前(k-2)个项相同,

    算法设计与分析 王红梅

    5 算法的后验分析 1 .3 实验项目— — —求最大公约数 阅读材料— — —人工神经网络与 BP 算法 习题 1 第 2 章 NP 完全理论 2 .1 下界 2 . 1 . 1 平凡下界 2 . 1 . 2 判定树模型 2 . 1 . 3 最优算法 2 .2 算法的...

    数据结构经典问题和算法分析

    如果原问题可分割成k个子问题(1&lt;k≤n),且这些子问题都可解,并可利用这些子问题的解求出原问题的解,那么这种分治法就是可行的。由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。在...

    5-8无分隔符字典问题 算法分析

    问题描述: 设S={a1, a2,…, an}是n个互不相同的符号组成的符号集。Lk={b1b2…bn | biÎS,1£i£k}是S中字符组成的长度为k 的全体字符串。SÍLk是Lk 的无分隔符字典是指...输出每组的Lk的最大无分隔符字典的元素个数。

    严版数据结构习题答案

    分析可知,当n和k的最大公约数为p时,只要分别以A[0],A[1],...A[p-1]为起点执行上述算法,就可以保证每一个元素都被且仅被右移一次,从而满足题目要求.也就是说,A的所有元素分别处在p个"循环链"上面.举例如下: n=15,k=6...

    提取$$ | V_ {cd} | $$ | Vcd | 和$$ | V_ {cs} | $$ | Vcs | 使用晶格QCD $$ D \ rightarrow \ pi(K)\ ell \ nu $$ D→π(K)ℓν形状因数从实验衰减率得出

    我们给出Cabibbo–Kobayashi–Maskawa矩阵元素$$ | V_ {cd} | $$ | Vcd |的确定。 和$$ | V_ {cs} | $$ | Vcs | 通过结合半瘦矢量形式因子$$ f _ + ^ {D \ rightarrow \ pi}(q ^ 2)$$ f + D→π(q2)和$$ f _ + ^...

    超声波马达之设计及分析-90

    复合式超音波马达是同等大小之马达中具有最大致动力的,本文即针对此型式之马达作一设计,首先选择压电陶瓷材料之机电耦合因子k 和压电常数d 较大者,并输入高频之电压使其产生良好的的致动力,其次再利用马达之驱动...

    leetcode岛屿的最大面积-LeetCode:标记我在https://leetcode.com/上练习的问题的解决方案

    个元素 Top-K Frequency elements 根据字母频率排序 图的遍历 岛屿的最大面积 朋友圈 岛屿数量 二叉树叶子到根的路径 划分 IP 地址 封闭区域 可达性分析 回溯 数字组合 单词搜索 排列组合 有重复元素下的排列组合 ...

    java 经典习题.doc

    题目:输入两个正整数m和n,求其最大公约数和最小公倍数。 1.程序分析:利用辗除法。 【程序7】 题目:输入一行字符,分别统计出其中英文字母、空格、数字和其它字符的个数。 1.程序分析:利用while语句,条件为...

    python使用分治法实现求解最大值的方法

    本文实例讲述了python使用分治法实现求解最大值的方法。分享给大家供大家参考。具体分析如下: 题目: 给定一个顺序表,编写一个求出其最大值...题目看懂了就好说了,关键是要把顺序表分解成为k个元素为2的列表,然后找

Global site tag (gtag.js) - Google Analytics