N-грамм, который является наиболее частым среди всех слов

Я столкнулся со следующей проблемой на собеседовании по программированию:

Задача 1: N-граммы

N-грамма - это последовательность из N последовательных символов данного слова. У слова «пилот» есть три 3-грамма: «пил», «ило» и «лот». Для данного набора слов и длины n граммов Ваша задача:

• write a function that finds the n-gram that is the most frequent one among all the words
• print the result to the standard output (stdout)
• if there are multiple n-grams having the same maximum frequency please print the one that is the smallest lexicographically (the first one according to the dictionary sorting order)

Обратите внимание, что ваша функция получит следующие аргументы:

• text
    ○ which is a string containing words separated by whitespaces
• ngramLength
    ○ which is an integer value giving the length of the n-gram

Ограничения данных

• the length of the text string will not exceed 250,000 characters
• all words are alphanumeric (they contain only English letters a-z, A-Z and numbers 0-9)

Ограничения эффективности

• your function is expected to print the result in less than 2 seconds

Пример вводимого текста: «aaaab a0a baaab c»

Вывод aaa ngramLength: 3

Объяснение

Для входных данных, представленных выше, 3 грамма, отсортированные по частоте:

• "aaa" with a frequency of 3
• "aab" with a frequency of 2
• "a0a" with a frequency of 1
• "baa" with a frequency of 1

Если у меня есть только один час, чтобы решить проблему, и я решил использовать язык C для ее решения: стоит ли реализовать хеш-таблицу для подсчета частоты N-граммов за это время? потому что в библиотеке C нет реализации хеш-таблицы ...

Если да, то я думал реализовать хеш-таблицу, используя отдельную цепочку с упорядоченными связными списками. Эти реализации сокращают время, необходимое для решения проблемы ...

Это самый быстрый вариант?

Спасибо!!!


person andrestoga    schedule 04.09.2014    source источник
comment
Это настоящее собеседование по кодированию?   -  person templatetypedef    schedule 04.09.2014
comment
Вы определили, что двоичное дерево (скажем, AVL) не справляется с этой задачей?   -  person Iwillnotexist Idonotexist    schedule 04.09.2014
comment
Неужели 3 грамма - это больше всего, что вам нужно? Существует (26 + 26 + 10) ^ 3 = 238328 возможных 3-граммов только с буквенно-цифровыми символами, поэтому прямая LUT выглядит выполнимой.   -  person Iwillnotexist Idonotexist    schedule 04.09.2014
comment
Я бы заранее выделил необходимое количество сегментов в одном массиве (что возможно, поскольку у вас есть верхняя граница длины текста) и сохраню только указатели на них в хеш-таблице. Используйте эвристику перехода на передний план / вставку назад, чтобы ускорить поиск хеш-таблицы. И в конце отсортируем массив. На практике использование дерева происходит медленнее.   -  person michaelmeyer    schedule 04.09.2014
comment
Считать. Сколько 3 граммов в тексте из 1000 символов?   -  person Colonel Panic    schedule 05.09.2014


Ответы (8)


Если важна эффективность реализации и вы используете C, я бы инициализировал массив указателей на начало n-граммов в строке, использовал qsort для сортировки указателей в соответствии с n-граммами, частью которых они являются, а затем перебрать этот отсортированный массив и вычислить количество.

Это должно выполняться достаточно быстро, и нет необходимости кодировать какие-либо причудливые структуры данных.

