Qual é a melhor maneira de embaralhar um NSMutableArray?

187

Se você possui um NSMutableArray, como embaralha os elementos aleatoriamente?

(Eu tenho minha própria resposta para isso, que está publicada abaixo, mas eu sou novo no Cocoa e estou interessado em saber se existe uma maneira melhor.)


Atualização: conforme observado pelo @Mukesh, no iOS 10+ e no macOS 10.12+, existe um -[NSMutableArray shuffledArray]método que pode ser usado para embaralhar. Consulte https://developer.apple.com/documentation/foundation/nsarray/1640855-shuffledarray?language=objc para obter detalhes. (Mas observe que isso cria uma nova matriz, em vez de embaralhar os elementos no lugar.)

Kristopher Johnson
fonte
Dê uma olhada nesta pergunta: Problemas do mundo real com embaralhamento ingênuo em relação ao seu algoritmo de embaralhamento.
craigb
Aqui é uma implementação em Swift: iosdevelopertips.com/swift-code/swift-shuffle-array-type.html
Kristopher Johnson
5
O melhor atual é Fisher-Yates :for (NSUInteger i = self.count; i > 1; i--) [self exchangeObjectAtIndex:i - 1 withObjectAtIndex:arc4random_uniform((u_int32_t)i)];
Cœur
2
Caras, de iOS 10 ++ novo conceito de variedade aleatória dada pela Apple, olhe para esta resposta
Mukesh
Problema com o existente APIé que ele retorna um novo Arrayendereço para um novo local na memória.
TheTiger

Respostas:

349

Resolvi isso adicionando uma categoria ao NSMutableArray.

Edit: Removido o método desnecessário graças à resposta de Ladd.

Edit: Alterado (arc4random() % nElements)para arc4random_uniform(nElements)obrigado para responder por Gregory Goltsov e comentários por miho e blahdiblah

Edit: Melhoria do loop, graças ao comentário de Ron

Edit: Adicionado verificação de que o array não está vazio, graças ao comentário 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
fonte
10
Ótima solução. E sim, como o willc2 menciona, substituir random () por arc4random () é uma boa melhoria, pois não é necessário propagação.
Jason Moore
4
@ Jason: Às vezes (por exemplo, ao testar), poder fornecer uma semente é uma coisa boa. Kristopher: bom algoritmo. É uma implementação do algoritmo Fisher-Yates: en.wikipedia.org/wiki/Fisher-Yates_shuffle
JeremyP
4
Uma melhoria super menor : na última iteração do loop, i == count - 1. Isso não significa que estamos trocando o objeto no índice i consigo? Podemos ajustar o código para sempre pular a última iteração?
Ron
10
Você considera que uma moeda é lançada apenas se o resultado for o oposto do lado que estava originalmente para cima?
Kristopher Johnson
4
Esse shuffle é sutilmente tendencioso. Use em arc4random_uniform(nElements)vez de arc4random()%nElements. Veja a página de manual do arc4random e esta explicação do viés do módulo para mais informações.
blahdiblah
38

Como ainda não posso comentar, pensei em contribuir com uma resposta completa. Modifiquei a implementação de Kristopher Johnson para o meu projeto de várias maneiras (realmente tentando torná-lo o mais conciso possível), uma delas arc4random_uniform()porque evita o viés do módulo .

// 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
fonte
2
Observe que você está chamando [self count](um getter de propriedade) duas vezes em cada iteração pelo loop. Eu acho que movê-lo para fora do circuito vale a pena a perda de concisão.
22412 Kristopher Johnson
1
E é por isso que ainda prefiro, em [object method]vez de object.method: as pessoas tendem a esquecer que o último não é tão barato quanto acessar um membro struct, ele vem com o custo de uma chamada de método ... muito ruim em um loop.
precisa saber é o seguinte
Obrigado pelas correções - por engano, presumi que a contagem estava armazenada em cache. Atualizado a resposta.
gregoltsov
10

Se você importar GameplayKit, há uma shuffledAPI:

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

let shuffledArray = array.shuffled()
andreacipriani
fonte
Eu tenho o myArray e quero criar um novo shuffleArray. Como faço com Objective-C?
Omkar Jadhav
shuffledArray = [array shuffledArray];
andreacipriani
Observe que esse método faz parte, GameplayKitentão você precisa importá-lo.
AnthoPak 08/07/19
9

