From 74b6fef903f2aa16f6628d033df11dd841515e6c Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sun, 8 Nov 2026 18:25:11 +0100 Subject: [PATCH] CountingSort: move to net10.0, copy-per-call benchmark, stable counting sort Targets net10.0 with BenchmarkDotNet 0.15.8, MSTest 4.5.2, Microsoft.NET.Test.Sdk 18.11.0 and coverlet.collector 10.1.0. CountingSort() drops the benchmark-only parameter and the zeroing loop and returns empty input at once. Adds StableCountingSort() with a long key range and an Array.MaxLength guard. The benchmark moves to CountingSortBenchmarks, copies its input per call and uses Array.Sort() as the baseline; the worst case outlier is 2,000,000 instead of Int32.MaxValue / 2. Tests use (expected, actual) order and cover the article example, empty, negative and stable cases. --- .../CountingSort/CountingSort.csproj | 4 +- .../CountingSort/CountingSortBenchmarks.cs | 43 +++++ .../CountingSort/CountingSortMethods.cs | 115 ++++++------ .../CountingSort/CountingSort/Program.cs | 2 +- .../CountingSortTests.csproj | 10 +- .../CountingSortUnitTests.cs | 166 +++++++++++++----- 6 files changed, 238 insertions(+), 102 deletions(-) create mode 100644 csharp-algorithms/CountingSort/CountingSort/CountingSortBenchmarks.cs diff --git a/csharp-algorithms/CountingSort/CountingSort/CountingSort.csproj b/csharp-algorithms/CountingSort/CountingSort/CountingSort.csproj index 79e7148fd0..62be4f867f 100644 --- a/csharp-algorithms/CountingSort/CountingSort/CountingSort.csproj +++ b/csharp-algorithms/CountingSort/CountingSort/CountingSort.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net10.0 enable enable - + diff --git a/csharp-algorithms/CountingSort/CountingSort/CountingSortBenchmarks.cs b/csharp-algorithms/CountingSort/CountingSort/CountingSortBenchmarks.cs new file mode 100644 index 0000000000..65e815bd71 --- /dev/null +++ b/csharp-algorithms/CountingSort/CountingSort/CountingSortBenchmarks.cs @@ -0,0 +1,43 @@ +using BenchmarkDotNet.Attributes; + +namespace CountingSort; + +public class CountingSortBenchmarks +{ + private readonly CountingSortMethods _sorter = new(); + private int[] _source = []; + private int[] _work = []; + + [Params("Best Case", "Average Case", "Worst Case")] + public string Input { get; set; } = "Average Case"; + + [GlobalSetup] + public void Setup() + { + _source = Input switch + { + "Best Case" => CountingSortMethods.CreateRandomArray(20_000, 1, 2), + "Average Case" => CountingSortMethods.CreateRandomArray(20_000, 10_000, 30_000), + _ => CountingSortMethods.CreateImbalancedArray( + CountingSortMethods.CreateRandomArray(19_999, 10_000, 30_000)) + }; + _work = new int[_source.Length]; + } + + [Benchmark] + public int[] CountingSort() + { + _source.CopyTo(_work, 0); + + return _sorter.CountingSort(_work); + } + + [Benchmark(Baseline = true)] + public int[] ArraySort() + { + _source.CopyTo(_work, 0); + Array.Sort(_work); + + return _work; + } +} diff --git a/csharp-algorithms/CountingSort/CountingSort/CountingSortMethods.cs b/csharp-algorithms/CountingSort/CountingSort/CountingSortMethods.cs index 7bcf4bc92c..c469f89235 100644 --- a/csharp-algorithms/CountingSort/CountingSort/CountingSortMethods.cs +++ b/csharp-algorithms/CountingSort/CountingSort/CountingSortMethods.cs @@ -1,79 +1,86 @@ -using BenchmarkDotNet.Attributes; +namespace CountingSort; -namespace CountingSort +public class CountingSortMethods { - public class CountingSortMethods + public int[] CountingSort(int[] array) { - public IEnumerable SampleArrays() + var size = array.Length; + if (size == 0) + return array; + + var maxElement = GetMaxVal(array, size); + var occurrences = new int[maxElement + 1]; + + for (int i = 0; i < size; i++) { - yield return new object[] { CreateRandomArray(20000, 1, 2), "Best Case" }; - yield return new object[] { CreateRandomArray(20000, 10000, 30000), "Average Case" }; - yield return new object[] { CreateImbalancedArray(CreateRandomArray(19999, 10000, 30000)), "Worst Case" }; + occurrences[array[i]]++; } - [Benchmark] - [ArgumentsSource(nameof(SampleArrays))] - public int[] CountingSort(int[] array, string arrayName) + for (int i = 0, j = 0; i <= maxElement; i++) { - var size = array.Length; - var maxElement = GetMaxVal(array, size); - var occurrences = new int[maxElement + 1]; - - for (int i = 0; i < maxElement + 1; i++) + while (occurrences[i] > 0) { - occurrences[i] = 0; + array[j] = i; + j++; + occurrences[i]--; } + } - for (int i = 0; i < size; i++) - { - occurrences[array[i]]++; - } + return array; + } - for (int i = 0, j = 0; i <= maxElement; i++) - { - while (occurrences[i] > 0) - { - array[j] = i; - j++; - occurrences[i]--; - } - } + public static int GetMaxVal(int[] array, int size) + { + var maxVal = array[0]; - return array; - } + for (int i = 1; i < size; i++) + if (array[i] > maxVal) + maxVal = array[i]; - public static int GetMaxVal(int[] array, int size) - { - var maxVal = array[0]; + return maxVal; + } - for (int i = 1; i < size; i++) - if (array[i] > maxVal) - maxVal = array[i]; + public static T[] StableCountingSort(T[] items, Func getKey) + { + if (items.Length == 0) + return []; - return maxVal; - } + var min = items.Min(getKey); + var max = items.Max(getKey); + var range = (long)max - min + 1; + if (range > Array.MaxLength) + throw new ArgumentOutOfRangeException(nameof(items), "The key range is too wide for counting sort."); - - public static int[] CreateRandomArray(int size, int lower, int upper) - { - var array = new int[size]; - var rand = new Random(); + var counts = new int[range]; - for (int i = 0; i < size; i++) - array[i] = rand.Next(lower, upper); + foreach (var item in items) + counts[getKey(item) - min]++; - return array; - } + for (int i = 1; i < counts.Length; i++) + counts[i] += counts[i - 1]; + + var sorted = new T[items.Length]; - public static int[] CreateImbalancedArray(int[] array) + for (int i = items.Length - 1; i >= 0; i--) { - List numbers = new List(); + var index = getKey(items[i]) - min; + counts[index]--; + sorted[counts[index]] = items[i]; + } - numbers.AddRange(array); - numbers.Add(Int32.MaxValue/2); + return sorted; + } - return numbers.ToArray(); - } + public static int[] CreateRandomArray(int size, int lower, int upper) + { + var array = new int[size]; + var rand = new Random(); + + for (int i = 0; i < size; i++) + array[i] = rand.Next(lower, upper); + return array; } + + public static int[] CreateImbalancedArray(int[] array) => [.. array, 2_000_000]; } diff --git a/csharp-algorithms/CountingSort/CountingSort/Program.cs b/csharp-algorithms/CountingSort/CountingSort/Program.cs index c69838dba6..5b6cf226b5 100644 --- a/csharp-algorithms/CountingSort/CountingSort/Program.cs +++ b/csharp-algorithms/CountingSort/CountingSort/Program.cs @@ -1,4 +1,4 @@ using BenchmarkDotNet.Running; using CountingSort; -var summary = BenchmarkRunner.Run(); \ No newline at end of file +BenchmarkRunner.Run(args: args); diff --git a/csharp-algorithms/CountingSort/CountingSortTests/CountingSortTests.csproj b/csharp-algorithms/CountingSort/CountingSortTests/CountingSortTests.csproj index 3cdce1f5a9..a17b2418ab 100644 --- a/csharp-algorithms/CountingSort/CountingSortTests/CountingSortTests.csproj +++ b/csharp-algorithms/CountingSort/CountingSortTests/CountingSortTests.csproj @@ -1,7 +1,7 @@  - net6.0 + net10.0 enable enable @@ -9,10 +9,10 @@ - - - - + + + + diff --git a/csharp-algorithms/CountingSort/CountingSortTests/CountingSortUnitTests.cs b/csharp-algorithms/CountingSort/CountingSortTests/CountingSortUnitTests.cs index 219fb8247e..817f12a3f2 100644 --- a/csharp-algorithms/CountingSort/CountingSortTests/CountingSortUnitTests.cs +++ b/csharp-algorithms/CountingSort/CountingSortTests/CountingSortUnitTests.cs @@ -1,56 +1,142 @@ using CountingSort; -namespace CountingSortTests +namespace CountingSortTests; + +[TestClass] +public class CountingSortUnitTests { - [TestClass] - public class CountingSortUnitTests + private record Reading(string City, int Temperature); + + [TestMethod] + public void GivenUnsortedArray_WhenArrayIsNotEmpty_ThenReturnSortedArray() + { + var array = new int[] { 73, 57, 49, 99, 133, 20, 1 }; + var expected = new int[] { 1, 20, 49, 57, 73, 99, 133 }; + var sortFunction = new CountingSortMethods(); + + var sortedArray = sortFunction.CountingSort(array); + + Assert.IsNotNull(sortedArray); + CollectionAssert.AreEqual(expected, sortedArray); + } + + [TestMethod] + public void GivenArticleExample_WhenSorted_ThenReturnSortedArray() + { + var array = new int[] { 7, 1, 2, 8, 9, 9, 4, 1, 5, 5 }; + var expected = new int[] { 1, 1, 2, 4, 5, 5, 7, 8, 9, 9 }; + + var sortedArray = new CountingSortMethods().CountingSort(array); + + CollectionAssert.AreEqual(expected, sortedArray); + } + + [TestMethod] + public void GivenRandomArray_WhenSorted_ThenMatchesArraySort() { - [TestMethod] - public void GivenUnsortedArray_WhenArrayIsNotEmpty_ThenReturnSortedArray() - { - var array = new int[] { 73, 57, 49, 99, 133, 20, 1 }; - var expected = new int[] { 1, 20, 49, 57, 73, 99, 133 }; - var sortFunction = new CountingSortMethods(); + var array = CountingSortMethods.CreateRandomArray(5_000, 0, 1_000); + var expected = (int[])array.Clone(); + Array.Sort(expected); - var sortedArray = sortFunction.CountingSort(array, string.Empty); + var sortedArray = new CountingSortMethods().CountingSort(array); - Assert.IsNotNull(sortedArray); - CollectionAssert.AreEqual(sortedArray, expected); - } + CollectionAssert.AreEqual(expected, sortedArray); + } - [TestMethod] - public void GivenUnsortedArray_WhenArrayIsRandomized_ThenCheckInstanceType() - { - var sortFunction = new CountingSortMethods(); - var array = CountingSortMethods.CreateRandomArray(200, 0, 200); + [TestMethod] + public void GivenSortedArray_WhenSorted_ThenArrayIsUnchanged() + { + var array = new int[] { 1, 2, 2, 3, 10 }; + var expected = new int[] { 1, 2, 2, 3, 10 }; - var sortedArray = sortFunction.CountingSort(array, string.Empty); + var sortedArray = new CountingSortMethods().CountingSort(array); - Assert.IsInstanceOfType(sortedArray, typeof(int[])); - } + CollectionAssert.AreEqual(expected, sortedArray); + } - [TestMethod] - public void GivenUnsortedArray_WhenArrayIsSorted_ThenResultingArrayIsNotNull() - { - var sortFunction = new CountingSortMethods(); - var array = CountingSortMethods.CreateRandomArray(200, 0, 200); + [TestMethod] + public void GivenArray_WhenArrayHasSingleElement_ThenReturnSortedArray() + { + var array = new int[] { 73 }; + var expected = new int[] { 73 }; + var sortFunction = new CountingSortMethods(); - var sortedArray = sortFunction.CountingSort(array, string.Empty); + var sortedArray = sortFunction.CountingSort(array); - Assert.IsNotNull(sortedArray); - } + Assert.IsNotNull(sortedArray); + CollectionAssert.AreEqual(expected, sortedArray); + } - [TestMethod] - public void GivenArray_WhenArrayHasSingleElement_ThenReturnSortedArray() - { - var array = new int[] { 73 }; - var expected = new int[] { 73 }; - var sortFunction = new CountingSortMethods(); + [TestMethod] + public void GivenEmptyArray_WhenSorted_ThenReturnEmptyArray() + { + var sortedArray = new CountingSortMethods().CountingSort([]); + + Assert.IsEmpty(sortedArray); + } + + [TestMethod] + public void GivenNegativeNumber_WhenSortedWithSimpleVersion_ThenThrows() + { + var array = new int[] { 3, -1, 2 }; - var sortedArray = sortFunction.CountingSort(array, string.Empty); + Assert.ThrowsExactly( + () => new CountingSortMethods().CountingSort(array)); + } + + [TestMethod] + public void GivenReadingsWithEqualKeys_WhenStableSorted_ThenEqualKeysKeepTheirOrder() + { + Reading[] readings = + [ + new("Oslo", -3), new("Cairo", 24), new("Lima", 18), + new("Tromso", -3), new("Accra", 24), new("Quito", 18) + ]; + + var sorted = CountingSortMethods.StableCountingSort(readings, r => r.Temperature); + + CollectionAssert.AreEqual( + new[] { "Oslo", "Tromso", "Lima", "Quito", "Cairo", "Accra" }, + sorted.Select(r => r.City).ToArray()); + } + + [TestMethod] + public void GivenRandomKeysWithNegatives_WhenStableSorted_ThenMatchesOrderBy() + { + var random = new Random(42); + var items = Enumerable.Range(0, 2_000) + .Select(i => (Key: random.Next(-500, 500), Position: i)) + .ToArray(); + var expected = items.OrderBy(x => x.Key).ToArray(); + + var sorted = CountingSortMethods.StableCountingSort(items, x => x.Key); + + CollectionAssert.AreEqual(expected, sorted); + } + + [TestMethod] + public void GivenEmptyArray_WhenStableSorted_ThenReturnEmptyArray() + { + var sorted = CountingSortMethods.StableCountingSort(Array.Empty(), x => x); + + Assert.IsEmpty(sorted); + } + + [TestMethod] + public void GivenFullIntRange_WhenStableSorted_ThenThrowsBeforeAllocating() + { + int[] keys = [int.MinValue, int.MaxValue]; + + Assert.ThrowsExactly( + () => CountingSortMethods.StableCountingSort(keys, x => x)); + } + + [TestMethod] + public void GivenIntMaxValue_WhenSortedWithSimpleVersion_ThenThrowsOverflow() + { + int[] array = [1, int.MaxValue]; - Assert.IsNotNull(sortedArray); - CollectionAssert.AreEqual(sortedArray, expected); - } + Assert.ThrowsExactly( + () => new CountingSortMethods().CountingSort(array)); } -} \ No newline at end of file +}