С тех пор Сравнение веб -скребка Пост был так любим, вот еще одно сравнение. Задача состоит в том, чтобы реализовать алгоритм автозаполнения из Рыба раковина За исключением складывания дела:
Если то, что вы напечатали, соответствует началу любых имен файлов, завершено к тому или самому большому префиксу, общему для всех из них. Если нет ничего, что начинается с того, что вы набрали, перейдите к следующему правилу.
Если то, что вы напечатали, соответствует смежной части ровного имени файла, даже если оно не в начале, завершено до этого. Если нет совпадений, перейдите к следующему правилу.
Если именно одно имя файла содержит символы, которые вы набрали по порядку, завершите это.
(Как вы, наверное, догадались, если вы следите за мной, я был вдохновлен сделать это после моего поста на глубине-первая и ширина-первая автозаполнение .)
Я сгенерировал миллион тестов, используя сценарий Python, который я опубликую в конце. Он имеет функции для генерации набора с конкретными решениями (например, набор, в котором возможно частичное завершение из префикса, набор, в котором ничего не может быть завершено, набор, в котором можно завершить из смежного непревзойденного), поэтому гарантированно, что все Кодовые пути будут протестированы в несколько реалистичных количествах.
Вот моя реализация Python.
import json, datetime def autocomplete(typed, possibilities): """The basic breadth-first algorithm: add the biggest prefix common to all continuations.""" # Exclude things that don't match the typed prefix. matches = [p for p in possibilities if p.startswith(typed)] # str.startswith is slower than slicing, but more idiomatic, so it's a fairer comparison. # If there's nothing that starts with the given prefix, proceed to the next rule. if not matches: return autocomplete_contiguous(typed, possibilities) # Scan the common prefix 1 char at a time. i = len(typed) while True: char = None for s in matches: if len(s) <= i or char and s[i] != char: return s[:i] if not char: char = s[i] i += 1 def autocomplete_contiguous(typed, possibilities): """Fallen back to if nothing starts with typed. Checks for things that contain it.""" matches = [p for p in possibilities if typed in p] if len(matches) == 1: return matches[0] if not matches: return autocomplete_in_order(typed, possibilities) return typed def autocomplete_in_order(typed, possibilities): """Fallen back to if nothing contains the typed characters contiguously. Checks if anything has them in order.""" matches = [] for s in possibilities: lastmatch = 0 for char in typed: index = s.find(char, lastmatch) if index < lastmatch: break lastmatch = index + 1 else: # It's ambiguous. if matches: return typed matches.append(s) # There's less than 2 matches. if matches: return matches[0] return typed if __name__ == '__main__': with open('data') as f: sets = json.load(f) start = datetime.datetime.now() for s in sets: autocomplete(s['typed'], s['possibilities']) end = datetime.datetime.now() print(end - start)
Обратите внимание, что я не запускаю часы, пока JSON не будет проанализирован, потому что я сравниваю алгоритм автозаполнения.
Если я сбрасываю сброс основного блока и связанного с ним импорта, чтобы мы измеряем только реализацию автозаполнения, Python выходит на 30 строк. (Я использую cloc для подсчета строк кода, поэтому комментарии/Docstrings и пустые строки исключены.) Довольно впечатляет такой сложный алгоритм.
Запустив Python 3.7, потребовалось около 5,4 секунды, чтобы запустить все миллион случаев, и для этого использовалось 1,7 ГБ памяти. Честно говоря, я ожидал хуже.
Реализация GO (без параллелизма):
package main import ( "encoding/json" "fmt" "io/ioutil" "strings" "time" ) type Case struct { Typed string Possibilities []string } func main() { var rawData, err = ioutil.ReadFile("data") if err != nil { panic(err) } var data = make([]Case, 0, 1000000) err = json.Unmarshal(rawData, &data) if err != nil { panic(err) } var before = time.Now() for i := range data { autocomplete(data[i].Typed, data[i].Possibilities) } var after = time.Now() fmt.Printf("Took %f\n", float64(after.Sub(before))/float64(time.Second)) } func autocomplete(typed string, possibilities []string) string { var matches []string // Exclude things that don't match the typed prefix. for _, s := range possibilities { if strings.HasPrefix(s, typed) { matches = append(matches, s) } } if len(matches) == 0 { return autocompleteContiguous(typed, possibilities) } // Scan the common prefix 1 char at a time. for i := len(typed); true; i++ { var char byte = '~' // A dumb hack: this char doesn't appear in the dataset. for _, s := range matches { if len(s) <= i || char != '~' && s[i] != char { return s[:i] } if char == '~' { char = s[i] } } } // Go can't tell this is unreachable panic("Shouldn't happen") } func autocompleteContiguous(typed string, possibilities []string) string { var matches []string // Exclude things that don't match the typed prefix. for _, s := range possibilities { if strings.Contains(s, typed) { matches = append(matches, s) } } if len(matches) == 1 { return matches[0] } else if len(matches) == 0 { return autocompleteInOrder(typed, possibilities) } return typed } func autocompleteInOrder(typed string, possibilities []string) string { var matches []string // Exclude things that don't match the typed prefix. for _, s := range possibilities { var lastmatch = 0 for i, char := range typed { var index = strings.IndexRune(s[lastmatch:], char) if index == -1 { break } // The index will be offset by our slicing, so account for that. lastmatch += index + 1 // If we just found the last character, record this as a match. if i+1 == len(typed) { // There's already a match. It's ambiguous. if len(matches) > 0 { return typed } matches = append(matches, s) } } } // If we get here, there's less than 2 matches. if len(matches) == 1 { return matches[0] } return typed }
64 строки кода после удаления основной функции и определения структуры случая. (Я на самом деле собирался хранить случаи в файле данных в виде массивов JSON [Typed, возможностей], но у Go нет типов кортежей, поэтому он не мог легко проанализировать список неравномерных элементов, поэтому я. Отформатировал их как объекты JSON).
Конечно, если я не считаю строки «кода», которые состоят из только Заключительная скобка или скобка, количество линий Go падает до 44. Я амбивалентно, справедливо ли они считать их, поскольку они не представляют какую -либо дополнительную семантическую сложность, но занимайте вертикальное пространство, уменьшая количество кода, которое я могу установить на экране одновременно.
Время выполнения, однако, является звездным 0,5 секунды. Было использовано около 1,4 ГБ памяти, но только 700 МБ жили в физической памяти (я просматриваю использование в топ ). Я побежал Go 1.14.4.
Реализация Haskell – самая сложная измерение производительности из -за ленивой оценки. Поскольку я ничего не делаю с результатами, мне пришлось прочитать о способах заставить Хаскелла оценить, и даже пришлось Пост На R/Haskell, когда я не мог понять, как убедиться, что JSON был действительно проанализирован, прежде чем начать часы. Я думаю, что эта реализация является справедливым судом.
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} import Data.Time import Data.Text (Text) import qualified Data.Text as Text import Data.Aeson import Data.Maybe import Control.Exception import Control.DeepSeq import GHC.Generics data Case = Case { typed :: !Text , possibilities :: ![Text] } deriving (Generic, Show, Read, Eq) instance FromJSON Case instance NFData Case -- | The simple algorithm. autocomplete :: Case -> Text autocomplete (Case typed possibilities) = let suffixes = mapMaybe (Text.stripPrefix typed) possibilities in if not $ null suffixes then typed <> commonPrefix suffixes else autocompleteContiguous typed possibilities commonPrefix :: [Text] -> Text commonPrefix [] = "" commonPrefix [x] = x commonPrefix (x:y:xs) = case Text.commonPrefixes x y of Nothing -> "" Just (p,_,_) -> commonPrefix (p:xs) autocompleteContiguous typed possibilities = let matches = filter (Text.isInfixOf typed) possibilities in if length matches > 1 then typed else if length matches == 1 then head matches else autocompleteSparse typed possibilities autocompleteSparse typed possibilities = let matches = filter (isMatch typed) possibilities in if length matches == 1 then head matches else typed where isMatch "" str = True isMatch substr str = case Text.findIndex (== Text.head substr) str of Nothing -> False Just index -> isMatch (Text.tail substr) (Text.drop (index + 1) str) main = do cases <- (fromJust <$> decodeFileStrict' "data") :: IO [Case] mapM_ (evaluate . force) cases start <- getCurrentTime mapM_ (evaluate . autocomplete) cases end <- getCurrentTime putStrLn $ "Took " ++ (show $ diffUTCTime end start)
После удаления Главный
, Случай
Производство и связанные с ними импорт, а также подписи типа (я не думаю, что здесь действительно необходимы), это 33 строки.
На самом деле я не придумал эту реализацию автозаполнение
и CommonPrefix
; R/Haskell Polks помогли мне. (Черт возьми, я собирался использовать строка
вместо Текст
Анкет Это было бы ужасно.)
Время выполнения без оптимизации составляет 2,25 секунды; Если я компилируюсь с -О2
, это падает до 1.2. Неплохо, но я все еще немного разочарован, что это так далеко позади.
И использование памяти! По какой -то причине он выделяет 3 ГБ. Флаг оптимизации не влияет на это. Я не удивлюсь, если это то, что я делаю не так, но Шиш. Я надеялся, что даже наивная реализация не займет почти вдвое больше памяти, чем Динамически напечатано Python One.
Обновление: я попробовал +Rts -s
Флаги, предложенные кем -то на R/Haskell, и заявили, что только 1,5 ГБ памяти использовались. Но это не может быть правдой, не так ли? верхняя
показал увеличение активной памяти на 3 ГБ. Странный.
Вот как это прошло. Не стесняйтесь критиковать мои решения в комментариях. Наконец, вот сценарий, который я использовал для создания тестовых случаев:
import string, json, time, random allowed_chars = string.ascii_lowercase + string.digits def gendata(): start = time.time() funcs = ( mkset_full_prefix_completion, mkset_partial_prefix_completion, mkset_typed_no_completion, mkset_prefix_ambiguous, mkset_contain_ambiguous, mkset_contain_completion, mkset_sparse_ambiguous, mkset_sparse_completion, ) sets = [random.choice(funcs)() for i in range(1000000)] end = time.time() print(f"generated data in {end - start}") with open('data', 'w') as f: json.dump(sets, f) def mkset_full_prefix_completion(): """Full completion from prefix is possible.""" typed = randomchars(allowed_chars, 1, 4) target = typed + randomchars(allowed_chars, 5, 10) # To ensure there's only one possibility, others can't have the last char in typed. new_chars = ''.join(c for c in allowed_chars if c != typed[-1]) possibilities = [target, *(randomchars(allowed_chars, 5, 15) for i in range(random.randint(2, 19)))] random.shuffle(possibilities) return {'typed': typed, 'possibilities': possibilities} def mkset_partial_prefix_completion(): """Partial completion from prefix is possible.""" typed = randomchars(allowed_chars, 0, 3) common = typed + randomchars(allowed_chars, 1, 5) # Make sure there's one that uses a continuation the others don't, so full completion isn't possible. dropped_char = random.choice(allowed_chars) new_chars = ''.join(c for c in allowed_chars if c != dropped_char) possibilities = [common + dropped_char + randomchars(allowed_chars, 5, 10), *(common + randomchars(new_chars, 5, 10) for i in range(random.randint(2, 19)))] random.shuffle(possibilities) return {'typed': typed, 'possibilities': possibilities} def mkset_typed_no_completion(): """The user's typed something with no completion.""" typed = randomchars(allowed_chars, 5, 10) # Don't allow choosing some char in the typed string. dropped_char = random.choice(typed) new_chars = ''.join(c for c in allowed_chars if c != dropped_char) possibilities = [randomchars(new_chars, 5, 15) for i in range(random.randint(2, 20))] random.shuffle(possibilities) return {'typed': typed, 'possibilities': possibilities} def mkset_prefix_ambiguous(): """There are multiple things that start with what the user's typed, and immediately diverge.""" typed = randomchars(allowed_chars, 1, 5) p1 = typed + randomchars(allowed_chars, 5, 10) # Pick another possibility that immediately diverges from p1. p2_chars = ''.join(c for c in allowed_chars if c != p1[len(typed)]) p2 = typed + randomchars(p2_chars, 5, 10) possibilities = [p1, p2, *(randomchars(allowed_chars, 5, 15) for i in range(random.randint(2, 18)))] random.shuffle(possibilities) return {'typed': typed, 'possibilities': possibilities} def mkset_contain_ambiguous(): """There's nothing that starts with what the user's typed, but two that contain it.""" typed = randomchars(allowed_chars, 1, 5) p1 = randomchars(allowed_chars, 2, 5) + typed + randomchars(allowed_chars, 2, 5) # Pick another possibility that immediately diverges from p1. new_chars = ''.join(c for c in allowed_chars if c != p1[len(typed)]) p2 = randomchars(new_chars, 2, 5) + typed + randomchars(new_chars, 2, 5) possibilities = [p1, p2, *(randomchars(new_chars, 5, 15) for i in range(random.randint(2, 18)))] random.shuffle(possibilities) return {'typed': typed, 'possibilities': possibilities} def mkset_contain_completion(): """There's nothing that starts with what the user's typed, but one that contains it.""" typed = randomchars(allowed_chars, 1, 5) match = randomchars(allowed_chars, 2, 5) + typed + randomchars(allowed_chars, 2, 5) # Make sure nothing else can contain it. dropped_char = random.choice(typed) new_chars = ''.join(c for c in allowed_chars if c != dropped_char) possibilities = [match, *(randomchars(new_chars, 5, 15) for i in range(random.randint(2, 19)))] random.shuffle(possibilities) return {'typed': typed, 'possibilities': possibilities} def mkset_sparse_ambiguous(): """There's nothing that starts with what the user's typed, nothing that contains it, but two that contain all the characters.""" typed = randomchars(allowed_chars, 2, 5) p1 = intersperse_chars(typed, randomchars(allowed_chars, 5, 15)) p2 = intersperse_chars(typed, randomchars(allowed_chars, 5, 15)) # Make sure nothing else can contain it. dropped_char = random.choice(typed) new_chars = ''.join(c for c in allowed_chars if c != dropped_char) possibilities = [p1, p2, *(randomchars(new_chars, 5, 15) for i in range(random.randint(2, 18)))] random.shuffle(possibilities) return {'typed': typed, 'possibilities': possibilities} def mkset_sparse_completion(): """There's nothing that starts with what the user's typed, nothing that contains it, but one that contains all the characters.""" typed = randomchars(allowed_chars, 2, 5) match = intersperse_chars(typed, randomchars(allowed_chars, 5, 15)) # Make sure nothing else can contain it. dropped_char = random.choice(typed) new_chars = ''.join(c for c in allowed_chars if c != dropped_char) possibilities = [match, *(randomchars(new_chars, 5, 15) for i in range(random.randint(2, 19)))] random.shuffle(possibilities) return {'typed': typed, 'possibilities': possibilities} def randomchars(charset, lower, upper): return ''.join(random.choice(charset) for i in range(random.randint(lower, upper))) def intersperse_chars(chars, string): final, string = string[0], string[1:] for c in chars: skip = random.randint(0, 3) final += c final += string[skip:] string = string[:skip] return final + string if __name__ == '__main__': gendata()
Оригинал: “https://dev.to/yujiri8/comparing-an-algorithm-in-python-go-haskell-2olm”