22
$\begingroup$

As a generalization of DeleteDuplicates, I want to delete duplicates from a list, but only after n number of duplicates.

Say, n = 3 means that three duplicates are allowed.

I made my own function:

DeleteDuplicatesN[x_, n_] := 
  x[[
    Sort[
      Flatten[#[[1 ;; Min[Length[#], n]]]& /@ 
       (Flatten[Position[x, #]]& /@ DeleteDuplicates[x])]]]]

DeleteDuplicatesN[{1, 2, 3, 2, 1, 1, 1, 2, 3, 5, 5, 5, 5, 1, 7, 4, 7, 1}, 3]
{1, 2, 3, 2, 1, 1, 2, 3, 5, 5, 5, 7, 4, 7}

Is there a better method — faster or more elegant?

For example, using only DeleteDuplicates or DeleteDuplicatesBy?

$\endgroup$
0

5 Answers 5

25
$\begingroup$

I think you'll find this faster:

dd1[list_, n_] := 
  Module[{pi = Flatten[Values[PositionIndex[list][[All, ;; UpTo@n]]]]},
   list[[Sort@pi]]];

Using RandomInteger[20000, 20000] as a test list and allowing 3 duplicates, your code took ~37 seconds, this needed ~0.03 seconds.

Comparable in speed, simpler:

dd2[list_, n_] := 
  list[[Union @@ 
     GatherBy[Range@Length@list, list[[#]] &][[All, ;; UpTo@n]]]];

For large lists that aren't grossly filled with duplicated elements, this offers a performance edge (e.g., with RandomInteger[10000000,20000000] test list, over 6X speed of above methods):

dd=Module[{o = Ordering@#},
 o[[o]] = Join @@ Range[Tally[#[[o]]][[All, 2]]];
 Pick[#, UnitStep[#2 - o], 1]]&;
$\endgroup$
8
  • $\begingroup$ Nice. If nobody comes with a better code in a day or so I will accept your answer. $\endgroup$ Commented Aug 19, 2020 at 22:47
  • 1
    $\begingroup$ We can use Sort[pi] instead of pi[[Ordering[pi]]]. If I am not mistaken it is the same speed but more transparent code. $\endgroup$ Commented Aug 19, 2020 at 23:08
  • $\begingroup$ @azerbajdzan - yes, that function was snipped from some code that used the ordering later. I've made the readability change. $\endgroup$
    – ciao
    Commented Aug 19, 2020 at 23:16
  • 1
    $\begingroup$ Maybe this is a good one for the Function Repository? $\endgroup$ Commented Aug 20, 2020 at 8:13
  • 2
    $\begingroup$ @azerbajdzan - ? It is written as a pure function - the slot are the arguments. It is called the same way as the other examples. $\endgroup$
    – ciao
    Commented Aug 20, 2020 at 19:15
3
$\begingroup$
list = {1, 2, 3, 2, 1, 1, 1, 2, 3, 5, 5, 5, 5, 1, 7, 4, 7, 1};

A variant of the first accepted answer:

list[[Union @@ PositionIndex[list][[All, ;; UpTo[3]]]]] // RepeatedTiming

(*{0.0000210309, {1, 2, 3, 2, 1, 1, 2, 3, 5, 5, 5, 7, 4, 7}}*)

list[[Sort@
    Flatten[Values[
      PositionIndex[list][[All, ;; UpTo[3]]]]]]] // RepeatedTiming

(*{0.0000263463, {1, 2, 3, 2, 1, 1, 2, 3, 5, 5, 5, 7, 4, 7}}*)

Another variant using Tally:

list[[Union @@ Tally[PositionIndex[list]][[All, 1, ;; UpTo[3]]]]]

(*{1, 2, 3, 2, 1, 1, 2, 3, 5, 5, 5, 7, 4, 7}*)
$\endgroup$
3
$\begingroup$

or

DeleteDuplicatesN[list_, n_] := Module[{counts},
  counts = <||>;
  
  First@Last@Reap[
    Scan[
      (counts[#] = Lookup[counts, #, 0] + 1;
       If[counts[#] <= n, Sow[#]]) &,
      list
    ]
  ]
]

list = {1, 2, 3, 2, 1, 1, 1, 2, 3, 5, 5, 5, 5, 1, 7, 4, 7, 1};
result = DeleteDuplicatesN[list, 3]
$\endgroup$
3
$\begingroup$

Below are one original algo, one basically re-shuffled dd1 from @ciao and one based on dd from @ciao.

Data:

list0 = {1, 2, 3, 2, 1, 1, 1, 2, 3, 5, 5, 5, 5, 1, 7, 4, 7, 1};
list1 =RandomInteger[20000, 20000];
list2 = RandomInteger[10000000, 20000000];
list3 = RandomSample[Table[Table[#, RandomInteger[{1, 10}]] &@RandomInteger[{1, 100},RandomInteger[{1, 100}]], 1000] // Flatten];

Proposed functions:

ddn1[list_,n_]:=list[[Sort@Flatten[Nearest[list->"Index",DeleteDuplicates@list,{All,0.}][[All,;;UpTo@n]]]]];
ddn2[list_,n_]:=list[[Sort@Flatten[Values[PositionIndex[list]][[All,;;UpTo@n]]]]];(* based on dd1 from @ciao *)
ddd=Module[{o=Ordering@#,t},t=Tally[#[[o]]][[All,2]];o[[o]]=Join@@(Range@Range[Max@t])[[t]];Delete[#,Partition[Pick[Range@Length@#,UnitStep[#2-o],0],1]]]&;(* optimized dd from @ciao + Delete picked positions instead of Picking the list *)

For short lists (list0):

enter image description here

For medium lists (list1):

enter image description here

For large lists (list2):

enter image description here

For special case lists proposed by @azerbajdzan (list3):

enter image description here

Bottomline:

For small lists, surprisingly, ddn2 is a bit faster than @ciao's dd1, though they are basically identical.

For medium and large lists, ddd, which is based on dd by @ciao with optimized Range and employs Delete[list, Pick@Range] instead of Pick@list, is times faster than the original dd.

For special lists like proposed by @azerbajdzan, ddn1 seems to be a bit faster than dd2 by @ciao.

$\endgroup$
3
  • 1
    $\begingroup$ Depends on data you were testing on. Try on RandomSample[ Table[Table[#, RandomInteger[{1, 10}]] &@ RandomInteger[{1, 100}, RandomInteger[{1, 100}]], 1000] // Flatten]; Your code is much slower. $\endgroup$ Commented Nov 13 at 9:15
  • $\begingroup$ @azerbajdzan it's much faster now. $\endgroup$
    – Anton
    Commented Nov 14 at 8:30
  • 1
    $\begingroup$ +1 for revisiting this interesting problem. $\endgroup$
    – ciao
    Commented Nov 14 at 22:21
2
$\begingroup$
list = {1, 2, 3, 2, 1, 1, 1, 2, 3, 5, 5, 5, 5, 1, 7, 4, 7, 1};

Using DeleteElements (new in 13.1)

p = Rule @@ Reverse /@ Cases[Tally @ list + Threaded[{0, -3}], {_, _?Positive}]

{3, 1} -> {1, 5}

Delete 3 ones and 1 five

Reverse @ DeleteElements[Reverse @ list, p]

{1, 2, 3, 2, 1, 1, 2, 3, 5, 5, 5, 7, 4, 7}

Since DeleteElements works from left to right we have to Reverse twice

$\endgroup$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.