Cách tốt nhất để xóa khỏi NSMutableArray trong khi lặp lại?


194

Trong Ca cao, nếu tôi muốn lặp qua NSMutableArray và xóa nhiều đối tượng phù hợp với một tiêu chí nhất định, cách tốt nhất để làm điều này mà không cần khởi động lại vòng lặp mỗi khi tôi xóa đối tượng?

Cảm ơn,

Chỉnh sửa: Chỉ cần làm rõ - Tôi đang tìm kiếm cách tốt nhất, ví dụ: một cái gì đó thanh lịch hơn là cập nhật thủ công chỉ mục tôi đang ở. Ví dụ trong C ++ tôi có thể làm;

iterator it = someList.begin();

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

9
Vòng từ phía sau ra phía trước.
Hot Licks

Không ai trả lời "TẠI SAO"
onmyway133

1
@HotLicks Một trong những mục yêu thích mọi thời đại của tôi và là giải pháp được đánh giá thấp nhất trong lập trình nói chung: D
Julian F. Weinert

Câu trả lời:


388

Để rõ ràng, tôi muốn tạo một vòng lặp ban đầu nơi tôi thu thập các mục cần xóa. Sau đó tôi xóa chúng. Đây là một mẫu sử dụng cú pháp Objective-C 2.0:

NSMutableArray *discardedItems = [NSMutableArray array];

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

[originalArrayOfItems removeObjectsInArray:discardedItems];

Sau đó, không có câu hỏi về việc liệu các chỉ số đang được cập nhật chính xác, hoặc các chi tiết kế toán nhỏ khác.

Chỉnh sửa để thêm:

Nó đã được lưu ý trong các câu trả lời khác rằng công thức nghịch đảo nên nhanh hơn. tức là Nếu bạn lặp qua mảng và soạn một mảng mới của các đối tượng cần giữ, thay vì các đối tượng để loại bỏ. Điều đó có thể đúng (mặc dù về bộ nhớ và chi phí xử lý để phân bổ một mảng mới và loại bỏ mảng cũ thì sao?) Nhưng ngay cả khi nó nhanh hơn thì nó cũng không phải là vấn đề lớn đối với việc triển khai ngây thơ, bởi vì NSArrays không hành xử như mảng "bình thường". Họ nói chuyện nhưng họ đi bộ khác nhau. Xem một phân tích tốt ở đây:

Công thức nghịch đảo có thể nhanh hơn, nhưng tôi không bao giờ cần quan tâm đến việc đó, bởi vì công thức trên luôn đủ nhanh cho nhu cầu của tôi.

Đối với tôi, thông điệp mang về nhà là sử dụng bất kỳ công thức nào là rõ ràng nhất đối với bạn. Tối ưu hóa chỉ khi cần thiết. Cá nhân tôi thấy công thức trên rõ ràng nhất, đó là lý do tại sao tôi sử dụng nó. Nhưng nếu công thức nghịch đảo rõ ràng hơn với bạn, hãy tìm nó.


47
Coi chừng điều này có thể tạo ra lỗi nếu các đối tượng nhiều hơn một lần trong một mảng. Để thay thế, bạn có thể sử dụng NSMutableIndex Set và - (void) removeObjectsAtIndexes.
Georg Schölly

1
Nó thực sự là nhanh hơn để làm ngược lại. Sự khác biệt hiệu năng là khá lớn, nhưng nếu mảng của bạn không lớn thì thời gian sẽ trở nên tầm thường.
dùng1032657

Tôi đã sử dụng đảo ngược ... tạo mảng như nil .. sau đó chỉ thêm những gì tôi muốn và tải lại dữ liệu ... đã thực hiện ... đã tạo dummyArray là gương của mảng chính để chúng tôi có dữ liệu thực tế chính
Fahim Parkar

