How to rank elements in c# especially when it has duplicates - c#

I have a requirement to rank the array elements and the array has duplicate values. I tried following this Ranking items in a list with LINQ but this doesn't work when the array has duplicate values in it. Any easy way to do it in c#?
For Example :
input = [650,150,150,200]
output = [1,3,3,2]
For Example :
input = [650,200,200,150]
output = [1,2,2,3]
Update: The requirement is as below, what if I add one more element to the array
Ex: [650,150,150,200,100] output needs to be [1,3,3,2,5] instead of [1,3,3,2,4]

You can create a dictionary as rank-lookup source:
int[] array = new[] {650,150,150,200};
Dictionary<int, int> numRanks = array
.GroupBy(i => i)
.OrderByDescending(g => g.Key)
.Select((g, index) => (num:g.Key, rank:index+1))
.ToDictionary(x => x.num, x => x.rank);
int[] result = array.Select(i => numRanks[i]).ToArray();
For your updated requirement you could use a similar approach using a Lookup<TKey, TValue>:
var rankLookup = array
.OrderByDescending(i => i)
.Select((num, index) => (num, index))
.ToLookup(x => x.num, x => x.index + 1);
int[] result = array.Select(i => rankLookup[i].First()).ToArray();
The lookup is like a dictionary that allows duplicate keys. You need to use First here because you are just interested in the rank. If you'd use Count() you'd know how many duplicates it had.

You could create an array of items, distinct and in order, then use the indices to determine the rank of each item.
var ranks = input.Distinct().OrderByDescending(x => x).ToArray();
var ranked = input.Select(x => Array.IndexOf(ranks, x) + 1);
Working example
Update after comment
If rankings need to be skipped, just remove the Distinct:
var ranks = input.OrderByDescending(x => x).ToArray();
var ranked = input.Select(x => Array.IndexOf(ranks, x) + 1);
Array.IndexOf will take the first element when there are duplicates.
Working example

Related

Is it possible to implement a "Smaller Numbers than Current" using a single LINQ query?

