Случайный алгоритм списка воспроизведения

Мне нужно создать список чисел из диапазона (например, от x до y) в случайном порядке, чтобы каждый порядок имел равные шансы.

Мне нужно это для музыкального плеера, который я пишу на С#, для создания списков воспроизведения в случайном порядке.

Любые идеи?

Спасибо.

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

Вот что я до сих пор писала:

    public static IEnumerable<int> RandomIndexes(int count)
    {
        if (count > 0)
        {
            int[] indexes = new int[count];
            int indexesCountMinus1 = count - 1;

            for (int i = 0; i < count; i++)
            {
                indexes[i] = i;
            }

            Random random = new Random();

            while (indexesCountMinus1 > 0)
            {
                int currIndex = random.Next(0, indexesCountMinus1 + 1);
                yield return indexes[currIndex];

                indexes[currIndex] = indexes[indexesCountMinus1];
                indexesCountMinus1--;
            }

            yield return indexes[0];
        }
    }

Он работает, но единственная проблема заключается в том, что мне нужно выделить массив в памяти размером count. Я ищу что-то, что доза не требует выделения памяти.

Спасибо.

Ответ 1

Это может быть сложно, если вы не будете осторожны (т.е. используете алгоритм наивного перетасовки). Посмотрите на алгоритм Fisher-Yates/Knuth shuffle для правильного распределения значений.

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

Здесь подробнее от Джеффа Этвуда.

Наконец, здесь Jon Skeet реализация и описание.

ИЗМЕНИТЬ

Я не верю, что существует решение, удовлетворяющее вашим двум противоречивым требованиям (во-первых, чтобы быть случайным без повторов и во-вторых, чтобы не выделять дополнительную память). Я считаю, что вы можете преждевременно оптимизировать свое решение, поскольку последствия для памяти должны быть незначительными, если только вы не встроены. Или, может быть, я недостаточно умен, чтобы придумать ответ.

С этим, здесь код, который создаст массив равномерно распределенных случайных индексов с использованием алгоритма Кнута-Фишера-Йейса (с небольшой модификацией). Вы можете кэшировать результирующий массив или выполнить любое количество оптимизаций в зависимости от остальной части вашей реализации.

  private static int[] BuildShuffledIndexArray( int size ) {

     int[] array = new int[size];
     Random rand = new Random();
     for ( int currentIndex = array.Length - 1; currentIndex > 0; currentIndex-- ) {
        int nextIndex = rand.Next( currentIndex + 1 );
        Swap( array, currentIndex, nextIndex );
     }
     return array;
  }

  private static void Swap( IList<int> array, int firstIndex, int secondIndex ) {

     if ( array[firstIndex] == 0 ) {
        array[firstIndex] = firstIndex;
     }
     if ( array[secondIndex] == 0 ) {
        array[secondIndex] = secondIndex;
     }
     int temp = array[secondIndex];
     array[secondIndex] = array[firstIndex];
     array[firstIndex] = temp;
  }

ПРИМЕЧАНИЕ. Вы можете использовать ushort вместо int до половины размера в памяти, если в вашем плейлисте не больше 65 535 элементов. Вы всегда можете программно переключаться на int, если размер превышает ushort.MaxValue. Если бы я лично добавил в плейлист более 65 тыс. Элементов, я бы не стал шокирован увеличением использования памяти.

Помните, что это управляемый язык. VM всегда резервирует больше памяти, чем вы используете, чтобы ограничить количество раз, когда нужно запросить ОС для большей ОЗУ и ограничить фрагментацию.

ИЗМЕНИТЬ

Хорошо, последняя попытка: мы можем попытаться настроить компиляцию производительности/памяти: вы можете создать свой список целых чисел, а затем записать его на диск. Затем просто держите указатель на смещение в файле. Затем каждый раз, когда вам нужен новый номер, у вас просто есть дисковый ввод-вывод. Возможно, вы можете найти какой-то баланс здесь и просто прочитать N-размерные блоки данных в памяти, где N - это номер, с которым вам удобно.

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

