Quelle est la meilleure façon de mélanger un NSMutableArray?

188

Si vous avez un NSMutableArray, comment mélangez-vous les éléments au hasard?

(J'ai ma propre réponse à ce sujet, qui est publiée ci-dessous, mais je suis nouveau sur Cocoa et je suis intéressé de savoir s'il existe un meilleur moyen.)


Mise à jour: comme indiqué par @Mukesh, à partir d'iOS 10+ et de macOS 10.12+, il existe une -[NSMutableArray shuffledArray]méthode qui peut être utilisée pour mélanger. Voir https://developer.apple.com/documentation/foundation/nsarray/1640855-shuffledarray?language=objc pour plus de détails. (Mais notez que cela crée un nouveau tableau, plutôt que de mélanger les éléments en place.)

Kristopher Johnson
la source
Jetez un œil à cette question: Problèmes du monde réel avec le brassage naïf par rapport à votre algorithme de brassage.
craigb
Voici une implémentation dans Swift: iosdevelopertips.com/swift-code/swift-shuffle-array-type.html
Kristopher Johnson
6
Le meilleur du moment est Fisher-Yates :for (NSUInteger i = self.count; i > 1; i--) [self exchangeObjectAtIndex:i - 1 withObjectAtIndex:arc4random_uniform((u_int32_t)i)];
Cœur
2
Les gars, à partir d'iOS 10 ++ nouveau concept de tableau aléatoire donné par Apple, regardez cette réponse
Mukesh
Le problème avec l'existant APIest qu'il renvoie un nouveau Arrayqui s'adresse à un nouvel emplacement dans la mémoire.
TheTiger

Réponses:

351

J'ai résolu ce problème en ajoutant une catégorie à NSMutableArray.

Edit: Suppression de la méthode inutile grâce à la réponse de Ladd.

Edit: Changé (arc4random() % nElements)en arc4random_uniform(nElements)merci à la réponse de Gregory Goltsov et aux commentaires de miho et blahdiblah

Edit: Amélioration de la boucle, grâce au commentaire de Ron

Edit: Ajout de la vérification que le tableau n'est pas vide, grâce au commentaire de 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
la source
10
Belle solution. Et oui, comme willc2 le mentionne, remplacer random () par arc4random () est une belle amélioration car aucun ensemencement n'est nécessaire.
Jason Moore
4
@Jason: Parfois (par exemple lors des tests), être en mesure de fournir une graine est une bonne chose. Kristopher: bel algorithme. C'est une implémentation de l'algorithme Fisher-Yates: en.wikipedia.org/wiki/Fisher-Yates_shuffle
JeremyP
4
Une amélioration super mineure : dans la dernière itération de la boucle, i == compte - 1. Cela ne signifie-t-il pas que nous échangeons l'objet à l'index i avec lui-même? Pouvons-nous modifier le code pour toujours sauter la dernière itération?
Ron
10
Considérez-vous qu'une pièce ne doit être retournée que si le résultat est l'opposé du côté qui était à l'origine vers le haut?
Kristopher Johnson
4
Ce mélange est subtilement biaisé. Utilisez à la arc4random_uniform(nElements)place de arc4random()%nElements. Voir la page de manuel arc4random et cette explication du biais modulo pour plus d'informations.
blahdiblah
38

Comme je ne peux pas encore commenter, j'ai pensé que je contribuerais à une réponse complète. J'ai modifié l'implémentation de Kristopher Johnson pour mon projet de plusieurs façons (en essayant vraiment de la rendre aussi concise que possible), l'une d'entre elles étant arc4random_uniform()parce qu'elle évite le biais 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
la source
2
Notez que vous appelez [self count](un getter de propriété) deux fois à chaque itération de la boucle. Je pense que le sortir de la boucle vaut la perte de concision.
Kristopher Johnson
1
Et c'est pourquoi je préfère toujours [object method]au lieu de object.method: les gens ont tendance à oublier que ce dernier n'est pas aussi bon marché que d'accéder à un membre de struct, cela vient avec le coût d'un appel de méthode ... très mauvais dans une boucle.
DarkDust
Merci pour les corrections - j'ai supposé à tort que le compte était mis en cache, pour une raison quelconque. Mise à jour de la réponse.
gregoltsov
10

Si vous importez GameplayKit, il existe une shuffledAPI:

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

let shuffledArray = array.shuffled()
andreacipriani
la source
J'ai myArray et je souhaite en créer un nouveau shuffleArray. Comment faire avec Objective - C?
Omkar Jadhav
shuffledArray = [array shuffledArray];
andreacipriani
Notez que cette méthode en fait partie GameplayKit, vous devez donc l'importer.
AnthoPak
9

Une solution légèrement améliorée et concise (par rapport aux meilleures réponses).

