Алгоритм группировки слов анаграммы

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

вход:

man car kile arc none like

выход:

man
car arc
kile like
none

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

пример: man = > ' m ' + ' a ' + 'n', но это не даст уникальных значений.

любой предложение?


см. следующий код в C#:

string line = Console.ReadLine();
string []words=line.Split(' ');
int[] numbers = GetUniqueInts(words);
for (int i = 0; i < words.Length; i++)
{
    if (table.ContainsKey(numbers[i]))
    {
        table[numbers[i]] = table[numbers[i]].Append(words[i]);
    }
    else
    {
        table.Add(numbers[i],new StringBuilder(words[i]));
    }

}

проблема в том, как развиваться GetUniqueInts(string []) метод.

14 ответов


Не беспокойтесь о пользовательской хэш-функции вообще. Используйте обычную строковую хэш-функцию на любой платформе. Важно сделать ключ для вашей хэш-таблицы идеей "отсортированного слова" - где слово сортируется по буквам, поэтому" автомобиль " = > "acr". Все анаграммы будут иметь одно и то же"отсортированное слово".

просто есть хэш из "отсортированного слова"в" список слов для этого отсортированного слова". В LINQ это невероятно просто:

using System;
using System.Collections.Generic;
using System.Linq;

class FindAnagrams
{
    static void Main(string[] args)
    {
        var lookup = args.ToLookup(word => SortLetters(word));

        foreach (var entry in lookup)
        {
            foreach (var word in entry)
            {
                Console.Write(word);
                Console.Write(" ");
            }
            Console.WriteLine();
        }
    }

    static string SortLetters(string original)
    {
        char[] letters = original.ToCharArray();
        Array.Sort(letters);
        return new string(letters);
    }
}

образец использовать:

c:\Users\Jon\Test>FindAnagrams.exe man car kile arc none like
man
car arc
kile like
none

я использовал схему, вдохновленную Геделем:

назначьте простые числа P_1 в p_26 буквам (в любом порядке, но для получения небольших хэш-значений лучше всего давать общим буквам маленькие простые числа).

построил гистограмму букв в слове.

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

Python-кода:

primes = [2, 41, 37, 47, 3, 67, 71, 23, 5, 101, 61, 17, 19, 13, 31, 43, 97, 29, 11, 7, 73, 83, 79, 89, 59, 53]


def get_frequency_map(word):
    map = {}

    for letter in word:
        map[letter] = map.get(letter, 0) + 1

    return map


def hash(word):
    map = get_frequency_map(word)
    product = 1
    for letter in map.iterkeys():
        product = product * primes[ord(letter)-97] ** map.get(letter, 0)
    return product

этот ловко преобразует сложную проблему поиска субанаграмм в (также известную как сложную) проблему факторинга больших чисел...


версия Python для хихиканья:

from collections import defaultdict
res = defaultdict(list)
L = "car, acr, bat, tab, get, cat".split(", ")

for w in L:
    res["".join(sorted(w))].append(w)

print(res.values())

Я не думаю, что вы найдете что-нибудь лучше, чем хэш-таблицу с пользовательской хэш-функцией (которая сортировала бы буквы слова перед хэшированием).

сумма букв никогда не будет работать, потому что вы не можете сделать " ac " и " bb " разными.


вам понадобятся большие целые числа (или битовый вектор на самом деле), но может работать следующее

первому вхождению каждой буквы get присваивается номер бита для этой буквы, второе вхождение получает номер бита для этой буквы + 26.

a #1 = 1 b #1 = 2 c #1 = 4 a #2 = 2^26 b #2 = 2 ^ 27

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

хранилище требования к значениям слов будут:

Н * 26 бит

где n-максимальное число вхождений любой повторяющейся буквы.


Я бы не использовал хэширование, так как он добавляет дополнительную сложность для поиска и добавляет. Хэширование, сортировка и умножение будут медленнее, чем простое решение гистограммы на основе массива с отслеживанием уникумов. В худшем случае O (2n):

// structured for clarity
static bool isAnagram(String s1, String s2)
{
    int[] histogram = new int[256];

    int uniques = 0;

    // scan first string
    foreach (int c in s1)
    {
        // count occurrence
        int count = ++histogram[c];

        // count uniques
        if (count == 1)
        {
            ++uniques;
        }
    }

    // scan second string
    foreach (int c in s2)
    {
        // reverse count occurrence
        int count = --histogram[c];

        // reverse count uniques
        if (count == 0)
        {
            --uniques;
        }
        else if (count < 0) // trivial reject of longer strings or more occurrences
        {
            return false;
        }
    }

    // final histogram unique count should be 0
    return (uniques == 0);
}

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

unsigned char letter_frequency[26];

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

С некоторыми экспериментами с очень большим словарем я не нашел слова, которое превышало бы число частот 9 для любой буквы, поэтому "подпись" может быть представлена как набор цифр 0..9 (размер может быть легко вдвое уменьшен путем упаковки в байты как шестнадцатеричный и далее уменьшен двоичным кодированием числа, но я до сих пор не беспокоился об этом).

вот функция ruby для вычисления подписи данного слова и хранения его в хэш, отбрасывая дубликаты. Из хэша я позже построю таблицу SQL:

