From 840fc9bb3677a28bd58732a88e95a21b30b2848f Mon Sep 17 00:00:00 2001 From: Konstantinos Kalafatis Date: Sat, 24 Aug 2024 12:33:43 +0300 Subject: [PATCH 1/3] Add Unit Test for Handling Negative Hash Codes in Custom HashTable Implementation This commit introduces a new unit test and a supporting class to validate the handling of negative hash codes within our custom HashTable implementation. Changes: Unit Test: Test_NegativeHashKey_ReturnsCorrectValue Purpose: The test ensures that the HashTable correctly handles keys with negative hash codes. This scenario is important for robustness, as real-world use cases might involve hash codes that are negative, especially when custom GetHashCode implementations are involved. Implementation: A new HashTable is instantiated with a small initial capacity (4) to ensure hash collisions and proper management of entries. The test adds a key-value pair to the HashTable where the key (NegativeHashKey) intentionally generates a negative hash code. The test then asserts that the value can be correctly retrieved using a key that generates the same negative hash code, verifying the integrity of the HashTable under these conditions. Supporting Class: NegativeHashKey Purpose: The NegativeHashKey class is designed to simulate keys that produce negative hash codes, which is essential for triggering the edge case being tested. Implementation: The class contains an integer id used to generate a negative hash code by returning the negation of id in the GetHashCode method. The Equals method is overridden to ensure correct key comparison based on the id field, allowing the HashTable to manage and compare instances of NegativeHashKey accurately. --- .../Hashing/HashTableTests.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/DataStructures.Tests/Hashing/HashTableTests.cs b/DataStructures.Tests/Hashing/HashTableTests.cs index bf0658ea..10ee6d7d 100644 --- a/DataStructures.Tests/Hashing/HashTableTests.cs +++ b/DataStructures.Tests/Hashing/HashTableTests.cs @@ -381,4 +381,37 @@ public void This_Get_KeyNotFoundException_WhenKeyDoesNotExist() Console.WriteLine(value); }); } + + [Test] + public void Test_NegativeHashKey_ReturnsCorrectValue() + { + var hashTable = new HashTable(4); + hashTable.Add(new NegativeHashKey(1), 1); + Assert.That(hashTable[new NegativeHashKey(1)], Is.EqualTo(1)); + } +} + +public class NegativeHashKey +{ + private readonly int id; + + public NegativeHashKey(int id) + { + this.id = id; + } + + public override int GetHashCode() + { + // Return a negative hash code + return -id; + } + + public override bool Equals(object? obj) + { + if (obj is NegativeHashKey other) + { + return id == other.id; + } + return false; + } } From 9439ee2375fb5e277e923610a66a154b25ef238c Mon Sep 17 00:00:00 2001 From: Konstantinos Kalafatis Date: Sun, 22 Sep 2024 12:47:23 +0300 Subject: [PATCH 2/3] Add TimSorterSettings for configuration and flexibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactored TimSorter to introduce a TimSorterSettings class, which encapsulates configuration parameters like minMerge and minGallop. This change separates configuration concerns from the sorting logic, improving code readability, maintainability, and testability. - Introduced TimSorterSettings class with minMerge and minGallop parameters. - Updated TimSorter constructor to accept a settings object for configuration. - Enhanced testability by allowing customizable settings for different test scenarios. - Simplified TimSorter’s constructor and reduced parameter clutter. - Facilitated future scalability by allowing easy extension of configuration options. This change adheres to the Single Responsibility Principle (SRP) and improves flexibility in sorting behavior across different contexts. --- .../Sorters/Comparison/TimSorterTests.cs | 6 ++--- Algorithms/Sorters/Comparison/TimSorter.cs | 24 ++++++++++++++++--- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/Algorithms.Tests/Sorters/Comparison/TimSorterTests.cs b/Algorithms.Tests/Sorters/Comparison/TimSorterTests.cs index c02a13b1..7605412a 100755 --- a/Algorithms.Tests/Sorters/Comparison/TimSorterTests.cs +++ b/Algorithms.Tests/Sorters/Comparison/TimSorterTests.cs @@ -15,7 +15,7 @@ public static void ArraySorted( [Random(0, 10_000, 2000)] int n) { // Arrange - var sorter = new TimSorter(); + var sorter = new TimSorter(new TimSorterSettings()); var (correctArray, testArray) = RandomHelper.GetArrays(n); // Act @@ -30,7 +30,7 @@ public static void ArraySorted( public static void TinyArray() { // Arrange - var sorter = new TimSorter(); + var sorter = new TimSorter(new TimSorterSettings()); var tinyArray = new[] { 1 }; var correctArray = new[] { 1 }; @@ -45,7 +45,7 @@ public static void TinyArray() public static void SmallChunks() { // Arrange - var sorter = new TimSorter(); + var sorter = new TimSorter(new TimSorterSettings()); var (correctArray, testArray) = RandomHelper.GetArrays(800); Array.Sort(correctArray, IntComparer); Array.Sort(testArray, IntComparer); diff --git a/Algorithms/Sorters/Comparison/TimSorter.cs b/Algorithms/Sorters/Comparison/TimSorter.cs index df2220ac..e2d221fc 100755 --- a/Algorithms/Sorters/Comparison/TimSorter.cs +++ b/Algorithms/Sorters/Comparison/TimSorter.cs @@ -27,6 +27,10 @@ public class TimSorter : IComparisonSorter { private readonly int minMerge; private readonly int initMinGallop; + + // Pool of reusable TimChunk objects for memory efficiency. + private readonly TimChunk[] chunkPool = new TimChunk[2]; + private readonly int[] runBase; private readonly int[] runLengths; @@ -50,15 +54,16 @@ private class TimChunk public int Wins { get; set; } } - public TimSorter(int minMerge = 32, int minGallop = 7) + public TimSorter(TimSorterSettings settings) { initMinGallop = minGallop; - this.minMerge = minMerge; runBase = new int[85]; runLengths = new int[85]; stackSize = 0; - this.minGallop = minGallop; + + minGallop = settings.MinGallop; + minMerge = settings.MinMerge; } /// @@ -631,3 +636,16 @@ private bool GallopMerge(TimChunk left, TimChunk right, ref int dest) return false; } } + +public class TimSorterSettings +{ + public int MinMerge { get; } + + public int MinGallop { get; } + + public TimSorterSettings(int minMerge = 32, int minGallop = 7) + { + MinMerge = minMerge; + MinGallop = minGallop; + } +} From d97a99f4d80445dc6915ab25c39680bdf35400c5 Mon Sep 17 00:00:00 2001 From: Konstantinos Kalafatis Date: Sun, 22 Sep 2024 13:31:11 +0300 Subject: [PATCH 3/3] Extract galloping methods into static GallopingStrategy class - Moved galloping logic (GallopLeft, GallopRight, LeftRun, RightRun, FinalOffset) from TimSorter to a new GallopingStrategy static class. - Simplified the code by removing the interface and making all methods static since there's no need for instance-specific behavior. - The refactored GallopingStrategy class now encapsulates galloping functionality, improving modularity and testability. - Updated TimSorter to use GallopingStrategy for gallop operations, enhancing code clarity and separation of concerns. --- .../Sorters/Comparison/TimSorterTests.cs | 7 +- .../Sorters/Utils/GallopingStrategyTests.cs | 120 +++++++++++++++++ Algorithms/Sorters/Comparison/TimSorter.cs | 121 ++---------------- Algorithms/Sorters/Utils/GallopingStrategy.cs | 106 +++++++++++++++ 4 files changed, 238 insertions(+), 116 deletions(-) create mode 100644 Algorithms.Tests/Sorters/Utils/GallopingStrategyTests.cs create mode 100644 Algorithms/Sorters/Utils/GallopingStrategy.cs diff --git a/Algorithms.Tests/Sorters/Comparison/TimSorterTests.cs b/Algorithms.Tests/Sorters/Comparison/TimSorterTests.cs index 7605412a..822ac789 100755 --- a/Algorithms.Tests/Sorters/Comparison/TimSorterTests.cs +++ b/Algorithms.Tests/Sorters/Comparison/TimSorterTests.cs @@ -9,13 +9,14 @@ namespace Algorithms.Tests.Sorters.Comparison; public static class TimSorterTests { private static readonly IntComparer IntComparer = new(); + private static readonly TimSorterSettings Settings = new(); [Test] public static void ArraySorted( [Random(0, 10_000, 2000)] int n) { // Arrange - var sorter = new TimSorter(new TimSorterSettings()); + var sorter = new TimSorter(Settings, IntComparer); var (correctArray, testArray) = RandomHelper.GetArrays(n); // Act @@ -30,7 +31,7 @@ public static void ArraySorted( public static void TinyArray() { // Arrange - var sorter = new TimSorter(new TimSorterSettings()); + var sorter = new TimSorter(Settings, IntComparer); var tinyArray = new[] { 1 }; var correctArray = new[] { 1 }; @@ -45,7 +46,7 @@ public static void TinyArray() public static void SmallChunks() { // Arrange - var sorter = new TimSorter(new TimSorterSettings()); + var sorter = new TimSorter(Settings, IntComparer); var (correctArray, testArray) = RandomHelper.GetArrays(800); Array.Sort(correctArray, IntComparer); Array.Sort(testArray, IntComparer); diff --git a/Algorithms.Tests/Sorters/Utils/GallopingStrategyTests.cs b/Algorithms.Tests/Sorters/Utils/GallopingStrategyTests.cs new file mode 100644 index 00000000..2c7e6050 --- /dev/null +++ b/Algorithms.Tests/Sorters/Utils/GallopingStrategyTests.cs @@ -0,0 +1,120 @@ +using Algorithms.Sorters.Utils; +using NUnit.Framework; +using System.Collections.Generic; + +namespace Algorithms.Tests.Sorters.Utils +{ + [TestFixture] + public class GallopingStrategyTests + { + private readonly IComparer comparer = Comparer.Default; + +[Test] + public void GallopLeft_KeyPresent_ReturnsCorrectIndex() + { + var array = new[] { 1, 2, 3, 4, 5 }; + var index = GallopingStrategy.GallopLeft(array, 3, 0, array.Length, comparer); + Assert.That(index, Is.EqualTo(2)); + } + + [Test] + public void GallopLeft_KeyNotPresent_ReturnsCorrectIndex() + { + var array = new[] { 1, 2, 4, 5 }; + var index = GallopingStrategy.GallopLeft(array, 3, 0, array.Length, comparer); + Assert.That(index, Is.EqualTo(2)); + } + + [Test] + public void GallopLeft_KeyLessThanAll_ReturnsZero() + { + var array = new[] { 2, 3, 4, 5 }; + var index = GallopingStrategy.GallopLeft(array, 1, 0, array.Length, comparer); + Assert.That(index, Is.EqualTo(0)); + } + + [Test] + public void GallopLeft_KeyGreaterThanAll_ReturnsLength() + { + var array = new[] { 1, 2, 3, 4 }; + var index = GallopingStrategy.GallopLeft(array, 5, 0, array.Length, comparer); + Assert.That(index, Is.EqualTo(array.Length)); + } + + [Test] + public void GallopRight_KeyPresent_ReturnsCorrectIndex() + { + var array = new[] { 1, 2, 3, 4, 5 }; + var index = GallopingStrategy.GallopRight(array, 3, 0, array.Length, comparer); + Assert.That(index, Is.EqualTo(3)); + } + + [Test] + public void GallopRight_KeyNotPresent_ReturnsCorrectIndex() + { + var array = new[] { 1, 2, 4, 5 }; + var index = GallopingStrategy.GallopRight(array, 3, 0, array.Length, comparer); + Assert.That(index, Is.EqualTo(2)); + } + + [Test] + public void GallopRight_KeyLessThanAll_ReturnsZero() + { + var array = new[] { 2, 3, 4, 5 }; + var index = GallopingStrategy.GallopRight(array, 1, 0, array.Length, comparer); + Assert.That(index, Is.EqualTo(0)); + } + + [Test] + public void GallopRight_KeyGreaterThanAll_ReturnsLength() + { + var array = new[] { 1, 2, 3, 4 }; + var index = GallopingStrategy.GallopRight(array, 5, 0, array.Length, comparer); + Assert.That(index, Is.EqualTo(array.Length)); + } + + [Test] + public void GallopLeft_EmptyArray_ReturnsZero() + { + var array = new int[] { }; + var index = GallopingStrategy.GallopLeft(array, 1, 0, array.Length, comparer); + Assert.That(index, Is.EqualTo(0)); + } + + [Test] + public void GallopRight_EmptyArray_ReturnsZero() + { + var array = new int[] { }; + var index = GallopingStrategy.GallopRight(array, 1, 0, array.Length, comparer); + Assert.That(index, Is.EqualTo(0)); + } + + // Test when (shiftable << 1) < 0 is true + [Test] + public void TestBoundLeftShift_WhenShiftableCausesNegativeShift_ReturnsShiftedValuePlusOne() + { + // Arrange + int shiftable = int.MaxValue; // This should cause a negative result after left shift + + // Act + int result = GallopingStrategy.BoundLeftShift(shiftable); + + // Assert + Assert.That((shiftable << 1) + 1, Is.EqualTo(result)); // True branch + } + + // Test when (shiftable << 1) < 0 is false + [Test] + public void TestBoundLeftShift_WhenShiftableDoesNotCauseNegativeShift_ReturnsMaxValue() + { + // Arrange + int shiftable = 1; // This will not cause a negative result after left shift + + // Act + int result = GallopingStrategy.BoundLeftShift(shiftable); + + // Assert + Assert.That(int.MaxValue, Is.EqualTo(result)); // False branch + } + } +} diff --git a/Algorithms/Sorters/Comparison/TimSorter.cs b/Algorithms/Sorters/Comparison/TimSorter.cs index e2d221fc..0115e560 100755 --- a/Algorithms/Sorters/Comparison/TimSorter.cs +++ b/Algorithms/Sorters/Comparison/TimSorter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Algorithms.Sorters.Utils; namespace Algorithms.Sorters.Comparison; @@ -54,7 +55,7 @@ private class TimChunk public int Wins { get; set; } } - public TimSorter(TimSorterSettings settings) + public TimSorter(TimSorterSettings settings, IComparer comparer) { initMinGallop = minGallop; runBase = new int[85]; @@ -64,6 +65,8 @@ public TimSorter(TimSorterSettings settings) minGallop = settings.MinGallop; minMerge = settings.MinMerge; + + this.comparer = comparer ?? Comparer.Default; } /// @@ -163,15 +166,6 @@ private static void ReverseRange(T[] array, int start, int end) } } - /// - /// Left shift a value, preventing a roll over to negative numbers. - /// - /// int value to left shift. - /// Left shifted value, bound to 2,147,483,647. - private static int BoundLeftShift(int shiftable) => (shiftable << 1) < 0 - ? (shiftable << 1) + 1 - : int.MaxValue; - /// /// Check the chunks before getting in to a merge to make sure there's something to actually do. /// @@ -270,105 +264,6 @@ private int CountRunAndMakeAscending(T[] array, int start) return runHi - start; } - /// - /// Find the position in the array that a key should fit to the left of where it currently sits. - /// - /// Array to search. - /// Key to place in the array. - /// Base index for the key. - /// Length of the chunk to run through. - /// Initial starting position to start from. - /// Offset for the key's location. - private int GallopLeft(T[] array, T key, int i, int len, int hint) - { - var (offset, lastOfs) = comparer.Compare(key, array[i + hint]) > 0 - ? RightRun(array, key, i, len, hint, 0) - : LeftRun(array, key, i, hint, 1); - - return FinalOffset(array, key, i, offset, lastOfs, 1); - } - - /// - /// Find the position in the array that a key should fit to the right of where it currently sits. - /// - /// Array to search. - /// Key to place in the array. - /// Base index for the key. - /// Length of the chunk to run through. - /// Initial starting position to start from. - /// Offset for the key's location. - private int GallopRight(T[] array, T key, int i, int len, int hint) - { - var (offset, lastOfs) = comparer.Compare(key, array[i + hint]) < 0 - ? LeftRun(array, key, i, hint, 0) - : RightRun(array, key, i, len, hint, -1); - - return FinalOffset(array, key, i, offset, lastOfs, 0); - } - - private (int offset, int lastOfs) LeftRun(T[] array, T key, int i, int hint, int lt) - { - var maxOfs = hint + 1; - var (offset, tmp) = (1, 0); - - while (offset < maxOfs && comparer.Compare(key, array[i + hint - offset]) < lt) - { - tmp = offset; - offset = BoundLeftShift(offset); - } - - if (offset > maxOfs) - { - offset = maxOfs; - } - - var lastOfs = hint - offset; - offset = hint - tmp; - - return (offset, lastOfs); - } - - private (int offset, int lastOfs) RightRun(T[] array, T key, int i, int len, int hint, int gt) - { - var (offset, lastOfs) = (1, 0); - var maxOfs = len - hint; - while (offset < maxOfs && comparer.Compare(key, array[i + hint + offset]) > gt) - { - lastOfs = offset; - offset = BoundLeftShift(offset); - } - - if (offset > maxOfs) - { - offset = maxOfs; - } - - offset += hint; - lastOfs += hint; - - return (offset, lastOfs); - } - - private int FinalOffset(T[] array, T key, int i, int offset, int lastOfs, int lt) - { - lastOfs++; - while (lastOfs < offset) - { - var m = lastOfs + (int)((uint)(offset - lastOfs) >> 1); - - if (comparer.Compare(key, array[i + m]) < lt) - { - offset = m; - } - else - { - lastOfs = m + 1; - } - } - - return offset; - } - /// /// Sorts the specified portion of the specified array using a binary /// insertion sort. It requires O(n log n) compares, but O(n^2) data movement. @@ -470,7 +365,7 @@ private void MergeAt(T[] array, int index) stackSize--; - var k = GallopRight(array, array[baseB], baseA, lenA, 0); + var k = GallopingStrategy.GallopRight(array, array[baseB], baseA, lenA, comparer); baseA += k; lenA -= k; @@ -480,7 +375,7 @@ private void MergeAt(T[] array, int index) return; } - lenB = GallopLeft(array, array[baseA + lenA - 1], baseB, lenB, lenB - 1); + lenB = GallopingStrategy.GallopLeft(array, array[baseA + lenA - 1], baseB, lenB, comparer); if (lenB <= 0) { @@ -595,7 +490,7 @@ private bool StableMerge(TimChunk left, TimChunk right, ref int dest, int private bool GallopMerge(TimChunk left, TimChunk right, ref int dest) { - left.Wins = GallopRight(left.Array, right.Array[right.Index], left.Index, left.Remaining, 0); + left.Wins = GallopingStrategy.GallopRight(left.Array, right.Array[right.Index], left.Index, left.Remaining, comparer); if (left.Wins != 0) { Array.Copy(left.Array, left.Index, right.Array, dest, left.Wins); @@ -614,7 +509,7 @@ private bool GallopMerge(TimChunk left, TimChunk right, ref int dest) return true; } - right.Wins = GallopLeft(right.Array, left.Array[left.Index], right.Index, right.Remaining, 0); + right.Wins = GallopingStrategy.GallopLeft(right.Array, left.Array[left.Index], right.Index, right.Remaining, comparer); if (right.Wins != 0) { Array.Copy(right.Array, right.Index, right.Array, dest, right.Wins); diff --git a/Algorithms/Sorters/Utils/GallopingStrategy.cs b/Algorithms/Sorters/Utils/GallopingStrategy.cs new file mode 100644 index 00000000..2226064b --- /dev/null +++ b/Algorithms/Sorters/Utils/GallopingStrategy.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Algorithms.Sorters.Utils +{ + public static class GallopingStrategy + { + public static int GallopLeft(T[] array, T key, int baseIndex, int length, IComparer comparer) + { + if (array.Length == 0) + { + return 0; + } + + var (offset, lastOfs) = comparer.Compare(key, array[baseIndex]) > 0 + ? RightRun(array, key, baseIndex, length, 0, comparer) + : LeftRun(array, key, baseIndex, 0, comparer); + + return FinalOffset(array, key, baseIndex, offset, lastOfs, 1, comparer); + } + + public static int GallopRight(T[] array, T key, int baseIndex, int length, IComparer comparer) + { + if (array.Length == 0) + { + return 0; + } + + var (offset, lastOfs) = comparer.Compare(key, array[baseIndex]) < 0 + ? LeftRun(array, key, baseIndex, length, comparer) + : RightRun(array, key, baseIndex, length, 0, comparer); + + return FinalOffset(array, key, baseIndex, offset, lastOfs, 0, comparer); + } + + public static int BoundLeftShift(int shiftable) => (shiftable << 1) < 0 + ? (shiftable << 1) + 1 + : int.MaxValue; + + private static (int offset, int lastOfs) LeftRun(T[] array, T key, int baseIndex, int hint, IComparer comparer) + { + var maxOfs = hint + 1; + var (offset, tmp) = (1, 0); + + while (offset < maxOfs && comparer.Compare(key, array[baseIndex + hint - offset]) < 0) + { + tmp = offset; + offset = BoundLeftShift(offset); + } + + if (offset > maxOfs) + { + offset = maxOfs; + } + + var lastOfs = hint - offset; + offset = hint - tmp; + + return (offset, lastOfs); + } + + private static (int offset, int lastOfs) RightRun(T[] array, T key, int baseIndex, int len, int hint, IComparer comparer) + { + var (offset, lastOfs) = (1, 0); + var maxOfs = len - hint; + while (offset < maxOfs && comparer.Compare(key, array[baseIndex + hint + offset]) > 0) + { + lastOfs = offset; + offset = BoundLeftShift(offset); + } + + if (offset > maxOfs) + { + offset = maxOfs; + } + + offset += hint; + lastOfs += hint; + + return (offset, lastOfs); + } + + private static int FinalOffset(T[] array, T key, int baseIndex, int offset, int lastOfs, int lt, IComparer comparer) + { + lastOfs++; + while (lastOfs < offset) + { + var m = lastOfs + (int)((uint)(offset - lastOfs) >> 1); + + if (comparer.Compare(key, array[baseIndex + m]) < lt) + { + offset = m; + } + else + { + lastOfs = m + 1; + } + } + + return offset; + } + } +}