Подтвердить что ты не робот

Фильтровать список строк, игнорируя подстроки других элементов

Как я могу фильтровать список, содержащий строки и подстроки, чтобы возвращать только самые длинные строки. (Если какой-либо элемент в списке является подстрокой другого, верните только более длинную строку.)

У меня есть эта функция. Есть ли более быстрый способ?

def filterSublist(lst):
    uniq = lst
    for elem in lst:
        uniq = [x for x in uniq if (x == elem) or (x not in elem)]
    return uniq

lst = ["a", "abc", "b", "d", "xy", "xyz"]
print filterSublist(lst)

> ['abc', 'd', 'xyz']
> Function time: 0.000011
4b9b3361

Ответ 1

Простым квадратичным решением будет следующее:

res = []
n = len(lst)
for i in xrange(n):
    if not any(i != j and lst[i] in lst[j] for j in xrange(n)):
        res.append(lst[i])

Но мы можем сделать гораздо лучше:

Пусть $ - символ, который не отображается ни в одной из ваших строк, и имеет меньшее значение, чем все ваши фактические символы.

Пусть S будет конкатенацией всех ваших строк, с $ между ними. В вашем примере S = a$abc$b$d$xy$xyz.

Вы можете построить массив суффикса массива S в линейном времени. Вы также можете использовать гораздо более простой алгоритм построения O (n log ^ 2 n), который я описал в другом ответе.

Теперь для каждой строки в lst проверьте, выполняется ли она в массиве суффикса ровно один раз. Вы можете выполнить два бинарных поиска, чтобы найти местоположения подстроки, они образуют непрерывный диапазон в массиве суффикса. Если строка встречается более одного раза, вы удаляете ее.

С запрограммированной информацией LCP это можно сделать и в линейном времени.

Пример O (n log ^ 2 n), адаптированный из моего массива суффиксов:

def findFirst(lo, hi, pred):
  """ Find the first i in range(lo, hi) with pred(i) == True.
  Requires pred to be a monotone. If there is no such i, return hi. """
  while lo < hi:
    mid = (lo + hi) // 2
    if pred(mid): hi = mid;
    else: lo = mid + 1
  return lo

