Skip to content

Commit b654e7e

Browse files
committed
Better resize strategy for the dynamic array
1 parent 29a30a0 commit b654e7e

2 files changed

Lines changed: 158 additions & 21 deletions

File tree

core/src/main/java/ai/timefold/solver/core/impl/util/DynamicIntArray.java

Lines changed: 74 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
*/
1010
public final class DynamicIntArray {
1111

12+
// Growth factor for array expansion; not too much, the point of this class is to avoid excessive memory use.
13+
private static final double GROWTH_FACTOR = 1.2;
14+
// Minimum capacity increment to avoid small incremental growth
15+
private static final int MIN_CAPACITY_INCREMENT = 10;
16+
1217
private final int maxLength;
1318
private int[] array;
1419
private int firstIndex;
@@ -33,7 +38,8 @@ public DynamicIntArray(int maxLength) {
3338
/**
3439
* Sets the value at the specified index.
3540
* If this is the first element, the array is created.
36-
* If the index is lower than the current firstIndex, the array is reallocated.
41+
* If the index is lower than the current firstIndex or higher than the current lastIndex,
42+
* the array is reallocated with a growth strategy to reduce frequent reallocations.
3743
*
3844
* @param index the index at which to set the value
3945
* @param value the value to set
@@ -43,42 +49,81 @@ public void set(int index, int value) {
4349
throw new ArrayIndexOutOfBoundsException(index);
4450
}
4551
if (array == null) {
46-
// First element, create the array with size 1
47-
array = new int[1];
52+
// First element, create the array with initial capacity
53+
var initialCapacity = Math.min(MIN_CAPACITY_INCREMENT, maxLength);
54+
array = new int[initialCapacity];
4855
firstIndex = index;
4956
lastIndex = index;
5057
array[0] = value;
5158
} else if (index < firstIndex) {
5259
// New index is lower than first index, need to reallocate
53-
int newSize = lastIndex - index + 1;
54-
int[] newArray = new int[newSize];
60+
var currentSize = lastIndex - firstIndex + 1;
61+
var offset = firstIndex - index;
5562

56-
// Copy existing elements to new array with offset
57-
int offset = firstIndex - index;
58-
System.arraycopy(array, 0, newArray, offset, lastIndex - firstIndex + 1);
63+
// Calculate new capacity with growth strategy
64+
var requiredCapacity = currentSize + offset;
65+
var newCapacity = calculateNewCapacity(requiredCapacity);
5966

60-
// Update first index and array
61-
firstIndex = index;
67+
// Copy existing elements to new array with offset
68+
var newArray = new int[newCapacity];
69+
System.arraycopy(array, 0, newArray, offset, currentSize);
6270
array = newArray;
71+
firstIndex = index;
6372
array[0] = value;
6473
} else if (index > lastIndex) {
6574
// New index is higher than last index, need to expand
66-
int newSize = index - firstIndex + 1;
67-
int[] newArray = new int[newSize];
75+
var currentSize = lastIndex - firstIndex + 1;
76+
var newSize = index - firstIndex + 1;
77+
78+
if (newSize > array.length) {
79+
// Calculate new capacity with growth strategy
80+
var newCapacity = calculateNewCapacity(newSize);
6881

69-
// Copy existing elements to new array
70-
System.arraycopy(array, 0, newArray, 0, array.length);
82+
// Copy existing elements to new array
83+
var newArray = new int[newCapacity];
84+
System.arraycopy(array, 0, newArray, 0, currentSize);
85+
array = newArray;
86+
}
7187

72-
// Update last index and array
88+
// Update last index
7389
lastIndex = index;
74-
array = newArray;
7590
array[index - firstIndex] = value;
7691
} else {
7792
// Index is within existing range
7893
array[index - firstIndex] = value;
7994
}
8095
}
8196

97+
/**
98+
* Calculates the new capacity based on the required capacity and growth strategy.
99+
*
100+
* @param requiredCapacity the minimum capacity needed
101+
* @return the new capacity
102+
*/
103+
private int calculateNewCapacity(int requiredCapacity) {
104+
var currentCapacity = array != null ? array.length : 0;
105+
106+
if (requiredCapacity <= currentCapacity) {
107+
return currentCapacity;
108+
}
109+
110+
// Calculate new capacity using growth factor
111+
var newCapacity = (int) (currentCapacity * GROWTH_FACTOR);
112+
113+
// Ensure minimum increment
114+
if (newCapacity - currentCapacity < MIN_CAPACITY_INCREMENT) {
115+
newCapacity = currentCapacity + MIN_CAPACITY_INCREMENT;
116+
}
117+
118+
// Ensure new capacity is at least the required capacity
119+
if (newCapacity < requiredCapacity) {
120+
newCapacity = requiredCapacity;
121+
}
122+
123+
// Ensure new capacity doesn't exceed maxLength
124+
return Math.min(newCapacity, maxLength);
125+
}
126+
82127
/**
83128
* Gets the value at the specified index.
84129
*
@@ -144,10 +189,19 @@ int length() {
144189
return lastIndex + 1;
145190
}
146191

192+
/**
193+
* Clears the array by setting all values to 0.
194+
* The array structure is preserved, only the values are reset.
195+
*/
147196
public void clear() {
148-
// Fill rather than reallocate.
149-
// We keep the original bounds, assuming the array is likely to be filled up again.
150-
Arrays.fill(array, 0);
197+
// If array is null, there's nothing to clear
198+
if (array == null) {
199+
return;
200+
}
201+
202+
// Only clear the used portion of the array (from firstIndex to lastIndex)
203+
// This is more efficient for large arrays with sparse indices
204+
Arrays.fill(array, 0, lastIndex - firstIndex + 1, 0);
151205
}
152206

153-
}
207+
}

