Apa Cara Terbaik untuk Mengocok NSMutableArray?

187

Jika Anda memiliki NSMutableArray, bagaimana Anda mengocok elemen secara acak?

(Saya punya jawaban sendiri untuk ini, yang diposting di bawah ini, tetapi saya baru mengenal Cocoa dan saya tertarik untuk mengetahui apakah ada cara yang lebih baik.)


Pembaruan: Seperti dicatat oleh @Mukesh, pada iOS 10+ dan macOS 10.12+, ada -[NSMutableArray shuffledArray]metode yang dapat digunakan untuk mengacak. Lihat https://developer.apple.com/documentation/foundation/nsarray/1640855-shuffledarray?language=objc untuk detailnya. (Tetapi perhatikan bahwa ini menciptakan array baru, daripada mengocok elemen di tempat.)

Kristopher Johnson
sumber
Lihatlah pertanyaan ini: Masalah dunia nyata dengan pengocokan naif sehubungan dengan algoritma pengocokan Anda.
craigb
Berikut ini adalah implementasi di Swift: iosdevelopertips.com/swift-code/swift-shuffle-array-type.html
Kristopher Johnson
5
Yang terbaik saat ini adalah Fisher-Yates :for (NSUInteger i = self.count; i > 1; i--) [self exchangeObjectAtIndex:i - 1 withObjectAtIndex:arc4random_uniform((u_int32_t)i)];
Cœur
2
Guys, dari iOS 10 ++ konsep baru array shuffle yang diberikan oleh Apple, Lihat jawaban ini
Mukesh
Masalah dengan yang sudah ada APIadalah ia mengembalikan Arrayyang alamat baru ke lokasi baru di memori.
TheTiger

Jawaban:

349

Saya memecahkan ini dengan menambahkan kategori ke NSMutableArray.

Sunting: Menghapus metode yang tidak perlu berkat jawaban oleh Ladd.

Sunting: Diubah (arc4random() % nElements)menjadi arc4random_uniform(nElements)berkat oleh jawaban dari Gregory Goltsov dan komentar oleh miho dan blahdiblah

Sunting: Perbaikan lingkaran, terima kasih atas komentar oleh Ron

Sunting: Ditambahkan, periksa bahwa array tidak kosong, terima kasih atas komentar dari 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
Kristopher Johnson
sumber
10
Solusi bagus Dan ya, seperti yang disebutkan oleh willc2, mengganti random () dengan arc4random () adalah perbaikan yang bagus karena tidak diperlukan seeding.
Jason Moore
4
@Jason: Terkadang (misalnya saat pengujian), bisa memasok benih adalah hal yang baik. Kristopher: algoritma yang bagus. Ini merupakan implementasi dari algoritma Fisher-Yates: en.wikipedia.org/wiki/Fisher-Yates_shuffle
JeremyP
4
Peningkatan super minor : dalam iterasi terakhir dari loop, i == count - 1. Apakah itu tidak berarti bahwa kita bertukar objek pada indeks i dengan dirinya sendiri? Bisakah kita mengubah kode untuk selalu melewati iterasi terakhir?
Ron
10
Apakah Anda menganggap koin dibalik hanya jika hasilnya kebalikan dari sisi yang semula naik?
Kristopher Johnson
4
Acak ini bias secara halus. Gunakan arc4random_uniform(nElements)sebagai ganti arc4random()%nElements. Lihat halaman manual arc4random dan penjelasan tentang bias modulo ini untuk informasi lebih lanjut.
blahdiblah
38

Karena saya belum bisa berkomentar, saya pikir saya akan berkontribusi dalam tanggapan penuh. Saya memodifikasi implementasi Kristopher Johnson untuk proyek saya dalam beberapa cara (benar-benar berusaha membuatnya sesingkat mungkin), salah satunya adalah arc4random_uniform()karena ia menghindari bias 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
gregoltsov
sumber
2
Perhatikan bahwa Anda menelepon [self count](pengambil properti) dua kali pada setiap iterasi melalui loop. Saya pikir memindahkannya keluar dari loop bernilai kehilangan keringkasan.
Kristopher Johnson
1
Dan itu sebabnya saya masih lebih suka [object method]daripada object.method: orang cenderung lupa bahwa nanti tidak semurah mengakses anggota struct, ia datang dengan biaya pemanggilan metode ... sangat buruk dalam satu lingkaran.
DarkDust
Terima kasih atas koreksinya - Saya salah menganggap penghitungan di-cache, untuk beberapa alasan. Diperbarui jawabannya.
gregoltsov
10

Jika Anda mengimpor GameplayKit, ada shuffledAPI:

https://developer.apple.com/reference/foundation/nsarray/1640855-shuffled

let shuffledArray = array.shuffled()
andreacipriani
sumber
Saya memiliki myArray dan ingin membuat shuffleArray baru. Bagaimana saya melakukannya dengan Objective - C?
Omkar Jadhav
shuffledArray = [array shuffledArray];
andreacipriani
Perhatikan bahwa metode ini adalah bagian dari GameplayKitjadi Anda harus mengimpornya.
AnthoPak
9

Solusi yang sedikit lebih baik dan ringkas (dibandingkan dengan jawaban teratas).

