diff --git a/Algorithms.Tests/Sorters/Comparison/TimSorterTests.cs b/Algorithms.Tests/Sorters/Comparison/TimSorterTests.cs index c02a13b1..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(); + 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(); + 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(); + 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 df2220ac..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; @@ -27,6 +28,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 +55,18 @@ private class TimChunk public int Wins { get; set; } } - public TimSorter(int minMerge = 32, int minGallop = 7) + public TimSorter(TimSorterSettings settings, IComparer comparer) { initMinGallop = minGallop; - this.minMerge = minMerge; runBase = new int[85]; runLengths = new int[85]; stackSize = 0; - this.minGallop = minGallop; + + minGallop = settings.MinGallop; + minMerge = settings.MinMerge; + + this.comparer = comparer ?? Comparer.Default; } /// @@ -158,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. /// @@ -265,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. @@ -465,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; @@ -475,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) { @@ -590,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); @@ -609,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); @@ -631,3 +531,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; + } +} 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; + } + } +} 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; + } }