迭代时从NSMutableArray中删除的最佳方法?


194

在可可中,如果我想遍历NSMutableArray并删除多个符合特定条件的对象,那么在每次删除对象时都无需重新启动循环的最佳方法是什么?

谢谢,

编辑:只是为了澄清-我一直在寻找最佳方法,例如,比手动更新索引更优雅的方法。例如在C ++中,我可以做到;

iterator it = someList.begin();

while (it != someList.end())
{
    if (shouldRemove(it))   
        it = someList.erase(it);
}

9
从后到前循环。
2013年

没有人回答“为什么”
onmyway133'9

1
@HotLicks我一直以来最喜欢的应用程序之一,也是一般编程中最被低估的解决方案:D
朱利安·F·韦纳特

Answers:


388

为了清楚起见,我希望创建一个初始循环,在其中收集要删除的项目。然后我删除它们。这是使用Objective-C 2.0语法的示例:

NSMutableArray *discardedItems = [NSMutableArray array];

for (SomeObjectClass *item in originalArrayOfItems) {
    if ([item shouldBeDiscarded])
        [discardedItems addObject:item];
}

[originalArrayOfItems removeObjectsInArray:discardedItems];

毫无疑问,索引是否被正确更新,或者其他小的簿记细节。

编辑添加:

在其他答案中已经指出,逆公式应该更快。即,如果您遍历该数组并组成一个要保留的对象而不是要丢弃的对象的新数组。这可能是正确的(尽管分配一个新数组并丢弃旧数组的内存和处理成本如何?),但即使更快,它也可能不如天真的实现那么重要,因为NSArrays表现不像“普通”数组。他们说话,但走的路却不同。在这里查看良好的分析:

逆公式可能更快,但是我从来不需要关心它是否是反斜,因为上面的公式一直足够快以满足我的需求。

对我来说,要传达的信息是使用您最清楚的任何表达方式。仅在必要时进行优化。我个人认为上述配方最清晰,这就是为什么要使用它。但是,如果您对逆公式更清楚,那就继续吧。


47
请注意,如果数组中的对象不止一次,这可能会产生错误。或者,您可以使用NSMutableIndexSet和-(void)removeObjectsAtIndexes。
GeorgSchölly09年

1
实际上,进行逆运算会更快。性能差异非常大,但是如果您的阵列不是那么大,那么无论哪种方式,时间都将变得微不足道。
user1032657 2013年

我使用了revers ...将数组设为nil ..然后仅添加了我想要的内容并重新加载了数据...完成了... 创建了dummyArray这是主数组的镜像,这样我们就可以得到主要的实际数据
Fahim Parkar

需要分配额外的内存来进行逆运算。它不是就地算法,在某些情况下也不是一个好主意。当直接删除数组时,您只需移动数组(这非常有效,因为它不会一步一步移动,它可以移动很大的块),但是当您创建一个新数组时,您需要所有分配和赋值。因此,如果您仅删除一些项目,则反向操作会慢得多。这是一种典型的似乎正确的算法。
杰克

82

另一种变化。这样您将获得可读性和良好的性能:

NSMutableIndexSet *discardedItems = [NSMutableIndexSet indexSet];
SomeObjectClass *item;
NSUInteger index = 0;

for (item in originalArrayOfItems) {
    if ([item shouldBeDiscarded])
        [discardedItems addIndex:index];
    index++;
}

[originalArrayOfItems removeObjectsAtIndexes:discardedItems];

这太棒了。我试图从数组中删除多个项目,这很正常。其他方法正在引起问题:)谢谢您
AbhinavVinay 2013年

科里,这个回答(如下)说,removeObjectsAtIndexes删除对象是最糟糕的方法,您是否同意?我之所以这样问是因为您的答案现在太旧了。选择最好的还是好吗?
Hemang

就可读性而言,我非常喜欢此解决方案。与@HotLicks答案相比,性能如何?
Victor MaiaAldecôa2014年

从Obj-C中的数组删除对象时,绝对应鼓励使用此方法。
北极熊

enumerateObjectsUsingBlock:会免费为您提供索引增量。
pkamb

42

这是一个非常简单的问题。您只是向后迭代:

for (NSInteger i = array.count - 1; i >= 0; i--) {
   ElementType* element = array[i];
   if ([element shouldBeRemoved]) {
       [array removeObjectAtIndex:i];
   }
}

这是非常常见的模式。


会错过Jens的答案,因为他没有写出代码:P .. thnx m8
FlowUI。SimpleUITesting.com

3
这是最好的方法。重要的是要记住,尽管Objective-C中的数组索引被声明为无符号,但迭代变量应为有符号变量(我可以想象苹果现在对此感到后悔)。
mojuba '16

39

其他一些答案在非常大的数组上的性能会很差,这是因为方法类似removeObject:并且removeObjectsInArray:涉及对接收器进行线性搜索,这很浪费,因为您已经知道对象在哪里。此外,任何对removeObjectAtIndex:都必须一次将索引中的值复制到数组的末尾,最多一次复制一个插槽。

以下是更有效的方法:

NSMutableArray *array = ...
NSMutableArray *itemsToKeep = [NSMutableArray arrayWithCapacity:[array count]];
for (id object in array) {
    if (! shouldRemove(object)) {
        [itemsToKeep addObject:object];
    }
}
[array setArray:itemsToKeep];

由于我们设置了的容量itemsToKeep,因此我们不会在调整大小时浪费任何时间来复制值。我们没有在适当的地方修改数组,因此我们可以自由使用快速枚举。使用setArray:替换arraywith 的内容itemsToKeep将非常有效。根据您的代码,您甚至可以将最后一行替换为:

[array release];
array = [itemsToKeep retain];

因此,甚至不需要复制值,只需交换一个指针。


就时间复杂度而言,这种算法被证明是很好的。我试图从空间复杂性的角度来理解它。我有相同的实现方式,在这里我用实际的“数组计数”设置了“ itemsToKeep”的容量。但是说我不想从数组中删除几个对象,所以不要将其添加到itemsToKeep中。因此,itemsToKeep的容量为10,但实际上我只存储了6个对象。这是否意味着我在浪费4个对象的空间?PS并没有发现错误,只是试图了解算法的复杂性。:)
tech_human 2014年

是的,您保留了不使用的四个指针的空间。请记住,数组仅包含指针,而不包含对象本身,因此对于四个对象,这意味着16字节(32位体系结构)或32字节(64位)。
benzado 2014年

请注意,任何一种解决方案最多都需要0个额外的空间(就地删除项目),或者最坏的情况是原始数组大小的两倍(因为要复制数组)。同样,由于我们处理的是指针而不是对象的完整副本,因此在大多数情况下,这是非常便宜的。
benzado 2014年

好,知道了。谢谢奔驰!!
tech_human14年

根据这篇文章,关于数组大小的arrayWithCapacity:方法的提示实际上并未使用。
克雷姆克

28

您可以使用NSpredicate从可变数组中删除项目。这不需要for循环。

例如,如果您具有名称的NSMutableArray,则可以创建这样的谓词:

NSPredicate *caseInsensitiveBNames = 
[NSPredicate predicateWithFormat:@"SELF beginswith[c] 'b'"];

下一行将为您提供一个仅包含以b开头的名称的数组。

[namesArray filterUsingPredicate:caseInsensitiveBNames];

如果您在创建所需谓词时遇到困难,请使用此apple开发人员链接


3
不需要可见的循环。筛选器操作中有一个循环,您只是看不到它。
热舔

18

我使用4种不同的方法进行了性能测试。每个测试都会遍历100,000个元素数组中的所有元素,并每5个元素删除一次。有/没有优化,结果变化不大。这些都是在iPad 4上完成的:

(1)removeObjectAtIndex:- 271毫秒

(2)removeObjectsAtIndexes:- 1010毫秒(因为建索引集取〜700毫秒;否则这基本上是与调用removeObjectAtIndex:针对每个项目)

(3)removeObjects:- 326毫秒

(4)使用通过测试的对象创建一个新数组- -17毫秒

因此,创建新阵列是迄今为止最快的。其他方法都是可比较的,除了使用removeObjectsAtIndexes:将要删除的更多项目会更糟之外,因为建立索引集需要时间。


1
您是否花了时间创建一个新数组并随后对其进行分配。我不相信它可以在17毫秒内单独完成。再加上75,000个作业?
杰克