def processword(word, downcase)
  word.chomp!
  word.squeeze!(" ") 
  word.chomp!(" ")
  if (downcase)
    word.downcase!
  end
  if ($dict[word]==nil) 
    stdword=word.downcase
    signature=$letters.collect {|letter| stdword.count(letter)}
    signature.each do |cnt|
      if (cnt>9)
        puts "Signature overflow:#{word}|#{signature}|#{cnt}"
      end
    end
    $dict[word]=[$wordid,signature]
    $wordid=$wordid+1
  end
end

назначьте уникальное простое число буквам a-z

повторите свой массив слов, создавая произведение простых чисел на основе букв в каждом слове.
Храните этот продукт в списке слов с соответствующим словом.

сортировка массива по возрастанию по произведению.

массив, делаем управление перерыв при каждом изменении продукта.


В C я только что реализовал следующий хэш, который в основном делает 26-битную битовую маску о том, имеет ли слово в словаре определенную букву в нем. Итак, все анаграммы имеют одинаковый хэш. Хэш не учитывает повторяющиеся буквы, поэтому будет некоторая дополнительная перегрузка, но ему все равно удается быть быстрее, чем моя реализация perl.

#define BUCKETS 49999

struct bucket {
    char *word;
    struct bucket *next;
};

static struct bucket hash_table[BUCKETS];

static unsigned int hash_word(char *word)
{
    char *p = word;
    unsigned int hash = 0;

    while (*p) {
        if (*p < 97 || *p > 122) {
            return 0;
        }
        hash |= 2 << (*p - 97);
        *p++;
    }

    return hash % BUCKETS;
}

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


Я создам hasmap на основе образца слова и остальных алфавитов, которые мне все равно.

например, если слово "автомобиль" мой хэш-стол будет таким: a, 0 б,Макс. c, 1 Д Макс э,Макс ... .. r, 2 . В результате любой имеет больше 3 будет рассматривать как не соответствующий

(больше тюнинга...) И мой метод сравнения будет сравнивать хэш-сумму в самом вычислении хэша. Это не будет продолжаться, как только он может идентифицировать слово не равный.

public static HashMap<String, Integer> getHashMap(String word) {
        HashMap<String, Integer> map = new HashMap<String, Integer>();
        String[] chars = word.split("");
        int index = 0;
        for (String c : chars) {
            map.put(c, index);
            index++;
        }
        return map;
    }

    public static int alphaHash(String word, int base,
            HashMap<String, Integer> map) {
        String[] chars = word.split("");
        int result = 0;
        for (String c : chars) {
            if (c.length() <= 0 || c.equals(null)) {
                continue;
            }
            int index = 0;
            if (map.containsKey(c)) {
                index = map.get(c);
            } else {
                index = Integer.MAX_VALUE;
            }
            result += index;
            if (result > base) {
                return result;
            }
        }
        return result;
    }

основным методом

  HashMap<String, Integer> map = getHashMap(sample);
        int sampleHash = alphaHash(sample, Integer.MAX_VALUE, map);
        for (String s : args) {
                if (sampleHash == alphaHash(s, sampleHash, map)) {
                    System.out.print(s + " ");
                }
            }

анаграммы можно найти следующим образом:

  1. длина слова должна совпадать.
  2. выполнить сложение каждого символа в терминах целочисленного значения. Эта сумма будет соответствовать, если вы выполните то же самое на anagram.
  3. выполнить умножение каждого символа в терминах целочисленного значения. Оцененное значение будет соответствовать, если вы выполните то же самое на anagram.

Итак, я продумал выше трех проверок, мы можем найти анаграммы. Поправьте меня, если я неправильный.


пример: abc cba

длина обоих слов 3.

сумма отдельных символов для обоих слов равна 294.

Prod отдельных символов для обоих слов 941094.


JavaScript-версию. использование хэширования.

временная сложность: 0 (nm), где n-количество слов, m-длина слова

var words = 'cat act mac tac ten cam net'.split(' '),
    hashMap = {};

words.forEach(function(w){
    w = w.split('').sort().join('');
    hashMap[w] = (hashMap[w]|0) + 1;
});

function print(obj,key){ 
    console.log(key, obj[key]);
}

Object.keys(hashMap).forEach(print.bind(null,hashMap))

просто хочу добавить простое решение python в дополнение к другим полезным ответам:

def check_permutation_group(word_list):
    result = {}

    for word in word_list:
        hash_arr_for_word = [0] * 128  # assuming standard ascii

        for char in word:
            char_int = ord(char)
            hash_arr_for_word[char_int] += 1

        hash_for_word = ''.join(str(item) for item in hash_arr_for_word)

        if not result.get(hash_for_word, None):
            result[str(hash_for_word)] = [word]
        else:
            result[str(hash_for_word)] += [word]

return list(result.values())

код python:

line = "man car kile arc none like"
hmap = {}
for w in line.split():
  ws = ''.join(sorted(w))
  try:
    hmap[ws].append(w)
  except KeyError:
    hmap[ws] = [w]

for i in hmap:
   print hmap[i]

выход:

['car', 'arc']
['kile', 'like']
['none']
['man']