core/src/test/java/ai/timefold/solver/core/impl/util/DynamicIntArrayTest.java

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,4 +310,87 @@ void testWithSparseIndices() {
310310
assertThat(array.length()).isEqualTo(1001); // 0-1000 inclusive
311311
}
312312
}
313-
}
313+
314+
@Nested
315+
@DisplayName("Clear method tests")
316+
class ClearMethodTests {
317+
318+
@Test
319+
@DisplayName("Clear on empty array does nothing")
320+
void clearEmptyArray() {
321+
var array = new DynamicIntArray();
322+
323+
// Should not throw an exception
324+
array.clear();
325+
326+
assertThat(array.length()).isZero();
327+
}
328+
329+
@Test
330+
@DisplayName("Clear resets all values to 0 but preserves array structure")
331+
void clearResetsValues() {
332+
var array = new DynamicIntArray();
333+
array.set(5, 24);
334+
array.set(10, 42);
335+
336+
array.clear();
337+
338+
// Values should be reset to 0
339+
assertThat(array.get(5)).isZero();
340+
assertThat(array.get(10)).isZero();
341+
342+
// Array structure should be preserved
343+
assertThat(array.getFirstIndex()).isEqualTo(5);
344+
assertThat(array.getLastIndex()).isEqualTo(10);
345+
assertThat(array.containsIndex(5)).isTrue();
346+
assertThat(array.containsIndex(10)).isTrue();
347+
assertThat(array.length()).isEqualTo(11); // 0-10 inclusive
348+
}
349+
350+
@Test
351+
@DisplayName("Clear and then set new values")
352+
void clearAndSetNewValues() {
353+
var array = new DynamicIntArray();
354+
array.set(5, 24);
355+
array.set(10, 42);
356+
357+
array.clear();
358+
359+
// Set new values
360+
array.set(7, 99);
361+
362+
// New values should be set correctly
363+
assertThat(array.get(7)).isEqualTo(99);
364+
365+
// Old indices should still be in the array but with value 0
366+
assertThat(array.get(5)).isZero();
367+
assertThat(array.get(10)).isZero();
368+
369+
// Array structure should be updated
370+
assertThat(array.getFirstIndex()).isEqualTo(5);
371+
assertThat(array.getLastIndex()).isEqualTo(10);
372+
}
373+
374+
@Test
375+
@DisplayName("Clear with sparse indices")
376+
void clearWithSparseIndices() {
377+
var array = new DynamicIntArray();
378+
array.set(10, 1);
379+
array.set(100, 2);
380+
array.set(1000, 3);
381+
382+
array.clear();
383+
384+
// All values should be reset to 0
385+
assertThat(array.get(10)).isZero();
386+
assertThat(array.get(100)).isZero();
387+
assertThat(array.get(1000)).isZero();
388+
389+
// Array structure should be preserved
390+
assertThat(array.getFirstIndex()).isEqualTo(10);
391+
assertThat(array.getLastIndex()).isEqualTo(1000);
392+
assertThat(array.length()).isEqualTo(1001); // 0-1000 inclusive
393+
}
394+
}
395+
396+
}

0 commit comments

Comments
 (0)