person btilly    schedule 04.09.2014
comment
Единственное, что я могу придумать, может быть быстрее, чем это, - это использование смещений вместо фактических указателей. Предполагая, что 64-битные (собственные) указатели, вы можете уменьшить вдвое оперативную память с 4-байтовыми смещениями. Умное кодирование могло бы сжать необходимые 18 бит в 2 байта для большего. - person phs; 04.09.2014
comment
@phs Сортировка - O(n log(n)), а решение на основе хеша - O(n). Так что это не должно иметь наилучшей производительности. Это очень простой подход. - person btilly; 04.09.2014
comment
В малых масштабах асимптотический предел имеет меньшее значение, чем детали оборудования. Я подозреваю, что эффективность строки кэша и детали выбранной функции хеширования будут иметь большое значение. Опять же, только догадываюсь. - person phs; 04.09.2014
comment
@phs Я не вижу причин, по которым переход к случайному ведру в хэше менее эффективное использование кеша, чем сравнение двух случайных указателей из региона. Так что хеширование - всегда выигрыш. Но если вы создадите массив ngram, а затем отсортируете его (вместо использования указателей), вы можете победить хеширование. - person btilly; 04.09.2014
comment
Отличный ответ. После сортировки вы можете подсчитать с пространством O (1) за один проход от большего индекса массива к меньшему. - person גלעד ברקן; 04.09.2014

Извините за публикацию python, но я бы сделал следующее: у вас могут появиться идеи по поводу алгоритма. Обратите внимание, что эта программа решает на порядок больше слов.

from itertools import groupby

someText = "thibbbs is a test and aaa it may haaave some abbba reptetitions "
someText *= 40000
print len(someText)
n = 3

ngrams = []
for word in filter(lambda x: len(x) >= n, someText.split(" ")):
    for i in range(len(word)-n+1):
        ngrams.append(word[i:i+n])
        # you could inline all logic here
        # add to an ordered list for which the frequiency is the key for ordering and the paylod the actual word

ngrams_freq = list([[len(list(group)), key] for key, group in groupby(sorted(ngrams, key=str.lower))])

ngrams_freq_sorted = sorted(ngrams_freq, reverse=True)

popular_ngrams = []

for freq in ngrams_freq_sorted:
    if freq[0] == ngrams_freq_sorted[0][0]:
        popular_ngrams.append(freq[1])
    else:
        break

print "Most popular ngram: " + sorted(popular_ngrams, key=str.lower)[0]
# > 2560000
# > Most popular ngram: aaa
# > [Finished in 1.3s]**
person AlexanderBrevig    schedule 04.09.2014
comment
В моем тесте поддержание словаря ngram / count было в два раза быстрее, чем ваше решение. И был код попроще. - person btilly; 04.09.2014
comment
Я ожидал обоих, поэтому я упомянул об этом в комментариях. Я добавил в свой пост ответ на c ++. Думаю, это была небольшая забавная проблема. - person AlexanderBrevig; 04.09.2014

Итак, основной рецепт этой проблемы:

  1. Найти все n-граммы в строке
  2. Сопоставьте все повторяющиеся записи с новой структурой, которая имеет n-грамм и количество раз, когда она встречается

Вы можете найти мое решение на C ++ здесь: http://ideone.com/MNFSis

Данный:

const unsigned int MAX_STR_LEN = 250000;
const unsigned short NGRAM = 3;
const unsigned int NGRAMS = MAX_STR_LEN-NGRAM;
//we will need a maximum of "the length of our string" - "the length of our n-gram"
//places to store our n-grams, and each ngram is specified by NGRAM+1 for '\0'
char ngrams[NGRAMS][NGRAM+1] = { 0 };

Затем для первого шага - это код:

const char *ptr = str;
int idx = 0;
//notTerminated checks ptr[0] to ptr[NGRAM-1] are not '\0'
while (notTerminated(ptr)) { 
    //noSpace checks ptr[0] to ptr[NGRAM-1] are isalpha()
    if (noSpace(ptr)) {
        //safely copy our current n-gram over to the ngrams array
        //we're iterating over ptr and because we're here we know ptr and the next NGRAM spaces
        //are valid letters
        for (int i=0; i<NGRAM; i++) {
            ngrams[idx][i] = ptr[i];
        }
        ngrams[idx][NGRAM] = '\0'; //important to zero-terminate
        idx++;
    }
    ptr++;
}

