Yineleme sırasında NSMutableArray'den kaldırmanın en iyi yolu?


194

Kakao, bir NSMutableArray döngü ve belirli bir ölçütlere uyan birden çok nesne kaldırmak istiyorsanız, bir nesneyi her kaldırdığınızda döngü yeniden başlatmadan bunu yapmanın en iyi yolu nedir?

Teşekkürler,

Düzenleme: Sadece açıklığa kavuşturmak için - Ben en iyi yolu, örneğin elle indeks güncelleme daha zarif bir şey arıyordu. Örneğin C ++ ile yapabilirim;

iterator it = someList.begin();

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

9
Arkadan öne doğru döngü.
Hot Licks

Kimse "NEDEN" cevap
onmyway133 10:14

1
@HotLicks Tüm zamanların favorilerimden biri ve genel olarak programlamadaki en az tahmin edilen çözüm: D
Julian F. Weinert

Yanıtlar:


388

Açıklık için silinecek öğeleri topladığım bir başlangıç ​​döngüsü oluşturmayı seviyorum. Sonra onları silerim. İşte Objective-C 2.0 sözdizimini kullanan bir örnek:

NSMutableArray *discardedItems = [NSMutableArray array];

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

[originalArrayOfItems removeObjectsInArray:discardedItems];

Ardından, endekslerin doğru bir şekilde güncellenip güncellenmediği veya diğer küçük defter tutma detaylarıyla ilgili bir soru yoktur.

Eklemek için düzenlendi:

Diğer cevaplarda ters formülasyonun daha hızlı olması gerektiği belirtilmiştir. yani dizi boyunca yineleme yapar ve atılacak nesneler yerine saklanacak yeni bir nesne dizisi oluşturursanız. Bu doğru olabilir (yeni bir dizi ayırmanın ve eski diziyi atmanın bellek ve işleme maliyeti ne olursa olsun), ancak daha hızlı olsa bile, saf bir uygulama için olduğu kadar büyük bir anlaşma olmayabilir, çünkü NSArrays "normal" diziler gibi davranmayın. Konuşmayı konuşuyorlar ama farklı bir yürüyüş yapıyorlar. Burada iyi bir analize bakın:

Ters formülasyon daha hızlı olabilir, ancak bunun olup olmadığına hiç dikkat etmemeliydim, çünkü yukarıdaki formülasyon her zaman ihtiyaçlarım için yeterince hızlıydı.

Benim için eve götüren mesaj size en açık formülasyonu kullanmaktır. Yalnızca gerekirse optimize edin. Kişisel olarak yukarıdaki formülasyonu en açık şekilde buluyorum, bu yüzden kullanıyorum. Ancak ters formülasyon sizin için daha açıksa, bunun için gidin.


47
Nesneler bir dizide birden fazla ise, bu hatalar oluşturabilirsiniz. Alternatif olarak bir NSMutableIndexSet ve - (void) removeObjectsAtIndexes kullanabilirsiniz.
Georg Schölly

1
Aslında tersini yapmak daha hızlı. Performans farkı oldukça büyük, ancak diziniz o kadar büyük değilse, zamanlar her iki şekilde de önemsiz olacaktır.
user1032657

Ben ters yaptı ... nil olarak yapılan dizi .. sonra sadece ne istediğimi ekledi ve verileri yeniden yükleyin ... bitti ... biz ana gerçek veri var böylece ana dizi ayna olan dummyArray yarattı
Fahim Parkar

Ters yapmak için fazladan bellek ayrılması gerekir. Bu, yerinde bir algoritma değildir ve bazı durumlarda iyi bir fikir değildir. Doğrudan kaldırdığınızda diziyi kaydırırsınız (tek tek hareket etmeyeceği için çok etkilidir, büyük bir yığın değiştirebilir), ancak yeni bir dizi oluşturduğunuzda tüm ayırma ve atamaya ihtiyacınız vardır. Bu nedenle, yalnızca birkaç öğeyi silerseniz, tersi çok daha yavaş olacaktır. Tipik görünen bir algoritma.
jack

82

Bir varyasyon daha. Böylece okunabilirlik ve iyi performans elde edersiniz:

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

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

[originalArrayOfItems removeObjectsAtIndexes:discardedItems];

Bu harika. Diziden birden fazla öğe kaldırmaya çalışıyordu ve bu mükemmel çalıştı. Diğer yöntemler sorunlara neden oluyordu :) Teşekkürler adam
AbhinavVinay

