Статьи

Совет: как случайным образом перемешать массив в AS3

Иногда у вас есть набор предметов — это могут быть строки, числа или числа, что угодно, порядок которых вы хотите рандомизировать. Это особенно полезно для викторин и азартных игр, но пригодится во всех других приложениях. Самый простой способ, который я нашел для этого, — вставить все элементы в массив, а затем перетасовать его, как колоду карт. Но как мы можем это сделать …?

В качестве простого примера мы будем использовать буквы алфавита:

1
var letters:Array = [«A», «B», «C», «D», «E», «F», «G», «H», «I», «J», «K», «L», «M», «N», «O», «P», «Q», «R», «S», «T», «U», «V», «W», «X», «Y», «Z»];

Существуют разные подходы к сортировке массива.


Мы могли бы создать второй массив и скопировать каждый элемент первого в случайную позицию во втором:

Код для этого может выглядеть следующим образом (см. Этот Быстрый совет по получению случайного целого числа в пределах определенного диапазона для более подробной информации):

1
2
3
4
5
6
7
8
9
var letters:Array = [«A», «B», «C», «D», «E», «F», «G», «H», «I», «J», «K», «L», «M», «N», «O», «P», «Q», «R», «S», «T», «U», «V», «W», «X», «Y», «Z»];
var shuffledLetters:Array = new Array(letters.length);
 
var randomPos:int = 0;
for (var i:int = 0; i < letters.length; i++)
{
    randomPos = int(Math.random() * letters.length);
    shuffledLetters[randomPos] = letters[i];
}

Но есть одна огромная проблема. Что если случайная позиция, выбранная для C равна 6?

Здесь A перезаписывается и, следовательно, не будет в перемешанном массиве. Это не то, что мы хотим, поэтому мы должны проверить, что слот пуст, прежде чем копировать письмо, и выбрать другой слот, если это не так:

01
02
03
04
05
06
07
08
09
10
11
12
13
var letters:Array = [«A», «B», «C», «D», «E», «F», «G», «H», «I», «J», «K», «L», «M», «N», «O», «P», «Q», «R», «S», «T», «U», «V», «W», «X», «Y», «Z»];
var shuffledLetters:Array = new Array(letters.length);
 
var randomPos:int = 0;
for (var i:int = 0; i < letters.length; i++)
{
    randomPos = int(Math.random() * letters.length);
    while (shuffledLetters[randomPos] != null) //repeat as long as the slot is not empty
    {
        randomPos = int(Math.random() * letters.length);
    }
    shuffledLetters[randomPos] = letters[i];
}

Это работает. Проблема в том, что это неэффективно. Видите, буква A гарантированно помещается в выбранный слот, потому что это первая выбранная буква, поэтому все слоты будут пустыми. С B есть шанс 25 на 26, что первый выбранный слот будет пустым. Когда мы на полпути к алфавиту, этот шанс падает до 50/50. К тому времени, когда мы достигнем V , есть вероятность 50/50, что мы не найдем пустой слот до четвертой попытки.

Это означает, что мы, скорее всего, будем вызывать Math.random() wayyyyy более 26 раз. И Math.random() является относительно медленной функцией. Какой другой подход мы можем использовать?


Что если бы мы сохранили список всех пустых слотов в третьем массиве, который уменьшился бы при их просмотре?

… и так далее, повторяя, пока массив пустых слотов не содержит элементов. Код для этого будет выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
var letters:Array = [«A», «B», «C», «D», «E», «F», «G», «H», «I», «J», «K», «L», «M», «N», «O», «P», «Q», «R», «S», «T», «U», «V», «W», «X», «Y», «Z»];
var shuffledLetters:Array = new Array(letters.length);
 
var emptySlots:Array = new Array();
for (var n:int = 0; n < letters.length; n++)
{
    emptySlots.push(n);
}
 
var randomPos:Number = 0;
var actualSlot:Number = 0;
for (var i:int = 0; i < letters.length; i++)
{
    randomPos = int(Math.random() * emptySlots.length);
    actualSlot = emptySlots[randomPos];
    shuffledLetters[actualSlot] = letters[i];
    emptySlots.splice(randomPos, 1);
}

Здесь мы используем метод Array.splice () для удаления одного элемента из списка пустых слотов. Это фактически удаляет элемент, а не просто устанавливает его значение равным null ; поэтому после объединения первого слота emptySlots.length будет 25 а не 26 .

Эта функция splice() великолепна; мы можем использовать это для третьего подхода к тасованию, который исключает этот массив среднего человека.


Вместо того, чтобы удалять элементы из массива пустых слотов, когда мы закончили с ними, мы могли бы удалить их из исходного массива без перетасовки.

Поначалу это звучит не очень полезно — но что, если мы выбрали случайные элементы из исходного массива вместо случайного выбора их пунктов назначения ?

