查找最小的k个元素

目标:给定一个整型序列,找到最小的k个元素

例如:3,2,5,6,2,1,7,8 。若k=3,则返回1,2,3


思路

1. quickSort先对序列进行排序,花费O(nlog)时间,然后取出k个元素,花费O(k)时间,所以总的时间为O(k+nlogn)

2. 首先遍历序列前k个元素,存放到一个数组中,利用选择或交换排序,找出这k个数中的最大数k_max,所花O(k)时间。然后再遍历整个序列的后n-k个数,新的元素x<?k_max 如果小于,则用x代替k_max,否则,则不做更新数组。总的时间复杂度为n*O(k)=O(nk)时间。

回顾选择排序

长度为n的无序数组,第一次遍历n个数,选出最小的数,与第一个元素交换。第二遍遍历n-1个数,就是除去了第一个元素了,在n-1个元素中找出最小的数,与第二个数交换(在n-1个元素的数组中就相当于是第一个元素了)。

时间复杂度O(n^2),空间复杂度O(1)用于存放交换用到的temp和记录最小值的index。

代码:

public static void select(String[] array){
	int i = 0;
	while(i<array.length){
		String min = array[i];
		int min_index = i;
		for(int j=i+1; j<array.length;j++){
			if(array[j].compareTo(min)<0){
				min = array[j];
				min_index = j;
			}
		}
		String temp = array[i];
		array[i] = min;
		array[min_index] = temp;
		i++;
	}
}


3. 利用k个元素的最大堆

回顾堆数据结构

二叉堆(常简称为堆)是完全二叉树或者是近似完全二叉树
最大堆:父节点键值总是大于等于任意一个子节点的键值。且每个节点的左子树和右子树都是一个二叉堆。(最小堆相反)
其他类型:二项式堆,斐波那契堆
存储:
用数组表示。i节点的父节点下标就是(i-1)/2。左右子节点分别是2*i+1和2*i+2。例如,第0个节点的左右节点下表为1和2. (0节点就是root)

堆的插入:每次将新数据放在数组末尾,然后将其上浮,恢复堆次序
public static void minHeapUp(String[] array, int i){
	//插入的时候会用到上浮操作
	int j;
	String temp = array[i];
	j = (i - 1)/2;//父节点
	while(j>=0 && i!=0){
		//因为是最小堆,所以如果父节点比子节点小,这是正常情况,
		//不需要恢复堆次序,所以直接break
		if(array[j].compareTo(temp)<=0)
			break;
		array[i] = array[j];
		i=j;
		j = (i-1)/2;
	}
	array[i] = temp;
}
对于maxHeapUp,把array[j].compareTo(temp)<=0换成>=0即可
堆的删除:每次将数组第一个元素删除,即root,然后取出数组最后一个元素补位到root,再将其下沉,恢复堆次序。
public static void minHeapDown(String[] array, int i, int n){
	//删除节点会用到下移操作
	int j;
	String temp = array[i];
	j = 2 * i + 1;//子节点
	while(j<n){
		if(j + 1 < n && array[j+1].compareTo(array[j])<0)
			j = j+1;//在保证有右子节点的情况下,找左右孩子中<span style="color:#ff0000;">较小</span>的
		//子节点比父节点大,则不用对父节点下移
		if(array[j].compareTo(temp)>=0)
			break;
		array[i] = array[j];
		i=j;
		j = 2*i+1;
	}
	array[i] = temp;
}
对于最大堆
public static void maxHeapFixdown(String[] array, int i, int n){
	//删除节点会用到下移操作
	int j;
	String temp = array[i];//要下沉的节点
	j = 2 * i + 1;//子节点
	while(j<n){
		if(j + 1 < n && array[j+1].compareTo(array[j])>0)
			j = j+1;//在保证有右子节点的情况下,找左右孩子中<span style="color:#ff0000;">较大</span>的
		//子节点比父节点大,则不用对父节点下移
		if(array[j].compareTo(temp)<=0)
			break;
		array[i] = array[j];
		i=j;
		j = 2*i+1;
	}
	array[i] = temp;
}



恢复堆次序,先在左右子节点中找小的,如果父节点比这个小的子节点还要小,就不用调整了,否则将父节点与它交换,再考虑后面的节点。
堆化数组
对于叶子节点,不动。从内节点开始向下调整。
建立堆:
1. 插入法
从空堆开始,依次插入每一个结点,直到所有的结点全部插入到堆为止。 
  时间:O(n*log(n)) 
