Skip to content

Commit ab2b5cc

Browse files
authored
Increase unit test coverage of HashTable and TimSort (#466)
1 parent 5eb0254 commit ab2b5cc

File tree

5 files changed

+291
-118
lines changed

5 files changed

+291
-118
lines changed

Algorithms.Tests/Sorters/Comparison/TimSorterTests.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ namespace Algorithms.Tests.Sorters.Comparison;
99
public static class TimSorterTests
1010
{
1111
private static readonly IntComparer IntComparer = new();
12+
private static readonly TimSorterSettings Settings = new();
1213

1314
[Test]
1415
public static void ArraySorted(
1516
[Random(0, 10_000, 2000)] int n)
1617
{
1718
// Arrange
18-
var sorter = new TimSorter<int>();
19+
var sorter = new TimSorter<int>(Settings, IntComparer);
1920
var (correctArray, testArray) = RandomHelper.GetArrays(n);
2021

2122
// Act
@@ -30,7 +31,7 @@ public static void ArraySorted(
3031
public static void TinyArray()
3132
{
3233
// Arrange
33-
var sorter = new TimSorter<int>();
34+
var sorter = new TimSorter<int>(Settings, IntComparer);
3435
var tinyArray = new[] { 1 };
3536
var correctArray = new[] { 1 };
3637

@@ -45,7 +46,7 @@ public static void TinyArray()
4546
public static void SmallChunks()
4647
{
4748
// Arrange
48-
var sorter = new TimSorter<int>();
49+
var sorter = new TimSorter<int>(Settings, IntComparer);
4950
var (correctArray, testArray) = RandomHelper.GetArrays(800);
5051
Array.Sort(correctArray, IntComparer);
5152
Array.Sort(testArray, IntComparer);
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
using Algorithms.Sorters.Utils;
2+
using NUnit.Framework;
3+
using System.Collections.Generic;
4+
5+
namespace Algorithms.Tests.Sorters.Utils
6+
{
7+
[TestFixture]
8+
public class GallopingStrategyTests
9+
{
10+
private readonly IComparer<int> comparer = Comparer<int>.Default;
11+
12+
[Test]
13+
public void GallopLeft_KeyPresent_ReturnsCorrectIndex()
14+
{
15+
var array = new[] { 1, 2, 3, 4, 5 };
16+
var index = GallopingStrategy<int>.GallopLeft(array, 3, 0, array.Length, comparer);
17+
Assert.That(index, Is.EqualTo(2));
18+
}
19+
20+
[Test]
21+
public void GallopLeft_KeyNotPresent_ReturnsCorrectIndex()
22+
{
23+
var array = new[] { 1, 2, 4, 5 };
24+
var index = GallopingStrategy<int>.GallopLeft(array, 3, 0, array.Length, comparer);
25+
Assert.That(index, Is.EqualTo(2));
26+
}
27+
28+
[Test]
29+
public void GallopLeft_KeyLessThanAll_ReturnsZero()
30+
{
31+
var array = new[] { 2, 3, 4, 5 };
32+
var index = GallopingStrategy<int>.GallopLeft(array, 1, 0, array.Length, comparer);
33+
Assert.That(index, Is.EqualTo(0));
34+
}
35+
36+
[Test]
37+
public void GallopLeft_KeyGreaterThanAll_ReturnsLength()
38+
{
39+
var array = new[] { 1, 2, 3, 4 };
40+
var index = GallopingStrategy<int>.GallopLeft(array, 5, 0, array.Length, comparer);
41+
Assert.That(index, Is.EqualTo(array.Length));
42+
}
43+
44+
[Test]
45+
public void GallopRight_KeyPresent_ReturnsCorrectIndex()
46+
{
47+
var array = new[] { 1, 2, 3, 4, 5 };
48+
var index = GallopingStrategy<int>.GallopRight(array, 3, 0, array.Length, comparer);
49+
Assert.That(index, Is.EqualTo(3));
50+
}
51+
52+
[Test]
53+
public void GallopRight_KeyNotPresent_ReturnsCorrectIndex()
54+
{
55+
var array = new[] { 1, 2, 4, 5 };
56+
var index = GallopingStrategy<int>.GallopRight(array, 3, 0, array.Length, comparer);
57+
Assert.That(index, Is.EqualTo(2));
58+
}
59+
60+
[Test]
61+
public void GallopRight_KeyLessThanAll_ReturnsZero()
62+
{
63+
var array = new[] { 2, 3, 4, 5 };
64+
var index = GallopingStrategy<int>.GallopRight(array, 1, 0, array.Length, comparer);
65+
Assert.That(index, Is.EqualTo(0));
66+
}
67+
68+
[Test]
69+
public void GallopRight_KeyGreaterThanAll_ReturnsLength()
70+
{
71+
var array = new[] { 1, 2, 3, 4 };
72+
var index = GallopingStrategy<int>.GallopRight(array, 5, 0, array.Length, comparer);
73+
Assert.That(index, Is.EqualTo(array.Length));
74+
}
75+
76+
[Test]
77+
public void GallopLeft_EmptyArray_ReturnsZero()
78+
{
79+
var array = new int[] { };
80+
var index = GallopingStrategy<int>.GallopLeft(array, 1, 0, array.Length, comparer);
81+
Assert.That(index, Is.EqualTo(0));
82+
}
83+
84+
[Test]
85+
public void GallopRight_EmptyArray_ReturnsZero()
86+
{
87+
var array = new int[] { };
88+
var index = GallopingStrategy<int>.GallopRight(array, 1, 0, array.Length, comparer);
89+
Assert.That(index, Is.EqualTo(0));
90+
}
91+
92+
// Test when (shiftable << 1) < 0 is true
93+
[Test]
94+
public void TestBoundLeftShift_WhenShiftableCausesNegativeShift_ReturnsShiftedValuePlusOne()
95+
{
96+
// Arrange
97+
int shiftable = int.MaxValue; // This should cause a negative result after left shift
98+
99+
// Act
100+
int result = GallopingStrategy<int>.BoundLeftShift(shiftable);
101+
102+
// Assert
103+
Assert.That((shiftable << 1) + 1, Is.EqualTo(result)); // True branch
104+
}
105+
106+
// Test when (shiftable << 1) < 0 is false
107+
[Test]
108+
public void TestBoundLeftShift_WhenShiftableDoesNotCauseNegativeShift_ReturnsMaxValue()
109+
{
110+
// Arrange
111+
int shiftable = 1; // This will not cause a negative result after left shift
112+
113+
// Act
114+
int result = GallopingStrategy<int>.BoundLeftShift(shiftable);
115+
116+
// Assert
117+
Assert.That(int.MaxValue, Is.EqualTo(result)); // False branch
118+
}
119+
}
120+
}

Algorithms/Sorters/Comparison/TimSorter.cs

Lines changed: 28 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using Algorithms.Sorters.Utils;
34

45
namespace Algorithms.Sorters.Comparison;
56

@@ -27,6 +28,10 @@ public class TimSorter<T> : IComparisonSorter<T>
2728
{
2829
private readonly int minMerge;
2930
private readonly int initMinGallop;
31+
32+
// Pool of reusable TimChunk objects for memory efficiency.
33+
private readonly TimChunk<T>[] chunkPool = new TimChunk<T>[2];
34+
3035
private readonly int[] runBase;
3136
private readonly int[] runLengths;
3237

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

53-
public TimSorter(int minMerge = 32, int minGallop = 7)
58+
public TimSorter(TimSorterSettings settings, IComparer<T> comparer)
5459
{
5560
initMinGallop = minGallop;
56-
this.minMerge = minMerge;
5761
runBase = new int[85];
5862
runLengths = new int[85];
5963

6064
stackSize = 0;
61-
this.minGallop = minGallop;
65+
66+
minGallop = settings.MinGallop;
67+
minMerge = settings.MinMerge;
68+
69+
this.comparer = comparer ?? Comparer<T>.Default;
6270
}
6371

6472
/// <summary>
@@ -158,15 +166,6 @@ private static void ReverseRange(T[] array, int start, int end)
158166
}
159167
}
160168

161-
/// <summary>
162-
/// Left shift a value, preventing a roll over to negative numbers.
163-
/// </summary>
164-
/// <param name="shiftable">int value to left shift.</param>
165-
/// <returns>Left shifted value, bound to 2,147,483,647.</returns>
166-
private static int BoundLeftShift(int shiftable) => (shiftable << 1) < 0
167-
? (shiftable << 1) + 1
168-
: int.MaxValue;
169-
170169
/// <summary>
171170
/// Check the chunks before getting in to a merge to make sure there's something to actually do.
172171
/// </summary>
@@ -265,105 +264,6 @@ private int CountRunAndMakeAscending(T[] array, int start)
265264
return runHi - start;
266265
}
267266

268-
/// <summary>
269-
/// Find the position in the array that a key should fit to the left of where it currently sits.
270-
/// </summary>
271-
/// <param name="array">Array to search.</param>
272-
/// <param name="key">Key to place in the array.</param>
273-
/// <param name="i">Base index for the key.</param>
274-
/// <param name="len">Length of the chunk to run through.</param>
275-
/// <param name="hint">Initial starting position to start from.</param>
276-
/// <returns>Offset for the key's location.</returns>
277-
private int GallopLeft(T[] array, T key, int i, int len, int hint)
278-
{
279-
var (offset, lastOfs) = comparer.Compare(key, array[i + hint]) > 0
280-
? RightRun(array, key, i, len, hint, 0)
281-
: LeftRun(array, key, i, hint, 1);
282-
283-
return FinalOffset(array, key, i, offset, lastOfs, 1);
284-
}
285-
286-
/// <summary>
287-
/// Find the position in the array that a key should fit to the right of where it currently sits.
288-
/// </summary>
289-
/// <param name="array">Array to search.</param>
290-
/// <param name="key">Key to place in the array.</param>
291-
/// <param name="i">Base index for the key.</param>
292-
/// <param name="len">Length of the chunk to run through.</param>
293-
/// <param name="hint">Initial starting position to start from.</param>
294-
/// <returns>Offset for the key's location.</returns>
295-
private int GallopRight(T[] array, T key, int i, int len, int hint)
296-
{
297-
var (offset, lastOfs) = comparer.Compare(key, array[i + hint]) < 0
298-
? LeftRun(array, key, i, hint, 0)
299-
: RightRun(array, key, i, len, hint, -1);
300-
301-
return FinalOffset(array, key, i, offset, lastOfs, 0);
302-
}
303-
304-
private (int offset, int lastOfs) LeftRun(T[] array, T key, int i, int hint, int lt)
305-
{
306-
var maxOfs = hint + 1;
307-
var (offset, tmp) = (1, 0);
308-
309-
while (offset < maxOfs && comparer.Compare(key, array[i + hint - offset]) < lt)
310-
{
311-
tmp = offset;
312-
offset = BoundLeftShift(offset);
313-
}
314-
315-
if (offset > maxOfs)
316-
{
317-
offset = maxOfs;
318-
}
319-
320-
var lastOfs = hint - offset;
321-
offset = hint - tmp;
322-
323-
return (offset, lastOfs);
324-
}
325-
326-
private (int offset, int lastOfs) RightRun(T[] array, T key, int i, int len, int hint, int gt)
327-
{
328-
var (offset, lastOfs) = (1, 0);
329-
var maxOfs = len - hint;
330-
while (offset < maxOfs && comparer.Compare(key, array[i + hint + offset]) > gt)
331-
{
332-
lastOfs = offset;
333-
offset = BoundLeftShift(offset);
334-
}
335-
336-
if (offset > maxOfs)
337-
{
338-
offset = maxOfs;
339-
}
340-
341-
offset += hint;
342-
lastOfs += hint;
343-
344-
return (offset, lastOfs);
345-
}
346-
347-
private int FinalOffset(T[] array, T key, int i, int offset, int lastOfs, int lt)
348-
{
349-
lastOfs++;
350-
while (lastOfs < offset)
351-
{
352-
var m = lastOfs + (int)((uint)(offset - lastOfs) >> 1);
353-
354-
if (comparer.Compare(key, array[i + m]) < lt)
355-
{
356-
offset = m;
357-
}
358-
else
359-
{
360-
lastOfs = m + 1;
361-
}
362-
}
363-
364-
return offset;
365-
}
366-
367267
/// <summary>
368268
/// Sorts the specified portion of the specified array using a binary
369269
/// 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)
465365

466366
stackSize--;
467367

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

470370
baseA += k;
471371
lenA -= k;
@@ -475,7 +375,7 @@ private void MergeAt(T[] array, int index)
475375
return;
476376
}
477377

478-
lenB = GallopLeft(array, array[baseA + lenA - 1], baseB, lenB, lenB - 1);
378+
lenB = GallopingStrategy<T>.GallopLeft(array, array[baseA + lenA - 1], baseB, lenB, comparer);
479379

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

591491
private bool GallopMerge(TimChunk<T> left, TimChunk<T> right, ref int dest)
592492
{
593-
left.Wins = GallopRight(left.Array, right.Array[right.Index], left.Index, left.Remaining, 0);
493+
left.Wins = GallopingStrategy<T>.GallopRight(left.Array, right.Array[right.Index], left.Index, left.Remaining, comparer);
594494
if (left.Wins != 0)
595495
{
596496
Array.Copy(left.Array, left.Index, right.Array, dest, left.Wins);
@@ -609,7 +509,7 @@ private bool GallopMerge(TimChunk<T> left, TimChunk<T> right, ref int dest)
609509
return true;
610510
}
611511

612-
right.Wins = GallopLeft(right.Array, left.Array[left.Index], right.Index, right.Remaining, 0);
512+
right.Wins = GallopingStrategy<T>.GallopLeft(right.Array, left.Array[left.Index], right.Index, right.Remaining, comparer);
613513
if (right.Wins != 0)
614514
{
615515
Array.Copy(right.Array, right.Index, right.Array, dest, right.Wins);
@@ -631,3 +531,16 @@ private bool GallopMerge(TimChunk<T> left, TimChunk<T> right, ref int dest)
631531
return false;
632532
}
633533
}
534+
535+
public class TimSorterSettings
536+
{
537+
public int MinMerge { get; }
538+
539+
public int MinGallop { get; }
540+
541+
public TimSorterSettings(int minMerge = 32, int minGallop = 7)
542+
{
543+
MinMerge = minMerge;
544+
MinGallop = minGallop;
545+
}
546+
}

0 commit comments

Comments
 (0)