L'algorithme est le même et est décrit dans la littérature sous le nom de " Fisher-Yates shuffle ".

En 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

Dans Swift 3.2 et 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))))
        }
    }
}

Dans Swift 3.0 et 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])
        }
    }
}

Remarque: Une solution plus concise dans Swift est possible à partir d'iOS10 en utilisant GameplayKit.

Remarque: Un algorithme de mélange instable (avec toutes les positions forcées de changer si nombre> 1) est également disponible

Cœur
la source
Quelle serait la différence entre cela et l'algorithme de Kristopher Johnson?
Iulian Onofrei
@IulianOnofrei, à l'origine, le code de Kristopher Johnson n'était pas optimal et j'ai amélioré sa réponse, puis il a été édité à nouveau avec une vérification initiale inutile ajoutée. Je préfère ma façon concise de l'écrire. L'algorithme est le même et est décrit dans la littérature sous le nom de " Fisher-Yates shuffle ".
Cœur
6

C'est le moyen le plus simple et le plus rapide de mélanger les NSArrays ou NSMutableArrays (les puzzles d'objets sont un NSMutableArray, il contient des objets de puzzle. J'ai ajouté à l'index de variable d'objet puzzle qui indique la position initiale dans le tableau)

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

sortie du journal:

 #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

vous pouvez également comparer obj1 avec obj2 et décider de ce que vous voulez renvoyer les valeurs possibles:

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

la source
1
Aussi pour cette solution, utilisez arc4random () ou seed.
Johan Kool
17
Ce shuffle est imparfait - comme Microsoft l'a récemment rappelé: robweir.com/blog/2010/02/microsoft-random-browser-ballot.html .
Raphael Schweikert
D'accord, imparfait parce que "le tri nécessite une définition cohérente de la commande", comme indiqué dans cet article sur la SEP. Ça a l'air élégant, mais ça ne l'est pas.
Jeff
2

Il existe une belle bibliothèque populaire, qui comprend cette méthode, appelée SSToolKit dans GitHub . Le fichier NSMutableArray + SSToolkitAdditions.h contient la méthode shuffle. Vous pouvez également l'utiliser. Parmi cela, il semble y avoir des tonnes de choses utiles.

La page principale de cette bibliothèque est ici .

Si vous utilisez ceci, votre code sera comme ceci:

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

Cette bibliothèque possède également un Pod (voir CocoaPods)

Denis Kutlubaev
la source
2

À partir d'iOS 10, vous pouvez utiliser NSArray shuffled()depuis GameplayKit . Voici une aide pour Array dans 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())
    }
}
Cœur
la source
1

Si les éléments ont des répétitions.

par exemple, tableau: AAABB ou BBAAA

la seule solution est: ABEBA

sequenceSelected est un NSMutableArray qui stocke les éléments de la classe obj, qui sont des pointeurs vers une séquence.

- (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;
                }
            }
        }
    }
}
Point gamma
la source
l'utilisation de a staticempêche de travailler sur plusieurs instances: il serait beaucoup plus sûr et lisible d'utiliser deux méthodes, une principale qui mélange et appelle la méthode secondaire, tandis que la méthode secondaire ne s'appelle qu'elle-même et ne se remet jamais. Il y a aussi une faute d'orthographe.
Cœur
-1
NSUInteger randomIndex = arc4random() % [theArray count];
Kal
la source
2
ou arc4random_uniform([theArray count])serait encore mieux, si disponible sur la version de Mac OS X ou iOS que vous prenez en charge.
Kristopher Johnson
1
J'ai donné comme ça le numéro va se répéter.
Vineesh TP
-1

La réponse de Kristopher Johnson est plutôt sympa, mais ce n'est pas totalement aléatoire.

Étant donné un tableau de 2 éléments, cette fonction retourne toujours le tableau inversé, car vous générez la plage de votre aléatoire sur le reste des index. Une shuffle()fonction plus précise serait comme

- (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
la source
Je pense que l'algorithme que vous avez suggéré est un "shuffle naïf". Voir blog.codinghorror.com/the-danger-of-naivete . Je pense que ma réponse a 50% de chances d'échanger les éléments s'il n'y en a que deux: quand i vaut zéro, arc4random_uniform (2) retournera soit 0 soit 1, donc l'élément zéro sera soit échangé avec lui-même, soit échangé avec le oneth élément. A l'itération suivante, quand i vaut 1, arc4random (1) retournera toujours 0, et le ième élément sera toujours échangé avec lui-même, ce qui est inefficace mais pas incorrect. (Peut-être que la condition de la boucle devrait l'être i < (count-1).)
Kristopher Johnson
-2

Edit: Ce n'est pas correct. À des fins de référence, je n'ai pas supprimé ce message. Voir les commentaires sur la raison pour laquelle cette approche n'est pas correcte.

Code simple ici:

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