Ответ 2

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

IEnumerable<Song> GetSongOrder(List<Song> allSongs)
{
    var playOrder = new List<Song>();
    while (true)
    {
        // this step assigns an integer weight to each song,
        // corresponding to how likely it is to be played next.
        // in a better implementation, this would look at the total number of
        // songs as well, and provide a smoother ramp up/down.
        var weights = allSongs.Select(x => playOrder.LastIndexOf(x) > playOrder.Length - 10 ? 50 : 1);

        int position = random.Next(weights.Sum());
        foreach (int i in Enumerable.Range(allSongs.Length))
        {
            position -= weights[i];
            if (position < 0)
            {
                var song = allSongs[i];
                playOrder.Add(song);
                yield return song;
                break;
            }
        }

        // trim playOrder to prevent infinite memory here as well.
        if (playOrder.Length > allSongs.Length * 10)
            playOrder = playOrder.Skip(allSongs.Length * 8).ToList();
    }    
}

Это сделало бы песни упорядоченными, пока они еще не были сыграны. Это обеспечивает "более плавные" переходы с конца одного перетасовки на следующий, потому что первая песня следующего перетасовка может быть той же песней, что и последняя перетасовка с вероятностью 1/(всего песен), тогда как этот алгоритм имеет более низкую (и настраиваемый) шанс услышать одну из последних x песен снова.

Ответ 3

Если вы используете максимальный регистр сдвига с линейной обратной связью, вы будете использовать O (1) памяти и примерно O (1) раз. Смотрите здесь для удобной реализации C (две строки! woo-hoo!) и таблицы условий обратной связи для использования.

И вот решение:

public class MaximalLFSR
{
    private int GetFeedbackSize(uint v)
    {
        uint r = 0;

        while ((v >>= 1) != 0)
        {
          r++;
        }
        if (r < 4)
            r = 4;
        return (int)r;
    }

    static uint[] _feedback = new uint[] {
        0x9, 0x17, 0x30, 0x44, 0x8e,
        0x108, 0x20d, 0x402, 0x829, 0x1013, 0x203d, 0x4001, 0x801f,
        0x1002a, 0x2018b, 0x400e3, 0x801e1, 0x10011e, 0x2002cc, 0x400079, 0x80035e,
        0x1000160, 0x20001e4, 0x4000203, 0x8000100, 0x10000235, 0x2000027d, 0x4000016f, 0x80000478
    };

    private uint GetFeedbackTerm(int bits)
    {
        if (bits < 4 || bits >= 28)
            throw new ArgumentOutOfRangeException("bits");
        return _feedback[bits];
    }

    public IEnumerable<int> RandomIndexes(int count)
    {
        if (count < 0)
            throw new ArgumentOutOfRangeException("count");

        int bitsForFeedback = GetFeedbackSize((uint)count);

        Random r = new Random();
        uint i = (uint)(r.Next(1, count - 1));

        uint feedback = GetFeedbackTerm(bitsForFeedback);
        int valuesReturned = 0;
        while (valuesReturned < count)
        {
            if ((i & 1) != 0)
            {
                i = (i >> 1) ^ feedback;
            }
            else {
                i = (i >> 1);
            }
            if (i <= count)
            {
                valuesReturned++;
                yield return (int)(i-1);
            }
        }
    }
}

Теперь я выбрал случай обратной связи (плохо) случайным образом по ссылке выше. Вы также можете реализовать версию с несколькими максимальными терминами, и вы выбираете одну из них наугад, но знаете что? Это довольно хорошо для того, что вы хотите.

Вот тестовый код:

    static void Main(string[] args)
    {
        while (true)
        {
            Console.Write("Enter a count: ");
            string s = Console.ReadLine();
            int count;
            if (Int32.TryParse(s, out count))
            {
                MaximalLFSR lfsr = new MaximalLFSR();
                foreach (int i in lfsr.RandomIndexes(count))
                {
                    Console.Write(i + ", ");
                }
            }
            Console.WriteLine("Done.");
        }
    }

