Random playlist algorithm

后端 未结 12 1471
北恋
北恋 2020-12-09 04:58

I need to create a list of numbers from a range (for example from x to y) in a random order so that every order has an equal chance.

I need this for a music player I

相关标签:
12条回答
  • 2020-12-09 05:32

    From a logical standpoint, it is possible. Given a list of n songs, there are n! permutations; if you assign each permutation a number from 1 to n! (or 0 to n!-1 :-D) and pick one of those numbers at random, you can then store the number of the permutation that you are currently using, along with the original list and the index of the current song within the permutation.

    For example, if you have a list of songs {1, 2, 3}, your permutations are:

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

    So the only data I need to track is the original list ({1, 2, 3}), the current song index (e.g. 1) and the index of the permutation (e.g. 3). Then, if I want to find the next song to play, I know it's third (2, but zero-based) song of permutation 3, e.g. Song 1.

    However, this method relies on you having an efficient means of determining the ith song of the jth permutation, which until I've had chance to think (or someone with a stronger mathematical background than I can interject) is equivalent to "then a miracle happens". But the principle is there.

    0 讨论(0)
  • 2020-12-09 05:37

    As many others have said you should implement THEN optimize, and only optimize the parts that need it (which you check on with a profiler). I offer a (hopefully) elegant method of getting the list you need, which doesn't really care so much about performance:

    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);
                }
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-09 05:39

    I think you should stick to your current solution (the one in your edit).

    To do a re-order with no repetitions & not making your code behave unreliable, you have to track what you have already used / like by keeping unused indexes or indirectly by swapping from the original list.

    I suggest to check it in the context of the working application i.e. if its of any significance vs. the memory used by other pieces of the system.

    0 讨论(0)
  • 2020-12-09 05:40

    There are a number of methods of generating permutations without needing to store the state. See this question.

    0 讨论(0)
  • 2020-12-09 05:47

    If you use a maximal linear feedback shift register, you will use O(1) of memory and roughly O(1) time. See here for a handy C implementation (two lines! woo-hoo!) and tables of feedback terms to use.

    And here is a solution:

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

    Now, I selected the feedback terms (badly) at random from the link above. You could also implement a version that had multiple maximal terms and you select one of those at random, but you know what? This is pretty dang good for what you want.

    Here is test code:

        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.");
            }
        }
    

    Be aware that maximal LFSR's never generate 0. I've hacked around this by returning the i term - 1. This works well enough. Also, since you want to guarantee uniqueness, I ignore anything out of range - the LFSR only generates sequences up to powers of two, so in high ranges, it will generate wost case 2x-1 too many values. These will get skipped - that will still be faster than FYK.

    0 讨论(0)
  • 2020-12-09 05:49

    If memory was really a concern after a certain number of records and it's safe to say that if that memory boundary is reached, there's enough items in the list to not matter if there are some repeats, just as long as the same song was not repeated twice, I would use a combination method.

    Case 1: If count < max memory constraint, generate the playlist ahead of time and use Knuth shuffle (see Jon Skeet's implementation, mentioned in other answers).

    Case 2: If count >= max memory constraint, the song to be played will be determined at run time (I'd do it as soon as the song starts playing so the next song is already generated by the time the current song ends). Save the last [max memory constraint, or some token value] number of songs played, generate a random number (R) between 1 and song count, and if R = one of X last songs played, generate a new R until it is not in the list. Play that song.

    Your max memory constraints will always be upheld, although performance can suffer in case 2 if you've played a lot of songs/get repeat random numbers frequently by chance.

    0 讨论(0)
提交回复
热议问题