Uma solução ligeiramente aprimorada e concisa (em comparação com as principais respostas).

O algoritmo é o mesmo e é descrito na literatura como " shuffle de Fisher-Yates ".

No Objetivo-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

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

No 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: É possível uma solução mais concisa no Swift no iOS10 usandoGameplayKit .

Nota: Um algoritmo para embaralhar instável (com todas as posições forçadas a mudar se a contagem> 1) também estiver disponível

Cœur
fonte
Qual seria a diferença entre este e o algoritmo de Kristopher Johnson?
Iulian Onofrei
@IulianOnofrei, originalmente, o código de Kristopher Johnson não era o ideal e eu melhorei sua resposta, depois ele foi editado novamente com uma verificação inicial inútil adicionada. Eu prefiro minha maneira concisa de escrever. O algoritmo é o mesmo e é descrito na literatura como " Shuffle de Fisher-Yates ".
Cœur
6

Esta é a maneira mais simples e rápida de embaralhar NSArrays ou NSMutableArrays (quebra-cabeças de objetos é um NSMutableArray, contém objetos de quebra-cabeça. Adicionei ao índice de variável de objeto de quebra-cabeça que indica a posição inicial na matriz)

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

saída de 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

você também pode comparar obj1 com obj2 e decidir o que deseja retornar possíveis valores:

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

fonte
1
Também para esta solução, use arc4random () ou seed.
Johan Kool
17
Esse shuffle é falho - como a Microsoft recentemente lembrou: robweir.com/blog/2010/02/microsoft-random-browser-ballot.html .
Raphael Schweikert
Concordado, falho porque "a classificação requer uma definição autoconsistente de pedido", conforme apontado no artigo sobre a EM. Parece elegante, mas não é.
Jeff
2

Existe uma boa biblioteca popular, que tem esse método como parte, chamada SSToolKit no GitHub . O arquivo NSMutableArray + SSToolkitAdditions.h contém o método shuffle. Você pode usá-lo também. Entre isso, parece haver toneladas de coisas úteis.

A página principal desta biblioteca está aqui .

Se você usar isso, seu código será assim:

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

Esta biblioteca também possui um Pod (consulte CocoaPods)

Denis Kutlubaev
fonte
2

No iOS 10, você pode usar o NSArray shuffled()no GameplayKit . Aqui está um ajudante para o Array no 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
fonte
1

Se os elementos tiverem repetições.

por exemplo, matriz: AAABB ou BBAAA

única solução é: ABABA

sequenceSelected é um NSMutableArray que armazena elementos da classe obj, que são ponteiros para alguma sequência.

- (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;
                }
            }
        }
    }
}
Ponto Gama
fonte
o uso de staticimpede impede o trabalho em várias instâncias: seria muito mais seguro e legível usar dois métodos, um principal que embaralha e chama o método secundário, enquanto o método secundário apenas se autodenomina e nunca reorganiza. Também há um erro de ortografia.
Cœur
-1
NSUInteger randomIndex = arc4random() % [theArray count];
kal
fonte
2
ou arc4random_uniform([theArray count])seria ainda melhor, se disponível na versão do Mac OS X ou iOS compatível.
22412 Kristopher Johnson
1
Como nós demos desta forma, o número será repetido.
Vineesh TP
-1

Resposta de Kristopher Johnson é bastante agradável, mas não é totalmente aleatória.

Dada uma matriz de 2 elementos, essa função sempre retorna a matriz inversa, porque você está gerando o intervalo aleatório nos demais índices. Uma shuffle()função mais precisa seria como

- (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
fonte
Eu acho que o algoritmo que você sugeriu é um "embaralhamento ingênuo". Consulte blog.codinghorror.com/the-danger-of-naivete . Eu acho que minha resposta tem 50% de chance de trocar os elementos se houver apenas dois: quando i for zero, arc4random_uniform (2) retornará 0 ou 1; portanto, o elemento zeroth será trocado consigo mesmo ou trocado com o oneth elemento. Na próxima iteração, quando i for 1, arc4random (1) sempre retornará 0 e o i-ésimo elemento sempre será trocado consigo mesmo, o que é ineficiente, mas não incorreto. (Talvez a condição de loop deve ser i < (count-1).)
Kristopher Johnson
-2

Edit: Isso não está correto. Para fins de referência, não excluí esta postagem. Veja comentários sobre o motivo pelo qual essa abordagem não está correta.

Código simples aqui:

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