
Автор: Роман Дмитриевич

Автор: Роман Дмитриевич
Алгоритм поиска строки Бойера — Мура считается наиболее быстрым среди алгоритмов общего назначения, предназначенных для поиска подстроки в строке. Был разработан Робертом Бойером и Джеем Муром в 1977 году. Преимущество этого алгоритма в том, что ценой некоторого количества предварительных вычислений над шаблоном (но не над строкой, в которой ведётся поиск) шаблон сравнивается с исходным текстом не во всех позициях — часть проверок пропускаются как заведомо не дающие результата.
Мы с Вами уже рассмотрели 2 подхода поиска подстроки: в лоб и Кнута — Морриса — Пратта.
И хотя последний можно отнести к правильным, сегодня мы разберем подход, являющийся классикой в решении данной задачи.
Простота, является характеристикой правильного решения и тривиальность данного подхода это подтверждает.
Основная идея алгоритм — начать поиск не с начала, а с конца подстроки. Наткнувшись на несовпадение, мы просто смещаем подстроку до самого правого вхождения данного символа.
Пример можете посмотреть в картинке заголовка 🙂
Можно заметить, что, как и в случае Кнута-Морриса-Пратта, мы так же можем осуществить предкомпиляцию выражения.
Для этого удобно в словарь заносить пары ключ = числовое значение символа(ord), значение = порядковый номер в подстроке.
А вот и реализация:
def bmPredCompil(x):
d = {}
lenX = len(x)
for i in xrange(len(x)):
# сколько символов с правого края до этой буквы
d[ord(x[i])] = lenX - i
return d
Осталось осуществить сам поиск со сдвигом:
def boyerMurSearch(s, x):
d = bmPredCompil(x)
# k - проход по s
# j - проход по x
# i - место начала прохода по s
lenX = i = j = k = len(x)
while j > 0 and i<=len(s):
# совпали, двигаемся дальше (от конца к началу)
if s[k-1] == x[j-1]:
k -= 1
j -= 1
# иначе, продвигаемся по строке на d и начинаем с правого конца подстроки снова
else:
i += d[ord(s[i])]
j = lenX
k = i
if j <= 0:# нашли
return k
return None # не нашли
Вот собственно и все! 🙂
Реализация, как и сама идея, достаточно прозрачна.
Автор: Pavel Petropavlov
Когда то давно, мы уже рассматривали пример решения задачи рекурсивным образом, на примере задачи по размену денег. Пришло время вспомнить некоторые классические подходы и начать хотелось бы с задачи о путешествии шахматного коня.
Решение приведенное ниже относится к так называемым алгоритмам с возвратом.
Разделяй и властвуй -основная идея рекурсии. Т.е. мы сложную задачу делим на маленькие простые решаем их по отдельности в цепочке. В случае данной задачи, можно рассмотреть подзадачу, которая состоит в том, чтобы либо выполнить какой-либо очередной ход, либо обнаружить, что дальнейшие ходы невозможны.
Итак, каждый ход мы будем характеризовать 3-мя числами: его порядковым номером i и парой координат x, y. История ходов будет храниться в матрице списков, в соответствующей ячейке записываем порядковый номер хода. Соответственно, если в ячейке 0, значит туда мы еще не ходили.
Важно определиться с вычислением новой координаты. Можно заметить, что у коня есть 8 позиций-кандидатов (u, v) куда может прыгнуть конь.
Будем получать новые координаты прибавляя соответствующие смещения, хранящиеся в 2х списках dx, dy и отслеживать допустимость хода (находимся в пределах доски).
Характерной чертой данного алгоритма является то, что каждый шаг, выполняемый в попытке приблизиться к полному решению, запоминается таким образом, чтобы от него можно было позднее отказаться, если выяснится, что он не может привести к полному решению. Такое действие называется возвратом. А класс алгоритмов, как не трудно догадаться, алгоритмы с возвратом.
Очень советую читать именно алгоритм, тем более Питон в этом сильно помогает. Это даст куда больше толку. чем множество лишних слов!)
#Задача о коняшке
def knightsTour(x0, y0, done, size=5):
#создаем шахматную доску в виде 2го списка
h = [[0 for j in xrange(size)] for i in xrange(size)]
#начальная координата(1го хода)
h[x0][y0] = 1
#Возможные ходы
dx = [2, 1, -1, -2, -2, -1, 1, 2]
dy = [1, 2, 2, 1, -1, -2, -2, -1]
def CanBeDone(u, v, i):
h[u][v] = i
done = tryNextMove(u, v, i)
if not done:
h[u][v] = 0
return done
def tryNextMove(x,y, i):
#eos - показывает все ли варианты возможных 8ми ходов мы рассмотрели
#done - показывает удачна ли данная ветка решения
#k - порядковый номер рассмотренной попытки из 8 допустимых
env = {'done': False, 'eos': False, 'u': x, 'v': y, 'k': -1}
def next():
x = env['u']
y = env['v']
while env['k'] < 8:
env['k'] +=1;
if env['k'] < 8:
env['u'] = x + dx[env['k']]
env['v'] = y + dy[env['k']]
if (env['u'] >= 0 and env['u']=0 and env['v'] break
env['eos'] = (env['k']==8)
if i < size**2:#если доска не заполнена
next()
while not env['eos'] and not CanBeDone(env['u'], env['v'], i+1):
next()
done = not env['eos']
else:
done = True
return done
tryNextMove(x0, y0, 1)
print h
Привожу пример заполнения шахматной доски.
Автор: Pavel Petropavlov
Поиск информации — одно из основных использований компьютера. Одна из простейших задач поиска информации — поиск точно заданной подстроки в строке. Тем не менее, эта задача чрезвычайно важна — она применяется в текстовых редакторах, СУБД, поисковых машинах…
С поиском подстроки мы встречаемся постоянно. Всем любимый ctrl+f, это именно то, что нам нужно.
Часто ли вы задумывались, а как оно работает? Многие из Вас, наверняка, скажут, что если потребуется — сделаем.
Большинство, думаю, решат эту задачу в лоб и для наглядности я покажу, как это сделать.
Прямой поиск, или, как еще часто говорят, «просто взять и поискать» — это первое решение, которое приходит в голову неискушенному программисту. Суть проста: идти по проверяемой строке A и искать в ней вхождение первого символа искомой строки X. Когда находим, делаем гипотезу, что это и есть то самое искомое вхождение. Затем остается проверять по очереди все последующие символы шаблона на совпадение с соответствующими символами строки A. Если они все совпали — значит вот оно, прямо перед нами. Но вот если какой-то из символов не совпал, то ничего не остается, как признать нашу гипотезу неверной, что возвращает нас к символу, следующему за вхождением первого символа из X.
Многие люди ошибаются в этом пункте, считая, что не надо возвращаться назад, а можно продолжать обработку строки A с текущей позиции. Почему это не так, легко продемонстрировать на примере поиска X=«AAAB» в A=«AAAAB». Первая гипотеза нас приведет к четвертому символу A: «AAAAB», где мы обнаружим несоответствие. Если не откатиться назад, то вхождение мы так и не обнаружим, хотя оно есть.
Неправильные гипотезы неизбежны, а из-за таких откатываний назад, при плохом стечении обстоятельств, может оказаться, что мы каждый символ в A проверили около |X| раз. То есть вычислительная сложность сложность алгоритма O(|X||A|). Так поиск фразы в параграфе может и затянуться…
Справедливости ради следует отметить, что если строки невелики, то такой алгоритм может работать быстрее «правильных» алгоритмов за счет более предсказуемого с точки зрения процессора поведения.
А вот и реализация:
def stringSearch(s, x):
i=j=0
lengthS = len(s)# Длина строки в которой ищем
lengthX = len(x)# -||- которую ищем
# пока не достигли одного из концов
while i<=lengthS - lengthX and j>lengthx:
# если совпали буквы продвигаемся по обеим строкам
if li[i+j]==x[j]:
j+=1
# иначе двигаемся по строке(+1), начиная с 0 символа подстроки
else:
i+=1
j=0
# если дошли до конца подстроки - нашли, иначе - нет
return i if j==lengthX else None
Алгоритм прост и тривиален, а главное, как было замечено выше, зачастую неплохо работает, особенно, когда строка поиска не сильно велика.
Разобравшись с прямым подходм, давайте посмотрим, как решать эту задачу — правильно.
Алгоритм Кнута-Морриса-Пратта (КМП)
Алгоритм Бойера — Мура
Используемая литература:
Автор: Pavel Petropavlov
Алгоритм был разработан Кнутом (Knuth) и Праттом (Pratt) и независимо от них Моррисом (Morris) в 1977 г.
Он относится к «правильным» подходам решения поставленной задачи, в отличии от тривиального подхода, рассмотренного ранее.
Данный подход хоть и считается достаточно тривиальным, описания, которые нашел я, зачастую пестрят математическими основами и доказательствами, которые сбивают с сути. Так в книге, уважаемого Никлауса Вирта, приводится описание, которое я так и не одолел.
Однако я нашел пару статей, которые достаточно информативны, они приведены в ссылках и рекомендуемы для ознакомления.
Для понимания: Префикс строки A[..i] — это строка из i первых символов строки A. Суффикс строки A[j..] — это строка из |A|-j+1 последних символов. Подстроку из Aбудем обозначать как A[i..j], а A[i] — i-ый символ строки.
В статье http://habrahabr.ru/post/111449/ сказано:
Идея КМП-поиска – при каждом несовпадении двух символов текста и образа образ сдвигается на все пройденное расстояние, так как меньшие сдвиги не могут привести к полному совпадению.
Многие люди ошибаются в этом пункте, считая, что не надо возвращаться назад, а можно продолжать обработку строки A с текущей позиции. Почему это не так легко продемонстрировать на примере поиска X=«AAAB» в A=«AAAAB». Первая гипотеза нас приведет к четвертому символу A: «AAAAB», где мы обнаружим несоответствие. Если не откатиться назад, то вхождение мы так и не обнаружим, хотя оно есть.
Для реализации данного алгоритма, нам необходимо рассмотреть, так называемую, префикс функцию.
Префикс-функция — это массив чисел, вычисляющийся, как наибольшая длина суффикса, совпадающего с её префиксом. Как пример, берем каждый возможный префикс строки и смотрим самое длинное совпадение начала с концом префикса (не учитывая тривиальное совпадение самого с собой).
Вот пример для «ababcaba»:
| суффикс | префикс | p |
|---|---|---|
| a | a | 0 |
| ab | ab | 0 |
| aba | aba | 1 |
| abab | abab | 2 |
| ababc | ababc | 0 |
| ababca | ababca | 1 |
| ababcab | ababcab | 2 |
| ababcaba | ababcaba | 3 |
Итак, идея ясна? Ну тогда попробум написать префикс функцию в лоб:
def predkompil(x):
#первый символ всегда 0, поэтому заносим и пропускаем
d = {0:0}
for i in xrange(1,len(x)):
# проходоим от конца к началу
j = i
sdvig = 0
while j>0:
j -= 1
if x[j] == x[i-sdvig]:
sdvig += 1
else:
j += sdvig
sdvig = 0
d[i] = sdvig
return d
Вроде работает, но как то много сравнений и проходов. Попробуем оптимизировать.
Начать можно с замены строки j = i, на j = d[i-1]+1.
Что нам это дает? А то, что незачем просматривать каждый раз с конца и до начала. Можно заметить, что если мы на предыдущем шаге (i-1) нашли вхождение > 0 (d[i-1]), то и начинать стоит с данной позиции (d[i-1]).
Пример:
При i=4 ABAAB -> 2, тогда на i+1, т.е. i=5 смотрим не с j=i(5), а с 3! Т.к. либо счетчик вырастет и станет 3 в случае ABAABA, либо сбросится в 0 при любой другой букве.
И хотя мы делаем меньше сравнений(пропускаем середину слова), однако весь совпадающий префикс мы проходим, что не есть хорошо.
Это уже не плохо, однако, мы с Вами так и не научились использовать информацию, полученную на предыдущих и
В этом посте я писал почему работа с файлами и другими объектами, требующими гарантированного закрытия должна должна производиться через with. Однако кроме минуса в виде добавления в код лишнего уровеня вложенности with еще и решает только часть проблемы — если код обработки файла не локален (нужно возвращать дескриптор в вызывающий код или хранить неопределенное время) with не может помочь. И собственно никто вообще не может помочь — суровая реальность состоит в том, что python не гарантирует вызов деструктора объекта. Т.е. если вы работаете на CPython, и не создаете циклических ссылок, то за крайне редкими исключениями деструктор будет вызываться вовремя. Но если вы используете ironpython/jython/pypy то ситуация становится совсем печальна.
Для тех кто, вдруг, не в курсе — немного про С++, как пример удобной для программиста реализации деструкторов. С++ гарантирует вызов деструктора у объекта по его уничтожению чтобы не произошло (за исключением полного краха программы/пропадания питания/конца света/etc). Уничтожение наступает либо по выходу из блока в котором определена переменная, либо по удалению объекта с помощью оператора delete при выделении объекта в куче.
// C++
{
// open file
std::fstream fs(fname, std::ios_base::out);
process_file(fs);
} // file is closed before we get beyong this line, no matter what happened
Гарантированный вызов деструктора — великое программистское благо, позволяющее не заботится о освобождении некоторых ресурсов, не загромождать код всякими with/using/try-finally & Co, и даже для объектов в куче есть всякие unique_ptr. Но все это хорошо работает только в том случае, если объект имел некую локальную область жизни(впрочем unique_ptr/shared_ptr могут и больше). Если же объект выделяется из кучи в одном месте, а освобождается в другом то можно забыть сделать ему delete — получаем утечку памяти. Не смотря на множество способов значительно сократить вероятность такого исхода (например арены) полностью исключить его нельзя.
В качестве решения этой проблемы в современных языках используются сборщики мусора. Периодически они проходятся по памяти, и тем или иным способом удаляют неиспользуемые объекты. Все бы хорошо, но у сборщиков мусора возникают небольшие или большие проблемы с вызовами деструкторов у объектов. Во-первых все сборщики мусора бессильны перед циклическими ссылками. Если объект a ссылается на b, а b ссылается на a, то не понятно в каком порядке вызывать деструкторы. Сначала у a или сначала у b. Если сначала у a, то при вызове деструктора b его ссылка на a будет не валидна и наоборот. Отсутствие информации о смысле взаимосвязей между объектами не дает сборщику мусора вызвать деструкторы в корректном порядке. Копирующие сборщики мусора пошли еще дальше. Они вообще ничего не вызывают, оставляя программиста один на один с этой проблемой и гордо подписываясь «современный сборщик мусора».
Проблема, очевидно, состоит в том что оперативная память это не единственный ресурс требующий освобождения. Еще, как минимум, есть объекты OS, мютексы, транзакции БД, и много много другого. Часть из таких объектов будет через время прикрыта другим кодом (например транзакции БД — но в любом случае они будут создавать лишнюю нагрузку на базу все это время), но объекты OS останутся с процессом до его смерти. А ведь бОльшая часть таких объектов имею локальную область видимости и при гарантированном вызове деструктор был бы идеальным местом для их освобождения. Таким образом переходя от С++ к языкам со сборкой мусора но с недетерменированным вызовом деструкторов мы выигрываем в коде освобождения памяти, но проигрываем на других объектах. Теперь вместо delete «где-то там» вы должны написать dispose/using/with/try-finally прямо тут на каждый объект. Впрочем если файл, например, является не локальным, то и это не поможет. Для примера можно открыть Экслера и глянуть как в яве правильно работать с файлами. Страница ужасного кода ради одного маленького файлика. Не уверен, что такая сборка мусора того стоила.
Итак посмотрим как себя ведут с деструкторами разные варианты питона. В качестве примера возьмем вот такую программу:
import gc
import sys
class DestTest(object):
def __init__(self, val):
self.val = val
def __del__(self):
sys.stdout.write(str(self) + '.__del__()n')
def __str__(self):
return "DestTest({})".format(self.val)
def __repr__(self):
return "DestTest({})".format(self.val)
def simple_var():
d = DestTest("simple var")
def mdeleted_var():
d = DestTest("manyally deleted var")
del d
def simple_list():
a = [DestTest("simple list")]
def internal_exc():
try:
d = DestTest("exception_func")
raise IndexError()
except:
pass
def exception_func():
d = DestTest("exception_func")
raise IndexError()
def self_ref_list():
a = [DestTest("self-ref list")]
a.append(a)
def cycle_refs():
d1 = DestTest("cycle_ref_obj1")
d2 = DestTest("cycle_ref_obj2")
d3 = DestTest("free_obj")
d1.ref = d2
d2.ref = d1
d2.ref2 = d3
simple_var()
sys.stdout.write("after simple varn")
sys.stdout.write("n")
simple_list()
sys.stdout.write("after simple listn")
sys.stdout.write("n")
mdeleted_var()
sys.stdout.write("after manually deleted varn")
sys.stdout.write("n")
self_ref_list()
sys.stdout.write("after self ref listn")
gc.collect()
sys.stdout.write("after gc.collect()n")
sys.stdout.write("n")
internal_exc()
sys.stdout.write("after internal excn")
try:
exception_func()
except Exception as x:
pass
sys.stdout.write("after exception funcn")
gc.collect()
sys.stdout.write("after gc.collect()n")
try:
sys.exc_clear()
except AttributeError:
pass
else:
sys.stdout.write("after sys.exc_clear()n")
sys.stdout.write("n")
cycle_refs()
sys.stdout.write("after cycle refsn")
gc.collect()
sys.stdout.write("after gc.collect()n")
sys.stdout.write("n")
d = DestTest("module var")
import gc
import sys
class DestTest(object):
def __init__(self, val):
self.val = val
def __del__(self):
sys.stdout.write(str(self) + '.__del__()n')
def __str__(self):
return "DestTest({})".format(self.val)
def __repr__(self):
return "DestTest({})".format(self.val)
def simple_var():
d = DestTest("simple var")
def mdeleted_var():
d = DestTest("manyally deleted var")
del d
def simple_list():
a = [DestTest("simple list")]
def internal_exc():
try:
d = DestTest("exception_func")
raise IndexError()
except:
pass
def exception_func():
d = DestTest("exception_func")
raise IndexError()
def self_ref_list():
a = [DestTest("self-ref list")]
a.append(a)
def cycle_refs():
d1 = DestTest("cycle_ref_obj1")
d2 = DestTest("cycle_ref_obj2")
d3 = DestTest("free_obj")
d1.ref = d2
d2.ref = d1
d2.ref2 = d3
simple_var()
sys.stdout.write("after simple varn")
sys.stdout.write("n")
simple_list()
sys.stdout.write("after simple listn")
sys.stdout.write("n")
mdeleted_var()
sys.stdout.write("after manually deleted varn")
sys.stdout.write("n")
self_ref_list()
sys.stdout.write("after self ref listn")
gc.collect()
sys.stdout.write("after gc.collect()n")
sys.stdout.write("n")
internal_exc()
sys.stdout.write("after internal excn")
try:
exception_func()
except Exception as x:
pass
sys.stdout.write("after exception funcn")
gc.collect()
sys.stdout.write("after gc.collect()n")
try:
sys.exc_clear()
except AttributeError:
pass
else:
sys.stdout.write("after sys.exc_clear()n")
sys.stdout.write("n")
cycle_refs()
sys.stdout.write("after cycle refsn")
gc.collect()
sys.stdout.write("after gc.collect()n")
sys.stdout.write("n")
d = DestTest("module var")
В идеальном мире вызов деструктора у объектов класса DestTest во всех этих функциях должен происходить до выхода из соответствующей функции. Итак что получается:
python2.7
DestTest(simple var).__del__()
after simple var
DestTest(simple list).__del__()
after simple list
DestTest(manyally deleted var).__del__()
after manually deleted var
after self ref list
DestTest(self-ref list).__del__()
after gc.collect()
after exception func
after gc.collect()
DestTest(exception_func).__del__()
after sys.exc_clear()
after cycle refs
after gc.collect()
Exception AttributeError: "'NoneType' object has no attribute 'stdout'" in
ignored
Более-менее. Деструктор у циклических ссылок не был вызван, как и ожидалось. Для уничтожения объектов, связанных с исключением, дошедшем до уровня модуля приходится вызывать sys.clear_exc(), в остальных случаях с исключениями все ок. Интересно, что питон освободил sys.stdout раньше, чем переменную d, в итоге чего ее деструктор отработал не корректно (впрочем это поведение не всегда повторяется).
python3.3
DestTest(simple var).__del__()
after simple var
DestTest(simple list).__del__()
after simple list
DestTest(manyally deleted var).__del__()
after manually deleted var
after self ref list
DestTest(self-ref list).__del__()
after gc.collect()
DestTest(exception_func).__del__()
after exception func
after gc.collect()
after cycle refs
after gc.collect()
Exception AttributeError: "'NoneType' object has no attribute 'stdout'" in
ignore
Почти то-же самое, что и 2.7, только sys.exc_clear() больше не нужно.
ironpython2.7
after simple var
after simple list
after manually deleted var
after self ref list
DestTest(self-ref list).__del__()
DestTest(manyally deleted var).__del__()
DestTest(simple list).__del__()
DestTest(simple var).__del__()
after gc.collect()
after exception func
DestTest(exception_func).__del__()
after gc.collect()
after sys.exc_clear()
after cycle refs
DestTest(free_obj).__del__()
DestTest(cycle_ref_obj2).__del__()
DestTest(cycle_ref_obj1).__del__()
after gc.collect()
DestTest(module var).__del__()
Удаляет все, но только при сборке мусора, до те
х пор все объекты живут где-то в памяти.
И — встречаем победителя. Сама лаконичность или мир всем вашим деструкторам от нашей Java и копирующего сборщика мусора: jython2.7a2 — Java HotSpot(TM) Client VM (Oracle Corporation) on java1.7.0_07
after simple var
after simple list
after manually deleted var
after self ref list
after gc.collect()
after exception func
after gc.collect()
after sys.exc_clear()
after cycle refs
after gc.collect()
Правда в ява есть и другие варианты сборщика мусора, может там чуть по-лучше.
А вывод все тот-же. По возможности — используйте with. По невозможности — попробуйте поправить код, так что-бы можно было использовать with. Иначе — аккуратно вызывайте деструкторы руками
P.S. Как я люблю, когда мне предлагают делать какую-то рутинную работу в коде «аккуратно».
Ссылки:
koder-ua.blogspot.com/2012/10/python-with.html
en.wikipedia.org/wiki/Region-based_memory_management
Исходники этого и других постов со скриптами лежат тут — github.com/koder-ua. При использовании их, пожалуйста, ссылайтесь на koder-ua.blogspot.com.
Автор: konstantin danilov