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..91e9ab7e 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,8 @@ public TimSorter(TimSorterSettings settings, IComparer comparer) /// Compares elements. public void Sort(T[] array, IComparer comparer) { - this.comparer = comparer; + ArgumentNullException.ThrowIfNull(array); + 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 0e338178..bc294bb7 100644 --- a/DataStructures.Tests/Hashing/HashTableTests.cs +++ b/DataStructures.Tests/Hashing/HashTableTests.cs @@ -113,6 +113,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() { @@ -154,12 +166,45 @@ 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 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() { @@ -234,6 +279,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() { @@ -313,6 +369,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() { @@ -388,6 +451,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() { @@ -396,14 +476,14 @@ 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 <= 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); } @@ -473,23 +553,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() { @@ -506,3 +591,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 25dd72a1..31ec67be 100644 --- a/DataStructures/Hashing/HashTable.cs +++ b/DataStructures/Hashing/HashTable.cs @@ -11,12 +11,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; @@ -54,35 +52,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++; } } @@ -134,7 +123,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)); } @@ -145,21 +134,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++; } /// @@ -173,7 +160,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)); } @@ -186,7 +173,6 @@ public bool Remove(TKey? key) entries[index] = null; size--; - version++; if (size <= threshold / 4) { @@ -204,7 +190,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)); } @@ -231,7 +217,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)); } @@ -251,7 +237,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)); } @@ -266,7 +252,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)); } @@ -286,18 +272,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 + ? Math.Max(capacity / 2, DefaultCapacity) + : capacity * 2; var newEntries = new Entry[newCapacity]; foreach (var entry in entries) @@ -319,6 +306,5 @@ public void Resize() capacity = newCapacity; threshold = (int)(capacity * loadFactor); entries = newEntries; - version++; } }