【问题标题】:What's the fastest way to remove duplicates from an array in Objective-C在 Objective-C 中从数组中删除重复项的最快方法是什么
【发布时间】:2016-04-16 17:39:28
【问题描述】:

准备面试。我正在尝试通过解决以下问题来练习:给定一个 NSNumbers 的输入数组,其中一些数字是重复的,如何创建另一个仅具有原始数组中唯一值的数组。

我看到了两种方法:

  1. 蛮力:遍历数组中的每个元素,同时在一个元素处将其与唯一列表中的数字集进行比较,如果匹配,则不要存储它,否则将其添加到唯一的列表。 O(n^2) 最坏情况时间?

  2. 基于哈希表的方法:有一个长度为 N 的哈希表。has-table 的每个元素都是 NSSet。每个数字都使用散列函数映射到 0,...N-1。如果存在于“mapped-index”对应的NSSet中,则不添加到“unique array”中。如果没有,则将其添加到 set 和 unique 数组中。

这是 O(N) 复杂度吗?

  • 我查看了实现方法 2 的两种方法 A. 大小为 N 的 NSMutableArray 在开始时全部初始化为 [NSNull null] 对象。 B. NSMutableDictionary where key = hashed mapping integer

每种方法的代码如下。

我注意到 一世。对于如下所示的长度为 403 的输入数组(0.055ms vs .12ms),2A(数组方法)的运行时间是 2B(可变字典方法)的一半。
ii. 1 的运行时间比 0.25ms 差 5 倍。如果没有任何重复,这种差异就更大了。

我的问题是:

  1. 有比 2 更好的算法吗?
  2. 算法 2 有没有更好的实现方式?
  3. 为什么字典方法较慢?我如何使用 Instruments profiling 为自己回答这个问题。即我如何知道使用 Instruments 的每个步骤所用的确切时间?

代码

哈希码函数

#define NUM_BUCKETS 127
#define RANDOMIZER 11
#define NUM_ITER 40000

int hashcode(int value)
{
    int retVal = (value*RANDOMIZER)%NUM_BUCKETS ;
    if(retVal<0)
    {
        retVal+=NUM_BUCKETS ;
    }
    return retVal ;
}

1.蛮力方法

    NSMutableArray *smooshedArr=[[NSMutableArray alloc] init] ;
    double startTime ;

    startTime=CFAbsoluteTimeGetCurrent() ;
    for(int iter=0;iter<=NUM_ITER;iter++)
    {
        [smooshedArr removeAllObjects] ;
        [smooshedArr addObject:ints[0]] ;

        int i,j ;
        for(i=1;i<[ints count];i++)
        {
            for(j=0;j<[smooshedArr count];j++)
            {
                if([ints[i] intValue] == [smooshedArr[j] intValue])
                {
                    break ;
                }
            }
            if(j==[smooshedArr count])
            {
                [smooshedArr addObject:ints[i]] ;
            }
        }
    }
    NSLog(@"Bruteforce took %.3fms to remove duplicates from array of length %lu",(CFAbsoluteTimeGetCurrent()-startTime)*1000/NUM_ITER,(unsigned long)[ints count]) ;
    NSLog(@"Smooshed arary is %@",smooshedArr) ;

2A。基于数组的哈希表

    NSMutableArray *hashTable = [[NSMutableArray alloc] init] ;

    startTime=CFAbsoluteTimeGetCurrent() ;
    for(int iter=0;iter<=NUM_ITER;iter++)
    {
        [smooshedArr removeAllObjects];
        for (NSInteger i = 0; i < NUM_BUCKETS; ++i)
        {
            [hashTable addObject:[NSNull null]];
        }

        [smooshedArr addObject:ints[0]] ;

        int indexToInsert = hashcode([ints[0] intValue]) ;
        hashTable[indexToInsert]=[[NSMutableSet alloc] init] ;
        [hashTable[indexToInsert] addObject:ints[0]] ;

        int i ;
        for(i=1;i<[ints count];i++)
        {
            //Find hascode of element i
            //If the list at index = hashcode in hashCodeArary is empty, then create a NSMutableSet, set toInsert = True
            //If not empty, check if the element exists in the set. If yes, setToInsert=False. If no, setToInsert=True
            int indexToInsert = hashcode([ints[i] intValue]) ;
            BOOL toInsert=false ;

            if(hashTable[indexToInsert] == [NSNull null])
            {
                hashTable[indexToInsert]=[[NSMutableSet alloc] init] ;
                toInsert=true ;
            }
            else
            {
                if(![hashTable[indexToInsert] containsObject:ints[i]])
                    toInsert=true ;
            }
            if(toInsert)
            {
                [hashTable[indexToInsert] addObject:ints[i]] ;
                [smooshedArr addObject:ints[i]] ;
            }
        }
    }
    NSLog(@"MutableArray (no cheat) took %.3fms to remove duplicates from array of length %lu",(CFAbsoluteTimeGetCurrent()-startTime)*1000/NUM_ITER,(unsigned long)[ints count]) ;

