Алгоритм - Как эффективно удалить дубликаты элементов в списке? - PullRequest
11 голосов
/ 26 ноября 2009

Есть список L . Он содержит элементы произвольного типа каждый . Как эффективно удалить все дублирующиеся элементы в таком списке? ЗАКАЗ должен быть сохранен

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

Похожие вопросы

Ответы [ 15 ]

28 голосов
/ 26 ноября 2009

Предполагается, что заказ имеет значение:

  • Создать пустой набор S и пустой список M.
  • Сканирование списка L по одному элементу за раз.
  • Если элемент находится в наборе S, пропустите его.
  • В противном случае добавьте его к M и к S.
  • Повторите для всех элементов в L.
  • Возвращение М.

В Python:

>>> L = [2, 1, 4, 3, 5, 1, 2, 1, 1, 6, 5]
>>> S = set()
>>> M = []
>>> for e in L:
...     if e in S:
...         continue
...     S.add(e)
...     M.append(e)
... 
>>> M
[2, 1, 4, 3, 5, 6]

Если заказ не имеет значения:

M = list(set(L))
18 голосов
/ 26 ноября 2009

Особый случай: хеширование и равенство

Во-первых, нам нужно определить что-то в отношении допущений, а именно: наличие равных и функциональных отношений. Что я имею в виду под этим? Я имею в виду, что для множества исходных объектов S, для любых двух объектов x1 и x2, которые являются элементами S, существует (хеш) функция F такая, что:

if (x1.equals(x2)) then F(x1) == F(x2)

У Java такие отношения. Это позволяет вам проверять дубликаты как операцию, близкую к O (1), и, таким образом, сводит алгоритм к простой задаче O (n). Если заказ не важен, это простой вкладыш:

List result = new ArrayList(new HashSet(inputList));

Если заказ важен:

List outputList = new ArrayList();
Set set = new HashSet();
for (Object item : inputList) {
  if (!set.contains(item)) {
    outputList.add(item);
    set.add(item);
  }
}

Вы заметите, что я сказал "около O (1)". Это связано с тем, что такие структуры данных (как Java HashMap или HashSet) полагаются на метод, в котором часть хеш-кода используется для поиска элемента (часто называемого сегментом) в резервном хранилище. Количество ведер является степенью 2. Таким образом, индекс в этом списке легко рассчитать. hashCode () возвращает int. Если у вас есть 16 сегментов, вы можете найти, какой из них использовать, добавив хэш-код с 15, давая вам число от 0 до 15.

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

Поиск также может быть дорогим. Рассмотрим этот класс:

public class A {
  private final int a;

  A(int a) { this.a == a; }

  public boolean equals(Object ob) {
    if (ob.getClass() != getClass()) return false;
    A other = (A)ob;
    return other.a == a;
  }

  public int hashCode() { return 7; }
}

Этот код является абсолютно легальным и соответствует контракту equals-hashCode.

Если ваш набор не содержит ничего, кроме экземпляров A, ваша вставка / поиск теперь превращается в операцию O (n), превращая всю вставку в O (n 2 ).

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

Наконец, нужно сказать, что это особый случай . Если вы используете язык без «ярлыка хэширования», то это уже другая история.

Общий случай: нет заказа

Если для списка не существует никакой функции упорядочения, то вы застряли с O (n 2 ) сравнением грубой силы каждого объекта с любым другим объектом. Итак, на Java:

List result = new ArrayList();
for (Object item : inputList) {
  boolean duplicate = false;
  for (Object ob : result) {
    if (ob.equals(item)) {
      duplicate = true;
      break;
    }
  }
  if (!duplicate) {
    result.add(item);
  }
}

Общий случай: заказ

Если существует функция упорядочения (как, например, со списком целых чисел или строк), то вы сортируете список (то есть O (n log n)), а затем сравниваете каждый элемент в списке со следующим ( O (n)), поэтому общий алгоритм равен O (n log n). В Java:

Collections.sort(inputList);
List result = new ArrayList();
Object prev = null;
for (Object item : inputList) {
  if (!item.equals(prev)) {
    result.add(item);
  }
  prev = item;
}

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

7 голосов
/ 26 ноября 2009

в haskell это будет охватываться функциями nub и nubBy

nub :: Eq a => [a] -> [a]
nub [] = []
nub (x:xs) = x : nub (filter (/= x) xs)

nubBy :: (a -> a -> Bool) -> [a] -> [a]
nubBy f [] = []
nubBy f (x:xs) = x : nub (filter (not.f x) xs)

nubBy ослабляет зависимость от класса типов Eq, вместо этого позволяя вам определить собственную функцию равенства для фильтрации дубликатов.

Эти функции работают со списком согласованных произвольных типов (например, [1,2,"three"] не разрешен в haskell), и они оба сохраняют порядок.

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


import qualified Data.Map as Map

undup x = go x Map.empty
    where
        go [] _ = []
        go (x:xs) m case Map.lookup x m of
                         Just _  -> go xs m
                         Nothing -> go xs (Map.insert x True m)

Это прямой перевод решения @ FogleBird. К сожалению, это не работает без импорта.


