Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Unit Test for Negative Hash Codes in HashTable and Note on TimSort Test Limitations #466

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Algorithms.Tests/Sorters/Comparison/TimSorterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>();
var sorter = new TimSorter<int>(Settings, IntComparer);
var (correctArray, testArray) = RandomHelper.GetArrays(n);

// Act
Expand All @@ -30,7 +31,7 @@ public static void ArraySorted(
public static void TinyArray()
{
// Arrange
var sorter = new TimSorter<int>();
var sorter = new TimSorter<int>(Settings, IntComparer);
var tinyArray = new[] { 1 };
var correctArray = new[] { 1 };

Expand All @@ -45,7 +46,7 @@ public static void TinyArray()
public static void SmallChunks()
{
// Arrange
var sorter = new TimSorter<int>();
var sorter = new TimSorter<int>(Settings, IntComparer);
var (correctArray, testArray) = RandomHelper.GetArrays(800);
Array.Sort(correctArray, IntComparer);
Array.Sort(testArray, IntComparer);
Expand Down
120 changes: 120 additions & 0 deletions Algorithms.Tests/Sorters/Utils/GallopingStrategyTests.cs
Original file line number Diff line number Diff line change
@@ -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<int> comparer = Comparer<int>.Default;

[Test]
public void GallopLeft_KeyPresent_ReturnsCorrectIndex()
{
var array = new[] { 1, 2, 3, 4, 5 };
var index = GallopingStrategy<int>.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<int>.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<int>.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<int>.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<int>.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<int>.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<int>.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<int>.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<int>.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<int>.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<int>.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<int>.BoundLeftShift(shiftable);

// Assert
Assert.That(int.MaxValue, Is.EqualTo(result)); // False branch
}
}
}
143 changes: 28 additions & 115 deletions Algorithms/Sorters/Comparison/TimSorter.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Algorithms.Sorters.Utils;

namespace Algorithms.Sorters.Comparison;

Expand Down Expand Up @@ -27,6 +28,10 @@ public class TimSorter<T> : IComparisonSorter<T>
{
private readonly int minMerge;
private readonly int initMinGallop;

// Pool of reusable TimChunk objects for memory efficiency.
private readonly TimChunk<T>[] chunkPool = new TimChunk<T>[2];

private readonly int[] runBase;
private readonly int[] runLengths;

Expand All @@ -50,15 +55,18 @@ private class TimChunk<Tc>
public int Wins { get; set; }
}

public TimSorter(int minMerge = 32, int minGallop = 7)
public TimSorter(TimSorterSettings settings, IComparer<T> 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<T>.Default;
}

/// <summary>
Expand Down Expand Up @@ -158,15 +166,6 @@ private static void ReverseRange(T[] array, int start, int end)
}
}

/// <summary>
/// Left shift a value, preventing a roll over to negative numbers.
/// </summary>
/// <param name="shiftable">int value to left shift.</param>
/// <returns>Left shifted value, bound to 2,147,483,647.</returns>
private static int BoundLeftShift(int shiftable) => (shiftable << 1) < 0
? (shiftable << 1) + 1
: int.MaxValue;

/// <summary>
/// Check the chunks before getting in to a merge to make sure there's something to actually do.
/// </summary>
Expand Down Expand Up @@ -265,105 +264,6 @@ private int CountRunAndMakeAscending(T[] array, int start)
return runHi - start;
}

/// <summary>
/// Find the position in the array that a key should fit to the left of where it currently sits.
/// </summary>
/// <param name="array">Array to search.</param>
/// <param name="key">Key to place in the array.</param>
/// <param name="i">Base index for the key.</param>
/// <param name="len">Length of the chunk to run through.</param>
/// <param name="hint">Initial starting position to start from.</param>
/// <returns>Offset for the key's location.</returns>
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);
}

/// <summary>
/// Find the position in the array that a key should fit to the right of where it currently sits.
/// </summary>
/// <param name="array">Array to search.</param>
/// <param name="key">Key to place in the array.</param>
/// <param name="i">Base index for the key.</param>
/// <param name="len">Length of the chunk to run through.</param>
/// <param name="hint">Initial starting position to start from.</param>
/// <returns>Offset for the key's location.</returns>
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;
}

/// <summary>
/// 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.
Expand Down Expand Up @@ -465,7 +365,7 @@ private void MergeAt(T[] array, int index)

stackSize--;

var k = GallopRight(array, array[baseB], baseA, lenA, 0);
var k = GallopingStrategy<T>.GallopRight(array, array[baseB], baseA, lenA, comparer);

baseA += k;
lenA -= k;
Expand All @@ -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<T>.GallopLeft(array, array[baseA + lenA - 1], baseB, lenB, comparer);

if (lenB <= 0)
{
Expand Down Expand Up @@ -590,7 +490,7 @@ private bool StableMerge(TimChunk<T> left, TimChunk<T> right, ref int dest, int

private bool GallopMerge(TimChunk<T> left, TimChunk<T> right, ref int dest)
{
left.Wins = GallopRight(left.Array, right.Array[right.Index], left.Index, left.Remaining, 0);
left.Wins = GallopingStrategy<T>.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);
Expand All @@ -609,7 +509,7 @@ private bool GallopMerge(TimChunk<T> left, TimChunk<T> right, ref int dest)
return true;
}

right.Wins = GallopLeft(right.Array, left.Array[left.Index], right.Index, right.Remaining, 0);
right.Wins = GallopingStrategy<T>.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);
Expand All @@ -631,3 +531,16 @@ private bool GallopMerge(TimChunk<T> left, TimChunk<T> 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;
}
}
Loading
Loading