按属性值从数组中删除项目

Posted

技术标签:

【中文标题】按属性值从数组中删除项目【英文标题】:Removing item from array by property value 【发布时间】:2013-10-01 04:19:54 【问题描述】:

我正在寻找最有效和记忆友好的方式。

假设我有一个 Person 对象数组。每个人都有一个由NSString 表示的头发颜色。然后假设我想从数组中删除所有头发颜色为棕色的 Person 对象。

我该怎么做?

请记住,您不能从正在枚举的数组中删除对象。

【问题讨论】:

它们是否已排序?有重复吗?这是一个理论问题,还是您的代码存在特定的性能问题?如果是后者,您能否提供更多细节?另外,需要明确的是,您具体指的是对现有的 NSMutableArray 进行变异,而不是生成一个不包含原始中所有棕色头发的人的新数组 - 对吗? 【参考方案1】:

有两种通用方法。我们可以对每个元素进行测试,如果满足测试条件则立即删除该元素,或者我们可以测试每个元素并存储满足测试条件的元素的索引,然后一次性删除所有此类元素。由于内存使用是一个真正的问题,后一种方法的存储要求可能会使其不受欢迎。

对于“存储所有索引以删除,然后删除它们”的方法,我们需要考虑前一种方法所涉及的细节,以及它们将如何影响该方法的正确性和速度。这种方法有两个致命错误在等待。第一种是不根据其在数组中的索引删除评估对象,而是使用removeObject: 方法。 removeObject: 对数组进行线性搜索以找到要删除的对象。对于一个大的、未排序的数据集,随着时间随着输入大小的平方而增加,这将破坏我们的性能。顺便说一句,使用indexOfObject: 然后removeObjectAtIndex: 一样糟糕,所以我们也应该避免它。第二个致命错误是在索引 0 处开始我们的迭代。NSMutableArray 在添加或删除对象后重新排列索引,因此如果我们从索引 0 开始,即使有一个对象,我们也会保证索引越界异常在迭代过程中删除。所以,我们必须从数组的后面开始,只删除索引低于我们目前检查过的每个索引的对象。

考虑到这一点,实际上有两个明显的选择:for 循环从数组的末尾而不是开头开始,或者 NSArray 方法 enumerateObjectsWithOptions:usingBlock: 方法。每个示例如下:

[persons enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(Person *p, NSUInteger index, BOOL *stop) 
    if ([p.hairColor isEqualToString:@"brown"]) 
        [persons removeObjectAtIndex:index];
    
];

NSInteger count = [persons count];
for (NSInteger index = (count - 1); index >= 0; index--) 
    Person *p = persons[index];
    if ([p.hairColor isEqualToString:@"brown"]) 
        [persons removeObjectAtIndex:index];
    

我的测试似乎显示for 循环稍微快一些——对于 500,000 个元素,可能快了大约四分之一秒,基本上是 8.5 秒和 8.25 秒之间的差异。所以我建议使用块方法,因为它更安全,感觉更习惯。

【讨论】:

根据这家伙的说法,枚举可能很慢:darkdust.net/writings/objective-c/… @ChoppinBroccoli 我阅读这些结果的方式是,块枚举非常快。你具体指的是什么?不过,对于 for 循环更快,我可能是错的 - 当我写那部分时我真的很累,今天早上想知道我是否没有转置我的数字。我将再次运行它们并编辑我的帖子。 其实我觉得你是对的。我没有看块枚举,只是常规枚举。 将性能与 removeObjectsAtIndexes:filteredArrayUsingPredicate: 进行比较会很有趣,请参阅下面的答案 @PierreHouston 针对indexesOfObjectsPassingTestremoveObjectsAtIndexes 进行了测试,@Carl 的答案是最好的。 :]【参考方案2】:

假设您正在处理一个可变数组并且它没有排序/索引(即您必须扫描数组),您可以使用 enumerateObjectsWithOptionsNSEnumerationReverse 选项以相反的顺序遍历数组:

[array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) 
    // now you can remove the object without affecting the enumeration
];

通过逆序,你可以从被枚举的数组中移除一个对象。

【讨论】:

【参考方案3】:
NSMutableArray * tempArray = [self.peopleArray mutableCopy];

for (Person * person in peopleArray)

 if ([person.hair isEqualToString: @"Brown Hair"])
     [tempArray removeObject: person]



self.peopleArray = tempArray;

或者 NSPredicate 也可以:http://nshipster.com/nspredicate/

【讨论】:

由于调用 removeObject: 需要对数组进行线性搜索,这将在更大的数据集上真正变慢。【参考方案4】:

关键是使用谓词过滤数组。请看下面的代码;

