【问题标题】:Algorithm - How to delete duplicate elements in a list efficiently?算法 - 如何有效地删除列表中的重复元素?
【发布时间】:2010-12-20 13:30:49
【问题描述】:

有一个列表L。它包含任意类型的元素。 如何有效地删除此类列表中的所有重复元素? 必须保留订单

只需要一个算法,所以不允许导入任何外部库。

相关问题

【问题讨论】:

  • 这在 Haskell 中是不可能的,因为只有类型才能以大写字母 =P 开头。
  • Data.List.nub 效率不高。

标签: java c++ python algorithm haskell


【解决方案1】:

接近公认答案的通用解决方案

k = ['apple', 'orange', 'orange', 'grapes', 'apple', 'apple', 'apple']
m = []


def remove_duplicates(k):
    for i in range(len(k)):
        for j in range(i, len(k)-1):
            if k[i] == k[j+1]:
                m.append(j+1)

    l = list(dict.fromkeys(m))
    l.sort(reverse=True)

    for i in l:
        k.pop(i)

    return k


print(remove_duplicates(k))

【讨论】:

    【解决方案2】:

    我已经为字符串编写了一个算法。其实你有什么类型并不重要。

    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;
    }
    

    【讨论】:

      【解决方案3】:

      算法 delete_duplicates (a[1....n])

      //从给定数组中删除重复项

      //输入参数:a[1:n],n个元素的数组

      {

      temp[1:n];//n 个元素的数组

       temp[i]=a[i];for i=1 to n
      
           temp[i].value=a[i]
      
              temp[i].key=i
      

      *//根据'value'对数组temp进行排序。*

      //基于 'value' 从 temp 中删除重复元素。

      //基于'key'对数组temp进行排序。//使用temp构造一个数组p。

      p[i]=temp[i].value
      
      return p
      

      使用“键”在输出数组中维护其他元素。考虑键的长度为 O(n),对键和值执行排序所需的时间为 O(nlogn)。所以从数组中删除所有重复项所花费的时间是 O(nlogn)。

      【讨论】:

        【解决方案4】:

        我的 Java 代码:

        ArrayList<Integer> list = new ArrayList<Integer>();
        
        list.addAll({1,2,1,3,4,5,2,3,4,3});
        
        for (int i=0; i<list.size(); i++)
        {
            for (int j=i+1; j<list.size(); j++)
            {
                if (list.get(i) == list.get(j))
                {
                    list.remove(i);
                    j--;
                }
            }
        }
        

        或者干脆这样做:

        SetList<Integer> unique = new SetList<Integer>();
        
        unique.addAll(list);
        

        两种方式都有 Time = nk ~ O(n^2)

        其中 n 是输入列表的大小,

        k 是输入列表的唯一成员数

        【讨论】:

          【解决方案5】:

          在 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:]
          

          案例:项目是可散列的

          解决方案取自here

          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:]
          

          【讨论】:

            【解决方案6】:

            在 haskell 中,这将被 nubnubBy 函数覆盖

            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 查询的回应,这里提供了一个从结果中完全删除重复项的解决方案。它与原版并不完全不同,只是添加了一个额外的案例。但是,运行时性能会慢得多,因为您要遍历每个子列表两次,一次用于 elem,第二次用于 recusion。另请注意,现在它不适用于无限列表。

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

            有趣的是,您不需要过滤第二个递归案例,因为 elem 已经检测到没有重复项。

            【讨论】:

            • 在旁注中,你如何修改nub 来删除这两个元素,如果它们是重复的,即。 [1,2,2,3] -&gt; [1,3] ?
            【解决方案7】:

            这取决于您所说的“有效”。天真的算法是 O(n^2),我假设你的实际意思是你想要比这更低阶的东西。

            正如 Maxim100 所说,您可以通过将列表与一系列数字配对来保留顺序,使用您喜欢的任何算法,然后将其余部分恢复为原始顺序。在 Haskell 中,它看起来像这样:

            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(上)和Data.Ord(比较)。我可以背诵这些函数的定义,但有什么意义呢?

            【讨论】:

            【解决方案8】:

            也许您应该首先考虑使用关联数组(在 python 中也称为 dict)以避免重复元素。

            【讨论】:

              【解决方案9】:

              特例:散列和等式

              首先,我们需要确定一些关于假设的事情,即是否存在等式和具有函数关系。我这是什么意思?我的意思是,对于源对象集 S,给定作为 S 元素的任意两个对象 x1 和 x2,存在一个(散列)函数 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() 返回一个整数。如果您有 16 个存储桶,您可以通过将 hashCode 与 15 进行“与”运算来找到要使用的存储桶,得到一个从 0 到 15 的数字。

              当你尝试往那个桶里放东西时,它可能已经被占用了。如果是这样,那么将对该存储桶中的所有条目进行线性比较。如果碰撞率变得太高或者你试图在结构中放置太多元素,那么结构将会增长,通常会增加一倍(但始终是 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(n2)。

              显然这是一个极端的例子,但有必要指出这种机制还依赖于映射或集合使用的值空间内相对良好的哈希分布。

              最后,必须说这是个特例。如果您使用的语言没有这种“散列快捷方式”,那就另当别论了。

              一般情况:无订购

              如果列表不存在排序函数,那么您将陷入对每个对象与每个其他对象的 O(n2) 蛮力比较。所以在 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;
              }
              

              注意:以上示例假定列表中没有空值。

              【讨论】:

              • FogleBird给出的方法是O(n),因为e in SS.addM.append都是O(1)
              • 而且仅供参考,我提到了 O(1) 案例(对于 Java),但是,就像在 Python 中一样,它是基于存在相等哈希码关系的假设,这很好,但事实并非如此一般情况。
              • 我正要根据你的第一句话“如果没有排序你坚持 O(n^2)”b/c 你可以用哈希表解决它。然后我看到了你关于 HashSet 的 ArrayList 的最后一节,好吧,你去吧。也许downvoters 没有阅读您的全部回复...?
              • 您的'一般情况:订购'解决方案不保留原始订单(OP 要求)。顺便说一句,prev = item 可以提升到if 套件。
              • 澄清一下,“接近 O(1)”是什么意思:一个存储桶可能包含多个条目,但通过适当调整大小,可以将负载因子保持在给定阈值以下,例如。 0.7,给出 O(1) 预期或平均查找时间。由于调整大小,插入和删除时间可能是 O(n) 最坏的情况,但是使用上述加倍策略,它将在 O(1) 摊销时间内运行,这意味着一串 n 插入/删除将在 O(n ) 时间,即使每个单独的操作可能不是 O(1)。
              【解决方案10】:

              在java中,它是一个单行。

              Set set = new LinkedHashSet(list);
              

              会给你一个删除重复项目的集合。

              【讨论】:

              • 虽然不是所要求的...但您不会得到相同的 List 对象减去重复项。
              • @TofuBeer:虽然有提示。
              • 以防其他人像我一样感到困惑:TofuBeer 在彼得编辑答案以使用 LinkedHashSet 而不是原始 HashSet 之前发表了该评论。
              • 鉴于列表仍然包含重复项,它仍然不是“正确的”......:-P
              • 当然,但由于提问者还要求 Haskell,其中可变数据的形式非常糟糕,我不确定应该如何认真对待“要求”。您可以将“删除某些成员”表示“改变原始成员”,也可以将其表示“创建一个不包括某些元素的新容器”。但是,即使在后一种情况下,您也应该得到一个 List,而这段代码并没有这样做。因此,如果这是一项学校作业,它会失败,但如果问题的核心是“如何在不破坏顺序的情况下在 Java 中唯一化顺序数据?”则通过。
              【解决方案11】:
              • 遍历列表并为每个项目分配顺序索引
              • 根据元素的一些比较函数对列表进行排序
              • 删除重复项
              • 根据分配的索引对列表进行排序

              为简单起见,项目的索引可以存储在 std::map 之类的东西中

              如果我没有错过任何东西,看起来像 O(n*log n)

              【讨论】:

                【解决方案12】:

                Python 中的单行解决方案
                使用列表理解:

                >>> L = [2, 1, 4, 3, 5, 1, 2, 1, 1, 6, 5]
                >>> M = []
                >>> zip(*[(e,M.append(e)) for e in L if not e in M])[0]
                (2, 1, 4, 3, 5, 6)
                

                【讨论】:

                • 如果你把它放在你原来的帖子中说你找到了解决方案会更好,因为这个问题是你首先提出的
                • [(M.append(e) or e) for e in L if e not in M] 不那么丑陋,并且与'zip' 变体具有相同的效率(O(n**2))。它适用于您不能使用setsort 即几乎从不使用的情况。
                • 实际上M 包含结果,因此如果您必须在一行中执行,则:collections.deque((M.append(e) for e in L if e not in M), maxlen=0)。在这里,我使用了 itertools 配方:consume = lambda it: deque(it, maxlen=0) 它执行迭代,直到迭代器耗尽。最终结果在M 列表中。它使用一半的内存但时间效率相同O(n**2).
                【解决方案13】:

                在 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]
                >>>
                

                【讨论】:

                • 这是@FogleBird 的复制粘贴,不是吗?
                • 只有数据L。你看不到吗?我没有使用集合,只是普通的列表附加。
                【解决方案14】:

                对于 Java 可以这样:

                private static <T> void removeDuplicates(final List<T> list)
                {
                    final LinkedHashSet<T> set;
                
                    set = new LinkedHashSet<T>(list); 
                    list.clear(); 
                    list.addAll(set);
                }
                

                【讨论】:

                  【解决方案15】:

                  如果顺序无关紧要,您可能想试试这个用 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]
                  

                  【讨论】:

                    【解决方案16】:

                    假设顺序很重要:

                    • 创建一个空集 S 和一个空列表 M。
                    • 一次扫描列表 L 个元素。
                    • 如果元素在集合 S 中,则跳过它。
                    • 否则,将其添加到 M 和 S。
                    • 对 L 中的所有元素重复。
                    • 返回 M.

                    在 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))
                    

                    【讨论】:

                    • 在您的第一个解决方案中,集合 S 不是必需的。如果 L M 中尚未包含 M 中的元素,您应该能够附加它们。这样做无需其他数据结构即可完成相同的操作。
                    • 集合 S 是使该算法 O(n*log(n)) 而不是 O(n^2) 所必需的。在列表中搜索元素是 O(n),但在 Set 中是 O(1)。
                    • 如果某些元素不可散列怎么办?
                    • 如果元素不可散列,那么您可以使用搜索树(如在 STL 中)实现您的集合,算法将是 O(n*log n)。
                    • 为了使树解决方案起作用,元素必须相互比较。只有“朴素”的 n^2 算法只需要相等性测试,这是对任何唯一性问题的最低假设。 (顺便问一下,问题的措辞是否暗示了作业问题?)
                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2018-08-12
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2017-09-03
                    相关资源
                    最近更新 更多