Ruby递归索引/搜索方法(使用中间比较)返回错误的索引值

时间:2019-02-16 22:27:38

标签: ruby recursion

在递归单元上的自学Ruby。

我正在写一个带有两个参数的方法:bsearch(array,target)。数组将始终被排序,我希望该方法使用递归以这种方式返回在其中找到目标的索引:

将目标与(排序的)数组的中间元素进行比较。如果它大于中间元素,则在数组的后半部分再次运行该方法。如果它小于中间元素,请使用数组的前半部分再次运行该方法。

对于任何小于中间元素的目标,我都取得了不错的结果,但是当目标大于中间元素时,我遇到了问题。我可以理解这些方法调用产生的结果,但是我不确定如何修复我的方法以获得正确的输出。

def bsearch(arr, target)
    middle_index = arr.length / 2

    return middle_index if arr[middle_index] == target
    return nil if arr.length == 1

    if target > arr[middle_index]
        bsearch(arr[middle_index+1..-1], target)
    elsif target < arr[middle_index]
        bsearch(arr[0...middle_index], target)
    end
end

当我输入时:

bsearch([1, 2, 3], 1) # => 0
bsearch([2, 3, 4, 5], 3) # => 1
bsearch([2, 4, 6, 8, 10], 6) # => 2

所有这些都正确输出,但是当我运行时:

bsearch([1, 3, 4, 5, 9], 5) # => 3
bsearch([1, 2, 3, 4, 5, 6], 6) # => 5

它们分别返回0和1。我可以看到为什么它们做为0和1时会在较小的较新版本的arr中作为目标的索引:[5,9](5处在索引0处),然后[5,6](6处在索引处) 1)。

在这两种情况下如何访问正确的middle_index?

关于如何改进/简化我的方法的任何评论和推理都将对我有所帮助!

4 个答案:

答案 0 :(得分:1)

您可以编写如下的递归。

def bsearch(arr, target)
  return nil if target < arr.first || target > arr.last
  recurse(arr, target, 0, arr.size-1)
end

def recurse(arr, target, low, high)
  mid = (low+high)/2
  case target <=> arr[mid]
  when 0
    mid
  when -1
    recurse(arr, target, low, mid-1) unless low==mid
  when 1
    recurse(arr, target, mid+1, high) unless high==mid
  end
end

arr = [1, 2, 3, 5, 6]
bsearch(arr, 5) #=> 3 
bsearch arr, 1) #=> 0 
bsearch arr, 4) #=> nil 
bsearch arr, 0) #=> nil 

复杂的递归方法可能很难调试。可以插入puts语句,但结果可能会造成混淆,因为尚不清楚正在调用该方法的哪个嵌套实例。这是一种适用于此问题的技术,可对那些调试工作有所帮助。

INDENT = 4

def indent
  @offset += INDENT
  puts
end

def undent
  @offset -= INDENT
  puts
end

def pu(str)
  puts ' '*@offset + str
end

def bsearch(arr, target)
  @offset = 0
  pu "passed to bsearch: arr=#{arr}, target=#{target}"
  puts
  return nil if target < arr.first || target > arr.last
  recurse(arr, target, 0, arr.size-1)
end

def recurse(arr, target, low, high)
  pu "passed to recurse: low=#{low}, high=#{high}"
  mid = (low+high)/2
  pu "mid=#{mid}"
  case target <=> arr[mid]
  when 0
    pu "target==arr[mid] so return mid=#{mid}"
    rv = mid
  when -1
    pu "target < arr[mid]"
    if low==mid
      rv = nil
      pu "low==mid so return nil"
    else
      pu "calling recurse(arr, target, #{low}, #{mid-1})"
      indent   
      rv = recurse(arr, target, low, mid-1)
      pu "recurse(arr, target, #{low}, #{mid-1}) returned #{rv}"
    end
  when 1
    pu "target > arr[mid]"
    if high==mid
      rv = nil
      pu "high==mid so return nil"
    else
      pu "calling recurse(arr, target, #{mid+1}, #{high})"
      indent   
      rv = recurse(arr, target, mid+1, high)
      pu "recurse(arr, target, #{mid+1}, #{high}) returned #{rv}"
    end
  end
  pu "returning #{rv.nil? ? "nil" : rv}"
  undent
  rv
end

bsearch [1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13], 2 

打印以下内容。

passed to bsearch: arr=[1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13]
                   target=2

