Cách tốt nhất để xáo trộn NSMutableArray là gì?


187

Nếu bạn có một NSMutableArray, làm thế nào để bạn xáo trộn các yếu tố ngẫu nhiên?

(Tôi có câu trả lời của riêng mình cho vấn đề này, được đăng dưới đây, nhưng tôi mới biết về ca cao và tôi muốn biết liệu có cách nào tốt hơn không.)


Cập nhật: Theo ghi nhận của @Mukesh, kể từ iOS 10+ và macOS 10.12+, có một -[NSMutableArray shuffledArray]phương pháp có thể được sử dụng để xáo trộn. Xem https://developer.apple.com/documentation/foundation/nsarray/1640855-shuffledarray?lingu=objc để biết chi tiết. (Nhưng lưu ý rằng điều này tạo ra một mảng mới, thay vì xáo trộn các phần tử tại chỗ.)


Hãy xem câu hỏi này: Các vấn đề trong thế giới thực với sự xáo trộn ngây thơ liên quan đến thuật toán xáo trộn của bạn.
craigb


5
Tốt nhất hiện tại là Fisher-Yates :for (NSUInteger i = self.count; i > 1; i--) [self exchangeObjectAtIndex:i - 1 withObjectAtIndex:arc4random_uniform((u_int32_t)i)];
Cur

2
Các bạn, từ iOS 10 ++ khái niệm mới về mảng xáo trộn do Apple đưa ra, Hãy xem câu trả lời này
Mukesh

Vấn đề với hiện tại APIlà nó trả về một Arrayđịa chỉ mới đến vị trí mới trong bộ nhớ.
TheTiger

Câu trả lời:



349

Tôi đã giải quyết điều này bằng cách thêm một danh mục vào NSMutableArray.

Chỉnh sửa: Đã xóa phương thức không cần thiết nhờ câu trả lời của Ladd.

Chỉnh sửa: Thay đổi (arc4random() % nElements)để arc4random_uniform(nElements)cảm ơn câu trả lời của Gregory Goltsov và nhận xét của miho và blahdibmus

Chỉnh sửa: Cải thiện vòng lặp, nhờ nhận xét của Ron

Chỉnh sửa: Đã thêm kiểm tra mảng đó không trống, nhờ nhận xét của Mahesh Agrawal

//  NSMutableArray_Shuffling.h

#if TARGET_OS_IPHONE
#import <UIKit/UIKit.h>
#else
#include <Cocoa/Cocoa.h>
#endif

// This category enhances NSMutableArray by providing
// methods to randomly shuffle the elements.
@interface NSMutableArray (Shuffling)
- (void)shuffle;
@end


//  NSMutableArray_Shuffling.m

#import "NSMutableArray_Shuffling.h"

@implementation NSMutableArray (Shuffling)

- (void)shuffle
{
    NSUInteger count = [self count];
    if (count <= 1) return;
    for (NSUInteger i = 0; i < count - 1; ++i) {
        NSInteger remainingCount = count - i;
        NSInteger exchangeIndex = i + arc4random_uniform((u_int32_t )remainingCount);
        [self exchangeObjectAtIndex:i withObjectAtIndex:exchangeIndex];
    }
}

@end

10
Giải pháp tốt đẹp. Và có, như willc2 đề cập, thay thế ngẫu nhiên () bằng arc4random () là một cải tiến tốt vì không cần phải gieo hạt.
Jason Moore

4
@Jason: Đôi khi (ví dụ khi thử nghiệm), có thể cung cấp hạt giống là một điều tốt. Kristopher: thuật toán tốt đẹp. Đây là một triển khai của thuật toán Fisher-Yates: en.wikipedia.org/wiki/Fisher-Yates_shuffle
JeremyP

4
Một cải tiến siêu nhỏ : trong lần lặp cuối cùng của vòng lặp, i == đếm - 1. Điều đó không có nghĩa là chúng ta đang trao đổi đối tượng ở chỉ mục i với chính nó? Chúng ta có thể điều chỉnh mã để luôn bỏ qua lần lặp cuối không?
Ron

10
Bạn có xem xét một đồng xu chỉ được lật nếu kết quả ngược lại với mặt ban đầu không?
Kristopher Johnson

