Який найкращий спосіб перетасувати NSMutableArray?


187

Якщо у вас є NSMutableArray, як ви переміщуєте елементи випадковим чином?

(У мене є своя відповідь на це, яка розміщена нижче, але я новачок у какао і мені цікаво знати, чи є кращий спосіб.)


Оновлення: Як зазначає @Mukesh, станом на iOS 10+ та macOS 10.12+ є -[NSMutableArray shuffledArray]метод, який можна використовувати для переміщення. Докладні відомості див. У https://developer.apple.com/documentation/foundation/nsarray/1640855-shuffledarray?language=objc . (Але зауважте, що це створює новий масив, а не переміщення елементів на місці.)


Погляньте на це питання: Проблеми в реальному світі при наївному перетасуванні щодо вашого алгоритму перетасування.
craigb


5
Наразі найкращим є Fisher-Yates :for (NSUInteger i = self.count; i > 1; i--) [self exchangeObjectAtIndex:i - 1 withObjectAtIndex:arc4random_uniform((u_int32_t)i)];
Cœur

2
Хлопці, з iOS 10 ++ нова концепція масиву перетасовок, що дається Apple, подивіться на цю відповідь
Mukesh

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

Відповіді:



349

Я вирішив це, додавши категорію до NSMutableArray.

Редагувати: Видалено непотрібний метод завдяки відповіді Ладда.

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

Редагувати: поліпшення циклу, завдяки коментарю Рона

Редагувати: Додано перевірити, чи масив не порожній, завдяки коментарю 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
Приємне рішення. І так, як згадує willc2, заміна випадкового () на arc4random () - це приємне поліпшення, оскільки не потрібно висівати насіння.
Джейсон Мур

4
@Jason: Іноді (наприклад, під час тестування) можливість поставити насіння - це добре. Крістофер: приємний алгоритм. Це реалізація алгоритму Фішера-Йейтса: en.wikipedia.org/wiki/Fisher-Yates_shuffle
JeremyP

4
Супер незначне поліпшення: в останній ітерації циклу, я == кол - 1. Хіба це не означає , що ми обмінюємося об'єкт з індексом I з самим собою? Чи можемо ми налаштувати код, щоб завжди пропустити останню ітерацію?
Рон

10
Чи вважаєте ви монету перевернутою, лише якщо результат протилежний стороні, яка спочатку була вгору?
Крістофер Джонсон

4
Це переміщення є тонко упередженим. Використовуйте arc4random_uniform(nElements)замість arc4random()%nElements. Дивіться сторінку arc4random man і це пояснення зміщення модуля для отримання додаткової інформації.
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](отримувач властивостей) двічі під час кожної ітерації через цикл. Я думаю, що переміщення його з циклу варто втратити стислість.
Крістофер Джонсон

1
І тому я все одно віддаю перевагу [object method]замість цього object.method: люди, як правило, забувають, що пізніше це не так дешево, як доступ до члена структури, це пов'язано з вартістю виклику методу ... дуже погано в циклі.
DarkDust

Дякую за виправлення - я неправильно припустив, що підрахунок був кешований, чомусь. Оновлено відповідь.
gregoltsov

10

Якщо ви імпортуєте GameplayKit, існує shuffledAPI:

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

let shuffledArray = array.shuffled()

У мене є мійArray і хочу створити новий shuffleArray з нього. Як це зробити з ціллю 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 () або насіння.
Йохан Кул

17
Ця зміна є помилковою - як останнім часом Microsoft нагадував: robweir.com/blog/2010/02/microsoft-random-browser-ballot.html .
Рафаель Швайкерт

Погоджено, помилково, оскільки "сортування вимагає неузгодженого визначення порядку", як зазначено у цій статті про МС. Виглядає елегантно, але це не так.
Джефф

2

Існує приємна популярна бібліотека, у якій цей метод є частиною, який називається SSToolKit в GitHub . Файл NSMutableArray + SSToolkitAdditions.h містить метод перетасовки. Ви також можете використовувати його. Серед цього, здається, є багато корисних речей.

Головна сторінка цієї бібліотеки тут .

Якщо ви користуєтесь цим, ваш код буде таким:

#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% шансів замінити елементи, якщо їх лише два: коли я дорівнює нулю, 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;
        }
    }];
}

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.