Я относительно новичок в Python; Я написал следующий код, чтобы найти ближайший символ в строке относительно индекса в queries
, и я хочу найти способ оптимизации кода:
Пример:
Строка ввода: s = 'adarshravi'
И queries = [2, 4]
(это индексы символов, дубликаты которых должны быть найдены, а вывод должен иметь индекс ближайшего дубликата, а если нет дубликатов символов, то для этого будет [CN00 ])
Вывод для вышеуказанных запросов будет: [0, -1]
Объяснение для вывода:
Для индекса 2 символ в строке есть a
, a's
в строке два других a's
, один - по 0
а другой - по индексу 7
, поэтому ближайший между ними - тот, который находится на 0- 0'th
позиции, а символ при 4th
индексе s
который не повторяется в строке, поэтому -1
def closest(s, queries):
s = s.lower()
listIdx = []
for i in queries:
foundidx = []
srchChr = s[i]
for j in range(0, len(s)):
if s[j] == srchChr:
foundidx.append(j)
if len(foundidx) < 2:
listIdx.append(-1)
else:
lastIdx = -1
dist = 0
foundidx.remove(i)
for fnditem in foundidx:
if dist == 0:
lastIdx = fnditem
dist = abs(fnditem - i)
else:
if abs(fnditem - i) < dist:
lastIdx = fnditem
dist = abs(fnditem - i)
listIdx.append(lastIdx)
return listIdx
Мы можем построить список индексов, таких как:
from itertools import zip_longest
def ranges(k, n):
for t in zip_longest(range(k-1, -1, -1), range(k+1, n)):
yield from filter(lambda x: x is not None, t)
это, таким образом, генерирует такие индексы, как:
>>> list(ranges(3, 10))
[2, 4, 1, 5, 0, 6, 7, 8, 9]
Мы можем использовать приведенное выше, чтобы проверить ближайший символ:
def close(text, idx):
ci = text[idx]
return next(filter(lambda i: ci == text[i], ranges(idx, len(text))), -1)
Это дает:
>>> close('adarshravi', 0)
2
>>> close('adarshravi', 1)
-1
>>> close('adarshravi', 2)
0
>>> close('adarshravi', 3)
6
>>> close('adarshravi', 4)
-1
closest
- это просто "отображение" функции close
над списком:
from functools import partial
def closest(text, indices):
return map(partial(close, text), indices)
например:
>>> list(closest('adarshravi', range(5)))
[2, -1, 0, 6, -1]
def closest_duplicates(s, queries):
result = []
for index in queries:
result.append(closest_duplicate(s, s[index], index))
return result
этот парень ищет отдельные предметы
следующий код запускает 2 индекса: один от начала до левого, а другой - вправо. нам не нужно запускать этот цикл больше, чем длина строки - 1. Когда они достигают конца или первого раза, символ найден, мы возвращаем индекс. если не найдено, мы возвращаем -1
def closest_duplicate(s, letter, index):
min_distance = -1
for i in range(1, len(s)):
left_i = index - i
right_i = index + i
if left_i == -1 and right_i == len(s):
break
if left_i > -1 and s[left_i] == letter :
min_distance = left_i
break
if right_i < len(s) and s[right_i] == letter:
min_distance = right_i
break
return min_distance
тесты ниже
if __name__ == '__main__':
s = 'adarshravi'
indexes = [2, 4]
result = closest_duplicates(s, indexes)
print(result)
batman = 'ilovebatmanandbatman'
indx = [1,2,5,6]
result = closest_duplicates(batman, indx)
print(result)
batman = 'iloveabatmanbatmanandbatman'
indx = [7]
result = closest_duplicates(batman, indx)
print(result)
Это работает, создавая кортежи с индексами, а затем сравнивая значение абзаца разности двух индексов, если символ в кортеже одинаковый. При создании s_lst
кортежи из queries
исключаются, чтобы избежать совпадения с самим собой
s = 'adarshravi'
queries = [2, 4]
queries = [(i, s[i]) for i in queries]
s_lst = [(i, v) for i, v in enumerate(s) if any(v in x for x in queries)]
s_lst = [i for i in s_lst if not any(i[0] in x for x in queries)]
res = []
for i in queries:
if not any(i[1] in x for x in s_lst):
res.append(-1)
else:
close = None
for j in s_lst:
if j[1] == i[1] and close == None:
close = j
elif abs(j[0] - i[0]) < abs(close[0] - i[0]):
close = j
res.append(close[0])
print(res)
# [0, -1]
Это получает индексы всех персонажей, прежде чем мы начнем поиск ближайших матчей. Затем мы можем избежать избыточных вычислений, а также выполнять простые проверки в случае, когда символ встречается только один или два раза:
from collections import defaultdict
my_str = 'shroijsfrondhslmbs'
query = [4, 2, 11]
def closest_matches(in_str, query):
closest = []
character_positions = defaultdict(list)
valid_chars = {in_str[idx] for idx in query}
for i, character in enumerate(in_str):
if character not in valid_chars:
continue
character_positions[character].append(i)
for idx in query:
char = in_str[idx]
if len(character_positions[char]) is 1:
closest.append(-1)
continue
elif len(character_positions[char]) is 2:
closest.append(next(idx_i for idx_i in character_positions[char] if idx_i is not idx))
continue
shortest_dist = min(abs(idx_i - idx) for idx_i in character_positions[char] if idx_i is not idx)
closest_match = next(idx_i for idx_i in character_positions[char] if abs(idx_i - idx) == shortest_dist)
closest.append(closest_match)
return closest
closest_matches(my_str, query)
Выход: [-1, 8, -1]
s = 'adarshravi'
queries = [2, 4]
closest_matches(s, queries)
Выход: [0, -1]
Некоторые тайминги:
%timeit closest_matches(my_str, query)
Результаты: 8.98 µs ± 30.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
По сравнению с Виллемом:
%timeit list(closest(my_str, query))
Результаты: 55.8 µs ± 1.21 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
По сравнению с вашим оригинальным ответом:
%timeit closest(my_str, query)
Результаты: 11.4 µs ± 352 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
Так что вы уже очень хорошо себя чувствуете!
Чрезвычайно вероятно, что существует более оптимальное решение этой проблемы, которое я использую ниже, но я хотел показать, как я буду подходить к оптимизации этого кода, если бы мне назначили эту задачу. Кроме того, я не выполнил какую-либо часть кода, чтобы вы могли найти синтаксические ошибки.
================================================== ==========================
Предположим, что len(s) == n
и len(queries) == m
.
Ваш текущий код выполняет следующие действия:
For each query, q:
1. find the character of the query, c
2. find the indices of other characters in the string that match c
3. find the closest index to the original index with the same character as the original index
Шаги 1-3 выполняются m
раз, потому что есть m
запросов. И шаги 2 и 3 должны проходить по всей строке s
(в худшем случае ваша строка s
состоит из одного и того же символа), поэтому она выполняет n
шагов.
Таким образом, вы грубо выполняете 2n + 1
шаг для каждого запроса, поэтому в целом вы выполняете примерно (2n + 1) * m
шагов. Это (почти) то, что называется сложностью выполнения вашего алгоритма. В обозначениях с большими выводами сложность будет: O(n*m)
.
Позволяет извлечь шаги 2 и 3 в свои собственные функции:
def findIdxListByPos(s, i):
foundidx = []
srchChr = s[i]
for j in range(0, len(s)):
if s[j] == srchChr:
foundidx.append(j)
return foundIdx
def findClosestIndex(foundidx, i):
# this is not needed because if the character appeared only once,
# foundidx will be empty and the "for fnditem in foundidx" will not
# do anything, so you can remove it
if len(foundidx) < 2:
return -1
lastIdx = -1
dist = 0
foundidx.remove(i)
for fnditem in foundidx:
if dist == 0:
lastIdx = fnditem
dist = abs(fnditem - i)
else:
if abs(fnditem - i) < dist:
lastIdx = fnditem
dist = abs(fnditem - i)
return lastIdx
def closest(s, queries):
s = s.lower()
listIdx = []
for i in queries:
foundidx = findIdxListByPos(s, i)
lastIdx = findClosestIndex(foundidx, i)
listIdx.append(lastIdx)
return listIdx
Вы можете видеть, что в findIdxListByPos
вы всегда смотрите на каждую позицию в строке.
Теперь, позволяет сказать, что у вас есть случай, когда queries = [1, 1]
, то ваш подсчитывают два раза тот же foundidx
и тот же lastIdx
. Мы можем сохранить эти расчеты и повторно использовать их. То есть, вы сохраняете ваши foundidx
и lastIdx
внутри других переменных, которые не теряются после каждого запроса. Вы можете сделать это в словаре с символом запроса в качестве ключа. Если вы уже рассчитали этот ключ, вы не рассчитываете снова, просто повторно его используете.
Ваш код будет выглядеть так:
def findIdxListByPos(s, i):
foundidx = []
srchChr = s[i]
for j in range(0, len(s)):
if s[j] == srchChr:
foundidx.append(j)
return foundIdx
def findClosestIndex(foundidx, i):
lastIdx = -1
dist = 0
foundidx.remove(i)
for fnditem in foundidx:
if dist == 0:
lastIdx = fnditem
dist = abs(fnditem - i)
else:
if abs(fnditem - i) < dist:
lastIdx = fnditem
dist = abs(fnditem - i)
return lastIdx
def calculateQueryResult(s, i, allFoundIdx):
srchChr = s[i]
if srchChr not in allFoundIdx:
allFoundIdx[srchChr] = findIdxListByPos(s, i)
foundidx = allFoundIdx[srchChr]
return findClosestIndex(foundidx, i)
def closest(s, queries):
s = s.lower()
listIdx = []
allFoundIdx = {}
queriesResults = {}
for i in queries:
if i not in queriesResults:
queriesResults[i] = calculateQueryResult(s, i, allFoundIdx)
listIdx.append(queriesResults[i])
return listIdx
Это изменение увеличивает память, используемую вашим алгоритмом, и немного изменяет ее сложность во время выполнения.
Теперь, в худшем случае, у вас нет дубликатов в ваших запросах. Что происходит, когда у вас нет повторяющихся запросов? У вас есть запрос для каждого элемента в s
и все элементы в s
различны!
queries = [0,1,2,...,n]
Так len(queries) == n
, поэтому n == m
тогда ваш алгоритм теперь имеет сложность O(n*n) = O(n^2)
Теперь вы можете видеть, что в этом худшем случае ваш словарь allFoundIdx
будет содержать все символы со всеми позициями в строке. Таким образом, разумная память эквивалентна вычислению этого словаря upfront для всех значений в строке. Вычисление всех upfront не улучшает сложность выполнения, но это не делает его хуже.
def findClosestIndex(foundidx, i):
lastIdx = -1
dist = 0
foundidx.remove(i)
for fnditem in foundidx:
if dist == 0:
lastIdx = fnditem
dist = abs(fnditem - i)
else:
if abs(fnditem - i) < dist:
lastIdx = fnditem
dist = abs(fnditem - i)
return lastIdx
def calculateAllFoundIdx(s):
allFoundIdx = {}
for i in range(0, len(s)):
srchChr = s[i]
# you should read about the get method of dictionaries. This will
# return an empty list if there is no value for the key srchChr in the
# dictionary
allFoundIdx[srchChr] = allFoundIdx.get(srchChr, []).append(i)
return allFoundIdx
def closest(s, queries):
s = s.lower()
listIdx = []
queriesResults = {}
# this has complexity O(n)
allFoundIdx = calculateAllFoundIdx(s)
# this still has complexity O(n^2) because findClosestIndex still has O(n)
# the for loop executes it n times
for i in queries:
if i not in queriesResults:
srchChr = s[i]
foundidx = allFoundIdx[srchChr]
queriesResults[i] = findClosestIndex(foundidx, i)
listIdx.append(queriesResults[i])
return listIdx
Этот алгоритм все еще O(n^2)
но теперь вам просто нужно оптимизировать функцию findClosestIndex
, так как нет возможности не перебирать все запросы.
Таким образом, в findClosestIndex
вы получаете в качестве параметров список чисел (позиции какого-либо символа в исходной строке), который упорядочивается поэтапно (из-за способа его создания), а другой номер которого вы хотите найти ближайшим ( это число будет включено в список).
Самое близкое число (потому что список упорядочен) должен быть предыдущим или следующим в списке. Любое другое число "дальше", что эти два.
Поэтому в основном вы хотите найти индекс этого числа в списке, а затем предыдущий и следующий элементы в списке, а также сравнить их расстояния и вернуть наименьшее количество.
Чтобы найти номер в упорядоченном списке, вы используете двоичный поиск, и вам просто нужно быть осторожным с индексами, чтобы получить окончательный результат:
def binSearch(foundidx, idx):
hi = len(foundidx) - 1
lo = 0
while lo <= hi:
m = (hi + lo) / 2
if foundidx[m] < idx:
lo = m + 1
elif found[m] > idx:
hi = m - 1
else:
return m
# should never get here as we are sure the idx is in foundidx
return -1
def findClosestIndex(foundidx, idx):
if len(foundidx) == 1:
return -1
pos = binSearch(foundidx, idx)
if pos == 0:
return foundidx[pos + 1]
if pos == len(foundidx) - 1:
return foundidx[pos - 1]
prevDist = abs(foundidx[pos - 1] - idx)
postDist = abs(foundidx[pos + 1] - idx)
if prevDist <= postDist:
return pos - 1
return pos + 1
def calculateAllFoundIdx(s):
allFoundIdx = {}
for i in range(0, len(s)):
srchChr = s[i]
# you should read about the get method of dictionaries. This will
# return an empty array if there is no value for the key srchChr in the
# dictionary
allFoundIdx[srchChr] = allFoundIdx.get(srchChr, []).append(i)
return allFoundIdx
def closest(s, queries):
s = s.lower()
listIdx = []
queriesResults = {}
# this has complexity O(n)
allFoundIdx = calculateAllFoundIdx(s)
# this has now complexity O(n*log(n)) because findClosestIndex now has O(log(n))
for i in queries:
if i not in queriesResults:
srchChr = s[i]
foundidx = allFoundIdx[srchChr]
queriesResults[i] = findClosestIndex(foundidx, i)
listIdx.append(queriesResults[i])
return listIdx
Теперь findClosestIndex
имеет сложность O(log(n))
, поэтому closest
теперь имеет сложность O(n*log(n))
.
Хуже всего, когда все элементы из s
одинаковы, а queries = [0, 1,..., len(s) - 1]
s = 'adarshravi'
result = list()
indexes = [2, 4]
for index in indexes:
c = s[index]
back = index - 1
forward = index + 1
r = -1
while (back >= 0 or forward < len(s)):
if back >= 0 and c == s[back]:
r = back
break
if forward < len(s) and c == s[forward]:
r = forward
break
back -= 1
forward += 1
result.append(r)
print result