passed to recurse: low=0, high=11
mid=5
target < arr[mid]
calling recurse(arr, target, 0, 4)

    passed to recurse: low=0, high=4
    mid=2
    target < arr[mid]
    calling recurse(arr, target, 0, 1)

        passed to recurse: low=0, high=1
        mid=0
        target > arr[mid]
        calling recurse(arr, target, 1, 1)

            passed to recurse: low=1, high=1
            mid=1
            target==arr[mid] so return mid=1
            returning 1

        recurse(arr, target, 1, 1) returned 1
        returning 1

    recurse(arr, target, 0, 1) returned 1
    returning 1

recurse(arr, target, 0, 4) returned 1
returning 1

答案 1 :(得分:1)

每次递归到搜索数组的右半部分时,相对于原始数组的起始索引都会偏移middle_index + 1。因此,只需将偏移量添加到结果中即可!您只需要在方法中更改一行:

bsearch(arr[middle_index+1..-1], target)

成为

bsearch(arr[middle_index+1..-1], target) + middle_index + 1
#                                       ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

注意!您最初的方法是尾递归。这个不是,因为尾调用是+,而不是bsearch。 [Ruby没有优化尾部递归或尾部调用,因此这无关紧要,但是例如Scala优化了尾部递归,ECMAScript甚至优化了所有尾部调用,因此在那些语言中,我现在已经将使用O(1)内存的完全安全的方法转换为使用O(log n)内存的方法。]

这是因为我们必须将状态保存在某个地方,并且当我们进行递归编程时,我们通常将状态保存在堆栈中。 (这种递归编程方式通常用于不具有可变数据结构的纯功能语言,因此堆栈是您唯一可以存储状态的位置。)

在这种情况下,我将状态存储为对+的方法调用的堆栈,这些调用在实际搜索完成后执行。但是,堆栈上存储了两件事:指令指针和参数。

因此,将非尾递归方法转换为尾递归方法的标准方法是将使用方法调用累积的值移动到参数中,并将其传递给每个后续递归调用。

这需要我们修改方法的签名并添加其他参数:

def bsearch(arr, target, offset)
#                      ↑↑↑↑↑↑↑↑
  middle_index = arr.length / 2

  return middle_index if arr[middle_index] == target
  return nil if arr.length == 1

  if target > arr[middle_index]
    bsearch(arr[middle_index+1..-1], target, offset)
    #                                      ↑↑↑↑↑↑↑↑
  elsif target < arr[middle_index]
    bsearch(arr[0...middle_index], target, offset)
    #                                    ↑↑↑↑↑↑↑↑
  end
end

bsearch([1, 3, 4, 5, 9], 5, nil)

目前,我们实际上还没有做任何事情,只是在方法定义中添加了一个新参数,然后我们当然还需要在每个方法调用中添加一个参数。但是我们还没有做任何事情。我们实际上需要对该参数执行任何操作。我们或多或少地做着与以前相同的事情:

def bsearch(arr, target, offset)
  middle_index = arr.length / 2

  return offset + middle_index if arr[middle_index] == target
  #      ↑↑↑↑↑↑↑↑↑
  return nil if arr.length == 1

  if target > arr[middle_index]
    bsearch(arr[middle_index+1..-1], target, offset + middle_index + 1)
    #                                              ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
  elsif target < arr[middle_index]
    bsearch(arr[0...middle_index], target, offset)
  end
end

bsearch([1, 3, 4, 5, 9], 5, 0)
#                           ↑

当我们递归到搜索数组的右半部分时,我们需要确保我们“修改”(即传递新值)参数,我们需要确保在最终获得结果后实际添加累加值找到了该值,我们需要确保使用正确的值对其进行初始化。

但是,这有点丑陋,因为我们修改了方法的签名,现在调用者需要确保始终将0作为最后一个参数传递。这是糟糕的API设计。

我们可以通过将offset设置为默认参数为0的可选位置参数来解决此问题:

def bsearch(arr, target, offset=0)
#                              ↑↑

然后,我们不再需要在呼叫站点传递0。但是,这仍然很丑陋,因为它仍然会修改签名,例如有人可能不小心通过了42。基本上,我们现在向外部泄漏了一个私有的内部实现细节,即使我们的方法进行尾递归的累加器值。没人在乎我们是使用尾递归还是循环来实现我们的方法,还是通过将信鸽派遣到中国并让血奴中的孩子奴隶手工找到来实现。 (嗯,那将是非法,不道德和可怕的,但这不应成为我们方法签名的一部分。)