2. 调整法: 
    序列对应一个完全二叉树;从最后一个分支结点(n div 2-1)开始,到根(0)为止,依次对每个分支结点进行调整(下沉),
以便形成以每个分支结点为根的堆,当最后对树根结点进行调整后,整个树就变成了一个堆。 
  时间:O(n) 
public static void makeMinHeap(String[] array, int n){
	for(int i = n / 2 - 1; i >= 0; i--){
		minHeapFixdown(array,i,n);
	}
}
建立最大堆的过程。
public static void makeMaxHeap(String[] array, int n){
	for(int i = n / 2 - 1; i >= 0; i--){
		maxHeapFixdown(array,i,n);
	}


初始:45 36 18 53 72 30 48 93 15 35

这样就把一个数组创建成了一个最大堆。


堆排序
root是堆中最小的数据,所以取出后再执行删除操作,小的节点又会浮到root为止,再次取出并对小节点做上浮操作。
复杂度:
1. 由于二叉树的高度为lgN,重新恢复堆次序,小节点上浮最多会执行lgN次,所以O(lgN)。
2. 最多N-1次恢复堆操作。
3. 建堆需要进行N/2次down操作。
所以总的复杂度=lgN*(N-1) + N/2 = O(NlgN)


首先遍历数组中的前k个元素,建立最大堆,用时O(k)。root为k个元素中的最大元素,称作k_max。然后接着在数组中遍历剩下的N-k个元素。每次取出元素x与k_max比较,如果x<k_max,则用x取代k_max,然后调整恢复堆次序,使得新一轮的k个数中的最大值浮到root,成为新的k_max。如果x>k_max,则不操作。恢复堆次序耗时O(lgk)时间,最多可进行n-k次堆恢复次序,因此需要耗时(n-k)O(lgk)时间。

加上之前的O(k),整个查找过程需要耗时O(k+(n-k)lgk)=O(nlgk)时间。

代码

public static void getKminbymaxHeap(String[] array, int n, int k){
	String[] k_heap = new String[k];
	int i = 0;
	//先把数组前k项存进最大堆中
	while(i<k){
		k_heap[i] = array[i];
		i++;
	}
	makeMaxHeap(k_heap,k_heap.length);//建立k元素最大堆
	//当数组的后n-k里的元素比最大堆的root小时,执行交换和下沉操作
	while(i<array.length){
		if(array[i].compareTo(k_heap[0])<0){
			k_heap[0] = array[i];
			maxHeapFixdown(k_heap,0,k);
		} 
		i++;
	}
	//打印最大堆
	for(int j = 0; j<k_heap.length;j++){
		System.out.println(k_heap[j]);
	}
}



4. 既然有最大堆保存k个元素,我们可以尝试对整个数组中的元素建立一个最小堆,然后每次取出root,执行k次。

对于n个元素的数组建堆,需要O(n)时间,每次取出root然后恢复堆次序的时间是O(lgn),执行k次,所以总的时间,加上建堆,有O(n+klgn)复杂度


关于建立k个元素的最大堆,和建立n个元素的最小堆,也就是3,4两种方法的比较。

即O(nlgk)和O(n+klgn)。

通过两个复杂度的比值,即nlgk/(n+klgn),判断是大于1还是小于1.

当n趋向于无穷时nlgk/(n+klgn)=lgk,分子分母同时用洛必达法则得  lgk/(1+k/nln2),因为n趋向无穷,所以k/nln2->0,所以整个分式等于lgk>=1当k>=2.因此在这种情况下,建立k元素的最大堆的方案的复杂度要大于建立n元素的最小堆然后再取出k个元素的方案。但是如果考虑空间复杂度,则建立最小堆的空间复杂度为O(n),远大于k元素最大堆的空间复杂度O(k),所以,it depends.


对于n元素最小堆情况,当取出root,并且将数组尾巴的元素替换到root的位置之后,只需要对该root元素下移k次即可,不需要像堆操作的O(lgn)复杂度,只需要O(k)复杂度(每次下移),所以总的复杂度为O(n+k^2)即可。

Proof by intuition:

最小堆的数组存储方式,不是严格递增,但是总体的关系是递增的。每次下移的过程,都是把父节点与较小的子节点交换位置,所以第一次的堆次序恢复操作要执行k次,那个会被选出来的元素就上移了一位。由于每个子节点也是一个最小堆,所以在这个节点形成的子树中的其他元素肯定比这个元素要大,无需考虑它们的上浮。因此只需要移动k次即可。然后第二轮的堆次序恢复就执行k-1的,第三轮k-2次。。。

这样操作的缺点是破坏了最小堆的结构,但是遭到破坏的仅是在我们关心的元素以外,第一次保证了k层以上的最小堆结构,而下面的部分遭到破坏,第二层保证了k-1层,但是我们不care下面是否还保证最小堆结构了。

public static void getKmin(String[] array, int n, int k){
	int j = n-1;
	for(int i = k; i>0; i--, j--){
		String min = array[0];
		array[0] = array[j];
		minHeapFixdown(array,0,i);//这里只需要下降k次,因此n=i
		System.out.println(min);
	}
}

5. Randomized-Select,每次随机选取数列中的一个元素作为pivot,在E[O(n)]时间内找到第k小的元素,然后花O(k)时间遍历之前的比k小的元素,总的期望时间复杂度为O(n+k)=O(k)。但是在最坏情况下的复杂度为O(n^2),怎样解决呢?

partition的代码

private static int partition(int[] a, int lo, int hi)
{
	int x = a[lo];
	int i = lo;
	int j = lo + 1;
	while (j<=hi)
	{
		if(a[j]<=x)
		{
			i++;
			exch(a,i,j);
		}
		j++;
	}
	exch(a,lo,i);
	return i;//return index of item now known to be in place
}
随机partition代码
private static int randomized_partition(int[] a, int lo, int hi){
	int i = (int) Math.round(Math.random() * (hi - lo) + lo);
	exch(a, a[lo],a[i]);
	return partition(a,lo,hi);
}

随机快速选择代码

public static int randomized_select(int[] a, int lo, int hi, int k)//k is rank
{
	if(lo==hi) return a[lo];
	int j = randomized_partition(a,lo,hi);
	int rank_pivot = j - lo + 1;
	if (rank_pivot==k) return a[j];
	if(k<rank_pivot) return randomized_select(a,lo,j-1,k); 
	else return randomized_select(a,j+1,hi,k-rank_pivot);
}



6. Idea is generating good pivot recursively, not a random pivot just like what've been done in 5.

a. Divide the n elements into floor(n/5) groups. Each group has 5 elements. Find the median of each group. It needsO(n) time

b. Recursively select the median x of the floor(n/5) groups median. NeedT(n/5) time, also linear.

c. Partition with x as pivot. Let k = rank(x).

d. if i = k, then return x.

if i<k, then recursively select i smallest element in the lower part of array.

else, recursively select (i-k)th smallest element in the high part of array. It needsT(3/4n)

T(n)<=T(n/5)+T(3/4n)+O(n)

Proof T(n)<=cn by substitution.

T(n)>cn/5+3cn/4+O(n)=19n/20+O(n) = cn-(1/20-O(n))<cn for c sufficient large.




  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
您好,关于Python实现查找最小的k个数,我可以提供以下两种常见的方法: 方法一:使用堆 将数组前k个元素建立小根堆,然后遍历剩余元素,若比堆顶元素小,则将其加入堆中,同时将堆顶元素删除,最终堆中的k个元素即为所求。具体代码如下: import heapq def find_k_min(nums, k): if not nums or k <= 0 or k > len(nums): return None res = [] for i in range(k): heapq.heappush(res, nums[i]) for i in range(k, len(nums)): if nums[i] < res[0]: heapq.heappop(res) heapq.heappush(res, nums[i]) return res 方法二:使用快排的思想 使用快排的思想,每次选择一个基准元素,将小于基准元素的放在左边,大于等于基准元素的放在右边,然后判断基准元素的位置,若该位置等于k-1,则基准元素以及左边的元素就是所求;若该位置小于k-1,则在右半边继续查找;若该位置大于k-1,则在左半边继续查找。具体代码如下: def find_k_min(nums, k): if not nums or k <= 0 or k > len(nums): return None left, right = 0, len(nums)-1 while True: index = partition(nums, left, right) if index == k-1: return nums[:k] elif index < k-1: left = index + 1 else: right = index - 1 def partition(nums, left, right): pivot = nums[left] while left < right: while left < right and nums[right] >= pivot: right -= 1 nums[left] = nums[right] while left < right and nums[left] < pivot: left += 1 nums[right] = nums[left] nums[left] = pivot return left 希望能对您有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值