Помните, что максимальная LFSR никогда не генерирует 0. Я взломал это, вернув i-й термин - 1. Это работает достаточно хорошо. Кроме того, поскольку вы хотите гарантировать уникальность, я игнорирую что-либо вне диапазона - LFSR генерирует последовательности до двух значений, поэтому в высоких диапазонах он генерирует слишком много значений 2x-1. Они будут пропущены - это все равно будет быстрее, чем FYK.

Ответ 4

Если вы не перетасовываете исходный список композиций (который, как вы сказали, вы не хотите делать), вам нужно будет выделить дополнительную память, чтобы выполнить то, что вам нужно.

Если вы произвольно генерируете случайную перестановку индексов песни (как вы это делаете), вам, очевидно, необходимо выделить некоторый нетривиальный объем памяти для ее сохранения, либо закодированного, либо как список.

Если пользователю не нужно видеть список, вы можете генерировать произвольный порядок песен на лету: после каждой песни выберите другую случайную песню из пула неиграемых песен. Вы все равно должны следить за тем, какие песни уже были воспроизведены, но вы можете использовать бит для этого. Если у вас есть 10000 песен, вам нужно всего 10000 бит (1250 байт), каждый из которых отражает, была ли песня еще сыграна.

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

Ответ 5

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

Чтобы сделать повторный заказ без повторений и не сделать код ненадежным, вы должны отслеживать, что вы уже использовали/любите, сохраняя неиспользуемые индексы или косвенно путем замены из исходного списка.

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

Ответ 6

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

Случай 1: если count < максимальное ограничение памяти, заранее создать плейлист и использовать Knuth shuffle (см. реализацию Jon Skeet, упомянутую в других ответах).

Случай 2: Если count >= максимальное ограничение памяти, песня, которую нужно воспроизвести, будет определена во время выполнения (я сделаю это, как только начнется воспроизведение песни, чтобы следующая песня уже была создана к тому моменту, когда текущая конец песни). Сохраните последнее [максимальное ограничение памяти или некоторое количество токенов] количество воспроизводимых песен, сгенерируйте случайное число (R) между 1 и количеством песен, а если R = одна из X последних воспроизводимых песен, сгенерируйте новый R до тех пор, пока он не будет в списке. Воспроизвести эту песню.

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

Ответ 7

вы могли бы использовать трюк, который мы делаем на сервере sql, чтобы упорядочить наборы в случайном порядке, используя это с помощью guid. значения всегда распределены равными случайными.

private IEnumerable<int> RandomIndexes(int startIndexInclusive, int endIndexInclusive)
{
    if (endIndexInclusive < startIndexInclusive)
        throw new Exception("endIndex must be equal or higher than startIndex");

    List<int> originalList = new List<int>(endIndexInclusive - startIndexInclusive);
    for (int i = startIndexInclusive; i <= endIndexInclusive; i++)
        originalList.Add(i);

    return from i in originalList
           orderby Guid.NewGuid()
           select i;
}

Ответ 8

С логической точки зрения это возможно. Учитывая список русских песен, есть n! перестановки; если вы назначили каждой перестановке число от 1 до n! (или от 0 до n! -1:-D) и выберите одно из этих чисел случайным образом, вы можете сохранить номер перестановки, которую вы используете в настоящее время, вместе с исходным списком и индексом текущей песни в пределах перестановка.

Например, если у вас есть список песен {1, 2, 3}, ваши перестановки:

0: {1, 2, 3}
1: {1, 3, 2}
2: {2, 1, 3}
3: {2, 3, 1}
4: {3, 1, 2}
5: {3, 2, 1}

