Split a list into multiple lists at increasing sequence broken

前端 未结 9 1070
不知归路
不知归路 2021-02-18 19:11

I\'ve a List of int and I want to create multiple List after splitting the original list when a lower or same number is found. Numbers are not in sorted order.<

相关标签:
9条回答
  • 2021-02-18 19:49

    For things like this, I'm generally not a fan of solutions that use GroupBy or other methods that materialize the results. The reason is that you never know how long the input sequence will be, and materializations of these sub-sequences can be very costly.

    I prefer to stream the results as they are pulled. This allows implementations of IEnumerable<T> that stream results to continue streaming through your transformation of that stream.

    Note, this solution won't work if you break out of iterating through the sub-sequence and want to continue to the next sequence; if this is an issue, then one of the solutions that materialize the sub-sequences would probably be better.

    However, for forward-only iterations of the entire sequence (which is the most typical use case), this will work just fine.

    First, let's set up some helpers for our test classes:

    private static IEnumerable<T> CreateEnumerable<T>(IEnumerable<T> enumerable)
    {
        // Validate parameters.
        if (enumerable == null) throw new ArgumentNullException("enumerable");
    
        // Cycle through and yield.
        foreach (T t in enumerable)
            yield return t;
    }
    
    private static void EnumerateAndPrintResults<T>(IEnumerable<T> data,
        [CallerMemberName] string name = "") where T : IComparable<T>
    {
        // Write the name.
        Debug.WriteLine("Case: " + name);
    
        // Cycle through the chunks.
        foreach (IEnumerable<T> chunk in data.
            ChunkWhenNextSequenceElementIsNotGreater())
        {
            // Print opening brackets.
            Debug.Write("{ ");
    
            // Is this the first iteration?
            bool firstIteration = true;
    
            // Print the items.
            foreach (T t in chunk)
            {
                // If not the first iteration, write a comma.
                if (!firstIteration)
                {
                    // Write the comma.
                    Debug.Write(", ");
                }
    
                // Write the item.
                Debug.Write(t);
    
                // Flip the flag.
                firstIteration = false;
            }
    
            // Write the closing bracket.
            Debug.WriteLine(" }");
        }
    }
    

    CreateEnumerable is used for creating a streaming implementation, and EnumerateAndPrintResults will take the sequence, call ChunkWhenNextSequenceElementIsNotGreater (this is coming up and does the work) and output the results.

    Here's the implementation. Note, I've chosen to implement them as extension methods on IEnumerable<T>; this is the first benefit, as it doesn't require a materialized sequence (technically, none of the other solutions do either, but it's better to explicitly state it like this).

    First, the entry points:

    public static IEnumerable<IEnumerable<T>>
        ChunkWhenNextSequenceElementIsNotGreater<T>(
            this IEnumerable<T> source)
        where T : IComparable<T>
    {
        // Validate parameters.
        if (source == null) throw new ArgumentNullException("source");
    
        // Call the overload.
        return source.
            ChunkWhenNextSequenceElementIsNotGreater(
                Comparer<T>.Default.Compare);
    }
    
    public static IEnumerable<IEnumerable<T>>
        ChunkWhenNextSequenceElementIsNotGreater<T>(
            this IEnumerable<T> source,
                Comparison<T> comparer)
    {
        // Validate parameters.
        if (source == null) throw new ArgumentNullException("source");
        if (comparer == null) throw new ArgumentNullException("comparer");
    
        // Call the implementation.
        return source.
            ChunkWhenNextSequenceElementIsNotGreaterImplementation(
                comparer);
    }
    

    Note that this works on anything that implements IComparable<T> or where you provide a Comparison<T> delegate; this allows for any type and any kind of rules you want for performing the comparison.

    Here's the implementation:

    private static IEnumerable<IEnumerable<T>>
        ChunkWhenNextSequenceElementIsNotGreaterImplementation<T>(
            this IEnumerable<T> source, Comparison<T> comparer)
    {
        // Validate parameters.
        Debug.Assert(source != null);
        Debug.Assert(comparer != null);
    
        // Get the enumerator.
        using (IEnumerator<T> enumerator = source.GetEnumerator())
        {
            // Move to the first element.  If one can't, then get out.
            if (!enumerator.MoveNext()) yield break;
    
            // While true.
            while (true)
            {
                // The new enumerator.
                var chunkEnumerator = new 
                    ChunkWhenNextSequenceElementIsNotGreaterEnumerable<T>(
                        enumerator, comparer);
    
                // Yield.
                yield return chunkEnumerator;
    
                // If the last move next returned false, then get out.
                if (!chunkEnumerator.LastMoveNext) yield break;
            }
        }
    }
    

    Of note: this uses another class ChunkWhenNextSequenceElementIsNotGreaterEnumerable<T> to handle enumerating the sub-sequences. This class will iterate each of the items from the IEnumerator<T> that is obtained from the original IEnumerable<T>.GetEnumerator() call, but store the results of the last call to IEnumerator<T>.MoveNext().

    This sub-sequence generator is stored, and the value of the last call to MoveNext is checked to see if the end of the sequence has or hasn't been hit. If it has, then it simply breaks, otherwise, it moves to the next chunk.

    Here's the implementation of ChunkWhenNextSequenceElementIsNotGreaterEnumerable<T>:

    internal class 
        ChunkWhenNextSequenceElementIsNotGreaterEnumerable<T> : 
            IEnumerable<T>
    {
        #region Constructor.
    
        internal ChunkWhenNextSequenceElementIsNotGreaterEnumerable(
            IEnumerator<T> enumerator, Comparison<T> comparer)
        {
            // Validate parameters.
            if (enumerator == null) 
                throw new ArgumentNullException("enumerator");
            if (comparer == null) 
                throw new ArgumentNullException("comparer");
    
            // Assign values.
            _enumerator = enumerator;
            _comparer = comparer;
        }
    
        #endregion
    
        #region Instance state.
    
        private readonly IEnumerator<T> _enumerator;
    
        private readonly Comparison<T> _comparer;
    
        internal bool LastMoveNext { get; private set; }
    
        #endregion
    
        #region IEnumerable implementation.
    
        public IEnumerator<T> GetEnumerator()
        {
            // The assumption is that a call to MoveNext 
            // that returned true has already
            // occured.  Store as the previous value.
            T previous = _enumerator.Current;
    
            // Yield it.
            yield return previous;
    
            // While can move to the next item, and the previous 
            // item is less than or equal to the current item.
            while ((LastMoveNext = _enumerator.MoveNext()) && 
                _comparer(previous, _enumerator.Current) < 0)
            {
                // Yield.
                yield return _enumerator.Current;
    
                // Store the previous.
                previous = _enumerator.Current;
            }
        }
    
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    
        #endregion
    }
    

    Here's the test for the original condition in the question, along with the output:

    [TestMethod]
    public void TestStackOverflowCondition()
    {
        var data = new List<int> { 
            1, 2, 1, 2, 3, 3, 1, 2, 3, 4, 1, 2, 3, 4, 5, 6 
        };
        EnumerateAndPrintResults(data);
    }
    

    Output:

    Case: TestStackOverflowCondition
    { 1, 2 }
    { 1, 2, 3 }
    { 3 }
    { 1, 2, 3, 4 }
    { 1, 2, 3, 4, 5, 6 }
    

    Here's the same input, but streamed as an enumerable:

    [TestMethod]
    public void TestStackOverflowConditionEnumerable()
    {
        var data = new List<int> { 
            1, 2, 1, 2, 3, 3, 1, 2, 3, 4, 1, 2, 3, 4, 5, 6 
        };
        EnumerateAndPrintResults(CreateEnumerable(data));
    }
    

    Output:

    Case: TestStackOverflowConditionEnumerable
    { 1, 2 }
    { 1, 2, 3 }
    { 3 }
    { 1, 2, 3, 4 }
    { 1, 2, 3, 4, 5, 6 }
    

    Here's a test with non-sequential elements:

    [TestMethod]
    public void TestNonSequentialElements()
    {
        var data = new List<int> { 
            1, 3, 5, 7, 6, 8, 10, 2, 5, 8, 11, 11, 13 
        };
        EnumerateAndPrintResults(data);
    }
    

    Output:

    Case: TestNonSequentialElements
    { 1, 3, 5, 7 }
    { 6, 8, 10 }
    { 2, 5, 8, 11 }
    { 11, 13 }
    

    Finally, here's a test with characters instead of numbers:

    [TestMethod]
    public void TestNonSequentialCharacters()
    {
        var data = new List<char> { 
            '1', '3', '5', '7', '6', '8', 'a', '2', '5', '8', 'b', 'c', 'a' 
        };
        EnumerateAndPrintResults(data);
    }
    

    Output:

    Case: TestNonSequentialCharacters
    { 1, 3, 5, 7 }
    { 6, 8, a }
    { 2, 5, 8, b, c }
    { a }
    
    0 讨论(0)
  • 2021-02-18 19:49

    I use a dictionary to get 5 different list as below;

    static void Main(string[] args)
        {
            List<int> data = new List<int> { 1, 2, 1, 2, 3, 3, 1, 2, 3, 4, 1, 2, 3, 4, 5, 6 };
            Dictionary<int, List<int>> listDict = new Dictionary<int, List<int>>();
    
            int listCnt = 1;
            //as initial value get first value from list
            listDict.Add(listCnt, new List<int>());
            listDict[listCnt].Add(data[0]);
    
    
            for (int i = 1; i < data.Count; i++)
            {
                 if (data[i] > listDict[listCnt].Last())
                {
                    listDict[listCnt].Add(data[i]);
                }
                 else
                {
                    //increase list count and add a new list to dictionary
                    listCnt++;
                    listDict.Add(listCnt, new List<int>());
                    listDict[listCnt].Add(data[i]);
                }
            }
    
            //to use new lists
            foreach (var dic in listDict)
            {
                Console.WriteLine( $"List {dic.Key} : " + string.Join(",", dic.Value.Select(x => x.ToString()).ToArray()));
    
            }
    
        }
    

    Output :

    List 1 : 1,2
    List 2 : 1,2,3
    List 3 : 3
    List 4 : 1,2,3,4
    List 5 : 1,2,3,4,5,6
    
    0 讨论(0)
  • 2021-02-18 19:50

    I'd just go for something simple:

    public static IEnumerable<List<int>> SplitWhenNotIncreasing(List<int> numbers)
    {
        for (int i = 1, start = 0; i <= numbers.Count; ++i)
        {
            if (i != numbers.Count && numbers[i] > numbers[i - 1])
                continue;
    
            yield return numbers.GetRange(start, i - start);
            start = i;
        }
    }
    

    Which you'd use like so:

    List<int> data = new List<int> { 1, 2, 1, 2, 3, 3, 1, 2, 3, 4, 1, 2, 3, 4, 5, 6 };
    
    foreach (var subset in SplitWhenNotIncreasing(data))
        Console.WriteLine(string.Join(", ", subset));
    

    If you really did need to work with IEnumerable<T>, then the simplest way I can think of is like this:

    public sealed class IncreasingSubsetFinder<T> where T: IComparable<T>
    {
        public static IEnumerable<IEnumerable<T>> Find(IEnumerable<T> numbers)
        {
            return new IncreasingSubsetFinder<T>().find(numbers.GetEnumerator());
        }
    
        IEnumerable<IEnumerable<T>> find(IEnumerator<T> iter)
        {
            if (!iter.MoveNext())
                yield break;
    
            while (!done)
                yield return increasingSubset(iter);
        }
    
        IEnumerable<T> increasingSubset(IEnumerator<T> iter)
        {
            while (!done)
            {
                T prev = iter.Current; 
                yield return prev;
    
                if ((done = !iter.MoveNext()) || iter.Current.CompareTo(prev) <= 0)
                    yield break;
            }
        }
    
        bool done;
    }
    

    Which you would call like this:

    List<int> data = new List<int> { 1, 2, 1, 2, 3, 3, 1, 2, 3, 4, 1, 2, 3, 4, 5, 6 };
    
    foreach (var subset in IncreasingSubsetFinder<int>.Find(data))
        Console.WriteLine(string.Join(", ", subset));
    
    0 讨论(0)
  • 2021-02-18 19:58

    You can use the index to get the previous item and calculate the group id out of comparing the values. Then group on the group ids and get the values out:

    List<int> data = new List<int> { 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 2, 3, 4, 5, 6 };
    
    int groupId = 0;
    var groups = data.Select
                     ( (item, index)
                       => new
                          { Item = item
                          , Group = index > 0 && item <= data[index - 1] ? ++groupId : groupId
                          }
                     );
    
    List<List<int>> list = groups.GroupBy(g => g.Group)
                                 .Select(x => x.Select(y => y.Item).ToList())
                                 .ToList();
    
    0 讨论(0)
  • 2021-02-18 19:58

    I really like Matthew Watson's solution. If however you do not want to rely on List<T>, here is my simple generic approach enumerating the enumerable once at most and still retaining the capability for lazy evaluation.

    public static IEnumerable<IEnumerable<T>> AscendingSubsets<T>(this IEnumerable<T> superset) where T :IComparable<T>
    {     
        var supersetEnumerator = superset.GetEnumerator();
    
        if (!supersetEnumerator.MoveNext())
        {
            yield break;
        }    
    
        T oldItem = supersetEnumerator.Current;
        List<T> subset = new List<T>() { oldItem };
    
        while (supersetEnumerator.MoveNext())
        {
            T currentItem = supersetEnumerator.Current;
    
            if (currentItem.CompareTo(oldItem) > 0)
            {
                subset.Add(currentItem);
            }
            else
            {
                yield return subset;
                subset = new List<T>() { currentItem };
            }
    
            oldItem = supersetEnumerator.Current;
        }
    
        yield return subset;
    }
    

    Edit: Simplified the solution further to only use one enumerator.

    0 讨论(0)
  • 2021-02-18 19:58

    You can do it with Linq using the index to calculate the group:

    var result = data.Select((n, i) => new { N = n, G = (i > 0 && n > data[i - 1] ? data[i - 1] + 1 : n) - i })
                     .GroupBy(a => a.G)
                     .Select(g => g.Select(n => n.N).ToArray())
                     .ToArray();
    
    0 讨论(0)
提交回复
热议问题