… и так далее, пока первый массив не содержит элементов.

В отличие от двух других подходов, мы получаем без исходного массива. Является ли это проблемой или нет, зависит от этого проекта.

Код может выглядеть так:

01
02
03
04
05
06
07
08
09
10
var letters:Array = [«A», «B», «C», «D», «E», «F», «G», «H», «I», «J», «K», «L», «M», «N», «O», «P», «Q», «R», «S», «T», «U», «V», «W», «X», «Y», «Z»];
var shuffledLetters:Array = new Array(letters.length);
 
var randomPos:Number = 0;
for (var i:int = 0; i < shuffledLetters.length; i++) //use shuffledLetters.length because splice() will change letters.length
{
    randomPos = int(Math.random() * letters.length);
    shuffledLetters[i] = letters[randomPos];
    letters.splice(randomPos, 1);
}

Фактически, поскольку splice() возвращает Array всех элементов, которые вы объединяете, мы могли бы немного упростить код:

1
2
3
4
5
6
7
8
9
var letters:Array = [«A», «B», «C», «D», «E», «F», «G», «H», «I», «J», «K», «L», «M», «N», «O», «P», «Q», «R», «S», «T», «U», «V», «W», «X», «Y», «Z»];
var shuffledLetters:Array = new Array(letters.length);
 
var randomPos:Number = 0;
for (var i:int = 0; i < shuffledLetters.length; i++)
{
    randomPos = int(Math.random() * letters.length);
    shuffledLetters[i] = letters.splice(randomPos, 1)[0];
}

Это аккуратнее. У меня есть еще один подход, которым можно поделиться; этот очень отличается от других, которые мы использовали до сих пор.


Массивы имеют метод sort () , который по умолчанию переставляет все элементы массива в возрастающем алфавитно-цифровом порядке, например так:

1
2
3
var mixedLetters:Array = [«G», «B», «P», «M», «F»];
mixedLetters.sort();
//mixedLetters is now [«B», «F», «G», «M», «P»]

Вы можете передать параметры этому методу, например Array.DESCENDING , который меняет порядок сортировки:

1
2
3
var mixedLetters:Array = [«G», «B», «P», «M», «F»];
mixedLetters.sort(Array.DESCENDING);
//mixedLetters is now [«P», «M», «G», «F», «B»]

(Существуют и другие варианты , и вы можете передать их столько, сколько захотите.)

Вы также можете передать ссылку на функцию , которая сообщает Flash, как решить, к какому порядку относятся любые два элемента. Например, эту функцию можно использовать для числовой сортировки:

1
2
3
4
5
6
function numericalSort(a:Number, b:Number):Number
{
    if (a < b) return -1;
    if (a == b) return 0;
    if (a > b) return 1;
}

Flash просматривает каждую пару соседних элементов в массиве и переставляет их в соответствии с этой функцией: он меняет их местами, если возвращается значение 1 , и оставляет их в покое в противном случае. (Если вы не передадите Array.DESCENDING а также функцию, в этом случае она меняет местами их, если возвращается значение -1 , и оставляет их в покое в противном случае.) Flash повторяет это по всему массиву снова и снова, пока все пары не вернут 0 или -1 ( 0 или 1 если используется Array.DESCENDING ).

Мы можем связываться с этим. Вместо того, чтобы указывать истинную причину, по которой любые два элемента должны быть поменяны местами, мы можем просто сказать, чтобы они менялись местами случайным образом, используя функцию сортировки, подобную этой:

1
2
3
4
5
function randomSort(a:*, b:*):Number //* means any kind of input
{
    if (Math.random() < 0.5) return -1;
    else return 1;
}

Легко! Теперь мы можем использовать его в нашем коде так:

01
02
03
04
05
06
07
08
09
10
function randomSort(a:*, b:*):Number
{
    if (Math.random() < 0.5) return -1;
    else return 1;
}
 
var letters:Array = [«A», «B», «C», «D», «E», «F», «G», «H», «I», «J», «K», «L», «M», «N», «O», «P», «Q», «R», «S», «T», «U», «V», «W», «X», «Y», «Z»];
letters.sort(randomSort);
 
//(no need for the shuffledLetters[] Array)

Обратите внимание, что результирующий массив не будет случайным образом перетасовываться, как другие используемые нами подходы — в этом подходе нет даже 1/26 вероятности того, что любая данная буква окажется в любом данном слоте. Это потому, что мы меняем только смежные пары элементов и делаем не более того. Тем не менее, это аккуратный метод 🙂

Есть много других методов, я знаю. Есть что-нибудь, что тебе нравится больше, чем эти?

Редактировать: Вот отличный пост с некоторыми очень классными визуализациями, объясняющими случайную случайность Фишера-Йейтса, которая работает на месте. Я очень рекомендую это!