4
Shuffle này là thiên vị tinh tế. Sử dụng arc4random_uniform(nElements)thay vì arc4random()%nElements. Xem trang man arc4randomgiải thích về độ lệch modulo để biết thêm.
blahdibmus

38

Vì tôi chưa thể bình luận, tôi nghĩ rằng tôi đã đóng góp một phản hồi đầy đủ. Tôi đã sửa đổi triển khai của Kristopher Johnson cho dự án của mình theo một số cách (thực sự cố gắng làm cho nó ngắn gọn nhất có thể), một trong số đó là arc4random_uniform()vì nó tránh được sự thiên vị modulo .

// NSMutableArray+Shuffling.h
#import <Foundation/Foundation.h>

/** This category enhances NSMutableArray by providing methods to randomly
 * shuffle the elements using the Fisher-Yates algorithm.
 */
@interface NSMutableArray (Shuffling)
- (void)shuffle;
@end

// NSMutableArray+Shuffling.m
#import "NSMutableArray+Shuffling.h"

@implementation NSMutableArray (Shuffling)

- (void)shuffle
{
    NSUInteger count = [self count];
    for (uint i = 0; i < count - 1; ++i)
    {
        // Select a random element between i and end of array to swap with.
        int nElements = count - i;
        int n = arc4random_uniform(nElements) + i;
        [self exchangeObjectAtIndex:i withObjectAtIndex:n];
    }
}

@end

2
Lưu ý rằng bạn đang gọi [self count](một getter thuộc tính) hai lần trên mỗi lần lặp thông qua vòng lặp. Tôi nghĩ rằng việc di chuyển nó ra khỏi vòng lặp có giá trị mất đi sự đồng nhất.
Kristopher Johnson

1
Và đó là lý do tại sao tôi vẫn thích [object method]thay vì object.method: mọi người có xu hướng quên rằng sau này không rẻ bằng việc truy cập một thành viên cấu trúc, nó đi kèm với chi phí của một cuộc gọi phương thức ... rất tệ trong một vòng lặp.
DarkDust

Cảm ơn bạn đã sửa chữa - vì một số lý do tôi đã giả định rằng bộ nhớ cache đã bị lưu vào bộ nhớ cache. Cập nhật câu trả lời.
gregoltsov

10

Nếu bạn nhập GameplayKit, có shuffledAPI:

https://developer.apple.com/reference/foundation/nsarray/1640855- xáo trộn

let shuffledArray = array.shuffled()

Tôi có myArray và muốn tạo một shuffleArray mới của nó. Làm thế nào để tôi làm điều đó với Objective - C?
Omkar Jadhav

shuffledArray = [array shuffledArray];
andreacipriani

Lưu ý rằng phương pháp này là một phần của GameplayKitvì vậy bạn phải nhập nó.
AnthoPak

9

Một giải pháp cải tiến và súc tích một chút (so với các câu trả lời hàng đầu).

Thuật toán này giống nhau và được mô tả trong tài liệu là " Ngẫu nhiên Fisher-Yates ".

Trong Mục tiêu-C:

@implementation NSMutableArray (Shuffle)
// Fisher-Yates shuffle
- (void)shuffle
{
    for (NSUInteger i = self.count; i > 1; i--)
        [self exchangeObjectAtIndex:i - 1 withObjectAtIndex:arc4random_uniform((u_int32_t)i)];
}
@end

Trong Swift 3.2 và 4.x:

extension Array {
    /// Fisher-Yates shuffle
    mutating func shuffle() {
        for i in stride(from: count - 1, to: 0, by: -1) {
            swapAt(i, Int(arc4random_uniform(UInt32(i + 1))))
        }
    }
}

Trong Swift 3.0 và 3.1:

extension Array {
    /// Fisher-Yates shuffle
    mutating func shuffle() {
        for i in stride(from: count - 1, to: 0, by: -1) {
            let j = Int(arc4random_uniform(UInt32(i + 1)))
            (self[i], self[j]) = (self[j], self[i])
        }
    }
}

Lưu ý: Có thể sử dụng giải pháp ngắn gọn hơn trong Swift từ iOS10 GameplayKit.

Lưu ý: Thuật toán xáo trộn không ổn định (với tất cả các vị trí buộc phải thay đổi nếu số lượng> 1) cũng có sẵn


Điều gì sẽ là sự khác biệt giữa thuật toán này và thuật toán của Kristopher Johnson?
Iulian Onofrei

