Shuffle List of strings, but not too much - c#

I have a list of strings in C# such as:
List<string> myList;
Lets say I populate it by adding 20 strings, starting from "1", up to "20".
myList.Add("1"); // And so on...
How can I, in the most efficient and elegant way, randomly shuffle this list of strings while restricting how far each item of the list can end up from it's original index to, say, 4 .
Example of what I want:
I want the order:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
For example, to be shuffled to the following:
2 5 4 3 6 1 8 7 10 9 12 11 15 14 13 16 18 15 20 19
One (straightforward) way of doing it would be to split the list into four parts and shuffle the parts separately. But I am still not sure how efficient this would be.
Efficiency in my case means not doing something that is overcomplicated or just stupid.

The following Linq will create a new index where the new index is limited to distance places away from original index
List<string> list = Enumerable.Range(1, 20).Select(i => i.ToString()).ToList();
Random rng = new Random();
int distance = 4;
List<string> newList = list
.Select((s, i) =>
new {OrigIndex = i, NewIndex = i + rng.Next(-distance, distance+1), Val = s})
.OrderBy(a => a.NewIndex).ThenBy(a=>a.OrigIndex)
.Select(a => a.Val)
.ToList();

You can use the following code:
List<int> myList = Enumerable.Range(1, 20).ToList(); // Feed the list from 1 to 20
int numberByGroups = 4;
List<int> result = new List<int>();
for (int skip = 0; skip < myList.Count; skip = skip + numberByGroups)
{
result.AddRange(myList.Skip(skip) // skip the already used numbers
.Take(numberByGroups) // create a group
.OrderBy(a => Guid.NewGuid()) // "Shuffle"
.ToList());
}
Console.WriteLine(String.Join(", ", result));
Which will shuffle by groups of numberByGroups

Related

Average every 20 numbers in an array

I have an a large array ( +400 numbers) decimal[] Raw where I need to average every 20 numbers, send those numbers to a new array, or list RawAvgList,
then form a new array, or list to get that average of the numbers in RawAvgList. Aka my code should find the average of the first 20 numbers and stores them in my new array/list, then next 20, then next 20. It should also for count if there are more or less then 20 number at the end of the large array
Should my while loop be in another loop that restarts the counting index??
Should I just be removing every 20 numbers as I go? I know just simply using the Average() on the decimal[] Raw is an option but the numbers needs to be more exact then that function can give. I have also tried using IndexRange but when the number isn't divisible by my count (20) it give and error, which will happen.
I have just been stumped for so long I am at my wits end and frustrated beyond belief, anything to help.
int unitof = 20;
decimal[] Raw = new decimal[] { Decimal.Parse(line.Substring(9).ToString(), style1) };
for (int i = 0; i < Raw.Length; i++)
{
while (count < Raw.Count())
{
RawAvgList.Add(// ** Average of every 20 numbers **//);
count += unitof; // 20 by 20 counter
}
// Reset counter or add another counter??
}
Edit (8/22/2022)
I added the IEnumerable<IEnumerable> Chunk as suggested, but I believe something else went wrong or I didn't fully understand how it worked because i have never used chunks.
I implemented the Chunk
public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> values, int chunkSize)
{
return values
.Select((v, i) => new { v, groupIndex = i / chunkSize })
.GroupBy(x => x.groupIndex)
.Select(g => g.Select(x => x.v));
}
added what you suggested
var rawAvgList = Raw.Chunk(20).Select(chunk => chunk.Average()).ToArray();
var result = rawAvgList.Average();
and then tried printing to the Console.Writeline()
Console.WriteLine($"{result} \t " + LengthRaw++);
Which got me and output of
36.41 0
37.94 1
38.35 2
37.63 3
36.41 4
36.41 5
36.21 6
36.82 7
37.43 8
37.43 9
37.43 10
37.43 11
37.43 12
37.94 13
37.94 14
37.84 15
37.43 16
37.84 17
37.43 18
37.84 19
37.84 20
When the output should be ( I am only using 21 numbers at the moment but it will be more then that later)
37.37 0
37.84 1
You can use Enumerable.Chunk() to split the data into batches of at most 20, and then average all those chunks:
decimal[] raw = new decimal[10000]; // Fill with your data.,
var rawAvgList = raw.Chunk(20).Select(chunk => chunk.Average()).ToArray();
var result = rawAvgList.Average();
However I don't know what you meant by
It should also for count if there are more or less then 20 number at
the end of the large array
The last block which will be averaged will be less than 20 long if the input is not a multiple of 20 items long, but all other blocks will be exactly 20 long.

set a sequence on a list of items

