From 9d7f71e2e9f4732e974ec7e40c4f364e8ee59d48 Mon Sep 17 00:00:00 2001 From: Kerry M-R Date: Wed, 1 Oct 2025 17:18:23 +0930 Subject: [PATCH 1/4] Added more TimSorterTests tests and changed method names for clarity. Added tests in HashTableTests for collision handling and resizing behavior. Improved null handling in HashTable methods and adjusted resizing logic. --- .../Sorters/Comparison/TimSorterTests.cs | 78 +++++++++++++++++- Algorithms/Sorters/Comparison/TimSorter.cs | 14 ++-- .../Sorters/Comparison/TimSorterSettings.cs | 12 +-- .../Hashing/HashTableTests.cs | 82 ++++++++++++++++--- DataStructures/Hashing/HashTable.cs | 50 ++++------- 5 files changed, 176 insertions(+), 60 deletions(-) diff --git a/Algorithms.Tests/Sorters/Comparison/TimSorterTests.cs b/Algorithms.Tests/Sorters/Comparison/TimSorterTests.cs index 582ed27c..bf69f31b 100755 --- a/Algorithms.Tests/Sorters/Comparison/TimSorterTests.cs +++ b/Algorithms.Tests/Sorters/Comparison/TimSorterTests.cs @@ -9,8 +9,8 @@ public static class TimSorterTests private static readonly TimSorterSettings Settings = new(); [Test] - public static void ArraySorted( - [Random(0, 10_000, 2000)] int n) + public static void Sort_ShouldBeEquivalentToSuccessfulBasicSort( + [Random(0, 10_000, 5000)] int n) { // Arrange var sorter = new TimSorter(Settings, IntComparer); @@ -25,7 +25,7 @@ public static void ArraySorted( } [Test] - public static void TinyArray() + public static void Sort_TinyArray_ShouldSortCorrectly() { // Arrange var sorter = new TimSorter(Settings, IntComparer); @@ -40,7 +40,7 @@ public static void TinyArray() } [Test] - public static void SmallChunks() + public static void Sort_SmallChunks_ShouldSortCorrectly() { // Arrange var sorter = new TimSorter(Settings, IntComparer); @@ -63,4 +63,74 @@ public static void SmallChunks() // Assert Assert.That(correctArray, Is.EqualTo(testArray)); } + + [Test] + public static void Sort_ThrowsArgumentNullException_WhenArrayIsNull() + { + // Arrange + var sorter = new TimSorter(Settings, IntComparer); + + // Act & Assert + Assert.Throws(() => sorter.Sort(null!, IntComparer)); + } + + [Test] + public static void Sort_UsesDefaultComparer_WhenComparerIsNull() + { + // Arrange + var sorter = new TimSorter(Settings, null!); + var (correctArray, testArray) = RandomHelper.GetArrays(20); + + // Act + sorter.Sort(testArray, IntComparer); + Array.Sort(correctArray, IntComparer); + + // Assert + Assert.That(correctArray, Is.EqualTo(testArray)); + } + + [Test] + public static void Sort_AlreadySortedArray_RemainsUnchanged() + { + // Arrange + var sorter = new TimSorter(Settings, IntComparer); + var array = new[] { 1, 2, 3, 4, 5 }; + var expected = new[] { 1, 2, 3, 4, 5 }; + + // Act + sorter.Sort(array, IntComparer); + + // Assert + Assert.That(array, Is.EqualTo(expected)); + } + + [Test] + public static void MergeAt_ShouldReturnEarly_WhenLenAIsZero() + { + // Arrange: left run is all less than right run's first element + var array = Enumerable.Range(1, 25).Concat(Enumerable.Range(100, 25)).ToArray(); + var sortedArray = Enumerable.Range(1, 25).Concat(Enumerable.Range(100, 25)).ToArray(); + var sorter = new TimSorter(new TimSorterSettings(), Comparer.Default); + + // Act + sorter.Sort(array, Comparer.Default); + + // Assert: Array order will not have changed, and the lenA <= 0 branch should be hit + Assert.That(sortedArray, Is.EqualTo(array)); + } + + [Test] + public static void MergeAt_ShouldReturnEarly_WhenLenBIsZero() + { + // Arrange: right run is all less than left run's last element + var array = Enumerable.Range(100, 25).Concat(Enumerable.Range(1, 25)).ToArray(); + var sortedArray = Enumerable.Range(1, 25).Concat(Enumerable.Range(100, 25)).ToArray(); + var sorter = new TimSorter(new TimSorterSettings(), Comparer.Default); + + // Act + sorter.Sort(array, Comparer.Default); + + // Assert: The left and right sides of the array should have swapped places, and the lenB <= 0 branch should be hit + Assert.That(sortedArray, Is.EqualTo(array)); + } } diff --git a/Algorithms/Sorters/Comparison/TimSorter.cs b/Algorithms/Sorters/Comparison/TimSorter.cs index 40254962..e83ceec0 100755 --- a/Algorithms/Sorters/Comparison/TimSorter.cs +++ b/Algorithms/Sorters/Comparison/TimSorter.cs @@ -8,7 +8,7 @@ namespace Algorithms.Sorters.Comparison; /// /// This class is based on a Java interpretation of Tim Peter's original work. /// Java class is viewable here: -/// http://cr.openjdk.java.net/~martin/webrevs/openjdk7/timsort/raw_files/new/src/share/classes/java/util/TimSort.java +/// https://web.archive.org/web/20190119032242/http://cr.openjdk.java.net:80/~martin/webrevs/openjdk7/timsort/raw_files/new/src/share/classes/java/util/TimSort.java /// /// Tim Peters's list sort for Python, is described in detail here: /// http://svn.python.org/projects/python/trunk/Objects/listsort.txt @@ -27,9 +27,6 @@ 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; @@ -56,6 +53,11 @@ private class TimChunk public TimSorter(TimSorterSettings settings, IComparer comparer) { initMinGallop = minGallop; + + // Using the worst case stack size from the C implementation. + // Based on the findings in the original listsort.txt: + // ... the stack can never grow larger than about log_base_phi(N) entries, where phi = (1 + sqrt(5)) / 2 ~= 1.618. + // Thus a small # of stack slots suffice for very large arrays ... runBase = new int[85]; runLengths = new int[85]; @@ -77,7 +79,9 @@ public TimSorter(TimSorterSettings settings, IComparer comparer) /// Compares elements. public void Sort(T[] array, IComparer comparer) { - this.comparer = comparer; + ArgumentNullException.ThrowIfNull(array); + this.comparer = comparer ?? Comparer.Default; + var start = 0; var remaining = array.Length; diff --git a/Algorithms/Sorters/Comparison/TimSorterSettings.cs b/Algorithms/Sorters/Comparison/TimSorterSettings.cs index 0f804fbd..8a29d230 100644 --- a/Algorithms/Sorters/Comparison/TimSorterSettings.cs +++ b/Algorithms/Sorters/Comparison/TimSorterSettings.cs @@ -1,14 +1,8 @@ namespace Algorithms.Sorters.Comparison; -public class TimSorterSettings +public class TimSorterSettings(int minMerge = 32, int minGallop = 7) { - public int MinMerge { get; } + public int MinMerge { get; } = minMerge; - public int MinGallop { get; } - - public TimSorterSettings(int minMerge = 32, int minGallop = 7) - { - MinMerge = minMerge; - MinGallop = minGallop; - } + public int MinGallop { get; } = minGallop; } diff --git a/DataStructures.Tests/Hashing/HashTableTests.cs b/DataStructures.Tests/Hashing/HashTableTests.cs index ce1e0476..20b91e6d 100644 --- a/DataStructures.Tests/Hashing/HashTableTests.cs +++ b/DataStructures.Tests/Hashing/HashTableTests.cs @@ -117,6 +117,18 @@ public void Add_IncreasesCount_WhenValueAlreadyExists() Assert.That(hashTable.Count, Is.EqualTo(2)); } + [Test] + public void Add_ThrowsException_OnCollision() + { + // Arrange + var hashTable = new HashTable(); + hashTable.Add(new Collider(1), 1); + + + // Act & Assert + Assert.Throws(() => hashTable.Add(new Collider(1), 2)); + } + [Test] public void Remove_ThrowsException_WhenKeyIsNull() { @@ -158,12 +170,28 @@ public void Remove_DecreasesCount_WhenKeyExists() public void Remove_DoesNotDecreaseCount_WhenKeyDoesNotExist() { var hashTable = new HashTable(); - hashTable.Remove("a"); Assert.That(hashTable.Count, Is.EqualTo(0)); } + [Test] + public void Remove_TriggersResizeDown() + { + var hashTable = new HashTable(4); + for (var i = 1; i <= 50; i++) + { + hashTable.Add(i, $"Value{i}"); + } + + for (var i = 1; i <= 40; i++) + { + hashTable.Remove(i); + } + + Assert.That(hashTable.Capacity, Is.EqualTo(40)); + } + [Test] public void ContainsValue_ReturnsFalse_WhenValueDoesNotExist() { @@ -238,6 +266,17 @@ public void Clear_RemovesAllElements() Assert.That(hashTable.ContainsKey("a"), Is.False); } + [Test] + public void Clear_ResetsTable() + { + var hashTable = new HashTable(); + hashTable.Add(1, "A"); + hashTable.Clear(); + hashTable.Add(2, "B"); + Assert.That(hashTable.Count, Is.EqualTo(1)); + Assert.That(hashTable[2], Is.EqualTo("B")); + } + [Test] public void Resize_IncreasesCapacity() { @@ -317,6 +356,13 @@ public void Constructor_ThrowsException_WhenLoadFactorIsGreaterThanOne() Assert.Throws(() => new HashTable(4, 2)); } + [Test] + public void Constructor_RoundsCapacityToPrime() + { + var hashTable = new HashTable(17); + Assert.That(hashTable.Capacity, Is.EqualTo(19)); + } + [Test] public void GetIndex_ThrowsException_WhenKeyIsNull() { @@ -399,7 +445,7 @@ public void Add_ShouldTriggerResize_WhenThresholdExceeded() var hashTable = new HashTable(initialCapacity); // Act - for (int i = 1; i <= 4; i++) // Start keys from 1 to avoid default(TKey) = 0 issue + for (var i = 1; i <= 4; i++) // Start keys from 1 to avoid default(TKey) = 0 issue { hashTable.Add(i, $"Value{i}"); } @@ -476,23 +522,28 @@ public void Capacity_Increases_WhenResizeOccurs() var initialCapacity = 4; var hashTable = new HashTable(initialCapacity); - for (int i = 1; i <= 5; i++) + for (var i = 1; i <= 5; i++) { hashTable.Add(i, $"Value{i}"); } hashTable.Capacity.Should().BeGreaterThan(initialCapacity); } -} - -public class NegativeHashKey -{ - private readonly int id; - public NegativeHashKey(int id) + [Test] + public void IndexerSet_Throws_KeyNotFound() { - this.id = id; + // Arrange + var hashTable = new HashTable(); + + // Act & Assert + Assert.Throws(() => hashTable[1] = "A"); } +} + +public class NegativeHashKey(int id) +{ + private readonly int id = id; public override int GetHashCode() { @@ -509,3 +560,14 @@ public override bool Equals(object? obj) return false; } } + +/// +/// Class to simulate hash collisions +/// +/// Id of this object +public class Collider(int id) +{ + private readonly int id = id; + public override int GetHashCode() => 42; // Force all instances to collide + public override bool Equals(object? obj) => obj is Collider other && other.id == id; +} diff --git a/DataStructures/Hashing/HashTable.cs b/DataStructures/Hashing/HashTable.cs index 8f6aac78..fa4d7836 100644 --- a/DataStructures/Hashing/HashTable.cs +++ b/DataStructures/Hashing/HashTable.cs @@ -14,12 +14,10 @@ public class HashTable { private const int DefaultCapacity = 16; private const float DefaultLoadFactor = 0.75f; - private readonly float loadFactor; private int capacity; private int size; private int threshold; - private int version; private Entry?[] entries; @@ -57,35 +55,26 @@ public TValue this[TKey? key] { get { - if (EqualityComparer.Default.Equals(key, default(TKey))) + if (EqualityComparer.Default.Equals(key, default)) { throw new ArgumentNullException(nameof(key)); } - var entry = FindEntry(key); - if (entry == null) - { - throw new KeyNotFoundException(); - } - + var entry = FindEntry(key) + ?? throw new KeyNotFoundException(); return entry.Value!; } set { - if (EqualityComparer.Default.Equals(key, default(TKey))) + if (EqualityComparer.Default.Equals(key, default)) { throw new ArgumentNullException(nameof(key)); } - var entry = FindEntry(key); - if (entry == null) - { - throw new KeyNotFoundException(); - } - + var entry = FindEntry(key) + ?? throw new KeyNotFoundException(); entry.Value = value; - version++; } } @@ -137,7 +126,7 @@ public HashTable(int capacity = DefaultCapacity, float loadFactor = DefaultLoadF /// public void Add(TKey? key, TValue? value) { - if (EqualityComparer.Default.Equals(key, default(TKey))) + if (EqualityComparer.Default.Equals(key, default)) { throw new ArgumentNullException(nameof(key)); } @@ -148,21 +137,19 @@ public void Add(TKey? key, TValue? value) } var index = GetIndex(key); - if ( - entries[index] != null && + if (entries[index] != null && EqualityComparer.Default.Equals(entries[index]!.Key!, key)) { throw new ArgumentException("Key already exists"); } - if (EqualityComparer.Default.Equals(value, default(TValue))) + if (EqualityComparer.Default.Equals(value, default)) { throw new ArgumentNullException(nameof(value)); } entries[index] = new Entry(key!, value!); size++; - version++; } /// @@ -176,7 +163,7 @@ public void Add(TKey? key, TValue? value) /// public bool Remove(TKey? key) { - if (EqualityComparer.Default.Equals(key, default(TKey))) + if (EqualityComparer.Default.Equals(key, default)) { throw new ArgumentNullException(nameof(key)); } @@ -189,7 +176,6 @@ public bool Remove(TKey? key) entries[index] = null; size--; - version++; if (size <= threshold / 4) { @@ -207,7 +193,7 @@ public bool Remove(TKey? key) /// Thrown when is null. public int GetIndex(TKey? key) { - if (EqualityComparer.Default.Equals(key, default(TKey))) + if (EqualityComparer.Default.Equals(key, default)) { throw new ArgumentNullException(nameof(key)); } @@ -234,7 +220,7 @@ public int GetIndex(TKey? key) /// public Entry? FindEntry(TKey? key) { - if (EqualityComparer.Default.Equals(key, default(TKey))) + if (EqualityComparer.Default.Equals(key, default)) { throw new ArgumentNullException(nameof(key)); } @@ -254,7 +240,7 @@ public int GetIndex(TKey? key) /// public bool ContainsKey(TKey? key) { - if (EqualityComparer.Default.Equals(key, default(TKey))) + if (EqualityComparer.Default.Equals(key, default)) { throw new ArgumentNullException(nameof(key)); } @@ -269,7 +255,7 @@ public bool ContainsKey(TKey? key) /// True if the hash table contains the value, false otherwise. public bool ContainsValue(TValue? value) { - if (EqualityComparer.Default.Equals(value, default(TValue))) + if (EqualityComparer.Default.Equals(value, default)) { throw new ArgumentNullException(nameof(value)); } @@ -289,18 +275,19 @@ public void Clear() threshold = (int)(capacity * loadFactor); entries = new Entry[capacity]; size = 0; - version++; } /// /// Resizes the hash table. /// /// - /// This method doubles the capacity of the hash table and rehashes all the elements. + /// This method doubles or halves the capacity of the hash table and rehashes all the elements. /// public void Resize() { - var newCapacity = capacity * 2; + var newCapacity = size <= threshold / 2 + ? capacity / 2 + : capacity * 2; var newEntries = new Entry[newCapacity]; foreach (var entry in entries) @@ -322,6 +309,5 @@ public void Resize() capacity = newCapacity; threshold = (int)(capacity * loadFactor); entries = newEntries; - version++; } } From ff81a5d17868bd9f525b5c4aa693117630217665 Mon Sep 17 00:00:00 2001 From: Kerry M-R Date: Wed, 1 Oct 2025 17:43:51 +0930 Subject: [PATCH 2/4] Use the default capacity as the minimum for shrinking a HashTable Remove un-needed default comparer --- Algorithms/Sorters/Comparison/TimSorter.cs | 1 - DataStructures/Hashing/HashTable.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Algorithms/Sorters/Comparison/TimSorter.cs b/Algorithms/Sorters/Comparison/TimSorter.cs index e83ceec0..91e9ab7e 100755 --- a/Algorithms/Sorters/Comparison/TimSorter.cs +++ b/Algorithms/Sorters/Comparison/TimSorter.cs @@ -80,7 +80,6 @@ public TimSorter(TimSorterSettings settings, IComparer comparer) public void Sort(T[] array, IComparer comparer) { ArgumentNullException.ThrowIfNull(array); - this.comparer = comparer ?? Comparer.Default; var start = 0; var remaining = array.Length; diff --git a/DataStructures/Hashing/HashTable.cs b/DataStructures/Hashing/HashTable.cs index fa4d7836..7f65ba6a 100644 --- a/DataStructures/Hashing/HashTable.cs +++ b/DataStructures/Hashing/HashTable.cs @@ -286,7 +286,7 @@ public void Clear() public void Resize() { var newCapacity = size <= threshold / 2 - ? capacity / 2 + ? Math.Min(capacity / 2, DefaultCapacity) : capacity * 2; var newEntries = new Entry[newCapacity]; From 2d39e23ec7c7ef1c86c9db59c5aa6976aa5f28f2 Mon Sep 17 00:00:00 2001 From: Kerry M-R Date: Wed, 1 Oct 2025 19:36:20 +0930 Subject: [PATCH 3/4] Correct Math function for DefaultCapacity on HashTable Add test for minimum HashTable size --- DataStructures.Tests/Hashing/HashTableTests.cs | 17 +++++++++++++++++ DataStructures/Hashing/HashTable.cs | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/DataStructures.Tests/Hashing/HashTableTests.cs b/DataStructures.Tests/Hashing/HashTableTests.cs index 20b91e6d..730e021c 100644 --- a/DataStructures.Tests/Hashing/HashTableTests.cs +++ b/DataStructures.Tests/Hashing/HashTableTests.cs @@ -192,6 +192,23 @@ public void Remove_TriggersResizeDown() Assert.That(hashTable.Capacity, Is.EqualTo(40)); } + [Test] + public void Remove_TriggersResizeDown_MinimumOfDefaultCapacity() + { + var hashTable = new HashTable(4); + for (var i = 1; i <= 50; i++) + { + hashTable.Add(i, $"Value{i}"); + } + + for (var i = 1; i <= 48; i++) + { + hashTable.Remove(i); + } + + Assert.That(hashTable.Capacity, Is.EqualTo(16)); + } + [Test] public void ContainsValue_ReturnsFalse_WhenValueDoesNotExist() { diff --git a/DataStructures/Hashing/HashTable.cs b/DataStructures/Hashing/HashTable.cs index 7f65ba6a..bf2a10a5 100644 --- a/DataStructures/Hashing/HashTable.cs +++ b/DataStructures/Hashing/HashTable.cs @@ -286,7 +286,7 @@ public void Clear() public void Resize() { var newCapacity = size <= threshold / 2 - ? Math.Min(capacity / 2, DefaultCapacity) + ? Math.Max(capacity / 2, DefaultCapacity) : capacity * 2; var newEntries = new Entry[newCapacity]; From 476437e7b04acf1c14df0c1a658ebd65b04b62ff Mon Sep 17 00:00:00 2001 From: Kerry M-R Date: Wed, 1 Oct 2025 22:07:15 +0930 Subject: [PATCH 4/4] Add resize test for negative hashkeys --- .../Hashing/HashTableTests.cs | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/DataStructures.Tests/Hashing/HashTableTests.cs b/DataStructures.Tests/Hashing/HashTableTests.cs index 730e021c..3b81c35d 100644 --- a/DataStructures.Tests/Hashing/HashTableTests.cs +++ b/DataStructures.Tests/Hashing/HashTableTests.cs @@ -454,6 +454,23 @@ public void Test_NegativeHashKey_ReturnsCorrectValue() Assert.That(hashTable[new NegativeHashKey(1)], Is.EqualTo(1)); } + [Test] + public void Resize_HandlesNegativeHashCodeCorrectly() + { + // Arrange + var hashTable = new HashTable(2); + + // Act + hashTable.Add(new NegativeHashKey(1), "A"); + hashTable.Add(new NegativeHashKey(2), "B"); + hashTable.Add(new NegativeHashKey(3), "C"); + + // Assert + Assert.That(hashTable[new NegativeHashKey(1)], Is.EqualTo("A")); + Assert.That(hashTable[new NegativeHashKey(2)], Is.EqualTo("B")); + Assert.That(hashTable[new NegativeHashKey(3)], Is.EqualTo("C")); + } + [Test] public void Add_ShouldTriggerResize_WhenThresholdExceeded() { @@ -462,14 +479,14 @@ public void Add_ShouldTriggerResize_WhenThresholdExceeded() var hashTable = new HashTable(initialCapacity); // Act - for (var i = 1; i <= 4; i++) // Start keys from 1 to avoid default(TKey) = 0 issue + for (var i = 1; i <= 32; i++) { hashTable.Add(i, $"Value{i}"); } // Assert - hashTable.Capacity.Should().BeGreaterThan(initialCapacity); // Ensure resizing occurred - hashTable.Count.Should().Be(4); // Verify count reflects number of added items + hashTable.Capacity.Should().BeGreaterThan(initialCapacity); + hashTable.Count.Should().Be(32); }