Corey, bu cevap (aşağıda) removeObjectsAtIndexes, nesneleri kaldırmak için en kötü yöntem olduğunu söyledi , buna katılıyor musunuz? Bunu soruyorum çünkü cevabınız çok eski. Yine de en iyisini seçmek iyi mi?
Hemang

Bu çözümü okunabilirlik açısından çok seviyorum. @HotLicks cevabı ile karşılaştırıldığında performans açısından nasıl?
Victor Maia Aldecôa

Obj-C'deki diziden nesneleri silerken bu yöntem kesinlikle teşvik edilmelidir.
boreas

enumerateObjectsUsingBlock:size endeks artışını ücretsiz olarak verir.
pkamb

42

Bu çok basit bir problem. Sadece geriye doğru yineliyorsunuz:

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

Bu çok yaygın bir örüntüdür.


Jens cevap kaçırmış olurdu, çünkü o kodu
yazmadı

3
Bu en iyi yöntemdir. Yineleme değişkeninin imzalı bir değişken olması gerektiğini hatırlamak önemlidir, ancak Objective-C'deki dizi indeksleri imzasız olarak bildirilir (Apple'ın şimdi nasıl pişman olduğunu hayal edebiliyorum).
mojuba

39

Diğer cevapların bazıları çok büyük dizilerde düşük performans gösterecektir, çünkü yöntemler alıcının doğrusal bir arama yapmasını içerir removeObject:ve removeObjectsInArray:içerir, bu da bir israftır, çünkü nesnenin nerede olduğunu zaten biliyorsunuzdur. Ayrıca, yapılacak herhangi bir çağrının removeObjectAtIndex:değerleri dizinden dizinin sonuna kadar her seferinde bir yuva kadar kopyalaması gerekir.

Daha verimli şunlar olabilir:

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

Kapasitesini ayarladığımızdan, itemsToKeepyeniden boyutlandırma sırasında değerleri kopyalamak için zaman kaybetmeyiz. Diziyi yerinde değiştirmiyoruz, bu nedenle Hızlı Numaralandırma'yı kullanmakta özgürüz. Kullanılması setArray:içeriğini değiştirmek için arraybirlikte itemsToKeepverimli olacaktır. Kodunuza bağlı olarak, son satırı şununla da değiştirebilirsiniz:

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

Bu yüzden değerleri kopyalamaya bile gerek yok, sadece bir işaretçiyi değiştirin.


Bu algo zaman karmaşıklığı açısından iyi olduğunu kanıtlamaktadır. Uzay karmaşıklığı açısından anlamaya çalışıyorum. Gerçek 'Dizi sayısı' ile 'itemsToKeep' için kapasite ayarladığım yerde de aynı uygulama var. Ama diyelim ki diziden birkaç nesne istemiyorum, bu yüzden itemsToKeep'e eklemiyorum. Öyleyse itemToKeep'imin kapasitesi 10, ancak aslında sadece 6 nesne saklıyorum. Bu, 4 nesnenin boşa harcadığım anlamına mı geliyor? PS hata bulmuyor, sadece algo karmaşıklığını anlamaya çalışıyor. :)
tech_human

Evet, kullanmadığınız dört işaretçi için ayrılan yeriniz var. Dizinin yalnızca nesneler değil işaretçiler içerdiğini unutmayın, bu nedenle 16 bayt (32 bit mimari) veya 32 bayt (64 bit) anlamına gelen dört nesne için.
benzado

Çözümlerden herhangi birinin en iyi şekilde 0 ek alan (öğelerin yerinde silinmesi) veya orijinal dizi boyutunun (dizinin bir kopyasını oluşturduğunuzdan) en az iki katına çıkacağını unutmayın. Yine, nesnelerin tam kopyaları değil, işaretçilerle uğraştığımız için, bu çoğu durumda oldukça ucuzdur.
benzado

Tamam anladım. Teşekkürler Benzado !!
tech_human

Göre , bu mesaj, arrayWithCapacity ipucu: dizinin büyüklüğü hakkında bir yöntem gerçekten kullanılan değildir.
Kremk

28

Değiştirilebilir dizinizden öğeleri kaldırmak için NSpredicate'i kullanabilirsiniz. Bunun için döngüler gerekmez.

Örneğin, bir NSMutableArray adınız varsa, bunun gibi bir yüklem oluşturabilirsiniz:

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

Aşağıdaki satırda yalnızca b ile başlayan isimler içeren bir dizi bırakılır.

[namesArray filterUsingPredicate:caseInsensitiveBNames];

İhtiyacınız olan tahminleri oluşturmakta sorun yaşıyorsanız, bu elma geliştirici bağlantısını kullanın .


