Какой лучший способ перемешать массив NSMutableArray?


187

Если у вас есть NSMutableArray , как вы перемешиваете элементы случайным образом?

(У меня есть собственный ответ на этот вопрос, который опубликован ниже, но я новичок в Какао, и мне интересно знать, есть ли лучший способ.)


Обновление: как отмечает @Mukesh, начиная с iOS 10+ и macOS 10.12+, существует -[NSMutableArray shuffledArray]метод, который можно использовать для перемешивания. Подробнее см. Https://developer.apple.com/documentation/foundation/nsarray/1640855-shuffledarray?language=objc . (Но учтите, что это создает новый массив, а не перетасовывает элементы на месте.)


Взгляните на этот вопрос: реальные проблемы с наивным тасованием относительно вашего алгоритма тасования.
Крейгб


5
Текущий лучше всего Фишер-Йейтс :for (NSUInteger i = self.count; i > 1; i--) [self exchangeObjectAtIndex:i - 1 withObjectAtIndex:arc4random_uniform((u_int32_t)i)];
Cœur


Проблема с существующим в APIтом, что он возвращает новое, Arrayкоторое обращается к новому месту в памяти.
TheTiger

Ответы:



349

Я решил это, добавив категорию в NSMutableArray.

Изменить: Удален ненужный метод благодаря ответу Лэдд.

Изменить: изменено (arc4random() % nElements)наarc4random_uniform(nElements) благодаря ответу Грегори Гольцов и комментарии по Михо и blahdiblah

Изменить: улучшение петли, благодаря комментарию Рона

Редактировать: добавлена ​​проверка, что массив не пуст, благодаря комментарию Махеш Агравал

//  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
Хорошее решение. И да, как упоминает willc2, замена random () на arc4random () является хорошим улучшением, поскольку не требует заполнения.
Джейсон Мур

4
@ Джейсон: Иногда (например, при тестировании), возможность поставлять семена это хорошая вещь. Кристофер: хороший алгоритм. Это реализация алгоритма Фишера-Йейтса: en.wikipedia.org/wiki/Fisher-Yates_shuffle
JeremyP

4
Совершенно незначительное улучшение: в последней итерации цикла i == count - 1. Не означает ли это, что мы обмениваемся объектом по индексу i с самим собой? Можем ли мы настроить код, чтобы всегда пропускать последнюю итерацию?
Рон

10
Считаете ли вы монету подброшенной, только если результат противоположен той стороне, которая была изначально вверх?
Кристофер Джонсон

4
Эта случайность слегка скрыта. Используйте arc4random_uniform(nElements)вместо arc4random()%nElements. См. Man-страницу arc4random и это объяснение смещения по модулю для получения дополнительной информации.
Blahdiblah

38

Поскольку я пока не могу комментировать, я подумала, что внесу полный ответ. Я изменил реализацию Кристофера Джонсона для своего проекта несколькими способами (действительно пытаясь сделать его как можно более кратким), одним из них является то, arc4random_uniform()что он избегает смещения по модулю .

// 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
Обратите внимание, что вы вызываете [self count](getter) дважды на каждой итерации цикла. Я думаю, что удаление этого из цикла стоит потери краткости.
Кристофер Джонсон

1
И именно поэтому я все же предпочитаю [object method]вместо object.method: люди склонны забывать, что последнее не так дешево, как доступ к члену структуры, оно идет со стоимостью вызова метода ... очень плохо в цикле.
DarkDust

Спасибо за исправления - я ошибочно предположил, что подсчет был кеширован по какой-то причине. Обновил ответ.
Грегольцов

10

Если вы импортируете GameplayKit, есть shuffledAPI:

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

let shuffledArray = array.shuffled()

У меня есть myArray и я хочу создать новый shuffleArray. Как мне сделать это с Objective - C?
Омкар Джадхав

shuffledArray = [array shuffledArray];
andreacipriani

Обратите внимание, что этот метод является частью, GameplayKitпоэтому вы должны импортировать его.
AnthoPak

9

Немного улучшенное и краткое решение (по сравнению с основными ответами).

