反復中にNSMutableArrayから削除する最良の方法?


194

Cocoaでは、NSMutableArrayをループして特定の基準を満たす複数のオブジェクトを削除する場合、オブジェクトを削除するたびにループを再起動せずにこれを行うための最良の方法は何ですか?

おかげで、

編集:明確にするために-私が最善の方法を探していました。たとえば、現在のインデックスを手動で更新するよりもエレガントな方法です。たとえば、C ++では私が実行できます。

iterator it = someList.begin();

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

9
後ろから前へループします。
Hot Licks 2013

「WHY」に答える人はいません
onmyway133 '10 / 09/14

1
@HotLicks私の昔からのお気に入りの1つであり、プログラミングにおける最も過小評価されているソリューション:D
Julian F. Weinert

回答:


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ölly2009年

1
実際には、逆を行う方が高速です。パフォーマンスの違いはかなり大きいですが、配列がそれほど大きくない場合は、どちらの場合でも時間は取るに足らないものになります。
user1032657

私はリバースを使用しました... nilとして配列を作成しました
Fahim Parkar

逆を行うには、追加のメモリを割り当てる必要があります。これはインプレースアルゴリズムではなく、場合によっては良いアイデアではありません。直接削除する場合は、配列をシフトするだけです(1つずつ移動しないため、非常に効率的です。大きなチャンクがシフトする可能性があります)。ただし、新しい配列を作成する場合は、すべての割り当てと割り当てが必要です。したがって、削除するアイテムが数個しかない場合は、逆がはるかに遅くなります。これは典型的な正しいアルゴリズムです。
ジャック

82

もう1つのバリエーション。したがって、読みやすさと優れたパフォーマンスが得られます。

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

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

[originalArrayOfItems removeObjectsAtIndexes:discardedItems];

これは素晴らしいです。配列から複数の項目を削除しようとしていましたが、これは完全に機能しました。他の方法には問題:)感謝の男引き起こしていた
AbhinavVinay

コーリー、この回答(下記)はremoveObjectsAtIndexes、オブジェクトを削除するための最悪の方法であると言いました、あなたはそれに同意しますか?あなたの答えが古すぎるので、私はこれを求めています。それでも最高のものを選ぶのは良いことですか?
Hemang 2013

私はこのソリューションを読みやすさの点で非常に気に入っています。@HotLicksの回答と比較した場合、パフォーマンスの点でどうですか?
Victor MaiaAldecôa2014年

Obj-Cで配列からオブジェクトを削除している間は、この方法をお勧めします。
ボレアス2016年

enumerateObjectsUsingBlock:インデックスの増分を無料で取得します。
pkamb 2018

42

これは非常に単純な問題です。あなたはただ後方に反復します:

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

これは非常に一般的なパターンです。


彼はコードを書き出さなかったので、イェンスの答えを見逃していたでしょう:P .. thnx m8
FlowUI。SimpleUITesting.com

3
これが最良の方法です。Objective-Cの配列インデックスは符号なしとして宣言されているにもかかわらず、反復変数は符号付き変数でなければならないことを覚えておくことが重要です(今、Appleがそれを後悔する方法は想像できます)。
mojuba

39

他の回答のいくつかは、非常に大規模な配列ではパフォーマンスが低下します。これは、メソッドがレシーバーの線形検索を行うことremoveObject:removeObjectsInArray:含むため、オブジェクトがどこにあるかを既に知っているため、無駄になります。また、への呼び出しremoveObjectAtIndex:は、一度に1スロットずつ、インデックスから配列の最後まで値をコピーする必要があります。

より効率的な方法は次のとおりです。

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

の容量を設定しているためitemsToKeep、サイズ変更中に値をコピーする時間を無駄にすることはありません。配列を変更しないため、高速列挙を自由に使用できます。使い方setArray:の内容交換するarrayとしてitemsToKeep効率的になります。コードによっては、最後の行を次のように置き換えることもできます。

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

したがって、値をコピーする必要すらなく、ポインターを交換するだけです。


このアルゴリズムは、時間の複雑さの点で優れていることがわかります。スペースの複雑さの観点から理解しようとしています。実際の「配列数」で「itemsToKeep」の容量を設定する同じ実装があります。しかし、配列からいくつかのオブジェクトを必要としないので、itemsToKeepに追加しません。つまり、私のitemsToKeepの容量は10ですが、実際には6つのオブジェクトしか格納できません。これは、4つのオブジェクトのスペースを無駄にしていることを意味しますか?PSは障害を見つけるのではなく、アルゴリズムの複雑さを理解しようとしています。:)
tech_human 2014年

はい、使用していない4つのポインター用にスペースが予約されています。配列にはポインタのみが含まれ、オブジェクト自体は含まれないことに注意してください。4つのオブジェクトの場合、16バイト(32ビットアーキテクチャ)または32バイト(64ビット)を意味します。
ベンザド2014年

ソリューションはどれも、せいぜい0の追加スペースしか必要としないことに注意してください(アイテムを削除します)、または元の配列のサイズを最低でも2倍にします(配列のコピーを作成するため)。繰り返しますが、オブジェクトの完全なコピーではなくポインタを扱っているため、ほとんどの状況でこれはかなり安価です。
ベンザド2014年

わかった。ありがとう弁財堂!!
tech_human 2014年

