Qual è il modo migliore per mescolare un NSMutableArray?


187

Se hai un NSMutableArray , come mescoli gli elementi in modo casuale?

(Ho una mia risposta per questo, che è pubblicata di seguito, ma sono nuovo di Cocoa e sono interessato a sapere se esiste un modo migliore.)


Aggiornamento: come notato da @Mukesh, a partire da iOS 10+ e macOS 10.12+, esiste un -[NSMutableArray shuffledArray]metodo che può essere utilizzato per la riproduzione casuale. Vedi https://developer.apple.com/documentation/foundation/nsarray/1640855-shuffledarray?language=objc per i dettagli. (Ma nota che questo crea un nuovo array, piuttosto che mescolare gli elementi sul posto.)


Dai un'occhiata a questa domanda: problemi del mondo reale con shuffling ingenuo rispetto al tuo algoritmo di shuffling.
Craigb,


5
L'attuale migliore è Fisher-Yates :for (NSUInteger i = self.count; i > 1; i--) [self exchangeObjectAtIndex:i - 1 withObjectAtIndex:arc4random_uniform((u_int32_t)i)];
Cœur

2
Ragazzi, da iOS 10 ++ nuovo concetto di array shuffle dato da Apple, guarda questa risposta
Mukesh,

Problema con esistente APIè che restituisce un nuovo Arrayche si rivolge a una nuova posizione nella memoria.
TheTiger

Risposte:



349

Ho risolto questo aggiungendo una categoria a NSMutableArray.

Modificare: Rimosso il metodo non necessario grazie alla risposta di Ladd.

Modifica: modificato (arc4random() % nElements)inarc4random_uniform(nElements) grazie alla risposta da Gregory Goltsov e commenti da Miho e blahdiblah

Modifica: miglioramento del ciclo, grazie al commento di Ron

Modifica: aggiunto verificare che l'array non sia vuoto, grazie al commento di 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
Bella soluzione. E sì, come accennerà willc2, la sostituzione di random () con arc4random () è un bel miglioramento poiché non è richiesto il seeding.
Jason Moore,

4
@Jason: A volte (ad esempio durante i test), essere in grado di fornire un seme è una buona cosa. Kristopher: simpatico algoritmo. È un'implementazione dell'algoritmo Fisher-Yates: en.wikipedia.org/wiki/Fisher-Yates_shuffle
JeremyP

4
Un miglioramento super minore : nell'ultima iterazione del loop, i == count - 1. Non significa che stiamo scambiando l'oggetto all'indice i con se stesso? Possiamo modificare il codice per saltare sempre l'ultima iterazione?
Ron,

10
Ritieni che una moneta venga lanciata solo se il risultato è l'opposto del lato che era originariamente alto?
Kristopher Johnson,

4
Questo shuffle è leggermente distorto. Usa arc4random_uniform(nElements)invece di arc4random()%nElements. Vedi la pagina man arc4random e questa spiegazione del bias del modulo per maggiori informazioni.
blahdiblah,

38

Dal momento che non posso ancora commentare, ho pensato di contribuire con una risposta completa. Ho modificato l'implementazione di Kristopher Johnson per il mio progetto in diversi modi (cercando davvero di renderlo il più conciso possibile), uno dei quali è arc4random_uniform()perché evita la distorsione del 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
Nota che stai chiamando [self count](un getter di proprietà) due volte su ogni iterazione attraverso il ciclo. Penso che spostarlo fuori dal giro valga la perdita di concisione.
Kristopher Johnson,

1
Ed è per questo che preferisco ancora [object method]piuttosto che object.method: le persone tendono a dimenticare che il successivo non è economico come l'accesso a un membro di una struttura, viene fornito con il costo di una chiamata di metodo ... molto male in un ciclo.
DarkDust,

Grazie per le correzioni - ho erroneamente ipotizzato che il conteggio fosse memorizzato nella cache, per qualche motivo. Aggiornato la risposta.
Gregregtsov,


9

Una soluzione leggermente migliorata e concisa (rispetto alle risposte migliori).

L'algoritmo è lo stesso ed è descritto in letteratura come " shuffle Fisher-Yates ".

In 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

In Swift 3.2 e 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))))
        }
    }
}

In Swift 3.0 e 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])
        }
    }
}

