Najlepszy sposób na usunięcie z NSMutableArray podczas iteracji?

194

W Cocoa, jeśli chcę przejść przez NSMutableArray i usunąć wiele obiektów spełniających określone kryteria, jaki jest najlepszy sposób, aby to zrobić bez ponownego uruchamiania pętli za każdym razem, gdy usuwam obiekt?

Dzięki,

Edycja: Tylko dla wyjaśnienia - szukałem najlepszego sposobu, np. Czegoś bardziej eleganckiego niż ręczne aktualizowanie indeksu, w którym jestem. Na przykład w C ++ mogę zrobić;

iterator it = someList.begin();

while (it != someList.end())
{
    if (shouldRemove(it))   
        it = someList.erase(it);
}
Andrew Grant
źródło
9
Pętla od tyłu do przodu.
Hot Licks
Nikt nie odpowiada na pytanie „DLACZEGO”
onmyway133,
1
@HotLicks Jeden z moich ulubionych i najbardziej niedocenione rozwiązanie w programowaniu: D
Julian F. Weinert

Odpowiedzi:

388

Dla jasności lubię tworzyć początkową pętlę, w której zbieram elementy do usunięcia. Potem je usuwam. Oto przykład z użyciem składni Objective-C 2.0:

NSMutableArray *discardedItems = [NSMutableArray array];

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

[originalArrayOfItems removeObjectsInArray:discardedItems];

Nie ma zatem wątpliwości, czy indeksy są aktualizowane poprawnie, czy też inne szczegóły dotyczące księgowości.

Edytowano, aby dodać:

W innych odpowiedziach zauważono, że odwrotne sformułowanie powinno być szybsze. tzn. jeśli wykonasz iterację przez tablicę i skomponujesz nową tablicę obiektów do zachowania, zamiast obiektów do odrzucenia. To może być prawda (choć co z pamięcią i kosztami przetwarzania przydzielania nowej tablicy i odrzucania starej?), Ale nawet jeśli jest szybsza, może nie być tak duża, jak w przypadku naiwnej implementacji, ponieważ NSArrays nie zachowuj się jak „normalne” tablice. Mówią, ale idą inną drogą.Zobacz dobrą analizę tutaj:

Odwrotne sformułowanie może być szybsze, ale nigdy nie musiałem się tym przejmować, ponieważ powyższy preparat zawsze był wystarczająco szybki dla moich potrzeb.

Dla mnie przesłaniem do domu jest użycie tego, co jest dla ciebie jasne. Zoptymalizuj tylko w razie potrzeby. Osobiście uważam powyższe sformułowanie za najbardziej zrozumiałe i dlatego go używam. Ale jeśli odwrotne sformułowanie jest dla ciebie wyraźniejsze, idź do niego.

Christopher Ashworth
źródło
47
Uwaga: może to powodować błędy, jeśli obiekty są więcej niż jeden raz w tablicy. Alternatywnie możesz użyć NSMutableIndexSet i - (void) removeObjectsAtIndexes.
Georg Schölly,
1
W rzeczywistości jest szybciej wykonać odwrotność. Różnica w wydajności jest dość duża, ale jeśli twoja tablica nie jest tak duża, czasy i tak będą trywialne.
user1032657
Użyłem odwracania ... stworzyłem tablicę jako zero ... następnie dodałem tylko to, czego chciałem i przeładowałem dane ... zrobiłem ... stworzyłem dummyArray, który jest lustrem głównej tablicy, dzięki czemu mamy główne rzeczywiste dane
Fahim Parkar
Dodatkowa pamięć musi zostać przydzielona do wykonania operacji odwrotnej. To nie jest algorytm lokalny i nie jest dobrym pomysłem w niektórych przypadkach. Kiedy usuwasz bezpośrednio, po prostu przesuwasz tablicę (co jest bardzo wydajne, ponieważ nie przesuwa się jeden po drugim, może przesunąć dużą porcję), ale podczas tworzenia nowej tablicy potrzebujesz całego przydziału i przypisania. Więc jeśli usuniesz tylko kilka elementów, odwrotność będzie znacznie wolniejsza. Jest to typowy algorytm, który wydaje się odpowiedni.
jack
82