Bộ nhớ thêm cần được phân bổ để làm ngược lại. Nó không phải là một thuật toán tại chỗ và không phải là một ý tưởng tốt trong một số trường hợp. Khi bạn loại bỏ trực tiếp, bạn chỉ cần dịch chuyển mảng (rất hiệu quả vì nó sẽ không di chuyển từng mảng một, nó có thể dịch chuyển một đoạn lớn), nhưng khi bạn tạo một mảng mới, bạn cần tất cả phân bổ và gán. Vì vậy, nếu bạn chỉ xóa một vài mục, nghịch đảo sẽ chậm hơn nhiều. Đó là một thuật toán có vẻ đúng.
jack

82

Thêm một biến thể. Vì vậy, bạn có được khả năng đọc và hiệu suất tốt:

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

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

[originalArrayOfItems removeObjectsAtIndexes:discardedItems];

Điều này thật tuyệt. Tôi đã cố gắng loại bỏ nhiều mục khỏi mảng và điều này hoạt động hoàn hảo. Các phương pháp khác đã gây ra vấn đề :) Cảm ơn người đàn ông
AbhinavVinay

Corey, câu trả lời này (bên dưới) cho biết, removeObjectsAtIndexeslà phương pháp tồi tệ nhất để loại bỏ các đối tượng, bạn có đồng ý với điều đó không? Tôi đang hỏi điều này bởi vì câu trả lời của bạn quá cũ. Vẫn là tốt để chọn tốt nhất?
Hemang

Tôi thích giải pháp này rất nhiều về khả năng đọc. Làm thế nào là nó về hiệu suất khi so sánh với câu trả lời @HotLicks?
Victor Maia Aldecôa

Phương pháp này nên được khuyến khích chắc chắn trong khi xóa các đối tượng khỏi mảng trong Obj-C.
Boreas

enumerateObjectsUsingBlock:sẽ giúp bạn tăng chỉ số miễn phí.
pkamb

42

Đây là một vấn đề rất đơn giản. Bạn chỉ cần lặp lại ngược lại:

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

Đây là một mô hình rất phổ biến.


sẽ bỏ lỡ câu trả lời của Jens, vì anh không viết mã: P .. thnx m8
FlowUI. SimpleUITesting.com

3
Đây là phương pháp tốt nhất. Điều quan trọng cần nhớ là biến lặp phải là một biến đã ký, mặc dù các chỉ số mảng trong Objective-C được khai báo là không dấu (tôi có thể tưởng tượng bây giờ Apple hối hận như thế nào).
mojuba

39

Một số câu trả lời khác sẽ có hiệu suất kém trên các mảng rất lớn, bởi vì các phương thức như removeObject:removeObjectsInArray:liên quan đến việc tìm kiếm tuyến tính của máy thu, điều này thật lãng phí vì bạn đã biết đối tượng ở đâu. Ngoài ra, bất kỳ lệnh gọi nào removeObjectAtIndex:cũng sẽ phải sao chép các giá trị từ chỉ mục vào cuối mảng lên theo một vị trí tại một thời điểm.

Hiệu quả hơn sẽ là như sau:

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

Vì chúng tôi đặt dung lượng itemsToKeep, chúng tôi không lãng phí bất kỳ lúc nào sao chép giá trị trong khi thay đổi kích thước. Chúng tôi không sửa đổi mảng tại chỗ, vì vậy chúng tôi có thể tự do sử dụng Bảng liệt kê nhanh. Sử dụng setArray:để thay thế nội dung arrayvới itemsToKeepsẽ có hiệu quả. Tùy thuộc vào mã của bạn, bạn thậm chí có thể thay thế dòng cuối cùng bằng:

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

Vì vậy, thậm chí không cần sao chép giá trị, chỉ trao đổi một con trỏ.


Thuật toán này chứng tỏ tốt về mặt phức tạp thời gian. Tôi đang cố gắng để hiểu nó về độ phức tạp không gian. Tôi có cùng cách triển khai xung quanh nơi tôi đặt dung lượng cho 'itemsToKeep' với 'Số lượng mảng' thực tế. Nhưng nói rằng tôi không muốn một vài đối tượng từ mảng vì vậy tôi không thêm nó vào itemsToKeep. Vì vậy, dung lượng vật phẩm của tôi ToKeep là 10, nhưng tôi thực sự chỉ lưu trữ 6 đối tượng trong đó. Điều này có nghĩa là tôi đang lãng phí không gian của 4 đối tượng? PS không tìm thấy lỗi, chỉ cố gắng để hiểu sự phức tạp của thuật toán. :)
tech_human

