【问题标题】:data structure interview : find max number in array数据结构面试:在数组中查找最大数
【发布时间】:2013-06-29 06:26:36
【问题描述】:

我最近在某个地方遇到了一个非常好的面试问题,我想问你们所有的天才,什么是最优化的解决方案。所以问题如下: 给定一个整数数组,找到一个最大数 n,使得至少有 n 个数组元素大于 n。输入数组未排序。

例如:

输入:1,2,5,7,8,10 输出:n = 4

输入:0,2,7,8,19,5,45,9,23 输出:n = 6

我能想到的一个解决方案(如果数组是排序的情况)是顺序扫描数组中的所有元素以找出 min:n 和 max:n。然后将 min:n 到 max:n 之间的整数递增并一一检查。但这是 O(N) 解决方案。有人可以推荐一个更好的吗?
例如: 对于输入 1 min:n = 2 和 max:n = 5
那么您将检查数字 2,3 和 4 作为答案。

从答案来看,如果数组未排序,则没有比 O(N) 更好的解决方案。但是下一个问题是如果给定的数组是排序的呢?

pseudocode :
// this assumes sorted input.
pubic int findhighestIndex(List<Integer> input){
it min=0,max=0,n=0,maxIndex=0;
for(int i=0;i<input.size();i++){
    if( input.get(i)>(input.size()-i) ){
        max=input.get(i);
        maxIndex=i;
        min=input.get(i-1);
        break;
    }
    else if(input.get(i)<(input.size()-i)){
        max=min=input.get(i);
    }
}
int i=max;
while( i>=min && (input.size()-maxIndex)<i ){
i--;
}
System.out.println(i);
}


更新:这个问题也被称为寻找 h-index

【问题讨论】:

  • “O(N) 解”中的 N 指的是什么?另外,是否给出了整数大小或数组大小的上限?
  • N 是数组中给定的元素个数。
  • 如果数组未排序,那么你不会比 O(N) 做得更好
  • 好的。所以下一个问题是如果数组是排序的。 ?
  • 我想我没有把问题说清楚,我刚刚编辑过,你能检查一下吗?

标签: arrays algorithm data-structures big-o


【解决方案1】:

编辑:刚刚为未排序的情况找到了O(n) 解决方案:) 见下文!

排序:

这可以在O(log N) 中通过对n 的二分查找来解决。我将在这里使用 OP 的符号,N = # of elementsn 是我们正在寻找的答案。

如果数组是排序的,这基本上意味着我们需要找到一个位置[N - n],以便数组中的这个位置包含一个大于n的值——如果是这样,那么至少有n值大于它,而不考虑重复值。

请注意,答案始终是可能的,因为在最坏的情况下,答案将是0,并且总是有至少 0 个大于它的元素。显然,对于较低的值,答案总是“更容易”,因为它更容易找到大于 1 的 1 个元素,而不是大于 10 的 10 个元素。但更重要的是,这个函数遵循单调(非递减)行为,这允许我们对它使用二进制搜索。

思路如下:

int N = 9;
int arr[10] = {0,2,5,7,8,9,19,23,45};

int lo = 0, hi = N+1, mid;
while(hi-lo > 1){
    mid = (hi+lo)/2;
    if(arr[N-mid] > mid) lo = mid;
    else hi = mid;
}
n = lo; //highest value that worked

细分:数组的大小为9。二进制搜索可能会开始尝试值n = 5,因此我们只需检查数组末尾的第 5 个元素是否大于 5。在这种情况下,8 &gt; 5,因此我们可以尝试更好的答案。然后搜索会尝试7,但位置[N-7] 的元素是5,它小于7,不满足我们的约束。因此,搜索的最后一次尝试是值 6,它返回 true 为 7 &gt; 6

未排序:

对于未排序的情况,这个想法非常相似!我们可以在O(n) 中解决它,通过使用Selection Algorithm 来识别第[N-n] 个元素,并在每一步以与二分查找相同的方式划分搜索空间。