Jeszcze jedna odmiana. Dzięki temu zyskujesz czytelność i dobrą wydajność:

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

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

[originalArrayOfItems removeObjectsAtIndexes:discardedItems];
Corey Floyd
źródło
To jest niesamowite. Próbowałem usunąć wiele elementów z tablicy i to zadziałało idealnie. Inne metody powodowały problemy :) Dzięki stary
AbhinavVinay
Corey, ta odpowiedź (poniżej) powiedziała, że removeObjectsAtIndexesto najgorsza metoda na usunięcie obiektów, zgadzasz się z tym? Proszę o to, ponieważ twoja odpowiedź jest już za stara. Nadal dobrze wybrać najlepszy?
Hemang
Bardzo podoba mi się to rozwiązanie pod względem czytelności. Jak to jest pod względem wydajności w porównaniu do odpowiedzi @HotLicks?
Victor Maia Aldecôa
Należy zdecydowanie zachęcać do tej metody podczas usuwania obiektów z tablicy w Obj-C.
boreas
enumerateObjectsUsingBlock:dostaniesz przyrost indeksu za darmo.
pkamb
42

To bardzo prosty problem. Po prostu iterujesz wstecz:

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

Jest to bardzo powszechny wzór.

Hot Licks
źródło
nie zauważyłby odpowiedzi Jensa, ponieważ nie napisał kodu: P .. thnx m8
FlowUI. SimpleUITesting.com
3
To najlepsza metoda. Ważne jest, aby pamiętać, że zmienna iteracyjna powinna być zmienną podpisaną, mimo że indeksy tablic w Objective-C są zadeklarowane jako niepodpisane (mogę sobie wyobrazić, jak Apple tego teraz żałuje).
mojuba,
39

Niektóre inne odpowiedzi miałyby niską wydajność na bardzo dużych tablicach, ponieważ metody takie jak removeObject:i removeObjectsInArray:polegają na przeprowadzaniu liniowego przeszukiwania odbiornika, co jest marnotrawstwem, ponieważ już wiesz, gdzie jest obiekt. Ponadto każde wywołanie do removeObjectAtIndex:będzie musiało kopiować wartości z indeksu na koniec tablicy w górę o jedno miejsce na raz.

Bardziej wydajne byłyby następujące:

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

Ponieważ ustawiamy pojemność itemsToKeep, nie marnujemy czasu na kopiowanie wartości podczas zmiany rozmiaru. Nie modyfikujemy tablicy w miejscu, więc możemy swobodnie korzystać z szybkiego wyliczania. Korzystanie setArray:zastąpić zawartość arrayze itemsToKeepbędzie skuteczny. W zależności od kodu możesz nawet zamienić ostatni wiersz na:

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

Więc nie ma nawet potrzeby kopiowania wartości, wystarczy zamienić wskaźnik.