2B。基于字典的哈希表

    NSMutableDictionary *hashDict = [[NSMutableDictionary alloc] init] ;
    //NSLog(@"Start of hashcode approach %.6f", CFAbsoluteTimeGetCurrent()) ;
    startTime=CFAbsoluteTimeGetCurrent() ;
    for(int iter=0;iter<=NUM_ITER;iter++)
    {
        //if(iter <4) NSLog(@"iter start: %.6f", CFAbsoluteTimeGetCurrent()) ;

        //if(iter <4) NSLog(@"init start: %.6f", CFAbsoluteTimeGetCurrent()) ;
          [smooshedArr removeAllObjects];
          [hashDict removeAllObjects] ;
        //if (iter<4) NSLog(@"init end: %.6f", CFAbsoluteTimeGetCurrent()) ;


        [smooshedArr addObject:ints[0]] ;

        int indexToInsert = hashcode([ints[0] intValue]) ;
        hashDict[@(indexToInsert)]=[[NSMutableSet alloc] init] ;
        [hashDict[@(indexToInsert)] addObject:ints[0]] ;

        int i ;
        for(i=1;i<[ints count];i++)
        {
            //Find hascode of element i
            //If the list at index = hashcode in hashCodeArary is empty, then create a NSMutableSet, set toInsert = True
            //If not empty, check if the element exists in the set. If yes, setToInsert=False. If no, setToInsert=True
            int indexToInsert = hashcode([ints[i] intValue]) ;
            BOOL toInsert=false ;

            if(hashDict[@(indexToInsert)] == nil)
            {
                hashDict[@(indexToInsert)]=[[NSMutableSet alloc] init] ;
                toInsert=true ;
            }
            else
            {
                if(![hashDict[@(indexToInsert)] containsObject:ints[i]])
                    toInsert=true ;
            }
            if(toInsert)
            {
                [hashDict[@(indexToInsert)] addObject:ints[i]] ;
                [smooshedArr addObject:ints[i]] ;
            }
        }
    }
    NSLog(@"Dictionary approach: %.3fms to remove duplicates from array of length %lu",(CFAbsoluteTimeGetCurrent()-startTime)*1000/NUM_ITER,(unsigned long)[ints count]) ;