创建和解除分配一个新的阵列所需的时间是最小的
user1032657

您显然没有测量反向循环方案。
2015年

第一个测试等效于反向环路方案。
user1032657

当您留下newArray并且prevArray为空时会发生什么?您必须将newArray复制到PrevArray的名称(或重命名),否则其他代码将如何引用它?
aremvee

17

使用循环递减索引:

for (NSInteger i = array.count - 1; i >= 0; --i) {

或使用您要保留的对象制作副本。

特别是,请勿使用for (id object in array)循环或NSEnumerator


3
您应该用代码m8写下答案。哈哈几乎错过了。
FlowUI。SimpleUITesting.com

这是不对的。“ for(数组中的id对象)”比“ for(NSInteger i = array.count-1; i> = 0; --i)”快,被称为快速迭代。使用迭代器绝对比索引快。
杰克

@jack-但是,如果从迭代器下删除元素,通常会造成破坏。(而且“快速迭代”并不总是那么快。)
Hot Licks

答案是从2008年开始。不存在快速迭代。
延斯·艾顿2014年

12

对于iOS 4+或OS X 10.6 +,Apple在中添加了passingTest一系列API NSMutableArray,例如– indexesOfObjectsPassingTest:。使用此类API的解决方案是:

NSIndexSet *indexesToBeRemoved = [someList indexesOfObjectsPassingTest:
    ^BOOL(id obj, NSUInteger idx, BOOL *stop) {
    return [self shouldRemove:obj];
}];
[someList removeObjectsAtIndexes:indexesToBeRemoved];

12

现在,您可以使用基于块的反向枚举。一个简单的示例代码:

NSMutableArray *array = [@[@{@"name": @"a", @"shouldDelete": @(YES)},
                           @{@"name": @"b", @"shouldDelete": @(NO)},
                           @{@"name": @"c", @"shouldDelete": @(YES)},
                           @{@"name": @"d", @"shouldDelete": @(NO)}] mutableCopy];

[array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    if([obj[@"shouldDelete"] boolValue])
        [array removeObjectAtIndex:idx];
}];

结果:

(
    {
        name = b;
        shouldDelete = 0;
    },
    {
        name = d;
        shouldDelete = 0;
    }
)

仅有一行代码的另一种选择:

[array filterUsingPredicate:[NSPredicate predicateWithFormat:@"shouldDelete == NO"]];

是否可以保证不会重新分配数组?
Cfr

重新分配是什么意思?
vikingosegundo

从线性结构中删除元素时,分配新的连续内存块并将所有元素移到那里可能是有效的。在这种情况下,所有指针将失效。
Cfr

由于删除操作不知道最后会删除多少个元素,因此删除期间这样做毫无意义。无论如何:实现细节,必须信任苹果编写合理的代码。
vikingosegundo

首先不是更好(因为您首先要考虑它的工作原理),其次不是重构安全。因此,最后我以经典的公认解决方案作为结尾,该解决方案实际上是很容易理解的。
Renetik

8

以更具声明性的方式,根据与要删除的项目匹配的条件,可以使用:

[theArray filterUsingPredicate:aPredicate]

@Nathan应该非常高效


6

这是简单干净的方法。我想在快速枚举调用中复制我的数组:

for (LineItem *item in [NSArray arrayWithArray:self.lineItems]) 
{
    if ([item.toBeRemoved boolValue] == YES) 
    {
        [self.lineItems removeObject:item];
    }
}

这样,您就可以枚举要删除的数组的副本,它们都持有相同的对象。NSArray仅保存对象指针,因此这在内存/性能方面绝对是明智的。


2
或者更简单-for (LineItem *item in self.lineItems.copy)
亚历山大摹


5

这应该做到这一点:

    NSMutableArray* myArray = ....;

    int i;
    for(i=0; i<[myArray count]; i++) {
        id element = [myArray objectAtIndex:i];
        if(element == ...) {
            [myArray removeObjectAtIndex:i];
            i--;
        }
    }

希望这可以帮助...


尽管是非正统的,但我发现向后迭代并删除它是一种干净且简单的解决方案。通常也是最快的方法之一。
rpetrich