Поэтому единственными данными, которые мне нужно отслеживать, является исходный список ({1, 2, 3}), текущий индекс песни (например, 1) и индекс перестановки (например, 3). Затем, если я хочу найти следующую песню для воспроизведения, я знаю ее третья (2, но нулевая) песня перестановки 3, например. Песня 1.

Однако этот метод основан на том, что у вас есть эффективный способ определения i-й песни j-й перестановки, которая, пока у меня не было возможности подумать (или кто-то с более сильным математическим фоном, чем я могу вставлять), эквивалентен "то происходит чудо". Но принцип есть.

Ответ 9

Существует несколько способов генерации перестановок без необходимости сохранения состояния. См. этот вопрос.

Ответ 10

Вам нужно будет выделить некоторую память, но это не должно быть много. Вы можете уменьшить объем памяти (степень, в которой я не уверен, поскольку я не знаю, что много о кишках С#), используя массив bool вместо int. В лучшем случае это будет использовать (count/8) байт памяти, что не так уж плохо (но я сомневаюсь, что С# фактически представляет bools как отдельные биты).

    public static IEnumerable<int> RandomIndexes(int count) {
        Random rand = new Random();
        bool[] used = new bool[count];

        int i;
        for (int counter = 0; counter < count; counter++) {
            while (used[i = rand.Next(count)]); //i = some random unused value
            used[i] = true;
            yield return i;
        }
    }

Надеюсь, что это поможет!

Ответ 11

Как и многие другие, вы должны внедрить THEN, оптимизировать и оптимизировать только те части, которые вам нужны (которые вы проверяете с помощью профилировщика). Я предлагаю (надеюсь) изящный способ получения списка, который вам нужен, что на самом деле не очень заботит производительность:

using System;
using System.Collections.Generic;
using System.Linq;

namespace Test
{
    class Program
    {
        static void Main(string[] a)
        {
            Random random = new Random();
            List<int> list1 = new List<int>(); //source list
            List<int> list2 = new List<int>();
            list2 = random.SequenceWhile((i) =>
                 {
                     if (list2.Contains(i))
                     {
                         return false;
                     }
                     list2.Add(i);
                     return true;
                 },
                 () => list2.Count == list1.Count,
                 list1.Count).ToList();

        }
    }
    public static class RandomExtensions
    {
        public static IEnumerable<int> SequenceWhile(
            this Random random, 
            Func<int, bool> shouldSkip, 
            Func<bool> continuationCondition,
            int maxValue)
        {
            int current = random.Next(maxValue);
            while (continuationCondition())
            {
                if (!shouldSkip(current))
                {
                    yield return current;
                }
                current = random.Next(maxValue);
            }
        }
    }
}

Ответ 12

Это практически невозможно сделать, не выделяя лишнюю память. Если вас беспокоит объем выделенной дополнительной памяти, вы всегда можете выбрать произвольное подмножество и перетасовать между ними. Вы будете получать повторы перед каждой песней, но с достаточно большим подмножеством, которое я гарантирую, что мало кто заметит.

const int MaxItemsToShuffle = 20;
public static IEnumerable<int> RandomIndexes(int count)
{
    Random random = new Random();

    int indexCount = Math.Min(count, MaxItemsToShuffle);
    int[] indexes = new int[indexCount];

    if (count > MaxItemsToShuffle)
    {
        int cur = 0, subsetCount = MaxItemsToShuffle;
        for (int i = 0; i < count; i += 1)
        {
            if (random.NextDouble() <= ((float)subsetCount / (float)(count - i + 1)))
            {
                indexes[cur] = i;
                cur += 1;
                subsetCount -= 1;
            }
        }
    }
    else
    {
        for (int i = 0; i < count; i += 1)
        {
            indexes[i] = i;
        }
    }

    for (int i = indexCount; i > 0; i -= 1)
    {
        int curIndex = random.Next(0, i);
        yield return indexes[curIndex];

        indexes[curIndex] = indexes[i - 1];
    }
}