Có, bạn có không gian dành riêng cho bốn con trỏ mà bạn không sử dụng. Hãy nhớ rằng mảng chỉ chứa con trỏ, không phải chính các đối tượng, vì vậy đối với bốn đối tượng có nghĩa là 16 byte (kiến trúc 32 bit) hoặc 32 byte (64 bit).
benzado

Lưu ý rằng bất kỳ giải pháp nào sẽ có nhiều nhất yêu cầu 0 không gian bổ sung (bạn xóa các mục tại chỗ) hoặc tệ nhất là gấp đôi kích thước mảng ban đầu (vì bạn tạo một bản sao của mảng). Một lần nữa, vì chúng ta đang xử lý các con trỏ và không phải là bản sao đầy đủ của các đối tượng, nên điều này khá rẻ trong hầu hết các trường hợp.
benzado

Được rồi, đã rõ. Cảm ơn Benzado !!
tech_human

Theo bài đăng này , gợi ý của mảngWithCapacity: phương thức về kích thước của mảng không thực sự được sử dụng.
Kremk

28

Bạn có thể sử dụng NSpredicate để xóa các mục khỏi mảng có thể thay đổi của bạn. Điều này đòi hỏi không có vòng lặp.

Ví dụ: nếu bạn có một tên NSMutableArray, bạn có thể tạo một vị từ như thế này:

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

Dòng sau sẽ để lại cho bạn một mảng chỉ chứa các tên bắt đầu bằng b.

[namesArray filterUsingPredicate:caseInsensitiveBNames];

Nếu bạn gặp khó khăn khi tạo các vị từ bạn cần, hãy sử dụng liên kết nhà phát triển apple này .


3
Yêu cầu không nhìn thấy cho các vòng lặp. Có một vòng lặp trong hoạt động của bộ lọc, bạn không nhìn thấy nó.
Hot Licks

18

Tôi đã làm một bài kiểm tra hiệu suất bằng 4 phương pháp khác nhau. Mỗi bài kiểm tra lặp lại qua tất cả các phần tử trong một mảng phần tử 100.000 và loại bỏ mọi mục thứ 5. Kết quả không thay đổi nhiều với / không có tối ưu hóa. Những điều này đã được thực hiện trên iPad 4:

(1) removeObjectAtIndex:- 271 ms

(2) removeObjectsAtIndexes:- 1010 ms (vì việc xây dựng bộ chỉ mục mất ~ 700 ms; nếu không, điều này về cơ bản giống như cách gọi removeObjectAtIndex: cho mỗi mục)

(3) removeObjects:- 326 ms

(4) tạo một mảng mới với các đối tượng vượt qua bài kiểm tra - 17 ms

Vì vậy, tạo ra một mảng mới là nhanh nhất. Các phương thức khác đều có thể so sánh được, ngoại trừ việc sử dụng removeObjectsAtIndexes: sẽ tệ hơn với nhiều mục cần xóa hơn, vì thời gian cần thiết để xây dựng bộ chỉ mục.


1
Bạn đã đếm thời gian để tạo một mảng mới và phân bổ nó sau đó. Tôi không tin rằng nó có thể làm điều đó một mình trong 17 ms. Cộng 75.000 giao?
jack

Thời gian cần thiết để tạo và phân bổ một mảng mới là tối thiểu
user1032657

Bạn rõ ràng đã không đo được sơ đồ vòng lặp ngược.
Hot Licks

Thử nghiệm đầu tiên tương đương với sơ đồ vòng lặp ngược.
dùng1032657