@IulianOnofrei, ban đầu, mã của Kristopher Johnson không tối ưu và tôi đã cải thiện câu trả lời của mình, sau đó nó được chỉnh sửa lại với một số kiểm tra ban đầu vô dụng được thêm vào. Tôi thích cách viết súc tích của tôi. Thuật toán này giống nhau và được mô tả trong tài liệu là " Ngẫu nhiên Fisher-Yates ".
Cœur

6

Đây là cách đơn giản nhất và nhanh nhất để xáo trộn NSArrays hoặc NSMutableArrays (câu đố đối tượng là NSMutableArray, nó chứa các đối tượng câu đố. Tôi đã thêm vào chỉ số biến đối tượng câu đố chỉ ra vị trí ban đầu trong mảng)

int randomSort(id obj1, id obj2, void *context ) {
        // returns random number -1 0 1
    return (random()%3 - 1);    
}

- (void)shuffle {
        // call custom sort function
    [puzzles sortUsingFunction:randomSort context:nil];

    // show in log how is our array sorted
        int i = 0;
    for (Puzzle * puzzle in puzzles) {
        NSLog(@" #%d has index %d", i, puzzle.index);
        i++;
    }
}

đầu ra nhật ký:

 #0 has index #6
 #1 has index #3
 #2 has index #9
 #3 has index #15
 #4 has index #8
 #5 has index #0
 #6 has index #1
 #7 has index #4
 #8 has index #7
 #9 has index #12
 #10 has index #14
 #11 has index #16
 #12 has index #17
 #13 has index #10
 #14 has index #11
 #15 has index #13
 #16 has index #5
 #17 has index #2

bạn cũng có thể so sánh obj1 với obj2 và quyết định những gì bạn muốn trả về các giá trị có thể là:

  • NSOrderedAsceinating = -1
  • NSOrderedSame = 0
  • NSOrderedDesceinating = 1

1
Ngoài ra đối với giải pháp này, sử dụng arc4random () hoặc hạt giống.
Johan Kool

17
Sự xáo trộn này là thiếu sót - như Microsoft gần đây đã được nhắc nhở: robweir.com/blog/2010/02/microsoft-random-browser-ballot.html .
Raphael Schweikert

Đồng ý, thiếu sót vì "sắp xếp đòi hỏi một định nghĩa tự đặt hàng" như đã chỉ ra trong bài báo đó về MS. Trông thanh lịch, nhưng không.
Jeff

2

Có một thư viện phổ biến đẹp, có phương thức này là một phần, được gọi là SSToolKit trong GitHub . Tệp NSMutableArray + SSToolkitAdditions.h chứa phương thức xáo trộn. Bạn cũng có thể sử dụng nó. Trong số này, dường như có rất nhiều điều hữu ích.

Trang chính của thư viện này là ở đây .

Nếu bạn sử dụng cái này, mã của bạn sẽ như thế này:

#import <SSCategories.h>
NSMutableArray *tableData = [NSMutableArray arrayWithArray:[temp shuffledArray]];

Thư viện này cũng có một Pod (xem Cốc Cốc)


2

Từ iOS 10, bạn có thể sử dụng NSArray shuffled()từ GameplayKit . Đây là một trợ giúp cho Array trong Swift 3:

import GameplayKit

extension Array {
    @available(iOS 10.0, macOS 10.12, tvOS 10.0, *)
    func shuffled() -> [Element] {
        return (self as NSArray).shuffled() as! [Element]
    }
    @available(iOS 10.0, macOS 10.12, tvOS 10.0, *)
    mutating func shuffle() {
        replaceSubrange(0..<count, with: shuffled())
    }
}

1

Nếu các yếu tố có lặp lại.

ví dụ: mảng: AAABB hoặc BBAAA

Giải pháp duy nhất là: ABABA

sequenceSelected là một NSMutableArray lưu trữ các phần tử của lớp obj, là con trỏ tới một chuỗi nào đó.

- (void)shuffleSequenceSelected {
    [sequenceSelected shuffle];
    [self shuffleSequenceSelectedLoop];
}