我们首先从[0][N-1] 搜索以找到中间元素(N/2 th),我们可以在另一个O(N) 步骤中重新排列数组,以便放置中间元素在它的正确位置,并且它之前的每个元素都有一个值&lt;= median,而它之后的每个元素都有一个值&gt;=median

现在,如果该值大于n(在本例中为N/2),我们在上面显示至少有n 元素大于n,并且因此我们只需要在数组的下半部分进一步搜索。 (如果中值低于n,我们只考虑数组的大半部分)

现在,假设median &gt;= N/2,我们将从索引[0][N/2] 重复相同的过程,使用O(N/2) 中的选择“排序”,依此类推,每次将搜索空间除以2。

C++代码如下:

int N = 9;
int arr[9] = {0,2,7,8,19,5,45,9,23};

int lo = 0, hi = N, mid;
while(hi-lo > 1){
  mid = (hi+lo)/2;
  std::nth_element(arr+lo, arr+mid, arr+hi);
  if(arr[mid] > N-mid) hi = mid;
  else lo = mid;
}
n = N-hi;

最后,我们实现了O(N) + O(N/2) + O(N/4) + ... = O(2*N) = O(N)的复杂度

【讨论】:

  • 将桶排序或基数排序视为一种简单的技巧,以在未排序的数组上获得〜线性复杂性。
  • 这是一个了不起的答案,但是在未排序的情况下,您的解决方案是次优的(并且“平均”仅为 O(n):请参阅 The Complexity section for std::nth_element)。请参阅William Gates 的解决方案的my implementation,以了解在保证 O(n) 时间内运行的实际上更快的算法。
【解决方案2】:

不涉及魔法

如果您一直在阅读上述内容并想“我怎么会在面试中提出这个问题”或“我真的可以相信这段代码没有错误”,那就不要再看了!让我向您介绍“正式程序设计”的快乐世界!

在这个答案中,我将解释我们如何将问题陈述转化为一对不等式,这反过来又会强制我们进行二分搜索,所以只有一种写法。我还会发现之前的答案中遗漏的几个错误和极端情况。

全部设置

假设我们有一个大小为N=7 的已排序非空数组。

N: 7
    i: 0 1 2 3 4 5 6
ar[i]: 3 3 4 5 6 6 7

我们真正想要的是i s.t.

ar[i] <= N-i-1

但是,我们想要最大的那个,也就是最右边的那个,所以一定是那个

ar[i+1] > N-i-1

变得正式

我们要做的是保留两个变量lohi st。我们一直都有

ar[lo] <= N-lo-1   (1)
ar[hi] > N-hi-1    (2)

(注意第二个等式中i+1 替换为hi)。

然后我们将小心地将变量移向彼此,直到lo+1 = hi,此时我们找到了我们最初寻找的i

现在我们需要一些起始值。

  • hi 的选项可以是 N。这超出了数组的范围,但我们永远不会读取它,所以我们假设它是一个满足等式 (2) 的巨大值。

  • lo 更难,因为我们甚至可以确定这样的值存在吗?不!数组[7,8,9] 没有满足所需属性的索引,因此我们找到了第一个极端情况。我们可以假设,如果任何索引满足 (1),它一定是0,但是我们必须引入一个测试,看看它是否真的可以继续。

甜!我们避免了一个讨厌的错误。

将其插入代码

好的,现在是调用二分搜索的时候了。确实工作已经完成了,我们简单地写:

if ar[0] > N-0-1:
    panic("No solutions found!")

lo, hi = 0, N
while lo+1 != hi:
    mid = (lo + hi)/2
    if ar[mid] <= N-mid-1:
        lo = mid
    if ar[mid] > N-mid-1:
        hi = mid

print "The solution is ar[%d] = %d" % (lo, ar[lo])

(请注意,我们可以将第二个if 更改为else,因为条件彼此相反)

结果