На данный момент у нас есть список всех n-граммов. Давайте найдем самый популярный из них:

FreqNode head = { "HEAD", 0, 0, 0 }; //the start of our list

for (int i=0; i<NGRAMS; i++) {
    if (ngrams[i][0] == '\0') break;
    //insertFreqNode takes a start node, this where we will start to search for duplicates
    //the simplest description is like this:
    //  1 we search from head down each child, if we find a node that has text equal to
    //    ngrams[i] then we update it's frequency count
    //  2 if the freq is >= to the current winner we place this as head.next
    //  3 after program is complete, our most popular nodes will be the first nodes
    //    I have not implemented sorting of these - it's an exercise for the reader ;)
    insertFreqNode(&head, ngrams[i]);
}

//as the list is ordered, head.next will always be the most popular n-gram
cout << "Winner is: " << head.next->str << " " << " with " << head.next->freq << " occurrences" << endl

Удачи тебе!

person AlexanderBrevig    schedule 04.09.2014

Ради интереса я написал версию SQL (SQL Server 2012):

if object_id('dbo.MaxNgram','IF') is not null
    drop function dbo.MaxNgram;
go

create function dbo.MaxNgram(
     @text      varchar(max)
    ,@length    int
) returns table with schemabinding as
return
    with 
    Delimiter(c) as ( select ' '),
    E1(N) as (
        select 1 from (values 
            (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
        )T(N)
    ),
    E2(N) as (
        select 1 from E1 a cross join E1 b
    ),
    E6(N) as (
        select 1 from E2 a cross join E2 b cross join E2 c
    ),
    tally(N) as (
        select top(isnull(datalength(@text),0))
             ROW_NUMBER() over (order by (select NULL))
        from E6
    ),
    cteStart(N1) as (
        select 1 union all
        select t.N+1 from tally t cross join delimiter 
            where substring(@text,t.N,1) = delimiter.c
    ),
    cteLen(N1,L1) as (
        select s.N1,
               isnull(nullif(charindex(delimiter.c,@text,s.N1),0) - s.N1,8000)
        from cteStart s
        cross join delimiter
    ),
    cteWords as (
        select ItemNumber = row_number() over (order by l.N1),
               Item       = substring(@text, l.N1, l.L1)
        from cteLen l
    ),
    mask(N) as ( 
        select top(@length) row_Number() over (order by (select NULL))
        from E6
    ),
    topItem as (
        select top 1
             substring(Item,m.N,@length) as Ngram
            ,count(*)                    as Length
        from cteWords   w
        cross join mask m
        where m.N     <= datalength(w.Item) + 1 - @length
          and @length <= datalength(w.Item) 
        group by 
            substring(Item,m.N,@length)
        order by 2 desc, 1 
    )
    select d.s
    from (
        select top 1 NGram,Length
        from topItem
    ) t
    cross apply (values (cast(NGram as varchar)),(cast(Length as varchar))) d(s)
;
go

который при вызове с образцом ввода, предоставленным OP

set nocount on;
select s as [ ] from MaxNgram(
    'aaaab a0a baaab c aab'
   ,3
);
go

дает по желанию

------------------------------
aaa
3
person Pieter Geerkens    schedule 04.09.2014

Если вы не привязаны к C, я написал этот скрипт Python примерно за 10 минут, который обрабатывает файл размером 1,5 МБ, содержащий более 265 000 слов, ищущих 3 грамма за 0,4 ​​секунды. (кроме вывода значений на экран)
Для теста используется текст Улисс Джеймса Джойса, его можно бесплатно найти здесь https://www.gutenberg.org/ebooks/4300

Здесь оба разделителя слов space и возврат каретки \n

import sys

text = open(sys.argv[1], 'r').read()
ngram_len = int(sys.argv[2])
text = text.replace('\n', ' ')
words = [word.lower() for word in text.split(' ')]
ngrams = {}
for word in words:
    word_len = len(word)
    if word_len < ngram_len:
        continue
    for i in range(0, (word_len - ngram_len) + 1):
        ngram = word[i:i+ngram_len]
        if ngram in ngrams:
            ngrams[ngram] += 1
        else:
            ngrams[ngram] = 1
ngrams_by_freq = {}
for key, val in ngrams.items():
        if val not in ngrams_by_freq:
                ngrams_by_freq[val] = [key]
        else:
                ngrams_by_freq[val].append(key)
ngrams_by_freq = sorted(ngrams_by_freq.items())
for key in ngrams_by_freq:
        print('{} with frequency of {}'.format(key[1:], key[0]))
person Andrea Tulimiero    schedule 28.01.2017

Вы можете преобразовать триграмму в код RADIX50. См. http://en.wikipedia.org/wiki/DEC_Radix-50.

В radix50 выходное значение триграммы соответствует 16-битному беззнаковому целочисленному значению.

После этого вы можете использовать триграмму в системе счисления в качестве индекса в массиве.

Итак, ваш код будет таким:

uint16_t counters[1 << 16]; // 64K counters

bzero(counters, sizeof(counters));

for(const char *p = txt; p[2] != 0; p++) 
  counters[radix50(p)]++;

После этого просто найдите максимальное значение в массиве и декодируйте индекс обратно в триграмму.

Я использовал этот трюк, когда реализовал алгоритм Уилбура-Ховайко для нечеткого поиска ~ 10 лет назад.

Вы можете скачать исходный код здесь: http://itman.narod.ru/source/jwilbur1.tar.gz.

person olegarch    schedule 04.09.2014
comment
Это нечувствительность к регистру A-Z, 0-9, SPACE, DOLLAR, DOT и UNDEF. Достаточно для вычисления триграмм для текстовой строки. - person olegarch; 04.09.2014
comment
Но у вас сейчас нет параметра ngramLength заранее, и это сильно зависит от того факта, что n=3. Но умное решение для триграммы - person jlhonora; 04.09.2014
comment
начать с системы счисления 50, for each c in counters.sorteddesc find most frequest starting -> counters2 //now the only thing see if something could be bigger among rest counters if ( max(counters2) > next in counters and bigger than local max found before) RESULT else store local max, and proceed to the next item - person zubrabubra; 05.09.2014

Вы можете решить эту проблему за O (nk) времени, где n - количество слов, а k - среднее количество n-граммов на слово.

Вы правы, полагая, что хеш-таблица - хорошее решение проблемы.

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

Кроме того, не забудьте выделить достаточно памяти для вашей хеш-таблицы: что-то примерно в два раза больше ожидаемого числа n-граммов должно быть в порядке. Поскольку ожидаемое количество n-граммов будет ‹= 250 000, хеш-таблицы 500 000 должно быть более чем достаточно.

Что касается скорости кодирования, небольшая длина ввода (250 000) делает возможной сортировку и подсчет. Самый быстрый способ - это, вероятно, сгенерировать массив указателей на каждую n-грамм, отсортировать массив, используя соответствующий компаратор, а затем пройтись по нему, отслеживая, какая n-грамм появляется больше всего.

person Richard    schedule 28.01.2017

Одно простое решение python для этого вопроса

your_str = "aaaab a0a baaab c"
str_list = your_str.split(" ")
str_hash = {}
ngram_len = 3

for str in str_list:
    start = 0
    end = ngram_len
    len_word = len(str)
    for i in range(0,len_word):
        if end <= len_word :
            if str_hash.get(str[start:end]):              
                str_hash[str[start:end]] = str_hash.get(str[start:end]) + 1
            else:
                str_hash[str[start:end]] = 1
            start = start +1
            end = end +1
        else:
            break

keys_sorted =sorted(str_hash.items())
for ngram in sorted(keys_sorted,key= lambda x : x[1],reverse = True):
    print "\"%s\" with a frequency of %s" % (ngram[0],ngram[1])
person Vishnu R    schedule 06.03.2021