Очень простой попыткой замены импорта Data.Map было бы реализовать дерево, что-то вроде этого

data Tree a = Empty
            | Node a (Tree a) (Tree a)
            deriving (Eq, Show, Read)

insert x Empty = Node x Empty Empty
insert x (Node a left right)
    | x < a = Node a (insert x left) right
    | otherwise = Node a left (insert x right)

lookup x Empty = Nothing --returning maybe type to maintain compatibility with Data.Map
lookup x (Node a left right)
    | x == a = Just x
    | x < a = lookup x left
    | otherwise = lookup x right

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


Кажется, я принимаю запросы. В ответ на запрос @Jonno_FTWs вот решение, которое полностью удаляет дубликаты из результата. Это не совсем отличается от оригинала, просто добавив дополнительный случай. Однако производительность во время выполнения будет намного медленнее, поскольку вы просматриваете каждый подсписок дважды, один раз для элемента, и второй раз для отклонения. Также обратите внимание, что теперь он не будет работать с бесконечными списками.

nub [] = []
nub (x:xs) | elem x xs = nub (filter (/=x) xs)
           | otherwise = x : nub xs

Интересно, что вам не нужно фильтровать второй рекурсивный случай, потому что elem уже обнаружил, что дубликатов нет.

7 голосов
/ 26 ноября 2009

Если порядок не имеет значения, вы можете попробовать этот алгоритм, написанный на Python:

>>> array = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6]
>>> unique = set(array)
>>> list(unique)
[1, 2, 3, 4, 5, 6]
4 голосов
/ 26 ноября 2009

В Python

>>> L = [2, 1, 4, 3, 5, 1, 2, 1, 1, 6, 5]
>>> a=[]
>>> for i in L:
...   if not i in a:
...     a.append(i)
...
>>> print a
[2, 1, 4, 3, 5, 6]
>>>
3 голосов
/ 26 ноября 2009

В Java это один лайнер.

Set set = new LinkedHashSet(list);

даст вам коллекцию с удаленными дублирующимися предметами.

2 голосов
/ 01 декабря 2009

Удаление дубликатов в списке в Python

Случай: элементы в списке не могут быть хэшируемыми или сопоставимыми

То есть мы не можем использовать set (dict) или sort.

from itertools import islice

def del_dups2(lst):
    """O(n**2) algorithm, O(1) in memory"""
    pos = 0
    for item in lst:
        if all(item != e for e in islice(lst, pos)):
            # we haven't seen `item` yet
            lst[pos] = item
            pos += 1
    del lst[pos:]

Чехол: товары хэши

Решение взято из здесь :

def del_dups(seq):
    """O(n) algorithm, O(log(n)) in memory (in theory)."""
    seen = {}
    pos = 0
    for item in seq:
        if item not in seen:
            seen[item] = True
            seq[pos] = item
            pos += 1
    del seq[pos:]

Чехол: предметы сравнимы, но не хэшируемы

То есть мы можем использовать sort. Это решение не сохраняет первоначальный порядок.

def del_dups3(lst):
    """O(n*log(n)) algorithm, O(1) memory"""
    lst.sort()
    it = iter(lst)
    for prev in it: # get the first element 
        break
    pos = 1 # start from the second element
    for item in it: 
        if item != prev: # we haven't seen `item` yet
            lst[pos] = prev = item
            pos += 1
    del lst[pos:]
2 голосов
/ 26 ноября 2009

Для Java может пойти с этим:

private static <T> void removeDuplicates(final List<T> list)
{
    final LinkedHashSet<T> set;

    set = new LinkedHashSet<T>(list); 
    list.clear(); 
    list.addAll(set);
}
1 голос
/ 18 ноября 2012

Я написал алгоритм для строки. На самом деле не имеет значения, какой у вас тип.

static string removeDuplicates(string str)
{
    if (String.IsNullOrEmpty(str) || str.Length < 2) {
        return str;
    }

    char[] arr = str.ToCharArray();
    int len = arr.Length;
    int pos = 1;

    for (int i = 1; i < len; ++i) {

        int j;

        for (j = 0; j < pos; ++j) {
            if (arr[i] == arr[j]) {
                break;
            }
        }

        if (j == pos) {
            arr[pos] = arr[i];
            ++pos;
        }
    }

    string finalStr = String.Empty;
    foreach (char c in arr.Take(pos)) {
        finalStr += c.ToString();
    }

    return finalStr;
}
1 голос
/ 28 ноября 2009

Это зависит от того, что вы подразумеваете под «эффективно». Наивным алгоритмом является O (n ^ 2), и я предполагаю, что вы на самом деле имеете в виду, что вы хотите что-то более низкого порядка, чем это.

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

superNub :: (Ord a) => [a] -> [a]
superNub xs = map snd 
              . sortBy (comparing fst) 
              . map head . groupBy ((==) `on` snd) 
              . sortBy (comparing snd) 
              . zip [1..] $ xs

Конечно, вам нужно импортировать Data.List (сортировка), Data.Function (on) и Data.Ord (сравнение). Я мог бы просто перечислить определения этих функций, но какой в ​​этом смысл?

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