# uses the algorithm described in /questions/293090/rank-the-suffixes-of-a-list/1445922#1445922
class SuffixArray(object):
  def __init__(self, s):
    """ build the suffix array of s in O(n log^2 n) where n = len(s). """
    n = len(s)
    log2 = 0
    while (1<<log2) < n:
      log2 += 1
    rank = [[0]*n for _ in xrange(log2)]
    for i in xrange(n):
      rank[0][i] = s[i]
    L = [0]*n
    for step in xrange(1, log2):
      length = 1 << step
      for i in xrange(n):
        L[i] = (rank[step - 1][i],
                rank[step - 1][i + length // 2] if i + length // 2 < n else -1,
                i)
      L.sort()
      for i in xrange(n):
        rank[step][L[i][2]] = \
          rank[step][L[i - 1][2]] if i > 0 and L[i][:2] == L[i-1][:2] else i
    self.log2 = log2
    self.rank = rank
    self.sa = [l[2] for l in L]
    self.s = s
    self.rev = [0]*n
    for i, j in enumerate(self.sa):
      self.rev[j] = i

  def lcp(self, x, y):
    """ compute the longest common prefix of s[x:] and s[y:] in O(log n). """
    n = len(self.s)
    if x == y:
      return n - x
    ret = 0
    for k in xrange(self.log2 - 1, -1, -1):
      if x >= n or y >= n:
        break
      if self.rank[k][x] == self.rank[k][y]:
        x += 1<<k
        y += 1<<k
        ret += 1<<k
    return ret

  def compareSubstrings(self, x, lx, y, ly):
    """ compare substrings s[x:x+lx] and s[y:y+yl] in O(log n). """
    l = min((self.lcp(x, y), lx, ly))
    if l == lx == ly: return 0
    if l == lx: return -1
    if l == ly: return 1
    return cmp(self.s[x + l], self.s[y + l])

  def count(self, x, l):
    """ count occurences of substring s[x:x+l] in O(log n). """
    n = len(self.s)
    cs = self.compareSubstrings
    lo = findFirst(0, n, lambda i: cs(self.sa[i], min(l, n - self.sa[i]), x, l) >= 0)
    hi = findFirst(0, n, lambda i: cs(self.sa[i], min(l, n - self.sa[i]), x, l) > 0)
    return hi - lo

  def debug(self):
    """ print the suffix array for debugging purposes. """
    for i, j in enumerate(self.sa):
      print str(i).ljust(4), self.s[j:], self.lcp(self.sa[i], self.sa[i-1]) if i >0 else "n/a"

def filterSublist(lst):
  splitter = "\x00"
  s = splitter.join(lst) + splitter
  sa = SuffixArray(s)
  res = []
  offset = 0
  for x in lst:
    if sa.count(offset, len(x)) == 1:
      res.append(x)
    offset += len(x) + 1
  return res

Однако накладные расходы интерпретации, вероятно, заставляют это быть медленнее, чем приближение O (n ^ 2), если S действительно велико (порядка 10 ^ 5 символов или более).

Ответ 2

Вы можете создать свою проблему в виде матрицы:

import numpy as np

lst = np.array(["a", "abc", "b", "d", "xy", "xyz"], object)
out = np.zeros((len(lst), len(lst)), dtype=int)
for i in range(len(lst)):
    for j in range(len(lst)):
        out[i,j] = lst[i] in lst[j]

откуда вы получаете out как:

array([[1, 1, 0, 0, 0, 0],
       [0, 1, 0, 0, 0, 0],
       [0, 1, 1, 0, 0, 0],
       [0, 0, 0, 1, 0, 0],
       [0, 0, 0, 0, 1, 1],
       [0, 0, 0, 0, 0, 1]])

тогда ответ будет индексом lst, где сумма òut вдоль столбцов 1 (строка только сама по себе):

lst[out.sum(axis=1)==1]
#array(['abc', 'd', 'xyz'], dtype=object)

EDIT: Вы можете сделать это гораздо эффективнее:

from numpy.lib.stride_tricks import as_strided
from string import find

size = len(lst)
a = np.char.array(lst)
a2 = np.char.array(as_strided(a, shape=(size, size),
                                 strides=(a.strides[0], 0)))

out = find(a2, a)
out[out==0] = 1
out[out==-1] = 0
print a[out.sum(axis=0)==1]
# chararray(['abc', 'd', 'xyz'], dtype='|S3')

a[out.sum(axis=0)==1]

Ответ 3

Имеет ли смысл порядок? Если нет,

a = ["a", "abc", "b", "d", "xy", "xyz"]

a.sort(key=len, reverse=True)
n = len(a)

for i in range(n - 1):
    if a[i]:
        for j in range(i + 1, n):
            if a[j] in a[i]:
                a[j] = ''


print filter(len, a)  # ['abc', 'xyz', 'd']

Не очень эффективный, но простой.

Ответ 4

O (n) решение:

import collections
def filterSublist1(words):
    longest = collections.defaultdict(str)
    for word in words:                                 # O(n)
        for i in range(1, len(word)+1):                # O(k)
            for j in range(len(word) - i + 1):         # O(k)
                subword = word[j:j+i]                  # O(1)
                if len(longest[subword]) < len(word):  # O(1)
                    longest[subword] = word            # O(1)

    return list(set(longest.values()))                 # O(n)
                                                       # Total: O(nk²)

Объяснение:

Чтобы понять временную сложность, я дал верхнюю оценку сложности в каждой строке в приведенном выше коде. Узкое место встречается в циклах for, где, поскольку они вложены, общая временная сложность будет O(nk²), где n - количество слов в списке, а k - длина среднее/длинное слово (например, в приведенном выше коде n = 6 и k = 3). Однако, считая, что слова не являются произвольно длинными строками, мы можем привязать k к некоторому небольшому значению - например, k=5, если мы рассмотрим среднюю длину слова в английском словаре. Поэтому, поскольку k ограничено значением, оно не включено в временную сложность, и мы получаем время выполнения O(n). Конечно, размер k добавит к постоянному коэффициенту, особенно если k не меньше n. Для английского словаря это означало бы, что когда n >> k² = 25 вы начнете видеть лучшие результаты, чем с другими алгоритмами (см. График ниже).

Алгоритм работает путем сопоставления каждого уникального подслова с самой длинной строкой, содержащей это подслово. Например, 'xyz' будет искать longest['x'], longest['y'], longest['z'], longest['xy'], longest['yz'] и longest['xyz'] и установить их равными 'xyz'. Когда это делается для каждого слова в списке, longest.keys() будет набором всех уникальных подслов всех слов, а longest.values() будет списком только слов, которые не являются подсловами других слов. Наконец, longest.values() может содержать дубликаты, поэтому они удаляются путем переноса в set.

Визуализация сложности времени

Ниже я протестировал алгоритм выше (вместе с вашим оригинальным алгоритмом), чтобы показать, что это решение действительно O(n) при использовании английских слов. Я тестировал это, используя timeit в список из 69000 английских слов. Я назвал этот алгоритм filterSublist1 и ваш исходный алгоритм filterSublist2.

enter image description here

График показан на оси логарифмического журнала, что означает, что временная сложность алгоритма для этого входного набора задается наклоном линий. Для filterSublist1 наклон равен ~ 1, что означает O(n1), а для filterSublist2 наклон равен ~ 2, что означает O(n2).

ПРИМЕЧАНИЕ. Мне не хватает filterSublist2() времени для 69000 слов, потому что мне не хотелось ждать.