benzado
źródło
Ten algo okazuje się być dobry pod względem złożoności czasowej. Staram się to zrozumieć pod względem złożoności przestrzeni. Mam tę samą implementację, w której ustawiam pojemność dla „itemsToKeep” z faktyczną „Array count”. Ale powiedz, że nie chcę kilku obiektów z tablicy, więc nie dodam ich do itemsToKeep. Tak więc pojemność moich przedmiotówToKeep wynosi 10, ale w rzeczywistości przechowuję tylko 6 obiektów. Czy to oznacza, że ​​marnuję miejsce na 4 przedmioty? PS nie znajduję błędów, tylko próbuję zrozumieć złożoność algo. :)
tech_human
Tak, masz miejsce zarezerwowane na cztery wskaźniki, których nie używasz. Pamiętaj, że tablica zawiera tylko wskaźniki, a nie same obiekty, więc dla czterech obiektów oznacza to 16 bajtów (architektura 32-bitowa) lub 32 bajty (64-bit).
benzado
Zauważ, że każde z rozwiązań będzie co najwyżej wymagało 0 dodatkowej przestrzeni (usuwasz elementy na miejscu) lub w najgorszym wypadku podwoi rozmiar oryginalnej tablicy (ponieważ tworzysz kopię tablicy). Ponownie, ponieważ mamy do czynienia ze wskaźnikami, a nie pełnymi kopiami obiektów, w większości przypadków jest to dość tanie.
benzado
Okej, rozumiem. Dzięki Benzado !!
tech_human
Zgodnie z tym postem podpowiedź tablicy arrayWithCapacity: metoda dotycząca rozmiaru tablicy nie jest faktycznie używana.
Kremk
28

Możesz użyć NSpredicate, aby usunąć elementy ze swojej zmiennej tablicy. To nie wymaga pętli.

Na przykład, jeśli masz NSMutableArray nazw, możesz utworzyć predykat taki jak ten:

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

W poniższym wierszu zostanie wyświetlona tablica zawierająca tylko nazwy zaczynające się na b.

[namesArray filterUsingPredicate:caseInsensitiveBNames];

Jeśli masz problemy z utworzeniem potrzebnych predykatów, skorzystaj z tego linku dla programistów Apple .


źródło
3
Nie wymaga widoczności dla pętli. W operacji filtrowania występuje pętla, po prostu jej nie widzisz.
Hot Licks
18

Zrobiłem test wydajności przy użyciu 4 różnych metod. W każdym teście powtarzano wszystkie elementy w tablicy 100 000 elementów i usuwano co piąty element. Wyniki nie różniły się zbytnio z / bez optymalizacji. Zostały one wykonane na iPadzie 4:

(1) removeObjectAtIndex:- 271 ms

(2) removeObjectsAtIndexes:- 1010 ms (ponieważ zbudowanie zestawu indeksów zajmuje ~ 700 ms; w przeciwnym razie jest to w zasadzie to samo, co wywołanie removeObjectAtIndex: dla każdego elementu)

(3) removeObjects:- 326 ms

(4) utwórz nową tablicę z obiektami, które przejdą test - 17 ms

Tak więc tworzenie nowej tablicy jest zdecydowanie najszybsze. Wszystkie pozostałe metody są porównywalne, z wyjątkiem tego, że użycie removeObjectsAtIndexes: będzie gorzej z większą liczbą elementów do usunięcia, ze względu na czas potrzebny na zbudowanie zestawu indeksów.

użytkownik1032657
źródło
1
Czy liczyłeś czas na utworzenie nowej tablicy i późniejszą dezalokację. Nie wierzę, że można to zrobić samemu w 17 ms. Plus 75 000 zleceń?
jack
Czas potrzebny na utworzenie i zwolnienie nowej tablicy jest minimalny
1032657
Najwyraźniej nie zmierzyłeś schematu pętli zwrotnej.
Hot Licks
Pierwszy test jest równoważny schematowi pętli zwrotnej.
user1032657
co się stanie, gdy pozostaniesz z nową tablicą, a tablica poprzedniej tablicy zostanie unieważniona? Będziesz musiał skopiować newArray do tego, do czego została nazwana PrevArray (lub zmienić jej nazwę), w przeciwnym razie, w jaki sposób odniesie się do twojego innego kodu?
aremvee,
17

Użyj pętli odliczającej indeksy:

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

lub wykonaj kopię z obiektami, które chcesz zachować.

W szczególności nie używaj for (id object in array)pętli ani NSEnumerator.

