Ближайший символ в строке с использованием Python

1

Я относительно новичок в 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
  • 0
    Ближе всего к чему? Индекс оригинальных символов? или ближе всего к 0? Кроме того, что твой вопрос здесь? Код не делает то, что вы ожидаете? Какие ошибки вы получаете?
  • 0
    @CapnJack Индекс оригинальных символов. Код работает нормально и не получает никаких ошибок, я просто хочу оптимизировать код. Мол, получай меньше времени на вывод!
Показать ещё 3 комментария
Теги:
python-3.x

6 ответов

3
Лучший ответ

Мы можем построить список индексов, таких как:

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]
2
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)
1

Это работает, создавая кортежи с индексами, а затем сравнивая значение абзаца разности двух индексов, если символ в кортеже одинаковый. При создании 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]
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)

Так что вы уже очень хорошо себя чувствуете!

0

Чрезвычайно вероятно, что существует более оптимальное решение этой проблемы, которое я использую ниже, но я хотел показать, как я буду подходить к оптимизации этого кода, если бы мне назначили эту задачу. Кроме того, я не выполнил какую-либо часть кода, чтобы вы могли найти синтаксические ошибки.

================================================== ==========================

Предположим, что 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]

-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
  • 0
    Вы пытались найти какую-нибудь другую строку?

Ещё вопросы

Сообщество Overcoder
Наверх
Меню