Каждый раз, когда вы возвращаетесь в правую половину поискового массива, ваш начальный индекс относительно исходного массива смещается на 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) memory.]
Это потому, что мы должны сохранять наше состояние где-то , и когда мы программируем рекурсивно, мы обычно сохраняем наше состояние настек.(Этот стиль рекурсивного программирования типичен для чисто функциональных языков, которые не имеют изменяемых структур данных, и поэтому стек - это единственное место, где вы можете хранить состояние.)
В этом случаеЯ сохранил состояние в виде стека вызовов методов для +
, которые выполняются после завершения фактического поиска.Однако в стеке хранятся две вещи: указатели инструкций и аргументы.
Итак, стандартный способ превратить нерекурсивный метод в хвостовой рекурсивный - это переместить значение, которое мынакапливать вызовы методов в аргументе и передавать его каждому последующему рекурсивному вызову.
Для этого необходимо изменить сигнатуру метода и добавить дополнительный параметр:
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
с именем foo'
("foo prime"), в Scala это fooRec
.Иногда это просто называется go
или doit
.
Например, в Scala мы определяем наш метод следующим образом:
def bsearch[A : Ordering](arr: IndexedSeq[A])(target: A) = {
def bsearchRec(arr: IndexedSeq[A], target: A, accumulator: Long = 0) = {
??? // the code
}
bsearchRec(arr, target)
}
или в ECMAScript следующим образом:
function bsearch(arr, target) {
function bsearchRec(arr, target, accumulator = 0) {
// the code
}
return bsearchRec(arr, target);
}
К сожалению, в Ruby нет таких подпрограмм.Нашими альтернативами являются закрытые методы и лямбды:
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)
Или
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)
Это создаст новую лямбду при каждом вызове, поэтому мы можем извлечь эту лямбду из метода влокальная переменная, но затем нам нужно превратить сам метод в блок, чтобы он мог закрывать эту переменную:
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
}
define_method(:bsearch) {|arr, target|
bsearch_rec.(arr, target)
}
bsearch([1, 3, 4, 5, 9], 5)