Skip to content

Commit 73d96f4

Browse files
committed
major: adding the missing algorithm
1 parent f9ce5c0 commit 73d96f4

2 files changed

Lines changed: 180 additions & 29 deletions

File tree

Lines changed: 154 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package com.thealgorithms.sorts;
22

3+
import java.util.Arrays;
4+
35
/**
4-
* Library Sort (also known as Gapped Insertion Sort) is traditionally implemented
5-
* using periodic gaps between elements for faster insertion. This implementation
6-
* uses binary search to find the insertion position combined with array shifting,
7-
* which is a simplified variant without gap-based optimization.
8-
* Time Complexity: O(n^2) worst case due to element shifting
6+
* Library Sort (also known as Gapped Insertion Sort) maintains a sparse
7+
* working array with gaps distributed between elements, so that most
8+
* insertions land directly in an empty gap without shifting anything.
9+
* Elements are inserted in rounds that double in size (1, 2, 4, 8, ...);
10+
* after each round the array is rebalanced so gaps are spread out evenly
11+
* again for the next round.
12+
* Time Complexity: O(n log n) expected, O(n^2) worst case if gaps collapse
913
* Space Complexity: O(n)
1014
*
1115
* @see <a href="https://en.wikipedia.org/wiki/Library_sort">
@@ -14,6 +18,8 @@
1418
*/
1519
public final class LibrarySort {
1620

21+
private static final int GAP_FACTOR = 2;
22+
1723
private LibrarySort() {
1824
// Utility class
1925
}
@@ -33,49 +39,169 @@ public static int[] sort(final int[] array) {
3339
return array;
3440
}
3541

36-
int n = array.length;
37-
Integer[] spaced = new Integer[2 * n];
42+
final int n = array.length;
43+
final int capacity = GAP_FACTOR * n;
44+
final int[] data = new int[capacity];
45+
final boolean[] occupied = new boolean[capacity];
3846

39-
spaced[0] = array[0];
40-
int inserted = 1;
47+
final int mid = capacity / 2;
48+
data[mid] = array[0];
49+
occupied[mid] = true;
4150

42-
for (int i = 1; i < n; i++) {
43-
int pos = binarySearch(spaced, inserted, array[i]);
44-
for (int j = inserted; j > pos; j--) {
45-
spaced[j] = spaced[j - 1];
51+
int filled = 1;
52+
int nextToInsert = 1;
53+
int round = 0;
54+
while (nextToInsert < n) {
55+
final int roundSize = Math.min(1 << round, n - nextToInsert);
56+
for (int i = 0; i < roundSize; i++) {
57+
insert(data, occupied, array[nextToInsert + i]);
58+
filled++;
59+
}
60+
nextToInsert += roundSize;
61+
round++;
62+
if (nextToInsert < n) {
63+
rebalance(data, occupied, filled);
4664
}
47-
spaced[pos] = array[i];
48-
inserted++;
4965
}
5066

5167
int idx = 0;
52-
for (int i = 0; i < 2 * n; i++) {
53-
if (spaced[i] != null) {
54-
array[idx++] = spaced[i];
68+
for (int i = 0; i < capacity; i++) {
69+
if (occupied[i]) {
70+
array[idx++] = data[i];
5571
}
5672
}
5773
return array;
5874
}
5975

6076
/**
61-
* Binary search to find insertion position among inserted elements.
62-
*
63-
* @param spaced the spaced array
64-
* @param inserted number of elements inserted so far
65-
* @param target the value to find position for
66-
* @return the correct insertion index
77+
* Inserts {@code value} into the gapped array, placing it directly in an
78+
* empty gap when possible, otherwise shifting toward the nearest gap.
79+
*/
80+
private static void insert(final int[] data, final boolean[] occupied, final int value) {
81+
final int pos = findInsertionIndex(data, occupied, value);
82+
if (pos >= data.length) {
83+
insertAtEnd(data, occupied, value);
84+
return;
85+
}
86+
87+
if (!occupied[pos]) {
88+
data[pos] = value;
89+
occupied[pos] = true;
90+
return;
91+
}
92+
93+
int right = pos;
94+
while (right < data.length && occupied[right]) {
95+
right++;
96+
}
97+
int left = pos - 1;
98+
while (left >= 0 && occupied[left]) {
99+
left--;
100+
}
101+
102+
final boolean canGoRight = right < data.length;
103+
final boolean canGoLeft = left >= 0;
104+
105+
if (canGoRight && (!canGoLeft || (right - pos) <= (pos - left))) {
106+
// Shift data[pos, right) one slot to the right, opening a gap at pos.
107+
// occupied[pos] is untouched by the copy and was already true.
108+
System.arraycopy(data, pos, data, pos + 1, right - pos);
109+
occupied[right] = true;
110+
data[pos] = value;
111+
} else if (canGoLeft) {
112+
// Shift data[left + 1, pos) one slot to the left, opening a gap at pos - 1.
113+
// occupied[pos - 1] is untouched by the copy and was already true.
114+
System.arraycopy(data, left + 1, data, left, pos - 1 - left);
115+
occupied[left] = true;
116+
data[pos - 1] = value;
117+
} else {
118+
// Unreachable in practice: canGoRight and canGoLeft can only both be false if
119+
// every slot in this capacity-2n array is occupied, but at most n elements are
120+
// ever present at once. Kept as a defensive guard against that invariant breaking.
121+
throw new IllegalStateException("No gap available for insertion; rebalance too infrequent.");
122+
}
123+
}
124+
125+
/**
126+
* Handles insertion of a new global maximum, which must land after every
127+
* currently occupied slot. Since there is no room to its right, this
128+
* shifts occupied slots left into the nearest gap instead.
129+
*/
130+
private static void insertAtEnd(final int[] data, final boolean[] occupied, final int value) {
131+
final int last = data.length - 1;
132+
// occupied[last] is unreachable as false here: insertAtEnd() is only called when
133+
// findInsertionIndex() returns data.length, which requires data[last] to already be
134+
// occupied. Kept as a defensive guard in case that invariant is ever broken.
135+
if (!occupied[last]) {
136+
data[last] = value;
137+
occupied[last] = true;
138+
return;
139+
}
140+
int left = last - 1;
141+
while (left >= 0 && occupied[left]) {
142+
left--;
143+
}
144+
// left < 0 is unreachable in practice: at most n elements ever occupy this
145+
// capacity-2n array, so fewer than half the slots left of `last` can be filled,
146+
// guaranteeing a gap exists before the scan reaches index -1.
147+
if (left < 0) {
148+
throw new IllegalStateException("No gap available for insertion; rebalance too infrequent.");
149+
}
150+
// Shift data[left + 1, last] one slot to the left, opening a gap at last.
151+
// occupied[last] is untouched by the copy and was already true.
152+
System.arraycopy(data, left + 1, data, left, last - left);
153+
occupied[left] = true;
154+
data[last] = value;
155+
}
156+
157+
/**
158+
* Finds the leftmost index at which {@code value} can be inserted so
159+
* that occupied slots remain sorted. Empty slots are compared using the
160+
* value of the nearest occupied slot at or after them, which is a
161+
* monotonic function of index and therefore safe to binary search over.
67162
*/
68-
private static int binarySearch(final Integer[] spaced, final int inserted, final int target) {
163+
private static int findInsertionIndex(final int[] data, final boolean[] occupied, final int value) {
69164
int lo = 0;
70-
int hi = inserted;
165+
int hi = data.length;
71166
while (lo < hi) {
72-
int mid = lo + (hi - lo) / 2;
73-
if (spaced[mid] <= target) {
167+
final int mid = lo + (hi - lo) / 2;
168+
final int probe = nearestOccupiedValueAtOrAfter(data, occupied, mid);
169+
if (probe != Integer.MAX_VALUE && probe <= value) {
74170
lo = mid + 1;
75171
} else {
76172
hi = mid;
77173
}
78174
}
79175
return lo;
80176
}
177+
178+
private static int nearestOccupiedValueAtOrAfter(final int[] data, final boolean[] occupied, final int index) {
179+
for (int i = index; i < data.length; i++) {
180+
if (occupied[i]) {
181+
return data[i];
182+
}
183+
}
184+
return Integer.MAX_VALUE;
185+
}
186+
187+
/**
188+
* Redistributes the {@code filled} occupied elements evenly across the
189+
* full capacity of {@code data}, restoring uniform gaps between them.
190+
*/
191+
private static void rebalance(final int[] data, final boolean[] occupied, final int filled) {
192+
final int capacity = data.length;
193+
final int[] temp = new int[filled];
194+
int idx = 0;
195+
for (int i = 0; i < capacity; i++) {
196+
if (occupied[i]) {
197+
temp[idx++] = data[i];
198+
}
199+
}
200+
Arrays.fill(occupied, false);
201+
for (int k = 0; k < filled; k++) {
202+
final int pos = (int) ((long) k * capacity / filled);
203+
data[pos] = temp[k];
204+
occupied[pos] = true;
205+
}
206+
}
81207
}

src/test/java/com/thealgorithms/sorts/LibrarySortTest.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
package com.thealgorithms.sorts;
2-
// author: Vraj Prajapati @Rosander0
32

43
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
54
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -42,4 +41,30 @@ public void testEmptyArray() {
4241
public void testNullArray() {
4342
assertThrows(IllegalArgumentException.class, () -> LibrarySort.sort(null));
4443
}
44+
45+
// --- Added to cover branches the tests above never reach ---
46+
47+
@Test
48+
public void testShiftLeftWhenRightSideIsFull() {
49+
// Right side of the target slot is completely occupied, forcing a left shift.
50+
assertArrayEquals(new int[] {0, 1, 2, 3, 4, 5, 6}, LibrarySort.sort(new int[] {0, 1, 2, 6, 4, 5, 3}));
51+
}
52+
53+
@Test
54+
public void testTieBreakPrefersRightWhenDistancesEqual() {
55+
// A gap exists on both sides at equal distance; algorithm should favor the right shift.
56+
assertArrayEquals(new int[] {0, 1, 2, 3}, LibrarySort.sort(new int[] {0, 1, 3, 2}));
57+
}
58+
59+
@Test
60+
public void testRightSearchRunsOffTheEnd() {
61+
// No gap anywhere to the right of the target slot, all the way to the array's end.
62+
assertArrayEquals(new int[] {0, 1, 2, 3, 4, 5, 6, 7}, LibrarySort.sort(new int[] {0, 1, 2, 3, 4, 5, 7, 6}));
63+
}
64+
65+
@Test
66+
public void testInsertAtEndWithNoTrailingGap() {
67+
// A new global maximum arrives with no trailing gap left, forcing insertAtEnd().
68+
assertArrayEquals(new int[] {0, 1, 2, 3, 4, 5, 6, 7}, LibrarySort.sort(new int[] {0, 1, 2, 3, 4, 5, 6, 7}));
69+
}
4570
}

0 commit comments

Comments
 (0)