Was doing this problem https://leetcode.com/problems/how-many-numbers-are-smaller-than-the-current-number/submissions/
Input: nums = [8,1,2,2,3]
Output: [4,0,1,1,3]
Explanation: For
nums[0]=8 there exist four smaller numbers than it (1, 2, 2 and 3).
For nums[1]=1 does not exist any smaller number than it. For nums[2]=2
there exist one smaller number than it (1). For nums[3]=2 there exist
one smaller number than it (1). For nums[4]=3 there exist three
smaller numbers than it (1, 2 and 2).
as LINQ-y as possible and came up with a solution which is only half LINQ :(
public class Solution {
public int[] SmallerNumbersThanCurrent(int[] nums) {
var groups = nums
.Select((val, index) => new { index, val })
.GroupBy(x => x.val)
.OrderBy(g => g.Key)
.Select(g => g.Select(x => x.index).ToArray());
var arr = new int[nums.Length];
int numSmaller = 0;
foreach (var indices in groups)
{
foreach (var index in indices)
{
arr[index] = numSmaller;
}
numSmaller += indices.Length;
}
return arr;
}
}
Is anyone here clever enough to help me figure out a way to LINQ-ify the second half of the solution? Preferably O(n log n) as code I have.
I hope I understood your question. You could do the following.
public int[] SmallerNumbersThanCurrent(int[] nums)
{
return nums.Select(x=> nums.Count(c=> c<x)).ToArray();
}
Though I don't think using one LINQ is a good idea here, it is possible to get rid of the foreach you have like this, assuming approximate nlog(n) is required:
nums.Select((num, index) => new { num, index })
// order number
.OrderBy(x => x.num)
// select number with their original index in nums and
// their order in the ordered collection
.Select((x, order) => new { x.num, x.index, order })
// Group the result by number
.GroupBy(x => x.num)
// Consolidate order in the ordered collection by selecting the minimum
// possible order
.Select(g => new
{
numWithOrder = g.Select(_ => new
{
num = _,
minOrder = g.First().order
})
})
// Flatten the collection
.SelectMany(g => g.numWithOrder)
// There should be minOrder number of results in the original collection
// are smaller than the number
.Select(x => new { x.num.index, result = x.minOrder })
// Restore as per original index
.OrderBy(x => x.index)
// Select final result
.Select(x => x.result)
As you might have seen, LINQ kills the readability of the code.
Here is another solution. It uses the Scan extension method from the System.Interactive package, for counting by accumulation the numbers that are smaller than the numbers of the current group.
public int[] SmallerNumbersThanCurrent(int[] nums)
{
return nums
.Select((x, i) => (Item: x, Index: i))
.GroupBy(x => x.Item, x => x.Index)
.OrderBy(g => g.Key)
.Scan(seed: (Indices: Enumerable.Empty<int>(), Counter: 0),
accumulator: (acc, x) => (x, acc.Counter + acc.Indices.Count()))
.SelectMany(acc => acc.Indices,
(acc, element) => (Index: element, CountOfSmallerNumbers: acc.Counter))
.OrderBy(x => x.Index)
.Select(x => x.CountOfSmallerNumbers)
.ToArray();
}
This solution is arguably even more obscure and unreadable than weichch's solution. 😃
The signature of the Scan extension method:
public static IEnumerable<TAccumulate> Scan<TSource, TAccumulate>(
this IEnumerable<TSource> source, TAccumulate seed,
Func<TAccumulate, TSource, TAccumulate> accumulator);
Generates a sequence of accumulated values by scanning the source sequence and applying an accumulator function.
public int[] SmallerNumbersThanCurrentShorter(int[] nums)
{
return (from x in nums select (from y in nums where y < x select y).Count()).ToArray();
}
just do this!!

Sort list and return original index

I need to sort a list "angle" containing doubles and as a result i want to return the original index.
I tried this with Linq but the index doesn't match the sort
var sorted = angle
.Select((x, i) => new KeyValuePair<double, int>(x, i))
.OrderBy(x => x.Key)
.ToList();
List<int> idx = sorted.Select(x => x.Value).ToList();
The results what I want from idx:
[2,4,5,1,3,0,7,6]
but I get: [4,3,0,6,7,1,5,2]
left the unsorted angles and right the wrong sort of index values as result

Get elements of an IEnumerator to a list

I'm new to c# so go easy on me. Anyways, I made a list of numbers
List<int> numbers = new List<int>();
and I want to make a list of each number and its count/frequency.
var grouped = numbers
.GroupBy(i => i)
.Select(i => new { Number = i.Key, Count = i.Count() });
In locals, I can see the group, which has an IEnumerator interface with all of the numbers and their count values image of what I'm talking about. So is there a way to make a list with the numbers and their frequency/count?
Thank you.
IEnumerable<T> is a sequence so it doesn't own a count. But Enumerable.Count is an extension method of IEnumerable<T>
That is, you don't necessarily need to convert an IEnumerable<T> into a List<T>:
var grouped = numbers
.GroupBy(i => i)
.Select(i => new { Number = i.Key, Count = i.Count() });
var groupedCount = grouped.Count();
// You may iterate grouped
foreach(var value in grouped)
{
Console.WriteLine($"{value.Number} {value.Count}");
}
If you really need List<T> semantics, you just need to call Enumerable.ToList:
var grouped = numbers
.GroupBy(i => i)
.Select(i => new { Number = i.Key, Count = i.Count() })
.ToList();
In the other hand, you may directly convert everything into a string as follows:
var groupText = string.Join("\n", numbers
.GroupBy(i => i)
.Select(i => $"Number: {i.Key} Count: {i.Count()}"))
To get a list, you just need to call ToList(), for example:
var grouped = numbers
.GroupBy(i => i)
.Select(i => new { Number = i.Key, Count = i.Count() })
.ToList();
However, you really don't need to do that, you can simply loop over the enumerable as it stands:
foreach(var item in grouped)
{
Console.WriteLine($"{item.Number} occurs {item.Count} times");
}
Sounds like you want ToDictionary with the number as key and the frequency as value:
var grouped = numbers
.GroupBy(i => i)
.Select(i => new { Number = i.Key, Count = i.Count() })
.ToDictionary(x => x.Number, x => x.Count);
Now you can easily print every number and its frequency by looping the dictionary.
In fact you don´t even need neither ToDictionary nor your Select, as the IGrouping returned from GroupBy also derives from IEnumerable which is why you can iterate over it.
foreach(var g in grouped = numbers.GroupBy(i => i))
{
var number = g.Key;
var freq = g.Count();
}

how to count same elements in different arrays in C#

I've a list that contains 4 sized arrays:
These arrays have 4 elements. I want to use another list that contains these arrays' first element's count. In addition, if their first elements are same, they should be summation. For example:
list[0] = {1,2,3,4}
list[1] = {1,1,5,3}
list[2] = {1,2,5,8}
list[3] = {2,2,3,3}
list[4] = {3,5,5,6}
list[5] = {4,4,4,4}
list[6] = {4,5,5,6}
So, anotherList should be:
anotherList = {3, 1, 1, 2}
How can I do this?
EDIT: Expected result is:
anotherList = list.Select(a => a[0]) // project each array to its first item
.GroupBy(x => x) // group first items by their value
.Select(g => g.Count()) // select count of same items
.ToList();
Output:
[ 3, 1, 1, 2 ]
NOTE: GroupBy internally uses Lookup which returns groups in same order as the are added, so it seems to be what you want.
UPDATE: Approach which does not depend on internal implementation of GroupBy
anotherList = list.Select((a,i) => new { Item = a[0], Index = i })
.GroupBy(x => x.Item)
.OrderBy(g => g.Min(x => x.Index))
.Select(g => g.Count())
.ToList();

How to select array index after Where clause using Linq?

Suppose I have the array string[] weekDays = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" }; , and I want to find out the index of array elements containing 's'. How can I do this using Linq ?
I've tried int[] indexOfDaysContainingS = weekDays.Where(day => day.Contains("s")).Select((day, index) => index).ToArray();, but this returns 0,1,2 as presumably it's getting the index of the filtered IEnumberable<string> after the Where() clause instead. If I put the Select() first, then all I have is the index and can't filter by the days.
What do I need to change to make it work and return 1,2,3 instead ?
You could do it this way:
weekDays.Select((day, index) => new { Day = day, Index = index })
.Where(x => x.Day.Contains("s"))
.Select(x => x.Index)
.ToArray();
Not sure if this is optimal..
Patko's answer is the way to go in the general case.
Here are 2 more options:
// Idea only works with collections that can be accessed quickly by index.
int[] indices = Enumerable.Range(0, weekDays.Length)
.Where(index => weekDays[index].Contains("s"))
.ToArray();
With MoreLinq:
// Similar to Patko's idea, except using a 'named' type.
int[] indices = weekDays.AsSmartEnumerable()
.Where(item => item.Value.Contains("s"))
.Select(item => item.Index)
.ToArray();
This should work:
weekDays.Where(a => a.Contains("s")).Select((a, i) => i).ToArray();

Categories

Resources