Nota: una soluzione più concisa in Swift è possibile da iOS10 utilizzando GameplayKit.

Nota: è disponibile anche un algoritmo per il mescolamento instabile (con tutte le posizioni costrette a cambiare se il conteggio> 1)


Quale sarebbe la differenza tra questo e l'algoritmo di Kristopher Johnson?
Iulian Onofrei,

@IulianOnofrei, in origine, il codice di Kristopher Johnson non era ottimale e ho migliorato la sua risposta, quindi è stato modificato di nuovo con qualche inutile controllo iniziale aggiunto. Preferisco il mio modo conciso di scriverlo. L'algoritmo è lo stesso ed è descritto in letteratura come " shuffle Fisher-Yates ".
Cœur il

6

Questo è il modo più semplice e veloce per mescolare NSArrays o NSMutableArrays (gli oggetti puzzle è un NSMutableArray, contiene oggetti puzzle. Ho aggiunto all'indice variabile oggetto puzzle che indica la posizione iniziale nell'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++;
    }
}

uscita registro:

 #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

puoi anche confrontare obj1 con obj2 e decidere cosa vuoi restituire possibili valori sono:

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

1
Anche per questa soluzione, utilizzare arc4random () o seed.
Johan Kool

17
Questo shuffle è difettoso - come Microsoft ha recentemente ricordato: robweir.com/blog/2010/02/microsoft-random-browser-ballot.html .
Raphael Schweikert,

Concordato, imperfetto perché "l'ordinamento richiede una definizione auto-coerente di ordinamento", come sottolineato in quell'articolo sulla SM. Sembra elegante, ma non lo è.
Jeff,

2

C'è una bella libreria popolare, che ha questo metodo come parte, chiamata SSToolKit in GitHub . Il file NSMutableArray + SSToolkitAdditions.h contiene il metodo shuffle. Puoi usarlo anche. Tra questi, sembrano esserci tonnellate di cose utili.

La pagina principale di questa biblioteca è qui .

Se lo usi, il tuo codice sarà così:

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

Questa libreria ha anche un Pod (vedi CocoaPods)


2

Da iOS 10, puoi usare NSArray shuffled()da GameplayKit . Ecco un aiuto per Array in 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

Se gli elementi hanno ripetizioni.

ad es. array: AAABB o BBAAA

l'unica soluzione è: ABABA

sequenceSelected è un NSMutableArray che memorizza elementi della classe obj, che sono puntatori a una sequenza.

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

l'uso di a staticimpedisce di lavorare su più istanze: sarebbe molto più sicuro e leggibile usare due metodi, uno principale che mescola e chiama il metodo secondario, mentre il metodo secondario chiama solo se stesso e non rimpasto mai. Inoltre c'è un errore di ortografia.
Cœur il

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

2
o arc4random_uniform([theArray count])sarebbe ancora meglio, se disponibile sulla versione di Mac OS X o iOS che stai supportando.
Kristopher Johnson,

1
Ho dato in questo modo il numero ripeterà.
Vineesh TP,

-1

La risposta di Kristopher Johnson è piuttosto carina, ma non è del tutto casuale.

Dato un array di 2 elementi, questa funzione restituisce sempre l'array inverso, perché stai generando l'intervallo del tuo casuale rispetto al resto degli indici. Una shuffle()funzione più accurata sarebbe come

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

Penso che l'algoritmo che hai suggerito sia un "inganno casuale". Vedi blog.codinghorror.com/the-danger-of-naivete . Penso che la mia risposta abbia una probabilità del 50% di scambiare gli elementi se ce ne sono solo due: quando i è zero, arc4random_uniform (2) restituirà 0 o 1, quindi l'elemento zeroth verrà scambiato con se stesso o scambiato con gli oneth elemento. Alla successiva iterazione, quando i è 1, arc4random (1) restituirà sempre 0 e l'elemento ith verrà sempre scambiato con se stesso, che è inefficiente ma non errato. (Forse la condizione del loop dovrebbe essere i < (count-1).)
Kristopher Johnson,

-2

Modifica: questo non è corretto. A scopo di riferimento, non ho eliminato questo post. Vedi i commenti sul motivo per cui questo approccio non è corretto.

Codice semplice qui:

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.