4

I have a script that takes an int[] array, converts it to a list and removes all further occurrences of the integers that already occurred at least 2 times. The problem I have is that when it gets into the loop where I am checking the count of each integers occurrences, I am getting stuck in a loop.

EDIT: "What I left out was that the list has to remain in its original order so that excess numbers are removed from top down. Sorry if that confused those who already answered!

I thought that the changed number of the occursintegerOccurrence would act as a change of count for the while loop.

Any ideas on what I'm missing here? Aside from any discernible skill.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Messaging;

public class Kata
{
    public static void Main()
    {
       int[] arr = new int[] {1, 2, 1, 4, 5, 1, 2, 2, 2};
        int occurrenceLimit = 2;
       var intList = arr.ToList();

        for (int i = 0; i < intList.Count; i++)
        {
            var occursintegerOccurrence = intList.Count(n => n == occurrenceLimit);

            do
            {
                occursintegerOccurrence = intList.Count(n => n == occurrenceLimit);
                foreach (var x in intList)
                {
                    Console.WriteLine(x);
                    intList.Remove(intList.LastIndexOf(occurrenceLimit));
                    // Tried changing the count here too
                    occursintegerOccurrence = intList.Count(n => n == occurrenceLimit);
                }
            } while (occursintegerOccurrence > occurrenceLimit);
        }
    }
}
StuartLC
  • 104,537
  • 17
  • 209
  • 285
3therk1ll
  • 2,056
  • 4
  • 35
  • 64
  • 5
    You can't modify intList when you're enumerating it with a foreach loop. .NET doesn't like that. You may need to create a copy of your intList and remove the items from that instead. – Jon Jul 14 '16 at 17:46
  • 2
    Have you stepped through the code in the debugger? That's a very quick way to find a lot of problems. – Tim Jul 14 '16 at 17:46
  • System.InvalidOperationException: Collection was modified; enumeration operation may not execute, look https://dotnetfiddle.net/ENMyqS – David Pine Jul 14 '16 at 17:48
  • 3
    As a side note, you could do that whole thing with a single LINQ statement: `var ints = arr.GroupBy(i => i).Where(g => g.Count() < 3).SelectMany(g => g).ToList();` Unless order is important. – itsme86 Jul 14 '16 at 17:48
  • I think he wants to keep at most 2 of each, in which case: `var ints = arr.GroupBy(i => i) .Select(grp => grp.Take(2)) .SelectMany(g => g) .ToList();` – StuartLC Jul 14 '16 at 17:51
  • @StuartLC That doesn't "remove all instances of integers that have more than two occurences."... It just takes the first 2 instances of each integer. If that's what the OP wants, he phrased it very poorly. – itsme86 Jul 14 '16 at 17:52
  • @StuartLC That's what I'm saying. You're not removing *all* instances of integers with more than two occurences, as stated by the OP. But he might have phrased his requirements incorrectly. – itsme86 Jul 14 '16 at 17:55
  • Ok, I get your point - all instances to be removed, not just those in excess. – StuartLC Jul 14 '16 at 17:56
  • No it is those in excess. So if there are three 2's, the last one is removed, but the remainder of the list is still in order. Sorry for the confusion on that one – 3therk1ll Jul 14 '16 at 17:58
  • Note that you want to use [`RemoveAt()`](https://msdn.microsoft.com/en-us/library/5cw9x18z(v=vs.110).aspx) to remove something at a specific index. Your code is not throwing an exception like it should because `Remove()` removes by finding *the item itself* in the list (if there are duplicates it removes the first one it finds then returns), so you code currently says "Remove the number 8 from the list", because the last 2 is at index 8, but number 8 doesn't exist so `Remove()` simply returns and your code continues running (infinite loop). – Quantic Jul 14 '16 at 18:07
  • In other words, the output should contain no more than 2 instances of the same integer? – Ivan Stoev Jul 14 '16 at 18:08
  • @IvanStoev Yes that's right – 3therk1ll Jul 14 '16 at 18:13

8 Answers8

5

Here's a fairly concise version, assuming that you want to remove all instances of integers with a count in excess of 2, leaving the remainder of the bag in its original sequence, with preference to retention traversing from left to right:

int[] arr = new int[] {1, 2, 1, 4, 5, 1, 2, 2, 2};
var ints = arr.Select((n, idx) => new {n, idx})
               .GroupBy(x => x.n)
               .SelectMany(grp => grp.Take(2))
               .OrderBy(x => x.idx)
               .Select(x => x.n)
               .ToList();

Result:

1, 2, 1, 4, 5, 2

It works by using the index overload of Select to project an anonymous Tuple and carrying through the original order to allow re-ordering at the end.

StuartLC
  • 104,537
  • 17
  • 209
  • 285
  • This works, and inadvertently solves the problem. But does not answer OP's question which is "Why is my program stuck in an infinite loop?", not "How can I remove duplicate items from a list but keeping it in order and leaving a certain number of duplicates behind?". They are bound to make exactly the same mistake later if they simply copy and paste this answer over their current one. – Quantic Jul 14 '16 at 19:42
  • 1
    Comments by Mangist et al identified the perils of mutating collections whilst iterating same, and the collective wisdom given was to approach the problem from a different angle, as is the case with all the answers provided to the OP. – StuartLC Jul 14 '16 at 20:22
5

The cause of the endless loop is the line

 intList.Remove(intList.LastIndexOf(occurrenceLimit));

..you are removing the value equals to the last occurence in the list of the occurrenceLimit value(=2), that it is "8" (the last index of the array counting from 0).

Since "8" it isn't present in the list, you don't remove anything and the loop permanence test doesn't ever change and so it is always verified and the loop never ends..

This method works for any values of occurrenceLimit but I think that the solution of StuartLC is better..

int[] arr = new int[] { 1, 2, 1, 4, 5, 1, 2, 2, 2 };
int?[] arr2 = new int?[arr.Length];
arr2.ToList().ForEach(i => i = null);
int occurrenceLimit = 2;

var ints = arr.GroupBy(x => x).Select(x => x.Key).ToList();

ints.ForEach(i => {
   int ndx = 0;
   for (int occ = 0; occ < occurrenceLimit; occ++){
        ndx = arr.ToList().IndexOf(i, ndx);
        if (ndx < 0) break;
        arr2[ndx++] = i;
   }
});

List<int?> intConverted = arr2.ToList();
intConverted.RemoveAll(i => i.Equals(null));
Ciro Corvino
  • 2,038
  • 5
  • 20
  • 33
1

this may help you

     namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            int[] arr = new int[] { 1, 2, 1, 4, 5, 1, 2, 2, 2 };
            int occurrenceLimit = 2;
            var newList = new List<Vm>();


            var result=new List<Vm>();

            for (int i = 0; i < arr.Length; i++)
            {
                var a = new Vm {Value = arr[i], Index = i};
                result.Add(a);
            }

            foreach (var item in result.GroupBy(x => x.Value))
            {
                newList.AddRange(item.Select(x => x).Take(occurrenceLimit));
            }

            Console.WriteLine(string.Join(",",newList.OrderBy(x=>x.Index).Select(a=>a.Value)));

            Console.ReadKey();
        }
    }

    public class Vm
    {
        public int Value { get; set; }
        public int Index { get; set; }
    }
}

