改组NSMutableArray的最佳方法是什么?


187

如果您有个NSMutableArray,您如何随机洗牌?

(我对此有自己的答案,发布在下面,但是我是可可的新手,我想知道是否有更好的方法。)


更新:正如@Mukesh所指出的那样,从iOS 10+和m​​acOS 10.12+开始,有一种-[NSMutableArray shuffledArray]方法可以用于随机播放。有关详细信息,请参见https://developer.apple.com/documentation/foundation/nsarray/1640855-shuffledarray?language=objc。(但是请注意,这将创建一个新数组,而不是将元素改组到位。)


看一下这个问题:天真的改组相对于改组算法的实际问题。
craigb


5
目前最好的是费雪耶茨for (NSUInteger i = self.count; i > 1; i--) [self exchangeObjectAtIndex:i - 1 withObjectAtIndex:arc4random_uniform((u_int32_t)i)];
心教堂

2
伙计们,来自苹果公司提供的iOS 10 ++ shuffle数组的新概念,看看这个答案
Mukesh

现有的问题API是它返回一个新Array地址,该地址寻址到存储器中的新位置。
TheTiger

Answers:



349

我通过在NSMutableArray中添加一个类别来解决此问题。

编辑:删除不必要的方法,感谢拉德的回答。

编辑:更改(arc4random() % nElements)arc4random_uniform(nElements)感谢格雷戈里·戈尔佐夫(Gregory Goltsov)的回答以及miho和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()替换random()是一个不错的改进,因为不需要种子。
杰森·摩尔

4
@杰森:有时候(例如在测试时),能够提供种子是一件好事。Kristopher:不错的算法。这是Fisher-Yates算法的实现: en.wikipedia.org/wiki/Fisher-Yates_shuffle
JeremyP 2011年

4
一个非常小的改进:在循环的最后一次迭代中,i == count-1。这是否意味着我们正在与索引i交换对象本身?我们可以调整代码以始终跳过最后一次迭代吗?
罗恩

10
您是否认为仅当结果与最初向上的一面相反时,硬币才被翻转?
克里斯托弗·约翰逊

4
这种洗牌是有偏见的。使用arc4random_uniform(nElements)代替arc4random()%nElements。有关更多信息,请参见arc4random手册页此模偏置说明
blahdiblah

38

由于我无法发表评论,因此我认为我会做出完整的回应。我以多种方式修改了克里斯托弗·约翰逊(Kristopher Johnson)对我的项目的实现(确实试图使其尽可能简洁),其中之一是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 2013年

感谢您的更正-由于某种原因,我错误地认为计数被缓存。更新了答案。
gregoltsov


9

略有改进和简洁的解决方案(与最佳答案相比)。

该算法是相同的,在文献中被描述为“ Fisher-Yates shuffle ”。

在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])
        }
    }
}

注意:从iOS10使用,可以在Swift中提供更简洁的解决方案GameplayKit

注意:还提供了一种用于不稳定混洗的算法(如果count> 1,则所有位置都必须更改)


此算法与Kristopher Johnson算法之间有什么区别?
Iulian Onofrei,2016年

@IulianOnofrei,最初,Kristopher Johnson的代码不是最佳的,我改善了他的答案,然后又对其进行了编辑,并添加了一些无用的初始检查。我更喜欢简洁的编写方式。该算法是相同的,在文献中被描述为“ Fisher-Yates shuffle ”。
心教堂

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()或种子。
Johan Kool

17
这种改组是有缺陷的–就像最近提醒微软的那样:robweir.com/blog/2010/02/microsoft-random-browser-ballot.html
拉斐尔·史威克

同意的,有缺陷的,因为该文章中有关MS的指出“排序需要顺序的自定义定义”。看起来很优雅,但不是。
杰夫,

2

有一个很好的受欢迎的库,该库包含此方法,在GitHub中称为SSToolKit。文件NSMutableArray + SSToolkitAdditions.h包含随机播放方法。您也可以使用它。其中,似乎有很多有用的东西。

该库的主页在这里

如果使用此代码,则代码将如下所示:

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

该库还具有一个Pod(请参阅CocoaPods)


2

在iOS 10中,您可以使用GameplayKit中的NSArrayshuffled()。这是Swift 3中Array的助手:

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

使用a static可以防止在多个实例上工作:使用两种方法会更安全和易读,一种是主方法,它会随机播放并调用辅助方法,而辅助方法仅调用自身而不进行重新洗手。还有一个拼写错误。
心教堂

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

2
甚至arc4random_uniform([theArray count])会更好,如果您支持的Mac OS X或iOS版本可用。
克里斯托弗·约翰逊

1
我给我们这样的数字将重复。
Vineesh TP

-1

克里斯托弗·约翰逊(Kristopher Johnson)的回答很不错,但这并不是完全随机的。

给定一个由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,并且ith元素将始终与其自身进行交换,这效率不高,但并非不正确。(也许应该是循环条件i < (count-1)。)
Kristopher Johnson

-2

编辑:这是不正确的。仅供参考,我没有删除此帖子。请参阅有关此方法不正确的原因的评论。

简单的代码在这里:

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.