3
Bir gerektirir görünür döngüler için. Filtre işleminde bir döngü var, sadece görmüyorsunuz.
Hot Licks

18

4 farklı yöntem kullanarak performans testi yaptım. Her test, 100.000 öğe dizisindeki tüm öğeler arasında yinelendi ve her 5. öğeyi kaldırıldı. Sonuçlar optimizasyon ile / optimizasyon olmadan çok fazla değişmedi. Bunlar bir iPad 4'te yapıldı:

(1) removeObjectAtIndex:- 271 ms

(2) removeObjectsAtIndexes:- 1010 ms (çünkü dizin kümesinin oluşturulması ~ 700 ms sürer; aksi takdirde bu temel olarak her öğe için removeObjectAtIndex çağrılmasıyla aynıdır)

(3) removeObjects:- 326 ms

(4) testi geçen nesnelerle yeni bir dizi oluşturun - 17 ms

Yani, yeni bir dizi oluşturmak çok hızlı. Diğer yöntemlerin tümü karşılaştırılabilir, ancak removeObjectsAtIndexes: dizininin oluşturulması için gereken süre nedeniyle kaldırılacak daha fazla öğe kullanıldığında daha kötü olacaktır.


1
Yeni bir dizi oluşturma ve daha sonra onu yeniden ayırma zamanını saydınız mı? Bunu 17 ms'de tek başına yapabileceğine inanmıyorum. Artı 75.000 ödev?
jack

Yeni bir dizi oluşturmak ve yeniden
konumlandırmak

Görünüşe göre ters döngü şemasını ölçmediniz.
Sıcak Yalama

İlk test, ters döngü şemasına eşdeğerdir.
user1032657

newArray öğenizden ayrılırsanız ve prevArray öğeniz Nulled olduğunda ne olur? NewArray öğesini PrevArray öğesinin adına kopyalamanız (veya yeniden adlandırmanız) gerekir, aksi takdirde diğer kodunuz buna nasıl başvurur?
aremvee

17

Endekslerde geri sayım döngüsünü kullanın:

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

veya saklamak istediğiniz nesnelerle bir kopyasını oluşturabilirsiniz.

Özellikle, bir for (id object in array)döngü kullanmayın NSEnumerator.


3
cevabınızı m8 kodunda yazmalısınız. haha neredeyse özlüyordu.
FlowUI. SimpleUITesting.com

Bu doğru değil. "for (dizideki id nesnesi)" "(NSInteger i = array.count - 1; i> = 0; --i)" için daha hızlıdır ve hızlı yineleme olarak adlandırılır. Yineleyici kullanmak kesinlikle endekslemekten daha hızlıdır.
jack

@jack - Ancak bir öğeyi yineleyicinin altından kaldırırsanız, genellikle tahribat yaratırsınız. (Ve "hızlı yineleme" her zaman çok daha hızlı değildir.)
Hot Licks

Cevap 2008'den geldi. Hızlı yineleme yoktu.
Jens Ayton

12

İOS 4+ veya OS X 10.6+ için Apple , gibi bir passingTestdizi API ekledi . Bu tür bir API ile bir çözüm şöyle olacaktır:NSMutableArray– indexesOfObjectsPassingTest:

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

12

Günümüzde tersine blok tabanlı numaralandırma kullanabilirsiniz. Basit bir örnek kod:

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

Sonuç:

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

sadece bir satır kod içeren başka bir seçenek:

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

Dizinin yeniden tahsis edilmeyeceğine dair bir garanti var mı?
Cfr

Yeniden tahsis ile ne demek istiyorsun?
vikingosegundo

Elemanı doğrusal yapıdan kaldırırken, yeni bitişik bellek bloğu tahsis etmek ve tüm elemanları oraya taşımak etkili olabilir. Bu durumda, tüm işaretçiler geçersiz kılınacaktır.
Cfr

Silme işlemi sonunda kaç öğenin silineceğini iddia edeceğini bilemeyeceği için, kaldırma sırasında bunu yapmak mantıklı gelmeyecektir. Neyse: uygulama detayı, w makul kod yazmak için elma güvenmek zorunda.
vikingosegundo

Birincisi daha hoş değil (çünkü ilk etapta nasıl çalıştığı hakkında çok düşünmeniz gerekiyor) ve ikincisi güvenli bir şekilde yeniden düzenleme yapmıyor. Sonunda aslında oldukça okunabilir klasik kabul edilmiş bir çözüm ile sona erdi.
Renetik

8

Daha açıklayıcı bir şekilde, kaldırılacak öğelerle eşleşen ölçütlere bağlı olarak şunları kullanabilirsiniz:

[theArray filterUsingPredicate:aPredicate]

@Nathan çok verimli olmalı


6

İşte kolay ve temiz yol. Hızlı numaralandırma çağrısında dizimi çoğaltmak istiyorum:

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

Bu şekilde, her ikisi de aynı nesneleri tutan, silinen dizinin bir kopyası aracılığıyla numaralandırırsınız. NSArray yalnızca nesne işaretçileri tutar, böylece bu tamamen iyi bir bellek / performans açısından akıllıca olur.


2
Veya daha da basit -for (LineItem *item in self.lineItems.copy)
Alexandre G

5

Kaldırmak istediğiniz nesneleri ikinci bir diziye ekleyin ve döngüden sonra -removeObjectsInArray: öğesini kullanın.


5

bunu yapmalı:

    NSMutableArray* myArray = ....;

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

Bu yardımcı olur umarım...


Alışılmadık olmasına rağmen, temiz ve basit bir çözüm olmaya devam ederken geriye doğru yinelenen ve sildiğimi gördüm. Genellikle en hızlı yöntemlerden biri.
rpetrich

Bunun nesi var? Temiz, hızlı, kolay okunabilir ve cazibe gibi çalışır. Bana göre en iyi cevap gibi görünüyor. Neden olumsuz bir puanı var? Burada bir şey mi eksik?
Steph Thirion

1
@Steph: Soru, "dizini manuel olarak güncellemekten daha zarif bir şey" belirtiyor.
Steve Madsen

1
ah. Ben-kesinlikle-istemek-güncelleme-dizin-manuel kısmını kaçırmıştı. teşekkürler steve. IMO bu çözüm seçilenden daha zariftir (geçici diziye gerek yoktur), bu yüzden ona karşı olumsuz oylar haksızdır.
Steph Thirion

@Steve: Düzenlemeleri kontrol ederseniz, cevabımı gönderdikten sonra bu bölüm eklenmişti .... değilse, "En zarif çözüm geriye doğru yineleme" :) ile cevap verirdim. İyi günler!
Pokot0

1

Kaldırılacak nesneleri neden başka bir NSMutableArray'e eklemiyorsunuz? Yinelemeyi tamamladığınızda, topladığınız nesneleri kaldırabilirsiniz.


1

Silmek istediğiniz öğeleri 'n'th öğesi,' n-1'th öğesi vb. İle değiştirmeye ne dersiniz?

İşiniz bittiğinde diziyi 'önceki boyut - takas sayısı' olarak yeniden boyutlandırırsınız


1

Dizinizdeki tüm nesneler benzersizse veya bulunduğunda bir nesnenin tüm örneklerini kaldırmak istiyorsanız, bir dizi kopyasında hızlı numaralandırma yapabilir ve nesneyi orijinalden kaldırmak için [NSMutableArray removeObject:] kullanabilirsiniz.

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

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

orijinal myArray+arrayWithArray yürütülürken güncellenirse ne olur ?
bioffe

1
@bioffe: O zaman kodunuzda bir hata var. NSMutableArray iş parçacığı için güvenli değildir, erişimi kilitler aracılığıyla denetlemeniz beklenir. Bu cevaba
dreamlax

1

benzado'nun yukarıdaki anwser preformace için yapmanız gereken şeydir. Uygulamalarımdan birinde removeObjectsInArray 1 dakikalık bir çalışma süresi aldı, sadece yeni bir diziye ekleme .023 saniye sürdü.


1

Ben şöyle bir blok kullanarak filtre sağlayan bir kategori tanımlamak:

@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

daha sonra şu şekilde kullanılabilir:

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

1

NSMutableArray'de aşağıdaki kategori yöntemini kullanmak daha iyi bir uygulama olabilir.

@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

Yüklem bloğu dizideki her bir nesne üzerinde işlem yapmak için uygulanabilir. Yüklem doğru döndürürse nesne kaldırılır.

Geçmişteki tüm tarihleri ​​kaldırmak için bir tarih dizisi örneği:

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

0

Yıllarca geriye doğru tekrarlamak benim favorimdi, ancak uzun bir süre önce 'en derin' (en yüksek sayım) nesnenin kaldırıldığı durumla hiç karşılaşmadım. İşaretçi bir sonraki dizine geçmeden önce hiçbir şey yok ve çöküyor.

Benzado'nun yolu şu anda yaptığım şeye en yakın olanıdır, ancak her kaldırmadan sonra yığın değişikliği olacağını asla fark etmedim.

Xcode 6 altında bu işe yarıyor

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];
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.