Điều gì xảy ra khi bạn bị bỏ lại với newArray của bạn và tiền tố của bạn bị hủy? Bạn sẽ phải sao chép newArray vào tên PrevArray được đặt tên (hoặc đổi tên nó) nếu không thì mã khác của bạn sẽ tham chiếu nó như thế nào?
aremvee

17

Sử dụng vòng lặp đếm ngược trên các chỉ số:

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

hoặc tạo một bản sao với các đối tượng bạn muốn giữ.

Cụ thể, không sử dụng for (id object in array)vòng lặp hoặc NSEnumerator.


3
bạn nên viết câu trả lời của bạn trong mã m8. haha gần như đã bỏ lỡ nó
FlowUI. SimpleUITesting.com

Điều này không chính xác. "for (id object in mảng)" nhanh hơn "for (NSInteger i = Array.count - 1; i> = 0; --i)" và được gọi là lặp nhanh. Sử dụng iterator chắc chắn là nhanh hơn so với lập chỉ mục.
jack

@jack - Nhưng nếu bạn loại bỏ một phần tử từ bên dưới một trình vòng lặp, bạn thường tạo sự tàn phá. (Và "lặp lại nhanh" không phải lúc nào cũng nhanh hơn nhiều.)
Hot Licks

Câu trả lời là từ năm 2008. Lặp lại nhanh không tồn tại.
Jens Ayton

12

Đối với iOS 4+ hoặc OS X 10.6+, Apple đã thêm passingTesthàng loạt API vào NSMutableArray, như – indexesOfObjectsPassingTest:. Một giải pháp với API như vậy sẽ là:

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

12

Ngày nay bạn có thể sử dụng phép liệt kê dựa trên khối đảo ngược. Một mã ví dụ đơn giản:

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

Kết quả:

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

một tùy chọn khác chỉ với một dòng mã:

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

Có đảm bảo rằng mảng sẽ không được phân bổ lại?
Cfr

Bạn có ý nghĩa gì với việc tái phân bổ?
vikingosegundo

Trong khi loại bỏ phần tử khỏi cấu trúc tuyến tính, có thể có hiệu quả để phân bổ khối bộ nhớ liền kề mới và di chuyển tất cả các phần tử ở đó. Trong trường hợp này, tất cả các con trỏ sẽ bị vô hiệu.
Cfr

Vì thao tác xóa sẽ không biết có bao nhiêu phần tử sẽ bị xóa vào cuối, nên sẽ không có ý nghĩa gì trong quá trình xóa. Dù sao: chi tiết thực hiện, w phải tin tưởng táo để viết mã hợp lý.
vikingosegundo

Thứ nhất là không đẹp hơn (vì bạn phải suy nghĩ nhiều về cách nó hoạt động ở nơi đầu tiên) và thứ hai là không tái cấu trúc an toàn. Vì vậy, cuối cùng tôi đã kết thúc với giải pháp được chấp nhận cổ điển mà thực sự khá dễ đọc.
Renetik

8

Theo cách khai báo hơn, tùy thuộc vào tiêu chí phù hợp với các mục cần loại bỏ, bạn có thể sử dụng:

[theArray filterUsingPredicate:aPredicate]

@Nathan nên rất hiệu quả


6

Đây là cách dễ dàng và sạch sẽ. Tôi muốn nhân đôi mảng của mình ngay trong lệnh gọi liệt kê nhanh:

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

Bằng cách này, bạn liệt kê thông qua một bản sao của mảng bị xóa khỏi, cả hai đều giữ cùng một đối tượng. Một NSArray chỉ giữ các con trỏ đối tượng nên đây là bộ nhớ / hiệu năng hoàn toàn tốt.


2
Hoặc thậm chí đơn giản hơn -for (LineItem *item in self.lineItems.copy)
Alexandre G

5

Thêm các đối tượng bạn muốn xóa vào mảng thứ hai và sau vòng lặp, sử dụng -removeObjectsInArray :.


5

điều này nên làm điều đó:

    NSMutableArray* myArray = ....;

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

hi vọng điêu nay co ich...


