查找最小的k个元素

来源:互联网 发布:淘宝格士子湖铺 编辑:程序博客网 时间:2024/06/05 23:00

目标:给定一个整型序列,找到最小的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){//因为是最小堆,所以如果父节点比子节点小,这是正常情况,//不需要恢复堆次序,所以直接breakif(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=iSystem.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 0
原创粉丝点击