大多数支持正确的尾部调用或正确的尾部递归的语言也支持嵌套或局部子例程,因此用于隐藏此类实现细节的标准模式是具有一个包装器方法,该方法不执行任何操作,而只调用执行该操作的嵌套方法。实际工作。通常,此方法以带有后缀的外部方法命名,即在Haskell中,通常有名为foo的{​​{1}}的辅助功能({foo prime”),在Scala中,它是foo'。有时,它简称为fooRecgo

例如在Scala中,我们将像这样定义我们的方法:

doit

或在ECMAScript中这样:

def bsearch[A : Ordering](arr: IndexedSeq[A])(target: A) = {
  def bsearchRec(arr: IndexedSeq[A], target: A, accumulator: Long = 0) = {
    ??? // the code
  }

  bsearchRec(arr, target)
}

不幸的是,Ruby没有这样的嵌套子例程。我们的替代方法是私有方法和lambda:

function bsearch(arr, target) {
  function bsearchRec(arr, target, accumulator = 0) {
    // the code
  }

  return bsearchRec(arr, target);
}

def bsearch(arr, target) bsearch_rec(arr, target) end

private def bsearch_rec(arr, target, offset=0)
  middle_index = arr.length / 2

  return offset + middle_index if arr[middle_index] == target
  return nil if arr.length == 1

  if target > arr[middle_index]
    bsearch_rec(arr[middle_index+1..-1], target, offset + middle_index + 1)
  elsif target < arr[middle_index]
    bsearch_rec(arr[0...middle_index], target, offset)
  end
end

bsearch([1, 3, 4, 5, 9], 5)

但是,这将在每次调用时创建一个新的lambda,因此我们可以将该lambda从方法中拉出到局部变量中,但是随后我们需要将方法本身变成一个块,以便可以覆盖该变量:

def bsearch(arr, target)
  bsearch_rec = nil

  bsearch_rec = ->(arr, target, offset=0) {
    middle_index = arr.length / 2

    return offset + middle_index if arr[middle_index] == target
    return nil if arr.length == 1

    if target > arr[middle_index]
      bsearch_rec.(arr[middle_index+1..-1], target, offset + middle_index + 1)
    elsif target < arr[middle_index]
      bsearch_rec.(arr[0...middle_index], target, offset)
    end
  }

  bsearch_rec.(arr, target)
end

bsearch([1, 3, 4, 5, 9], 5)

答案 2 :(得分:0)

理解这里的目标是使用递归,我建议按照以下方式进行操作。

您将希望通过递归来跟踪low_index和high_index(元素数),以便每次调用该方法时,您都在索引范围而不是原始列表的子集中寻找值。

# Array, Target, first index (0), number of elements in the array
def bsearch(arr, target, low, high)
    middle_index = (low + high) / 2

    if target > arr[middle_index]
        bsearch(arr, target, middle_index, high)
    elsif target < arr[middle_index]
        bsearch(arr, target, low, middle_index)
    elsif arr[middle_index] == target
      middle_index
    end
end


puts bsearch([1, 2, 3], 1, 0, 3)
#=> 0

puts bsearch([2, 3, 4, 5], 3, 0, 4)
#=> 1

puts bsearch([2, 4, 6, 8, 10], 6, 0, 5)
#=> 2

puts bsearch([1, 3, 4, 5, 9], 5, 0, 5)
#=> 3

puts bsearch([1, 2, 3, 4, 5, 6], 6, 0, 6)
#=> 5

如果列表中不存在元素,则不会考虑这一点,但是,这是递归路径的解决方案。

答案 3 :(得分:0)

我建议对代码进行一些调整以使其正常运行。 我看到了这些主要问题:

  • 当目标位于正确的位置时,您需要跟踪数组的放置部分以获取正确的索引(将idx添加为参数);
  • 检查并返回右侧部分的第一个元素,碰巧目标在那儿,但是您跳过了它;
  • 当索引超出边界时需要返回;

所以,这是代码。我留下了评论和调试打印

def bsearch(arr, target, idx = 0)
  middle_index = arr.length / 2

  # debug print
  p "#{arr} - left: #{arr[0...middle_index]} - right: #{arr[middle_index + 1..-1]} - middle_element: #{arr[middle_index]} - middle_index: #{middle_index}"

  return middle_index + idx if arr[middle_index] == target

  # check also the right position, comment out the line below to see how the debug print changes
  return middle_index + idx + 1 if arr[middle_index + 1] == target

  # || !arr[middle_index] to exit if out of boundaries
  return nil if arr.length == 1 || !arr[middle_index] 

  if target > arr[middle_index]
    # add middle_index + 1 to idx to keep track of the dropped part of the array
    bsearch(arr[middle_index + 1..-1], target, idx += middle_index + 1 )
  else  # target < arr[middle_index]
    bsearch(arr[0...middle_index], target)
  end
end

p bsearch([0,1,2,3,4,5,6], 4) # => 4


对于优化版本,您可能想研究Array#index的来源:)