Какой лучший способ перемешать массив 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.