Skip to content

Commit f34cbbf

Browse files
authored
Add LibrarySort Implementation (#7481)
* feat: add LibrarySort implementation * major: adding the missing algorithm
1 parent 53fc764 commit f34cbbf

2 files changed

Lines changed: 277 additions & 0 deletions

File tree

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
package com.thealgorithms.sorts;
2+
3+
import java.util.Arrays;
4+
5+
/**
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
13+
* Space Complexity: O(n)
14+
*
15+
* @see <a href="https://en.wikipedia.org/wiki/Library_sort">
16+
* Wikipedia: Library Sort</a>
17+
* @author Vraj Prajapati (@Rosander0)
18+
*/
19+
public final class LibrarySort {
20+
21+
private static final int GAP_FACTOR = 2;
22+
23+
private LibrarySort() {
24+
// Utility class
25+
}
26+
27+
/**
28+
* Sorts an array using the Library Sort algorithm.
29+
*
30+
* @param array the array to sort (must not be null)
31+
* @return the sorted array
32+
* @throws IllegalArgumentException if {@code array} is {@code null}
33+
*/
34+
public static int[] sort(final int[] array) {
35+
if (array == null) {
36+
throw new IllegalArgumentException("Input array must not be null.");
37+
}
38+
if (array.length <= 1) {
39+
return array;
40+
}
41+
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];
46+
47+
final int mid = capacity / 2;
48+
data[mid] = array[0];
49+
occupied[mid] = true;
50+
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);
64+
}
65+
}
66+
67+
int idx = 0;
68+
for (int i = 0; i < capacity; i++) {
69+
if (occupied[i]) {
70+
array[idx++] = data[i];
71+
}
72+
}
73+
return array;
74+
}
75+
76+
/**
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.
162+
*/
163+
private static int findInsertionIndex(final int[] data, final boolean[] occupied, final int value) {
164+
int lo = 0;
165+
int hi = data.length;
166+
while (lo < hi) {
167+
final int mid = lo + (hi - lo) / 2;
168+
final int probe = nearestOccupiedValueAtOrAfter(data, occupied, mid);
169+
if (probe != Integer.MAX_VALUE && probe <= value) {
170+
lo = mid + 1;
171+
} else {
172+
hi = mid;
173+
}
174+
}
175+
return lo;
176+
}
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+
}
207+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.thealgorithms.sorts;
2+
3+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
4+
import static org.junit.jupiter.api.Assertions.assertThrows;
5+
6+
import org.junit.jupiter.api.Test;
7+
8+
public class LibrarySortTest {
9+
10+
@Test
11+
public void testBasicSort() {
12+
assertArrayEquals(new int[] {1, 2, 3, 4, 5}, LibrarySort.sort(new int[] {5, 3, 1, 4, 2}));
13+
}
14+
15+
@Test
16+
public void testAlreadySorted() {
17+
assertArrayEquals(new int[] {1, 2, 3, 4, 5}, LibrarySort.sort(new int[] {1, 2, 3, 4, 5}));
18+
}
19+
20+
@Test
21+
public void testReverseSorted() {
22+
assertArrayEquals(new int[] {1, 2, 3, 4, 5}, LibrarySort.sort(new int[] {5, 4, 3, 2, 1}));
23+
}
24+
25+
@Test
26+
public void testDuplicates() {
27+
assertArrayEquals(new int[] {1, 2, 2, 3, 3}, LibrarySort.sort(new int[] {3, 2, 1, 3, 2}));
28+
}
29+
30+
@Test
31+
public void testSingleElement() {
32+
assertArrayEquals(new int[] {1}, LibrarySort.sort(new int[] {1}));
33+
}
34+
35+
@Test
36+
public void testEmptyArray() {
37+
assertArrayEquals(new int[] {}, LibrarySort.sort(new int[] {}));
38+
}
39+
40+
@Test
41+
public void testNullArray() {
42+
assertThrows(IllegalArgumentException.class, () -> LibrarySort.sort(null));
43+
}
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+
}
70+
}

0 commit comments

Comments
 (0)