Поиск дубликатов в несортированной последовательности эффективно
Мне нужен очень эффективный способ найти дубликаты в несортированной последовательности. Это то, что я придумал, но у него есть несколько недостатков, а именно:
- излишне подсчитывает вхождения вне 2
- потребляет всю последовательность перед получением дубликатов
- создает несколько промежуточных последовательностей
module Seq =
let duplicates items =
items
|> Seq.countBy id
|> Seq.filter (snd >> ((<) 1))
|> Seq.map fst
Независимо от недостатков, я не вижу причины заменять это дважды кодом. Можно ли улучшить это с помощью сравнительно сжатого кода?
Ответы
Ответ 1
Здесь императивное решение (которое, по общему признанию, немного длиннее):
let duplicates items =
seq {
let d = System.Collections.Generic.Dictionary()
for i in items do
match d.TryGetValue(i) with
| false,_ -> d.[i] <- false // first observance
| true,false -> d.[i] <- true; yield i // second observance
| true,true -> () // already seen at least twice
}
Ответ 2
Более элегантное функциональное решение:
let duplicates xs =
Seq.scan (fun xs x -> Set.add x xs) Set.empty xs
|> Seq.zip xs
|> Seq.choose (fun (x, xs) -> if Set.contains x xs then Some x else None)
Использует scan
для накапливания наборов всех элементов, которые видели до сих пор. Затем использует zip
для объединения каждого элемента с набором элементов перед ним. Наконец, использует choose
для фильтрации элементов, находящихся в наборе ранее просматриваемых элементов, т.е. Дубликатов.
ИЗМЕНИТЬ
На самом деле мой первоначальный ответ был совершенно неправильным. Во-первых, вы не хотите дублировать результаты. Во-вторых, вам нужна производительность.
Вот чисто функциональное решение, реализующее алгоритм, который вы выполняете:
let duplicates xs =
(Map.empty, xs)
||> Seq.scan (fun xs x ->
match Map.tryFind x xs with
| None -> Map.add x false xs
| Some false -> Map.add x true xs
| Some true -> xs)
|> Seq.zip xs
|> Seq.choose (fun (x, xs) ->
match Map.tryFind x xs with
| Some false -> Some x
| None | Some true -> None)
Это использует карту для отслеживания того, был ли каждый элемент замечен раньше одного или нескольких раз, а затем издает элемент, если он виден, только что увиденный раньше, т.е. в первый раз, когда он дублируется.
Вот более быстрая императивная версия:
let duplicates (xs: _ seq) =
seq { let d = System.Collections.Generic.Dictionary(HashIdentity.Structural)
let e = xs.GetEnumerator()
while e.MoveNext() do
let x = e.Current
let mutable seen = false
if d.TryGetValue(x, &seen) then
if not seen then
d.[x] <- true
yield x
else
d.[x] <- false }
Это примерно на 2 раза быстрее, чем любые другие ваши ответы (на момент написания).
Использование цикла for x in xs do
для перечисления элементов в последовательности существенно медленнее, чем использование GetEnumerator
напрямую, но создание собственного Enumerator
не намного быстрее, чем использование выражения вычисления с помощью yield
.
Обратите внимание, что член TryGetValue
Dictionary
позволяет мне избежать выделения во внутреннем цикле путем изменения значения выделенного стека, в то время как член расширения TryGetValue
, предложенный F # (и используемый kvb в его/ее ответе) выделяет возвращаемый кортеж.
Ответ 3
Это лучшее "функциональное" решение, которое я мог бы придумать, чтобы он не потреблял всю последовательность вперед.
let duplicates =
Seq.scan (fun (out, yielded:Set<_>, seen:Set<_>) item ->
if yielded.Contains item then
(None, yielded, seen)
else
if seen.Contains item then
(Some(item), yielded.Add item, seen.Remove item)
else
(None, yielded, seen.Add item)
) (None, Set.empty, Set.empty)
>> Seq.Choose (fun (x,_,_) -> x)
Ответ 4
Предполагая, что ваша последовательность конечна, для этого решения требуется один запуск последовательности:
open System.Collections.Generic
let duplicates items =
let dict = Dictionary()
items |> Seq.fold (fun acc item ->
match dict.TryGetValue item with
| true, 2 -> acc
| true, 1 -> dict.[item] <- 2; item::acc
| _ -> dict.[item] <- 1; acc) []
|> List.rev
Вы можете указать длину последовательности как емкость Dictionary
, но для этого требуется перечислить всю последовательность еще раз.
EDIT:
Чтобы решить вторую проблему, можно создать дубликаты по требованию:
open System.Collections.Generic
let duplicates items =
seq {
let dict = Dictionary()
for item in items do
match dict.TryGetValue item with
| true, 2 -> ()
| true, 1 -> dict.[item] <- 2; yield item
| _ -> dict.[item] <- 1
}
Ответ 5
Функциональное решение:
let duplicates items =
let test (unique, result) v =
if not(unique |> Set.contains v) then (unique |> Set.add v ,result)
elif not(result |> Set.contains v) then (unique,result |> Set.add v)
else (unique, result)
items |> Seq.fold test (Set.empty, Set.empty) |> snd |> Set.toSeq