输入测试开启,430 个元素有一些重复,平均超过 40000 次迭代

   NSArray *ints = @[@(2),@(3),@(4),@(1),@(6),@(9),@(2),@(2),@(3),@(21),@(22),@(45454),@(464642),@(65),@(45454),@(1234),@(3421),@(123),@(1234),@(11111),@(2727272),@(112),@(3),@(4),@(1),@(612211),@(9),@(2),@(2),@(3),@(21),@(22),@(45454),@(464642),@(65),@(45454),@(1234),@(3421),@(123),@(1234),@(11111),@(7272),@(1232),@(3),@(4),@(1),@(60),@(9),@(2),@(2),@(3),@(21),@(22),@(45454),@(464642),@(65),@(45454),@(1234),@(3421),@(123),@(1234),@(11111),@(2727272),@(2),@(3),@(4),@(1),@(6),@(9),@(2),@(2),@(3),@(21),@(22),@(45454),@(464642),@(65),@(45454),@(1234),@(3421),@(123),@(1234),@(11111),@(2727272),@(2),@(3),@(4),@(1),@(6),@(9),@(2),@(2),@(3),@(21),@(22),@(45454),@(464642),@(65),@(45454),@(1234),@(3421),@(123),@(1234),@(11111),@(72),@(2),@(3),@(4),@(1),@(6),@(9),@(2),@(2),@(3),@(21),@(22),@(45454),@(464642),@(65),@(45454),@(1234),@(3421),@(123),@(1234),@(11111),@(13272),@(2),@(3),@(4),@(18),@(6),@(9),@(2),@(2),@(3),@(21),@(22),@(45454),@(464642),@(65),@(45454),@(1234),@(3421),@(123),@(1234),@(11111),@(972),@(2),@(3),@(4),@(1),@(6),@(9),@(2),@(2),@(3),@(21),@(22),@(45454),@(464642),@(65),@(45454),@(1234),@(3421),@(123),@(1234),@(11111),@(3272),@(2),@(3),@(4),@(1),@(69),@(9),@(2),@(2),@(3),@(21),@(22),@(45454),@(464642),@(65),@(45454),@(1234),@(3421),@(123),@(1234),@(11111),@(1272),@(2),@(3),@(4),@(1),@(6),@(9),@(2),@(2),@(3),@(21),@(22),@(45454),@(464642),@(65),@(45454),@(1234),@(3421),@(123),@(1234),@(11111),@(2272),@(2),@(3),@(4),@(1),@(6),@(91),@(2),@(2),@(3),@(21),@(22),@(45454),@(464642),@(65),@(45454),@(1234),@(3421),@(123),@(1234),@(11111),@(7272),@(2),@(3),@(4),@(12),@(6),@(9),@(2),@(2),@(3),@(21),@(22),@(45454),@(464642),@(65),@(45454),@(1234),@(3421),@(123),@(1234),@(111),@(27272),@(2),@(321),@(4),@(1),@(6),@(9),@(2),@(2),@(3),@(21),@(22),@(45454),@(464642),@(65),@(4545411),@(12341),@(34210),@(123),@(1234),@(1111),@(727272),@(11187),@(9086),@(876543),@(74532),@(464642),@(65),@(45454),@(1234),@(3421),@(123),@(1234),@(11111),@(13272),@(2),@(3),@(4),@(18),@(6),@(9),@(2),@(2),@(3),@(21),@(22),@(45454),@(464642),@(658),@(45454),@(12934),@(38421),@(1243),@(12345),@(1112),@(72),@(52),@(3),@(498),@(1),@(6),@(9),@(2),@(2),@(3),@(21),@(22),@(45454),@(464642),@(65),@(45454),@(1234),@(650),@(45454),@(1234),@(3421),@(123),@(1234),@(111),@(27272),@(2),@(321),@(4),@(1),@(6),@(9),@(2),@(2),@(3),@(21),@(22),@(45454),@(464642),@(65),@(4545411),@(12341),@(34210),@(123),@(1234),@(1111),@(727272),@(11187),@(9086),@(876543),@(74532),@(464642),@(65),@(45454),@(1234),@(3421),@(123),@(1234),@(11111),@(13272),@(2),@(3),@(4),@(18),@(6),@(9),@(2),@(2),@(3),@(21),@(22),@(45454),@(464642),@(658),@(45454),@(12934),@(38421),@(1243),@(19992345),@(119875412),@(72),@(52),@(3),@(498),@(1),@(6),@(9),@(2),@(2),@(3),@(21),@(22),@(450454),@(46908764642),@(6753435),@(45498754),@(100234),@(65)] ;

【问题讨论】:

  • 为什么要使用数组?字典会更适合哈希表。
  • 谢谢@rmaddy。即使使用“NSNull null”循环,我也发现字典方法比数组方法慢。我已经编辑了我的 Q 以将完整的问题和代码放在上面。你能看看吗?
  • 如果不需要保持item的顺序,只需创建数组的NSSet即可。问题解决了。如果要保留订单,请创建 NSMutableSetNSMutableArray 来存储结果。遍历原始数组,检查项目是否在集合中,如果没有,则将其添加到集合并输出数组。 5 行代码,你无法获得更好的性能。
  • 不错的解决方案。把它减少近一半……你能找出答案让我接受吗? NSSet 下面是使用 Hashtable 吗?