I need to set up a recursive function in C# to set the sequence number of a list of items. More specifically a bom. For each bom level, I need to start the sequence at 10, and increment of 10. How do I keep track of what level i'm at, and what counter to increment. This is driving me nuts.
Short example of data below, the real boms have thousands of lines and up to 12-15 levels.
Order
Level
Sequence
1
1
10
2
2
10
3
3
10
4
3
20
5
2
20
6
3
10
7
4
10
8
3
20
9
4
10
10
4
20
11
2
30
12
3
10
13
1
20
14
1
30
I indented the levels, to make the structure a bit more clear. And pasted the results of your answer to this. As you can see, the new levels are not sequenced properly.
I think i this case we can use the new language feature local function, and see if a recursive function is really necessary, as they are generally some of the hardest code to debug and maintain, only to be used sparingly, if at all this year, for any given year :)
[Fact]
public void SequencingRecursiveTest()
{
// BOM like byte order mark in utf 8 text encoding? odd problem :D
// Anyway we have a long list of values and want to emit the value with a sequence number, starting from 10 with increment 10
// Like in most cases when contemplating recursion, first off what about not using recursion to keep code maintainable and clean,
// As it turns out, we can:
//However super sneakily we have to reset all 'bom' sequence counts below the highest when an element in the sequence breaks the chain of same or greater
var keyValues = new Dictionary<int, int>();
var firstValue = 10;
var increment = 10;
int lastBom = 0;
int greatesBom = 0;
KeyValuePair<int, int> GetValueWithSequenceResetIfLowerThanLast(int bom)
{
bool reset = bom < lastBom;
greatesBom = bom > greatesBom ? bom : greatesBom;
if (reset)
{
foreach (int keyBom in keyValues.Keys)
{
if (keyBom < greatesBom)
keyValues[keyBom] = firstValue;
}
}
else if (keyValues.ContainsKey(bom))
{
keyValues[bom] = keyValues[bom] + increment;
}
else
{
keyValues.Add(bom, firstValue);
}
lastBom = bom;
return new KeyValuePair<int, int>(bom, keyValues[bom]);
}
var valueList = new List<int> { 1, 2, 3, 3, 2, 3, 4, 3, 4, 4, 2, 3, 1, 1 };
var valueSequenceList = valueList.Aggregate(
new List<KeyValuePair<int, int>>(),
(source, item) =>
{
source.Add(GetValueWithSequenceResetIfLowerThanLast(item));
return source;
}
);
foreach (var element in valueSequenceList)
System.Diagnostics.Debug.WriteLine($"{element.Key}: {element.Value}");
}

LINQ non-linear order by string length

I'm trying to get a list of string ordered such that the longest are on either end of the list and the shortest are in the middle. For example:
A
BB
CCC
DDDD
EEEEE
FFFFFF
would get sorted as:
FFFFFF
DDDD
BB
A
CCC
EEEEE
EDIT: To clarify, I was specifically looking for a LINQ implementation to achieve the desired results because I wasn't sure how/if it was possible to do using LINQ.
You could create two ordered groups, then order the first group descending(already done) and the second group ascending:
var strings = new List<string> {
"A",
"BB",
"CCC",
"DDDD",
"EEEEE",
"FFFFFF"};
var two = strings.OrderByDescending(str => str.Length)
.Select((str, index) => new { str, index })
.GroupBy(x => x.index % 2)
.ToList(); // two groups, ToList to prevent double execution in following query
List<string> ordered = two.First()
.Concat(two.Last().OrderBy(x => x.str.Length))
.Select(x => x.str)
.ToList();
Result:
[0] "FFFFFF" string
[1] "DDDD" string
[2] "BB" string
[3] "A" string
[4] "CCC" string
[5] "EEEEE" string
Don't ask how and why... ^^
list.Sort(); // In case the list is not already sorted.
var length = list.Count;
var result = Enumerable.Range(0, length)
.Select(i => length - 1 - 2 * i)
.Select(i => list[Math.Abs(i - (i >> 31))])
.ToList();
Okay, before I forget how it works, here you go.
A list with 6 items for example has to be reordered to this; the longest string is at index 5, the shortest one at index 0 of the presorted list.
5 3 1 0 2 4
We start with Enumerable.Range(0, length) yielding
0 1 2 3 4 5
then we apply i => length - 1 - 2 * i yielding
5 3 1 -1 -3 -5
and we have the non-negative part correct. Now note that i >> 31 is an arithmetic left shift and will copy the sign bit into all bits. Therefore non-negative numbers yield 0 while negative numbers yield -1. That in turn means subtracting i >> 31 will not change non-negative numbers but add 1 to negative numbers yielding
5 3 1 0 -2 -4
and now we finally apply Math.Abs() and get
5 3 1 0 2 4
which is the desired result. It works similarly for lists of odd length.
Just another option, which I find more readable and easy to follow:
You have an ordered list:
var strings = new List<string> {
"A",
"BB",
"CCC",
"DDDD",
"EEEEE",
"FFFFFF"};
Create a new list and simply alternate where you add items::
var new_list = new List<string>(); // This will hold your results
bool start = true; // Insert at head or tail
foreach (var s in strings)
{
if (start)
new_list.Insert(0,s);
else
new_list.Add(s);
start = !start; // Flip the insert location
}
Sweet and simple :)
As for Daniel Bruckner comment, if you care about which strings comes first, you could also change the start condition to:
// This will make sure the longest strings is first
bool start= strings.Count()%2 == 1;