- (NSArray*)filterArray:(NSArray*)list

    return  [list filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings)
        People *currentObj = (People*)evaluatedObject;
        return (![currentObj.hairColour isEqualToString:@"brown"]);
    ]];

【讨论】:

如果你要使用谓词...[NSPredicate predicateWithFormat:@"hairColour == %@", hairColour]【参考方案5】:

试试这样,

        NSIndexSet *indices = [personsArray indexesOfObjectsPassingTest:^(id obj, NSUInteger idx, BOOL *stop) 
            return [[obj objectForKey:@"hair"] isEqual:@"Brown Hair"];
        ];
         NSArray *filtered = [personsArray objectsAtIndexes:indices];

        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF.hair=%@ ",@"Brown Hair"];
        NSArray*   myArray = [personsArray filteredArrayUsingPredicate:predicate];
        NSLog(@"%@",myArray);

【讨论】:

第一个例子效率最高。 Objc.io 的人做了一个比较,发现基于谓词的排序很慢。如果您将第一个示例调整为使用 indexOfObjectsWithOptions:passingTest: 并在选项中发送 NSEnumerationConcurrent 您将获得最佳性能。【参考方案6】:

如果您要制作过滤掉某些项目的数组副本,则制作一个新的可变数组,迭代原始数组并动态添加到副本中,正如其他人对此答案所建议的那样。但是您的问题是从现有(可能是可变的)数组中删除。

在迭代时,您可以构建一个要删除的对象数组,然后再删除它们:

NSMutableArray *thePeople = ...
NSString *hairColorToMatch = ...

NSMutableArray *matchingObjects = [NSMutableArray array];
for (People *person in thePeople) 
  if (person.hairColor isEqualToString:hairColorToMatch])
    [matchingObjects addObject:person];
[thePeople removeObjects:matchingObjects];

但这会创建一个临时数组,您可能认为这很浪费,更重要的是,很难看到removeObjects: 非常有效。另外,有人提到了一些关于具有重复项的数组,这在这种情况下应该可以工作,但不是最好的,每个重复项也在临时数组中,并且在removeObjects: 中进行冗余匹配。

可以改为按索引进行迭代并在进行时删除,但这会使循环逻辑相当尴尬。相反,我会收集索引集中的索引,然后再次删除:

NSMutableIndexSet *matchingIndexes = [NSMutableIndexSet indexSet];
for (NSUInteger n = thePeople.count, i = 0; i < n; ++i) 
  People *person = thePeople[i];
  if ([person.hairColor isEqualToString:hairColorToMatch])
    [matchingIndexes addIndex:i];

[thePeople removeObjectsAtIndexes:matchingIndexes];

我相信索引集的开销非常低,因此这几乎与您将获得的效率一样高,而且很难搞砸。像这样在最后批量删除的另一件事是,Apple 可能已将 removeObjectsAtIndexes: 优化为优于 removeObjectAtIndex: 的序列。因此,即使有创建索引集数据结构的开销,这也可能会在迭代时快速删除。如果数组有重复,这个也很好用。

如果相反,您确实在制作过滤副本,那么我认为您可以使用一些 KVC 集合运算符(我最近正在阅读这些内容,您可以根据 NSHipster 和 @ 做一些疯狂的事情987654322@)。显然没有,但很接近,需要在这个有点罗嗦的行中使用 KVC NSPredicate:

NSArray *subsetOfPeople = [allPeople filteredArrayUsingPredicate:
    [NSPredicate predicateWithFormat:@"SELF.hairColor != %@", hairColorToMatch]];

请继续在 NSArray 上创建一个类别,以使您的代码更简洁,filterWithFormat: 或其他内容。

(全部未经测试,直接输入SO)

【讨论】:

而不是 for 循环来生成索引,使用enumerateObjectsWithOptions:usingBlock: 可能会更好。由于数组未在块内修改,因此不需要NSEnumerationReverse,可以使用NSEnumerationConcurrent【参考方案7】:
NSMutableArray *arrayForStuff = ...

[arrayForStuff removeObjectAtIndex:[arrayForStuff indexOfObject:objectToRemove]];

【讨论】:

只有代码的答案几乎总是可以通过添加解释来改进。当用六个其他答案回答一个六年前的问题时,必须说明您的答案所针对的问题的新方面,并说明时间的流逝是否改变了答案。

以上是关于按属性值从数组中删除项目的主要内容,如果未能解决你的问题,请参考以下文章

TypeScript - 根据属性值从数组中取出对象

根据JavaScript中的属性值从对象数组中选择[重复]

根据值从对象数组中选择一个属性:Javascript

通过属性值从对象数组中获取JavaScript对象[重复]

通过属性值从对象数组中获取JavaScript对象[重复]

javascript 按属性从数组中删除对象