Jens Ayton
źródło
3
powinieneś zapisać swoją odpowiedź w kodzie m8. haha prawie tego przegapił.
FlowUI. SimpleUITesting.com
To nie jest w porządku. „for (identyfikator obiektu w tablicy)” jest szybszy niż „for (NSInteger i = array.count - 1; i> = 0; --i)” i nazywa się szybką iteracją. Korzystanie z iteratora jest zdecydowanie szybsze niż indeksowanie.
jack
@jack - Ale jeśli usuniesz element spod iteratora, zwykle wywołujesz spustoszenie. (A „szybka iteracja” nie zawsze jest o wiele szybsza.)
Hot Licks
Odpowiedź pochodzi z 2008 r. Szybka iteracja nie istniała.
Jens Ayton
12

W systemie iOS 4+ lub OS X 10.6+ Apple dodał passingTestserię interfejsów API NSMutableArray, takich jak – indexesOfObjectsPassingTest:. Rozwiązaniem z takim interfejsem API byłoby:

NSIndexSet *indexesToBeRemoved = [someList indexesOfObjectsPassingTest:
    ^BOOL(id obj, NSUInteger idx, BOOL *stop) {
    return [self shouldRemove:obj];
}];
[someList removeObjectsAtIndexes:indexesToBeRemoved];
zavié
źródło
12

Obecnie można używać odwróconego wyliczania opartego na blokach. Prosty przykładowy 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];
}];

Wynik:

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

inna opcja z tylko jednym wierszem kodu:

[array filterUsingPredicate:[NSPredicate predicateWithFormat:@"shouldDelete == NO"]];
vikingosegundo
źródło
Czy jest jakaś gwarancja, że ​​tablica nie zostanie ponownie przydzielona?
Por.
Co masz na myśli z realokacją?
vikingosegundo
Podczas usuwania elementu ze struktury liniowej może być skuteczne przydzielenie nowego ciągłego bloku pamięci i przeniesienie tam wszystkich elementów. W takim przypadku wszystkie wskaźniki zostaną unieważnione.
Fr.
Ponieważ operacja usuwania nie wiedziałaby, ile elementów zostanie usunięta na końcu, nie byłoby sensu tego robić podczas usuwania. W każdym razie: szczegóły implementacji, musimy ufać Apple, aby napisać rozsądny kod.
vikingosegundo
Po pierwsze nie jest ładniej (bo trzeba przede wszystkim przemyśleć, jak to działa), a po drugie nie jest bezpiecznie refaktoryzować. W końcu skończyłem na klasycznie przyjętym rozwiązaniu, które jest właściwie dość czytelne.
Renetik,
8

W bardziej deklaratywny sposób, w zależności od kryteriów pasujących do elementów do usunięcia, możesz użyć:

[theArray filterUsingPredicate:aPredicate]

@Nathan powinien być bardzo wydajny

elp
źródło
6

Oto prosty i czysty sposób. Lubię duplikować moją tablicę bezpośrednio w wywołaniu szybkiego wyliczania:

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

W ten sposób wyliczysz kopię usuwanej tablicy, która zawiera te same obiekty. NSArray przechowuje tylko wskaźniki obiektów, więc jest to całkowicie w porządku pod względem pamięci / wydajności.

Matjan
źródło
2
Lub nawet prościej -for (LineItem *item in self.lineItems.copy)
Alexandre G
5

Dodaj obiekty, które chcesz usunąć, do drugiej tablicy, a po pętli użyj -removeObjectsInArray :.

Nathan Kinsinger
źródło
5

powinno to zrobić:

    NSMutableArray* myArray = ....;

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

mam nadzieję że to pomoże...

