arrays - 查找数组中的元素,但是元素可以跳转

标签 arrays algorithm

有一个数组,其中除了一个单元格之外的所有单元格都是 0,我们想要找到该单个非零单元格的索引。问题是,每次检查此数组中的单元格时,该非零元素将执行以下操作之一:

  • 向前移动 1
  • 向后移动 1
  • 留在原地。

  • 例如,如果该元素当前位于位置 10,并且我检查 arr[5] 中的内容,那么在我检查 arr[5] 后,该元素可能位于位置 9、10 或 11 .

    我们只需要找到元素当前所在的位置,而不是它开始的位置(这是不可能的)。

    困难的部分是,如果我们写一个 for 循环,真的没有办法知道元素当前是在你的前面还是在你的后面。

    如果有帮助,请提供更多上下文:
  • 面试官确实给出了一个提示,也许我应该在检查 x 数量的单元格后将指针移回。问题是,我应该什么时候退回,退回多少个槽位?
  • 在“大声思考”的同时,我开始说一堆常见的方法,希望能有所收获。当我说递归时,面试官确实说“递归是一个好的开始”。我不知道递归真的是正确的方法,因为我不知道如何同时进行递归和 #1。
  • 面试官说这个问题不能用O(n^2)解决。所以我们至少要考虑 O(n^3),甚至可能是指数级的。
  • 最佳答案

    Tl;博士:最好的办法是依次检查数组中的每个偶数索引,根据需要循环多次,直到找到目标。平均而言,您会在第二次通过时偶然发现目标。

    首先,正如许多人已经说过的那样,确实不可能确保在任何给定的时间内找到目标元素。如果元素知道您的下一个样本将在哪里,它总是可以及时将自己放置在其他地方。您能做的最好的事情是以最小化预期访问次数的方式对数组进行采样 - 因为在每次采样之后,除了成功与否之外您什么也学不到,并且成功意味着您停止采样,因此可以描述最佳策略简单地作为应该检查的索引序列,仅取决于您正在查看的数组的大小。我们可以通过自动化手段依次测试每个策略,看看它们的表现如何。结果将取决于问题的具体情况,所以让我们做一些 假设 :

  • 这个问题没有指定我们的目标的起始位置。让我们假设起始位置是从整个阵列中统一选择的。
  • 这个问题没有具体说明我们的目标移动的概率。为简单起见,假设它独立于参数,例如数组中的当前位置、耗时和样本的历史。对每个选项使用概率 1/3 给我们的信息最少,所以让我们使用它。
  • 让我们在 100 101 个元素的数组上测试我们的算法。此外,让我们对每个算法进行一百万次测试,以合理确定其平均情况下的行为。

  • 算法 我测试过的是:
  • 随机抽样:每次尝试后,我们都会忘记我们在哪里寻找并随机选择一个全新的索引。每个样本都有独立的 1/n 成功机会,因此我们期望平均取 n 个样本。这是我们的控制。
  • Sweep:依次尝试每个位置,直到找到我们的目标。如果我们的目标没有移动,这将平均需要 n/2 个样本。然而,我们的目标正在移动,所以我们可能会在第一次扫描时错过它。
  • 慢扫描:相同,除了我们在继续之前测试每个位置几次。 Proposed by Patrick Trentin减速因子为 30 倍,测试时减速因子为 2 倍。
  • 快扫:与慢扫相反。在第一个样本之后,我们在测试下一个之前跳过 (k-1) 个单元格。第一遍从 ary[0] 开始,下一次从 ary[1] 开始,依此类推。使用从 2 到 5 的每个加速因子 (k) 进行测试。
  • 左右扫描:首先我们从左到右依次检查每个索引,然后从右到左依次检查每个索引。如果它一直在移动(事实并非如此),则该算法将保证找到我们的目标。
  • 聪明贪心:Proposed by Aziuth .该算法背后的想法是我们跟踪每个单元格持有目标的​​概率,然后始终以最高概率对单元格进行采样。一方面,这个算法相对复杂,另一方面,它听起来应该给我们最优的结果。

  • 结果:

    结果显示为[平均值]±[标准推导]。
  • 随机抽样:100.889145±100.318212

    在这一点上,我意识到我的代码中存在一个围栏错误。好在我们有一个对照样本。这也证实了我们有两到三位数的有用精度 (sqrt #samples),这与其他此类测试一致。
  • 扫频:100.327030 ± 91.210692

    我们的目标通过网井挤压的机会抵消了目标平均花费 n/2 次到达网的效果。该算法的平均表现并不比随机样本好,但它的性能更加一致,而且实现起来也不难。
  • 慢速扫描 (x0.5):128.272588 ± 99.003681

    虽然我们的网移动缓慢意味着我们的目标可能会在第一次扫网时被网夹住,不需要第二次扫网,但这也意味着第一次扫网需要两倍的时间。总而言之,依靠目标移动到我们身上似乎有点低效。
  • 快速扫描 x2:75.981733 ± 72.620600
  • 快速扫描 x3:84.576265 ± 83.117648
  • 快速扫描 x4:88.811068 ± 87.676049
  • 快速扫描 x5:91.264716 ± 90.337139

    这……起初有点令人惊讶。虽然每隔一步跳过意味着我们以两倍的圈数完成每一圈,但每一圈也减少了实际遇到目标的机会。一个更好的 View 是在扫帚空间中比较 Sweep 和 FastSweep:旋转每个样本,以便被采样的索引始终为 0,并且目标向左漂移得更快一点。在 Sweep 中,目标每一步以 0、1 或 2 的速度移动。与斐波那契基数的快速平行告诉我们,目标应该有 62% 的时间击中扫帚/网。如果未命中,则需要再过 100 回合才能返回。在 FastSweep 中,目标每一步以 1、2 或 3 的速度移动,这意味着它更频繁地错过,但重试也需要一半的时间。由于重试时间比命中率下降得更多,因此使用 FastSweep 比使用 Sweep 更有优势。
  • 左右扫频:100.572156±91.503060

    大多数情况下就像一个普通的扫描,它的分数和标准推导反射(reflect)了这一点。结果并不出人意料。
  • Aziuth 的聪明贪婪:87.982552 ± 85.649941

    在这一点上,我不得不承认我的代码中有一个错误:这个算法严重依赖于它的初始行为(Aziuth 未指定它并在我的测试中被随机选择)。但性能问题意味着该算法每次将始终选择相同的随机顺序。结果是随机化的特征,而不是整个算法的特征。

    总是选择最有可能的位置应该尽快找到我们的目标,对吧?不幸的是,这种复杂的算法几乎无法与 Sweep 3x 竞争。为什么?我意识到这只是推测,但让我们看看 Smart Greedy 实际生成的序列:在第一遍期间,每个单元格包含目标的概率相等,因此算法必须选择。如果它随机选择,它可以在概率下降到达所有单元格之前在 20% 的单元格中找到它们。之后,在最近没有对数组进行采样的地方,景观大部分是平滑的,因此算法最终会停止扫描并开始随机跳跃。真正的问题是该算法过于贪婪,并不真正关心放牧目标,因此它可以更轻松地选择目标。

    尽管如此,这种复杂的算法确实比简单的 Sweep 和随机采样器要好。然而,它仍然无法与 FastSweep 的简单性和惊人的效率相媲美。重复测试表明,初始随机化可以使效率在 80% 运行时间(20% 加速)和 90% 运行时间(10% 加速)之间摆动。

  • 最后,这里是 代码用于生成结果:
    class WalkSim
      attr_reader :limit, :current, :time, :p_stay
      def initialize limit, p_stay
        @p_stay = p_stay
        @limit = limit
        @current = rand (limit + 1)
        @time = 0
      end
    
      def poke n
        r = n == @current
        @current += (rand(2) == 1 ? 1 : -1) if rand > @p_stay
        @current = [0, @current, @limit].sort[1]
        @time += 1
        r
      end
    
      def WalkSim.bench limit, p_stay, runs
        histogram = Hash.new{0}
        runs.times do
          sim = WalkSim.new limit, p_stay
          gen = yield
          nil until sim.poke gen.next
          histogram[sim.time] += 1
        end
        histogram.to_a.sort
      end
    end
    
    class Array; def sum; reduce 0, :+; end; end
    
    def stats histogram
      count = histogram.map{|k,v|v}.sum.to_f
      avg = histogram.map{|k,v|k*v}.sum / count
      variance = histogram.map{|k,v|(k-avg)**2*v}.sum / (count - 1)
      {avg: avg, stddev: variance ** 0.5}
    end
    
    RUNS = 1_000_000
    PSTAY = 1.0/3
    LIMIT = 100
    
    puts "random sampling"
    p stats WalkSim.bench(LIMIT, PSTAY, RUNS) { 
      Enumerator.new {|y|loop{y.yield rand (LIMIT + 1)}}
    }
    
    puts "sweep"
    p stats WalkSim.bench(LIMIT, PSTAY, RUNS) { 
      Enumerator.new {|y|loop{0.upto(LIMIT){|i|y.yield i}}}
    }
    
    puts "x0.5 speed sweep"
    p stats WalkSim.bench(LIMIT, PSTAY, RUNS) { 
      Enumerator.new {|y|loop{0.upto(LIMIT){|i|2.times{y.yield i}}}}
    }
    (2..5).each do |speed|
      puts "x#{speed} speed sweep"
      p stats WalkSim.bench(LIMIT, PSTAY, RUNS) { 
        Enumerator.new {|y|loop{speed.times{|off|off.step(LIMIT, speed){|i|y.yield i}}}}
      }
    end
    puts "sweep LR"
    p stats WalkSim.bench(LIMIT, PSTAY, RUNS) { 
      Enumerator.new {|y|loop{
        0.upto(LIMIT){|i|y.yield i}
        LIMIT.downto(0){|i|y.yield i}
      }}
    }
    
    $sg_gen = Enumerator.new do |y|
      probs = Array.new(LIMIT + 1){1.0 / (LIMIT + 1)}
      loop do
        ix = probs.each_with_index.map{|v,i|[v,rand,i]}.max.last
        probs[ix] = 0
        probs = [probs[0] * (1 + PSTAY)/2 + probs[1] * (1 - PSTAY)/2,
                 *probs.each_cons(3).map{|a, b, c| (a + c) / 2 * (1 - PSTAY) + b * PSTAY},
                 probs[-1] * (1 + PSTAY)/2 + probs[-2] * (1 - PSTAY)/2]
    
        y.yield ix
      end
    end
    
    $sg_cache = []
    def sg_enum; Enumerator.new{|y| $sg_cache.each{|n| y.yield n}; $sg_gen.each{|n| $sg_cache.push n; y.yield n}}; end
    
    puts "smart greedy"
    p stats WalkSim.bench(LIMIT, PSTAY, RUNS) {sg_enum}
    

    关于arrays - 查找数组中的元素,但是元素可以跳转,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36149346/

    相关文章:

    javascript - 使用父对象的数据展平数组对象内的数组

    c - 将数组传递给函数时,是否需要提供元素数作为参数?

    javascript - 如何查找值是否与 Javascript 中的数组中的值之一匹配

    algorithm - 如何调整K-means聚类?

    r - 将集合分为n个不相等的子集,关键决定因素是该子集中的元素聚合并等于预定量吗?

    algorithm - 最小堆中第k小元素的复杂度分析

    c++ - 使用递归段错误核心转储对数组进行排序

    c# - 仅在数组中包含一个数字

    algorithm - 存在大小为 NxN 的 Hadamard 矩阵

    algorithm - 仅使用交换函数根据父数组对给定数组进行排序