I did the following:

  1. I created a Vm class with 2 props (Value and Index), in order to save the index of each value in the array.

  2. I goup by value and take 2 ccurence of each values.

  3. I order the result list base on the initial index.

Lucian Bumb
  • 2,821
  • 5
  • 26
  • 39
1

It can be done by defining your own enumerator method, which will count already happened occurrences:

using System;
using System.Collections.Generic;
using System.Linq;
static class Test {
    static IEnumerable<int> KeepNoMoreThen(this IEnumerable<int> source, int limit) {
        Dictionary<int, int> counts = new Dictionary<int, int>();
        foreach(int current in source) {
            int count;
            counts.TryGetValue(current, out count);
            if(count<limit) {
                counts[current]=count+1;
                yield return current;
            }
        }
    }
    static void Main() {
        int[] arr = new int[] { 1, 2, 1, 4, 5, 1, 2, 2, 2 };
        int occurrenceLimit = 2;
        List<int> result = arr.KeepNoMoreThen(occurrenceLimit).ToList();
        result.ForEach(Console.WriteLine);
    }
}
user4003407
  • 21,204
  • 4
  • 50
  • 60
  • If the range of items in the original array is known and limited (which I'd guess is *very likely*), you could replace the Dictionary with a simple array, which would simplify much of the code. – James Curran Jul 14 '16 at 18:31
1
var removal = arr.GroupBy (a =>a ).Where (a =>a.Count()>2).Select(a=>a.Key).ToArray();

var output = arr.Where (a =>!removal.Contains(a)).ToList();

removal is an array of the items which appear more than twice.

output is the original list with those items removed.

[Update -- Just discovered that this handles the problem as originally specified, not as later clarified)

James Curran
  • 101,701
  • 37
  • 181
  • 258
1

A single pass over the input array maintaining occurrence count dictionary should do the job in O(N) time:

int[] arr = new int[] { 1, 2, 1, 4, 5, 1, 2, 2, 2 };
int occurrenceLimit = 2;
var counts = new Dictionary<int, int>();
var resilt = arr.Where(n =>
{
    int count;
    if (counts.TryGetValue(n, out count) && count >= occurrenceLimit) return false;
    counts[n] = ++count;
    return true;
}).ToList();
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
1

Your code is stuck in an infinite loop because you are using List.Remove(), and the Remove() method removes an item by matching against the item you pass in. But you are passing in a list index instead of a list item, so you are getting unintended results. What you want to use is List.RemoveAt(), which removes an item by matching against the index.

So your code is stuck in an infinite loop because intList.LastIndexOf(occurrenceLimit) is returning 8, then Remove() looks for the item 8 in the list, but it doesn't find it so it returns false and your code continues to run. Changing this line:

intList.Remove(intList.LastIndexOf(occurrenceLimit));

to

intList.RemoveAt(intList.LastIndexOf(occurrenceLimit));

will "fix" your code and it will no longer get stuck in an infinite loop. It would then have the expected behavior of throwing an exception because you are modifying a collection that you are iterating through in a foreach.

As for your intended solution, I have rewritten your code with some changes, but keeping most of your code there instead of rewriting it entirely using LINQ or other magic. You had some issues:

1) You were counting the number of times occurenceLimit was found in the list, not the number of times an item was found in the list. I fixed this by comparing against intList[i].