- (void)shuffleSequenceSelectedLoop {
    NSUInteger count = sequenceSelected.count;
    for (NSUInteger i = 1; i < count-1; i++) {
        // Select a random element between i and end of array to swap with.
        NSInteger nElements = count - i;
        NSInteger n;
        if (i < count-2) { // i is between second  and second last element
            obj *A = [sequenceSelected objectAtIndex:i-1];
            obj *B = [sequenceSelected objectAtIndex:i];
            if (A == B) { // shuffle if current & previous same
                do {
                    n = arc4random_uniform(nElements) + i;
                    B = [sequenceSelected objectAtIndex:n];
                } while (A == B);
                [sequenceSelected exchangeObjectAtIndex:i withObjectAtIndex:n];
            }
        } else if (i == count-2) { // second last value to be shuffled with last value
            obj *A = [sequenceSelected objectAtIndex:i-1];// previous value
            obj *B = [sequenceSelected objectAtIndex:i]; // second last value
            obj *C = [sequenceSelected lastObject]; // last value
            if (A == B && B == C) {
                //reshufle
                sequenceSelected = [[[sequenceSelected reverseObjectEnumerator] allObjects] mutableCopy];
                [self shuffleSequenceSelectedLoop];
                return;
            }
            if (A == B) {
                if (B != C) {
                    [sequenceSelected exchangeObjectAtIndex:i withObjectAtIndex:count-1];
                } else {
                    // reshuffle
                    sequenceSelected = [[[sequenceSelected reverseObjectEnumerator] allObjects] mutableCopy];
                    [self shuffleSequenceSelectedLoop];
                    return;
                }
            }
        }
    }
}

sử dụng một staticngăn chặn làm việc trên nhiều trường hợp: sẽ an toàn hơn và dễ đọc hơn khi sử dụng hai phương thức, một phương thức chính thực hiện xáo trộn và gọi phương thức phụ, trong khi phương thức phụ chỉ tự gọi và không bao giờ chia sẻ lại. Ngoài ra có một lỗi chính tả.
Cœur

-1
NSUInteger randomIndex = arc4random() % [theArray count];

2
hoặc arc4random_uniform([theArray count])sẽ tốt hơn nữa, nếu có trên phiên bản Mac OS X hoặc iOS mà bạn đang hỗ trợ.
Kristopher Johnson

1
Tôi đã đưa ra như thế này con số sẽ lặp lại.
Vineesh TP

-1

Câu trả lời của Kristopher Johnson khá hay, nhưng nó không hoàn toàn ngẫu nhiên.

Cho một mảng gồm 2 phần tử, hàm này trả về luôn là mảng nghịch đảo, bởi vì bạn đang tạo phạm vi ngẫu nhiên của mình so với các chỉ mục còn lại. Một shuffle()chức năng chính xác hơn sẽ như thế nào

- (void)shuffle
{
   NSUInteger count = [self count];
   for (NSUInteger i = 0; i < count; ++i) {
       NSInteger exchangeIndex = arc4random_uniform(count);
       if (i != exchangeIndex) {
            [self exchangeObjectAtIndex:i withObjectAtIndex:exchangeIndex];
       }
   }
}

Tôi nghĩ rằng thuật toán mà bạn đề xuất là một "shuffle ngây thơ". Xem blog.codinghorror.com/the-danger-of-naivete . Tôi nghĩ rằng câu trả lời của tôi có 50% cơ hội hoán đổi các phần tử nếu chỉ có hai: khi tôi bằng 0, arc4random_uniform (2) sẽ trả về 0 hoặc 1, vì vậy phần tử zeroth sẽ được trao đổi với chính nó hoặc trao đổi với oneth thành phần. Ở lần lặp tiếp theo, khi i là 1, arc4random (1) sẽ luôn trả về 0 và phần tử thứ i sẽ luôn được trao đổi với chính nó, không hiệu quả nhưng không sai. (Có lẽ điều kiện vòng lặp phải là i < (count-1).)
Kristopher Johnson

-2

Chỉnh sửa: Điều này không chính xác. Đối với mục đích tham khảo, tôi đã không xóa bài viết này. Xem bình luận về lý do tại sao phương pháp này không chính xác.

Mã đơn giản ở đây:

- (NSArray *)shuffledArray:(NSArray *)array
{
    return [array sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
        if (arc4random() % 2) {
            return NSOrderedAscending;
        } else {
            return NSOrderedDescending;
        }
    }];
}

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.