Алгоритм такой же и описан в литературе как « случайный случай Фишера-Йейтса». ».

В 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

В Swift 3.2 и 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))))
        }
    }
}

В Swift 3.0 и 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])
        }
    }
}

Примечание: более краткое решение в Swift возможно с использованием iOS10 GameplayKit.

Примечание: также доступен алгоритм нестабильной тасовки (все позиции вынуждены менять, если число> 1).


Какая разница между этим и алгоритмом Кристофера Джонсона?
Юлиан Онофрей

@IulianOnofrei, изначально, код Кристофера Джонсона не был оптимальным, и я улучшил его ответ, затем он снова отредактирован с добавлением некоторой бесполезной начальной проверки. Я предпочитаю мой краткий способ написания этого. Алгоритм такой же и описан в литературе как « случай Фишера-Йейтса ».
Cœur

6

Это самый простой и быстрый способ перемешать NSArrays или NSMutableArrays (объектные головоломки - это NSMutableArray, он содержит объекты головоломки. Я добавил в индекс переменной объекта головоломки, который указывает начальную позицию в массиве)

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

вывод журнала:

 #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

Вы также можете сравнить obj1 с obj2 и решить, что вы хотите вернуть возможные значения:

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

1
Также для этого решения используйте arc4random () или seed.
Йохан Кул

17
Это случайное искажение - как недавно напомнили Microsoft: robweir.com/blog/2010/02/microsoft-random-browser-ballot.html .
Рафаэль Швейкерт

Согласен, ошибочен, потому что «сортировка требует самосогласованного определения порядка», как указано в этой статье о РС. Выглядит элегантно, но не так.
Джефф

2

Есть хорошая популярная библиотека, в которой есть этот метод, который называется SSToolKit в GitHub. . Файл NSMutableArray + SSToolkitAdditions.h содержит метод shuffle. Вы можете использовать это также. Среди этого, кажется, есть тонны полезных вещей.

Главная страница этой библиотеки находится здесь .

Если вы используете это, ваш код будет выглядеть так:

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

Эта библиотека также имеет Pod (см. CocoaPods)


2

С iOS 10 вы можете использовать NSArray shuffled()из GameplayKit . Вот помощник для Array в 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

Если элементы повторяются.

например, массив: AAABB или BBAAA

Единственное решение: ABABA

sequenceSelected NSMutableArray, который хранит элементы класса obj, которые являются указателями на некоторую последовательность.

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

использование метода « staticпредотвращает» работу с несколькими экземплярами: было бы намного безопаснее и удобочитаемее использовать два метода, основной из которых выполняет перемешивание и вызывает вторичный метод, тогда как вторичный метод вызывает только сам себя и никогда не переставляет. Также есть орфографическая ошибка.
Cœur

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

2
или arc4random_uniform([theArray count])было бы еще лучше, если бы оно было доступно в поддерживаемой вами версии Mac OS X или iOS.
Кристофер Джонсон

1
Я нам дал, как это число будет повторяться.
Vineesh TP

-1

Ответ Кристофера Джонсона довольно приятный, но не совсем случайный.

Учитывая массив из 2 элементов, эта функция всегда возвращает инвертированный массив, потому что вы генерируете диапазон случайных чисел по остальным индексам. Более точная shuffle()функция будет как

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

Я думаю, что алгоритм, который вы предложили, является «наивным тасованием». Смотрите blog.codinghorror.com/the-danger-of-naivete . Я думаю, что мой ответ с вероятностью 50% поменяет местами элементы, если их всего два: когда i равен нулю, arc4random_uniform (2) вернет либо 0, либо 1, поэтому нулевой элемент будет либо заменен на себя, либо заменен на один. элемент. На следующей итерации, когда i равен 1, arc4random (1) всегда будет возвращать 0, а i-й элемент будет всегда заменяться на себя, что неэффективно, но не некорректно. (Может быть, условие цикла должно быть i < (count-1).)
Кристофер Джонсон

-2

Редактировать: это не правильно. Для справки я не удалил этот пост. Смотрите комментарии о причине, почему этот подход не является правильным.

Простой код здесь:

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

Это случайное искажение
Cœur
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.