面试题30:寻找最大(小)的k个数

题目描述:输入n个整数,输出其中最大的k个。

举例:输入序列1、2、3、4、5、6、7、8,输出最大的4个数字为5、6、7、8。

可能存在的条件限制:

要求 时间 和 空间消耗最小、海量数据、待排序的数据可能是浮点数等

方法一(不推荐):对所有元素进行排序,之后取出前K个元素

思路:使用最快排序算法,选择快排 或 堆排

时间复杂度:O(n*logn) + O(K) = O(n*logn)

特点:需要对全部元素进行排序,K = 1 时,时间复杂度也为O(n*logn)

注意:题中只需得到最大的K个数,而不需要对后面N-K个数排序

方法二(不推荐):只需要对前K个元素排序,不需要对N-K个元素进行排序

思路:使用 选择排序 或 起泡排序,进行K次选择,可得到第k大的数

时间复杂度:O(n*k)

方法三(推荐,复杂度最小):不对前K个数进行排序 + 不对N-k个数排序,可以使用。 思路:寻找第K个大元素。

解法一:O(n)的算法,需要修改数组值(需要交换数值),寻找最大的k个数

具体方法:使用类似快速排序,执行一次快速排序后,每次只选择一部分继续执行快速排序,直到找到第K个大元素为止,此时这个元素在数组位置后面的元素即所求。

在数组S中找出一个元素X,把数组分为两部分Sa和Sb。Sa中的元素大于等于X,Sb中元素小于X。
这时有两种情况:
1. Sa中元素的个数小于k,则Sb中的第k-|Sa|个元素即为第k大数;
2. Sa中元素的个数大于等于k,则返回Sa中的第k大数。

时间复杂度:因为每次只选择一部分递归,所以时间复杂度为N(1+1/2+1/4+....+1/x)<2N,所以复杂度为O(N).

       若随机选取枢纽,线性期望时间O(N)。

       若选取数组的“中位数”作为枢纽,最坏情况下的时间复杂度O(N)

以下代码用的是中位数法,得到最大的K个数。

下面的方法是找到最小的k个元素。如果想要找最大的k个元素,实际要找的是最小的size-k个元素。

枢纽元就选择第一个元素。

void Swap(vector<int>& input, int i, int j) {
  int tmp = input[i];
  input[i] = input[j];
  input[j] = tmp;
}

int Partition(vector<int>& input, int start, int end) {
  int privot = input[start];
  int low = start + 1, high = end;
  while (1) {
    while (privot >= input[low]) ++low;
    while (privot < input[high]) --high;
    if (low < high) {
      Swap(input, low, high);
      ++low;
      --high;
    } else {
      break;
    }
  }
  Swap(input, high, start);
  return high;
}

void Recursion(vector<int>& input, int start, int end, int k) {
  if (start >= end) {
    return;
  }
  int mid = Partition(input, start, end);
  if (k == mid+1) {
    return;
  } else if (k > mid+1) {
    Recursion(input, mid + 1, end, k);//k在后半部分
  } else {
    Recursion(input, start, mid - 1, k);//k在前半部分
  }
}

vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
  vector<int> ret;
  int size = input.size();
  if (k > size || k == 0) {
    return ret;
  }
  Recursion(input, 0, size-1, k);
  ret.assign(input.begin(), input.begin() + k);
  return ret;
}

如果是求最小的k个数,可以有下面的解法二。

解法二:O(nlogk)算法,无需修改原有数组,特别适合处理海量数据

我们可以创建一个大小为k的容器来存储最小k的数字,接下来遍历数组,每次读入一个数。

如果容器中的数字个数少于k,则直接把这个数插入到容器中;

如果容器中已有k个数,此时我们不能再插入新的数字,而只是替换已有的数字。

当容器满了后,我们要做3件事:

1. 在已有的k个数中找到最大值;

2. 有可能在容器中删除最大值

3. 有可能要插入这个新的数。

如果我们用二叉树实现这个数据容器,那么我们可以在O(logk)时间内实现这三个步骤。

因此,对于n个数字而言,复杂度为O(nlogk)。

红黑树的查找、插入、删除时间复杂度都是O(logn),所以我们可以用红黑树实现这个容器。

stl中的set和multiset都是基于红黑树实现的,元素都是有序的。

因此我们可以用multiset(可以存放重复元素)实现这个容器。

具体代码如下:

type multiset<int, greater<int> > IntSet //比较方法是greater,所以set中的元素按从大到小排列
type multiset<int, greater<int> >::iterator IntSetIterator

IntSet GetLeastKNumbers(vector<int> &data, int k)
{
	IntSet ret;
	if(k < 1 || data.size() <= k)
	{
		return;//最小的k个数就是data
	}

	for(vector<int>::iterator it = data.begin(); it != data.end(); ++it)
	{
		if(ret.size() < k)
		{
			ret.insert(*it);
		}
		else
		{
			IntSetIterator it_greatest = ret.begin();//set中的最大值
			if(*it_greatest > *it)//set中最大值大于这个数,则把最大值从set中删除,再插入这个数
			{
				ret.erase(it_greatest);
				ret.insert(*it);
			}
		}
	}

	return ret;
}

两种解法的比较:

第一种解法,基于分区,即类似快速排序的方法,虽然时间复杂度是O(n),但是需要修改数组的值。

第二种解法虽然慢一点,但是有两个优点,一是无需修改原数组的值,二是适合海量数据处理。

我们每次读入data中的一个元素,读写都只是在大小为k的容器中进行的,如果是海量的数据,我们就不能把所有数据一次性装入内存,第一种方法就不行了。

而第二种方法判断是否需要把新数据放入到容器中,只要要求内存大小能够容纳这个容器即可,因此最适合的情形就是n很大,k很小的情况。

下表总结了这两种解法的特点:

 


 

 

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值