Mặc dù không chính thống, tôi đã thấy lặp đi lặp lại và xóa khi tôi trở thành một giải pháp đơn giản và sạch sẽ. Thường là một trong những phương pháp nhanh nhất.
rpetrich

Có chuyện gì với cái này vậy? Nó sạch sẽ, nhanh chóng, dễ đọc và nó hoạt động như một bùa mê. Đối với tôi nó giống như câu trả lời tốt nhất. Tại sao nó có điểm âm? Am i thiếu cái gì ở đây?
Steph Thirion

1
@Steph: Câu hỏi nêu rõ "một cái gì đó thanh lịch hơn là cập nhật chỉ mục thủ công."
Steve Madsen

1
Oh. Tôi đã bỏ lỡ phần I-hoàn toàn không muốn cập nhật chỉ mục thủ công. cảm ơn steve IMO giải pháp này thanh lịch hơn so với giải pháp được chọn (không cần mảng tạm thời), vì vậy những phiếu bầu tiêu cực chống lại nó cảm thấy không công bằng.
Steph Thirion

@Steve: Nếu bạn kiểm tra các chỉnh sửa, phần đó đã được thêm vào sau khi tôi gửi câu trả lời của mình .... nếu không, tôi sẽ trả lời với "Lặp lại ngược là giải pháp thanh lịch nhất" :). Chúc một ngày tốt lành!
Pokot0

1

Tại sao bạn không thêm các đối tượng cần xóa vào NSMutableArray khác. Khi bạn kết thúc việc lặp lại, bạn có thể xóa các đối tượng mà bạn đã thu thập.


1

Làm thế nào về việc hoán đổi các phần tử bạn muốn xóa với phần tử thứ n, phần tử thứ 1 và vân vân?

Khi bạn hoàn tất, bạn thay đổi kích thước mảng thành 'kích thước trước - số lần hoán đổi'


1

Nếu tất cả các đối tượng trong mảng của bạn là duy nhất hoặc bạn muốn xóa tất cả các lần xuất hiện của một đối tượng khi tìm thấy, bạn có thể liệt kê nhanh trên một bản sao mảng và sử dụng [NSMutableArray removeObject:] để xóa đối tượng khỏi bản gốc.

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

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

Điều gì xảy ra nếu myArray ban đầu được cập nhật trong khi +arrayWithArrayđang được thực thi?
bioffe

1
@bioffe: Sau đó, bạn có một lỗi trong mã của bạn. NSMutableArray không an toàn cho chuỗi, bạn sẽ kiểm soát truy cập thông qua các khóa. Xem câu trả lời này
dreamlax

1

anwser của benzado ở trên là những gì bạn nên làm cho khuôn mẫu. Trong một trong các ứng dụng của tôi, removeObjectsInArray mất 1 phút để chạy, chỉ cần thêm vào một mảng mới mất 0,23 giây.


1

Tôi xác định một danh mục cho phép tôi lọc bằng cách sử dụng một khối, như thế này:

@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

mà sau đó có thể được sử dụng như thế này:

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

1

Việc triển khai tốt hơn có thể là sử dụng phương thức thể loại dưới đây trên 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

Khối vị ngữ có thể được thực hiện để xử lý trên từng đối tượng trong mảng. Nếu vị ngữ trả về true, đối tượng sẽ bị xóa.

Một ví dụ cho mảng ngày để xóa tất cả các ngày nằm trong quá khứ:

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

0

Lặp lại ngược-ly là sở thích của tôi trong nhiều năm, nhưng trong một thời gian dài, tôi chưa bao giờ gặp phải trường hợp đối tượng 'sâu nhất' (tính cao nhất) bị loại bỏ trước tiên. Trong giây lát trước khi con trỏ chuyển sang chỉ mục tiếp theo, không có gì và nó bị hỏng.

Cách của Benzado là gần nhất với những gì tôi làm bây giờ nhưng tôi không bao giờ nhận ra rằng sẽ có sự cải tổ ngăn xếp sau mỗi lần xóa.

trong Xcode 6, công việc này

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];
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.