この投稿によると、配列のサイズに関するarrayWithCapacity:メソッドのヒントは実際には使用されていません。
Kremk 2014年

28

NSpredicateを使用して、可変配列から項目を削除できます。これにはforループは必要ありません。

たとえば、名前のNSMutableArrayがある場合、次のような述語を作成できます。

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

次の行では、bで始まる名前のみを含む配列が残ります。

[namesArray filterUsingPredicate:caseInsensitiveBNames];

必要な述語の作成に問題がある場合は、このアップル開発者向けリンクを使用してください


3
forループを表示する必要はありませ。フィルター操作にループがあり、表示されません。
ホットリックス2015

18

4つの異なる方法を使用してパフォーマンステストを行いました。各テストは、100,000要素配列のすべての要素を繰り返し、5番目ごとの項目を削除しました。最適化の有無にかかわらず、結果に大きな変化はありませんでした。これらはiPad 4で行われました:

(1)removeObjectAtIndex:- 271ミリ秒

(2)removeObjectsAtIndexes:- 1010ミリ秒(インデックスセットを構築すること〜700ミリ秒かかるので、そうでない場合、これは基本的に呼び出すremoveObjectAtIndexと同じである:各項目について)

(3)removeObjects:- 326ミリ秒

(4)テストに合格したオブジェクトで新しい配列を作成します- -17 ms

したがって、新しいアレイを作成するのが最速です。他のメソッドはすべて比較可能ですが、removeObjectsAtIndexes:を使用すると、インデックスセットの構築に時間がかかるため、削除する項目が増えるとパフォーマンスが低下します。


1
新しい配列を作成し、後で割り当てを解除する時間を数えましたか。それだけで17ミリ秒でできるとは思いません。プラス75,000課題?
ジャック、

新しいアレイの作成と割り当て解除に必要な時間は最小限です
user1032657

明らかに、逆ループスキームを測定していません。
ホットリックス

最初のテストは、逆ループ方式と同等です。
user1032657

newArrayを残し、prevArrayがNullになるとどうなりますか?newArrayをPrevArrayの名前にコピーする(または名前を変更する)必要があります。それ以外の場合、他のコードはそれをどのように参照しますか?
aremvee 2015

17

インデックスをカウントダウンするループを使用します。

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

または、保持したいオブジェクトを使用してコピーを作成します。

特に、for (id object in array)ループやを使用しないでくださいNSEnumerator


3
答えをコードm8に書き出す必要があります。はははほぼそれを見逃していた。
FlowUI。SimpleUITesting.com

これは正しくない。「for(id object in array)」は「for(NSInteger i = array.count-1; i> = 0; --i)」より高速であり、高速反復と呼ばれます。イテレータを使用すると、インデックス作成よりも確実に高速になります。
ジャック

@jack-しかし、イテレータの下から要素を削除すると、通常は大混乱を引き起こします。(そして「高速反復」は必ずしもそれほど高速であるとは限りません。)
Hot Licks

答えは2008年からです。高速反復は存在しませんでした。
Jens Ayton 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;
    }
)

コードが1行だけの別のオプション:

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

配列が再割り当てされないという保証はありますか?
Cfr

再割り当てとはどういう意味ですか?
vikingosegundo 2013年

線形構造から要素を削除する際に、新しい連続したメモリブロックを割り当てて、そこにすべての要素を移動すると効果的です。この場合、すべてのポインタが無効になります。
Cfr

削除操作では、最後に削除される要素の数がわからないため、削除中に削除しても意味がありません。とにかく、実装の詳細です。適切なコードを書くには、アップルを信頼する必要があります。
vikingosegundo 2013年

1つ目は(最初の場所でどのように機能するかについてよく考える必要があるため)良くなく、2つ目は安全なリファクタリングではありません。したがって、最終的に私は実際にかなり読みやすい古典的な受け入れられたソリューションで終わりました。
Renetik 2017

8

より宣言的な方法で、削除するアイテムと一致する基準に応じて、次のように使用できます。

[theArray filterUsingPredicate:aPredicate]

@ネイサンは非常に効率的でなければなりません


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)
アレクサンドル・G


5

これはそれを行うはずです:

    NSMutableArray* myArray = ....;

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

お役に立てれば...


正統ではありませんが、逆方向に繰り返して削除すると、クリーンでシンプルなソリューションになることがわかりました。通常、最速の方法の1つでもあります。
rpetrich 2009

これの何が問題になっていますか?クリーンで高速、読みやすく、魅力のように機能します。私にはそれが最良の答えのように見えます。なぜ負のスコアがあるのですか?ここで何か不足していますか?
Steph Thirion、2009

1
@Steph:「手動でインデックスを更新するよりもエレガントなもの」という質問です。
スティーブマドセン

1
ああ。I-absolutely-don't-want-to-update-the-index-manuallyの部分を見逃していました。スティーブに感謝します。IMOこのソリューションは、選択したものよりもエレガントです(一時的な配列は不要)。そのため、反対票を投じることは不公平に感じられます。
ステフティリオン

@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 2013

1

上記のベンザドのアンサーは、あなたが予備軍のために何をすべきかです。私のアプリケーションの1つで、removeObjectsInArrayの実行時間は1分で、新しい配列に追加するだけで0.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で以下のカテゴリメソッドを使用することです。

@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];
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.