NSMutableArray를 섞는 가장 좋은 방법은 무엇입니까?


187

가있는 경우 NSMutableArray요소를 무작위로 임의 섞는 방법은 무엇입니까?

(아래에 게시 된 이것에 대한 내 대답이 있지만 Cocoa를 처음 사용하고 더 좋은 방법이 있는지 알고 싶습니다.)


업데이트 : @Mukesh가 지적한 것처럼 iOS 10 이상 및 macOS 10.12 이상에서는 -[NSMutableArray shuffledArray]셔플하는 데 사용할 수 있는 방법이 있습니다. 자세한 내용은 https://developer.apple.com/documentation/foundation/nsarray/1640855-shuffledarray?language=objc 를 참조하십시오. (그러나 이것은 요소를 제자리에 섞지 않고 새로운 배열을 만듭니다.)


셔플 링 알고리즘과 관련하여 순진 셔플 링 과 관련된 실제 문제를 살펴보십시오 .
craigb

여기 스위프트의 구현은 다음과 같습니다 iosdevelopertips.com/swift-code/swift-shuffle-array-type.html
크리스토퍼 존슨

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에 범주를 추가하여이 문제를 해결했습니다.

편집 : Ladd의 답변 덕분에 불필요한 방법이 제거되었습니다.

편집 : Gregory Goltsov의 답변과 miho 및 blahdiblah의 의견 덕분에 변경 (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

10
좋은 해결책. willc2가 언급했듯이 random ()을 arc4random ()으로 바꾸는 것은 시드가 필요하지 않기 때문에 좋은 개선입니다.
Jason Moore

4
@Jason : 때때로 (예를 들어 테스트 할 때) 씨앗을 공급할 수있는 것이 좋습니다. Kristopher : 좋은 알고리즘. Fisher-Yates 알고리즘의 구현입니다. en.wikipedia.org/wiki/Fisher-Yates_shuffle
JeremyP

4
슈퍼 약간의 개선 : 루프의 마지막 반복에서, 내가 계산 == - 1. 우리는 자체 인덱스 난에서 객체를 교환하는 것을 의미하는 것이하지 않는 이유는 무엇입니까? 항상 마지막 반복을 건너 뛰도록 코드를 조정할 수 있습니까?
Ron

10
결과가 원래 있던 쪽과 반대 인 경우에만 동전이 뒤집힌 것으로 생각하십니까?
Kristopher Johnson

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]루프를 반복 할 때마다 (속성 게터)를 두 번 호출 합니다. 루프에서 벗어나면 간결함을 잃을 가치가 있다고 생각합니다.
Kristopher Johnson

1
그리고 그것이 내가 여전히 선호하는 이유 [object method]입니다 object.method. 사람들은 나중에 멤버 멤버에 액세스하는 것만 큼 싸지 않다는 것을 잊어 버리는 경향이 있습니다.
DarkDust

수정 해 주셔서 감사합니다. 어떤 이유로 카운트가 캐시되었다고 잘못 가정했습니다. 답변을 업데이트했습니다.
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

스위프트 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있습니다.

참고 : 불안정한 셔플 링 알고리즘 (카운트> 1 인 경우 모든 위치를 강제로 변경)도 사용할 수 있습니다


이 알고리즘과 Kristopher Johnson 알고리즘의 차이점은 무엇입니까?
Iulian Onofrei

@IulianOnofrei는 원래 Kristopher Johnson의 코드가 최적이 아니었고 답변을 개선 한 후 쓸모없는 초기 검사를 추가하여 다시 편집했습니다. 나는 간결한 작성 방법을 선호합니다. 알고리즘은 동일하며 문헌에서 " Fisher-Yates shuffle " 로 설명되어 있습니다.
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를 사용하십시오.
Johan Kool

17
robweir.com/blog/2010/02/microsoft-random-browser-ballot.html 에서 Microsoft가 최근에 상기 한 것처럼이 셔플은 결함이 있습니다.
Raphael Schweikert

MS에 관한 기사에서 지적한 "정렬에 대한 일관성있는 정의가 필요하기 때문에"합의에 결함이 있습니다. 우아해 보이지만 그렇지 않습니다.
Jeff

2

GitHub의 SSToolKit 이라는 부분 으로이 방법을 사용하는 인기있는 라이브러리 가 있습니다 . 파일 NSMutableArray + SSToolkitAdditions.h는 셔플 방법을 포함합니다. 사용할 수도 있습니다. 이 중에서도 유용한 것들이 많이있는 것 같습니다.

이 라이브러리의 메인 페이지는 여기에 있습니다 .

이것을 사용하면 코드는 다음과 같습니다.

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

이 라이브러리에는 포드도 있습니다 (CocoaPods 참조).


2

iOS 10 부터 GameplayKit의 NSArray shuffled() 사용할 수 있습니다 . 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 은 obj 클래스의 요소를 저장하는 NSMutableArray이며 일부 시퀀스에 대한 포인터입니다.

- (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하면 여러 인스턴스에서 작업 하는 것을 방지 할 수 있습니다. 두 가지 방법 (셔플을 수행하고 보조 메서드를 호출하는 기본 방법)을 사용하는 것이 훨씬 안전하고 읽기 쉬우 며 보조 메서드는 자체 호출 만하고 다시 섞지 않습니다. 또한 철자 실수가 있습니다.
Cœur

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

2
또는 arc4random_uniform([theArray count])지원하는 Mac OS X 또는 iOS 버전에서 사용 가능한 경우 더 좋습니다.
Kristopher Johnson

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가 0 일 때 arc4random_uniform (2)은 0 또는 1을 반환하므로 0 번째 요소는 자체와 교환되거나 1 번째 요소와 교환됩니다 요소. 다음 반복에서 i가 1이면 arc4random (1)은 항상 0을 반환하며, i 번째 요소는 항상 자체와 교환되므로 비효율적이지만 올바르지 않습니다. (아마 루프 조건이되어야합니다 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;
        }
    }];
}

이 셔플은 결함이 있습니다 – robweir.com/blog/2010/02/microsoft-random-browser-ballot.html
Cœur
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.