Как работает цикл при перемешивании массива и зачем он нужен в JavaScript
Что проверяют
Знаете ли вы алгоритм Фишера-Йетса и понимаете ли, почему он даёт равновероятные перестановки, а популярный однострочник нет. Следом часто просят найти ошибку в чужой реализации или объяснить, что будет при сдвиге границы случайного индекса.
Как отвечать
Цикл идёт с конца массива к началу. На шаге i выбирается случайный индекс j от 0 до i включительно: Math.floor(Math.random() * (i + 1)), и элементы i и j меняются местами, например деструктуризацией [a[i], a[j]] = [a[j], a[i]]. После этого позиция i больше не трогается.
Зачем именно такой цикл:
- равновероятность: на первом шаге
nвариантов, на каждом следующем на один меньше. Итого ровноn!равновероятных исходов, по одному на каждую перестановку; - сложность: O(n) по времени и O(1) по памяти, перемешивание на месте;
- мутация: функция меняет исходный массив. Если данные пришли из props или стора, сначала копия через
[...arr], иначе React не заметит изменения, а вы испортите чужое состояние.
Для фронтенда добавьте два момента. Перемешивание прямо в рендере даёт новый порядок на каждый ререндер, поэтому результат кладут в useMemo или считают один раз по событию. При серверном рендеринге порядок на сервере и в браузере разойдётся, и React сообщит об ошибке гидратации: перемешивайте на клиенте после монтирования или передавайте готовый порядок с сервера. И Math.random() не криптостойкий: для розыгрышей и всего, где важна непредсказуемость, берут crypto.getRandomValues().
Где спотыкаются
Пишут arr.sort(() => Math.random()-0.5). Компаратор противоречив, распределение неравномерное и зависит от алгоритма сортировки в движке, а сложность O(n log n).
Берут j из всего массива на каждом шаге. Исходов становится n^n, это число не делится на n! нацело, значит часть перестановок выпадает чаще остальных.
Пишут Math.random() * i без единицы. Это уже алгоритм Саттоло: ни один элемент не остаётся на своём месте, и получаются только циклические перестановки.