这怎么了 它干净,快速,易于阅读,并且就像一个饰物。对我来说,这似乎是最好的答案。为什么它的分数是负数?我在这里想念什么吗?
Steph Thirion,

1
@Steph:问题指出“比手动更新索引更优雅的东西”。
史蒂夫·马德森

1
哦。我错过了我绝对不想手动更新索引的部分。谢谢史蒂夫。国际海事组织,这一解决方案比所选择的解决方案更为优雅(不需要临时阵列),因此反对该解决方案的人感到不公平。
Steph Thirion,2010年

@Steve:如果您检查编辑,那一部分是在我提交我的答案之后添加的。祝你今天愉快!
Pokot0年

1

为什么不将要删除的对象添加到另一个NSMutableArray。完成迭代后,可以删除已收集的对象。


1

如何将要删除的元素与第n个元素,第n-1个元素等等交换?

完成后,将数组的大小调整为“以前的大小-交换数量”


1

如果数组中的所有对象都是唯一的,或者要在找到对象时删除所有出现的对象,则可以快速枚举数组副本并使用[NSMutableArray removeObject:]从原始对象中删除该对象。

NSMutableArray *myArray;
NSArray *myArrayCopy = [NSArray arrayWithArray:myArray];

for (NSObject *anObject in myArrayCopy) {
    if (shouldRemove(anObject)) {
        [myArray removeObject:anObject];
    }
}

如果原始myArray+arrayWithArray在执行时被更新会怎样?
bioffe 2011年

1
@bioffe:然后您的代码中有一个错误。NSMutableArray不是线程安全的,应该通过锁来控制访问。看到这个答案
dreamlax

1

上面的benzado的答案是您应该做的事。在我的一个应用程序中,removeObjectsInArray的运行时间为1分钟,仅将其添加到新数组中的时间为.023秒。


1

我定义了一个类别,使我可以使用块进行过滤,如下所示:

@implementation NSMutableArray (Filtering)

- (void)filterUsingTest:(BOOL (^)(id obj, NSUInteger idx))predicate {
    NSMutableIndexSet *indexesFailingTest = [[NSMutableIndexSet alloc] init];

    NSUInteger index = 0;
    for (id object in self) {
        if (!predicate(object, index)) {
            [indexesFailingTest addIndex:index];
        }
        ++index;
    }
    [self removeObjectsAtIndexes:indexesFailingTest];

    [indexesFailingTest release];
}

@end

然后可以这样使用:

[myMutableArray filterUsingTest:^BOOL(id obj, NSUInteger idx) {
    return [self doIWantToKeepThisObject:obj atIndex:idx];
}];

1

一个更好的实现是在NSMutableArray上使用下面的category方法。

@implementation NSMutableArray(BMCommons)

- (void)removeObjectsWithPredicate:(BOOL (^)(id obj))predicate {
    if (predicate != nil) {
        NSMutableArray *newArray = [[NSMutableArray alloc] initWithCapacity:self.count];
        for (id obj in self) {
            BOOL shouldRemove = predicate(obj);
            if (!shouldRemove) {
                [newArray addObject:obj];
            }
        }
        [self setArray:newArray];
    }
}

@end

谓词块可以实现为对数组中的每个对象进行处理。如果谓词返回true,则删除该对象。

一个日期数组的示例,用于删除过去的所有日期:

NSMutableArray *dates = ...;
[dates removeObjectsWithPredicate:^BOOL(id obj) {
    NSDate *date = (NSDate *)obj;
    return [date timeIntervalSinceNow] < 0;
}];

0

多年来,我一直很喜欢向后迭代,但是很长一段时间以来,我从未遇到过先删除“最深”(最高计数)对象的情况。在指针移动到下一个索引之前,没有任何东西,它崩溃了。

Benzado的方法与我现在所做的最接近,但我从未意识到每次删除后都会进行堆栈重新排列。

在Xcode 6下可以正常工作

NSMutableArray *itemsToKeep = [NSMutableArray arrayWithCapacity:[array count]];

    for (id object in array)
    {
        if ( [object isNotEqualTo:@"whatever"]) {
           [itemsToKeep addObject:object ];
        }
    }
    array = nil;
    array = [[NSMutableArray alloc]initWithArray:itemsToKeep];
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.