How can I ignore / skip 'n' elements while sorting string in a lexicographical order

I have the following c# code that sorts a string in a lexicographical (alphabetical) order.
string str = "ACGGACGAACT";
IEnumerable<string> sortedSubstrings =
Enumerable.Range(0, str.Length)
.Select(i => str.Substring(i))
.OrderBy(s => s);
Result:
0 AACT
1 ACGAACT
2 ACGGACGAACT
3 ACT
4 CGAACT
5 CGGACGAACT
6 CT
7 GAACT
8 GACGAACT
9 GACGAACT
10 T
However I want to enhance this sort by skipping the 3rd and the 4th character during the lexicographical sort process
In this case the lexicographical sort will be different to the one above.
result:
0 AA[CT
1 AC[T
2 AC[GG]ACGAACT
3 AC[GA]ACT
4 CG[GA]CGAACT
5 CG[AA]CT
6 CT
7 GA[CG]AACT
8 GA[AC]T
9 GG[AC]GAACT
10 T
how can I achieve this?
This can be done by tweaking the lambda passed to OrderBy. Something like this should do it:
var sortedSubstrings =
Enumerable.Range(0, str.Length)
.Select(i => str.Substring(i))
.OrderBy(s => s.Length < 3 ? s : s.Remove(2, Math.Min(s.Length - 2, 2)));
Edit: Corrected off-by-one error.
You can change the lambda passed to OrderBy to one which will remove the 3rd and 4th symbols from the string.
var sorted = source.OrderBy(s => new string(s.Where((ch, n) => n != 2 && n != 3).ToArray()));

Select items based on popularity: Avoiding a glorified sort

I have a site where users can post and vote on suggestions. On the from page I initially list 10 suggestions and the header fetches a new random suggestion every 7 seconds.
I want the votes to influence the probability a suggestion will show up, both on the 10-suggestion list and in the header-suggestion. To that end I have a small algorithm to calculate popularity, taking into account votes, age and a couple other things (needs lots of tweaking).
Anyway, after running the algorithm I have a dictionary of suggestions and popularity index, sorted by popularity:
{ S = Suggestion1, P = 0.86 }
{ S = Suggestion2, P = 0.643 }
{ S = Suggestion3, P = 0.134 }
{ S = Suggestion4, P = 0.07 }
{ S = Suggestion5, P = 0.0 }
{ . . .}
I don't want this to be a glorified sort, so I'd like to introduce some random element to the selection process.
In short, I'd like the popularity to be the probability a suggestion gets picked out of the list.
Having a full list of suggestion/popularity, how do I go about picking 10 out based on probabilities? How can I apply the same to the looping header suggestion?
I'm afraid I don't know how to do this very fast, but if you have the collection in memory you can do it like this:
Note that you do not need to sort the list for this algorithm to work.
First sum up all the probabilities (if the probability is linked to popularity, just sum the popularity numbers, where I assume higher values means higher probability)
Calculate a random number in the range of 0 up to but not including that sum
Start at one end of the list and iterate through it
For each element, if the random number you generated is less than the popularity, pick that element
If not, subtract the popularity of the element from the random number, and continue to the next
If the list is static, you could build ranges and do some binary searches, but if the list keeps changing, then I don't know a better way.
Here is a sample LINQPad program that demonstrates:
void Main()
{
var list = Enumerable.Range(1, 9)
.Select(i => new { V = i, P = i })
.ToArray();
list.Dump("list");
var sum =
(from element in list
select element.P).Sum();
Dictionary<int, int> selected = new Dictionary<int, int>();
foreach (var value in Enumerable.Range(0, sum))
{
var temp = value;
var v = 0;
foreach (var element in list)
{
if (temp < element.P)
{
v = element.V;
break;
}
temp -= element.P;
}
Debug.Assert(v > 0);
if (!selected.ContainsKey(v))
selected[v] = 1;
else
selected[v] += 1;
}
selected.Dump("how many times was each value selected?");
}
Output:
list
[] (9 items)
V P
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9
45 45 <-- sum
how many times was each value selected?
Dictionary<Int32,Int32> (9 items)
Key Value
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9
45 <-- again, sum

Categories