【问题标题】:What's the Best Way to Shuffle an NSMutableArray?洗牌 NSMutableArray 的最佳方法是什么?
【发布时间】:2010-09-08 13:41:53
【问题描述】:

如果你有NSMutableArray,你如何随机打乱元素?

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


更新:正如@Mukesh 所指出的,从 iOS 10+ 和 macOS 10.12+ 开始,有一个 -[NSMutableArray shuffledArray] 方法可用于随机播放。有关详细信息,请参阅https://developer.apple.com/documentation/foundation/nsarray/1640855-shuffledarray?language=objc。 (但请注意,这会创建一个新数组,而不是在原地打乱元素。)

【问题讨论】:

标签: objective-c cocoa shuffle


【解决方案1】:

我通过向 NSMutableArray 添加一个类别解决了这个问题。

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

编辑:(arc4random() % nElements) 更改为 arc4random_uniform(nElements) 感谢 Gregory Goltsov 的回答以及 miho 和 blahdiblah 的 cmets

编辑:循环改进,感谢 Ron 的评论

编辑:添加了检查数组是否为空,感谢 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

【讨论】:

  • 不错的解决方案。是的,正如 willc2 所提到的,用 arc4random() 替换 random() 是一个很好的改进,因为不需要播种。
  • @Jason:有时(例如在测试时),能够提供种子是一件好事。 Kristopher:不错的算法。这是 Fisher-Yates 算法的一个实现:en.wikipedia.org/wiki/Fisher-Yates_shuffle
  • 一个超级小的改进:在循环的最后一次迭代中,i == count - 1。这是否意味着我们正在交换索引 i 处的对象本身?我们可以调整代码以始终跳过最后一次迭代吗?
  • 你认为硬币只有在结果与最初向上的一面相反时才被翻转?
  • 这个洗牌有微妙的偏见。使用arc4random_uniform(nElements) 而不是arc4random()%nElements。请参阅the arc4random man pagethis explanation of modulo bias 了解更多信息。
【解决方案2】:

您不需要 swapObjectAtIndex 方法。 exchangeObjectAtIndex:withObjectAtIndex: 已经存在。

【讨论】:

    【解决方案3】:

    由于我还不能发表评论,我想我会做出完整的回应。我以多种方式为我的项目修改了 Kristopher Johnson 的实现(确实试图使其尽可能简洁),其中之一是 arc4random_uniform(),因为它避免了 modulo bias

    // 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
    

    【讨论】:

    • 请注意,您在循环的每次迭代中都调用了 [self count](属性获取器)两次。我认为将其移出循环是值得的。
    • 这就是为什么我仍然更喜欢[object method] 而不是object.method:人们往往会忘记后者并不像访问结构成员那样便宜,它伴随着方法调用的成本。 .. 在一个循环中非常糟糕。
    • 感谢您的更正 - 由于某种原因,我错误地认为计数已被缓存。更新了答案。
    【解决方案4】:

    如果导入GameplayKit,则有shuffled API:

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

    let shuffledArray = array.shuffled()
    

    【讨论】:

    • 我有 myArray 并想创建一个新的 shuffleArray 它。我如何使用 Objective-C 来做到这一点?
    • shuffledArray = [array shuffledArray];
    • 注意这个方法是GameplayKit的一部分,所以你必须导入它。
    【解决方案5】:

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

    算法相同,在文献中描述为“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])
            }
        }
    }
    

    注意:A more concise solution in Swift is possible from iOS10 using GameplayKit.

    注意:An algorithm for unstable shuffling (with all positions forced to change if count > 1) is also available

    【讨论】:

    • 这和 Kristopher Johnson 的算法有什么区别?
    • @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

    【讨论】:

    • 对于这个解决方案,使用 arc4random() 或种子。
    • 这种洗牌是有缺陷的——正如微软最近被提醒的那样:robweir.com/blog/2010/02/microsoft-random-browser-ballot.html
    • 同意,有缺陷,因为“排序需要一个自洽的排序定义”,正如那篇关于 MS 的文章中所指出的那样。看起来很优雅,其实不然。
    【解决方案7】:

    从 iOS 10 开始,您可以使用 NSArray shuffled() from GameplayKit。这是 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())
        }
    }
    

    【讨论】:

      【解决方案8】:

      有一个不错的流行库,其中包含此方法,称为SSToolKit in GitHub。 文件 NSMutableArray+SSToolkitAdditions.h 包含 shuffle 方法。你也可以使用它。其中,似乎有很多有用的东西。

      这个库的主页是here

      如果你使用这个,你的代码会是这样的:

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

      这个库还有一个 Pod(参见 CocoaPods)

      【讨论】:

        【解决方案9】:

        如果元素有重复。

        例如数组:A A A B B 或 B B A A A

        唯一的解决方案是:A B A B A

        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 可以防止在多个实例上工作:使用两种方法会更加安全和易读,主要的方法是洗牌并调用辅助方法,而辅助方法只调用自身并且永远不会重新洗牌。还有一个拼写错误。
        【解决方案10】:
        NSUInteger randomIndex = arc4random() % [theArray count];
        

        【讨论】:

        • arc4random_uniform([theArray count]) 会更好,如果在您支持的 Mac OS X 或 iOS 版本上可用。
        • 我这样给的数字会重复。
        【解决方案11】:

        Kristopher Johnson's answer 很不错,但也不是完全随机的。

        给定一个包含 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 &lt; (count-1)。)
        【解决方案12】:

        编辑:这是不正确的。出于参考目的,我没有删除这篇文章。请参阅 cmets 了解这种方法不正确的原因。

        这里有简单的代码:

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

        【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-10-26
        • 2011-01-28
        • 2016-12-19
        • 2017-06-17
        • 1970-01-01
        相关资源
        最近更新 更多