Pokot0
źródło
Chociaż niekonwencjonalne, iteracja wstecz i usuwanie trwa, gdy staje się czystym i prostym rozwiązaniem. Zwykle jedna z najszybszych metod.
rpetrich
Co jest z tym nie tak? Jest czysty, szybki, czytelny i działa jak urok. Dla mnie wygląda to na najlepszą odpowiedź. Dlaczego ma wynik ujemny? Czy coś mi umyka?
Steph Thirion,
1
@Steph: Pytanie brzmi „coś bardziej eleganckiego niż ręczne aktualizowanie indeksu”.
Steve Madsen
1
O. Brakowało mi części absolutnie nie chcę aktualizować indeksu ręcznie. dzięki Steve. IMO to rozwiązanie jest bardziej eleganckie niż wybrane (nie jest potrzebny tymczasowy układ), więc negatywne głosy przeciwko niemu wydają się niesprawiedliwe.
Steph Thirion
@ Steve: Jeśli sprawdzisz zmiany, ta część została dodana po przesłaniu mojej odpowiedzi ... jeśli nie, odpowiedziałbym: „Iteracja wstecz jest najbardziej eleganckim rozwiązaniem” :). Miłego dnia!
Pokot0
1

Dlaczego nie dodasz obiektów do usunięcia do innego NSMutableArray. Po zakończeniu iteracji możesz usunąć zgromadzone obiekty.

Paul Croarkin
źródło
1

Co powiesz na zamianę elementów, które chcesz usunąć, na n-ty element, n-1 element i tak dalej?

Po zakończeniu zmieniasz rozmiar tablicy na „poprzedni rozmiar - liczba zamian”

Cyber ​​Oliveira
źródło
1

Jeśli wszystkie obiekty w tablicy są unikalne lub chcesz usunąć wszystkie wystąpienia obiektu, gdy zostanie znaleziony, możesz szybko wyliczyć kopię tablicy i użyć [NSMutableArray removeObject:], aby usunąć obiekt z oryginału.

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

for (NSObject *anObject in myArrayCopy) {
    if (shouldRemove(anObject)) {
        [myArray removeObject:anObject];
    }
}
Lajos
źródło
co się stanie, jeśli oryginalny myArray zostanie zaktualizowany podczas +arrayWithArraywykonywania?
bioffe
1
@bioffe: Masz błąd w kodzie. NSMutableArray nie jest bezpieczny dla wątków, oczekuje się, że będziesz kontrolować dostęp za pomocą blokad. Zobacz tę odpowiedź
dreamlax
1

Powyższa odpowiedź benzado jest tym, co powinieneś zrobić dla preformace. W jednej z moich aplikacji removeObjectsInArray zajęło 1 minutę, samo dodanie do nowej tablicy zajęło 0,023 sekundy.

kajzer
źródło
1

Definiuję kategorię, która pozwala mi filtrować za pomocą bloku, jak poniżej:

@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

które można następnie wykorzystać w następujący sposób:

[myMutableArray filterUsingTest:^BOOL(id obj, NSUInteger idx) {
    return [self doIWantToKeepThisObject:obj atIndex:idx];
}];
Kristopher Johnson
źródło
1

Lepszą implementacją może być użycie metody kategorii poniżej na 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

Blok predykatów można zaimplementować w celu przetwarzania każdego obiektu w tablicy. Jeśli predykat zwróci wartość true, obiekt zostanie usunięty.

Przykład tablicy dat do usunięcia wszystkich dat, które leżą w przeszłości:

NSMutableArray *dates = ...;
[dates removeObjectsWithPredicate:^BOOL(id obj) {
    NSDate *date = (NSDate *)obj;
    return [date timeIntervalSinceNow] < 0;
}];
Werner Altewischer
źródło
0

Iterowanie wstecz było moim ulubionym od lat, ale przez długi czas nigdy nie spotkałem się z przypadkiem, w którym „najgłębszy” (najwyższy wynik) obiekt został usunięty jako pierwszy. Na chwilę przed przejściem wskaźnika do następnego indeksu nie ma nic i ulega awarii.

Sposób Benzado jest najbliższy temu, co robię teraz, ale nigdy nie zdawałem sobie sprawy, że po każdym usunięciu nastąpi przetasowanie stosu.

pod Xcode 6 to działa

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];
aremvee
źródło