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.)


Jetez un œil à cette question: Problèmes du monde réel avec le brassage naïf par rapport à votre algorithme de brassage.
craigb


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

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

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


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


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

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)


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

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

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

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

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

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.