Algoritmanya sama dan dijelaskan dalam literatur sebagai " Fisher-Yates shuffle ".

Dalam Objective-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

Dalam Swift 3.2 dan 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))))
        }
    }
}

Di Swift 3.0 dan 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])
        }
    }
}

Catatan: Solusi yang lebih ringkas di Swift dimungkinkan dari penggunaan iOS10 GameplayKit.

Catatan: Algoritma untuk pengocokan tidak stabil (dengan semua posisi dipaksa untuk berubah jika jumlah> 1) juga tersedia

Cur
sumber
Apa perbedaan antara ini dan algoritma Kristopher Johnson?
Iulian Onofrei
@IulianOnofrei, awalnya, kode Kristopher Johnson tidak optimal dan saya memperbaiki jawabannya, kemudian diedit lagi dengan beberapa pemeriksaan awal yang tidak berguna ditambahkan. Saya lebih suka cara penulisan singkat saya. Algoritmanya sama dan dijelaskan dalam literatur sebagai " Fisher-Yates shuffle ".
Cœur
6

Ini adalah cara termudah dan tercepat untuk mengocok NSArrays atau NSMutableArrays (puzzle objek adalah NSMutableArray, ini berisi objek puzzle. Saya telah menambahkan indeks variabel objek puzzle yang menunjukkan posisi awal dalam array)

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

output log:

 #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

Anda juga dapat membandingkan obj1 dengan obj2 dan memutuskan apa yang ingin Anda kembalikan nilai yang mungkin adalah:

  • NSOrderedAscending = -1
  • NSOrderedSame = 0
  • NSOrderedDescending = 1

sumber
1
Juga untuk solusi ini, gunakan arc4random () atau seed.
Johan Kool
17
Acak ini cacat - karena Microsoft baru-baru ini diingatkan tentang: robweir.com/blog/2010/02/microsoft-random-browser-ballot.html .
Raphael Schweikert
Setuju, cacat karena "pengurutan memerlukan definisi pemesanan yang konsisten" seperti yang ditunjukkan dalam artikel tentang MS. Terlihat elegan, tetapi tidak.
Jeff
2

Ada perpustakaan populer yang bagus, yang memiliki metode ini sebagai bagiannya, disebut SSToolKit di GitHub . File NSMutableArray + SSToolkitAdditions.h berisi metode acak. Anda juga bisa menggunakannya. Di antara ini, tampaknya ada banyak hal berguna.

Halaman utama perpustakaan ini adalah sini .

Jika Anda menggunakan ini, kode Anda akan seperti ini:

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

Perpustakaan ini juga memiliki Pod (lihat CocoaPods)

Denis Kutlubaev
sumber
2

Dari iOS 10, Anda dapat menggunakan NSArray shuffled()dari GameplayKit . Inilah pembantu untuk Array di 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())
    }
}
Cur
sumber
1

Jika elemen memiliki pengulangan.

misalnya array: AAABB atau BBAAA

satu-satunya solusi adalah: ABABA

sequenceSelected adalah NSMutableArray yang menyimpan elemen obj kelas, yang merupakan petunjuk untuk beberapa urutan.

- (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;
                }
            }
        }
    }
}
Gamma-Point
sumber
menggunakan staticpencegah yang bekerja pada banyak contoh: akan jauh lebih aman dan mudah dibaca untuk menggunakan dua metode, yang utama mengacak dan memanggil metode sekunder, sedangkan metode sekunder hanya memanggil dirinya sendiri dan tidak pernah melakukan perombakan ulang. Juga ada kesalahan pengejaan.
Cœur
-1
NSUInteger randomIndex = arc4random() % [theArray count];
kal
sumber
2
atau arc4random_uniform([theArray count])akan lebih baik lagi, jika tersedia pada versi Mac OS X atau iOS yang Anda dukung.
Kristopher Johnson
1
Saya kami berikan seperti ini nomor akan ulangi.
Vineesh TP
-1

Jawaban Kristopher Johnson cukup bagus, tetapi itu tidak sepenuhnya acak.

Diberikan array dari 2 elemen, fungsi ini selalu mengembalikan array terbalik, karena Anda menghasilkan rentang acak Anda selama sisa indeks. shuffle()Seperti fungsi yang lebih akurat

- (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];
       }
   }
}
fcortes
sumber
Saya pikir algoritma yang Anda sarankan adalah "shuffle naif". Lihat blog.codinghorror.com/the-hanger-of-naivete . Saya pikir jawaban saya memiliki peluang 50% untuk menukar elemen jika hanya ada dua: ketika saya nol, arc4random_uniform (2) akan mengembalikan 0 atau 1, sehingga elemen nol akan ditukar dengan dirinya sendiri atau ditukar dengan yang lain. elemen. Pada iterasi berikutnya, ketika i adalah 1, arc4random (1) akan selalu mengembalikan 0, dan elemen ith akan selalu ditukar dengan dirinya sendiri, yang tidak efisien tetapi tidak salah. (Mungkin kondisi loop seharusnya i < (count-1).)
Kristopher Johnson
-2

Sunting: Ini tidak benar. Untuk tujuan referensi, saya tidak menghapus posting ini. Lihat komentar tentang alasan mengapa pendekatan ini tidak benar.

Kode sederhana di sini:

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