洗牌 NSMutableArray 的最佳方法是什么?

如果你有一个 NSMutableArray,你如何随机洗牌的元素?

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


更新: 正如@Mukesh 提到的,从 iOS 10 + 和 macOS 10.12 + 开始,有一个 -[NSMutableArray shuffledArray]方法可以用于 shuffle。详情请参阅 https://developer.apple.com/documentation/foundation/nsarray/1640855-shuffledarray?language=objc。(但请注意,这将创建一个新的数组,而不是在适当的位置重新组合元素。)

78441 次浏览

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

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

编辑: 由于 Gregory Goltsov 的回答和 miho 和 blahdibra 的评论,将 (arc4random() % nElements)改为 arc4random_uniform(nElements)

编辑: 循环改进,感谢 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

您不需要 swapObjectAtIndex 方法。 ExchangeObjectAtIndex: with ObjectAtIndex:已经存在。

这是对 NSArray 或 NSMutableArray 进行洗牌的最简单和最快的方法 (对象拼图是一个 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

您也可以比较 objec1和 objec2,然后决定返回什么 可能的值是:

  • 升序 = -1
  • NSOrdered势均力敌 = 0
  • 下降 = 1
NSUInteger randomIndex = arc4random() % [theArray count];

既然我还不能发表评论,我想我应该给出一个完整的回应。我为我的项目修改了 Christopher 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

有一个非常流行的库,其中包含这个方法,称为 GitHub 中的 SSToolKit。 文件 NSMutableArray + SSToolkitAdditions.h 包含 shuffle 方法。您也可以使用它。

这个库的主页是 给你

如果您使用这个,您的代码将是这样的:

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

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

如果元素有重复。

例如,array: A A B B 或 B B A A 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;
}
}
}
}
}

编辑: 这是不对的。作为参考,我没有删除这篇文章。请参阅关于此方法不正确的原因的注释。

这里的代码很简单:

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

略有改进的简明解决方案(与顶级答案相比)。

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

目标 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])
}
}
}

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

注: 还有一种不稳定洗牌算法(如果 count > 1,则所有位置都必须更改)

如果您导入 GameplayKit,有一个 shuffled API:

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

let shuffledArray = array.shuffled()

在 iOS10中,你可以使用 来自 GameplayKit 的 NSArray shuffled():

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