标签: objective-c algorithm nsmutablearray memset array-algorithms


【解决方案1】:

如果你正在准备面试,我建议你使用已经实现的框架类。不要重新实现轮子。尝试从上到下解决问题。不要考虑细节(哈希函数),考虑算法结构:

在伪代码中:

for number in input {
   if number appears for the first time {
      add number to output
   }
}

我们唯一的问题是如何实现number appears for the first time。这是唯一对性能有影响的一点。

在 Objective-C 中,我们可以使用 NSSet,这是一个专门为这个问题创建的类。

NSArray *input = @[... array of numbers];

NSMutableSet *foundNumbers = [NSMutableSet set];
NSMutableArray *output = [NSMutableArray array];

for (NSNumber *number in input) {
    if (![foundNumbers containsObject:number])) {
       [foundNumbers addObject:number];
       [output addObject:number];
    }
}

NSLog(@"Output: %@", output);

您只需要输入数组的一次传递。提高性能的唯一方法是使用与 NSSet 不同的结构,但是 NSSet 已经高度优化,您不太可能找到更好的选择。

如果您想开箱即用并且输入中的数字被限制在足够小的范围内(例如 0...65000),您可以创建一个包含 65000 个项目的 BOOL 数组,全部初始化为 @ 987654328@ 并将其用作快速设置实现。 但是,这会占用大量内存,而且除非input 数组非常长,否则不会有回报。

绝对不要实现自己的哈希表,NSDictionary 已经是哈希表了。您在第二个实现中所做的只是对NSDictionary 的一个非常模糊的重新实现。存储桶只有在您可以将它们保留为简单数组时才起作用。一旦向其添加哈希函数,您将失去性能增益。

还要注意,代码的整体质量对于面试来说非常重要。不要使用#define 来声明常量。保持良好的编码风格(我强烈建议在运算符周围使用空格)。使用迭代器而不是 for(;;) 尝试比 hashDict 更好地命名变量(根据变量包含的数据命名变量)。

现在有个小秘密,还有一个 NSOrderedSet 类将 NSArrayNSSet 组合成一个对象,可以更轻松地解决您的问题:

NSOrderedSet *orderedSet = [NSOrderedSet orderedSetWithArray:ints];
NSLog(@"Output: %@", orderedSet);

【讨论】:

  • 感谢@Sulthan 的建议,非常感谢。您能否详细说明“(我强烈建议在运算符周围使用空格)”?您是指在 for、if 等之后添加空格吗?
  • @Sultan,你能告诉我是否有任何方法可以使用 Instruments 来了解我编写的更复杂的方法中的每个步骤需要多长时间?
  • @SmartHome 运算符为=+&lt;=== 等。这不是错误,但通常建议在二元运算符周围放置空格(1+2 变为1 + 2 等),因为它大大提高了可读性。您实际上在某些地方使用它(例如,===)。不需要使用空格,但您应该真正保持一致,并将它们放在任何地方或不放在任何地方。大多数编码标准也在关键字后放置空格(iffor),我也在这样做,但这并不像运算符周围的空格那么重要。
【解决方案2】:

实际上甚至不需要使用 NSOrderedSet —— 只需使用 NSSet 即可:

NSSet *set = [NSSet setWithArray:ints];

如果您需要一个数组作为输出,键值编码可以帮助您:

NSArray *array =  [ints valueForKeyPath:@"@distinctUnionOfObjects.self"];

【讨论】:

  • NSSet 不会保持项目的顺序。
  • @Sulthan,是的,但我在 OP 的问题中没有看到这样的要求。
【解决方案3】:

如果您不想使用额外的空间(哈希),如果数组中的数字序列无关紧要,但您仍然不想像蛮力一样慢,那么您可以对数组进行排序,然后删除一次重复。时间复杂度 nlog(n) + n

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2010-09-26
    • 1970-01-01
    • 2010-11-04
    • 2019-09-01
    • 1970-01-01
    • 2010-12-17
    • 1970-01-01
    • 2018-05-07
    相关资源
    最近更新 更多