Как из списка достать уникальные слова отфильтровав слова которые являются дубликатами или подстроками слов?
Допустим, у меня есть List<>, в котором есть много разных слов. Нужно в нём найти слова, которые повторяются.
Пример:
Нужно, чтобы программа нашла, что слово "анненский", которое встречается в 4 слове и так же "анненский", которое встречается в 5 слове. Далее, убрала все остальные слова и оставила только четвёртое слово без повторов.
Другой пример:
1 слово - "нагибин". 4 слово "нагибиннагибин". Нужно, чтобы осталось только первое слово.
Другой пример:
1 слово - "алиса". 4 слово - "алисаалиса". 5 слово "алисаалисаалиса". Нужно, чтобы осталось только первое слово "алиса".
Ответы (5 шт):
Классический подход к фильтрации дубликатов - это HashSet:
var set = new HashSet<string>(list);
list = set.ToList();
Всё!
Это половина ответа, тут скучно всё. Вторая часть - как определить, что слово написано два-три раза подряд.
Вторая часть концентрируется на том, является ли строка2 размноженной 1..n раз строкой1:
private bool IsDupString(string sample1, string sample2)
{
if (sample1.Length == sample2.Length)
return string.Equals(sample1, sample2);
// to guarantee that sample1 is less than sample2
if (sample1.Length > sample2.Length)
return IsDupString(sample2, sample1);
int ratio = TryGetRatio(sample1.Length, sample2.Length);
if (ratio == 0)
return false;
var i = 0;
while (i < sample1.Length)
{
var ch = sample1[i];
for (int j = 1; j < ratio; j++)
{
var pos = i + (sample1.Length) * j;
if (sample2[pos] != ch)
return false;
}
i++;
}
return true;
}
private int TryGetRatio(int length1, int length2)
{
double delta = 0.0001;
double ratio1 = (double)length2 / length1;
double ratio2 = length2 / length1;
if (Math.Abs(ratio1 - ratio2) > delta)
return 0;
return (int)ratio2;
}
Можете запустить на нескольких примерах, чтобы проверить работу. Возможно, можно написать проще, я не думал об этом, просто написал прямолинейный алгоритм.
А вообще собираем вместе - получаем нужный ответ:
var list = new List<string>()
{
"цыпа",
"алиса",
"цыпацыпа",
"алисаалиса",
};
var set = new HashSet<string>();
foreach (var item in list)
{
if(set.Any(x => IsDupString(item, x)))
continue;
if(!set.Contains(item))
set.Add(item);
}
Остался небольшой момент, что делать если в списке сначала встретится "алисаалиса" раньше чем просто "алиса", но мне кажется, что допилить текущий алгоритм будет несложно. И нет, сортировать нет смысла, можно проще.
Я Вам предложу очень простой алгоритм, который, правда, может свести весь большой список буквально к алфавиту.
Исходный список назовем просто list
Соритруем слова по длинне. В начале будут самые короткие. Назовем этот список list_sorted
Заводим Dictionary, я назову его просто dictionary. Начинаем с начала отсортированного списка вставлять слова в dictinary - естественно, без повторений.
При каждой вставке пробегаемся по списку list_sorted (или просто list) и выкидываем из него все слова, в котрых встречается очередное вставляемое в dictinary слово в качестве подстроки.
Всё.
Заметьте, что если у Вас встретится в спсике слово "и", то "или", "иллюзорный" и "имманул" в кончательный спсиок уже не попадут. Вам именно так надо?
То есть, уточните:
Если слово "алисаалисаалиса" встретися в исходном списке раньше слова "алиса" - то оба должны остаться в списке? если это так - просто не надо сортировать исходный спиок по длинне слов.
Могу добавить код, но просто напишиет в комментариях, соответствует ли алгоритм задаче...
Типичная задача для структуры данных Trie
public class TrieNode
{
private Dictionary<char, TrieNode> _children = new Dictionary<char, TrieNode>();
private String _payload = null;
public void Add(String str)
{
Add(str, 0);
}
private void Add(string str, int ind)
{
if (str.Length == ind)
{
_payload = str;
return;
}
var c = str[ind];
if (_children.ContainsKey(c)) _children[c].Add(str, ind + 1);
else
{
TrieNode next = new TrieNode();
_children[c] = next;
next.Add(str, ind + 1);
}
}
public IEnumerable<string> GetShortest()
{
if (_payload != null) yield return _payload;
else
foreach (var v in _children.Values)
foreach (var ret in v.GetShortest()) yield return ret;
}
}
Проверка
var list = new List<string>()
{
"цыпа",
"алиса",
"цыпацыпа",
"алисаалиса",
};
var root = new TrieNode();
foreach (var w in list) root.Add(w);
foreach (var ret in root.GetShortest()) Console.WriteLine(ret);
Результат
цыпа
алиса
Подход хорошо работает если много одинаковых слов. Этот подход можно оптимизировать по разному, но в среднем он быстрее всего остального, что можно придумать и кушает чуть больше памяти. Также для симльно длинных слов можно рекурсию заменить на цикл, но для относительно коротких слов это без разницы.
Применительно к вашей задаче, можно не добавлять длинное слово, если его префикс уже был добавлен ранее (или удалять все поддерево если добавляется префикс), что может сэкономить память.
Также эта структура фильтрует дубликаты. А если алфавит слов известен, то можно ещё и результат получить сортированным.
Алгоритм работает с линейной скоростью относительно слов и символов.
Для каждого слова s построить его сжатое представление в виде s = pq с помощью z-функции: для строки s длиной n найдём первую позицию i такую, что i + z[i] = n, и при этом n % i == 0. Тогда строку s можно сжать до строки длины i, и q = n / i
Например, 'папапа' => 'па'3
Сложить в map сжатые строки в виде пар (string:List<int>), где string - сжатая строка p, а количество повторов q добавляется в список.
Для каждого ключа p с длиной списка более 1 проверяем, являются ли бОльшие q кратными наименьшему из списка. Если да - выводим p с наименьшим количеством повторов.
Если нет - разбиваем список на группы, содержащие наименьшие делители и кратные им (если есть). Например, для 'па':{2,3,5,6,9} получаем 'папа', 'папапа' и 'папапапапа'
Попробовал всё-таки оставить свой вариант "кода". Банальный перебор слов. Не уверен, что это правильно, но это, вроде, работает.
var list = new List<string>()
{
"енн",
"ннннн",
"еннкнн",
"анненский",
"нннн",
"анненскийанненский",
"анненскийанненскийанненский",
"ннйнннн",
"иннннокнн",
};
List<string> newList = new List<string>(); //лист, где будут "правильные слова"
string tempWord = ""; //временная переменная для поиска слова
list.Sort();
foreach (var item in list) //логика такая - если данный элемент, который представлен в List, при добавлении себя к себе, существует к List, то его добавляем в "правильный лист"
{
tempWord = item + item;
for (int i = 0; i < list.Count; i++)
{
if (tempWord == list[i]) //если "двойное" слово существует в List, то добавляю его в другой List
{
newList.Add(item);
}
}
}
foreach (var i in newList) //вывод нового листа
{
Console.WriteLine(i);
}
Console.ReadKey();