在原始示例上运行它会得到:

The solution is ar[2] = 4

为了好玩,我还尝试使用相同的数组运行“i Code 4 Food”的代码。我认为他认为价值观是独一无二的,因为他回来了

lo = 4

这显然不起作用,因为ar[4] = 6,之后只有两个值。

【讨论】:

  • 糟糕,我似乎不太明白这个问题,而且结果不必在数组中。希望你会喜欢这个。
【解决方案3】:

不需要排序。

如果 a[1...N] 是输入数组,请注意您正在寻找的答案是

所以对于 0 i。

为了在 O(N) 时间内计算它,我们分配一个大小为 N+1 的数组 S,初始化为零。

通过a,当遇到元素a(= a[j])时,如果a > N,则增加S[N+1],否则增加S[a]。

元素的数量 > i 将由 S[i+1] + S[i+2] + ... + S[N+1] 给出。

我们可以通过 S 从 N+1 到 1 来计算每个 i,并保持一个累积和。

【讨论】:

  • 这是未排序输入的最简单解决方案。
【解决方案4】:

“i Code 4 Food”给出的答案绝对精彩。

但我认为你可以用另一种方式来决定起点(我不知道这是否更好)。

假设满足给定条件的元素是n。现在假设我想从排序后的数组中随机选择一个元素(让整数的随机变量为 X)然后 P( X > n) >= n/N (其中 N 是数组中元素的总数)。

但是根据马尔可夫不等式,我们有P( X > n) 。这里 E[X] 是期望值,即在这种情况下的平均值。

考虑到以上两个不等式,我们有 n/N 即 n^2

考虑例如输入:1,2,5,7,8,10,我们会得到不等式 n^2 所以 n 。所以我们可以在这里设定我们的起点。

【讨论】:

  • 你的方法很有趣。但是我不确定,这是概率方法吗?你能详细说明一下吗?
  • 是的,这是一种概率方法。我正在考虑从给定数组中随机选择的整数是 X,然后我正在考虑给定条件以获得不等式。
【解决方案5】:

如果您不允许排序,这只是另一种解决方案。

O(N log M) 

地点:

N=输入中的元素数

M=输入中的数字范围

算法

对答案进行二分搜索。

 First find max element(M) of input using linear scan.
 int lo=0, hi=M
 while(hi-lo>1)
 {
  int mid=(lo+hi)/2;
  int t=0;
  for(int i=0;i<N;i++)if(A[i]>mid)t++;
  if(t>=mid)lo=mid;
  else hi=mid-1;
 }
 return lo;

如果你在整数范围内做,log M因子只有32

【讨论】:

  • 那不是通常写成 O(N loglogN) 吗?
【解决方案6】:

由于我对威廉·盖茨的回答的编辑因“推广产品或服务”(什么?)而被拒绝,因此我在此处复制了实现他的解决方案的代码。在 C++ 中,这可以在保证线性时间内对任何数据集实现为:

#include <algorithm>
#include <vector>

size_t solve(std::vector<int> const &input) {
    std::vector<size_t> counts(input.size() + 1, 0);
    for (auto val : input) {
        if (0 <= val)
            ++counts[std::min(static_cast<size_t>(val), input.size())];
    }
    size_t n{ input.size() };
    for (size_t numGreater{ counts[n] }; 0 < n
         && numGreater < n; numGreater += counts[--n]);
    return n;
}

请注意,这需要 O(N) 额外内存和 O(N) 时间。

【讨论】:

  • 拒绝投票者是否愿意发表评论?我添加了缺少的头文件和缺少的 static_cast 来编译代码,以防导致混淆。然后我在 OP 提供的两个样本输入上运行它,并且都产生了预期的结果。此外,它是唯一一种在保证 O(N) 时间内运行的未排序输入算法。
猜你喜欢
  • 2016-01-31
  • 2018-01-08
  • 1970-01-01
  • 2021-11-29
  • 2013-11-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多