2) You were using Remove() instead of RemoveAt().

3) Your foreach and do while need some work. I went with a while to simplify the initial case, and then used a for loop so I can modify the list (you cannot modify a list that you are iterating over in a foreach). In this for loop I iterate to the number of occurences - occurenceLimit to remove all but the first occurenceLimit number of them -- your initial logic was missing this and if your code worked as intended you would have removed every single one.

    static void Main(string[] args)
    {
        int[] arr = new int[] { 1, 2, 1, 4, 5, 1, 2, 2, 2 };
        int occurrenceLimit = 2;
        var intList = arr.ToList();
        // Interestingly, this `.Count` property updates during the for loop iteration,
        // so even though we are removing items inside this `for` loop, we do not run off the
        // end of the list as Count is constantly updated.
        // Doing `var count = intList.Count`, `for (... i < count ...)` would blow up.
        for (int i = 0; i < intList.Count; i++)
        {
            // Find the number of times the item at index `i` occurs
            int occursintegerOccurrence = intList.Count(n => n == intList[i]);

            // If `occursintegerOccurrence` is greater than `occurenceLimit`
            // then remove all but the first `occurrenceLimit` number of them
            while (occursintegerOccurrence > occurrenceLimit)
            {
                // We are not enumerating the list, so we can remove items at will.
                for (var ii = 0; ii < occursintegerOccurrence - occurrenceLimit; ii++)
                {
                    var index = intList.LastIndexOf(intList[i]);
                    intList.RemoveAt(index);
                }

                occursintegerOccurrence = intList.Count(n => n == intList[i]);
            }
        }

        // Verify the results
        foreach (var item in intList)
        {
            Console.Write(item + " ");
        }

        Console.WriteLine(Environment.NewLine + "Done");
        Console.ReadLine();
    }
Quantic
  • 1,779
  • 19
  • 30
0

Here's a pretty optimal solution:

var list = new List<int> { 1, 2, 1, 4, 5, 1, 2, 2, 2 };
var occurrenceLimit = 2;

list.Reverse(); // Reverse list to make sure we remove LAST elements

// We will store count of each element's occurence here
var counts = new Dictionary<int, int>();

for (int i = list.Count - 1; i >= 0; i--)
{
    var elem = list[i];
    if (counts.ContainsKey(elem)) // If we already faced this element we increment the number of it's occurencies
    {
        counts[elem]++;
        if (counts[elem] > occurrenceLimit) // If it occured more then 2 times we remove it from the list
            list.RemoveAt(i);
    }        
    else
        counts.Add(elem, 1); // We haven't faced this element yet so add it to the dictionary with occurence count of 1
}

list.Reverse(); // Again reverse list

The key feature with list is that you have to traverse it backwards to have a possibility to remove items. When you traverse it as usual it will throw you an exception that explains that the list cannot modified. But when you are going backwards you can remove elements as you wish as this won't affect your further operations.

Ivan Yurchenko
  • 3,762
  • 1
  • 21
  • 35