diff --git a/commons-numbers-arrays/pom.xml b/commons-numbers-arrays/pom.xml index 2d86e9a15..65ab6b023 100644 --- a/commons-numbers-arrays/pom.xml +++ b/commons-numbers-arrays/pom.xml @@ -49,6 +49,18 @@ test + + org.apache.commons + commons-rng-sampling + test + + + + org.apache.commons + commons-rng-simple + test + + diff --git a/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/BitIndexUpdatingInterval.java b/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/BitIndexUpdatingInterval.java new file mode 100644 index 000000000..d7a69e227 --- /dev/null +++ b/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/BitIndexUpdatingInterval.java @@ -0,0 +1,239 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.arrays; + +/** + * An {@link UpdatingInterval} backed by a fixed size of bits. + * + *

This is a specialised class to implement a reduced API similar to a + * {@link java.util.BitSet}. It uses no bounds range checks and supports only + * the methods required to implement the {@link UpdatingInterval} API. + * + *

An offset is supported to allow the fixed size to cover a range of indices starting + * above 0 with the most efficient usage of storage. + * + *

See the BloomFilter code in Commons Collections for use of long[] data to store + * bits. + * + * @since 1.2 + */ +final class BitIndexUpdatingInterval implements UpdatingInterval { + /** All 64-bits bits set. */ + private static final long LONG_MASK = -1L; + /** A bit shift to apply to an integer to divided by 64 (2^6). */ + private static final int DIVIDE_BY_64 = 6; + + /** Bit indexes. */ + private final long[] data; + + /** Index offset. */ + private final int offset; + /** Left bound of the support. */ + private int left; + /** Right bound of the support. */ + private int right; + + /** + * Create an instance to store indices within the range {@code [left, right]}. + * The range is not validated. + * + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + BitIndexUpdatingInterval(int left, int right) { + this.offset = left; + this.left = left; + this.right = right; + // Allocate storage to store index==right + // Note: This may allow directly writing to index > right if there + // is extra capacity. + data = new long[getLongIndex(right - offset) + 1]; + } + + /** + * Create an instance with the range {@code [left, right]} and reusing the provided + * index {@code data}. + * + * @param data Data. + * @param offset Index offset. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + private BitIndexUpdatingInterval(long[] data, int offset, int left, int right) { + this.data = data; + this.offset = offset; + this.left = left; + this.right = right; + } + + /** + * Gets the filter index for the specified bit index assuming the filter is using + * 64-bit longs to store bits starting at index 0. + * + *

The index is assumed to be positive. For a positive index the result will match + * {@code bitIndex / 64}.

+ * + *

The divide is performed using bit shifts. If the input is negative the + * behavior is not defined.

+ * + * @param bitIndex the bit index (assumed to be positive) + * @return the index of the bit map in an array of bit maps. + */ + private static int getLongIndex(final int bitIndex) { + // An integer divide by 64 is equivalent to a shift of 6 bits if the integer is + // positive. + // We do not explicitly check for a negative here. Instead we use a + // signed shift. Any negative index will produce a negative value + // by sign-extension and if used as an index into an array it will throw an + // exception. + return bitIndex >> DIVIDE_BY_64; + } + + /** + * Gets the filter bit mask for the specified bit index assuming the filter is using + * 64-bit longs to store bits starting at index 0. The returned value is a + * {@code long} with only 1 bit set. + * + *

The index is assumed to be positive. For a positive index the result will match + * {@code 1L << (bitIndex % 64)}.

+ * + *

If the input is negative the behavior is not defined.

+ * + * @param bitIndex the bit index (assumed to be positive) + * @return the filter bit + */ + private static long getLongBit(final int bitIndex) { + // Bit shifts only use the first 6 bits. Thus it is not necessary to mask this + // using 0x3f (63) or compute bitIndex % 64. + // Note: If the index is negative the shift will be (64 - (bitIndex & 0x3f)) and + // this will identify an incorrect bit. + return 1L << bitIndex; + } + + /** + * Sets the bit at the specified index to {@code true}. + * + *

Warning: This has no range checks. + * + * @param bitIndex the bit index (assumed to be positive) + */ + void set(int bitIndex) { + // WARNING: No range checks !!! + final int index = bitIndex - offset; + final int i = getLongIndex(index); + final long m = getLongBit(index); + data[i] |= m; + } + + /** + * Returns the index of the first bit that is set to {@code true} that occurs on or + * after the specified starting index. + * + *

Warning: This has no range checks. It is assumed that {@code left <= k <= right}, + * that is there is a set bit on or after {@code k}. + * + * @param k Index to start checking from (inclusive). + * @return the index of the next set bit + */ + private int nextIndex(int k) { + // left <= k <= right + + final int index = k - offset; + int i = getLongIndex(index); + + // Mask bits after the bit index + // mask = 11111000 = -1L << (index % 64) + long bits = data[i] & (LONG_MASK << index); + for (;;) { + if (bits != 0) { + //(i+1) i + // | index | + // | | | + // 0 001010000 + return i * Long.SIZE + Long.numberOfTrailingZeros(bits) + offset; + } + // Unsupported: the interval should contain k + //if (++i == data.length) + // return right + 1 + bits = data[++i]; + } + } + + /** + * Returns the index of the first bit that is set to {@code true} that occurs on or + * before the specified starting index. + * + *

Warning: This has no range checks. It is assumed that {@code left <= k <= right}, + * that is there is a set bit on or before {@code k}. + * + * @param k Index to start checking from (inclusive). + * @return the index of the previous set bit + */ + private int previousIndex(int k) { + // left <= k <= right + + final int index = k - offset; + int i = getLongIndex(index); + + // Mask bits before the bit index + // mask = 00011111 = -1L >>> (64 - ((index + 1) % 64)) + long bits = data[i] & (LONG_MASK >>> -(index + 1)); + for (;;) { + if (bits != 0) { + //(i+1) i + // | index | + // | | | + // 0 001010000 + return (i + 1) * Long.SIZE - Long.numberOfLeadingZeros(bits) - 1 + offset; + } + // Unsupported: the interval should contain k + //if (i == 0) + // return left - 1 + bits = data[--i]; + } + } + + @Override + public int left() { + return left; + } + + @Override + public int right() { + return right; + } + + @Override + public int updateLeft(int k) { + // Assume left < k= < right + return left = nextIndex(k); + } + + @Override + public int updateRight(int k) { + // Assume left <= k < right + return right = previousIndex(k); + } + + @Override + public UpdatingInterval splitLeft(int ka, int kb) { + // Assume left < ka <= kb < right + final int lower = left; + left = nextIndex(kb + 1); + return new BitIndexUpdatingInterval(data, offset, lower, previousIndex(ka - 1)); + } +} diff --git a/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/HashIndexSet.java b/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/HashIndexSet.java new file mode 100644 index 000000000..388e82294 --- /dev/null +++ b/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/HashIndexSet.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.arrays; + +/** + * An index set backed by a open-addressed hash table using linear hashing. Table size is a power + * of 2 and has a maximum capacity of 2^29 with a fixed load factor of 0.5. If the functional + * capacity is exceeded then the set raises an {@link IllegalStateException}. + * + *

Values are stored using bit inversion. Any positive index will have a negative + * representation when stored. An empty slot is indicated by a zero. + * + *

This class has a minimal API. It can be used to ensure a collection of indices of + * a known size are unique: + * + *

{@code
+ * int[] keys = ...
+ * HashIndexSet set = new HashIndexSet(keys.length);
+ * for (int k : keys) {
+ *   if (set.add(k)) {
+ *     // first occurrence of k in keys
+ *   }
+ * }
+ * }
+ * + * @see Open addressing (Wikipedia) + * @since 1.2 + */ +final class HashIndexSet { + /** Message for an invalid index. */ + private static final String INVALID_INDEX = "Invalid index: "; + /** The maximum capacity of the set. */ + private static final int MAX_CAPACITY = 1 << 29; + /** The minimum size of the backing array. */ + private static final int MIN_SIZE = 16; + /** + * Unsigned 32-bit integer numerator of the golden ratio (0.618) with an assumed + * denominator of 2^32. + * + *
+     * 2654435769 = round(2^32 * (sqrt(5) - 1) / 2)
+     * Long.toHexString((long)(0x1p32 * (Math.sqrt(5.0) - 1) / 2))
+     * 
+ */ + private static final int PHI = 0x9e3779b9; + + /** The set. */ + private final int[] set; + /** The size. */ + private int size; + + /** + * Create an instance with size to store up to the specified {@code capacity}. + * + *

The functional capacity (number of indices that can be stored) is the next power + * of 2 above {@code capacity}; or a minimum size if the requested {@code capacity} is + * small. + * + * @param capacity Capacity. + */ + private HashIndexSet(int capacity) { + // This will generate a load factor at capacity in the range (0.25, 0.5] + // The use of Math.max will ignore zero/negative capacity requests. + set = new int[nextPow2(Math.max(MIN_SIZE, capacity * 2))]; + } + + /** + * Create an instance with size to store up to the specified {@code capacity}. + * The maximum supported {@code capacity} is 229. + * + * @param capacity Capacity. + * @return the hash index set + * @throws IllegalArgumentException if the {@code capacity} is too large. + */ + static HashIndexSet create(int capacity) { + if (capacity > MAX_CAPACITY) { + throw new IllegalArgumentException("Unsupported capacity: " + capacity); + } + return new HashIndexSet(capacity); + } + + /** + * Returns the closest power-of-two number greater than or equal to {@code value}. + * + *

Warning: This will return {@link Integer#MIN_VALUE} for any {@code value} above + * {@code 1 << 30}. This is the next power of 2 as an unsigned integer. + * + *

See Bit + * Hacks: Rounding up to a power of 2 + * + * @param value Value. + * @return the closest power-of-two number greater than or equal to value + */ + private static int nextPow2(int value) { + int result = value - 1; + result |= result >>> 1; + result |= result >>> 2; + result |= result >>> 4; + result |= result >>> 8; + return (result | (result >>> 16)) + 1; + } + + /** + * Adds the {@code index} to the set. + * + * @param index Index. + * @return true if the set was modified by the operation + * @throws IndexOutOfBoundsException if the index is negative + */ + boolean add(int index) { + if (index < 0) { + throw new IndexOutOfBoundsException(INVALID_INDEX + index); + } + final int[] keys = set; + final int key = ~index; + final int mask = keys.length - 1; + int pos = mix(index) & mask; + int curr = keys[pos]; + if (curr < 0) { + if (curr == key) { + // Already present + return false; + } + // Probe + while ((curr = keys[pos = (pos + 1) & mask]) < 0) { + if (curr == key) { + // Already present + return false; + } + } + } + // Insert + keys[pos] = key; + // Here the load factor is 0.5: Test if size > keys.length * 0.5 + if (++size > (mask + 1) >>> 1) { + // This is where we should grow the size of the set and re-insert + // all current keys into the new key storage. Here we are using a + // fixed capacity so raise an exception. + throw new IllegalStateException("Functional capacity exceeded: " + (keys.length >>> 1)); + } + return true; + } + + /** + * Mix the bits of an integer. + * + *

This is the fast hash function used in the linear hash implementation in the Koloboke Collections. + * + * @param x Bits. + * @return the mixed bits + */ + private static int mix(int x) { + final int h = x * PHI; + return h ^ (h >>> 16); + } +} diff --git a/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/IndexSupport.java b/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/IndexSupport.java new file mode 100644 index 000000000..35bfb2fce --- /dev/null +++ b/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/IndexSupport.java @@ -0,0 +1,334 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.arrays; + +/** + * Support for creating {@link UpdatingInterval} implementations and validating indices. + * + * @since 1.2 + */ +final class IndexSupport { + /** The upper threshold to use a modified insertion sort to find unique indices. */ + private static final int INSERTION_SORT_SIZE = 20; + + /** No instances. */ + private IndexSupport() {} + + /** + * Returns an interval that covers the specified indices {@code k}. + * + * @param left Lower bound of data (inclusive). + * @param right Upper bound of data (inclusive). + * @param k Indices. + * @param n Count of indices (must be strictly positive). + * @throws IndexOutOfBoundsException if any index {@code k} is not within the + * sub-range {@code [left, right]} + * @return the interval + */ + static UpdatingInterval createUpdatingInterval(int left, int right, int[] k, int n) { + // Note: A typical use case is to have a few indices. Thus the heuristics + // in this method should be very fast when n is small. + // We have a choice between a KeyUpdatingInterval which requires + // sorted keys or a BitIndexUpdatingInterval which handles keys in any order. + // The purpose of the heuristics is to avoid a very bad choice of data structure, + // rather than choosing the best data structure in all situations. As long as the + // choice is reasonable the speed will not impact a partition algorithm. + + // Simple cases + if (n == 2) { + if (k[0] == k[1]) { + return newUpdatingInterval(left, right, k, 1); + } + if (k[1] < k[0]) { + final int v = k[0]; + k[0] = k[1]; + k[1] = v; + } + return newUpdatingInterval(left, right, k, 2); + } + + // Strategy: Must be fast on already ascending data. + // Note: The recommended way to generate a lot of partition indices is to + // generate in sequence. + + // n <= small: + // Modified insertion sort (naturally finds ascending data) + // n > small: + // Look for ascending sequence and compact + // else: + // Remove duplicates using an order(1) data structure and sort + + if (n <= INSERTION_SORT_SIZE) { + final int unique = Sorting.insertionSortIndices(k, n); + return newUpdatingInterval(left, right, k, unique); + } + + if (isAscending(k, n)) { + // For sorted keys the KeyUpdatingInterval is fast. It may be slower than the + // BitIndexUpdatingInterval depending on data length but not significantly + // slower and the difference is lost in the time taken for partitioning. + // So always use the keys. + final int unique = compressDuplicates(k, n); + return newUpdatingInterval(left, right, k, unique); + } + + // At least 20 indices that are partially unordered. + + // Find min/max to understand the range. + int min = k[n - 1]; + int max = min; + for (int i = n - 1; --i >= 0;) { + min = Math.min(min, k[i]); + max = Math.max(max, k[i]); + } + + // Here we use a simple test based on the number of comparisons required + // to perform the expected next/previous look-ups after a split. + // It is expected that we can cut n keys a maximum of n-1 times. + // Each cut requires a scan next/previous to divide the interval into two intervals: + // + // cut + // | + // k1--------k2---------k3---- ... ---------kn initial interval + // <--| find previous + // find next |--> + // k1 k2---------k3---- ... ---------kn divided intervals + // + // An BitSet will scan from the cut location and find a match in time proportional to + // the index density. Average density is (size / n) and the scanning covers 64 + // indices together: Order(2 * n * (size / n) / 64) = Order(size / 32) + + // Sorted keys: Sort time Order(n log(n)) : Splitting time Order(log(n)) (binary search approx) + // Bit keys : Sort time Order(1) : Splitting time Order(size / 32) + + // Transition when n * n ~ size / 32 + // Benchmarking shows this is a reasonable approximation when size < 2^20. + // The speed of the bit keys is approximately independent of n and proportional to size. + // Large size observes degrading performance of the bit keys vs sorted keys. + // We introduce a penalty for each 4x increase over size = 2^20. + // n * n = size/32 * 2^log4(size / 2^20) + // The transition point still favours the bit keys when sorted keys would be faster. + // However the difference is held within 4x and the BitSet type structure is still fast + // enough to be negligible against the speed of partitioning. + + // Transition point: n = sqrt(size/32) + // size n + // 2^10 5.66 + // 2^15 32.0 + // 2^20 181.0 + + // Transition point: n = sqrt(size/32 * 2^(log4(size/2^20)))) + // size n + // 2^22 512.0 + // 2^24 1448.2 + // 2^28 11585 + // 2^31 55108 + + final int size = max - min + 1; + + // Divide by 32 is a shift of 5. This is reduced for each 4-fold size above 2^20. + // At 2^31 the shift reduces to 0. + int shift = 5; + if (size > (1 << 20)) { + // log4(size/2^20) == (log2(size) - 20) / 2 + shift -= (ceilLog2(size) - 20) >>> 1; + } + + if ((long) n * n > (size >> shift)) { + final BitIndexUpdatingInterval interval = new BitIndexUpdatingInterval(min, max); + for (int i = n; --i >= 0;) { + interval.set(k[i]); + } + return interval; + } + + // Sort with a hash set to filter indices + final int unique = Sorting.sortIndices(k, n); + return new KeyUpdatingInterval(k, unique); + } + + /** + * Test the data is in ascending order: {@code data[i] <= data[i+1]} for all {@code i}. + * Data is assumed to be at least length 1. + * + * @param data Data. + * @param n Length of data. + * @return true if ascending + */ + private static boolean isAscending(int[] data, int n) { + for (int i = 0; ++i < n;) { + if (data[i] < data[i - 1]) { + // descending + return false; + } + } + return true; + } + + /** + * Compress duplicates in the ascending data. + * + *

Warning: Requires {@code n > 0}. + * + * @param data Indices. + * @param n Number of indices. + * @return the number of unique indices + */ + private static int compressDuplicates(int[] data, int n) { + // Compress to remove duplicates + int last = 0; + int top = data[0]; + for (int i = 0; ++i < n;) { + final int v = data[i]; + if (v == top) { + continue; + } + top = v; + data[++last] = v; + } + return last + 1; + } + + /** + * Compute {@code ceil(log2(x))}. This is valid for all strictly positive {@code x}. + * + *

Returns -1 for {@code x = 0} in place of -infinity. + * + * @param x Value. + * @return {@code ceil(log2(x))} + */ + private static int ceilLog2(int x) { + return 32 - Integer.numberOfLeadingZeros(x - 1); + } + + /** + * Returns an interval that covers the specified indices {@code k}. + * The indices must be sorted. + * + * @param left Lower bound of data (inclusive). + * @param right Upper bound of data (inclusive). + * @param k Indices. + * @param n Count of indices (must be strictly positive). + * @throws IndexOutOfBoundsException if any index {@code k} is not within the + * sub-range {@code [left, right]} + * @return the interval + */ + private static UpdatingInterval newUpdatingInterval(int left, int right, int[] k, int n) { + return new KeyUpdatingInterval(k, n); + } + + /** + * Count the number of indices. Returns a negative value if the indices are sorted. + * + * @param keys Keys. + * @param n Count of indices. + * @return the count of (sorted) indices + */ + static int countIndices(UpdatingInterval keys, int n) { + if (keys instanceof KeyUpdatingInterval) { + return -((KeyUpdatingInterval) keys).size(); + } + return n; + } + + /** + * Checks if the sub-range from fromIndex (inclusive) to toIndex (exclusive) is + * within the bounds of range from 0 (inclusive) to length (exclusive). + * + *

This function provides the functionality of + * {@code java.utils.Objects.checkFromToIndex} introduced in JDK 9. The Objects + * javadoc has been reproduced for reference. The return value has been changed + * to void. + * + *

The sub-range is defined to be out of bounds if any of the following + * inequalities is true: + *

+ * + * @param fromIndex Lower-bound (inclusive) of the sub-range. + * @param toIndex Upper-bound (exclusive) of the sub-range. + * @param length Upper-bound (exclusive) of the range. + * @throws IndexOutOfBoundsException if the sub-range is out of bounds + */ + static void checkFromToIndex(int fromIndex, int toIndex, int length) { + // Checks as documented above + if (fromIndex < 0 || fromIndex > toIndex || toIndex > length) { + throw new IndexOutOfBoundsException( + msgRangeOutOfBounds(fromIndex, toIndex, length)); + } + } + + /** + * Checks if the {@code index} is within the half-open interval {@code [fromIndex, toIndex)}. + * + * @param fromIndex Lower-bound (inclusive) of the sub-range. + * @param toIndex Upper-bound (exclusive) of the sub-range. + * @param k Indices. + * @throws IndexOutOfBoundsException if any index is out of bounds + */ + static void checkIndices(int fromIndex, int toIndex, int[] k) { + for (final int i : k) { + checkIndex(fromIndex, toIndex, i); + } + } + + /** + * Checks if the {@code index} is within the half-open interval {@code [fromIndex, toIndex)}. + * + * @param fromIndex Lower-bound (inclusive) of the sub-range. + * @param toIndex Upper-bound (exclusive) of the sub-range. + * @param index Index. + * @throws IndexOutOfBoundsException if the index is out of bounds + */ + static void checkIndex(int fromIndex, int toIndex, int index) { + if (index < fromIndex || index >= toIndex) { + throw new IndexOutOfBoundsException( + msgIndexOutOfBounds(fromIndex, toIndex, index)); + } + } + + // Message formatting moved to separate methods to assist inlining of the validation methods. + + /** + * Format a message when range [from, to) is not entirely within the length. + * + * @param fromIndex Lower-bound (inclusive) of the sub-range. + * @param toIndex Upper-bound (exclusive) of the sub-range. + * @param length Upper-bound (exclusive) of the range. + * @return the message + */ + private static String msgRangeOutOfBounds(int fromIndex, int toIndex, int length) { + return String.format("Range [%d, %d) out of bounds for length %d", fromIndex, toIndex, length); + } + + /** + * Format a message when index is not within range [from, to). + * + * @param fromIndex Lower-bound (inclusive) of the sub-range. + * @param toIndex Upper-bound (exclusive) of the sub-range. + * @param index Index. + * @return the message + */ + private static String msgIndexOutOfBounds(int fromIndex, int toIndex, int index) { + return String.format("Index %d out of bounds for range [%d, %d)", index, fromIndex, toIndex); + } +} diff --git a/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/KeyUpdatingInterval.java b/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/KeyUpdatingInterval.java new file mode 100644 index 000000000..123c4ba47 --- /dev/null +++ b/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/KeyUpdatingInterval.java @@ -0,0 +1,279 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.arrays; + +/** + * An {@link UpdatingInterval} backed by an array of ordered keys. + * + * @since 1.2 + */ +final class KeyUpdatingInterval implements UpdatingInterval { + /** Size to use a scan of the keys when splitting instead of binary search. + * Note binary search has an overhead on small size due to the random left/right + * branching per iteration. It is much faster on very large sizes. */ + private static final int SCAN_SIZE = 256; + + /** The ordered keys. */ + private final int[] keys; + /** Index of the left key. */ + private int l; + /** Index of the right key. */ + private int r; + + /** + * Create an instance with the provided {@code indices}. + * + *

Warning: Indices must be sorted and distinct. + * + * @param indices Indices. + * @param n Number of indices. + */ + KeyUpdatingInterval(int[] indices, int n) { + this(indices, 0, n - 1); + } + + /** + * @param indices Indices. + * @param l Index of left key. + * @param r Index of right key. + */ + private KeyUpdatingInterval(int[] indices, int l, int r) { + keys = indices; + this.l = l; + this.r = r; + } + + @Override + public int left() { + return keys[l]; + } + + @Override + public int right() { + return keys[r]; + } + + @Override + public int updateLeft(int k) { + // Assume left < k <= right (i.e. we must move left at least 1) + // Search using a scan on the assumption that k is close to the end + int i = l; + do { + ++i; + } while (keys[i] < k); + l = i; + return keys[i]; + } + + @Override + public int updateRight(int k) { + // Assume left <= k < right (i.e. we must move right at least 1) + // Search using a scan on the assumption that k is close to the end + int i = r; + do { + --i; + } while (keys[i] > k); + r = i; + return keys[i]; + } + + @Override + public UpdatingInterval splitLeft(int ka, int kb) { + // left < ka <= kb < right + + // Find the new left bound for the upper interval. + // Switch to a linear scan if length is small. + int i; + if (r - l < SCAN_SIZE) { + i = r; + do { + --i; + } while (keys[i] > kb); + } else { + // Binary search + i = searchLessOrEqual(keys, l, r, kb); + } + final int lowerLeft = l; + l = i + 1; + + // Find the new right bound for the lower interval using a scan since a + // typical use case has ka == kb and this is faster than a second binary search. + while (keys[i] >= ka) { + --i; + } + // return left + return new KeyUpdatingInterval(keys, lowerLeft, i); + } + + /** + * Return the current number of indices in the interval. + * + * @return the size + */ + int size() { + return r - l + 1; + } + + /** + * Search the data for the largest index {@code i} where {@code a[i]} is + * less-than-or-equal to the {@code key}; else return {@code left - 1}. + *

+     * a[i] <= k    :   left <= i <= right, or (left - 1)
+     * 
+ * + *

The data is assumed to be in ascending order, otherwise the behaviour is undefined. + * If the range contains multiple elements with the {@code key} value, the result index + * may be any that match. + * + *

This is similar to using {@link java.util.Arrays#binarySearch(int[], int, int, int) + * Arrays.binarySearch}. The method differs in: + *

+ * + *

An equivalent use of binary search is: + *

{@code
+     * int i = Arrays.binarySearch(a, left, right + 1, k);
+     * if (i < 0) {
+     *     i = ~i - 1;
+     * }
+     * }
+ * + *

This specialisation avoids the caller checking the binary search result for the use + * case when the presence or absence of a key is not important; only that the returned + * index for an absence of a key is the largest index. When used on unique keys this + * method can be used to update an upper index so all keys are known to be below a key: + * + *

{@code
+     * int[] keys = ...
+     * // [i0, i1] contains all keys
+     * int i0 = 0;
+     * int i1 = keys.length - 1;
+     * // Update: [i0, i1] contains all keys <= k
+     * i1 = searchLessOrEqual(keys, i0, i1, k);
+     * }
+ * + * @param a Data. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param k Key. + * @return largest index {@code i} such that {@code a[i] <= k}, or {@code left - 1} if no + * such index exists + */ + static int searchLessOrEqual(int[] a, int left, int right, int k) { + int l = left; + int r = right; + while (l <= r) { + // Middle value + final int m = (l + r) >>> 1; + final int v = a[m]; + // Test: + // l------m------r + // v k update left + // k v update right + if (v < k) { + l = m + 1; + } else if (v > k) { + r = m - 1; + } else { + // Equal + return m; + } + } + // Return largest known value below: + // r is always moved downward when a middle index value is too high + return r; + } + + /** + * Search the data for the smallest index {@code i} where {@code a[i]} is + * greater-than-or-equal to the {@code key}; else return {@code right + 1}. + *
+     * a[i] >= k      :   left <= i <= right, or (right + 1)
+     * 
+ * + *

The data is assumed to be in ascending order, otherwise the behaviour is undefined. + * If the range contains multiple elements with the {@code key} value, the result index + * may be any that match. + * + *

This is similar to using {@link java.util.Arrays#binarySearch(int[], int, int, int) + * Arrays.binarySearch}. The method differs in: + *

+ * + *

An equivalent use of binary search is: + *

{@code
+     * int i = Arrays.binarySearch(a, left, right + 1, k);
+     * if (i < 0) {
+     *     i = ~i;
+     * }
+     * }
+ * + *

This specialisation avoids the caller checking the binary search result for the use + * case when the presence or absence of a key is not important; only that the returned + * index for an absence of a key is the smallest index. When used on unique keys this + * method can be used to update a lower index so all keys are known to be above a key: + * + *

{@code
+     * int[] keys = ...
+     * // [i0, i1] contains all keys
+     * int i0 = 0;
+     * int i1 = keys.length - 1;
+     * // Update: [i0, i1] contains all keys >= k
+     * i0 = searchGreaterOrEqual(keys, i0, i1, k);
+     * }
+ * + * @param a Data. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param k Key. + * @return largest index {@code i} such that {@code a[i] >= k}, or {@code right + 1} if no + * such index exists + */ + static int searchGreaterOrEqual(int[] a, int left, int right, int k) { + int l = left; + int r = right; + while (l <= r) { + // Middle value + final int m = (l + r) >>> 1; + final int v = a[m]; + // Test: + // l------m------r + // v k update left + // k v update right + if (v < k) { + l = m + 1; + } else if (v > k) { + r = m - 1; + } else { + // Equal + return m; + } + } + // Smallest known value above + // l is always moved upward when a middle index value is too low + return l; + } +} diff --git a/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/QuickSelect.java b/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/QuickSelect.java new file mode 100644 index 000000000..27bef7ef4 --- /dev/null +++ b/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/QuickSelect.java @@ -0,0 +1,1633 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.arrays; + +/** + * Partition array data. + * + *

Note: Requires that the floating-point data contains no NaN values; sorting does not + * respect the order of signed zeros imposed by {@link Double#compare(double, double)}; + * mixed signed zeros may be destroyed (the mixture updated during partitioning). The + * caller is responsible for counting a mixture of signed zeros and restoring them if + * required. + * + * @see Selection + * @since 1.2 + */ +final class QuickSelect { + // Implementation Notes + // + // Selection is performed using a quickselect variant to recursively divide the range + // to select the target index, or indices. Partition sizes or recursion are monitored + // will fall-backs on poor convergence of a linearselect (single index) or heapselect. + // + // Many implementations were tested, each with strengths and weaknesses on different + // input data containing random elements, repeat elements, elements with repeat + // patterns, and constant elements. The final implementation performs well across data + // types for single and multiple indices with no obvious weakness. + // See: o.a.c.numbers.examples.jmh.arrays for benchmarking implementations. + // + // Single indices are selected using a quickselect adaptive method based on Alexandrescu. + // The algorithm is a quickselect around a pivot identified using a + // sample-of-sample-of-samples created from the entire range data. This pivot will + // have known lower and upper margins and ensures elimination of a minimum fraction of + // data at each step. To increase speed the pivot can be identified using less of the data + // but without margin guarantees (sampling mode). The algorithm monitors partition sizes + // against the known margins. If the reduction in the partition size is not large enough + // then the algorithm can disable sampling mode and ensure linear performance by removing + // a set fraction of the data each iteration. + // + // Modifications from Alexandrescu are: + // 1. Initialise sampling mode using the Floyd-Rivest (FR) SELECT algorithm. + // 2. Adaption is adjusted to force use of the lower margin in the far-step method when + // sampling is disabled. + // 3. Change the far-step method to a min-of-4 then median-of-3 into the 2nd 12th-tile. + // The original method uses a lower-median-of-4, min-of-3 into the 4th 12th-tile. + // 4. Position the sample around the target k when in sampling mode for the non-far-step + // methods. + // + // The far step method is used when target k is within 1/12 of the end of the data A. + // The differences in the far-step method are: + // - The upper margin when not sampling is 8/24 vs. 9/24; the lower margin remains at 1/12. + // - The position of the sample is closer to the expected location of k < |A|/12. + // - Sampling mode uses a median-of-3 with adaptive k, matching the other step methods. + // Note the original min-of-3 sample is more likely to create a pivot too small if used + // with adaption of k leaving k in the larger partition and a wasted iteration. + // + // The Floyd-Rivest (FR) SELECT algorithm is preferred for sampling over using quickselect + // adaptive sampling. It uses a smaller sample and has improved heuristics to place the sample + // pivot. However the FR sample is a small range of the data and pivot selection can be poor + // if the sample is not representative. This can be mitigated by creating a random sample + // of the entire range for the pivot selection. This implementation does not use random + // sampling for the FR mode. Performance is identical on random data (randomisation is a + // negligible overhead) and faster on sorted data. Any data not suitable for the FR algorithm + // are immediately switched to the quickselect adaptive algorithm with sampling. Performance + // across a range of data shows this strategy is approximately mid-way in performance between + // FR with random sampling, and quickselect adaptive in sampling mode. The benefit is that + // sorted or partially partitioned data are not intentionally unordered as the method will + // only move elements known to be incorrectly placed in the array. + // + // Multiple indices are selected using a dual-pivot partition method by + // Yaroslavskiy to divide the interval containing the indices. When indices are effectively + // a single index the method can switch to the single index selection to use the FR algorithm. + // Alternative schemes to partition multiple indices are to repeat call single index select + // with cached pivots, or without cached pivots if processing indices in order as the previous + // index brackets the range for the next search. Caching pivots is the most effective + // alternative. It requires storing all pivots during select, and using the cache to look-up + // the search bounds (sub-range) for each target index. This requires 2n searches for n indices. + // All pivots must be stored to avoid destroying previously partitioned data on repeat entry + // to the array. The current scheme inverts this by requiring at most n-1 divides of the + // indices during recursion and has the advantage of tracking recursion depth during selection + // for each sub-range. Division of indices is a small overhead for the common case where + // the number of indices is far smaller than the size of the data. + // + // Dual-pivot paritioning adapted from Yaroslavskiy + // http://codeblab.com/wp-content/uploads/2009/09/DualPivotQuicksort.pdf + // + // Modified to allow partial sorting (partitioning): + // - Ignore insertion sort for tiny array (handled by calling code). + // - Ignore recursive calls for a full sort (handled by calling code). + // - Change to fast-forward over initial ascending / descending runs. + // - Change to fast-forward great when v > v2 and either break the sorting + // loop, or move a[great] direct to the correct location. + // - Change to use the 2nd and 4th of 5 elements for the pivots. + // - Identify a large central region using ~5/8 of the length to trigger search for + // equal values. + // + // For some indices and data a full sort of the data will be faster; this is impossible to + // predict on unknown data input and attempts to analyse the indices and data impact + // performance for the majority of use cases where sorting is not a suitable choice. + // Use of the sortselect finisher allows the current multiple indices method to degrade + // to a (non-optimised) dual-pivot quicksort (see below). + // + // heapselect vs sortselect + // + // Quickselect can switch to an alternative when: the range is very small + // (e.g. insertion sort); or the target index is close to the end (e.g. heapselect). + // Small ranges and a target index close to the end are handled using a hybrid of insertion + // sort and selection (sortselect). This is faster than heapselect for small distance from + // the edge (m) for a single index and has the advantage of sorting all upstream values from + // the target index (heapselect requires push-down of each successive value to sort). This + // allows the dual-pivot quickselect on multiple indices that saturate the range to degrade + // to a (non-optimised) dual-pivot quicksort. However sortselect is worst case Order(m * (r-l)) + // for range [l, r] so cannot be used when quickselect fails to converge as m may be very large. + // Thus heapselect is used as the stopper algorithm when quickselect progress is slow on + // multiple indices. If heapselect is used for small range handling the performance on + // saturated indices is significantly slower. Hence the presence of two final selection + // methods for different purposes. + + /** Sampling mode using Floyd-Rivest sampling. */ + static final int MODE_FR_SAMPLING = -1; + /** Sampling mode. */ + static final int MODE_SAMPLING = 0; + /** No sampling but use adaption of the target k. */ + static final int MODE_ADAPTION = 1; + /** No sampling and no adaption of target k (strict margins). */ + static final int MODE_STRICT = 2; + + /** Minimum size for sortselect. + * Below this perform a sort rather than selection. This is used to avoid + * sort select on tiny data. */ + private static final int MIN_SORTSELECT_SIZE = 4; + /** Single-pivot sortselect size for quickselect adaptive. Note that quickselect adaptive + * recursively calls quickselect so very small lengths are included with an initial medium + * length. Using lengths of 1023-5 and 2043-53 indicate optimum performance around 20-30. + * Note: The expand partition function assumes a sample of at least length 2 as each end + * of the sample is used as a sentinel; this imposes a minimum length of 24 on the range + * to ensure it contains a 12-th tile of length 2. Thus the absolute minimum for the + * distance from the edge is 12. */ + private static final int LINEAR_SORTSELECT_SIZE = 24; + /** Dual-pivot sortselect size for the distance of a single k from the edge of the + * range length n. Benchmarking in range [81+81, 243+243] suggests a value of ~20 (or + * higher on some hardware). Ranges are chosen based on third interval spacing between + * powers of 3. + * + *

Sortselect is faster at this small size than heapselect. A second advantage is + * that all indices closer to the edge than the target index are also sorted. This + * allows selection of multiple close indices to be performed with effectively the + * same speed. High density indices will result in recursion to very short fragments + * which also trigger use of sort select. The threshold for sorting short lengths is + * configured in {@link #dualPivotSortSelectSize(int, int)}. */ + private static final int DP_SORTSELECT_SIZE = 20; + /** Threshold to use Floyd-Rivest sub-sampling. This partitions a sample of the data to + * identify a pivot so that the target element is in the smaller set after partitioning. + * The original FR paper used 600 otherwise reverted to the target index as the pivot. + * This implementation reverts to quickselect adaptive which increases robustness + * at small size on a variety of data and allows raising the original FR threshold. */ + private static final int FR_SAMPLING_SIZE = 1200; + + /** Increment used for the recursion counter. The counter will overflow to negative when + * recursion has exceeded the maximum level. The counter is maintained in the upper bits + * of the dual-pivot control flags. */ + private static final int RECURSION_INCREMENT = 1 << 20; + /** Mask to extract the sort select size from the dual-pivot control flags. Currently + * the bits below those used for the recursion counter are only used for the sort select size + * so this can use a mask with all bits below the increment. */ + private static final int SORTSELECT_MASK = RECURSION_INCREMENT - 1; + + /** Threshold to use repeated step left: 7 / 16. */ + private static final double STEP_LEFT = 0.4375; + /** Threshold to use repeated step right: 9 / 16. */ + private static final double STEP_RIGHT = 0.5625; + /** Threshold to use repeated step far-left: 1 / 12. */ + private static final double STEP_FAR_LEFT = 0.08333333333333333; + /** Threshold to use repeated step far-right: 11 / 12. */ + private static final double STEP_FAR_RIGHT = 0.9166666666666666; + + /** No instances. */ + private QuickSelect() {} + + /** + * Partition the elements between {@code ka} and {@code kb} using a heap select + * algorithm. It is assumed {@code left <= ka <= kb <= right}. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param ka Lower index to select. + * @param kb Upper index to select. + */ + static void heapSelect(double[] a, int left, int right, int ka, int kb) { + if (right <= left) { + return; + } + // Use the smallest heap + if (kb - left < right - ka) { + heapSelectLeft(a, left, right, ka, kb); + } else { + heapSelectRight(a, left, right, ka, kb); + } + } + + /** + * Partition the elements between {@code ka} and {@code kb} using a heap select + * algorithm. It is assumed {@code left <= ka <= kb <= right}. + * + *

For best performance this should be called with {@code k} in the lower + * half of the range. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param ka Lower index to select. + * @param kb Upper index to select. + */ + static void heapSelectLeft(double[] a, int left, int right, int ka, int kb) { + // Create a max heap in-place in [left, k], rooted at a[left] = max + // |l|-max-heap-|k|--------------| + // Build the heap using Floyd's heap-construction algorithm for heap size n. + // Start at parent of the last element in the heap (k), + // i.e. start = parent(n-1) : parent(c) = floor((c - 1) / 2) : c = k - left + int end = kb + 1; + for (int p = left + ((kb - left - 1) >> 1); p >= left; p--) { + maxHeapSiftDown(a, a[p], p, left, end); + } + // Scan the remaining data and insert + // Mitigate worst case performance on descending data by backward sweep + double max = a[left]; + for (int i = right + 1; --i > kb;) { + final double v = a[i]; + if (v < max) { + a[i] = max; + maxHeapSiftDown(a, v, left, left, end); + max = a[left]; + } + } + // Partition [ka, kb] + // |l|-max-heap-|k|--------------| + // | <-swap-> | then sift down reduced size heap + // Avoid sifting heap of size 1 + final int last = Math.max(left, ka - 1); + while (--end > last) { + maxHeapSiftDown(a, a[end], left, left, end); + a[end] = max; + max = a[left]; + } + } + + /** + * Sift the element down the max heap. + * + *

Assumes {@code root <= p < end}, i.e. the max heap is above root. + * + * @param a Heap data. + * @param v Value to sift. + * @param p Start position. + * @param root Root of the heap. + * @param end End of the heap (exclusive). + */ + private static void maxHeapSiftDown(double[] a, double v, int p, int root, int end) { + // child2 = root + 2 * (parent - root) + 2 + // = 2 * parent - root + 2 + while (true) { + // Right child + int c = (p << 1) - root + 2; + if (c > end) { + // No left child + break; + } + // Use the left child if right doesn't exist, or it is greater + if (c == end || a[c] < a[c - 1]) { + --c; + } + if (v >= a[c]) { + // Parent greater than largest child - done + break; + } + // Swap and descend + a[p] = a[c]; + p = c; + } + a[p] = v; + } + + /** + * Partition the elements between {@code ka} and {@code kb} using a heap select + * algorithm. It is assumed {@code left <= ka <= kb <= right}. + * + *

For best performance this should be called with {@code k} in the upper + * half of the range. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param ka Lower index to select. + * @param kb Upper index to select. + */ + static void heapSelectRight(double[] a, int left, int right, int ka, int kb) { + // Create a min heap in-place in [k, right], rooted at a[right] = min + // |--------------|k|-min-heap-|r| + // Build the heap using Floyd's heap-construction algorithm for heap size n. + // Start at parent of the last element in the heap (k), + // i.e. start = parent(n-1) : parent(c) = floor((c - 1) / 2) : c = right - k + int end = ka - 1; + for (int p = right - ((right - ka - 1) >> 1); p <= right; p++) { + minHeapSiftDown(a, a[p], p, right, end); + } + // Scan the remaining data and insert + // Mitigate worst case performance on descending data by backward sweep + double min = a[right]; + for (int i = left - 1; ++i < ka;) { + final double v = a[i]; + if (v > min) { + a[i] = min; + minHeapSiftDown(a, v, right, right, end); + min = a[right]; + } + } + // Partition [ka, kb] + // |--------------|k|-min-heap-|r| + // | <-swap-> | then sift down reduced size heap + // Avoid sifting heap of size 1 + final int last = Math.min(right, kb + 1); + while (++end < last) { + minHeapSiftDown(a, a[end], right, right, end); + a[end] = min; + min = a[right]; + } + } + + /** + * Sift the element down the min heap. + * + *

Assumes {@code root >= p > end}, i.e. the max heap is below root. + * + * @param a Heap data. + * @param v Value to sift. + * @param p Start position. + * @param root Root of the heap. + * @param end End of the heap (exclusive). + */ + private static void minHeapSiftDown(double[] a, double v, int p, int root, int end) { + // child2 = root - 2 * (root - parent) - 2 + // = 2 * parent - root - 2 + while (true) { + // Right child + int c = (p << 1) - root - 2; + if (c < end) { + // No left child + break; + } + // Use the left child if right doesn't exist, or it is less + if (c == end || a[c] > a[c + 1]) { + ++c; + } + if (v <= a[c]) { + // Parent less than smallest child - done + break; + } + // Swap and descend + a[p] = a[c]; + p = c; + } + a[p] = v; + } + + /** + * Partition the elements between {@code ka} and {@code kb} using a sort select + * algorithm. It is assumed {@code left <= ka <= kb <= right}. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param ka Lower index to select. + * @param kb Upper index to select. + */ + static void sortSelect(double[] a, int left, int right, int ka, int kb) { + // Combine the test for right <= left with + // avoiding the overhead of sort select on tiny data. + if (right - left <= MIN_SORTSELECT_SIZE) { + Sorting.sort(a, left, right); + return; + } + // Sort the smallest side + if (kb - left < right - ka) { + sortSelectLeft(a, left, right, kb); + } else { + sortSelectRight(a, left, right, ka); + } + } + + /** + * Partition the minimum {@code n} elements below {@code k} where + * {@code n = k - left + 1}. Uses an insertion sort algorithm. + * + *

Works with any {@code k} in the range {@code left <= k <= right} + * and performs a full sort of the range below {@code k}. + * + *

For best performance this should be called with + * {@code k - left < right - k}, i.e. + * to partition a value in the lower half of the range. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param k Index to select. + */ + static void sortSelectLeft(double[] a, int left, int right, int k) { + // Sort + for (int i = left; ++i <= k;) { + final double v = a[i]; + // Move preceding higher elements above (if required) + if (v < a[i - 1]) { + int j = i; + while (--j >= left && v < a[j]) { + a[j + 1] = a[j]; + } + a[j + 1] = v; + } + } + // Scan the remaining data and insert + // Mitigate worst case performance on descending data by backward sweep + double m = a[k]; + for (int i = right + 1; --i > k;) { + final double v = a[i]; + if (v < m) { + a[i] = m; + int j = k; + while (--j >= left && v < a[j]) { + a[j + 1] = a[j]; + } + a[j + 1] = v; + m = a[k]; + } + } + } + + /** + * Partition the maximum {@code n} elements above {@code k} where + * {@code n = right - k + 1}. Uses an insertion sort algorithm. + * + *

Works with any {@code k} in the range {@code left <= k <= right} + * and can be used to perform a full sort of the range above {@code k}. + * + *

For best performance this should be called with + * {@code k - left > right - k}, i.e. + * to partition a value in the upper half of the range. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param k Index to select. + */ + static void sortSelectRight(double[] a, int left, int right, int k) { + // Sort + for (int i = right; --i >= k;) { + final double v = a[i]; + // Move succeeding lower elements below (if required) + if (v > a[i + 1]) { + int j = i; + while (++j <= right && v > a[j]) { + a[j - 1] = a[j]; + } + a[j - 1] = v; + } + } + // Scan the remaining data and insert + // Mitigate worst case performance on descending data by backward sweep + double m = a[k]; + for (int i = left - 1; ++i < k;) { + final double v = a[i]; + if (v > m) { + a[i] = m; + int j = k; + while (++j <= right && v > a[j]) { + a[j - 1] = a[j]; + } + a[j - 1] = v; + m = a[k]; + } + } + } + + /** + * Partition the array such that index {@code k} corresponds to its correctly + * sorted value in the equivalent fully sorted array. + * + *

Assumes {@code k} is a valid index into [left, right]. + * + * @param a Values. + * @param left Lower bound of data (inclusive). + * @param right Upper bound of data (inclusive). + * @param k Index. + */ + static void select(double[] a, int left, int right, int k) { + quickSelectAdaptive(a, left, right, k, k, new int[1], MODE_FR_SAMPLING); + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. + * + *

The count of the number of used indices is returned. If the keys are sorted in-place, + * the count is returned as a negative. + * + * @param a Values. + * @param left Lower bound of data (inclusive). + * @param right Upper bound of data (inclusive). + * @param k Indices (may be destructively modified). + * @param n Count of indices. + * @return the count of used indices + * @throws IndexOutOfBoundsException if any index {@code k} is not within the + * sub-range {@code [left, right]} + */ + static int select(double[] a, int left, int right, int[] k, int n) { + if (n < 1) { + return 0; + } + if (n == 1) { + quickSelectAdaptive(a, left, right, k[0], k[0], new int[1], MODE_FR_SAMPLING); + return -1; + } + + // Interval creation validates the indices are in [left, right] + final UpdatingInterval keys = IndexSupport.createUpdatingInterval(left, right, k, n); + + // Save number of used indices + final int count = IndexSupport.countIndices(keys, n); + + // Note: If the keys are not separated then they are effectively a single key. + // Any split of keys separated by the sort select size + // will be finished on the next iteration. + final int k1 = keys.left(); + final int kn = keys.right(); + if (kn - k1 < DP_SORTSELECT_SIZE) { + quickSelectAdaptive(a, left, right, k1, kn, new int[1], MODE_FR_SAMPLING); + } else { + // Dual-pivot mode with small range sort length configured using index density + dualPivotQuickSelect(a, left, right, keys, dualPivotFlags(left, right, k1, kn)); + } + return count; + } + + /** + * Partition the array such that indices {@code k} correspond to their + * correctly sorted value in the equivalent fully sorted array. + * + *

For all indices {@code [ka, kb]} and any index {@code i}: + * + *

{@code
+     * data[i < ka] <= data[ka] <= data[kb] <= data[kb < i]
+     * }
+ * + *

This function accepts indices {@code [ka, kb]} that define the + * range of indices to partition. It is expected that the range is small. + * + *

The {@code flags} are used to control the sampling mode and adaption of + * the index within the sample. + * + *

Returns the bounds containing {@code [ka, kb]}. These may be lower/higher + * than the keys if equal values are present in the data. + * + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param ka First key of interest. + * @param kb Last key of interest. + * @param bounds Upper bound of the range containing {@code [ka, kb]} (inclusive). + * @param flags Adaption flags. + * @return Lower bound of the range containing {@code [ka, kb]} (inclusive). + */ + static int quickSelectAdaptive(double[] a, int left, int right, int ka, int kb, + int[] bounds, int flags) { + int l = left; + int r = right; + int m = flags; + while (true) { + // Select when ka and kb are close to the same end + // |l|-----|ka|kkkkkkkk|kb|------|r| + if (Math.min(kb - l, r - ka) < LINEAR_SORTSELECT_SIZE) { + sortSelect(a, l, r, ka, kb); + bounds[0] = kb; + return ka; + } + + // Only target ka; kb is assumed to be close + int p0; + final int n = r - l; + // f in [0, 1] + final double f = (double) (ka - l) / n; + // Record the larger margin (start at 1/4) to create the estimated size. + // step L R + // far left 1/12 1/3 (use 1/4 + 1/32 + 1/64 ~ 0.328) + // left 1/6 1/4 + // middle 2/9 2/9 (use 1/4 - 1/32 ~ 0.219) + int margin = n >> 2; + if (m < MODE_SAMPLING && r - l > FR_SAMPLING_SIZE) { + // Floyd-Rivest sample step uses the same margins + p0 = sampleStep(a, l, r, ka, bounds); + if (f <= STEP_FAR_LEFT || f >= STEP_FAR_RIGHT) { + margin += (n >> 5) + (n >> 6); + } else if (f > STEP_LEFT && f < STEP_RIGHT) { + margin -= n >> 5; + } + } else if (f <= STEP_LEFT) { + if (f <= STEP_FAR_LEFT) { + margin += (n >> 5) + (n >> 6); + p0 = repeatedStepFarLeft(a, l, r, ka, bounds, m); + } else { + p0 = repeatedStepLeft(a, l, r, ka, bounds, m); + } + } else if (f >= STEP_RIGHT) { + if (f >= STEP_FAR_RIGHT) { + margin += (n >> 5) + (n >> 6); + p0 = repeatedStepFarRight(a, l, r, ka, bounds, m); + } else { + p0 = repeatedStepRight(a, l, r, ka, bounds, m); + } + } else { + margin -= n >> 5; + p0 = repeatedStep(a, l, r, ka, bounds, m); + } + + // Note: Here we expect [ka, kb] to be small and splitting is unlikely. + // p0 p1 + // |l|--|ka|kkkk|kb|--|P|-------------------|r| + // |l|----------------|P|--|ka|kkk|kb|------|r| + // |l|-----------|ka|k|P|k|kb|--------------|r| + final int p1 = bounds[0]; + if (kb < p0) { + // Entirely on left side + r = p0 - 1; + } else if (ka > p1) { + // Entirely on right side + l = p1 + 1; + } else { + // Pivot splits [ka, kb]. Expect ends to be close to the pivot and finish. + // Here we set the bounds for use after median-of-medians pivot selection. + // In the event there are many equal values this allows collecting those + // known to be equal together when moving around the medians sample. + if (kb > p1) { + sortSelectLeft(a, p1 + 1, r, kb); + bounds[0] = kb; + } + if (ka < p0) { + sortSelectRight(a, l, p0 - 1, ka); + p0 = ka; + } + return p0; + } + // Update mode based on target partition size + if (r - l > n - margin) { + m++; + } + } + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Partitions a Floyd-Rivest sample around a pivot offset so that the input {@code k} will + * fall in the smaller partition when the entire range is partitioned. + * + *

Assumes the range {@code r - l} is large. + * + * @param a Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param k Target index. + * @param upper Upper bound (inclusive) of the pivot range. + * @return Lower bound (inclusive) of the pivot range. + */ + private static int sampleStep(double[] a, int l, int r, int k, int[] upper) { + // Floyd-Rivest: use SELECT recursively on a sample of size S to get an estimate + // for the (k-l+1)-th smallest element into a[k], biased slightly so that the + // (k-l+1)-th element is expected to lie in the smaller set after partitioning. + final int n = r - l + 1; + final int ith = k - l + 1; + final double z = Math.log(n); + // sample size = 0.5 * n^(2/3) + final double s = 0.5 * Math.exp(0.6666666666666666 * z); + final double sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * Integer.signum(ith - (n >> 1)); + final int ll = Math.max(l, (int) (k - ith * s / n + sd)); + final int rr = Math.min(r, (int) (k + (n - ith) * s / n + sd)); + // Sample recursion restarts from [ll, rr] + final int p = quickSelectAdaptive(a, ll, rr, k, k, upper, MODE_FR_SAMPLING); + return expandPartition(a, l, r, ll, rr, p, upper[0], upper); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Assumes the range {@code r - l >= 8}; the caller is responsible for selection on a smaller + * range. If using a 12th-tile for sampling then assumes {@code r - l >= 11}. + * + *

Uses the Chen and Dumitrescu repeated step median-of-medians-of-medians algorithm + * with the median of 3 then median of 3; the final sample is placed in the + * 5th 9th-tile; the pivot chosen from the sample is adaptive using the input {@code k}. + * + *

Given a pivot in the middle of the sample this has margins of 2/9 and 2/9. + * + * @param a Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param k Target index. + * @param upper Upper bound (inclusive) of the pivot range. + * @param flags Control flags. + * @return Lower bound (inclusive) of the pivot range. + */ + private static int repeatedStep(double[] a, int l, int r, int k, int[] upper, int flags) { + // Adapted from Alexandrescu (2016), algorithm 8. + int fp; + int s; + int p; + if (flags <= MODE_SAMPLING) { + // Median into a 12th-tile + fp = (r - l + 1) / 12; + // Position the sample around the target k + s = k - mapDistance(k - l, l, r, fp); + p = k; + } else { + // i in tertile [3f':6f') + fp = (r - l + 1) / 9; + final int f3 = 3 * fp; + final int end = l + (f3 << 1); + for (int i = l + f3; i < end; i++) { + Sorting.sort3(a, i - f3, i, i + f3); + } + // 5th 9th-tile: [4f':5f') + s = l + (fp << 2); + // No adaption uses the middle to enforce strict margins + p = s + (flags == MODE_ADAPTION ? mapDistance(k - l, l, r, fp) : (fp >>> 1)); + } + final int e = s + fp - 1; + for (int i = s; i <= e; i++) { + Sorting.sort3(a, i - fp, i, i + fp); + } + p = quickSelectAdaptive(a, s, e, p, p, upper, MODE_FR_SAMPLING); + return expandPartition(a, l, r, s, e, p, upper[0], upper); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Assumes the range {@code r - l >= 11}; the caller is responsible for selection on a smaller + * range. + * + *

Uses the Chen and Dumitrescu repeated step median-of-medians-of-medians algorithm + * with the lower median of 4 then either median of 3 with the final sample placed in the + * 5th 12th-tile, or min of 3 with the final sample in the 4th 12th-tile; + * the pivot chosen from the sample is adaptive using the input {@code k}. + * + *

Given a pivot in the middle of the sample this has margins of 1/6 and 1/4. + * + * @param a Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param k Target index. + * @param upper Upper bound (inclusive) of the pivot range. + * @param flags Control flags. + * @return Lower bound (inclusive) of the pivot range. + */ + private static int repeatedStepLeft(double[] a, int l, int r, int k, int[] upper, int flags) { + // Adapted from Alexandrescu (2016), algorithm 9. + int fp; + int s; + int p; + if (flags <= MODE_SAMPLING) { + // Median into a 12th-tile + fp = (r - l + 1) / 12; + // Position the sample around the target k + // Avoid bounds error due to rounding as (k-l)/(r-l) -> 1/12 + s = Math.max(k - mapDistance(k - l, l, r, fp), l + fp); + p = k; + } else { + // i in 2nd quartile + final int f = (r - l + 1) >> 2; + final int f2 = f + f; + final int end = l + f2; + for (int i = l + f; i < end; i++) { + Sorting.lowerMedian4(a, i - f, i, i + f, i + f2); + } + // i in 5th 12th-tile + fp = f / 3; + s = l + f + fp; + // No adaption uses the middle to enforce strict margins + p = s + (flags == MODE_ADAPTION ? mapDistance(k - l, l, r, fp) : (fp >>> 1)); + } + final int e = s + fp - 1; + for (int i = s; i <= e; i++) { + Sorting.sort3(a, i - fp, i, i + fp); + } + p = quickSelectAdaptive(a, s, e, p, p, upper, MODE_FR_SAMPLING); + return expandPartition(a, l, r, s, e, p, upper[0], upper); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Assumes the range {@code r - l >= 11}; the caller is responsible for selection on a smaller + * range. + * + *

Uses the Chen and Dumitrescu repeated step median-of-medians-of-medians algorithm + * with the upper median of 4 then either median of 3 with the final sample placed in the + * 8th 12th-tile, or max of 3 with the final sample in the 9th 12th-tile; + * the pivot chosen from the sample is adaptive using the input {@code k}. + * + *

Given a pivot in the middle of the sample this has margins of 1/4 and 1/6. + * + * @param a Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param k Target index. + * @param upper Upper bound (inclusive) of the pivot range. + * @param flags Control flags. + * @return Lower bound (inclusive) of the pivot range. + */ + private static int repeatedStepRight(double[] a, int l, int r, int k, int[] upper, int flags) { + // Mirror image repeatedStepLeft using upper median into 3rd quartile + int fp; + int e; + int p; + if (flags <= MODE_SAMPLING) { + // Median into a 12th-tile + fp = (r - l + 1) / 12; + // Position the sample around the target k + // Avoid bounds error due to rounding as (r-k)/(r-l) -> 11/12 + e = Math.min(k + mapDistance(r - k, l, r, fp), r - fp); + p = k; + } else { + // i in 3rd quartile + final int f = (r - l + 1) >> 2; + final int f2 = f + f; + final int end = r - f2; + for (int i = r - f; i > end; i--) { + Sorting.upperMedian4(a, i - f2, i - f, i, i + f); + } + // i in 8th 12th-tile + fp = f / 3; + e = r - f - fp; + // No adaption uses the middle to enforce strict margins + p = e - (flags == MODE_ADAPTION ? mapDistance(r - k, l, r, fp) : (fp >>> 1)); + } + final int s = e - fp + 1; + for (int i = s; i <= e; i++) { + Sorting.sort3(a, i - fp, i, i + fp); + } + p = quickSelectAdaptive(a, s, e, p, p, upper, MODE_FR_SAMPLING); + return expandPartition(a, l, r, s, e, p, upper[0], upper); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Assumes the range {@code r - l >= 11}; the caller is responsible for selection on a smaller + * range. + * + *

Uses the Chen and Dumitrescu repeated step median-of-medians-of-medians algorithm + * with the minimum of 4 then median of 3; the final sample is placed in the + * 2nd 12th-tile; the pivot chosen from the sample is adaptive using the input {@code k}. + * + *

Given a pivot in the middle of the sample this has margins of 1/12 and 1/3. + * + * @param a Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param k Target index. + * @param upper Upper bound (inclusive) of the pivot range. + * @param flags Control flags. + * @return Lower bound (inclusive) of the pivot range. + */ + private static int repeatedStepFarLeft(double[] a, int l, int r, int k, int[] upper, int flags) { + // Far step has been changed from the Alexandrescu (2016) step of lower-median-of-4, min-of-3 + // into the 4th 12th-tile to a min-of-4, median-of-3 into the 2nd 12th-tile. + // The differences are: + // - The upper margin when not sampling is 8/24 vs. 9/24; the lower margin remains at 1/12. + // - The position of the sample is closer to the expected location of k < |A| / 12. + // - Sampling mode uses a median-of-3 with adaptive k, matching the other step methods. + // A min-of-3 sample can create a pivot too small if used with adaption of k leaving + // k in the larger parition and a wasted iteration. + // - Adaption is adjusted to force use of the lower margin when not sampling. + int fp; + int s; + int p; + if (flags <= MODE_SAMPLING) { + // 2nd 12th-tile + fp = (r - l + 1) / 12; + s = l + fp; + // Use adaption + p = s + mapDistance(k - l, l, r, fp); + } else { + // i in 2nd quartile; min into i-f (1st quartile) + final int f = (r - l + 1) >> 2; + final int f2 = f + f; + final int end = l + f2; + for (int i = l + f; i < end; i++) { + if (a[i + f] < a[i - f]) { + final double u = a[i + f]; + a[i + f] = a[i - f]; + a[i - f] = u; + } + if (a[i + f2] < a[i]) { + final double v = a[i + f2]; + a[i + f2] = a[i]; + a[i] = v; + } + if (a[i] < a[i - f]) { + final double u = a[i]; + a[i] = a[i - f]; + a[i - f] = u; + } + } + // 2nd 12th-tile + fp = f / 3; + s = l + fp; + // Lower margin has 2(d+1) elements; d == (position in sample) - s + // Force k into the lower margin + p = s + ((k - l) >>> 1); + } + final int e = s + fp - 1; + for (int i = s; i <= e; i++) { + Sorting.sort3(a, i - fp, i, i + fp); + } + p = quickSelectAdaptive(a, s, e, p, p, upper, MODE_FR_SAMPLING); + return expandPartition(a, l, r, s, e, p, upper[0], upper); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Assumes the range {@code r - l >= 11}; the caller is responsible for selection on a smaller + * range. + * + *

Uses the Chen and Dumitrescu repeated step median-of-medians-of-medians algorithm + * with the maximum of 4 then median of 3; the final sample is placed in the + * 11th 12th-tile; the pivot chosen from the sample is adaptive using the input {@code k}. + * + *

Given a pivot in the middle of the sample this has margins of 1/3 and 1/12. + * + * @param a Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param k Target index. + * @param upper Upper bound (inclusive) of the pivot range. + * @param flags Control flags. + * @return Lower bound (inclusive) of the pivot range. + */ + private static int repeatedStepFarRight(double[] a, int l, int r, int k, int[] upper, int flags) { + // Mirror image repeatedStepFarLeft + int fp; + int e; + int p; + if (flags <= MODE_SAMPLING) { + // 11th 12th-tile + fp = (r - l + 1) / 12; + e = r - fp; + // Use adaption + p = e - mapDistance(r - k, l, r, fp); + } else { + // i in 3rd quartile; max into i+f (4th quartile) + final int f = (r - l + 1) >> 2; + final int f2 = f + f; + final int end = r - f2; + for (int i = r - f; i > end; i--) { + if (a[i - f] > a[i + f]) { + final double u = a[i - f]; + a[i - f] = a[i + f]; + a[i + f] = u; + } + if (a[i - f2] > a[i]) { + final double v = a[i - f2]; + a[i - f2] = a[i]; + a[i] = v; + } + if (a[i] > a[i + f]) { + final double u = a[i]; + a[i] = a[i + f]; + a[i + f] = u; + } + } + // 11th 12th-tile + fp = f / 3; + e = r - fp; + // Upper margin has 2(d+1) elements; d == e - (position in sample) + // Force k into the upper margin + p = e - ((r - k) >>> 1); + } + final int s = e - fp + 1; + for (int i = s; i <= e; i++) { + Sorting.sort3(a, i - fp, i, i + fp); + } + p = quickSelectAdaptive(a, s, e, p, p, upper, MODE_FR_SAMPLING); + return expandPartition(a, l, r, s, e, p, upper[0], upper); + } + + /** + * Expand a partition around a single pivot. Partitioning exchanges array + * elements such that all elements smaller than pivot are before it and all + * elements larger than pivot are after it. The central region is already + * partitioned. + * + *

{@code
+     * |l             |s   |p0 p1|   e|                r|
+     * |    ???       | 

P | ??? | + * }

+ * + *

This requires that {@code start != end}. However it handles + * {@code left == start} and/or {@code end == right}. + * + * @param a Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param start Start of the partition range (inclusive). + * @param end End of the partitioned range (inclusive). + * @param pivot0 Lower pivot location (inclusive). + * @param pivot1 Upper pivot location (inclusive). + * @param upper Upper bound (inclusive) of the pivot range [k1]. + * @return Lower bound (inclusive) of the pivot range [k0]. + */ + // package-private for testing + static int expandPartition(double[] a, int left, int right, int start, int end, + int pivot0, int pivot1, int[] upper) { + // 3-way partition of the data using a pivot value into + // less-than, equal or greater-than. + // Based on Sedgewick's Bentley-McIroy partitioning: always swap i<->j then + // check for equal to the pivot and move again. + // + // Move sentinels from start and end to left and right. Scan towards the + // sentinels until >=,<=. Swap then move == to the pivot region. + // <-i j-> + // |l | | |p0 p1| | | r| + // |>=| ??? | < | == | > | ??? |<=| + // + // When either i or j reach the edge perform finishing loop. + // Finish loop for a[j] <= v replaces j with p1+1, optionally moves value + // to p0 for < and updates the pivot range p1 (and optionally p0): + // j-> + // |l |p0 p1| | | r| + // | < | == | > | ??? |<=| + + final double v = a[pivot0]; + // Use start/end as sentinels (requires start != end) + double vi = a[start]; + double vj = a[end]; + a[start] = a[left]; + a[end] = a[right]; + a[left] = vj; + a[right] = vi; + + int i = start + 1; + int j = end - 1; + + // Positioned for pre-in/decrement to write to pivot region + int p0 = pivot0 == start ? i : pivot0; + int p1 = pivot1 == end ? j : pivot1; + + while (true) { + do { + --i; + } while (a[i] < v); + do { + ++j; + } while (a[j] > v); + vj = a[i]; + vi = a[j]; + a[i] = vi; + a[j] = vj; + // Move the equal values to pivot region + if (vi == v) { + a[i] = a[--p0]; + a[p0] = v; + } + if (vj == v) { + a[j] = a[++p1]; + a[p1] = v; + } + // Termination check and finishing loops. + // Note: This works even if pivot region is zero length (p1 == p0-1 due to + // length 1 pivot region at either start/end) because we pre-inc/decrement + // one side and post-inc/decrement the other side. + if (i == left) { + while (j < right) { + do { + ++j; + } while (a[j] > v); + final double w = a[j]; + // Move upper bound of pivot region + a[j] = a[++p1]; + a[p1] = v; + // Move lower bound of pivot region + if (w != v) { + a[p0] = w; + p0++; + } + } + break; + } + if (j == right) { + while (i > left) { + do { + --i; + } while (a[i] < v); + final double w = a[i]; + // Move lower bound of pivot region + a[i] = a[--p0]; + a[p0] = v; + // Move upper bound of pivot region + if (w != v) { + a[p1] = w; + p1--; + } + } + break; + } + } + + upper[0] = p1; + return p0; + } + + /** + * Partition the array such that indices {@code k} correspond to their + * correctly sorted value in the equivalent fully sorted array. + * + *

For all indices {@code k} and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

This function accepts a {@link UpdatingInterval} of indices {@code k} that define the + * range of indices to partition. The {@link UpdatingInterval} can be narrowed or split as + * partitioning divides the range. + * + *

Uses an introselect variant. The quickselect is a dual-pivot quicksort + * partition method by Vladimir Yaroslavskiy; the fall-back on poor convergence of + * the quickselect is a heapselect. + * + *

The {@code flags} contain the the current recursion count and the configured + * length threshold for {@code r - l} to perform sort select. The count is in the upper + * bits and the threshold is in the lower bits. + * + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param k Interval of indices to partition (ordered). + * @param flags Control flags. + */ + // package-private for testing + static void dualPivotQuickSelect(double[] a, int left, int right, UpdatingInterval k, int flags) { + // If partitioning splits the interval then recursion is used for the left-most side(s) + // and the right-most side remains within this function. If partitioning does + // not split the interval then it remains within this function. + int l = left; + int r = right; + int f = flags; + int ka = k.left(); + int kb = k.right(); + final int[] upper = {0, 0, 0}; + while (true) { + // Select when ka and kb are close to the same end, + // or the entire range is small + // |l|-----|ka|--------|kb|------|r| + final int n = r - l; + if (Math.min(kb - l, r - ka) < DP_SORTSELECT_SIZE || + n < (f & SORTSELECT_MASK)) { + sortSelect(a, l, r, ka, kb); + return; + } + if (kb - ka < DP_SORTSELECT_SIZE) { + // Switch to single-pivot mode with Floyd-Rivest sub-sampling + quickSelectAdaptive(a, l, r, ka, kb, upper, MODE_FR_SAMPLING); + return; + } + if (f < 0) { + // Excess recursion, switch to heap select + heapSelect(a, l, r, ka, kb); + return; + } + + // Dual-pivot partitioning + final int p0 = partition(a, l, r, upper); + final int p1 = upper[0]; + + // Recursion to max depth + // Note: Here we possibly branch left, middle and right with multiple keys. + // It is possible that the partition has split the keys + // and the recursion proceeds with a reduced set in each region. + // p0 p1 p2 p3 + // |l|--|ka|--k----k--|P|------k--|kb|----|P|----|r| + // kb | ka + f += RECURSION_INCREMENT; + // Recurse left side if required + if (ka < p0) { + if (kb <= p1) { + // Entirely on left side + r = p0 - 1; + if (r < kb) { + kb = k.updateRight(r); + } + continue; + } + dualPivotQuickSelect(a, l, p0 - 1, k.splitLeft(p0, p1), f); + // Here we must process middle and/or right + ka = k.left(); + } else if (kb <= p1) { + // No middle/right side + return; + } else if (ka <= p1) { + // Advance lower bound + ka = k.updateLeft(p1 + 1); + } + // Recurse middle if required + final int p2 = upper[1]; + final int p3 = upper[2]; + if (ka < p2) { + l = p1 + 1; + if (kb <= p3) { + // Entirely in middle + r = p2 - 1; + if (r < kb) { + kb = k.updateRight(r); + } + continue; + } + dualPivotQuickSelect(a, l, p2 - 1, k.splitLeft(p2, p3), f); + ka = k.left(); + } else if (kb <= p3) { + // No right side + return; + } else if (ka <= p3) { + ka = k.updateLeft(p3 + 1); + } + // Continue right + l = p3 + 1; + } + } + + /** + * Partition an array slice around 2 pivots. Partitioning exchanges array elements + * such that all elements smaller than pivot are before it and all elements larger + * than pivot are after it. + * + *

This method returns 4 points describing the pivot ranges of equal values. + * + *

{@code
+     *         |k0  k1|                |k2  k3|
+     * |   

P | + * }

+ * + * + * + *

Bounds are set so {@code i < k0}, {@code i > k3} and {@code k1 < i < k2} are + * unsorted. When the range {@code [k0, k3]} contains fully sorted elements the result + * is set to {@code k1 = k3; k2 == k0}. This can occur if + * {@code P1 == P2} or there are zero or one value between the pivots + * {@code P1 < v < P2}. Any sort/partition of ranges [left, k0-1], [k1+1, k2-1] and + * [k3+1, right] must check the length is {@code > 1}. + * + * @param a Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param bounds Points [k1, k2, k3]. + * @return Lower bound (inclusive) of the pivot range [k0]. + */ + private static int partition(double[] a, int left, int right, int[] bounds) { + // Pick 2 pivots from 5 approximately uniform through the range. + // Spacing is ~ 1/7 made using shifts. Other strategies are equal or much + // worse. 1/7 = 5/35 ~ 1/8 + 1/64 : 0.1429 ~ 0.1406 + // Ensure the value is above zero to choose different points! + final int n = right - left; + final int step = 1 + (n >>> 3) + (n >>> 6); + final int i3 = left + (n >>> 1); + final int i2 = i3 - step; + final int i1 = i2 - step; + final int i4 = i3 + step; + final int i5 = i4 + step; + Sorting.sort5(a, i1, i2, i3, i4, i5); + + // Partition data using pivots P1 and P2 into less-than, greater-than or between. + // Pivot values P1 & P2 are placed at the end. If P1 < P2, P2 acts as a sentinel. + // k traverses the unknown region ??? and values moved if less-than or + // greater-than: + // + // left less k great right + // |P1| P2 |P2| + // + // P2 (gt, right) + // + // At the end pivots are swapped back to behind the less and great pointers. + // + // | P2 | + + // Swap ends to the pivot locations. + final double v1 = a[i2]; + a[i2] = a[left]; + a[left] = v1; + final double v2 = a[i4]; + a[i4] = a[right]; + a[right] = v2; + + // pointers + int less = left; + int great = right; + + // Fast-forward ascending / descending runs to reduce swaps. + // Cannot overrun as end pivots (v1 <= v2) act as sentinels. + do { + ++less; + } while (a[less] < v1); + do { + --great; + } while (a[great] > v2); + + // a[less - 1] < P1 : a[great + 1] > P2 + // unvisited in [less, great] + SORTING: + for (int k = less - 1; ++k <= great;) { + final double v = a[k]; + if (v < v1) { + // swap(a, k, less++) + a[k] = a[less]; + a[less] = v; + less++; + } else if (v > v2) { + // while k < great and a[great] > v2: + // great-- + while (a[great] > v2) { + if (great-- == k) { + // Done + break SORTING; + } + } + // swap(a, k, great--) + // if a[k] < v1: + // swap(a, k, less++) + final double w = a[great]; + a[great] = v; + great--; + // delay a[k] = w + if (w < v1) { + a[k] = a[less]; + a[less] = w; + less++; + } else { + a[k] = w; + } + } + } + + // Change to inclusive ends : a[less] < P1 : a[great] > P2 + less--; + great++; + // Move the pivots to correct locations + a[left] = a[less]; + a[less] = v1; + a[right] = a[great]; + a[great] = v2; + + // Record the pivot locations + final int lower = less; + bounds[2] = great; + + // equal elements + // Original paper: If middle partition is bigger than a threshold + // then check for equal elements. + + // Note: This is extra work. When performing partitioning the region of interest + // may be entirely above or below the central region and this can be skipped. + + // Here we look for equal elements if the centre is more than 5/8 the length. + // 5/8 = 1/2 + 1/8. Pivots must be different. + if ((great - less) > (n >>> 1) + (n >>> 3) && v1 != v2) { + + // Fast-forward to reduce swaps. Changes inclusive ends to exclusive ends. + // Since v1 != v2 these act as sentinels to prevent overrun. + do { + ++less; + } while (a[less] == v1); + do { + --great; + } while (a[great] == v2); + + // This copies the logic in the sorting loop using == comparisons + EQUAL: + for (int k = less - 1; ++k <= great;) { + final double v = a[k]; + if (v == v1) { + a[k] = a[less]; + a[less] = v; + less++; + } else if (v == v2) { + while (a[great] == v2) { + if (great-- == k) { + // Done + break EQUAL; + } + } + final double w = a[great]; + a[great] = v; + great--; + if (w == v1) { + a[k] = a[less]; + a[less] = w; + less++; + } else { + a[k] = w; + } + } + } + + // Change to inclusive ends + less--; + great++; + } + + // Between pivots in (less, great) + if (v1 != v2 && less < great - 1) { + // Record the pivot end points + bounds[0] = less; + bounds[1] = great; + } else { + // No unsorted internal region (set k1 = k3; k2 = k0) + bounds[0] = bounds[2]; + bounds[1] = lower; + } + + return lower; + } + + /** + * Map the distance from the edge of {@code [l, r]} to a new distance in {@code [0, n)}. + * + *

The provides the adaption {@code kf'/|A|} from Alexandrescu (2016) where + * {@code k == d}, {@code f' == n} and {@code |A| == r-l+1}. + * + *

For convenience this accepts the input range {@code [l, r]}. + * + * @param d Distance from the edge in {@code [0, r - l]}. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param n Size of the new range. + * @return the mapped distance in [0, n) + */ + private static int mapDistance(int d, int l, int r, int n) { + return (int) (d * (n - 1.0) / (r - l)); + } + + /** + * Configure the dual-pivot control flags. This packs the maximum recursion depth and + * sort select size into a single integer. + * + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param k1 First key of interest. + * @param kn Last key of interest. + * @return the flags + */ + private static int dualPivotFlags(int left, int right, int k1, int kn) { + final int maxDepth = dualPivotMaxDepth(right - left); + final int ss = dualPivotSortSelectSize(k1, kn); + return dualPivotFlags(maxDepth, ss); + } + + /** + * Configure the dual-pivot control flags. This packs the maximum recursion depth and + * sort select size into a single integer. + * + * @param maxDepth Maximum recursion depth. + * @param ss Sort select size. + * @return the flags + */ + static int dualPivotFlags(int maxDepth, int ss) { + // The flags are packed using the upper bits to count back from -1 in + // step sizes. The lower bits pack the sort select size. + int flags = Integer.MIN_VALUE - maxDepth * RECURSION_INCREMENT; + flags &= ~SORTSELECT_MASK; + return flags | ss; + } + + /** + * Compute the maximum recursion depth for dual pivot recursion. + * This is an approximation to {@code 2 * log3 (x)}. + * + *

The result is between {@code 2*floor(log3(x))} and {@code 2*ceil(log3(x))}. + * The result is correctly rounded when {@code x +/- 1} is a power of 3. + * + * @param x Value. + * @return maximum recursion depth + */ + static int dualPivotMaxDepth(int x) { + // log3(2) ~ 1.5849625 + // log3(x) ~ log2(x) * 0.630929753... ~ log2(x) * 323 / 512 (0.630859375) + // Use (floor(log2(x))+1) * 323 / 256 + return ((32 - Integer.numberOfLeadingZeros(x)) * 323) >>> 8; + } + + /** + * Configure the sort select size for dual pivot partitioning. + * + * @param k1 First key of interest. + * @param kn Last key of interest. + * @return the sort select size. + */ + private static int dualPivotSortSelectSize(int k1, int kn) { + // Configure the sort select size based on the index density + // l---k1---k---k-----k--k------kn----r + // + // For a full sort the dual-pivot quicksort can switch to insertion sort + // when the length is small. The optimum value is dependent on the + // hardware and the insertion sort implementation. Benchmarks show that + // insertion sort can be used at length 80-120. + // + // During selection the SORTSELECT_SIZE specifies the distance from the edge + // to use sort select. When keys are not dense there may be a small length + // that is ignored by sort select due to the presence of another key. + // Diagram of k-l = SORTSELECT_SIZE and r-k < SORTSELECT_SIZE where a second + // key b is blocking the use of sort select. The key b is closest it can be to the right + // key to enable blocking; it could be further away (up to k = left). + // + // |--SORTSELECT_SIZE--| + // |--SORTSELECT_SIZE--| + // l--b----------------k--r + // l----b--------------k----r + // l------b------------k------r + // l--------b----------k--------r + // l----------b--------k----------r + // l------------b------k------------r + // l--------------b----k--------------r + // l----------------b--k----------------r + // l------------------bk------------------r + // |--SORTSELECT_SIZE--| + // + // For all these cases the partitioning method would have to run. Assuming ideal + // dual-pivot partitioning into thirds, and that the left key is randomly positioned + // in [left, k) it is more likely that after partitioning 2 partitions will have to + // be processed rather than 1 partition. In this case the options are: + // - split the range using partitioning; sort select next iteration + // - use sort select with a edge distance above the optimum length for single k selection + // + // Contrast with a longer length: + // |--SORTSELECT_SIZE--| + // l-------------------k-----k-------k-------------------r + // |--SORTSELECT_SIZE--| + // Here partitioning has to run and 1, 2, or 3 partitions processed. But all k can + // be found with a sort. In this case sort select could be used with a much higher + // length (e.g. 80 - 120). + // + // When keys are extremely sparse (never within SORTSELECT_SIZE) then no switch + // to sort select based on length is *required*. It may still be beneficial to avoid + // partitioning if the length is very small due to the overhead of partitioning. + // + // Benchmarking with different lengths for a switch to sort select show inconsistent + // behaviour across platforms due to the variable speed of insertion sort at longer + // lengths. Attempts to transition the length based on various ramps schemes can + // be incorrect and result is a slowdown rather than speed-up (if the correct threshold + // is not chosen). + // + // Here we use a much simpler scheme based on these observations: + // - If the average separation is very high then no length will collect extra indices + // from a sort select over the current trigger of using the distance from the end. But + // using a length dependence will not effect the work done by sort select as it only + // performs the minimum sorting required. + // - If the average separation is within the SORTSELECT_SIZE then a round of + // partitioning will create multiple regions that all require a sort selection. + // Thus a partitioning round can be avoided if the length is small. + // - If the keys are at the end with nothing in between then partitioning will be able + // to split them but a sort will have to sort the entire range: + // lk-------------------------------kr + // After partitioning starts the chance of keys being at the ends is low as keys + // should be random within the divided range. + // - Extremely high density keys is rare. It is only expected to saturate the range + // with short lengths, e.g. 100 quantiles for length 1000 = separation 10 (high density) + // but for length 10000 = separation 100 (low density). + // - The density of (non-uniform) keys is hard to predict without complex analysis. + // + // Benchmarking using random keys at various density show no performance loss from + // using a fixed size for the length dependence of sort select, if the size is small. + // A large length can impact performance with low density keys, and on machines + // where insertion sort is slower. Extreme performance gains occur when the average + // separation of random keys is below 8-16, or of uniform keys around 32, by using a + // sort at lengths up to 90. But this threshold shows performance loss greater than + // the gains with separation of 64-128 on random keys, and on machines with slow + // insertion sort. The transition to using an insertion sort of a longer length + // is difficult to predict for all situations. + + // Let partitioning run if the initial length is small. + // Use kn - k1 as a proxy for the length. If length is actually very large then + // the final selection is insignificant. This avoids slowdown for small lengths + // where the keys may only be at the ends. Note ideal dual-pivot partitioning + // creates thirds so 1 iteration on SORTSELECT_SIZE * 3 should create + // SORTSELECT_SIZE partitions. + if (kn - k1 < DP_SORTSELECT_SIZE * 3) { + return 0; + } + // Here partitioning will run at least once. + // Stable performance across platforms using a modest length dependence. + return DP_SORTSELECT_SIZE * 2; + } +} diff --git a/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/Selection.java b/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/Selection.java new file mode 100644 index 000000000..60c7d73f7 --- /dev/null +++ b/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/Selection.java @@ -0,0 +1,334 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.arrays; + +/** + * Select indices in array data. + * + *

Arranges elements such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+ * data[i < k] <= data[k] <= data[k < i]
+ * }
+ * + *

Examples: + * + *

+ * data    [0, 1, 2, 1, 2, 5, 2, 3, 3, 6, 7, 7, 7, 7]
+ *
+ *
+ * k=4   : [0, 1, 2, 1], [2], [5, 2, 3, 3, 6, 7, 7, 7, 7]
+ * k=4,8 : [0, 1, 2, 1], [2], [3, 3, 2], [5], [6, 7, 7, 7, 7]
+ * 
+ * + *

This implementation can select on multiple indices and will handle duplicate and + * unordered indices. The method detects ordered indices (with or without duplicates) and + * uses this during processing. Passing ordered indices is recommended if the order is already + * known; for example using uniform spacing through the array data, or to select the top and + * bottom {@code n} values from the data. + * + *

A quickselect adaptive method is used for single indices. This uses analysis of the + * partition sizes after each division to update the algorithm mode. If the partition + * containing the target does not sufficiently reduce in size then the algorithm is + * progressively changed to use partitions with guaranteed margins. This ensures a set fraction + * of data is eliminated each step and worse-case linear run time performance. This method can + * handle a range of indices {@code [ka, kb]} with a small separation by targeting the start of + * the range {@code ka} and then selecting the remaining elements {@code (ka, kb]} that are at + * the edge of the partition bounded by {@code ka}. + * + *

Multiple keys are partitioned collectively using an introsort method which only recurses + * into partitions containing indices. Excess recursion will trigger use of a heapselect + * on the remaining range of indices ensuring non-quadratic worse case performance. Any + * partition containing a single index, adjacent pair of indices, or range of indices with a + * small separation will use the quickselect adaptive method for single keys. Note that the + * maximum number of times that {@code n} indices can be split is {@code n - 1} before all + * indices are handled as singles. + * + *

Floating-point order + * + *

The {@code <} relation does not impose a total order on all floating-point values. + * This class respects the ordering imposed by {@link Double#compare(double, double)}. + * {@code -0.0} is treated as less than value {@code 0.0}; {@code Double.NaN} is + * considered greater than any other value; and all {@code Double.NaN} values are + * considered equal. + * + *

References + * + *

Quickselect is introduced in Hoare [1]. This selects an element {@code k} from {@code n} + * using repeat division of the data around a partition element, recursing into the + * partition that contains {@code k}. + * + *

Introsort/select is introduced in Musser [2]. This detects excess recursion in + * quicksort/select and reverts to a heapsort or linear select to achieve an improved worst + * case bound. + * + *

Use of sampling to identify a pivot that places {@code k} in the smaller partition is + * performed in the SELECT algorithm of Floyd and Rivest [3, 4]. + * + *

A worst-case linear time algorithm PICK is described in Blum et al [5]. This uses + * the median of medians as a partition element for selection which ensures a minimum fraction of + * the elements are eliminated per iteration. This was extended to use an asymmetric pivot choice + * with efficient placement of the medians sample location in the QuickselectAdpative algorithm of + * Alexandrescu [6]. + * + *

    + *
  1. Hoare (1961) + * Algorithm 65: Find + * Comm. ACM. 4 (7): 321–322 + *
  2. Musser (1999) + * Introspective Sorting and Selection Algorithms + * + * Software: Practice and Experience 27, 983-993. + *
  3. Floyd and Rivest (1975) + * Algorithm 489: The Algorithm SELECT—for Finding the ith Smallest of n elements. + * Comm. ACM. 18 (3): 173. + *
  4. Kiwiel (2005) + * On Floyd and Rivest's SELECT algorithm. + * Theoretical Computer Science 347, 214-238. + *
  5. Blum, Floyd, Pratt, Rivest, and Tarjan (1973) + * Time bounds for selection. + * + * Journal of Computer and System Sciences. 7 (4): 448–461. + *
  6. Alexandrescu (2016) + * Fast Deterministic Selection + * arXiv:1606.00484. + *
  7. Quickselect (Wikipedia) + *
  8. Introsort (Wikipedia) + *
  9. Introselect (Wikipedia) + *
  10. Floyd-Rivest algorithm (Wikipedia) + *
  11. Median of medians (Wikipedia) + *
+ * + * @since 1.2 + */ +public final class Selection { + + /** No instances. */ + private Selection() {} + + /** + * Partition the array such that index {@code k} corresponds to its correctly + * sorted value in the equivalent fully sorted array. + * + * @param a Values. + * @param k Index. + * @throws IndexOutOfBoundsException if index {@code k} is not within the + * sub-range {@code [0, a.length)} + */ + public static void select(double[] a, int k) { + IndexSupport.checkIndex(0, a.length, k); + doSelect(a, 0, a.length, k); + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. + * + * @param a Values. + * @param k Indices (may be destructively modified). + * @throws IndexOutOfBoundsException if any index {@code k} is not within the + * sub-range {@code [0, a.length)} + */ + public static void select(double[] a, int[] k) { + IndexSupport.checkIndices(0, a.length, k); + doSelect(a, 0, a.length, k); + } + + /** + * Partition the array such that index {@code k} corresponds to its correctly + * sorted value in the equivalent fully sorted array. + * + * @param a Values. + * @param fromIndex Index of the first element (inclusive). + * @param toIndex Index of the last element (exclusive). + * @param k Index. + * @throws IndexOutOfBoundsException if the sub-range {@code [fromIndex, toIndex)} is out of + * bounds of range {@code [0, a.length)}; or if index {@code k} is not within the + * sub-range {@code [fromIndex, toIndex)} + */ + public static void select(double[] a, int fromIndex, int toIndex, int k) { + IndexSupport.checkFromToIndex(fromIndex, toIndex, a.length); + IndexSupport.checkIndex(fromIndex, toIndex, k); + doSelect(a, fromIndex, toIndex, k); + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. + * + * @param a Values. + * @param fromIndex Index of the first element (inclusive). + * @param toIndex Index of the last element (exclusive). + * @param k Indices (may be destructively modified). + * @throws IndexOutOfBoundsException if the sub-range {@code [fromIndex, toIndex)} is out of + * bounds of range {@code [0, a.length)}; or if any index {@code k} is not within the + * sub-range {@code [fromIndex, toIndex)} + */ + public static void select(double[] a, int fromIndex, int toIndex, int[] k) { + IndexSupport.checkFromToIndex(fromIndex, toIndex, a.length); + IndexSupport.checkIndices(fromIndex, toIndex, k); + doSelect(a, fromIndex, toIndex, k); + } + + /** + * Partition the array such that index {@code k} corresponds to its correctly + * sorted value in the equivalent fully sorted array. + * + *

This method pre/post-processes the data and indices to respect the ordering + * imposed by {@link Double#compare(double, double)}. + * + * @param fromIndex Index of the first element (inclusive). + * @param toIndex Index of the last element (exclusive). + * @param a Values. + * @param k Index. + */ + private static void doSelect(double[] a, int fromIndex, int toIndex, int k) { + if (toIndex - fromIndex <= 1) { + return; + } + // Sort NaN / count signed zeros. + // Caution: This loop contributes significantly to the runtime. + int cn = 0; + int end = toIndex; + for (int i = toIndex; --i >= fromIndex;) { + final double v = a[i]; + // Count negative zeros using a sign bit check + if (Double.doubleToRawLongBits(v) == Long.MIN_VALUE) { + cn++; + // Change to positive zero. + // Data must be repaired after selection. + a[i] = 0.0; + } else if (v != v) { + // Move NaN to end + a[i] = a[--end]; + a[end] = v; + } + } + + // Partition + if (end - fromIndex > 1 && k < end) { + QuickSelect.select(a, fromIndex, end - 1, k); + } + + // Restore signed zeros + if (cn != 0) { + // Use partition index below zero to fast-forward to zero as much as possible + for (int j = a[k] < 0 ? k : -1;;) { + if (a[++j] == 0) { + a[j] = -0.0; + if (--cn == 0) { + break; + } + } + } + } + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. + * + *

This method pre/post-processes the data and indices to respect the ordering + * imposed by {@link Double#compare(double, double)}. + * + * @param fromIndex Index of the first element (inclusive). + * @param toIndex Index of the last element (exclusive). + * @param a Values. + * @param k Indices (may be destructively modified). + */ + private static void doSelect(double[] a, int fromIndex, int toIndex, int[] k) { + if (k.length == 0 || toIndex - fromIndex <= 1) { + return; + } + // Sort NaN / count signed zeros. + // Caution: This loop contributes significantly to the runtime for single indices. + int cn = 0; + int end = toIndex; + for (int i = toIndex; --i >= fromIndex;) { + final double v = a[i]; + // Count negative zeros using a sign bit check + if (Double.doubleToRawLongBits(v) == Long.MIN_VALUE) { + cn++; + // Change to positive zero. + // Data must be repaired after selection. + a[i] = 0.0; + } else if (v != v) { + // Move NaN to end + a[i] = a[--end]; + a[end] = v; + } + } + + // Partition + int n = 0; + if (end - fromIndex > 1) { + n = k.length; + // Filter indices invalidated by NaN check + if (end < toIndex) { + for (int i = n; --i >= 0;) { + final int index = k[i]; + if (index >= end) { + // Move to end + k[i] = k[--n]; + k[n] = index; + } + } + } + // Return n, the count of used indices in k. + // Use this to post-process zeros. + n = QuickSelect.select(a, fromIndex, end - 1, k, n); + } + + // Restore signed zeros + if (cn != 0) { + // Use partition indices below zero to fast-forward to zero as much as possible + int j = -1; + if (n < 0) { + // Binary search on -n sorted indices: hi = (-n) - 1 + int lo = 0; + int hi = ~n; + while (lo <= hi) { + final int mid = (lo + hi) >>> 1; + if (a[k[mid]] < 0) { + j = mid; + lo = mid + 1; + } else { + hi = mid - 1; + } + } + } else { + // Unsorted, process all indices + for (int i = n; --i >= 0;) { + if (a[k[i]] < 0) { + j = k[i]; + } + } + } + for (;;) { + if (a[++j] == 0) { + a[j] = -0.0; + if (--cn == 0) { + break; + } + } + } + } + } +} diff --git a/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/Sorting.java b/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/Sorting.java new file mode 100644 index 000000000..bfd06aaeb --- /dev/null +++ b/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/Sorting.java @@ -0,0 +1,341 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.arrays; + +import java.util.Arrays; + +/** + * Support class for sorting arrays. + * + *

Optimal sorting networks are used for small fixed size array sorting. + * + *

Note: Requires that the floating-point data contains no NaN values; sorting + * does not respect the order of signed zeros imposed by {@link Double#compare(double, double)}. + * + * @see Sorting network (Wikipedia) + * @see Sorting Networks (Bert Dobbelaere) + * + * @since 1.2 + */ +final class Sorting { + + /** No instances. */ + private Sorting() {} + + /** + * Sorts an array using an insertion sort. + * + * @param x Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + static void sort(double[] x, int left, int right) { + for (int i = left; ++i <= right;) { + final double v = x[i]; + // Move preceding higher elements above (if required) + if (v < x[i - 1]) { + int j = i; + while (--j >= left && v < x[j]) { + x[j + 1] = x[j]; + } + x[j + 1] = v; + } + } + } + + /** + * Sorts the elements at the given distinct indices in an array. + * + * @param x Data array. + * @param a Index. + * @param b Index. + * @param c Index. + */ + static void sort3(double[] x, int a, int b, int c) { + // Decision tree avoiding swaps: + // Order [(0,2)] + // Move point 1 above point 2 or below point 0 + final double u = x[a]; + final double v = x[b]; + final double w = x[c]; + if (w < u) { + if (v < w) { + x[a] = v; + x[b] = w; + x[c] = u; + return; + } + if (u < v) { + x[a] = w; + x[b] = u; + x[c] = v; + return; + } + // z < y < z + x[a] = w; + x[c] = u; + return; + } + if (v < u) { + // y < x < z + x[a] = v; + x[b] = u; + return; + } + if (w < v) { + // x < z < y + x[b] = w; + x[c] = v; + } + // x < y < z + } + + /** + * Sorts the elements at the given distinct indices in an array. + * + * @param x Data array. + * @param a Index. + * @param b Index. + * @param c Index. + * @param d Index. + * @param e Index. + */ + static void sort5(double[] x, int a, int b, int c, int d, int e) { + // Uses an optimal sorting network from Knuth's Art of Computer Programming. + // 9 comparisons. + // Order pairs: + // [(0,3),(1,4)] + // [(0,2),(1,3)] + // [(0,1),(2,4)] + // [(1,2),(3,4)] + // [(2,3)] + if (x[e] < x[b]) { + final double u = x[e]; + x[e] = x[b]; + x[b] = u; + } + if (x[d] < x[a]) { + final double v = x[d]; + x[d] = x[a]; + x[a] = v; + } + + if (x[d] < x[b]) { + final double u = x[d]; + x[d] = x[b]; + x[b] = u; + } + if (x[c] < x[a]) { + final double v = x[c]; + x[c] = x[a]; + x[a] = v; + } + + if (x[e] < x[c]) { + final double u = x[e]; + x[e] = x[c]; + x[c] = u; + } + if (x[b] < x[a]) { + final double v = x[b]; + x[b] = x[a]; + x[a] = v; + } + + if (x[e] < x[d]) { + final double u = x[e]; + x[e] = x[d]; + x[d] = u; + } + if (x[c] < x[b]) { + final double v = x[c]; + x[c] = x[b]; + x[b] = v; + } + + if (x[d] < x[c]) { + final double u = x[d]; + x[d] = x[c]; + x[c] = u; + } + } + + /** + * Place the lower median of 4 elements in {@code b}; the smaller element in + * {@code a}; and the larger two elements in {@code c, d}. + * + * @param x Values + * @param a Index. + * @param b Index. + * @param c Index. + * @param d Index. + */ + static void lowerMedian4(double[] x, int a, int b, int c, int d) { + // 3 to 5 comparisons + if (x[d] < x[b]) { + final double u = x[d]; + x[d] = x[b]; + x[b] = u; + } + if (x[c] < x[a]) { + final double v = x[c]; + x[c] = x[a]; + x[a] = v; + } + // a--c + // b--d + if (x[c] < x[b]) { + final double u = x[c]; + x[c] = x[b]; + x[b] = u; + } else if (x[b] < x[a]) { + // a--c + // b--d + final double xb = x[a]; + x[a] = x[b]; + x[b] = xb; + // b--c + // a--d + if (x[d] < xb) { + x[b] = x[d]; + // Move a pair to maintain the sorted order + x[d] = x[c]; + x[c] = xb; + } + } + } + + /** + * Place the upper median of 4 elements in {@code c}; the smaller two elements in + * {@code a,b}; and the larger element in {@code d}. + * + * @param x Values + * @param a Index. + * @param b Index. + * @param c Index. + * @param d Index. + */ + static void upperMedian4(double[] x, int a, int b, int c, int d) { + // 3 to 5 comparisons + if (x[d] < x[b]) { + final double u = x[d]; + x[d] = x[b]; + x[b] = u; + } + if (x[c] < x[a]) { + final double v = x[c]; + x[c] = x[a]; + x[a] = v; + } + // a--c + // b--d + if (x[b] > x[c]) { + final double u = x[c]; + x[c] = x[b]; + x[b] = u; + } else if (x[c] > x[d]) { + // a--c + // b--d + final double xc = x[d]; + x[d] = x[c]; + x[c] = xc; + // a--d + // b--c + if (x[a] > xc) { + x[c] = x[a]; + // Move a pair to maintain the sorted order + x[a] = x[b]; + x[b] = xc; + } + } + } + + /** + * Sort the unique indices in-place to the start of the array. The number of + * unique indices is returned. + * + *

Uses an insertion sort modified to ignore duplicates. Use on small {@code n}. + * + *

Warning: Requires {@code n > 0}. The array contents after the count of unique + * indices {@code c} are unchanged (i.e. {@code [c, n)}. This may change the count of + * each unique index in the entire array. + * + * @param x Indices. + * @param n Number of indices. + * @return the number of indices + */ + static int insertionSortIndices(int[] x, int n) { + int unique = 1; + // Do an insertion sort but only compare the current set of unique values. + for (int i = 0; ++i < n;) { + final int v = x[i]; + int j = unique - 1; + if (v > x[j]) { + // Insert at end + x[unique] = v; + unique++; + } else if (v < x[j]) { + // Find insertion point in the unique indices + do { + --j; + } while (j >= 0 && v < x[j]); + // Insertion point = j + 1 + // Insert if at start or non-duplicate + if (j < 0 || v != x[j]) { + // Move (j, unique) to (j+1, unique+1) + for (int k = unique; --k > j;) { + x[k + 1] = x[k]; + } + x[j + 1] = v; + unique++; + } + } + } + return unique; + } + + /** + * Sort the unique indices in-place to the start of the array. The number of + * unique indices is returned. + * + *

Uses an Order(1) data structure to ignore duplicates. + * + *

Warning: Requires {@code n > 0}. The array contents after the count of unique + * indices is unchanged. This may change the count of each unique index in the + * entire array. + * + * @param x Indices. + * @param n Number of indices. + * @return the number of indices + */ + static int sortIndices(int[] x, int n) { + // Duplicates are checked using a primitive hash set. + // Storage (bytes) = 4 * next-power-of-2(n*2) => 2-4 times n + final HashIndexSet set = HashIndexSet.create(n); + int i = 0; + int last = 0; + set.add(x[0]); + while (++i < n) { + final int v = x[i]; + if (set.add(v)) { + x[++last] = v; + } + } + Arrays.sort(x, 0, ++last); + return last; + } +} diff --git a/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/UpdatingInterval.java b/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/UpdatingInterval.java new file mode 100644 index 000000000..5b8e82ffa --- /dev/null +++ b/commons-numbers-arrays/src/main/java/org/apache/commons/numbers/arrays/UpdatingInterval.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.arrays; + +/** + * An interval that contains indices used for partitioning an array into multiple regions. + * + *

The interval provides the following functionality: + * + *

+ * + *

Note that the interval provides the supported bounds. If an search index {@code k} is + * outside the supported bounds the result is undefined. + * + *

Implementations may assume indices are positive. + * + * @since 1.2 + */ +interface UpdatingInterval { + /** + * The start (inclusive) of the interval. + * + * @return start of the interval + */ + int left(); + + /** + * The end (inclusive) of the interval. + * + * @return end of the interval + */ + int right(); + + /** + * Update the left bound of the interval so {@code k <= left}. + * + *

Note: Requires {@code left < k <= right}, i.e. there exists a valid interval + * above the index. + * + *

{@code
+     * l-----------k----------r
+     *             |--> l
+     * }
+ * + * @param k Index to start checking from (inclusive). + * @return the new left + */ + int updateLeft(int k); + + /** + * Update the right bound of the interval so {@code right <= k}. + * + *

Note: Requires {@code left <= k < right}, i.e. there exists a valid interval + * below the index. + * + *

{@code
+     * l-----------k----------r
+     *        r <--|
+     * }
+ * + * @param k Index to start checking from (inclusive). + * @return the new right + */ + int updateRight(int k); + + /** + * Split the interval using two splitting indices. Returns the left interval that occurs + * before the specified split index {@code ka}, and updates the current interval left bound + * to after the specified split index {@code kb}. + * + *

Note: Requires {@code left < ka <= kb < right}, i.e. there exists a valid interval + * both above and below the splitting indices. + * + *

{@code
+     * l-----------ka-kb----------r
+     *      r1 <--|     |--> l1
+     *
+     * r1 < ka
+     * l1 > kb
+     * }
+ * + *

If {@code ka <= left} or {@code kb >= right} the result is undefined. + * + * @param ka Split index. + * @param kb Split index. + * @return the left interval + */ + UpdatingInterval splitLeft(int ka, int kb); +} diff --git a/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/HashIndexSetTest.java b/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/HashIndexSetTest.java new file mode 100644 index 000000000..458d5bc0e --- /dev/null +++ b/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/HashIndexSetTest.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.arrays; + +import java.util.BitSet; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.apache.commons.rng.UniformRandomProvider; +import org.apache.commons.rng.simple.RandomSource; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * Test for {@link HashIndexSet}. + */ +class HashIndexSetTest { + + @Test + void testInvalidCapacityThrows() { + final int maxCapacity = 1 << 29; + Assertions.assertThrows(IllegalArgumentException.class, () -> HashIndexSet.create(maxCapacity + 1)); + Assertions.assertThrows(IllegalArgumentException.class, () -> HashIndexSet.create(Integer.MAX_VALUE)); + } + + @Test + void testInvalidIndexThrows() { + final HashIndexSet set = HashIndexSet.create(16); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> set.add(-1)); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> set.add(Integer.MIN_VALUE)); + } + + @ParameterizedTest + @ValueSource(ints = {10, 32}) + void testCapacityExceededThrows(int capacity) { + final HashIndexSet set = HashIndexSet.create(capacity); + IntStream.range(0, capacity).forEach(set::add); + // Now add more and expect an exception. + // With a load factor of 0.5 we can only add twice the requested capacity + // before an exception occurs. + Assertions.assertThrows(IllegalStateException.class, () -> { + for (int i = capacity; i < capacity * 2; i++) { + set.add(i); + } + }); + } + + @ParameterizedTest + @MethodSource(value = {"testIndices"}) + void testAdd(int[] indices, int capacity) { + final HashIndexSet set = HashIndexSet.create(capacity); + final BitSet ref = new BitSet(capacity); + for (final int i : indices) { + final boolean observed = ref.get(i); + // Add returns true if not already present + Assertions.assertEquals(!observed, set.add(i), () -> String.valueOf(i)); + ref.set(i); + } + } + + static Stream testIndices() { + final Stream.Builder builder = Stream.builder(); + + builder.accept(Arguments.of(new int[] {1, 2}, 10)); + builder.accept(Arguments.of(new int[] {1, 2, 3, 4, 5}, 10)); + + // Add duplicates + builder.accept(Arguments.of(new int[] {1, 1, 1, 2, 3, 4, 5, 6, 7}, 10)); + builder.accept(Arguments.of(new int[] {5, 6, 2, 2, 3, 8, 1, 1, 4, 3}, 10)); + builder.accept(Arguments.of(new int[] {2, 2, 2, 2, 2}, 10)); + builder.accept(Arguments.of(new int[] {2000, 2001, 2000, 2001}, 2010)); + + final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create(); + for (final int size : new int[] {5, 500}) { + // Sparse + builder.accept(Arguments.of(rng.ints(10, 0, size).toArray(), size)); + // With duplicates + builder.accept(Arguments.of(rng.ints(size, 0, size).toArray(), size)); + builder.accept(Arguments.of(rng.ints(size, 0, size >> 1).toArray(), size)); + builder.accept(Arguments.of(rng.ints(size, 0, size >> 2).toArray(), size)); + } + + return builder.build(); + } +} diff --git a/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/IndexSupportTest.java b/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/IndexSupportTest.java new file mode 100644 index 000000000..2f25a4a85 --- /dev/null +++ b/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/IndexSupportTest.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.arrays; + +import java.util.stream.Stream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Test for {@link IndexSupport}. + */ +class IndexSupportTest { + + @ParameterizedTest + @MethodSource + void testCheckIndex(int from, int to, int index) { + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> IndexSupport.checkIndex(from, to, index)); + final int[] k1 = new int[] {index}; + final int[] k2 = new int[] {from, to - 1, index}; + final int[] k3 = new int[] {index, from, to - 1}; + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> IndexSupport.checkIndices(from, to, k1)); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> IndexSupport.checkIndices(from, to, k2)); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> IndexSupport.checkIndices(from, to, k3)); + } + + static Stream testCheckIndex() { + final Stream.Builder builder = Stream.builder(); + builder.add(Arguments.of(0, 10, -1)); + builder.add(Arguments.of(0, 10, Integer.MIN_VALUE)); + builder.add(Arguments.of(0, 10, 10)); + builder.add(Arguments.of(5, 10, 0)); + builder.add(Arguments.of(5, Integer.MAX_VALUE, Integer.MAX_VALUE)); + return builder.build(); + } + + @ParameterizedTest + @MethodSource + void testCheckFromToIndex(int from, int to, int length) { + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> IndexSupport.checkFromToIndex(from, to, length)); + } + + static Stream testCheckFromToIndex() { + final Stream.Builder builder = Stream.builder(); + // fromIndex < 0 + builder.add(Arguments.of(-1, 10, 10)); + builder.add(Arguments.of(Integer.MIN_VALUE, 10, 10)); + builder.add(Arguments.of(Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE)); + // fromIndex > toIndex + builder.add(Arguments.of(2, 1, 10)); + builder.add(Arguments.of(20, 10, 10)); + builder.add(Arguments.of(0, -1, 10)); + // toIndex > length + builder.add(Arguments.of(0, 11, 10)); + builder.add(Arguments.of(0, Integer.MAX_VALUE, Integer.MAX_VALUE - 1)); + // length < 0 + builder.add(Arguments.of(0, 1, -1)); + builder.add(Arguments.of(0, 1, Integer.MIN_VALUE)); + builder.add(Arguments.of(0, Integer.MAX_VALUE, Integer.MIN_VALUE)); + return builder.build(); + } +} diff --git a/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/KeyUpdatingIntervalTest.java b/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/KeyUpdatingIntervalTest.java new file mode 100644 index 000000000..1a7abcef5 --- /dev/null +++ b/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/KeyUpdatingIntervalTest.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.arrays; + +import java.util.stream.Stream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Test for {@link KeyUpdatingInterval}. + */ +class KeyUpdatingIntervalTest { + + @ParameterizedTest + @MethodSource + void testSearch(int[] keys, int left, int right) { + // Clip to correct range + final int l = left < 0 ? 0 : left; + final int r = right < 0 ? keys.length - 1 : right; + for (int i = l; i <= r; i++) { + final int k = keys[i]; + // Unspecified index when key is present + Assertions.assertEquals(k, keys[KeyUpdatingInterval.searchLessOrEqual(keys, l, r, k)], "leq"); + Assertions.assertEquals(k, keys[KeyUpdatingInterval.searchGreaterOrEqual(keys, l, r, k)], "geq"); + } + // Search above/below keys + Assertions.assertEquals(l - 1, KeyUpdatingInterval.searchLessOrEqual(keys, l, r, keys[l] - 44), "leq below"); + Assertions.assertEquals(r, KeyUpdatingInterval.searchLessOrEqual(keys, l, r, keys[r] + 44), "leq above"); + Assertions.assertEquals(l, KeyUpdatingInterval.searchGreaterOrEqual(keys, l, r, keys[l] - 44), "geq below"); + Assertions.assertEquals(r + 1, KeyUpdatingInterval.searchGreaterOrEqual(keys, l, r, keys[r] + 44), "geq above"); + // Search between neighbour keys + for (int i = l + 1; i <= r; i++) { + // Bound: keys[i-1] < k < keys[i] + final int k1 = keys[i - 1]; + final int k2 = keys[i]; + for (int k = k1 + 1; k < k2; k++) { + Assertions.assertEquals(i - 1, KeyUpdatingInterval.searchLessOrEqual(keys, l, r, k), "leq between"); + Assertions.assertEquals(i, KeyUpdatingInterval.searchGreaterOrEqual(keys, l, r, k), "geq between"); + } + } + } + + static Stream testSearch() { + final Stream.Builder builder = Stream.builder(); + final int allIndices = -1; + builder.add(Arguments.of(new int[] {1}, allIndices, allIndices)); + builder.add(Arguments.of(new int[] {1, 2}, allIndices, allIndices)); + builder.add(Arguments.of(new int[] {1, 10}, allIndices, allIndices)); + builder.add(Arguments.of(new int[] {1, 2, 3}, allIndices, allIndices)); + builder.add(Arguments.of(new int[] {1, 4, 7}, allIndices, allIndices)); + builder.add(Arguments.of(new int[] {1, 4, 5, 7}, allIndices, allIndices)); + // Duplicates. These match binary search when found. + builder.add(Arguments.of(new int[] {1, 1, 1, 1, 1, 1}, allIndices, allIndices)); + builder.add(Arguments.of(new int[] {1, 1, 1, 1, 3, 3, 3, 3, 3, 5, 5, 5, 5}, allIndices, allIndices)); + // Part of the range + builder.add(Arguments.of(new int[] {1, 4, 5, 7, 13, 15}, 2, 4)); + builder.add(Arguments.of(new int[] {1, 4, 5, 7, 13, 15}, 0, 3)); + builder.add(Arguments.of(new int[] {1, 4, 5, 7, 13, 15}, 3, 5)); + return builder.build(); + } +} diff --git a/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/SelectionTest.java b/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/SelectionTest.java new file mode 100644 index 000000000..43118b925 --- /dev/null +++ b/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/SelectionTest.java @@ -0,0 +1,1034 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.arrays; + +import java.util.Arrays; +import java.util.function.Consumer; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.apache.commons.rng.UniformRandomProvider; +import org.apache.commons.rng.simple.RandomSource; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Test for {@link Selection} and {@link QuickSelect}. + */ +class SelectionTest { + /** Default sub-sampling size for the Floyd-Rivest algorithm. */ + private static final int SU = 1200; + /** Signal to ignore the range of [from, to). */ + private static final int IGNORE_FROM = -1236481268; + + /** + * {@link UpdatingInterval} for range {@code [left, right]}. + */ + static final class RangeInterval implements UpdatingInterval { + /** Left bound of the interval. */ + private int left; + /** Right bound of the interval. */ + private int right; + + /** + * @param left Left bound. + * @param right Right bound. + */ + RangeInterval(int left, int right) { + this.left = left; + this.right = right; + } + + @Override + public int left() { + return left; + } + + @Override + public int right() { + return right; + } + + @Override + public int updateLeft(int k) { + // Assume left < k <= right + left = k; + return k; + } + + @Override + public int updateRight(int k) { + // Assume left <= k < right + right = k; + return k; + } + + @Override + public UpdatingInterval splitLeft(int ka, int kb) { + // Assume left < ka <= kb < right + final int lower = left; + left = kb + 1; + return new RangeInterval(lower, ka - 1); + } + } + + /** + * Partition function. Used to test different implementations. + */ + private interface DoubleRangePartitionFunction { + /** + * Partition the array such that range of indices {@code [ka, kb]} correspond to + * their correctly sorted value in the equivalent fully sorted array. For all + * indices {@code k} and any index {@code i}: + * + *

{@code
+         * data[i < k] <= data[k] <= data[k < i]
+         * }
+ * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param ka Lower index to select. + * @param kb Upper index to select. + */ + void partition(double[] a, int left, int right, int ka, int kb); + } + + /** + * Partition function. Used to test different implementations. + */ + private interface DoublePartitionFunction { + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *
{@code
+         * data[i < k] <= data[k] <= data[k < i]
+         * }
+ * + *

This method allows variable length indices using a count of the indices to + * process. + * + * @param a Values. + * @param k Indices. + * @param n Count of indices. + */ + void partition(double[] a, int[] k, int n); + } + + /** + * Return a sorted copy of the {@code values}. + * + * @param values Values. + * @return the copy + */ + private static double[] sort(double[] values) { + final double[] sorted = values.clone(); + Arrays.sort(sorted); + return sorted; + } + + /** + * Return a copy of the {@code values} sorted in the range {@code [from, to]}. + * + * @param values Values. + * @param from From (inclusive). + * @param to To (inclusive). + * @return the copy + */ + private static double[] sort(double[] values, int from, int to) { + final double[] sorted = values.clone(); + Arrays.sort(sorted, from, to + 1); + return sorted; + } + + /** + * Move NaN values to the end of the array. + * This allows all other values to be compared using {@code <, ==, >} operators (with + * the exception of signed zeros). + * + * @param data Values. + * @return index of last non-NaN value (or -1) + */ + private static int sortNaN(double[] data) { + int end = data.length; + // Find first non-NaN + while (--end >= 0) { + if (!Double.isNaN(data[end])) { + break; + } + } + for (int i = end; --i >= 0;) { + final double v = data[i]; + if (Double.isNaN(v)) { + // swap(data, i, end--) + data[i] = data[end]; + data[end] = v; + end--; + } + } + return end; + } + + /** + * Replace negative zeros with a proxy. Uses -{@link Double#MIN_VALUE} as the proxy. + * + * @param a Data. + * @param from Lower bound (inclusive). + * @param to Upper bound (inclusive). + */ + private static void replaceNegativeZeros(double[] a, int from, int to) { + for (int i = from; i <= to; i++) { + if (Double.doubleToRawLongBits(a[i]) == Long.MIN_VALUE) { + a[i] = -Double.MIN_VALUE; + } + } + } + + /** + * Restore proxy negative zeros. + * + * @param a Data. + * @param from Lower bound (inclusive). + * @param to Upper bound (inclusive). + */ + private static void restoreNegativeZeros(double[] a, int from, int to) { + for (int i = from; i <= to; i++) { + if (a[i] == -Double.MIN_VALUE) { + a[i] = -0.0; + } + } + } + + /** + * Shuffles the entries of the given array. + * + * @param rng Source of randomness. + * @param array Array whose entries will be shuffled (in-place). + * @return Shuffled input array. + */ + // TODO - replace with Commons RNG 1.6: o.a.c.rng.sampling.ArraySampler + private static double[] shuffle(UniformRandomProvider rng, double[] array) { + for (int i = array.length; i > 1; i--) { + swap(array, i - 1, rng.nextInt(i)); + } + return array; + } + + /** + * Swaps the two specified elements in the array. + * + * @param array Array. + * @param i First index. + * @param j Second index. + */ + private static void swap(double[] array, int i, int j) { + final double tmp = array[i]; + array[i] = array[j]; + array[j] = tmp; + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleHeapSelect", "testDoubleSelectMinMax", "testDoubleSelectMinMax2"}) + void testDoubleHeapSelectLeft(double[] values, int from, int to) { + final double[] sorted = sort(values, from, to); + + final double[] x = values.clone(); + final DoubleRangePartitionFunction fun = QuickSelect::heapSelectLeft; + + for (int k = from; k <= to; k++) { + assertPartitionRange(sorted, fun, x.clone(), from, to, k, k); + if (k > from) { + // Sort an extra 1 + assertPartitionRange(sorted, fun, x.clone(), from, to, k - 1, k); + if (k > from + 1) { + // Sort all + // Test clipping with k < from + assertPartitionRange(sorted, fun, x.clone(), from, to, from - 23, k); + } + } + } + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleHeapSelect", "testDoubleSelectMinMax", "testDoubleSelectMinMax2"}) + void testDoubleHeapSelectRight(double[] values, int from, int to) { + final double[] sorted = sort(values, from, to); + + final double[] x = values.clone(); + final DoubleRangePartitionFunction fun = QuickSelect::heapSelectRight; + + for (int k = from; k <= to; k++) { + assertPartitionRange(sorted, fun, x.clone(), from, to, k, k); + if (k < to) { + // Sort an extra 1 + assertPartitionRange(sorted, fun, x.clone(), from, to, k, k + 1); + if (k < to - 1) { + // Sort all + // Test clipping with k > to + assertPartitionRange(sorted, fun, x.clone(), from, to, k, to + 23); + } + } + } + } + + static Stream testDoubleHeapSelect() { + final Stream.Builder builder = Stream.builder(); + builder.add(Arguments.of(new double[] {1}, 0, 0)); + builder.add(Arguments.of(new double[] {3, 2, 1}, 1, 1)); + builder.add(Arguments.of(new double[] {2, 1}, 0, 1)); + builder.add(Arguments.of(new double[] {4, 3, 2, 1}, 1, 2)); + builder.add(Arguments.of(new double[] {-1, 0.0, -0.5, -0.5, 1}, 0, 4)); + builder.add(Arguments.of(new double[] {-1, 0.0, -0.5, -0.5, 1}, 0, 2)); + builder.add(Arguments.of(new double[] {1, 0.0, -0.5, -0.5, -1}, 0, 4)); + builder.add(Arguments.of(new double[] {-1, 2, -3, 4, -4, 3, -2, 1}, 1, 6)); + return builder.build(); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleHeapSelectRange"}) + void testDoubleHeapSelectRange(double[] values, int from, int to, int k1, int k2) { + assertPartitionRange(sort(values, from, to), + QuickSelect::heapSelect, values, from, to, k1, k2); + } + + static Stream testDoubleHeapSelectRange() { + final Stream.Builder builder = Stream.builder(); + builder.add(Arguments.of(new double[] {-1, 2, -3, 4, -4, 3, -2, 1}, 0, 7, 1, 2)); + builder.add(Arguments.of(new double[] {-1, 2, -3, 4, -4, 3, -2, 1}, 0, 7, 2, 2)); + builder.add(Arguments.of(new double[] {-1, 2, -3, 4, -4, 3, -2, 1}, 0, 7, 5, 7)); + builder.add(Arguments.of(new double[] {-1, 2, -3, 4, -4, 3, -2, 1}, 0, 7, 1, 6)); + builder.add(Arguments.of(new double[] {-1, 2, -3, 4, -4, 3, -2, 1}, 0, 7, 0, 3)); + builder.add(Arguments.of(new double[] {-1, 2, -3, 4, -4, 3, -2, 1}, 0, 7, 4, 7)); + return builder.build(); + } + + static Stream testDoubleSelectMinMax() { + final Stream.Builder builder = Stream.builder(); + builder.add(Arguments.of(new double[] {1, 2, 3, 4, 5}, 0, 4)); + builder.add(Arguments.of(new double[] {5, 4, 3, 2, 1}, 0, 4)); + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(); + for (final int size : new int[] {5, 10}) { + final double[] values = rng.doubles(size).toArray(); + builder.add(Arguments.of(values.clone(), 0, size - 1)); + builder.add(Arguments.of(values.clone(), size >>> 1, size - 1)); + builder.add(Arguments.of(values.clone(), 1, size >>> 1)); + } + builder.add(Arguments.of(new double[] {-0.5, 0.0}, 0, 1)); + builder.add(Arguments.of(new double[] {0.0, -0.5}, 0, 1)); + builder.add(Arguments.of(new double[] {-0.5, -0.5}, 0, 1)); + builder.add(Arguments.of(new double[] {0.0, 0.0}, 0, 1)); + builder.add(Arguments.of(new double[] {0.0, -0.5, 0.0, -0.5}, 0, 3)); + builder.add(Arguments.of(new double[] {-0.5, 0.0, -0.5, 0.0}, 0, 3)); + builder.add(Arguments.of(new double[] {0.0, -0.5, -0.5, 0.0}, 0, 3)); + builder.add(Arguments.of(new double[] {-0.5, 0.0, 0.0, -0.5}, 0, 3)); + return builder.build(); + } + + static Stream testDoubleSelectMinMax2() { + final Stream.Builder builder = Stream.builder(); + final double[] values = {-0.5, 0.0, 1}; + final double x = Double.NaN; + final double y = 42; + for (final double a : values) { + for (final double b : values) { + builder.add(Arguments.of(new double[] {a, b}, 0, 1)); + builder.add(Arguments.of(new double[] {x, a, b, y}, 1, 2)); + for (final double c : values) { + builder.add(Arguments.of(new double[] {a, b, c}, 0, 2)); + builder.add(Arguments.of(new double[] {x, a, b, c, y}, 1, 3)); + for (final double d : values) { + builder.add(Arguments.of(new double[] {a, b, c, d}, 0, 3)); + builder.add(Arguments.of(new double[] {x, a, b, c, d, y}, 1, 4)); + } + } + } + } + builder.add(Arguments.of(new double[] {-1, -1, -1, 4, 3, 2, 1, y}, 3, 6)); + builder.add(Arguments.of(new double[] {1, 2, 3, 4, 5}, 0, 4)); + builder.add(Arguments.of(new double[] {5, 4, 3, 2, 1}, 0, 4)); + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(); + for (final int size : new int[] {5, 10}) { + final double[] a = rng.doubles(size).toArray(); + builder.add(Arguments.of(a.clone(), 0, size - 1)); + builder.add(Arguments.of(a.clone(), size >>> 1, size - 1)); + builder.add(Arguments.of(a.clone(), 1, size >>> 1)); + } + builder.add(Arguments.of(new double[] {-0.5, 0.0}, 0, 1)); + builder.add(Arguments.of(new double[] {0.0, -0.5}, 0, 1)); + builder.add(Arguments.of(new double[] {-0.5, -0.5}, 0, 1)); + builder.add(Arguments.of(new double[] {0.0, 0.0}, 0, 1)); + builder.add(Arguments.of(new double[] {0.0, -0.5, 0.0, -0.5}, 0, 3)); + builder.add(Arguments.of(new double[] {-0.5, 0.0, -0.5, 0.0}, 0, 3)); + builder.add(Arguments.of(new double[] {0.0, -0.5, -0.5, 0.0}, 0, 3)); + builder.add(Arguments.of(new double[] {-0.5, 0.0, 0.0, -0.5}, 0, 3)); + return builder.build(); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleHeapSelect", "testDoubleSelectMinMax", "testDoubleSelectMinMax2"}) + void testDoubleSortSelectLeft(double[] values, int from, int to) { + final double[] sorted = sort(values, from, to); + + final double[] x = values.clone(); + final DoubleRangePartitionFunction fun = (a, l, r, ka, kb) -> + QuickSelect.sortSelectLeft(a, l, r, kb); + + for (int k = from; k <= to; k++) { + assertPartitionRange(sorted, fun, x.clone(), from, to, from, k); + } + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleHeapSelect", "testDoubleSelectMinMax", "testDoubleSelectMinMax2"}) + void testDoubleSortSelectRight(double[] values, int from, int to) { + final double[] sorted = sort(values, from, to); + + final double[] x = values.clone(); + final DoubleRangePartitionFunction fun = (a, l, r, ka, kb) -> + QuickSelect.sortSelectRight(a, l, r, ka); + + for (int k = from; k <= to; k++) { + assertPartitionRange(sorted, fun, x.clone(), from, to, k, to); + } + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleHeapSelectRange"}) + void testDoubleSortSelectRange(double[] values, int from, int to, int k1, int k2) { + assertPartitionRange(sort(values, from, to), + QuickSelect::sortSelect, values, from, to, k1, k2); + } + + /** + * Assert the function correctly partitions the range. + * + * @param sorted Expected sort result. + * @param fun Partition function. + * @param values Values. + * @param from From (inclusive). + * @param to To (inclusive). + * @param ka Lower index to select. + * @param kb Upper index to select. + */ + private static void assertPartitionRange(double[] sorted, + DoubleRangePartitionFunction fun, + double[] values, int from, int to, int ka, int kb) { + Arrays.sort(sorted, from, to + 1); + fun.partition(values, from, to, ka, kb); + // Clip + ka = ka < from ? from : ka; + kb = kb > to ? to : kb; + for (int i = ka; i <= kb; i++) { + final int index = i; + Assertions.assertEquals(sorted[i], values[i], () -> "index: " + index); + } + // Check the data is the same + Arrays.sort(values, from, to + 1); + Assertions.assertArrayEquals(sorted, values, "Data destroyed"); + } + + @ParameterizedTest + @MethodSource + void testDoubleSelectThrows(double[] values, int[] indices, int from, int to) { + final double[] x = values.clone(); + final int[] k = indices.clone(); + if (from == IGNORE_FROM) { + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> Selection.select(values, indices)); + } else { + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> Selection.select(values, from, to, indices)); + } + Assertions.assertArrayEquals(x, values, "Data modified"); + Assertions.assertArrayEquals(k, indices, "Indices modified"); + if (k.length != 1) { + return; + } + if (from == IGNORE_FROM) { + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> Selection.select(values, k[0])); + } else { + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> Selection.select(values, from, to, k[0])); + } + Assertions.assertArrayEquals(x, values, "Data modified for single k"); + } + + static Stream testDoubleSelectThrows() { + final Stream.Builder builder = Stream.builder(); + final double[] a = {1, 2, 3, Double.NaN, 0.0, -0.0}; + // Invalid range + builder.add(Arguments.of(a.clone(), new int[] {0}, 0, a.length + 1)); + builder.add(Arguments.of(a.clone(), new int[] {0}, -1, a.length)); + builder.add(Arguments.of(a.clone(), new int[] {0}, 0, 0)); + builder.add(Arguments.of(a.clone(), new int[] {0}, a.length, 0)); + builder.add(Arguments.of(a.clone(), new int[] {1}, 3, 1)); + // Single k + // Full length + builder.add(Arguments.of(a.clone(), new int[] {-1}, IGNORE_FROM, 0)); + builder.add(Arguments.of(a.clone(), new int[] {10}, IGNORE_FROM, 0)); + // Range + builder.add(Arguments.of(a.clone(), new int[] {-1}, 0, 5)); + builder.add(Arguments.of(a.clone(), new int[] {1}, 2, 5)); + builder.add(Arguments.of(a.clone(), new int[] {10}, 2, 5)); + // Multiple k, some invalid + // Full length + builder.add(Arguments.of(a.clone(), new int[] {0, -1, 1, 2}, IGNORE_FROM, 0)); + builder.add(Arguments.of(a.clone(), new int[] {0, 2, 3, 10}, IGNORE_FROM, 0)); + // Range + builder.add(Arguments.of(a.clone(), new int[] {0, -1, 1, 2}, 0, 5)); + builder.add(Arguments.of(a.clone(), new int[] {2, 3, 1}, 2, 5)); + builder.add(Arguments.of(a.clone(), new int[] {2, 10, 3}, 2, 5)); + return builder.build(); + } + + @ParameterizedTest + @MethodSource(value = {"testDoublePartition", "testDoublePartitionBigData"}) + void testDoubleQuickSelectAdaptiveFRSampling(double[] values, int[] indices) { + assertQuickSelectAdaptive(values, indices, QuickSelect.MODE_FR_SAMPLING); + } + + @ParameterizedTest + @MethodSource(value = {"testDoublePartition", "testDoublePartitionBigData"}) + void testDoubleQuickSelectAdaptiveSampling(double[] values, int[] indices) { + assertQuickSelectAdaptive(values, indices, QuickSelect.MODE_SAMPLING); + } + + @ParameterizedTest + @MethodSource(value = {"testDoublePartition", "testDoublePartitionBigData"}) + void testDoubleQuickSelectAdaptiveAdaption(double[] values, int[] indices) { + assertQuickSelectAdaptive(values, indices, QuickSelect.MODE_ADAPTION); + } + + @ParameterizedTest + @MethodSource(value = {"testDoublePartition", "testDoublePartitionBigData"}) + void testDoubleQuickSelectAdaptiveStrict(double[] values, int[] indices) { + assertQuickSelectAdaptive(values, indices, QuickSelect.MODE_STRICT); + } + + private static void assertQuickSelectAdaptive(double[] values, int[] indices, int mode) { + Assumptions.assumeTrue(indices.length == 1 || + (indices.length == 2 && Math.abs(indices[1] - indices[0]) < 10)); + final int k1 = Math.min(indices[0], indices[indices.length - 1]); + final int kn = Math.max(indices[0], indices[indices.length - 1]); + assertPartition(values, indices, (a, k, n) -> { + final int right = sortNaN(a); + if (right < 1) { + return; + } + replaceNegativeZeros(a, 0, right); + QuickSelect.quickSelectAdaptive(a, 0, right, k1, kn, new int[1], mode); + restoreNegativeZeros(a, 0, right); + }, true); + } + + @ParameterizedTest + @MethodSource(value = {"testDoublePartition", "testDoublePartitionBigData"}) + void testDoubleDualPivotQuickSelectMaxRecursion(double[] values, int[] indices) { + assertPartition(values, indices, (a, k, n) -> { + final int right = sortNaN(a); + // Sanitise indices + k = Arrays.stream(k).filter(i -> i <= right).toArray(); + if (right < 1 || k.length == 0) { + return; + } + replaceNegativeZeros(a, 0, right); + QuickSelect.dualPivotQuickSelect(a, 0, right, + IndexSupport.createUpdatingInterval(0, right, k, k.length), + QuickSelect.dualPivotFlags(2, 5)); + restoreNegativeZeros(a, 0, right); + }, false); + } + + @ParameterizedTest + @MethodSource(value = {"testDoublePartition", "testDoublePartitionBigData"}) + void testDoubleSelect(double[] values, int[] indices) { + assertPartition(values, indices, (a, k, n) -> { + double[] b = a; + if (n == 1) { + b = a.clone(); + Selection.select(b, k[0]); + } + Selection.select(a, Arrays.copyOf(k, n)); + if (n == 1) { + Assertions.assertArrayEquals(a, b, "single k mismatch"); + } + }, false); + } + + @ParameterizedTest + @MethodSource(value = {"testDoublePartition", "testDoublePartitionBigData"}) + void testDoubleSelectRange(double[] values, int[] indices) { + assertPartition(values, indices, (a, k, n) -> { + double[] b = a; + if (n == 1) { + b = a.clone(); + Selection.select(b, 0, b.length, k[0]); + } + Selection.select(a, 0, a.length, Arrays.copyOf(k, n)); + if (n == 1) { + Assertions.assertArrayEquals(a, b, "single k mismatch"); + } + }, false); + } + + static void assertPartition(double[] values, int[] indices, DoublePartitionFunction function, + boolean sortedRange) { + final double[] data = values.clone(); + final double[] sorted = sort(values); + // Indices may be destructively modified + function.partition(data, indices.clone(), indices.length); + if (indices.length == 0) { + return; + } + for (final int k : indices) { + Assertions.assertEquals(sorted[k], data[k], () -> "k[" + k + "]"); + } + // Check partial ordering + Arrays.sort(indices); + int i = 0; + for (final int k : indices) { + final double value = sorted[k]; + while (i < k) { + final int j = i; + Assertions.assertTrue(Double.compare(data[i], value) <= 0, + () -> j + " < " + k + " : " + data[j] + " < " + value); + i++; + } + } + final int k = indices[indices.length - 1]; + final double value = sorted[k]; + while (i < data.length) { + final int j = i; + Assertions.assertTrue(Double.compare(data[i], value) >= 0, + () -> k + " < " + j); + i++; + } + if (sortedRange) { + final double[] a = Arrays.copyOfRange(sorted, indices[0], k + 1); + final double[] b = Arrays.copyOfRange(data, indices[0], k + 1); + Assertions.assertArrayEquals(a, b, "Entire range of indices is not sorted"); + } + Arrays.sort(data); + Assertions.assertArrayEquals(sorted, data, "Data destroyed"); + } + + static Stream testDoublePartition() { + final Stream.Builder builder = Stream.builder(); + UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(123); + // Sizes above and below the threshold for partitioning. + // The largest size should trigger single-pivot sub-sampling for pivot selection. + for (final int size : new int[] {5, 47, SU + 10}) { + final int halfSize = size >>> 1; + final int from = -halfSize; + final int to = -halfSize + size; + final double[] values = IntStream.range(from, to).asDoubleStream().toArray(); + final double[] zeros = values.clone(); + final int quarterSize = size >>> 2; + Arrays.fill(zeros, quarterSize, halfSize, -0.0); + Arrays.fill(zeros, halfSize, halfSize + quarterSize, 0.0); + for (final int k : new int[] {1, 2, 3, size}) { + for (int i = 0; i < 15; i++) { + // Note: Duplicate indices do not matter + final int[] indices = rng.ints(k, 0, size).toArray(); + builder.add(Arguments.of( + shuffle(rng, values.clone()), + indices.clone())); + builder.add(Arguments.of( + shuffle(rng, zeros.clone()), + indices.clone())); + } + } + // Test sequential processing by creating potential ranges + // after an initial low point. This should be high enough + // so any range analysis that joins indices will leave the initial + // index as a single point. + final int limit = 50; + if (size > limit) { + for (int i = 0; i < 10; i++) { + final int[] indices = rng.ints(size - limit, limit, size).toArray(); + // This sets a low index + indices[rng.nextInt(indices.length)] = rng.nextInt(0, limit >>> 1); + builder.add(Arguments.of( + shuffle(rng, values.clone()), + indices.clone())); + } + } + // min; max; min/max + builder.add(Arguments.of(values.clone(), new int[] {0})); + builder.add(Arguments.of(values.clone(), new int[] {size - 1})); + builder.add(Arguments.of(values.clone(), new int[] {0, size - 1})); + builder.add(Arguments.of(zeros.clone(), new int[] {0})); + builder.add(Arguments.of(zeros.clone(), new int[] {size - 1})); + builder.add(Arguments.of(zeros.clone(), new int[] {0, size - 1})); + } + final double nan = Double.NaN; + builder.add(Arguments.of(new double[] {}, new int[0])); + builder.add(Arguments.of(new double[] {nan}, new int[] {0})); + builder.add(Arguments.of(new double[] {-0.0, nan}, new int[] {1})); + builder.add(Arguments.of(new double[] {nan, nan, nan}, new int[] {2})); + builder.add(Arguments.of(new double[] {nan, 0.0, -0.0, nan}, new int[] {3})); + builder.add(Arguments.of(new double[] {nan, 0.0, -0.0, nan}, new int[] {1, 2})); + builder.add(Arguments.of(new double[] {nan, 0.0, 1, -0.0, nan}, new int[] {1, 3})); + builder.add(Arguments.of(new double[] {nan, 0.0, -0.0}, new int[] {0, 2})); + builder.add(Arguments.of(new double[] {nan, 1.23, 0.0, -4.56, -0.0, nan}, new int[] {0, 1, 3})); + // Dual-pivot with a large middle region (> 5 / 8) requires equal elements loop + final int n = 128; + final double[] x = IntStream.range(0, n).asDoubleStream().toArray(); + // Put equal elements in the central region: + // 2/16 6/16 10/16 14/16 + // | P2 | + final int sixteenth = n / 16; + final int i2 = 2 * sixteenth; + final int i6 = 6 * sixteenth; + final double p1 = x[i2]; + final double p2 = x[n - i2]; + // Lots of values equal to the pivots + Arrays.fill(x, i2, i6, p1); + Arrays.fill(x, n - i6, n - i2, p2); + // Equal value in between the pivots + Arrays.fill(x, i6, n - i6, (p1 + p2) / 2); + // Shuffle this and partition in the middle. + // Also partition with the pivots in P1 and P2 using thirds. + final int third = (int) (n / 3.0); + // Use a fix seed to ensure we hit coverage with only 5 loops. + rng = RandomSource.XO_SHI_RO_128_PP.create(-8111061151820577011L); + for (int i = 0; i < 5; i++) { + builder.add(Arguments.of(shuffle(rng, x.clone()), new int[] {n >> 1})); + builder.add(Arguments.of(shuffle(rng, x.clone()), + new int[] {third, 2 * third})); + } + // A single value smaller/greater than the pivot at the left/right/both ends + Arrays.fill(x, 1); + for (int i = 0; i <= 2; i++) { + for (int j = 0; j <= 2; j++) { + x[n - 1] = i; + x[0] = j; + builder.add(Arguments.of(x.clone(), new int[] {50})); + } + } + // Reverse data. Makes it simple to detect failed range selection. + final double[] a = IntStream.range(0, 50).asDoubleStream().toArray(); + for (int i = -1, j = a.length; ++i < --j;) { + final double v = a[i]; + a[i] = a[j]; + a[j] = v; + } + builder.add(Arguments.of(a, new int[] {1, 1})); + builder.add(Arguments.of(a, new int[] {1, 2})); + builder.add(Arguments.of(a, new int[] {10, 12})); + builder.add(Arguments.of(a, new int[] {10, 42})); + builder.add(Arguments.of(a, new int[] {1, 48})); + builder.add(Arguments.of(a, new int[] {48, 49})); + return builder.build(); + } + + static Stream testDoublePartitionBigData() { + final Stream.Builder builder = Stream.builder(); + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(123); + // Sizes above the threshold (1200) for recursive partitioning + for (final int size : new int[] {1000, 5000, 10000}) { + final double[] a = IntStream.range(0, size).asDoubleStream().toArray(); + // With repeat elements + final double[] b = rng.ints(size, 0, size >> 3).asDoubleStream().toArray(); + for (int i = 0; i < 15; i++) { + builder.add(Arguments.of( + shuffle(rng, a.clone()), + new int[] {rng.nextInt(size)})); + builder.add(Arguments.of(b.clone(), + new int[] {rng.nextInt(size)})); + } + } + // Hit Floyd-Rivest sub-sampling conditions. + // Close to edge but outside edge select size. + final int n = 7000; + final double[] x = IntStream.range(0, n).asDoubleStream().toArray(); + builder.add(Arguments.of(x.clone(), new int[] {20})); + builder.add(Arguments.of(x.clone(), new int[] {n - 1 - 20})); + // Constant value when using FR partitioning + Arrays.fill(x, 1.23); + builder.add(Arguments.of(x, new int[] {x.length >>> 1})); + return builder.build(); + } + + @ParameterizedTest + @MethodSource + void testDoubleExpandPartition(double[] values, int start, int end, int pivot0, int pivot1) { + final int[] upper = new int[1]; + final double[] sorted = sort(values); + final double v = values[pivot0]; + final int p0 = QuickSelect.expandPartition(values, 0, values.length - 1, start, end, pivot0, pivot1, upper); + final int p1 = upper[0]; + for (int i = 0; i < p0; i++) { + final int index = i; + Assertions.assertTrue(values[i] < v, + () -> String.format("[%d] : %s < %s", index, values[index], v)); + } + for (int i = p0; i <= p1; i++) { + final int index = i; + Assertions.assertEquals(v, values[i], + () -> String.format("[%d] : %s == %s", index, values[index], v)); + } + for (int i = p1 + 1; i < values.length; i++) { + final int index = i; + Assertions.assertTrue(values[i] > v, + () -> String.format("[%d] : %s > %s", index, values[index], v)); + } + Arrays.sort(values); + Assertions.assertArrayEquals(sorted, values, "Data destroyed"); + } + + static Stream testDoubleExpandPartition() { + final Stream.Builder builder = Stream.builder(); + // Create data: + // |l |start |p0 p1| end| r| + // | ??? | < | == | > | ??? | + // Arguments: data, start, end, pivot0, pivot1 + + // Create the data with unique values 42 and 0 either side of + // [start, end] (e.g. region ???). These are permuted for -1 and 10 + // to create cases that may or not have to swap elements. + + // Single pivot + addExpandPartitionArguments(builder, new double[] {42, 1, 2, 3, 4, 0}, 1, 4, 2, 2); + // Pivot range + addExpandPartitionArguments(builder, new double[] {42, 1, 2, 2, 3, 0}, 1, 4, 2, 3); + // Single pivot at start/end + addExpandPartitionArguments(builder, new double[] {42, 1, 2, 3, 4, 0}, 1, 4, 1, 1); + addExpandPartitionArguments(builder, new double[] {42, 1, 2, 3, 4, 0}, 1, 4, 4, 4); + // Pivot range at start/end + addExpandPartitionArguments(builder, new double[] {42, 1, 1, 2, 3, 0}, 1, 4, 1, 2); + addExpandPartitionArguments(builder, new double[] {42, 1, 2, 3, 3, 0}, 1, 4, 3, 4); + addExpandPartitionArguments(builder, new double[] {42, 1, 2, 2, 2, 0}, 1, 4, 2, 4); + addExpandPartitionArguments(builder, new double[] {42, 1, 1, 1, 2, 0}, 1, 4, 1, 3); + addExpandPartitionArguments(builder, new double[] {42, 1, 1, 1, 1, 0}, 1, 4, 1, 4); + + // Single pivot at left/right + addExpandPartitionArguments(builder, new double[] {1, 2, 3, 4, 0}, 0, 3, 0, 0); + addExpandPartitionArguments(builder, new double[] {42, 1, 2, 3, 4}, 1, 4, 4, 4); + // Pivot range at left/right + addExpandPartitionArguments(builder, new double[] {1, 1, 2, 3, 4, 0}, 0, 4, 0, 1); + addExpandPartitionArguments(builder, new double[] {42, 1, 2, 3, 4, 4}, 1, 5, 4, 5); + addExpandPartitionArguments(builder, new double[] {1, 1, 1, 1, 2, 0}, 0, 4, 0, 3); + addExpandPartitionArguments(builder, new double[] {42, 3, 4, 4, 4, 4}, 1, 5, 2, 5); + addExpandPartitionArguments(builder, new double[] {1, 1, 1, 1, 1, 0}, 0, 4, 0, 4); + addExpandPartitionArguments(builder, new double[] {42, 4, 4, 4, 4, 4}, 1, 5, 1, 5); + + // Minimum range: [start, end] == length 2 + addExpandPartitionArguments(builder, new double[] {42, 1, 2, 0}, 1, 2, 1, 1); + addExpandPartitionArguments(builder, new double[] {42, 1, 2, 0}, 1, 2, 2, 2); + addExpandPartitionArguments(builder, new double[] {42, 1, 1, 0}, 1, 2, 1, 2); + addExpandPartitionArguments(builder, new double[] {42, 1, 2}, 1, 2, 1, 1); + addExpandPartitionArguments(builder, new double[] {42, 1, 2}, 1, 2, 2, 2); + addExpandPartitionArguments(builder, new double[] {42, 1, 1}, 1, 2, 1, 2); + addExpandPartitionArguments(builder, new double[] {1, 2, 0}, 0, 1, 0, 0); + addExpandPartitionArguments(builder, new double[] {1, 2, 0}, 0, 1, 1, 1); + addExpandPartitionArguments(builder, new double[] {1, 1, 0}, 0, 1, 0, 1); + addExpandPartitionArguments(builder, new double[] {1, 2}, 0, 1, 0, 0); + addExpandPartitionArguments(builder, new double[] {1, 2}, 0, 1, 1, 1); + addExpandPartitionArguments(builder, new double[] {1, 1}, 0, 1, 0, 1); + + return builder.build(); + } + + private static void addExpandPartitionArguments(Stream.Builder builder, + double[] a, int start, int end, int pivot0, int pivot1) { + builder.add(Arguments.of(a.clone(), start, end, pivot0, pivot1)); + final double[] b = a.clone(); + if (replace(a, 42, -1)) { + builder.add(Arguments.of(a.clone(), start, end, pivot0, pivot1)); + if (replace(a, 0, 10)) { + builder.add(Arguments.of(a, start, end, pivot0, pivot1)); + } + } + if (replace(b, 0, 10)) { + builder.add(Arguments.of(b, start, end, pivot0, pivot1)); + } + } + + private static boolean replace(double[] a, int x, int y) { + boolean updated = false; + for (int i = 0; i < a.length; i++) { + if (a[i] == x) { + a[i] = y; + updated = true; + } + } + return updated; + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort"}) + void testDoubleSortUsingHeapSelect(double[] values) { + Assumptions.assumeTrue(values.length > 0); + assertSort(values, x -> { + final int right = sortNaN(x); + // heapSelect is robust to right <= left + replaceNegativeZeros(x, 0, right); + QuickSelect.heapSelect(x, 0, right, 0, right); + restoreNegativeZeros(x, 0, right); + }); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort"}) + void testDoubleSortUsingHeapSelectLeft(double[] values) { + Assumptions.assumeTrue(values.length > 0); + assertSort(values, x -> { + final int right = sortNaN(x); + if (right < 1) { + return; + } + replaceNegativeZeros(x, 0, right); + QuickSelect.heapSelectLeft(x, 0, right, 0, right); + restoreNegativeZeros(x, 0, right); + }); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort"}) + void testDoubleSortUsingHeapSelectRight(double[] values) { + Assumptions.assumeTrue(values.length > 0); + assertSort(values, x -> { + final int right = sortNaN(x); + if (right < 1) { + return; + } + replaceNegativeZeros(x, 0, right); + QuickSelect.heapSelectRight(x, 0, right, 0, right); + restoreNegativeZeros(x, 0, right); + }); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort"}) + void testDoubleSortUsingSelection(double[] values) { + // This tests that the select function performs + // a full sort when the interval is saturated + assertSort(values, a -> { + final int right = sortNaN(a); + if (right < 1) { + return; + } + replaceNegativeZeros(a, 0, right); + QuickSelect.dualPivotQuickSelect(a, 0, right, new RangeInterval(0, right), + QuickSelect.dualPivotFlags(QuickSelect.dualPivotMaxDepth(right), 20)); + restoreNegativeZeros(a, 0, right); + }); + } + + private static void assertSort(double[] values, Consumer function) { + final double[] data = values.clone(); + final double[] sorted = sort(values); + function.accept(data); + Assertions.assertArrayEquals(sorted, data); + } + + static Stream testDoubleSort() { + final Stream.Builder builder = Stream.builder(); + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(123); + // Sizes above and below the threshold for partitioning + for (final int size : new int[] {5, 50}) { + double[] a = new double[size]; + Arrays.fill(a, 1.23); + builder.add(a.clone()); + for (int ii = 0; ii < size; ii++) { + a[ii] = ii; + } + builder.add(a.clone()); + for (int ii = 0; ii < size; ii++) { + a[ii] = size - ii; + } + builder.add(a.clone()); + for (int i = 0; i < 5; i++) { + a = rng.doubles(size).toArray(); + builder.add(a.clone()); + final int j = rng.nextInt(size); + final int k = rng.nextInt(size); + a[j] = Double.NaN; + a[k] = Double.NaN; + builder.add(a.clone()); + a[j] = -0.0; + a[k] = 0.0; + builder.add(a.clone()); + for (int z = 0; z < size; z++) { + a[z] = rng.nextBoolean() ? -0.0 : 0.0; + } + builder.add(a.clone()); + a[j] = -rng.nextDouble(); + a[k] = rng.nextDouble(); + builder.add(a.clone()); + } + } + final double nan = Double.NaN; + builder.add(new double[] {}); + builder.add(new double[] {nan}); + builder.add(new double[] {-0.0, nan}); + builder.add(new double[] {nan, nan, nan}); + builder.add(new double[] {nan, 0.0, -0.0, nan}); + builder.add(new double[] {nan, 0.0, -0.0}); + builder.add(new double[] {nan, 0.0, 1, -0.0}); + builder.add(new double[] {nan, 1.23, 0.0, -4.56, -0.0, nan}); + return builder.build(); + } + + @Test + void testDualPivotMaxDepth() { + // Reasonable behaviour at small x + Assertions.assertEquals(0, log3(0)); + Assertions.assertEquals(0, log3(1)); + Assertions.assertEquals(1, log3(2)); + Assertions.assertEquals(1, log3(3)); + Assertions.assertEquals(1, log3(4)); + Assertions.assertEquals(1, log3(5)); + Assertions.assertEquals(1, log3(6)); + Assertions.assertEquals(1, log3(7)); + Assertions.assertEquals(2, log3(8)); + // log3(2^31-1) = 19.5588223... + Assertions.assertEquals(19, log3(Integer.MAX_VALUE)); + // Create a series of powers of 3, start at 3^2 + long p = 3; + for (int i = 2;; i++) { + p *= 3; + if (p > Integer.MAX_VALUE) { + break; + } + final int x = (int) p; + // Computes round(log3(x)) when x is close to a power of 3 + Assertions.assertEquals(i, log3(x - 1)); + Assertions.assertEquals(i, log3(x)); + Assertions.assertEquals(i, log3(x + 1)); + // Half-way point is within the bracket [i, i+1] + final int y = (int) Math.floor(Math.pow(3, i + 0.5)); + Assertions.assertTrue(log3(y) >= i); + Assertions.assertTrue(log3(y + 1) <= i + 1); + } + } + + /** + * Compute an approximation to log3(x). + * + * @param x Value + * @return log3(x) + */ + private static int log3(int x) { + // Use half of the dual-pivot max recursion depth + return QuickSelect.dualPivotMaxDepth(x) >>> 1; + } +} diff --git a/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/SortingTest.java b/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/SortingTest.java new file mode 100644 index 000000000..310947a5e --- /dev/null +++ b/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/SortingTest.java @@ -0,0 +1,474 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.arrays; + +import java.util.Arrays; +import java.util.BitSet; +import java.util.function.Consumer; +import java.util.function.ToIntFunction; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.apache.commons.rng.UniformRandomProvider; +import org.apache.commons.rng.sampling.PermutationSampler; +import org.apache.commons.rng.simple.RandomSource; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Test for {@link Sorting}. + * + *

Sorting tests for floating point values do not include NaN or signed zero (-0.0). + */ +class SortingTest { + + /** + * Interface to test sorting of indices. +§ */ + interface IndexSort { + /** + * Sort the indices into unique ascending order. + * + * @param a Indices. + * @param n Number of indices. + * @return number of unique indices. + */ + int insertionSortIndices(int[] a, int n); + } + + // double[] + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort"}) + void testDoubleInsertionSort(double[] values) { + assertSort(values, x -> Sorting.sort(x, 0, x.length - 1)); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort", "testDoubleSort3"}) + void testDoubleSort3(double[] values) { + final double[] data = Arrays.copyOf(values, 3); + assertSort(data, x -> Sorting.sort3(x, 0, 1, 2)); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort", "testDoubleSort5"}) + void testDoubleSort5(double[] values) { + final double[] data = Arrays.copyOf(values, 5); + assertSort(data, x -> Sorting.sort5(x, 0, 1, 2, 3, 4)); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort3Internal"}) + void testDoubleSort3Internal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + assertSortInternal(values, x -> Sorting.sort3(x, a, b, c), indices); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort5Internal"}) + void testDoubleSort5Internal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + final int d = indices[3]; + final int e = indices[4]; + assertSortInternal(values, x -> Sorting.sort5(x, a, b, c, d, e), indices); + } + + /** + * Assert that the sort {@code function} computes the same result as + * {@link Arrays#sort(double[])}. + * + * @param values Data. + * @param function Sort function. + */ + private static void assertSort(double[] values, Consumer function) { + final double[] expected = values.clone(); + Arrays.sort(expected); + final double[] actual = values.clone(); + function.accept(actual); + Assertions.assertArrayEquals(expected, actual, "Invalid sort"); + } + + /** + * Assert that the sort {@code function} computes the same result as + * {@link Arrays#sort(double[])} run on the provided {@code indices}. + * + * @param values Data. + * @param function Sort function. + * @param indices Indices. + */ + private static void assertSortInternal(double[] values, Consumer function, int... indices) { + Assertions.assertFalse(containsDuplicates(indices), () -> "Duplicate indices: " + Arrays.toString(indices)); + // Pick out the data to sort + final double[] expected = extractIndices(values, indices); + Arrays.sort(expected); + final double[] data = values.clone(); + function.accept(data); + // Pick out the data that was sorted + final double[] actual = extractIndices(data, indices); + Assertions.assertArrayEquals(expected, actual, "Invalid sort"); + // Check outside the sorted indices + OUTSIDE: for (int i = 0; i < values.length; i++) { + for (final int ignore : indices) { + if (i == ignore) { + continue OUTSIDE; + } + } + Assertions.assertEquals(values[i], data[i]); + } + } + + static Stream testDoubleSort() { + final Stream.Builder builder = Stream.builder(); + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(); + for (final int size : new int[] {10, 15}) { + double[] a = new double[size]; + Arrays.fill(a, 1.23); + builder.add(a.clone()); + for (int ii = 0; ii < size; ii++) { + a[ii] = ii; + } + builder.add(a.clone()); + for (int ii = 0; ii < size; ii++) { + a[ii] = size - ii; + } + builder.add(a); + for (int i = 0; i < 5; i++) { + builder.add(rng.doubles(size).toArray().clone()); + } + } + return builder.build(); + } + + static Stream testDoubleSort3() { + // Permutations is 3! = 6 + final double x = 3.35; + final double y = 12.3; + final double z = -9.99; + final double[][] a = { + {x, y, z}, + {x, z, y}, + {z, x, y}, + {y, x, z}, + {y, z, x}, + {z, y, x}, + }; + return Arrays.stream(a); + } + + static Stream testDoubleSort5() { + final Stream.Builder builder = Stream.builder(); + final double[] a = new double[5]; + // Permutations is 5! = 120 + final int shift = 42; + for (int i = 0; i < 5; i++) { + a[0] = i + shift; + for (int j = 0; j < 5; j++) { + if (j == i) { + continue; + } + a[1] = j + shift; + for (int k = 0; k < 5; k++) { + if (k == j || k == i) { + continue; + } + a[2] = k + shift; + for (int l = 0; l < 5; l++) { + if (l == k || l == j || l == i) { + continue; + } + a[3] = l + shift; + for (int m = 0; m < 5; m++) { + if (m == l || m == k || m == j || m == i) { + continue; + } + a[3] = m + shift; + builder.add(a.clone()); + } + } + } + } + } + return builder.build(); + } + + static Stream testDoubleSort3Internal() { + return testDoubleSortInternal(3); + } + + static Stream testDoubleSort5Internal() { + return testDoubleSortInternal(5); + } + + static Stream testDoubleSortInternal(int k) { + final Stream.Builder builder = Stream.builder(); + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(); + for (final int size : new int[] {k, 2 * k, 4 * k}) { + final PermutationSampler s = new PermutationSampler(rng, size, k); + for (int i = k * k; i-- >= 0;) { + final double[] a = rng.doubles(size).toArray(); + final int[] indices = s.sample(); + builder.add(Arguments.of(a, indices)); + } + } + return builder.build(); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort4Internal"}) + void testDoubleLowerMedian4Internal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + final int d = indices[3]; + assertMedian(values, x -> { + Sorting.lowerMedian4(x, a, b, c, d); + return b; + }, true, false, indices); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort4Internal"}) + void testDoubleUpperMedian4Internal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + final int d = indices[3]; + assertMedian(values, x -> { + Sorting.upperMedian4(x, a, b, c, d); + return c; + }, false, false, indices); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort4"}) + void testDoubleLowerMedian4(double[] a) { + // This method computes in place + assertMedian(a, x -> { + Sorting.lowerMedian4(x, 0, 1, 2, 3); + return 1; + }, true, true, 0, 1, 2, 3); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort4"}) + void testDoubleUpperMedian4(double[] a) { + // This method computes in place + assertMedian(a, x -> { + Sorting.upperMedian4(x, 0, 1, 2, 3); + return 2; + }, false, true, 0, 1, 2, 3); + } + + /** + * Assert that the median {@code function} computes the same result as + * {@link Arrays#sort(double[])} run on the provided {@code indices}. + * + * @param values Data. + * @param function Sort function. + * @param lower For even lengths use the lower median; else the upper median. + * @param stable If true then no swaps should be made on the second pass. + * @param indices Indices. + */ + private static void assertMedian(double[] values, ToIntFunction function, + boolean lower, boolean stable, int... indices) { + Assertions.assertFalse(containsDuplicates(indices), () -> "Duplicate indices: " + Arrays.toString(indices)); + // Pick out the data to sort + final double[] expected = extractIndices(values, indices); + Arrays.sort(expected); + final double[] data = values.clone(); + final int m = function.applyAsInt(data); + Assertions.assertEquals(expected[(lower ? -1 : 0) + (expected.length >>> 1)], data[m]); + // Check outside the sorted indices + OUTSIDE: for (int i = 0; i < values.length; i++) { + for (final int ignore : indices) { + if (i == ignore) { + continue OUTSIDE; + } + } + Assertions.assertEquals(values[i], data[i]); + } + // This is not a strict requirement but check that no swaps occur on a second pass + if (stable) { + final double[] x = data.clone(); + final int m2 = function.applyAsInt(data); + Assertions.assertEquals(m, m2); + Assertions.assertArrayEquals(x, data); + } + } + + + static Stream testDoubleSort4() { + final Stream.Builder builder = Stream.builder(); + final double[] a = new double[4]; + // Permutations is 4! = 24 + final int shift = 42; + for (int i = 0; i < 4; i++) { + a[0] = i + shift; + for (int j = 0; j < 4; j++) { + if (j == i) { + continue; + } + a[1] = j + shift; + for (int k = 0; k < 4; k++) { + if (k == j || k == i) { + continue; + } + a[2] = k + shift; + for (int l = 0; l < 4; l++) { + if (l == k || l == j || l == i) { + continue; + } + a[3] = l + shift; + builder.add(a.clone()); + } + } + } + } + return builder.build(); + } + + static Stream testDoubleSort4Internal() { + final int k = 4; + final Stream.Builder builder = Stream.builder(); + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(); + for (final int size : new int[] {k, 2 * k, 4 * k}) { + double[] a = rng.doubles(size).toArray(); + final PermutationSampler s = new PermutationSampler(rng, size, k); + for (int i = k * k; i-- >= 0;) { + a = rng.doubles(size).toArray(); + final int[] indices = s.sample(); + builder.add(Arguments.of(a, indices)); + } + } + return builder.build(); + } + + // Sorting unique indices + + @ParameterizedTest + @MethodSource(value = {"testSortIndices"}) + void testInsertionSortIndices(int[] values, int n) { + assertSortIndices(Sorting::insertionSortIndices, values, n, 1); + } + + @ParameterizedTest + @MethodSource(value = {"testSortIndices"}) + void testSortIndices(int[] values, int n) { + assertSortIndices(Sorting::sortIndices, values, n, 1); + } + + private static void assertSortIndices(IndexSort fun, int[] values, int n, int minSupportedLength) { + // Negative n is a signal to use the full length + n = n < 0 ? values.length : n; + Assumptions.assumeTrue(n >= minSupportedLength); + final int[] x = values.clone(); + final int[] expected = Arrays.stream(values).limit(n) + .distinct().sorted().toArray(); + final int unique = fun.insertionSortIndices(x, n); + Assertions.assertEquals(expected.length, unique, "Incorrect unique length"); + for (int i = 0; i < expected.length; i++) { + final int index = i; + Assertions.assertEquals(expected[i], x[i], () -> "Error @ " + index); + } + // Test values after unique should be in the entire original data + final BitSet set = new BitSet(); + Arrays.stream(values).limit(n).forEach(set::set); + for (int i = expected.length; i < n; i++) { + Assertions.assertTrue(set.get(x[i]), "Data up to n destroyed"); + } + // Data after n should be untouched + for (int i = n; i < values.length; i++) { + Assertions.assertEquals(values[i], x[i], "Data after n destroyed"); + } + } + + static Stream testSortIndices() { + // Create data that should exercise all strategies in the heuristics in + // Sorting::sortIndices used to choose a sorting method + final Stream.Builder builder = Stream.builder(); + // Use length -1 to use the array length + builder.add(Arguments.of(new int[0], -1)); + builder.add(Arguments.of(new int[3], -1)); + builder.add(Arguments.of(new int[3], -1)); + builder.add(Arguments.of(new int[] {42}, -1)); + builder.add(Arguments.of(new int[] {1, 2, 3}, -1)); + builder.add(Arguments.of(new int[] {3, 2, 1}, -1)); + builder.add(Arguments.of(new int[] {42, 5, 7}, -1)); + // Duplicates + builder.add(Arguments.of(new int[] {1, 1}, -1)); + builder.add(Arguments.of(new int[] {1, 1, 1}, -1)); + builder.add(Arguments.of(new int[] {42, 5, 2, 9, 2, 9, 7, 7, 4}, -1)); + // Truncated indices + builder.add(Arguments.of(new int[] {3, 2, 1}, 1)); + builder.add(Arguments.of(new int[] {3, 2, 1}, 2)); + builder.add(Arguments.of(new int[] {2, 2, 1}, 2)); + builder.add(Arguments.of(new int[] {42, 5, 7, 7, 4}, 3)); + builder.add(Arguments.of(new int[] {5, 4, 3, 2, 1}, 3)); + builder.add(Arguments.of(new int[] {1, 2, 3, 4, 5}, 3)); + builder.add(Arguments.of(new int[] {5, 3, 1, 2, 4}, 3)); + // Some random indices with duplicates + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(); + for (final int size : new int[] {5, 10, 30}) { + final int maxIndex = size >>> 1; + for (int i = 0; i < 5; i++) { + builder.add(Arguments.of(rng.ints(size, 0, maxIndex).toArray(), -1)); + } + } + // A lot of duplicates + builder.add(Arguments.of(rng.ints(50, 0, 3).toArray(), -1)); + builder.add(Arguments.of(rng.ints(50, 0, 5).toArray(), -1)); + builder.add(Arguments.of(rng.ints(50, 0, 10).toArray(), -1)); + // Bug where the first index was ignored when using an IndexSet + builder.add(Arguments.of(IntStream.range(0, 50).map(x -> 50 - x).toArray(), -1)); + // Sparse + builder.add(Arguments.of(rng.ints(25, 0, 100000).toArray(), -1)); + // Ascending + builder.add(Arguments.of(IntStream.range(99, 134).toArray(), -1)); + builder.add(Arguments.of(IntStream.range(99, 134).map(x -> x * 2).toArray(), -1)); + builder.add(Arguments.of(IntStream.range(99, 134).map(x -> x * 3).toArray(), -1)); + return builder.build(); + } + + // Helper methods + + private static double[] extractIndices(double[] values, int[] indices) { + final double[] data = new double[indices.length]; + for (int i = 0; i < indices.length; i++) { + data[i] = values[indices[i]]; + } + return data; + } + + private static boolean containsDuplicates(int[] indices) { + for (int i = 0; i < indices.length; i++) { + for (int j = 0; j < i; j++) { + if (indices[i] == indices[j]) { + return true; + } + } + } + return false; + } +} diff --git a/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/UpdatingIntervalTest.java b/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/UpdatingIntervalTest.java new file mode 100644 index 000000000..40f59b206 --- /dev/null +++ b/commons-numbers-arrays/src/test/java/org/apache/commons/numbers/arrays/UpdatingIntervalTest.java @@ -0,0 +1,371 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.arrays; + +import java.util.Arrays; +import java.util.BitSet; +import java.util.function.BiFunction; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.apache.commons.rng.UniformRandomProvider; +import org.apache.commons.rng.sampling.PermutationSampler; +import org.apache.commons.rng.simple.RandomSource; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Test for {@link UpdatingInterval} implementations. + */ +class UpdatingIntervalTest { + + /** + * Create a KeyUpdatingInterval with the {@code indices}. This will create + * distinct and sorted indices. + * + * @param indices Indices. + * @param n Number of indices. + * @return the interval + * @throws IllegalArgumentException if {@code n == 0} + */ + private static KeyUpdatingInterval createKeyUpdatingInterval(int[] indices, int n) { + if (n <= 0) { + throw new IllegalArgumentException("No indices to define the range"); + } + // If duplicates are not removed then the test will fail during the split + // if the splitting index is a duplicate. + final int[] k = Arrays.stream(indices).distinct().sorted().toArray(); + return new KeyUpdatingInterval(k, k.length); + } + + /** + * Create a BitIndexUpdatingInterval with the {@code indices}. The capacity is defined by the + * range required to store the minimum and maximum index. + * + * @param indices Indices. + * @param n Number of indices. + * @return the interval + * @throws IllegalArgumentException if {@code n == 0} + */ + private static BitIndexUpdatingInterval createBitIndexUpdatingInterval(int[] indices, int n) { + if (n <= 0) { + throw new IllegalArgumentException("No indices to define the range"); + } + int min = indices[0]; + int max = min; + for (int i = 1; i < n; i++) { + min = Math.min(min, indices[i]); + max = Math.max(max, indices[i]); + } + final BitIndexUpdatingInterval set = new BitIndexUpdatingInterval(min, max); + for (int i = -1; ++i < n;) { + set.set(indices[i]); + } + return set; + } + + @ParameterizedTest + @MethodSource(value = {"testIndices"}) + void testUpdateKeyInterval(int[] indices, int[] k) { + assertUpdate(UpdatingIntervalTest::createKeyUpdatingInterval, indices, k); + } + + @ParameterizedTest + @MethodSource(value = {"testIndices"}) + void testUpdateBitIndexUpdatingInterval(int[] indices, int[] k) { + // Skip this due to excess memory consumption + Assumptions.assumeTrue(k[k.length - 1] < Integer.MAX_VALUE - 1); + assertUpdate(UpdatingIntervalTest::createBitIndexUpdatingInterval, indices, k); + } + + @ParameterizedTest + @MethodSource(value = {"testIndices"}) + void testUpdateIndexSupport(int[] indices, int[] k) { + final int l = k[0]; + final int r = k[k.length - 1]; + assertUpdate((x, n) -> IndexSupport.createUpdatingInterval(l, r, x, n), indices, k); + } + + @ParameterizedTest + @MethodSource(value = {"testIndices"}) + void testSplitKeyInterval(int[] indices, int[] k) { + assertSplit(UpdatingIntervalTest::createKeyUpdatingInterval, indices, k); + } + + @ParameterizedTest + @MethodSource(value = {"testIndices"}) + void testSplitBitIndexUpdatingInterval(int[] indices, int[] k) { + // Skip this due to excess memory consumption + Assumptions.assumeTrue(k[k.length - 1] < Integer.MAX_VALUE - 1); + assertSplit(UpdatingIntervalTest::createBitIndexUpdatingInterval, indices, k); + } + + @ParameterizedTest + @MethodSource(value = {"testIndices"}) + void testSplitIndexSupport(int[] indices, int[] k) { + final int l = k[0]; + final int r = k[k.length - 1]; + assertSplit((x, n) -> IndexSupport.createUpdatingInterval(l, r, x, n), indices, k); + } + + /** + * Assert the {@link UpdatingInterval#updateLeft(int)} and {@link UpdatingInterval#updateRight(int)} methods. + * These are tested by successive calls to reduce the interval by 1 index until it + * has only 1 index remaining. + * + * @param constructor Interval constructor. + * @param indices Indices. + */ + private static void assertUpdate(BiFunction constructor, + int[] indices, int[] k) { + UpdatingInterval interval = constructor.apply(indices.clone(), indices.length); + final int nm1 = k.length - 1; + Assertions.assertEquals(k[0], interval.left()); + Assertions.assertEquals(k[nm1], interval.right()); + + // Use updateLeft to reduce the interval to length 1 + for (int i = 1; i < k.length; i++) { + // rounded down median between indices + final int m = (k[i - 1] + k[i]) >>> 1; + interval.updateLeft(m + 1); + Assertions.assertEquals(k[i], interval.left()); + } + Assertions.assertEquals(interval.left(), interval.right()); + + // Use updateRight to reduce the interval to length 1 + interval = constructor.apply(indices.clone(), indices.length); + for (int i = k.length; --i > 0;) { + // rounded up median between indices + final int m = 1 + ((k[i - 1] + k[i]) >>> 1); + interval.updateRight(m - 1); + Assertions.assertEquals(k[i - 1], interval.right()); + } + Assertions.assertEquals(interval.left(), interval.right()); + } + + /** + * Assert the {@link UpdatingInterval#splitLeft(int, int)} method. + * These are tested by successive calls to split the interval around the mid-point. + * + * @param constructor Interval constructor. + * @param indices Indices. + * @param k Sorted unique indices. + */ + private static void assertSplit(BiFunction constructor, int[] indices, int[] k) { + assertSplitMedian(constructor.apply(indices.clone(), indices.length), + k, 0, k.length - 1); + assertSplitMiddleIndices(constructor.apply(indices.clone(), indices.length), + k, 0, k.length - 1); + } + + /** + * Assert a split using the median value between the split median. + * + * @param interval Interval. + * @param indices Indices. + * @param i Low index into the indices (inclusive). + * @param j High index into the indices (inclusive). + */ + private static void assertSplitMedian(UpdatingInterval interval, int[] indices, int i, int j) { + if (indices[i] + 1 >= indices[j]) { + // Cannot split - no value between the low and high points + return; + } + // Find the expected split about the median + final int m = (indices[i] + indices[j]) >>> 1; + // Binary search finds the value or the insertion index of the value + int hi = Arrays.binarySearch(indices, i, j + 1, m + 1); + if (hi < 0) { + // Use the insertion index + hi = ~hi; + } + // Scan for the lower index + int lo = hi; + do { + --lo; + } while (indices[lo] >= m); + + final int left = interval.left(); + final int right = interval.right(); + + final UpdatingInterval leftInterval = interval.splitLeft(m, m); + Assertions.assertEquals(left, leftInterval.left()); + Assertions.assertEquals(indices[lo], leftInterval.right()); + Assertions.assertEquals(indices[hi], interval.left()); + Assertions.assertEquals(right, interval.right()); + + // Recurse + assertSplitMedian(leftInterval, indices, i, lo); + assertSplitMedian(interval, indices, hi, j); + } + + /** + * Assert a split using the two middle indices. + * + * @param interval Interval. + * @param indices Indices. + * @param i Low index into the indices (inclusive). + * @param j High index into the indices (inclusive). + */ + private static void assertSplitMiddleIndices(UpdatingInterval interval, int[] indices, int i, int j) { + if (i + 3 >= j) { + // Cannot split - not two indices between low and high index + return; + } + // Middle two indices + final int m1 = (i + j) >>> 1; + final int m2 = m1 + 1; + + final int left = interval.left(); + final int right = interval.right(); + final UpdatingInterval leftInterval = interval.splitLeft(indices[m1], indices[m2]); + Assertions.assertEquals(left, leftInterval.left()); + Assertions.assertEquals(indices[m1 - 1], leftInterval.right()); + Assertions.assertEquals(indices[m2 + 1], interval.left()); + Assertions.assertEquals(right, interval.right()); + + // Recurse + assertSplitMiddleIndices(leftInterval, indices, i, m1 - 1); + assertSplitMiddleIndices(interval, indices, m2 + 1, j); + } + + static Stream testIndices() { + final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create(); + final Stream.Builder builder = Stream.builder(); + // Create unique sorted indices + builder.accept(new int[] {4}); + builder.accept(new int[] {4, 78}); + builder.accept(new int[] {4, 78, 999}); + builder.accept(new int[] {4, 78, 79, 999}); + builder.accept(new int[] {4, 5, 6, 7, 8}); + for (final int size : new int[] {25, 100, 400, 800}) { + final BitSet set = new BitSet(size); + for (final int n : new int[] {2, size / 8, size / 4, size / 2}) { + set.clear(); + rng.ints(n, 0, size).forEach(set::set); + final int[] a = set.stream().toArray(); + builder.accept(a.clone()); + // Force use of index 0 and max index + a[0] = 0; + a[a.length - 1] = Integer.MAX_VALUE - 1; + builder.accept(a); + } + } + // Builder contains sorted unique indices. + // Final required arguments are: [indices, sorted unique indices] + // Expand to have indices as: sorted, sorted with duplicates, unsorted, unsorted with duplicates + Stream.Builder out = Stream.builder(); + builder.build().forEach(a -> { + final int[] c = a.clone(); + out.accept(Arguments.of(a.clone(), c.clone())); + // Duplicates + final int[] b = Arrays.copyOf(a, a.length * 2); + for (int i = a.length; i < b.length; i++) { + b[i] = a[rng.nextInt(a.length)]; + } + Arrays.sort(b); + out.accept(Arguments.of(b.clone(), c.clone())); + // Unsorted + PermutationSampler.shuffle(rng, a); + PermutationSampler.shuffle(rng, b); + out.accept(Arguments.of(a, c.clone())); + out.accept(Arguments.of(b, c.clone())); + }); + return out.build(); + } + + @Test + void testIndexIntervalCreate() { + // The above tests verify the UpdatingInterval implementations all work. + // Hit all paths in the analysis performed to create an interval. + + // 1 key + Assertions.assertEquals(KeyUpdatingInterval.class, + IndexSupport.createUpdatingInterval(0, 2, new int[] {1}, 1).getClass()); + + // 2 close keys + Assertions.assertEquals(KeyUpdatingInterval.class, + IndexSupport.createUpdatingInterval(0, 2, new int[] {2, 1}, 2).getClass()); + Assertions.assertEquals(KeyUpdatingInterval.class, + IndexSupport.createUpdatingInterval(0, 2, new int[] {1, 2}, 2).getClass()); + + // 2 unsorted keys + Assertions.assertEquals(KeyUpdatingInterval.class, + IndexSupport.createUpdatingInterval(0, 1000, new int[] {200, 1}, 2).getClass()); + + // Sorted number of keys saturating the range + Assertions.assertEquals(KeyUpdatingInterval.class, + IndexSupport.createUpdatingInterval(0, 20, new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, 11).getClass()); + // Sorted keys with duplicates + Assertions.assertEquals(KeyUpdatingInterval.class, + IndexSupport.createUpdatingInterval(0, 10, new int[] {1, 1, 2, 2, 3, 3, 4, 4, 5, 5}, 10).getClass()); + Assertions.assertEquals(KeyUpdatingInterval.class, + IndexSupport.createUpdatingInterval(0, 1000, new int[] {100, 100, 200, 200, 300, 300, 400, 400, 500, 500}, 10).getClass()); + // Small number of keys saturating the range + Assertions.assertEquals(KeyUpdatingInterval.class, + IndexSupport.createUpdatingInterval(0, 20, new int[] {11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1}, 11).getClass()); + // Keys over a huge range + Assertions.assertEquals(KeyUpdatingInterval.class, + IndexSupport.createUpdatingInterval(0, Integer.MAX_VALUE - 1, + new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, Integer.MAX_VALUE - 1}, 11).getClass()); + + // Small number of sorted keys over a moderate range + int[] k = IntStream.range(0, 30).map(i -> i * 64) .toArray(); + Assertions.assertEquals(KeyUpdatingInterval.class, + IndexSupport.createUpdatingInterval(0, 30 * 64, k.clone(), k.length).getClass()); + // Same keys not sorted + reverse(k, 0, k.length); + Assertions.assertEquals(BitIndexUpdatingInterval.class, + IndexSupport.createUpdatingInterval(0, 30 * 64, k.clone(), k.length).getClass()); + // Same keys over a huge range + k[k.length - 1] = Integer.MAX_VALUE - 1; + Assertions.assertEquals(KeyUpdatingInterval.class, + IndexSupport.createUpdatingInterval(0, Integer.MAX_VALUE - 1, k, k.length).getClass()); + + // Moderate number of sorted keys over a moderate range + k = IntStream.range(0, 3000).map(i -> i * 64) .toArray(); + Assertions.assertEquals(KeyUpdatingInterval.class, + IndexSupport.createUpdatingInterval(0, 3000 * 64, k.clone(), k.length).getClass()); + // Same keys not sorted + reverse(k, 0, k.length); + Assertions.assertEquals(BitIndexUpdatingInterval.class, + IndexSupport.createUpdatingInterval(0, 3000 * 64, k.clone(), k.length).getClass()); + // Same keys over a huge range + k[k.length - 1] = Integer.MAX_VALUE - 1; + Assertions.assertEquals(KeyUpdatingInterval.class, + IndexSupport.createUpdatingInterval(0, Integer.MAX_VALUE - 1, k.clone(), k.length).getClass()); + } + + /** + * Reverse (part of) the data. + * + * @param a Data. + * @param from Start index to reverse (inclusive). + * @param to End index to reverse (exclusive). + */ + private static void reverse(int[] a, int from, int to) { + for (int i = from - 1, j = to; ++i < --j;) { + final int v = a[i]; + a[i] = a[j]; + a[j] = v; + } + } +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/BinarySearchKeyInterval.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/BinarySearchKeyInterval.java new file mode 100644 index 000000000..89e47439d --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/BinarySearchKeyInterval.java @@ -0,0 +1,159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * An {@link SearchableInterval} backed by an array of ordered keys. The interval is searched using + * a binary search. + * + * @since 1.2 + */ +final class BinarySearchKeyInterval implements SearchableInterval, SearchableInterval2 { + /** The ordered keys for descending search. */ + private final int[] keys; + /** The original number of keys - 1. This is more convenient to store for the use cases. */ + private final int nm1; + + /** + * Create an instance with the provided keys. + * + * @param indices Indices. + * @param n Number of indices. + */ + BinarySearchKeyInterval(int[] indices, int n) { + nm1 = n - 1; + keys = indices; + } + + /** + * Initialise an instance with the {@code indices}. The indices are used in place. + * + * @param indices Indices. + * @param n Number of indices. + * @return the interval + * @throws IllegalArgumentException if the indices are not unique and ordered; + * or {@code n <= 0} + */ + static BinarySearchKeyInterval of(int[] indices, int n) { + // Check the indices are uniquely ordered + if (n <= 0) { + throw new IllegalArgumentException("No indices to define the range"); + } + int p = indices[0]; + for (int i = 0; ++i < n;) { + final int c = indices[i]; + if (c <= p) { + throw new IllegalArgumentException("Indices are not unique and ordered"); + } + p = c; + } + return new BinarySearchKeyInterval(indices, n); + } + + @Override + public int left() { + return keys[0]; + } + + @Override + public int right() { + return keys[nm1]; + } + + @Override + public int previousIndex(int k) { + // Assume left <= k <= right thus no index checks required. + // IndexOutOfBoundsException indicates incorrect usage by the caller. + return keys[Partition.searchLessOrEqual(keys, 0, nm1, k)]; + } + + @Override + public int nextIndex(int k) { + // Assume left <= k <= right thus no index checks required. + // IndexOutOfBoundsException indicates incorrect usage by the caller. + return keys[Partition.searchGreaterOrEqual(keys, 0, nm1, k)]; + } + + @Override + public int split(int ka, int kb, int[] upper) { + int i = Partition.searchGreaterOrEqual(keys, 0, nm1, kb + 1); + upper[0] = keys[i]; + // Find the lower using a scan since a typical use case has ka == kb + // and a scan is faster than a second binary search. + do { + --i; + } while (keys[i] >= ka); + return keys[i]; + } + + @Override + public int start() { + return 0; + } + + @Override + public int end() { + return nm1; + } + + @Override + public int index(int i) { + return keys[i]; + } + + // Use case for previous/next is when left/right is within + // a partition pivot [p0, p1]. Most likely case is p0 == p1 + // and a scan is faster. + + @Override + public int previous(int i, int k) { + // index(start) <= k < index(i) + int j = i; + do { + --j; + } while (keys[j] > k); + return j; + } + + @Override + public int next(int i, int k) { + // index(i) < k <= index(end) + int j = i; + do { + ++j; + } while (keys[j] < k); + return j; + } + + @Override + public int split(int lo, int hi, int ka, int kb, int[] upper) { + // index(lo) < ka <= kb < index(hi) + + // We could test if ka/kb is above or below the + // median (keys[lo] + keys[hi]) >>> 1 to pick the side to search + + int j = Partition.searchGreaterOrEqual(keys, lo, hi, kb + 1); + upper[0] = j; + // Find the lower using a scan since a typical use case has ka == kb + // and a scan is faster than a second binary search. + do { + --j; + } while (keys[j] >= ka); + return j; + } +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/BitIndexUpdatingInterval.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/BitIndexUpdatingInterval.java new file mode 100644 index 000000000..e67000f9e --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/BitIndexUpdatingInterval.java @@ -0,0 +1,411 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * An {@link UpdatingInterval} and {@link SplittingInterval} backed by a fixed size of bits. + * + *

This is a specialised class to implement a reduced API similar to a + * {@link java.util.BitSet}. It uses no bounds range checks and supports only + * the methods required to implement the {@link UpdatingInterval} API. + * + *

An offset is supported to allow the fixed size to cover a range of indices starting + * above 0 with the most efficient usage of storage. + * + *

See the BloomFilter code in Commons Collections for use of long[] data to store + * bits. + * + * @since 1.2 + */ +final class BitIndexUpdatingInterval implements UpdatingInterval, SplittingInterval, IntervalAnalysis { + /** All 64-bits bits set. */ + private static final long LONG_MASK = -1L; + /** A bit shift to apply to an integer to divided by 64 (2^6). */ + private static final int DIVIDE_BY_64 = 6; + + /** Bit indexes. */ + private final long[] data; + + /** Index offset. */ + private final int offset; + /** Left bound of the support. */ + private int left; + /** Right bound of the support. */ + private int right; + + /** + * Create an instance to store indices within the range {@code [left, right]}. + * The range is not validated. + * + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + BitIndexUpdatingInterval(int left, int right) { + this.offset = left; + this.left = left; + this.right = right; + // Allocate storage to store index==right + // Note: This may allow directly writing to index > right if there + // is extra capacity. + data = new long[getLongIndex(right - offset) + 1]; + } + + /** + * Create an instance with the range {@code [left, right]} and reusing the provided + * index {@code data}. + * + * @param data Data. + * @param offset Index offset. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + private BitIndexUpdatingInterval(long[] data, int offset, int left, int right) { + this.data = data; + this.offset = offset; + this.left = left; + this.right = right; + } + + /** + * Create an instance to store indices within the range {@code [left, right]}. + * + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @return the index set + * @throws IllegalArgumentException if {@code right < left}; {@code left < 0} or + * {@code right == Integer.MAX_VALUE} + */ + static BitIndexUpdatingInterval ofRange(int left, int right) { + if (left < 0) { + throw new IllegalArgumentException("Invalid lower index: " + left); + } + if (right == Integer.MAX_VALUE) { + throw new IllegalArgumentException("Invalid upper index: " + right); + } + if (right < left) { + throw new IllegalArgumentException( + String.format("Invalid range: [%d, %d]", left, right)); + } + return new BitIndexUpdatingInterval(left, right); + } + + /** + * Initialise an instance with the {@code indices}. The capacity is defined by the + * range required to store the minimum and maximum index. + * + * @param indices Indices. + * @param n Number of indices. + * @return the index set + * @throws IllegalArgumentException if {@code n == 0} + */ + static BitIndexUpdatingInterval of(int[] indices, int n) { + if (n <= 0) { + throw new IllegalArgumentException("No indices to define the range"); + } + int min = indices[0]; + int max = min; + for (int i = 1; i < n; i++) { + min = Math.min(min, indices[i]); + max = Math.max(max, indices[i]); + } + final BitIndexUpdatingInterval set = BitIndexUpdatingInterval.ofRange(min, max); + for (int i = -1; ++i < n;) { + set.set(indices[i]); + } + return set; + } + + /** + * Return the memory footprint in bytes. This is always a multiple of 64. + * + *

The result is {@code 64 * ceil((right - offset + 1) / 64)}. + * + *

This method is intended to provided information to choose if the data structure + * is memory efficient. + * + *

Warning: It is assumed {@code 0 <= left <= right}. Use with the min/max index + * that is to be stored. + * + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @return the memory footprint + */ + static long memoryFootprint(int left, int right) { + return (getLongIndex(right - left) + 1L) * Long.BYTES; + } + + /** + * Gets the filter index for the specified bit index assuming the filter is using + * 64-bit longs to store bits starting at index 0. + * + *

The index is assumed to be positive. For a positive index the result will match + * {@code bitIndex / 64}.

+ * + *

The divide is performed using bit shifts. If the input is negative the + * behavior is not defined.

+ * + * @param bitIndex the bit index (assumed to be positive) + * @return the index of the bit map in an array of bit maps. + */ + private static int getLongIndex(final int bitIndex) { + // An integer divide by 64 is equivalent to a shift of 6 bits if the integer is + // positive. + // We do not explicitly check for a negative here. Instead we use a + // signed shift. Any negative index will produce a negative value + // by sign-extension and if used as an index into an array it will throw an + // exception. + return bitIndex >> DIVIDE_BY_64; + } + + /** + * Gets the filter bit mask for the specified bit index assuming the filter is using + * 64-bit longs to store bits starting at index 0. The returned value is a + * {@code long} with only 1 bit set. + * + *

The index is assumed to be positive. For a positive index the result will match + * {@code 1L << (bitIndex % 64)}.

+ * + *

If the input is negative the behavior is not defined.

+ * + * @param bitIndex the bit index (assumed to be positive) + * @return the filter bit + */ + private static long getLongBit(final int bitIndex) { + // Bit shifts only use the first 6 bits. Thus it is not necessary to mask this + // using 0x3f (63) or compute bitIndex % 64. + // Note: If the index is negative the shift will be (64 - (bitIndex & 0x3f)) and + // this will identify an incorrect bit. + return 1L << bitIndex; + } + + /** + * Sets the bit at the specified index to {@code true}. + * + *

Warning: This has no range checks. + * + * @param bitIndex the bit index (assumed to be positive) + */ + void set(int bitIndex) { + // WARNING: No range checks !!! + final int index = bitIndex - offset; + final int i = getLongIndex(index); + final long m = getLongBit(index); + data[i] |= m; + } + + /** + * Returns the index of the first bit that is set to {@code true} that occurs on or + * after the specified starting index. + * + *

Warning: This has no range checks. It is assumed that {@code left <= k <= right}, + * that is there is a set bit on or after {@code k}. + * + * @param k Index to start checking from (inclusive). + * @return the index of the next set bit + */ + private int nextIndex(int k) { + // left <= k <= right + + final int index = k - offset; + int i = getLongIndex(index); + + // Mask bits after the bit index + // mask = 11111000 = -1L << (index % 64) + long bits = data[i] & (LONG_MASK << index); + for (;;) { + if (bits != 0) { + //(i+1) i + // | index | + // | | | + // 0 001010000 + return i * Long.SIZE + Long.numberOfTrailingZeros(bits) + offset; + } + // Unsupported: the interval should contain k + //if (++i == data.length) + // return right + 1 + bits = data[++i]; + } + } + + /** + * Returns the index of the first bit that is set to {@code true} that occurs on or + * before the specified starting index. + * + *

Warning: This has no range checks. It is assumed that {@code left <= k <= right}, + * that is there is a set bit on or before {@code k}. + * + * @param k Index to start checking from (inclusive). + * @return the index of the previous set bit + */ + private int previousIndex(int k) { + // left <= k <= right + + final int index = k - offset; + int i = getLongIndex(index); + + // Mask bits before the bit index + // mask = 00011111 = -1L >>> (64 - ((index + 1) % 64)) + long bits = data[i] & (LONG_MASK >>> -(index + 1)); + for (;;) { + if (bits != 0) { + //(i+1) i + // | index | + // | | | + // 0 001010000 + return (i + 1) * Long.SIZE - Long.numberOfLeadingZeros(bits) - 1 + offset; + } + // Unsupported: the interval should contain k + //if (i == 0) + // return left - 1 + bits = data[--i]; + } + } + + @Override + public int left() { + return left; + } + + @Override + public int right() { + return right; + } + + @Override + public int updateLeft(int k) { + // Assume left < k= < right + return left = nextIndex(k); + } + + @Override + public int updateRight(int k) { + // Assume left <= k < right + return right = previousIndex(k); + } + + @Override + public UpdatingInterval splitLeft(int ka, int kb) { + // Assume left < ka <= kb < right + final int lower = left; + left = nextIndex(kb + 1); + return new BitIndexUpdatingInterval(data, offset, lower, previousIndex(ka - 1)); + } + + @Override + public UpdatingInterval splitRight(int ka, int kb) { + // Assume left < ka <= kb < right + final int upper = right; + right = previousIndex(ka - 1); + return new BitIndexUpdatingInterval(data, offset, nextIndex(kb + 1), upper); + } + + @Override + public boolean empty() { + // Empty when the interval is invalid. Signalled by a negative right bound. + return right < 0; + } + + @Override + public SplittingInterval split(int ka, int kb) { + if (ka <= left) { + // No left interval + if (kb >= right) { + // No right interval + invalidate(); + } else if (kb >= left) { + // Update the left bound + left = nextIndex(kb + 1); + } + return null; + } + if (kb >= right) { + // No right interval. + // Find new right bound for the left-side. + final int r = ka <= right ? previousIndex(ka - 1) : right; + invalidate(); + return new BitIndexUpdatingInterval(data, offset, left, r); + } + // Split + return (SplittingInterval) splitLeft(ka, kb); + } + + /** + * Invalidate the interval and mark as empty. + */ + private void invalidate() { + right = -1; + } + + @Override + public boolean saturated(int separation) { + // Support saturation analysis at separation relevant to the + // quickselect implementations + if (separation == 3) { + return saturated3(); + } + if (separation == 4) { + return saturated4(); + } + return false; + } + + /** + * Test if saturated as a separation of {@code 2^3}. + * + * @return true if saturated + */ + private boolean saturated3() { + int c = 0; + for (long x : data) { + // Shift powers of 2 and mask out the bits that were shifted + x = x | (x >>> 1); + x = x | (x >>> 2); + x = (x | (x >>> 4)) & 0b0000000100000001000000010000000100000001000000010000000100000001L; + // Expect a population count intrinsic method + // Add [0, 8] + c += Long.bitCount(x); + } + // Multiply by 8 + return c << 3 >= right - left; + } + + /** + * Test if saturated as a separation of {@code 2^4}. + * + * @return true if saturated + */ + private boolean saturated4() { + int c = 0; + for (long x : data) { + // Shift powers of 2 and mask out the bits that were shifted + x = x | (x >>> 1); + x = x | (x >>> 2); + x = x | (x >>> 4); + x = (x | (x >>> 8)) & 0b0000000000000001000000000000000100000000000000010000000000000001L; + // Count the bits using folding + // x = mask: + // 0000000000000001000000000000001000000000000000100000000000000010 (x += (x >>> 16)) + // 0000000100000001000000100000001000000011000000110000010000000100 (x += (x >>> 32)) + x = x + (x >>> 16); // put count of each 32 bits into their lowest 2 bits + x = x + (x >>> 32); // put count of each 64 bits into their lowest 3 bits + // Add [0, 4] + c += (int) x & 0b111; + } + // Multiply by 16 + return c << 4 >= right - left; + } +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/CompressedIndexSet.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/CompressedIndexSet.java new file mode 100644 index 000000000..e256d4506 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/CompressedIndexSet.java @@ -0,0 +1,698 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * A fixed size set of indices within an inclusive range {@code [left, right]}. + * + *

This is a specialised class to implement a data structure similar to a + * {@link java.util.BitSet}. It supports a fixed size and contains the methods required to + * store and look-up intervals of indices. + * + *

An offset is supported to allow the fixed size to cover a range of indices starting + * above 0 with the most efficient usage of storage. + * + *

In contrast to a {@link java.util.BitSet}, the data structure does not store all + * indices in the range. Indices are compressed by a power of 2. The structure can return + * with 100% accuracy if a query index is not within the range. It cannot return with 100% + * accuracy if a query index is contained within the range. The presence of a query index + * is a probabilistic statement that there is an index within a range of the query index. + * The range is defined by the compression level {@code c}. + * + *

Indices are stored offset from {@code left} and compressed. A compressed index + * represents 2c real indices: + * + *

+ * Interval:         012345678
+ * Compressed (c=1): 0-1-2-3-4
+ * Compressed (c=2): 0---1---2
+ * Compressed (c=2): 0-------1
+ * 
+ * + *

When scanning for the next index the identified compressed index is decompressed and + * the lower bound of the range represented by the index is returned. + * + *

When scanning for the previous index the identified compressed index is decompressed + * and the upper bound of the range represented by the index is returned. + * + *

When scanning in either direction, if the search index is inside a compressed index + * the search index is returned. + * + *

Note: Search for the {@link SearchableInterval} interface outside the supported bounds + * {@code [left, right]} is not supported and will result in an {@link IndexOutOfBoundsException}. + * + *

See the BloomFilter code in Commons Collections for use of long[] data to store + * bits. + * + * @since 1.2 + */ +final class CompressedIndexSet implements SearchableInterval, SearchableInterval2 { + /** All 64-bits bits set. */ + private static final long LONG_MASK = -1L; + /** A bit shift to apply to an integer to divided by 64 (2^6). */ + private static final int DIVIDE_BY_64 = 6; + + /** Bit indexes. */ + private final long[] data; + + /** Left bound of the support. */ + private final int left; + /** Right bound of the support. */ + private final int right; + /** Compression level. */ + private final int compression; + + /** + * Create an instance to store indices within the range {@code [left, right]}. + * + * @param compression Compression level (in {@code [1, 31])} + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + */ + private CompressedIndexSet(int compression, int l, int r) { + this.compression = compression; + this.left = l; + // Note: The functional upper bound may be higher but the next/previous functionality + // support scanning in the original [left, right] bound. + this.right = r; + // Note: This may allow directly writing to index > right if there + // is extra capacity. + data = new long[getLongIndex((r - l) >>> 1) + 1]; + } + + /** + * Create an instance to store indices within the range {@code [left, right]}. + * The instance is initially empty. + * + *

Warning: To use this object as an {@link SearchableInterval} the left and right + * indices should be added to the set. + * + * @param compression Compression level (in {@code [1, 31])} + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @return the index set + * @throws IllegalArgumentException if {@code compression} is not in {@code [1, 31]}; + * or if {@code right < left}; or if {@code left < 0} + */ + static CompressedIndexSet ofRange(int compression, int left, int right) { + checkCompression(compression); + checkLeft(left); + checkRange(left, right); + return new CompressedIndexSet(compression, left, right); + } + + /** + * Initialise an instance with the {@code indices}. The capacity is defined by the + * range required to store the minimum and maximum index at the specified + * {@code compression} level. + * + *

This object can be used as an {@link SearchableInterval} as the left and right + * indices will be set. + * + * @param compression Compression level (in {@code [1, 31])} + * @param indices Indices. + * @return the index set + * @throws IllegalArgumentException if {@code compression} is not in {@code [1, 31]}; + * or if {@code indices.length == 0}; or if {@code left < 0} + */ + static CompressedIndexSet of(int compression, int[] indices) { + return of(compression, indices, indices.length); + } + + /** + * Initialise an instance with the {@code indices}. The capacity is defined by the + * range required to store the minimum and maximum index at the specified + * {@code compression} level. + * + *

This object can be used as an {@link SearchableInterval} as the left and right + * indices will be set. + * + * @param compression Compression level (in {@code [1, 31])} + * @param indices Indices. + * @param n Number of indices. + * @return the index set + * @throws IllegalArgumentException if {@code compression} is not in {@code [1, 31]}; + * or if {@code n == 0}; or if {@code left < 0} + */ + static CompressedIndexSet of(int compression, int[] indices, int n) { + if (n <= 0) { + throw new IllegalArgumentException("No indices to define the range"); + } + checkCompression(compression); + int min = indices[0]; + int max = min; + for (int i = 0; ++i < n;) { + min = Math.min(min, indices[i]); + max = Math.max(max, indices[i]); + } + checkLeft(min); + final CompressedIndexSet set = new CompressedIndexSet(compression, min, max); + for (int i = -1; ++i < n;) { + set.set(indices[i]); + } + return set; + } + + /** + * Create an {@link IndexIterator} with the {@code indices}. + * + * @param compression Compression level (in {@code [1, 31])} + * @param indices Indices. + * @param n Number of indices. + * @return the index set + * @throws IllegalArgumentException if {@code compression} is not in {@code [1, 31]}; + * or if {@code n == 0}; or if {@code left < 0} + */ + static IndexIterator iterator(int compression, int[] indices, int n) { + return of(compression, indices, n).new Iterator(); + } + + /** + * Gets the compressed index for this instance using the left bound and the + * compression level. + * + * @param index Index. + * @return the compressed index + */ + private int compressIndex(int index) { + return (index - left) >>> compression; + } + + /** + * Gets the filter index for the specified bit index assuming the filter is using + * 64-bit longs to store bits starting at index 0. + * + *

The index is assumed to be positive. For a positive index the result will match + * {@code bitIndex / 64}.

+ * + *

The divide is performed using bit shifts. If the input is negative the + * behavior is not defined.

+ * + * @param bitIndex the bit index (assumed to be positive) + * @return the index of the bit map in an array of bit maps. + */ + private static int getLongIndex(final int bitIndex) { + // An integer divide by 64 is equivalent to a shift of 6 bits if the integer is + // positive. + // We do not explicitly check for a negative here. Instead we use a + // signed shift. Any negative index will produce a negative value + // by sign-extension and if used as an index into an array it will throw an + // exception. + return bitIndex >> DIVIDE_BY_64; + } + + /** + * Gets the filter bit mask for the specified bit index assuming the filter is using + * 64-bit longs to store bits starting at index 0. The returned value is a + * {@code long} with only 1 bit set. + * + *

The index is assumed to be positive. For a positive index the result will match + * {@code 1L << (bitIndex % 64)}.

+ * + *

If the input is negative the behavior is not defined.

+ * + * @param bitIndex the bit index (assumed to be positive) + * @return the filter bit + */ + private static long getLongBit(final int bitIndex) { + // Bit shifts only use the first 6 bits. Thus it is not necessary to mask this + // using 0x3f (63) or compute bitIndex % 64. + // Note: If the index is negative the shift will be (64 - (bitIndex & 0x3f)) and + // this will identify an incorrect bit. + return 1L << bitIndex; + } + + /** + * Returns the value of the bit with the specified index. + * + *

Warning: This has no range checks. + * + * @param bitIndex the bit index (assumed to be positive) + * @return the value of the bit with the specified index + */ + boolean get(int bitIndex) { + // WARNING: No range checks !!! + final int index = compressIndex(bitIndex); + final int i = getLongIndex(index); + final long m = getLongBit(index); + return (data[i] & m) != 0; + } + + /** + * Sets the bit at the specified index to {@code true}. + * + *

Warning: This has no range checks. + * + * @param bitIndex the bit index (assumed to be positive) + */ + void set(int bitIndex) { + // WARNING: No range checks !!! + final int index = compressIndex(bitIndex); + final int i = getLongIndex(index); + final long m = getLongBit(index); + data[i] |= m; + } + + + @Override + public int left() { + return left; + } + + @Override + public int right() { + return right; + } + + /** + * Returns the nearest index that occurs on or before the specified starting + * index, or {@code left - 1} if no such index exists. + * + *

This method exists for comparative testing to {@link #previousIndex(int)}. + * + * @param k Index to start checking from (inclusive). + * @return the previous index, or {@code left - 1} + */ + int previousIndexOrLeftMinus1(int k) { + if (k < left) { + // index is in an unknown range + return left - 1; + } + // Support searching backward through the known range + final int index = compressIndex(k > right ? right : k); + + int i = getLongIndex(index); + long bits = data[i]; + + // Check if this is within a compressed index. If so return the exact result. + if ((bits & getLongBit(index)) != 0) { + return Math.min(k, right); + } + + // Mask bits before the bit index + // mask = 00011111 = -1L >>> (64 - ((index + 1) % 64)) + bits &= LONG_MASK >>> -(index + 1); + for (;;) { + if (bits != 0) { + //(i+1) i + // | c | + // | | | + // 0 001010000 + final int c = (i + 1) * Long.SIZE - Long.numberOfLeadingZeros(bits); + // Decompress the prior unset bit to an index. When inflated this is the + // next index above the upper bound of the compressed range so subtract 1. + return (c << compression) - 1 + left; + } + if (i == 0) { + return left - 1; + } + bits = data[--i]; + } + } + + /** + * Returns the nearest index that occurs on or after the specified starting + * index, or {@code right + 1} if no such index exists. + * + *

This method exists for comparative testing to {@link #nextIndex(int)}. + * + * @param k Index to start checking from (inclusive). + * @return the next index, or {@code right + 1} + */ + int nextIndexOrRightPlus1(int k) { + if (k > right) { + // index is in an unknown range + return right + 1; + } + // Support searching forward through the known range + final int index = compressIndex(k < left ? left : k); + + int i = getLongIndex(index); + long bits = data[i]; + + // Check if this is within a compressed index. If so return the exact result. + if ((bits & getLongBit(index)) != 0) { + return Math.max(k, left); + } + + // Mask bits after the bit index + // mask = 11111000 = -1L << (index % 64) + bits &= LONG_MASK << index; + for (;;) { + if (bits != 0) { + //(i+1) i + // | c | + // | | | + // 0 001010000 + final int c = i * Long.SIZE + Long.numberOfTrailingZeros(bits); + // Decompress the set bit to an index. When inflated this is the lower bound of + // the compressed range and is OK for next scanning. + return (c << compression) + left; + } + if (++i == data.length) { + return right + 1; + } + bits = data[i]; + } + } + + @Override + public int previousIndex(int k) { + // WARNING: No range checks !!! + // Assume left <= k <= right and that left and right are set bits acting as sentinals. + final int index = compressIndex(k); + + int i = getLongIndex(index); + long bits = data[i]; + + // Check if this is within a compressed index. If so return the exact result. + if ((bits & getLongBit(index)) != 0) { + return k; + } + + // Mask bits before the bit index + // mask = 00011111 = -1L >>> (64 - ((index + 1) % 64)) + bits &= LONG_MASK >>> -(index + 1); + for (;;) { + if (bits != 0) { + //(i+1) i + // | c | + // | | | + // 0 001010000 + final int c = (i + 1) * Long.SIZE - Long.numberOfLeadingZeros(bits); + // Decompress the prior unset bit to an index. When inflated this is the + // next index above the upper bound of the compressed range so subtract 1. + return (c << compression) - 1 + left; + } + // Unsupported: the interval should contain k + //if (i == 0) { + // return left - 1; + //} + bits = data[--i]; + } + } + + @Override + public int nextIndex(int k) { + // WARNING: No range checks !!! + // Assume left <= k <= right and that left and right are set bits acting as sentinals. + final int index = compressIndex(k); + + int i = getLongIndex(index); + long bits = data[i]; + + // Check if this is within a compressed index. If so return the exact result. + if ((bits & getLongBit(index)) != 0) { + return k; + } + + // Mask bits after the bit index + // mask = 11111000 = -1L << (index % 64) + bits &= LONG_MASK << index; + for (;;) { + if (bits != 0) { + //(i+1) i + // | c | + // | | | + // 0 001010000 + final int c = i * Long.SIZE + Long.numberOfTrailingZeros(bits); + // Decompress the set bit to an index. When inflated this is the lower bound of + // the compressed range and is OK for next scanning. + return (c << compression) + left; + } + // Unsupported: the interval should contain k + //if (++i == data.length) { + // return right + 1; + //} + bits = data[++i]; + } + } + + // SearchableInterval2 + // This is exactly the same as SearchableInterval as the pointers i are the same as the keys k + + @Override + public int start() { + return left(); + } + + @Override + public int end() { + return right(); + } + + @Override + public int index(int i) { + return i; + } + + @Override + public int previous(int i, int k) { + return previousIndex(k); + } + + @Override + public int next(int i, int k) { + return nextIndex(k); + } + + /** + * Returns the index of the first bit that is set to {@code false} that occurs on or + * before the specified starting index within the supported range. If no such + * bit exists then {@code left - 1} is returned. + * + *

Assumes {@code k} is within an enabled compressed index. + * + * @param k Index to start checking from (inclusive). + * @return the index of the previous unset bit, or {@code left - 1} if there is no such bit + */ + int previousClearBit(int k) { + // WARNING: No range checks !!! + // Assume left <= k <= right and that left and right are set bits acting as sentinals. + final int index = compressIndex(k); + + int i = getLongIndex(index); + + // Note: This method is conceptually the same as previousIndex with the exception + // that: all the data is bit-flipped; a check is made when the scan reaches the end; + // and no check is made for k within an unset compressed index. + + // Mask bits before the bit index + // mask = 00011111 = -1L >>> (64 - ((index + 1) % 64)) + long bits = ~data[i] & (LONG_MASK >>> -(index + 1)); + for (;;) { + if (bits != 0) { + final int c = (i + 1) * Long.SIZE - Long.numberOfLeadingZeros(bits); + return (c << compression) - 1 + left; + } + if (i == 0) { + return left - 1; + } + bits = ~data[--i]; + } + } + + /** + * Returns the index of the first bit that is set to {@code false} that occurs on or + * after the specified starting index within the supported range. If no such + * bit exists then the {@code capacity} is returned where {@code capacity = index + 1} + * with {@code index} the largest index that can be added to the set without an error. + * + *

Assumes {@code k} is within an enabled compressed index. + * + * @param k Index to start checking from (inclusive). + * @return the index of the next unset bit, or the {@code capacity} if there is no such bit + */ + int nextClearBit(int k) { + // WARNING: No range checks !!! + // Assume left <= k <= right + final int index = compressIndex(k); + + int i = getLongIndex(index); + + // Note: This method is conceptually the same as nextIndex with the exception + // that: all the data is bit-flipped; a check is made for the capacity when the + // scan reaches the end; and no check is made for k within an unset compressed index. + + // Mask bits after the bit index + // mask = 11111000 = -1L << (fromIndex % 64) + long bits = ~data[i] & (LONG_MASK << index); + for (;;) { + if (bits != 0) { + final int c = i * Long.SIZE + Long.numberOfTrailingZeros(bits); + return (c << compression) + left; + } + if (++i == data.length) { + // Capacity + return right + 1; + } + bits = ~data[i]; + } + } + + /** + * Check the compression is valid. + * + * @param compression Compression level. + * @throws IllegalArgumentException if {@code compression} is not in {@code [1, 31]} + */ + private static void checkCompression(int compression) { + if (!(compression > 0 && compression <= 31)) { + throw new IllegalArgumentException("Invalid compression: " + compression); + } + } + + /** + * Check the lower bound to the range is valid. + * + * @param left Lower bound (inclusive). + * @throws IllegalArgumentException if {@code left < 0} + */ + private static void checkLeft(int left) { + if (left < 0) { + throw new IllegalArgumentException("Invalid lower index: " + left); + } + } + + /** + * Check the range is valid. + * + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @throws IllegalArgumentException if {@code right < left} + */ + private static void checkRange(int left, int right) { + if (right < left) { + throw new IllegalArgumentException( + String.format("Invalid range: [%d, %d]", left, right)); + } + } + + /** + * {@link IndexIterator} implementation. + * + *

This iterator can efficiently iterate over high-density indices + * if the compression level is set to create spacing equal to or above the expected + * separation between indices. + */ + private class Iterator implements IndexIterator { + /** Iterator left. l is a compressed index. */ + private int l; + /** Iterator right. (r+1) is a clear bit. */ + private int r; + /** Next iterator left. Cached for look ahead functionality. */ + private int nextL; + + /** + * Create an instance. + */ + Iterator() { + l = CompressedIndexSet.this.left(); + r = nextClearBit(l) - 1; + if (r < end()) { + nextL = nextIndex(r + 1); + } else { + // Entire range is saturated + r = end(); + } + } + + @Override + public int left() { + return l; + } + + @Override + public int right() { + return r; + } + + @Override + public int end() { + return CompressedIndexSet.this.right(); + } + + @Override + public boolean next() { + if (r < end()) { + // Reuse the cached next left and advance + l = nextL; + r = nextClearBit(l) - 1; + if (r < end()) { + nextL = nextIndex(r + 1); + } else { + r = end(); + } + return true; + } + return false; + } + + @Override + public boolean positionAfter(int index) { + // Even though this can provide random access we only allow advancing + if (r > index) { + return true; + } + if (index < end()) { + // Note: Uses 3 scans as it maintains the next left. + // For low density indices scanning for next left will be expensive + // and it would be more efficient to only compute next left on demand. + // For high density indices the next left will be close to + // the new right and the cost is low. + // This iterator favours use on high density indices. A variant + // iterator could be created for comparison purposes. + + if (get(index + 1)) { + // (index+1) is set. + // Find [left <= index+1 <= right] + r = nextClearBit(index + 1) - 1; + if (r < end()) { + nextL = nextIndex(r + 1); + } else { + r = end(); + } + l = index + 1; + //l = previousClearBit(index) + 1; + } else { + // (index+1) is clear. + // Advance to the next [left, right] pair + l = nextIndex(index + 1); + r = nextClearBit(l) - 1; + if (r < end()) { + nextL = nextIndex(r + 1); + } else { + r = end(); + } + } + return true; + } + // Advance to end. No next left. Not positioned after the target index. + l = r = end(); + return false; + } + + @Override + public boolean nextAfter(int index) { + if (r < end()) { + return nextL > index; + } + // no more indices + return true; + } + } +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/CompressedIndexSet2.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/CompressedIndexSet2.java new file mode 100644 index 000000000..27f9f6e13 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/CompressedIndexSet2.java @@ -0,0 +1,359 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * A fixed size set of indices within an inclusive range {@code [left, right]}. + * + *

This is a specialised class to implement a data structure similar to a + * {@link java.util.BitSet}. It supports a fixed size and contains the methods required to + * store and look-up intervals of indices. + * + *

An offset is supported to allow the fixed size to cover a range of indices starting + * above 0 with the most efficient usage of storage. + * + *

In contrast to a {@link java.util.BitSet}, the data structure does not store all + * indices in the range. Indices are compressed 2-to-1. The structure can return + * with 100% accuracy if a query index is not within the range. It cannot return with 100% + * accuracy if a query index is contained within the range. The presence of a query index + * is a probabilistic statement that there is an index within 1 of the query index. + * + *

Indices are stored offset from {@code left} and compressed. A compressed index + * represents 2 real indices: + * + *

+ * Interval:   012345678
+ * Compressed: 0-1-2-3-4
+ * 
+ * + *

When scanning for the next index the identified compressed index is decompressed and + * the lower bound of the range represented by the index is returned. + * + *

When scanning for the previous index the identified compressed index is decompressed + * and the upper bound of the range represented by the index is returned. + * + *

When scanning in either direction, if the search index is inside a compressed index + * the search index is returned. + * + *

Note: Search for the {@link SearchableInterval} interface outside the supported bounds + * {@code [left, right]} is not supported and will result in an {@link IndexOutOfBoundsException}. + * + *

See the BloomFilter code in Commons Collections for use of long[] data to store + * bits. + * + *

Note: This is a specialised version of {@link CompressedIndexSet} using a fixed + * compression of 1. This is used for performance testing. This is the most useful + * compression level for the partition algorithm as any compressed key that lies + * exactly on a partition index will only require a search for the min/max in the + * interval immediately below/above the partition index. A pair of indices (k, k+1) + * that is split into two compressed keys and lies exactly on a partition index will + * require a search for the min on one side and two maximum values on the other side; or + * max on one side and two minimum on the other. Each of these cases is handled by + * dedicated heapselect routines to find 1 or 2 values at the edge of a range. + * + * @since 1.2 + */ +final class CompressedIndexSet2 implements SearchableInterval { + /** All 64-bits bits set. */ + private static final long LONG_MASK = -1L; + /** A bit shift to apply to an integer to divided by 64 (2^6). */ + private static final int DIVIDE_BY_64 = 6; + + /** Bit indexes. */ + private final long[] data; + + /** Left bound of the support. */ + private final int left; + /** Right bound of the support. */ + private final int right; + + /** + * Create an instance to store indices within the range {@code [left, right]}. + * + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + */ + private CompressedIndexSet2(int l, int r) { + this.left = l; + // Note: The functional upper bound may be higher but the next/previous functionality + // support scanning in the original [left, right] bound. + this.right = r; + // Note: This may allow directly writing to index > right if there + // is extra capacity. + data = new long[getLongIndex((r - l) >>> 1) + 1]; + } + + /** + * Create an instance to store indices within the range {@code [left, right]}. + * The instance is initially empty. + * + *

Warning: To use this object as an {@link SearchableInterval} the left and right + * indices should be added to the set. + * + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @return the index set + * @throws IllegalArgumentException if {@code compression} is not in {@code [1, 31]}; + * or if {@code right < left}; or if {@code left < 0} + */ + static CompressedIndexSet2 ofRange(int left, int right) { + checkLeft(left); + checkRange(left, right); + return new CompressedIndexSet2(left, right); + } + + /** + * Initialise an instance with the {@code indices}. The capacity is defined by the + * range required to store the minimum and maximum index at the specified + * {@code compression} level. + * + *

This object can be used as an {@link SearchableInterval} as the left and right + * indices will be set. + * + * @param indices Indices. + * @return the index set + * @throws IllegalArgumentException if {@code compression} is not in {@code [1, 31]}; + * or if {@code indices.length == 0}; or if {@code left < 0} + */ + static CompressedIndexSet2 of(int[] indices) { + return of(indices, indices.length); + } + + /** + * Initialise an instance with the {@code indices}. The capacity is defined by the + * range required to store the minimum and maximum index at the specified + * {@code compression} level. + * + * @param indices Indices. + * @param n Number of indices. + * @return the index set + * @throws IllegalArgumentException if {@code compression} is not in {@code [1, 31]}; + * or if {@code n == 0}; or if {@code left < 0} + */ + static CompressedIndexSet2 of(int[] indices, int n) { + if (n <= 0) { + throw new IllegalArgumentException("No indices to define the range"); + } + int min = indices[0]; + int max = min; + for (int i = 0; ++i < n;) { + min = Math.min(min, indices[i]); + max = Math.max(max, indices[i]); + } + checkLeft(min); + final CompressedIndexSet2 set = new CompressedIndexSet2(min, max); + for (int i = -1; ++i < n;) { + set.set(indices[i]); + } + return set; + } + + /** + * Gets the compressed index for this instance using the left bound and the + * compression level. + * + * @param index Index. + * @return the compressed index + */ + private int compressIndex(int index) { + return (index - left) >>> 1; + } + + /** + * Gets the filter index for the specified bit index assuming the filter is using + * 64-bit longs to store bits starting at index 0. + * + *

The index is assumed to be positive. For a positive index the result will match + * {@code bitIndex / 64}.

+ * + *

The divide is performed using bit shifts. If the input is negative the + * behavior is not defined.

+ * + * @param bitIndex the bit index (assumed to be positive) + * @return the index of the bit map in an array of bit maps. + */ + private static int getLongIndex(final int bitIndex) { + // An integer divide by 64 is equivalent to a shift of 6 bits if the integer is + // positive. + // We do not explicitly check for a negative here. Instead we use a + // signed shift. Any negative index will produce a negative value + // by sign-extension and if used as an index into an array it will throw an + // exception. + return bitIndex >> DIVIDE_BY_64; + } + + /** + * Gets the filter bit mask for the specified bit index assuming the filter is using + * 64-bit longs to store bits starting at index 0. The returned value is a + * {@code long} with only 1 bit set. + * + *

The index is assumed to be positive. For a positive index the result will match + * {@code 1L << (bitIndex % 64)}.

+ * + *

If the input is negative the behavior is not defined.

+ * + * @param bitIndex the bit index (assumed to be positive) + * @return the filter bit + */ + private static long getLongBit(final int bitIndex) { + // Bit shifts only use the first 6 bits. Thus it is not necessary to mask this + // using 0x3f (63) or compute bitIndex % 64. + // Note: If the index is negative the shift will be (64 - (bitIndex & 0x3f)) and + // this will identify an incorrect bit. + return 1L << bitIndex; + } + + /** + * Returns the value of the bit with the specified index. + * + *

Warning: This has no range checks. + * + * @param bitIndex the bit index (assumed to be positive) + * @return the value of the bit with the specified index + */ + boolean get(int bitIndex) { + // WARNING: No range checks !!! + final int index = compressIndex(bitIndex); + final int i = getLongIndex(index); + final long m = getLongBit(index); + return (data[i] & m) != 0; + } + + /** + * Sets the bit at the specified index to {@code true}. + * + *

Warning: This has no range checks. + * + * @param bitIndex the bit index (assumed to be positive) + */ + void set(int bitIndex) { + // WARNING: No range checks !!! + final int index = compressIndex(bitIndex); + final int i = getLongIndex(index); + final long m = getLongBit(index); + data[i] |= m; + } + + + @Override + public int left() { + return left; + } + + @Override + public int right() { + return right; + } + + @Override + public int previousIndex(int k) { + // WARNING: No range checks !!! + // Assume left <= k <= right and that left and right are set bits acting as sentinels. + final int index = compressIndex(k); + + int i = getLongIndex(index); + long bits = data[i]; + + // Check if this is within a compressed index. If so return the exact result. + if ((bits & getLongBit(index)) != 0) { + return k; + } + + // Mask bits before the bit index + // mask = 00011111 = -1L >>> (64 - ((index + 1) % 64)) + bits &= LONG_MASK >>> -(index + 1); + for (;;) { + if (bits != 0) { + //(i+1) i + // | c | + // | | | + // 0 001010000 + final int c = (i + 1) * Long.SIZE - Long.numberOfLeadingZeros(bits); + // Decompress the prior unset bit to an index. When inflated this is the + // next index above the upper bound of the compressed range so subtract 1. + return (c << 1) - 1 + left; + } + // Unsupported: the interval should contain k + //if (i == 0) { + // return left - 1; + //} + bits = data[--i]; + } + } + + @Override + public int nextIndex(int k) { + // WARNING: No range checks !!! + // Assume left <= k <= right and that left and right are set bits acting as sentinels. + final int index = compressIndex(k); + + int i = getLongIndex(index); + long bits = data[i]; + + // Check if this is within a compressed index. If so return the exact result. + if ((bits & getLongBit(index)) != 0) { + return k; + } + + // Mask bits after the bit index + // mask = 11111000 = -1L << (index % 64) + bits &= LONG_MASK << index; + for (;;) { + if (bits != 0) { + //(i+1) i + // | c | + // | | | + // 0 001010000 + final int c = i * Long.SIZE + Long.numberOfTrailingZeros(bits); + // Decompress the set bit to an index. When inflated this is the lower bound of + // the compressed range and is OK for next scanning. + return (c << 1) + left; + } + // Unsupported: the interval should contain k + //if (++i == data.length) { + // return right + 1; + //} + bits = data[++i]; + } + } + + /** + * Check the lower bound to the range is valid. + * + * @param left Lower bound (inclusive). + * @throws IllegalArgumentException if {@code left < 0} + */ + private static void checkLeft(int left) { + if (left < 0) { + throw new IllegalArgumentException("Invalid lower index: " + left); + } + } + + /** + * Check the range is valid. + * + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @throws IllegalArgumentException if {@code right < left} + */ + private static void checkRange(int left, int right) { + if (right < left) { + throw new IllegalArgumentException( + String.format("Invalid range: [%d, %d]", left, right)); + } + } +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/DoubleDataTransformer.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/DoubleDataTransformer.java new file mode 100644 index 000000000..8852bedae --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/DoubleDataTransformer.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * Defines a transformer for {@code double[]} arrays. + * + *

This interface is not intended for a public API. It provides a consistent method + * to handle partial sorting of {@code double[]} data. + * + *

The transformer allows pre-processing data before applying a sort algorithm. + * This is required to handle {@code NaN} and signed-zeros {@code -0.0}. + * + *

Note: The {@code <} relation does not provide a total order on all double + * values: {@code -0.0 == 0.0} is {@code true} and a {@code NaN} + * value compares neither less than, greater than, nor equal to any value, + * even itself. + * + *

The {@link java.util.Arrays#sort(double[])} method respects the order imposed by + * {@link Double#compare(double, double)}: {@code -0.0} is treated as less than value + * {@code 0.0} and {@code Double.NaN} is considered greater than any + * other value and all {@code Double.NaN} values are considered equal. + * + *

This interface allows implementations to respect the behaviour + * {@link Double#compare(double, double)}, or implement different behaviour. + * + * @see java.util.Arrays#sort(double[]) + * @since 1.2 + */ +interface DoubleDataTransformer { + /** + * Pre-process the data for partitioning. + * + *

This method will scan all the data and apply + * processing to {@code NaN} and signed-zeros {@code -0.0}. + * + *

A method matching {@link java.util.Arrays#sort(double[])} would move + * all {@code NaN} to the end of the array and order zeros. However ordering + * zeros is not useful if the data is to be fully or partially reordered + * by the caller. Possible solutions are to count signed zeros, or ignore them since + * they will not interfere with comparison operators {@code <, ==, >}. + * + *

The length of the data that must be processed by partitioning can be + * accessed using {@link #length()}. For example if {@code NaN} values are moved + * to the end of the data they are already partitioned. A partition algorithm + * can then avoid processing {@code NaN} during partitioning. + * + * @param data Data. + * @return pre-processed data (may be a copy) + */ + double[] preProcess(double[] data); + + /** + * Get the size of the data. + * + *

Note: Although the pre-processed data array may be longer than this length some + * values may have been excluded from the data (e.g. removal of NaNs). This is the + * effective size of the data. + * + * @return the size + */ + int size(); + + /** + * Get the length of the pre-processed data that must be partitioned. + * + *

Note: Although the pre-processed data array may be longer than this length it is + * only required to partition indices below this length. For example the end of the + * array may contain values to ignore from partitioning such as {@code NaN}. + * + * @return the length + */ + int length(); + + /** + * Post-process the data after partitioning. This method can restore values that + * may have been removed from the pre-processed data, for example signed zeros + * or revert any special {@code NaN} value processing. + * + *

If no partition indices are available use {@code null} and {@code n = 0}. + * + * @param data Data. + * @param k Partition indices. + * @param n Count of partition indices. + */ + void postProcess(double[] data, int[] k, int n); + + /** + * Post-process the data after sorting. This method can restore values that + * may have been removed from the pre-processed data, for example signed zeros + * or revert any special {@code NaN} value processing. + * + *

Warning: Assumes data is fully sorted in {@code [0, length)} (see {@link #length()}). + * + * @param data Data. + */ + void postProcess(double[] data); +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/DoubleDataTransformers.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/DoubleDataTransformers.java new file mode 100644 index 000000000..b5351be76 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/DoubleDataTransformers.java @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.util.Arrays; +import java.util.function.Supplier; + +/** + * Support for creating {@link DoubleDataTransformer} implementations. + * + * @since 1.2 + */ +final class DoubleDataTransformers { + + /** No instances. */ + private DoubleDataTransformers() {} + + /** + * Creates a factory to supply a {@link DoubleDataTransformer} based on the + * {@code nanPolicy} and data {@code copy} policy. + * + *

The factory will supply instances that may be reused on the same thread. + * Multi-threaded usage should create an instance per thread. + * + * @param nanPolicy NaN policy. + * @param copy Set to {@code true} to use a copy of the data. + * @return the factory + */ + static Supplier createFactory(NaNPolicy nanPolicy, boolean copy) { + if (nanPolicy == NaNPolicy.ERROR) { + return () -> new NaNErrorTransformer(copy); + } + // Support including NaN / excluding NaN from the data size + final boolean includeNaN = nanPolicy == NaNPolicy.INCLUDE; + return () -> new SortTransformer(includeNaN, copy); + } + + /** + * A transformer that moves {@code NaN} to the upper end of the array. + * Signed zeros are counted. + */ + private abstract static class ReplaceSignedZerosTransformer implements DoubleDataTransformer { + /** Count of negative zeros. */ + protected int negativeZeroCount; + + @Override + public void postProcess(double[] data, int[] k, int n) { + // Restore signed zeros + if (negativeZeroCount != 0) { + // Use the partitioned indices to fast-forward as much as possible. + // Assumes partitioning has not changed indices (but + // reordering is OK). + int j = -1; + for (int i = 0; i < n; i++) { + if (data[k[i]] < 0) { + j = Math.max(j, k[i]); + } + } + for (int cn = negativeZeroCount;;) { + if (data[++j] == 0) { + data[j] = -0.0; + if (--cn == 0) { + break; + } + } + } + } + } + + @Override + public void postProcess(double[] data) { + if (negativeZeroCount != 0) { + // Find a zero + int j = Arrays.binarySearch(data, 0.0); + // Scan back to before zero + while (--j >= 0) { + if (data[j] != 0) { + break; + } + } + // Fix. Assume the zeros are all present so just overwrite + // the required count of signed zeros. + for (int cn = negativeZeroCount; --cn >= 0;) { + data[++j] = -0.0; + } + } + } + } + + /** + * A transformer that moves {@code NaN} to the upper end of the array. + * Signed zeros are counted. + */ + private static final class SortTransformer extends ReplaceSignedZerosTransformer { + /** Set to {@code true} to include NaN in the size of the data. */ + private final boolean includeNaN; + /** Set to {@code true} to use a copy of the data. */ + private final boolean copy; + /** Size of the data. */ + private int size; + /** Length of data to partition. */ + private int len; + + /** + * @param includeNaN Set to {@code true} to include NaN in the size of the data. + * @param copy Set to {@code true} to use a copy of the data. + */ + private SortTransformer(boolean includeNaN, boolean copy) { + this.includeNaN = includeNaN; + this.copy = copy; + } + + @Override + public double[] preProcess(double[] data) { + final double[] a = copy ? data.clone() : data; + // Sort NaN / count signed zeros + int cn = 0; + int end = a.length; + for (int i = end; i > 0;) { + final double v = a[--i]; + // Count negative zeros using a sign bit check. + // This requires a performance test. If the conversion to raw bits + // is natively supported this is faster than using the == check. + // if (v == 0.0 && Double.doubleToRawLongBits(v) < 0) { + if (Double.doubleToRawLongBits(v) == Long.MIN_VALUE) { + cn++; + // Change to positive zero. + // Data must be repaired after sort. + a[i] = 0.0; + } else if (v != v) { + // Move NaN to end + a[i] = a[--end]; + a[end] = v; + } + } + negativeZeroCount = cn; + len = end; + size = includeNaN ? a.length : len; + return a; + } + + @Override + public int size() { + return size; + } + + @Override + public int length() { + return len; + } + } + + /** + * A transformer that errors on {@code NaN}. + * Signed zeros are counted and restored. + */ + private static final class NaNErrorTransformer extends ReplaceSignedZerosTransformer { + /** Set to {@code true} to use a copy of the data. */ + private final boolean copy; + /** Size of the data. */ + private int size; + + /** + * @param copy Set to {@code true} to use a copy of the data. + */ + private NaNErrorTransformer(boolean copy) { + this.copy = copy; + } + + @Override + public double[] preProcess(double[] data) { + // Here we delay copy to not change the data if a NaN is found. + // But we commit to a double scan for signed zeros. + double[] a = data; + // Error on NaN / count signed zeros + int cn = 0; + for (int i = a.length; i > 0;) { + final double v = a[--i]; + // This requires a performance test + if (Double.doubleToRawLongBits(v) == Long.MIN_VALUE) { + cn++; + } else if (v != v) { + throw new IllegalArgumentException("NaN at: " + i); + } + } + negativeZeroCount = cn; + size = a.length; + // No NaNs so copy the data if required + if (copy) { + a = a.clone(); + } + // Re-write zeros if required + if (cn != 0) { + for (int i = a.length; i > 0;) { + if (Double.doubleToRawLongBits(a[--i]) == Long.MIN_VALUE) { + a[i] = 0.0; + if (--cn == 0) { + break; + } + } + } + } + return a; + } + + @Override + public int size() { + return size; + } + + @Override + public int length() { + return size; + } + } +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/DoubleMath.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/DoubleMath.java new file mode 100644 index 000000000..06de66092 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/DoubleMath.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * Support class for double math. + * + * @since 1.2 + */ +final class DoubleMath { + /** No instances. */ + private DoubleMath() {} + + /** + * Return {@code true} if {@code x > y}. + * + *

Respects the sort ordering of {@link Double#compare(double, double)}: + * + *

{@code
+     * Double.compare(x, y) > 0
+     * }
+ * + * @param x Value. + * @param y Value. + * @return {@code x > y} + */ + static boolean greaterThan(double x, double y) { + if (x > y) { + return true; + } + if (x < y) { + return false; + } + // Equal numbers; signed zeros (-0.0, 0.0); or NaNs + final long a = Double.doubleToLongBits(x); + final long b = Double.doubleToLongBits(y); + return a > b; + } + + /** + * Return {@code true} if {@code x < y}. + * + *

Respects the sort ordering of {@link Double#compare(double, double)}: + * + *

{@code
+     * Double.compare(x, y) < 0
+     * }
+ * + * @param x Value. + * @param y Value. + * @return {@code x < y} + */ + static boolean lessThan(double x, double y) { + if (x < y) { + return true; + } + if (x > y) { + return false; + } + // Equal numbers; signed zeros (-0.0, 0.0); or NaNs + final long a = Double.doubleToLongBits(x); + final long b = Double.doubleToLongBits(y); + return a < b; + } + +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/DualPivotingStrategy.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/DualPivotingStrategy.java new file mode 100644 index 000000000..5b2a99ee4 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/DualPivotingStrategy.java @@ -0,0 +1,817 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * A strategy to pick two pivot indices of an array for partitioning. + * + *

An ideal strategy will pick the tertiles across a variety of data so + * to divide the data into [1/3, 1/3, 1/3]. + * + * @see Tertile (Wiktionary) + * @since 1.2 + */ +enum DualPivotingStrategy { + /** + * Pivot around the medians at 1/3 and 2/3 of the range. + * + *

Requires {@code right - left >= 2}. + * + *

On sorted data the tertiles are: 0.3340 0.6670 + *

On random data the tertiles are: + *

+     *         min      max     mean       sd   median     skew
+     * [1]  0.0000   0.9970   0.3327   0.2357   0.2920   0.5654
+     * [2]  0.0020   1.0000   0.3346   0.2356   0.2940   0.5675
+     * [3]  0.0000   0.9970   0.3328   0.2356   0.2920   0.5656
+     * 
+ */ + MEDIANS { + @Override + int pivotIndex(double[] data, int left, int right, int[] pivot2) { + // Original 'medians' method from the dual-pivot quicksort paper by Vladimir Yaroslavskiy + final int len = right - left; + // Do not pivot at the ends by setting 1/3 to at least 1. + // This is safe if len >= 2. + final int third = Math.max(1, len / 3); + final int m1 = left + third; + final int m2 = right - third; + // Ensure p1 is lower + if (data[m1] < data[m2]) { + pivot2[0] = m2; + return m1; + } + pivot2[0] = m1; + return m2; + } + + @Override + int[] getSampledIndices(int left, int right) { + final int len = right - left; + final int third = Math.max(1, len / 3); + final int m1 = left + third; + final int m2 = right - third; + return new int[] {m1, m2}; + } + + @Override + int samplingEffect() { + return UNCHANGED; + } + }, + /** + * Pivot around the 2nd and 4th values from 5 approximately uniformly spaced within the range. + * Uses points +/- sixths from the median: 1/6, 1/3, 1/2, 2/3, 5/6. + * + *

Requires {@code right - left >= 4}. + * + *

Warning: This has the side effect that the 5 values are also sorted. + * + *

On sorted data the tertiles are: 0.3290 0.6710 + *

On random data the tertiles are: + *

+     *         min      max     mean       sd   median     skew
+     * [1]  0.0010   0.9820   0.3327   0.1778   0.3130   0.4650
+     * [2]  0.0030   0.9760   0.3348   0.1778   0.3150   0.4665
+     * [3]  0.0010   0.9870   0.3325   0.1779   0.3130   0.4698
+     * 
+ */ + SORT_5 { + @Override + int pivotIndex(double[] data, int left, int right, int[] pivot2) { + // 1/6 = 5/30 ~ 1/8 + 1/32 + 1/64 : 0.1666 ~ 0.1719 + // Ensure the value is above zero to choose different points! + // This is safe if len >= 4. + final int len = right - left; + final int sixth = 1 + (len >>> 3) + (len >>> 5) + (len >>> 6); + final int p3 = left + (len >>> 1); + final int p2 = p3 - sixth; + final int p1 = p2 - sixth; + final int p4 = p3 + sixth; + final int p5 = p4 + sixth; + Sorting.sort5(data, p1, p2, p3, p4, p5); + pivot2[0] = p4; + return p2; + } + + @Override + int[] getSampledIndices(int left, int right) { + final int len = right - left; + final int sixth = 1 + (len >>> 3) + (len >>> 5) + (len >>> 6); + final int p3 = left + (len >>> 1); + final int p2 = p3 - sixth; + final int p1 = p2 - sixth; + final int p4 = p3 + sixth; + final int p5 = p4 + sixth; + return new int[] {p1, p2, p3, p4, p5}; + } + + @Override + int samplingEffect() { + return SORT; + } + }, + /** + * Pivot around the 2nd and 4th values from 5 approximately uniformly spaced within the range. + * Uses points +/- sevenths from the median: 3/14, 5/14, 1/2, 9/14, 11/14. + * + *

Requires {@code right - left >= 4}. + * + *

Warning: This has the side effect that the 5 values are also sorted. + * + *

On sorted data the tertiles are: 0.3600 0.6400 + *

On random data the tertiles are: + *

+     *         min      max     mean       sd   median     skew
+     * [1]  0.0010   0.9790   0.3330   0.1780   0.3140   0.4665
+     * [2]  0.0030   0.9800   0.3348   0.1778   0.3150   0.4681
+     * [3]  0.0010   0.9770   0.3322   0.1777   0.3130   0.4677
+     * 
+ */ + SORT_5B { + @Override + int pivotIndex(double[] data, int left, int right, int[] pivot2) { + // 1/7 = 5/35 ~ 1/8 + 1/64 : 0.1429 ~ 0.1406 + // Ensure the value is above zero to choose different points! + // This is safe if len >= 4. + final int len = right - left; + final int seventh = 1 + (len >>> 3) + (len >>> 6); + final int p3 = left + (len >>> 1); + final int p2 = p3 - seventh; + final int p1 = p2 - seventh; + final int p4 = p3 + seventh; + final int p5 = p4 + seventh; + Sorting.sort5(data, p1, p2, p3, p4, p5); + pivot2[0] = p4; + return p2; + } + + @Override + int[] getSampledIndices(int left, int right) { + final int len = right - left; + final int seventh = 1 + (len >>> 3) + (len >>> 6); + final int p3 = left + (len >>> 1); + final int p2 = p3 - seventh; + final int p1 = p2 - seventh; + final int p4 = p3 + seventh; + final int p5 = p4 + seventh; + return new int[] {p1, p2, p3, p4, p5}; + } + + @Override + int samplingEffect() { + return SORT; + } + }, + /** + * This strategy is the same as {@link #SORT_5B} with the exception that it + * returns identical pivots if the data at the chosen pivots is equal. + * + *

This allows testing switching to a single pivot strategy against using + * a dual pivot partitioning with effectively only 1 pivot. This requires + * the dual pivot partition function to check pivot1 == pivot2. If the + * dual pivot partition function checks data[pivot1] == data[pivot2] then + * the switching choice cannot be enabled/disabled by changing pivoting strategy + * and must use another mechanism. + * + *

This specific strategy has been selected for single-pivot switching as + * {@link #SORT_5B} benchmarks as consistently fast across all data input. + */ + SORT_5B_SP { + @Override + int pivotIndex(double[] data, int left, int right, int[] pivot2) { + final int pivot1 = SORT_5B.pivotIndex(data, left, right, pivot2); + if (data[pivot1] == data[pivot2[0]]) { + // Here 3 of 5 middle values are the same. + // Present single-pivot pivot methods would not + // have an advantage pivoting on p2, p3, or p4; just use 'p2' + pivot2[0] = pivot1; + } + return pivot1; + } + + @Override + int[] getSampledIndices(int left, int right) { + return SORT_5B.getSampledIndices(left, right); + } + + @Override + int samplingEffect() { + return SORT; + } + }, + /** + * Pivot around the 2nd and 4th values from 5 approximately uniformly spaced within the range. + * Uses points +/- eights from the median: 1/4, 3/8, 1/2, 5/8, 3/4. + * + *

Requires {@code right - left >= 4}. + * + *

Warning: This has the side effect that the 5 values are also sorted. + * + *

On sorted data the tertiles are: 0.3750 0.6250 + *

On random data the tertiles are: + *

+     *         min      max     mean       sd   median     skew
+     * [1]  0.0010   0.9790   0.3324   0.1779   0.3130   0.4666
+     * [2]  0.0030   0.9850   0.3348   0.1778   0.3150   0.4686
+     * [3]  0.0010   0.9720   0.3327   0.1779   0.3130   0.4666
+     * 
+ */ + SORT_5C { + @Override + int pivotIndex(double[] data, int left, int right, int[] pivot2) { + // 1/8 = 0.125 + // Ensure the value is above zero to choose different points! + // This is safe if len >= 4. + final int len = right - left; + final int eighth = 1 + (len >>> 3); + final int p3 = left + (len >>> 1); + final int p2 = p3 - eighth; + final int p1 = p2 - eighth; + final int p4 = p3 + eighth; + final int p5 = p4 + eighth; + Sorting.sort5(data, p1, p2, p3, p4, p5); + pivot2[0] = p4; + return p2; + } + + @Override + int[] getSampledIndices(int left, int right) { + final int len = right - left; + final int eighth = 1 + (len >>> 3); + final int p3 = left + (len >>> 1); + final int p2 = p3 - eighth; + final int p1 = p2 - eighth; + final int p4 = p3 + eighth; + final int p5 = p4 + eighth; + return new int[] {p1, p2, p3, p4, p5}; + } + + @Override + int samplingEffect() { + return SORT; + } + }, + /** + * Pivot around the 2nd and 4th values from 5 medians approximately uniformly spaced within + * the range. The medians are from 3 samples. The 5 samples of 3 do not overlap thus this + * method requires {@code right - left >= 14}. The samples can be visualised as 5 sorted + * columns: + * + *
+     * v w x y z
+     * 1 2 3 4 5
+     * a b c d e
+     * 
+ * + *

The pivots are points 2 and 4. The other points are either known to be below or + * above the pivots; or potentially below or above the pivots. + * + *

Pivot 1: below {@code 1,a,b}; potentially below {@code v,c,d,e}. This ranks + * pivot 1 from 4/15 to 8/15 and exactly 5/15 if the input data is sorted/reverse sorted. + * + *

Pivot 2: above {@code 5,y,z}; potentially above {@code e,v,w,x}. This ranks + * pivot 2 from 7/15 to 11/15 and exactly 10/15 if the input data is sorted/reverse sorted. + * + *

Warning: This has the side effect that the 15 samples values are partially sorted. + * + *

On sorted data the tertiles are: 0.3140 0.6860 + *

On random data the tertiles are: + *

+     *         min      max     mean       sd   median     skew
+     * [1]  0.0090   0.9170   0.3783   0.1320   0.3730   0.2107
+     * [2]  0.0030   0.8950   0.2438   0.1328   0.2270   0.6150
+     * [3]  0.0110   0.9140   0.3779   0.1319   0.3730   0.2114
+     * 
+ *

Note the bias towards the outer regions. + */ + SORT_5_OF_3 { + @Override + int pivotIndex(double[] data, int left, int right, int[] pivot2) { + // Step size of 1/16 of the length + final int len = right - left; + final int step = Math.max(1, len >>> 4); + final int step3 = step * 3; + final int p3 = left + (len >>> 1); + final int p2 = p3 - step3; + final int p1 = p2 - step3; + final int p4 = p3 + step3; + final int p5 = p4 + step3; + // 5 medians of 3 + Sorting.sort3(data, p1 - step, p1, p1 + step); + Sorting.sort3(data, p2 - step, p2, p2 + step); + Sorting.sort3(data, p3 - step, p3, p3 + step); + Sorting.sort3(data, p4 - step, p4, p4 + step); + Sorting.sort3(data, p5 - step, p5, p5 + step); + // Sort the medians + Sorting.sort5(data, p1, p2, p3, p4, p5); + pivot2[0] = p4; + return p2; + } + + @Override + int[] getSampledIndices(int left, int right) { + final int len = right - left; + final int step = Math.max(1, len >>> 4); + final int step3 = step * 3; + final int p3 = left + (len >>> 1); + final int p2 = p3 - step3; + final int p1 = p2 - step3; + final int p4 = p3 + step3; + final int p5 = p4 + step3; + return new int[] { + p1 - step, p1, p1 + step, + p2 - step, p2, p2 + step, + p3 - step, p3, p3 + step, + p4 - step, p4, p4 + step, + p5 - step, p5, p5 + step, + }; + } + + @Override + int samplingEffect() { + return PARTIAL_SORT; + } + }, + /** + * Pivot around the 2nd and 3rd values from 4 medians approximately uniformly spaced within + * the range. The medians are from 3 samples. The 4 samples of 3 do not overlap thus this + * method requires {@code right - left >= 11}. The samples can be visualised as 4 sorted + * columns: + * + *

+     * w x y z
+     * 1 2 3 4
+     * a b c d
+     * 
+ * + *

The pivots are points 2 and 3. The other points are either known to be below or + * above the pivots; or potentially below or above the pivots. + * + *

Pivot 1: below {@code 1,a,b}; potentially below {@code w,c,d}. This ranks + * pivot 1 from 4/12 to 7/12 and exactly 5/12 if the input data is sorted/reverse sorted. + * + *

Pivot 2: above {@code 4,y,z}; potentially above {@code d,w,x}. This ranks + * pivot 2 from 5/15 to 8/12 and exactly 7/12 if the input data is sorted/reverse sorted. + * + *

Warning: This has the side effect that the 12 samples values are partially sorted. + * + *

On sorted data the tertiles are: 0.3850 0.6160 + *

On random data the tertiles are: + *

+     *         min      max     mean       sd   median     skew
+     * [1]  0.0160   0.9580   0.4269   0.1454   0.4230   0.1366
+     * [2]  0.0020   0.8270   0.1467   0.1193   0.1170   1.1417
+     * [3]  0.0140   0.9560   0.4264   0.1453   0.4230   0.1352
+     * 
+ *

Note the large bias towards the outer regions. + */ + SORT_4_OF_3 { + @Override + int pivotIndex(double[] data, int left, int right, int[] pivot2) { + // Step size of 1/13 of the length: 1/13 ~ 1/16 + 1/64 : 0.0769 ~ 0.0781 + final int len = right - left; + final int step = Math.max(1, (len >>> 4) + (len >>> 6)); + final int step3 = step * 3; + final int p1 = left + (step << 1) - 1; + final int p2 = p1 + step3; + final int p3 = p2 + step3; + final int p4 = p3 + step3; + // 5 medians of 3 + Sorting.sort3(data, p1 - step, p1, p1 + step); + Sorting.sort3(data, p2 - step, p2, p2 + step); + Sorting.sort3(data, p3 - step, p3, p3 + step); + Sorting.sort3(data, p4 - step, p4, p4 + step); + // Sort the medians + Sorting.sort4(data, p1, p2, p3, p4); + pivot2[0] = p3; + return p2; + } + + @Override + int[] getSampledIndices(int left, int right) { + final int len = right - left; + final int step = Math.max(1, (len >>> 4) + (len >>> 6)); + final int step3 = step * 3; + final int p1 = left + (step << 1) - 1; + final int p2 = p1 + step3; + final int p3 = p2 + step3; + final int p4 = p3 + step3; + return new int[] { + p1 - step, p1, p1 + step, + p2 - step, p2, p2 + step, + p3 - step, p3, p3 + step, + p4 - step, p4, p4 + step, + }; + } + + @Override + int samplingEffect() { + return PARTIAL_SORT; + } + }, + /** + * Pivot around the 1st and 3rd values from 3 medians approximately uniformly spaced within + * the range. The medians are from 3 samples. The 3 samples of 3 do not overlap thus this + * method requires {@code right - left >= 8}. The samples can be visualised as 3 sorted + * columns: + * + *

+     * x y z
+     * 1 2 3
+     * a b c
+     * 
+ * + *

The pivots are points 1 and 3. The other points are either known to be below or + * above the pivots; or potentially below or above the pivots. + * + *

Pivot 1: below {@code a}; potentially below {@code b, c}. This ranks + * pivot 1 from 2/9 to 4/9 and exactly 2/9 if the input data is sorted/reverse sorted. + * + *

Pivot 2: above {@code z}; potentially above {@code x,y}. This ranks + * pivot 2 from 6/9 to 8/9 and exactly 8/9 if the input data is sorted/reverse sorted. + * + *

Warning: This has the side effect that the 9 samples values are partially sorted. + * + *

On sorted data the tertiles are: 0.1280 0.8720 + *

On random data the tertiles are: + *

+     *         min      max     mean       sd   median     skew
+     * [1]  0.0010   0.9460   0.3062   0.1560   0.2910   0.4455
+     * [2]  0.0030   0.9820   0.3875   0.1813   0.3780   0.2512
+     * [3]  0.0010   0.9400   0.3063   0.1558   0.2910   0.4453
+     * 
+ *

Note the bias towards the central region. + */ + SORT_3_OF_3 { + @Override + int pivotIndex(double[] data, int left, int right, int[] pivot2) { + // Step size of 1/8 of the length + final int len = right - left; + final int step = Math.max(1, len >>> 3); + final int step3 = step * 3; + final int p2 = left + (len >>> 1); + final int p1 = p2 - step3; + final int p3 = p2 + step3; + // 3 medians of 3 + Sorting.sort3(data, p1 - step, p1, p1 + step); + Sorting.sort3(data, p2 - step, p2, p2 + step); + Sorting.sort3(data, p3 - step, p3, p3 + step); + // Sort the medians + Sorting.sort3(data, p1, p2, p3); + pivot2[0] = p3; + return p1; + } + + @Override + int[] getSampledIndices(int left, int right) { + final int len = right - left; + final int step = Math.max(1, len >>> 3); + final int step3 = step * 3; + final int p2 = left + (len >>> 1); + final int p1 = p2 - step3; + final int p3 = p2 + step3; + return new int[] { + p1 - step, p1, p1 + step, + p2 - step, p2, p2 + step, + p3 - step, p3, p3 + step, + }; + } + + @Override + int samplingEffect() { + return PARTIAL_SORT; + } + }, + /** + * Pivot around the 2nd and 4th values from 5 medians approximately uniformly spaced within + * the range. The medians are from 5 samples. The 5 samples of 5 do not overlap thus this + * method requires {@code right - left >= 24}. The samples can be visualised as 5 sorted + * columns: + * + *

+     * v w x y z
+     * q r s t u
+     * 1 2 3 4 5
+     * f g h i j
+     * a b c d e
+     * 
+ * + *

The pivots are points 2 and 4. The other points are either known to be below or + * above the pivots; or potentially below or above the pivots. + * + *

Pivot 1: below {@code 1,a,b,f,g}; potentially below {@code q,v,c,d,e,h,i,j}. This ranks + * pivot 1 from 6/25 to 14/25 and exactly 8/25 if the input data is sorted/reverse sorted. + * + *

Pivot 2 by symmetry from 12/25 to 20/25 and exactly 18/25 for sorted data. + * + *

Warning: This has the side effect that the 25 samples values are partially sorted. + * + *

On sorted data the tertiles are: 0.3050 0.6950 + *

On random data the tertiles are: + *

+     *         min      max     mean       sd   median     skew
+     * [1]  0.0270   0.8620   0.3996   0.1093   0.3970   0.1130
+     * [2]  0.0030   0.8100   0.2010   0.1106   0.1860   0.6691
+     * [3]  0.0270   0.8970   0.3994   0.1093   0.3970   0.1147
+     * 
+ *

Note the bias towards the outer regions on random data but the inner region on + * sorted data. + */ + SORT_5_OF_5 { + @Override + int pivotIndex(double[] data, int left, int right, int[] pivot2) { + // Step size of 1/25 of the length + final int len = right - left; + final int step = Math.max(1, len / 25); + final int step2 = step << 1; + final int step5 = step * 5; + final int p3 = left + (len >>> 1); + final int p2 = p3 - step5; + final int p1 = p2 - step5; + final int p4 = p3 + step5; + final int p5 = p4 + step5; + // 5 medians of 3 + Sorting.sort5(data, p1 - step2, p1 - step, p1, p1 + step, p1 + step2); + Sorting.sort5(data, p2 - step2, p2 - step, p2, p2 + step, p2 + step2); + Sorting.sort5(data, p3 - step2, p3 - step, p3, p3 + step, p3 + step2); + Sorting.sort5(data, p4 - step2, p4 - step, p4, p4 + step, p4 + step2); + Sorting.sort5(data, p5 - step2, p5 - step, p5, p5 + step, p5 + step2); + // Sort the medians + Sorting.sort5(data, p1, p2, p3, p4, p5); + pivot2[0] = p4; + return p2; + } + + @Override + int[] getSampledIndices(int left, int right) { + // Step size of 1/25 of the length + final int len = right - left; + final int step = Math.max(1, len / 25); + final int step2 = step << 1; + final int step5 = step * 5; + final int p3 = left + (len >>> 1); + final int p2 = p3 - step5; + final int p1 = p2 - step5; + final int p4 = p3 + step5; + final int p5 = p4 + step5; + return new int[] { + p1 - step2, p1 - step, p1, p1 + step, p1 + step2, + p2 - step2, p2 - step, p2, p2 + step, p2 + step2, + p3 - step2, p3 - step, p3, p3 + step, p3 + step2, + p4 - step2, p4 - step, p4, p4 + step, p4 + step2, + p5 - step2, p5 - step, p5, p5 + step, p5 + step2, + }; + } + + @Override + int samplingEffect() { + return PARTIAL_SORT; + } + }, + /** + * Pivot around the 3rd and 5th values from 7 approximately uniformly spaced within the range. + * Uses points +/- eights from the median: 1/8, 1/4, 3/8, 1/2, 5/8, 3/4, 7/8. + * + *

Requires {@code right - left >= 6}. + * + *

Warning: This has the side effect that the 7 values are also sorted. + * + *

On sorted data the tertiles are: 0.3760 0.6240 + *

On random data the tertiles are: + *

+     *         min      max     mean       sd   median     skew
+     * [1]  0.0020   0.9600   0.3745   0.1609   0.3640   0.3092
+     * [2]  0.0030   0.9490   0.2512   0.1440   0.2300   0.6920
+     * [3]  0.0030   0.9620   0.3743   0.1609   0.3640   0.3100
+     * 
+ *

Note the bias towards the outer regions. + */ + SORT_7 { + @Override + int pivotIndex(double[] data, int left, int right, int[] pivot2) { + // Ensure the value is above zero to choose different points! + // This is safe if len >= 4. + final int len = right - left; + final int eighth = Math.max(1, len >>> 3); + final int p4 = left + (len >>> 1); + final int p3 = p4 - eighth; + final int p2 = p3 - eighth; + final int p1 = p2 - eighth; + final int p5 = p4 + eighth; + final int p6 = p5 + eighth; + final int p7 = p6 + eighth; + Sorting.sort7(data, p1, p2, p3, p4, p5, p6, p7); + pivot2[0] = p5; + return p3; + } + + @Override + int[] getSampledIndices(int left, int right) { + final int len = right - left; + final int eighth = Math.max(1, len >>> 3); + final int p4 = left + (len >>> 1); + final int p3 = p4 - eighth; + final int p2 = p3 - eighth; + final int p1 = p2 - eighth; + final int p5 = p4 + eighth; + final int p6 = p5 + eighth; + final int p7 = p6 + eighth; + return new int[] {p1, p2, p3, p4, p5, p6, p7}; + } + + @Override + int samplingEffect() { + return SORT; + } + }, + /** + * Pivot around the 3rd and 6th values from 8 approximately uniformly spaced within the range. + * Uses points +/- ninths from the median: m - 4/9, m - 3/9, m - 2/9, m - 1/9; m + 1 + 1/9, + * m + 1 + 2/9, m + 1 + 3/9, m + 1 + 4/9. + * + *

Requires {@code right - left >= 7}. + * + *

Warning: This has the side effect that the 8 values are also sorted. + * + *

On sorted data the tertiles are: 0.3380 0.6630 + *

On random data the tertiles are: + *

+     *         min      max     mean       sd   median     skew
+     * [1]  0.0030   0.9480   0.3327   0.1485   0.3200   0.4044
+     * [2]  0.0050   0.9350   0.3345   0.1485   0.3220   0.4056
+     * [3]  0.0020   0.9320   0.3328   0.1485   0.3200   0.4063
+     * 
+ */ + SORT_8 { + @Override + int pivotIndex(double[] data, int left, int right, int[] pivot2) { + // 1/9 = 4/36 = 8/72 ~ 7/64 ~ 1/16 + 1/32 + 1/64 : 0.11111 ~ 0.1094 + // Ensure the value is above zero to choose different points! + // This is safe if len >= 7. + final int len = right - left; + final int ninth = Math.max(1, (len >>> 4) + (len >>> 5) + (len >>> 6)); + // Work from middle outward. This is deliberate to ensure data.length==7 + // throws an index out-of-bound exception. + final int m = left + (len >>> 1); + final int p4 = m - (ninth >> 1); + final int p3 = p4 - ninth; + final int p2 = p3 - ninth; + final int p1 = p2 - ninth; + final int p5 = m + (ninth >> 1) + 1; + final int p6 = p5 + ninth; + final int p7 = p6 + ninth; + final int p8 = p7 + ninth; + Sorting.sort8(data, p1, p2, p3, p4, p5, p6, p7, p8); + pivot2[0] = p6; + return p3; + } + + @Override + int[] getSampledIndices(int left, int right) { + final int len = right - left; + final int ninth = Math.max(1, (len >>> 4) + (len >>> 5) + (len >>> 6)); + final int m = left + (len >>> 1); + final int p4 = m - (ninth >> 1); + final int p3 = p4 - ninth; + final int p2 = p3 - ninth; + final int p1 = p2 - ninth; + final int p5 = m + (ninth >> 1) + 1; + final int p6 = p5 + ninth; + final int p7 = p6 + ninth; + final int p8 = p7 + ninth; + return new int[] {p1, p2, p3, p4, p5, p6, p7, p8}; + } + + @Override + int samplingEffect() { + return SORT; + } + }, + /** + * Pivot around the 4th and 8th values from 11 approximately uniformly spaced within the range. + * Uses points +/- twelfths from the median: ..., m - 1/12, m, m + 1/12, ... . + * + *

Requires {@code right - left >= 10}. + * + *

Warning: This has the side effect that the 11 values are also sorted. + * + *

On sorted data the tertiles are: 0.3460 0.6540 + *

On random data the tertiles are: + *

+     *         min      max     mean       sd   median     skew
+     * [1]  0.0060   0.9000   0.3328   0.1301   0.3230   0.3624
+     * [2]  0.0100   0.9190   0.3345   0.1299   0.3250   0.3643
+     * [3]  0.0060   0.8970   0.3327   0.1302   0.3230   0.3653
+     * 
+ */ + SORT_11 { + @Override + int pivotIndex(double[] data, int left, int right, int[] pivot2) { + // 1/12 = 8/96 ~ 1/16 + 1/32 ~ 9/96 : 0.8333 ~ 0.09375 + // Ensure the value is above zero to choose different points! + // This is safe if len >= 10. + final int len = right - left; + final int twelfth = Math.max(1, (len >>> 4) + (len >>> 6)); + final int p6 = left + (len >>> 1); + final int p5 = p6 - twelfth; + final int p4 = p5 - twelfth; + final int p3 = p4 - twelfth; + final int p2 = p3 - twelfth; + final int p1 = p2 - twelfth; + final int p7 = p6 + twelfth; + final int p8 = p7 + twelfth; + final int p9 = p8 + twelfth; + final int p10 = p9 + twelfth; + final int p11 = p10 + twelfth; + Sorting.sort11(data, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11); + pivot2[0] = p8; + return p4; + } + + @Override + int[] getSampledIndices(int left, int right) { + final int len = right - left; + final int twelfth = Math.max(1, (len >>> 4) + (len >>> 6)); + final int p6 = left + (len >>> 1); + final int p5 = p6 - twelfth; + final int p4 = p5 - twelfth; + final int p3 = p4 - twelfth; + final int p2 = p3 - twelfth; + final int p1 = p2 - twelfth; + final int p7 = p6 + twelfth; + final int p8 = p7 + twelfth; + final int p9 = p8 + twelfth; + final int p10 = p9 + twelfth; + final int p11 = p10 + twelfth; + return new int[] {p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11}; + } + + @Override + int samplingEffect() { + return SORT; + } + }; + + /** Sampled points are unchanged. */ + static final int UNCHANGED = 0; + /** Sampled points are partially sorted. */ + static final int PARTIAL_SORT = 0x1; + /** Sampled points are sorted. */ + static final int SORT = 0x2; + + /** + * Find two pivot indices of the array so that partitioning into 3-regions can be made. + * + *
{@code
+     * left <= p1 <= p2 <= right
+     * }
+ * + *

Returns two pivots so that {@code data[p1] <= data[p2]}. + * + * @param data Array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param pivot2 Second pivot. + * @return first pivot + */ + abstract int pivotIndex(double[] data, int left, int right, int[] pivot2); + + // The following methods allow the strategy and side effects to be tested + + /** + * Get the indices of points that will be sampled. + * + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @return the indices + */ + abstract int[] getSampledIndices(int left, int right); + + /** + * Get the effect on the sampled points. + *

+ * + * @return the effect + */ + abstract int samplingEffect(); +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/HashIndexSet.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/HashIndexSet.java new file mode 100644 index 000000000..a9e999abe --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/HashIndexSet.java @@ -0,0 +1,259 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * An index set backed by a open-addressed hash table using linear hashing. Table size is a power + * of 2 and has a maximum capacity of 2^29 with a fixed load factor of 0.5. If the functional + * capacity is exceeded then the set raises an {@link IllegalStateException}. + * + *

Values are stored using bit inversion. Any positive index will have a negative + * representation when stored. An empty slot is indicated by a zero. + * + *

This class has a minimal API. It can be used to ensure a collection of indices of + * a known size are unique: + * + *

{@code
+ * int[] keys = ...
+ * HashIndexSet set = new HashIndexSet(keys.length);
+ * for (int k : keys) {
+ *   if (set.add(k)) {
+ *     // first occurrence of k in keys
+ *   }
+ * }
+ * }
+ * + * @see Open addressing (Wikipedia) + * @since 1.2 + */ +final class HashIndexSet { + /** Message for an invalid index. */ + private static final String INVALID_INDEX = "Invalid index: "; + /** The maximum capacity of the set. */ + private static final int MAX_CAPACITY = 1 << 29; + /** The minimum size of the backing array. */ + private static final int MIN_SIZE = 16; + /** + * Unsigned 32-bit integer numerator of the golden ratio (0.618) with an assumed + * denominator of 2^32. + * + *
+     * 2654435769 = round(2^32 * (sqrt(5) - 1) / 2)
+     * Long.toHexString((long)(0x1p32 * (Math.sqrt(5.0) - 1) / 2))
+     * 
+ */ + private static final int PHI = 0x9e3779b9; + + /** The set. */ + private final int[] set; + /** The size. */ + private int size; + + /** + * Create an instance with size to store up to the specified {@code capacity}. + * + *

The functional capacity (number of indices that can be stored) is the next power + * of 2 above {@code capacity}; or a minimum size if the requested {@code capacity} is + * small. + * + * @param capacity Capacity (assumed to be positive). + */ + HashIndexSet(int capacity) { + if (capacity > MAX_CAPACITY) { + throw new IllegalArgumentException("Unsupported capacity: " + capacity); + } + // This will generate a load factor at capacity in the range (0.25, 0.5] + // The use of Math.max will ignore zero/negative capacity requests. + set = new int[nextPow2(Math.max(MIN_SIZE, capacity * 2))]; + } + + /** + * Return the memory footprint in bytes. This is always a power of 2. + * + *

This will return the size as if not limited to a capacity of 229. + * In this case the size will exceed the maximum size of an {@code int[]} array. + * + *

This method is intended to provide information to choose if the data structure + * is memory efficient. + * + * @param capacity Capacity. + * @return the memory footprint + */ + static long memoryFootprint(int capacity) { + if (capacity <= (MIN_SIZE >> 1)) { + // 4 bytes/int + return MIN_SIZE << 2; + } + // Double the next power of 2, then convert integer count to bytes (4 bytes/int) + // * 2 * 4 == * 2^3 + return Integer.toUnsignedLong(nextPow2(capacity)) << 3; + } + + /** + * Returns the closest power-of-two number greater than or equal to {@code value}. + * + *

Warning: This will return {@link Integer#MIN_VALUE} for any {@code value} above + * {@code 1 << 30}. This is the next power of 2 as an unsigned integer. + * + *

See Bit + * Hacks: Rounding up to a power of 2 + * + * @param value Value. + * @return the closest power-of-two number greater than or equal to value + */ + private static int nextPow2(int value) { + int result = value - 1; + result |= result >>> 1; + result |= result >>> 2; + result |= result >>> 4; + result |= result >>> 8; + return (result | (result >>> 16)) + 1; + } + + /** + * Adds the {@code index} to the set. + * + * @param index Index. + * @return true if the set was modified by the operation + * @throws IndexOutOfBoundsException if the index is negative + */ + boolean add(int index) { + if (index < 0) { + throw new IndexOutOfBoundsException(INVALID_INDEX + index); + } + final int[] keys = set; + final int key = ~index; + final int mask = keys.length - 1; + int pos = mix(index) & mask; + int curr = keys[pos]; + if (curr < 0) { + if (curr == key) { + // Already present + return false; + } + // Probe + while ((curr = keys[pos = (pos + 1) & mask]) < 0) { + if (curr == key) { + // Already present + return false; + } + } + } + // Insert + keys[pos] = key; + // Here the load factor is 0.5: Test if size > keys.length * 0.5 + if (++size > (mask + 1) >>> 1) { + // This is where we should grow the size of the set and re-insert + // all current keys into the new key storage. Here we are using a + // fixed capacity so raise an exception. + throw new IllegalStateException("Functional capacity exceeded: " + (keys.length >>> 1)); + } + return true; + } + + /** + * Test if the {@code index} is in the set. + * + *

This method is present for testing. It is not required when filtering a collection + * of indices with duplicates to a unique set of indices. + * + * @param index Index. + * @return true if the set contains the index + * @throws IndexOutOfBoundsException if the index is negative + */ + boolean contains(int index) { + if (index < 0) { + throw new IndexOutOfBoundsException(INVALID_INDEX + index); + } + final int[] keys = set; + final int mask = keys.length - 1; + int pos = mix(index) & mask; + int curr = keys[pos]; + if (curr == 0) { + return false; + } + final int key = ~index; + if (curr == key) { + return true; + } + // Probe + while (true) { + pos = (pos + 1) & mask; + curr = keys[pos]; + if (curr == 0) { + // No more entries + return false; + } + if (curr == key) { + return true; + } + } + } + + /** + * Mix the bits of an integer. + * + *

This is the fast hash function used in the linear hash implementation in the Koloboke Collections. + * + * @param x Bits. + * @return the mixed bits + */ + private static int mix(int x) { + final int h = x * PHI; + return h ^ (h >>> 16); + } + + /** + * Returns the number of distinct indices in the set. + * + * @return the size + */ + int size() { + return size; + } + + /** + * Write each index in the set into the provided array. Returns the number of indices. + * + *

The caller must ensure the output array has sufficient capacity. + * + *

Warning: The indices are not ordered. + * + *

This method is present for testing. It is not required when filtering a collection + * of indices with duplicates to a unique set of indices. It can be used to write + * out the unique indices, but they are not in the encounter order of indices + * {@link #add(int) added} to the set. + * + * @param a Output array. + * @return count of indices + * @see #size() + */ + int toArray(int[] a) { + final int[] keys = set; + int c = 0; + for (final int key : keys) { + if (key < 0) { + a[c++] = ~key; + } + } + // assert c == size + return c; + } +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/IndexIntervals.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/IndexIntervals.java new file mode 100644 index 000000000..2a94d293a --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/IndexIntervals.java @@ -0,0 +1,560 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * Support for creating {@link SearchableInterval}, {@link SearchableInterval2} and + * {@link UpdatingInterval} implementations. + * + * @since 1.2 + */ +final class IndexIntervals { + /** Size to perform key analysis. This avoids key analysis for a small number of keys. */ + private static final int KEY_ANALYSIS_SIZE = 10; + /** The upper threshold to use a modified insertion sort to find unique indices. */ + private static final int INDICES_INSERTION_SORT_SIZE = 20; + + /** Size to use a {@link BinarySearchKeyInterval}. Note that the + * {@link ScanningKeyInterval} uses points within the range to fast-forward + * scanning which improves performance significantly for a few hundred indices. + * Performance is similar when indices are in the thousands. Binary search is + * much faster when there are multiple thousands of indices. */ + private static final int BINARY_SEARCH_SIZE = 2048; + + /** No instances. */ + private IndexIntervals() {} + + /** + * Returns an interval that covers all indices ({@code [0, MAX_VALUE)}). + * + *

When used with a partition algorithm will cause a full sort + * of the range between the bounds {@code [ka, kb]}. + * + * @return the interval + */ + static SearchableInterval anyIndex() { + return AnyIndex.INSTANCE; + } + + /** + * Returns an interval that covers all indices ({@code [0, MAX_VALUE)}). + * + *

When used with a partition algorithm will cause a full sort + * of the range between the bounds {@code [ka, kb]}. + * + * @return the interval + */ + static SearchableInterval2 anyIndex2() { + return AnyIndex.INSTANCE; + } + + /** + * Returns an interval that covers a single index {@code k}. The interval cannot + * be split or the bounds updated. + * + * @param k Index. + * @return the interval + */ + static UpdatingInterval interval(int k) { + return new PointInterval(k); + } + + /** + * Returns an interval that covers all indices {@code [left, right]}. + * This method will sort the input bound to ensure {@code left <= right}. + * + *

When used with a partition algorithm will cause a full sort + * of the range between the bounds {@code [left, right]}. + * + * @param left Left bound (inclusive). + * @param right Right bound (inclusive). + * @return the interval + */ + static UpdatingInterval interval(int left, int right) { + // Sort the bound + final int l = left < right ? left : right; + final int r = left < right ? right : left; + return new RangeInterval(l, r); + } + + /** + * Returns an interval that covers the specified indices {@code k}. + * + * @param k Indices. + * @param n Count of indices (must be strictly positive). + * @return the interval + */ + static SearchableInterval createSearchableInterval(int[] k, int n) { + // Note: A typical use case is to have a few indices. Thus the heuristics + // in this method should be very fast when n is small. Here we skip them + // completely when the number of keys is tiny. + + if (n > KEY_ANALYSIS_SIZE) { + // Here we use a simple test based on the number of comparisons required + // to perform the expected next/previous look-ups. + // It is expected that we can cut n keys a maximum of n-1 times. + // Each cut requires a scan next/previous to divide the interval into two intervals: + // + // cut + // | + // k1--------k2---------k3---- ... ---------kn initial interval + // <--| find previous + // find next |--> + // k1 k2---------k3---- ... ---------kn divided intervals + // + // A ScanningKeyIndexInterval will scan n keys in both directions using n comparisons + // (if next takes m comparisons then previous will take n - m comparisons): Order(n^2) + // An IndexSet will scan from the cut location and find a match in time proportional to + // the index density. Average density is (size / n) and the scanning covers 64 + // indices together: Order(2 * n * (size / n) / 64) = Order(size / 32) + + // Get the range. This will throw an exception if there are no indices. + int min = k[n - 1]; + int max = min; + for (int i = n - 1; --i >= 0;) { + min = Math.min(min, k[i]); + max = Math.max(max, k[i]); + } + + // Transition when n * n ~ size / 32 + // Benchmarking shows this is a reasonable approximation when size is small. + // Speed of the IndexSet is approximately independent of n and proportional to size. + // Large size observes degrading performance more than expected from a linear relationship. + // Note the memory required is approximately (size / 8) bytes. + // We introduce a penalty for each 4x increase over size = 2^20 (== 128KiB). + // n * n = size/32 * 2^log4(size / 2^20) + + // Transition point: n = sqrt(size/32) + // size n + // 2^10 5.66 + // 2^15 32.0 + // 2^20 181.0 + + // Transition point: n = sqrt(size/32 * 2^(log4(size/2^20)))) + // size n + // 2^22 512.0 + // 2^24 1448.2 + // 2^28 11585 + // 2^31 55108 + + final int size = max - min + 1; + + // Divide by 32 is a shift of 5. This is reduced for each 4-fold size above 2^20. + // At 2^31 the shift reduces to 0. + int shift = 5; + if (size > (1 << 20)) { + // log4(size/2^20) == (log2(size) - 20) / 2 + shift -= (ceilLog2(size) - 20) >>> 1; + } + + if ((long) n * n > (size >> shift)) { + // Do not call IndexSet.of(k, n) which repeats the min/max search + // (especially given n is likely to be large). + final IndexSet interval = IndexSet.ofRange(min, max); + for (int i = n; --i >= 0;) { + interval.set(k[i]); + } + return interval; + } + + // Switch to binary search above a threshold. + // Note this invalidates the speed assumptions based on the number of comparisons. + // Benchmarking shows this is useful when the keys are in the thousands so this + // would be used when data size is in the millions. + if (n > BINARY_SEARCH_SIZE) { + final int unique = Sorting.sortIndices2(k, n); + return BinarySearchKeyInterval.of(k, unique); + } + + // Fall-though to the ScanningKeyIndexInterval... + } + + // This is the typical use case. + // Here n is small, or small compared to the min/max range of indices. + // Use a special method to sort unique indices (detects already sorted indices). + final int unique = Sorting.sortIndices2(k, n); + + return ScanningKeyInterval.of(k, unique); + } + + /** + * Returns an interval that covers the specified indices {@code k}. + * + * @param k Indices. + * @param n Count of indices (must be strictly positive). + * @return the interval + */ + static UpdatingInterval createUpdatingInterval(int[] k, int n) { + // Note: A typical use case is to have a few indices. Thus the heuristics + // in this method should be very fast when n is small. + // We have a choice between a KeyUpdatingInterval which requires + // sorted keys or a BitIndexUpdatingInterval which handles keys in any order. + // The purpose of the heuristics is to avoid a very bad choice of data structure, + // rather than choosing the best data structure in all situations. As long as the + // choice is reasonable the speed will not impact a partition algorithm. + + // Simple cases + if (n < 3) { + if (n == 1 || k[0] == k[1]) { + // 1 unique value + return IndexIntervals.interval(k[0]); + } + // 2 unique values + if (Math.abs(k[0] - k[1]) == 1) { + // Small range + return IndexIntervals.interval(k[0], k[1]); + } + // 2 well separated values + if (k[1] < k[0]) { + final int v = k[0]; + k[0] = k[1]; + k[1] = v; + } + return KeyUpdatingInterval.of(k, 2); + } + + // Strategy: Must be fast on already ascending data. + // Note: The recommended way to generate a lot of partition indices is to + // generate in sequence. + + // n <= small: + // Modified insertion sort (naturally finds ascending data) + // n > small: + // Look for ascending sequence and compact + // else: + // Remove duplicates using an order(1) data structure and sort + + if (n <= INDICES_INSERTION_SORT_SIZE) { + final int unique = Sorting.sortIndicesInsertionSort(k, n); + return KeyUpdatingInterval.of(k, unique); + } + + if (isAscending(k, n)) { + // For sorted keys the KeyUpdatingInterval is fast. It may be slower than the + // BitIndexUpdatingInterval depending on data length but not significantly + // slower and the difference is lost in the time taken for partitioning. + // So always use the keys. + final int unique = compressDuplicates(k, n); + return KeyUpdatingInterval.of(k, unique); + } + + // At least 20 indices that are partially unordered. + + // Find min/max to understand the range. + int min = k[n - 1]; + int max = min; + for (int i = n - 1; --i >= 0;) { + min = Math.min(min, k[i]); + max = Math.max(max, k[i]); + } + + // Here we use a simple test based on the number of comparisons required + // to perform the expected next/previous look-ups after a split. + // It is expected that we can cut n keys a maximum of n-1 times. + // Each cut requires a scan next/previous to divide the interval into two intervals: + // + // cut + // | + // k1--------k2---------k3---- ... ---------kn initial interval + // <--| find previous + // find next |--> + // k1 k2---------k3---- ... ---------kn divided intervals + // + // An BitSet will scan from the cut location and find a match in time proportional to + // the index density. Average density is (size / n) and the scanning covers 64 + // indices together: Order(2 * n * (size / n) / 64) = Order(size / 32) + + // Sorted keys: Sort time Order(n log(n)) : Splitting time Order(log(n)) (binary search approx) + // Bit keys : Sort time Order(1) : Splitting time Order(size / 32) + + // Transition when n * n ~ size / 32 + // Benchmarking shows this is a reasonable approximation when size < 2^20. + // The speed of the bit keys is approximately independent of n and proportional to size. + // Large size observes degrading performance of the bit keys vs sorted keys. + // We introduce a penalty for each 4x increase over size = 2^20. + // n * n = size/32 * 2^log4(size / 2^20) + // The transition point still favours the bit keys when sorted keys would be faster. + // However the difference is held within 4x and the BitSet type structure is still fast + // enough to be negligible against the speed of partitioning. + + // Transition point: n = sqrt(size/32) + // size n + // 2^10 5.66 + // 2^15 32.0 + // 2^20 181.0 + + // Transition point: n = sqrt(size/32 * 2^(log4(size/2^20)))) + // size n + // 2^22 512.0 + // 2^24 1448.2 + // 2^28 11585 + // 2^31 55108 + + final int size = max - min + 1; + + // Divide by 32 is a shift of 5. This is reduced for each 4-fold size above 2^20. + // At 2^31 the shift reduces to 0. + int shift = 5; + if (size > (1 << 20)) { + // log4(size/2^20) == (log2(size) - 20) / 2 + shift -= (ceilLog2(size) - 20) >>> 1; + } + + if ((long) n * n > (size >> shift)) { + // Do not call BitIndexUpdatingInterval.of(k, n) which repeats the min/max search + // (especially given n is likely to be large). + final BitIndexUpdatingInterval interval = BitIndexUpdatingInterval.ofRange(min, max); + for (int i = n; --i >= 0;) { + interval.set(k[i]); + } + return interval; + } + + // Sort with a hash set to filter indices + final int unique = Sorting.sortIndicesHashIndexSet(k, n); + return KeyUpdatingInterval.of(k, unique); + } + + /** + * Test the data is in ascending order: {@code data[i] <= data[i+1]} for all {@code i}. + * Data is assumed to be at least length 1. + * + * @param data Data. + * @param n Length of data. + * @return true if ascending + */ + private static boolean isAscending(int[] data, int n) { + for (int i = 0; ++i < n;) { + if (data[i] < data[i - 1]) { + // descending + return false; + } + } + return true; + } + + /** + * Compress duplicates in the ascending data. + * + *

Warning: Requires {@code n > 0}. + * + * @param data Indices. + * @param n Number of indices. + * @return the number of unique indices + */ + private static int compressDuplicates(int[] data, int n) { + // Compress to remove duplicates + int last = 0; + int top = data[0]; + for (int i = 0; ++i < n;) { + final int v = data[i]; + if (v == top) { + continue; + } + top = v; + data[++last] = v; + } + return last + 1; + } + + /** + * Compute {@code ceil(log2(x))}. This is valid for all strictly positive {@code x}. + * + *

Returns -1 for {@code x = 0} in place of -infinity. + * + * @param x Value. + * @return {@code ceil(log2(x))} + */ + private static int ceilLog2(int x) { + return 32 - Integer.numberOfLeadingZeros(x - 1); + } + + /** + * {@link SearchableInterval} for range {@code [0, MAX_VALUE)}. + */ + private static final class AnyIndex implements SearchableInterval, SearchableInterval2 { + /** Singleton instance. */ + static final AnyIndex INSTANCE = new AnyIndex(); + + @Override + public int left() { + return 0; + } + + @Override + public int right() { + return Integer.MAX_VALUE - 1; + } + + @Override + public int previousIndex(int k) { + return k; + } + + @Override + public int nextIndex(int k) { + return k; + } + + @Override + public int split(int ka, int kb, int[] upper) { + upper[0] = kb + 1; + return ka - 1; + } + + // IndexInterval2 + // This is exactly the same as IndexInterval as the pointers i are the same as the keys k + + @Override + public int start() { + return 0; + } + + @Override + public int end() { + return Integer.MAX_VALUE - 1; + } + + @Override + public int index(int i) { + return i; + } + + @Override + public int previous(int i, int k) { + return k; + } + + @Override + public int next(int i, int k) { + return k; + } + + @Override + public int split(int i1, int i2, int ka, int kb, int[] upper) { + upper[0] = kb + 1; + return ka - 1; + } + } + + /** + * {@link UpdatingInterval} for a single {@code index}. + */ + static final class PointInterval implements UpdatingInterval { + /** Left/right bound of the interval. */ + private final int index; + + /** + * @param k Left/right bound. + */ + PointInterval(int k) { + this.index = k; + } + + @Override + public int left() { + return index; + } + + @Override + public int right() { + return index; + } + + // Note: An UpdatingInterval is only required to update when a target index + // is within [left, right]. This is not possible for a single point. + + @Override + public int updateLeft(int k) { + throw new UnsupportedOperationException("updateLeft should not be called"); + } + + @Override + public int updateRight(int k) { + throw new UnsupportedOperationException("updateRight should not be called"); + } + + @Override + public UpdatingInterval splitLeft(int ka, int kb) { + throw new UnsupportedOperationException("splitLeft should not be called"); + } + + @Override + public UpdatingInterval splitRight(int ka, int kb) { + throw new UnsupportedOperationException("splitRight should not be called"); + } + } + + /** + * {@link UpdatingInterval} for range {@code [left, right]}. + */ + static final class RangeInterval implements UpdatingInterval { + /** Left bound of the interval. */ + private int left; + /** Right bound of the interval. */ + private int right; + + /** + * @param left Left bound. + * @param right Right bound. + */ + RangeInterval(int left, int right) { + this.left = left; + this.right = right; + } + + @Override + public int left() { + return left; + } + + @Override + public int right() { + return right; + } + + @Override + public int updateLeft(int k) { + // Assume left < k <= right + left = k; + return k; + } + + @Override + public int updateRight(int k) { + // Assume left <= k < right + right = k; + return k; + } + + @Override + public UpdatingInterval splitLeft(int ka, int kb) { + // Assume left < ka <= kb < right + final int lower = left; + left = kb + 1; + return new RangeInterval(lower, ka - 1); + } + + @Override + public UpdatingInterval splitRight(int ka, int kb) { + // Assume left < ka <= kb < right + final int upper = right; + right = ka - 1; + return new RangeInterval(kb + 1, upper); + } + } +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/IndexIterator.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/IndexIterator.java new file mode 100644 index 000000000..959717674 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/IndexIterator.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * An iterator of indices used for partitioning an array into multiple regions. + * + *

The iterator provides the functionality to iterate over blocks of indices + * defined by an inclusive interval {@code [left, right]}: + * + *

+ *   l----r
+ *            l----r
+ *                    lr
+ *                              lr
+ *                                       l----------------r
+ * 
+ * + * @since 1.2 + */ +interface IndexIterator { + /** + * The start (inclusive) of the current block of indices. + * + * @return start index + */ + int left(); + + /** + * The end (inclusive) of the current block of indices. + * + * @return end index + */ + int right(); + + /** + * The end index. + * + * @return the end index + */ + int end(); + + /** + * Advance the iterator to the next block of indices. + * + *

If there are no more indices the result of {@link #left()} and + * {@link #right()} is undefined. + * + * @return true if the iterator was advanced + */ + boolean next(); + + /** + * Advance the iterator so that {@code right > index}. + * + *

If there are no more indices the result of {@link #left()} and + * {@link #right()} is undefined. + * + *

The default implementation is: + *

{@code
+     * while (right() <= index) {
+     *     if (!next()) {
+     *         return false;
+     *     }
+     * }
+     * return false;
+     * }
+ * + *

Implementations may choose to set {@code left = index + 1} if the iterator + * range spans the {@code index}; all indices before {@code index} are no + * longer available for iteration. + * + * @param index Index. + * @return true if {@code right > index} + */ + default boolean positionAfter(int index) { + while (right() <= index) { + if (!next()) { + return false; + } + } + return true; + } + + /** + * Return true if the start of the next block of indices is after the specified {@code index}. + * A partition algorithm can use this to decide how to process the current block. + * + *

The default implementation is only true if there is no next index: + *

{@code
+     * return right() >= end();
+     * }
+ * + * @param index Index. + * @return true if the next {@code left > index}, or there is no next left + */ + default boolean nextAfter(int index) { + return right() >= end(); + } +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/IndexIterators.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/IndexIterators.java new file mode 100644 index 000000000..0f377a389 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/IndexIterators.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * Support for creating {@link IndexIterator} implementations. + * + * @since 1.2 + */ +final class IndexIterators { + + /** No instances. */ + private IndexIterators() {} + + /** + * Creates an iterator for index {@code k}. + * + * @param k Index. + * @return the iterator + */ + static IndexIterator ofIndex(int k) { + return new SingleIndex(k); + } + + /** + * Creates an iterator for the closed interval {@code [k1, k2]}. + * + *

This method handles duplicate indices; indices can be in any order. + * + * @param k1 Index. + * @param k2 Index. + * @return the iterator + */ + static IndexIterator ofInterval(int k1, int k2) { + // Eliminate duplicates + if (k1 == k2) { + return new SingleIndex(k1); + } + // Sort + final int i1 = k1 < k2 ? k1 : k2; + final int i2 = k1 < k2 ? k2 : k1; + return new SingleInterval(i1, i2); + } + + /** + * {@link IndexIterator} for a single index. + */ + private static final class SingleIndex implements IndexIterator { + /** Index. */ + private final int k; + + /** + * @param k Index. + */ + SingleIndex(int k) { + this.k = k; + } + + @Override + public int left() { + return k; + } + + @Override + public int right() { + return k; + } + + @Override + public int end() { + return k; + } + + @Override + public boolean next() { + return false; + } + + @Override + public boolean positionAfter(int index) { + return k > index; + } + + @Override + public boolean nextAfter(int index) { + // right >= end : no next index + return true; + } + } + + /** + * {@link IndexIterator} for a single closed interval {@code [left, right]}. + */ + private static final class SingleInterval implements IndexIterator { + /** Left index. */ + private final int l; + /** Right index. */ + private final int r; + + /** + * @param l Left index. + * @param r Right index. + */ + SingleInterval(int l, int r) { + this.l = l; + this.r = r; + } + + @Override + public int left() { + return l; + } + + @Override + public int right() { + return r; + } + + @Override + public int end() { + return r; + } + + @Override + public boolean next() { + return false; + } + + @Override + public boolean positionAfter(int index) { + return r > index; + } + + @Override + public boolean nextAfter(int index) { + // right >= end : no next index + return true; + } + } +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/IndexSet.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/IndexSet.java new file mode 100644 index 000000000..6467b80e7 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/IndexSet.java @@ -0,0 +1,1214 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.util.Arrays; +import java.util.function.IntConsumer; + +/** + * A fixed size set of indices within an inclusive range {@code [left, right]}. + * + *

This is a specialised class to implement a reduced API similar to a + * {@link java.util.BitSet}. It uses no bounds range checks and supports only a + * fixed size. It contains the methods required to store and look-up intervals of indices. + * + *

An offset is supported to allow the fixed size to cover a range of indices starting + * above 0 with the most efficient usage of storage. + * + *

The class has methods to directly set and get bits in the range. + * Implementations of the {@link PivotCache} interface use range checks and maintain + * floating pivots flanking the range to allow bracketing any index within the range. + * + *

Stores all pivots between the support {@code [left, right]}. Uses two + * floating pivots which are the closest known pivots surrounding this range. + * + *

See the BloomFilter code in Commons Collections for use of long[] data to store + * bits. + * + * @since 1.2 + */ +final class IndexSet implements PivotCache, SearchableInterval, SearchableInterval2 { + /** All 64-bits bits set. */ + private static final long LONG_MASK = -1L; + /** A bit shift to apply to an integer to divided by 64 (2^6). */ + private static final int DIVIDE_BY_64 = 6; + /** Default value for an unset upper floating pivot. + * Set as a value higher than any valid array index. */ + private static final int UPPER_DEFAULT = Integer.MAX_VALUE; + + /** Bit indexes. */ + private final long[] data; + + /** Left bound of the support. */ + private final int left; + /** Right bound of the support. */ + private final int right; + /** The upstream pivot closest to the left bound of the support. + * Provides a lower search bound for the range [left, right]. */ + private int lowerPivot = -1; + /** The downstream pivot closest to the right bound of the support. + * Provides an upper search bound for the range [left, right]. */ + private int upperPivot = UPPER_DEFAULT; + + /** + * Create an instance to store indices within the range {@code [left, right]}. + * + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + private IndexSet(int left, int right) { + this.left = left; + this.right = right; + // Allocate storage to store index==right + // Note: This may allow directly writing to index > right if there + // is extra capacity. Ranges checks to prevent this are provided by + // the PivotCache.add(int) method rather than using set(int). + data = new long[getLongIndex(right - left) + 1]; + } + + /** + * Create an instance to store indices within the range {@code [left, right]}. + * + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @return the index set + * @throws IllegalArgumentException if {@code right < left} + */ + static IndexSet ofRange(int left, int right) { + if (left < 0) { + throw new IllegalArgumentException("Invalid lower index: " + left); + } + checkRange(left, right); + return new IndexSet(left, right); + } + + /** + * Initialise an instance with the {@code indices}. The capacity is defined by the + * range required to store the minimum and maximum index. + * + * @param indices Indices. + * @return the index set + * @throws IllegalArgumentException if {@code indices.length == 0} + */ + static IndexSet of(int[] indices) { + return of(indices, indices.length); + } + + /** + * Initialise an instance with the {@code indices}. The capacity is defined by the + * range required to store the minimum and maximum index. + * + * @param indices Indices. + * @param n Number of indices. + * @return the index set + * @throws IllegalArgumentException if {@code n == 0} + */ + static IndexSet of(int[] indices, int n) { + if (n <= 0) { + throw new IllegalArgumentException("No indices to define the range"); + } + int min = indices[0]; + int max = min; + for (int i = 1; i < n; i++) { + min = Math.min(min, indices[i]); + max = Math.max(max, indices[i]); + } + final IndexSet set = IndexSet.ofRange(min, max); + for (int i = 0; i < n; i++) { + set.set(indices[i]); + } + return set; + } + + /** + * Return the memory footprint in bytes. This is always a multiple of 64. + * + *

The result is {@code 8 * ceil((right - left + 1) / 64)}. + * + *

This method is intended to provide information to choose if the data structure + * is memory efficient. + * + *

Warning: It is assumed {@code 0 <= left <= right}. Use with the min/max index + * that is to be stored. + * + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @return the memory footprint + */ + static long memoryFootprint(int left, int right) { + return (getLongIndex(right - left) + 1L) * Long.BYTES; + } + + /** + * Gets the filter index for the specified bit index assuming the filter is using + * 64-bit longs to store bits starting at index 0. + * + *

The index is assumed to be positive. For a positive index the result will match + * {@code bitIndex / 64}.

+ * + *

The divide is performed using bit shifts. If the input is negative the + * behavior is not defined.

+ * + * @param bitIndex the bit index (assumed to be positive) + * @return the index of the bit map in an array of bit maps. + */ + private static int getLongIndex(final int bitIndex) { + // An integer divide by 64 is equivalent to a shift of 6 bits if the integer is + // positive. + // We do not explicitly check for a negative here. Instead we use a + // signed shift. Any negative index will produce a negative value + // by sign-extension and if used as an index into an array it will throw an + // exception. + return bitIndex >> DIVIDE_BY_64; + } + + /** + * Gets the filter bit mask for the specified bit index assuming the filter is using + * 64-bit longs to store bits starting at index 0. The returned value is a + * {@code long} with only 1 bit set. + * + *

The index is assumed to be positive. For a positive index the result will match + * {@code 1L << (bitIndex % 64)}.

+ * + *

If the input is negative the behavior is not defined.

+ * + * @param bitIndex the bit index (assumed to be positive) + * @return the filter bit + */ + private static long getLongBit(final int bitIndex) { + // Bit shifts only use the first 6 bits. Thus it is not necessary to mask this + // using 0x3f (63) or compute bitIndex % 64. + // Note: If the index is negative the shift will be (64 - (bitIndex & 0x3f)) and + // this will identify an incorrect bit. + return 1L << bitIndex; + } + + // Compressed cardinality methods + + /** + * Returns the number of bits set to {@code true} in this {@code IndexSet} using a + * compression of 2 to 1. This counts as enabled all bits of each consecutive + * 2 bits if any of the consecutive 2 bits are set to {@code true}. + *
+     * 0010100011000101000100
+     * 0 2 2 0 2 0 2 2 0 2 0
+     * 
+ *

This method can be used to assess the saturation of the indices in the range. + * + * @return the number of bits set to {@code true} in this {@code IndexSet} using a + * compression of 2 to 1 + */ + public int cardinality2() { + int c = 0; + for (long x : data) { + // Shift and mask out the bits that were shifted + x = (x | (x >>> 1)) & 0b0101010101010101010101010101010101010101010101010101010101010101L; + // Add [0, 32] + c += Long.bitCount(x); + } + // Multiply by 2 + return c << 1; + } + + /** + * Returns the number of bits set to {@code true} in this {@code IndexSet} using a + * compression of 4 to 1. This counts as enabled all bits of each consecutive + * 4 bits if any of the consecutive 4 bits are set to {@code true}. + *

+     * 0010000011000101000100
+     * 4   0   4   4   4   0
+     * 
+ *

This method can be used to assess the saturation of the indices in the range. + * + * @return the number of bits set to {@code true} in this {@code IndexSet} using a compression + * of 8 to 1 + */ + public int cardinality4() { + int c = 0; + for (long x : data) { + // Shift powers of 2 and mask out the bits that were shifted + x = x | (x >>> 1); + x = (x | (x >>> 2)) & 0b0001000100010001000100010001000100010001000100010001000100010001L; + // Expect a population count intrinsic method + // Add [0, 16] + c += Long.bitCount(x); + } + // Multiply by 4 + return c << 2; + } + + /** + * Returns the number of bits set to {@code true} in this {@code IndexSet} using a + * compression of 8 to 1. This counts as enabled all bits of each consecutive + * 8 bits if any of the consecutive 8 bits are set to {@code true}. + *

+     * 0010000011000101000000
+     * 8       8       0
+     * 
+ *

This method can be used to assess the saturation of the indices in the range. + * + * @return the number of bits set to {@code true} in this {@code IndexSet} using a compression + * of 8 to 1 + */ + public int cardinality8() { + int c = 0; + for (long x : data) { + // Shift powers of 2 and mask out the bits that were shifted + x = x | (x >>> 1); + x = x | (x >>> 2); + x = (x | (x >>> 4)) & 0b0000000100000001000000010000000100000001000000010000000100000001L; + // Expect a population count intrinsic method + // Add [0, 8] + c += Long.bitCount(x); + } + // Multiply by 8 + return c << 3; + } + + /** + * Returns the number of bits set to {@code true} in this {@code IndexSet} using a + * compression of 16 to 1. This counts as enabled all bits of each consecutive + * 16 bits if any of the consecutive 16 bits are set to {@code true}. + *

+     * 0010000011000101000000
+     * 16              0
+     * 
+ *

This method can be used to assess the saturation of the indices in the range. + * + * @return the number of bits set to {@code true} in this {@code IndexSet} using a compression + * of 16 to 1 + */ + public int cardinality16() { + int c = 0; + for (long x : data) { + // Shift powers of 2 and mask out the bits that were shifted + x = x | (x >>> 1); + x = x | (x >>> 2); + x = x | (x >>> 4); + x = (x | (x >>> 8)) & 0b0000000000000001000000000000000100000000000000010000000000000001L; + // Count the bits using folding + // if x = mask: + // (x += (x >>> 16)) : 0000000000000001000000000000001000000000000000100000000000000010 + // (x += (x >>> 32)) : 0000000100000001000000100000001000000011000000110000010000000100 + x = x + (x >>> 16); // put count of each 32 bits into their lowest 2 bits + x = x + (x >>> 32); // put count of each 64 bits into their lowest 3 bits + // Add [0, 4] + c += (int) x & 0b111; + } + // Multiply by 16 + return c << 4; + } + + /** + * Returns the number of bits set to {@code true} in this {@code IndexSet} using a + * compression of 32 to 1. This counts as enabled all bits of each consecutive + * 32 bits if any of the consecutive 32 bits are set to {@code true}. + *

+     * 0010000011000101000000
+     * 32
+     * 
+ *

This method can be used to assess the saturation of the indices in the range. + * + * @return the number of bits set to {@code true} in this {@code IndexSet} using a compression + * of 32 to 1 + */ + public int cardinality32() { + int c = 0; + for (final long x : data) { + // Are any lower 32-bits or upper 32-bits set? + c += (int) x != 0 ? 1 : 0; + c += (x >>> 32) != 0L ? 1 : 0; + } + // Multiply by 32 + return c << 5; + } + + /** + * Returns the number of bits set to {@code true} in this {@code IndexSet} using a + * compression of 64 to 1. This counts as enabled all bits of each consecutive + * 64 bits if any of the consecutive 64 bits are set to {@code true}. + *

+     * 0010000011000101000000
+     * 64
+     * 
+ *

This method can be used to assess the saturation of the indices in the range. + * + * @return the number of bits set to {@code true} in this {@code IndexSet} using a compression + * of 64 to 1 + */ + public int cardinality64() { + int c = 0; + for (final long x : data) { + // Are any bits set? + c += x != 0L ? 1 : 0; + } + // Multiply by 64 + return c << 6; + } + + // Adapt method API from BitSet + + /** + * Returns the number of bits set to {@code true} in this {@code IndexSet}. + * + * @return the number of bits set to {@code true} in this {@code IndexSet} + */ + public int cardinality() { + int c = 0; + for (final long x : data) { + c += Long.bitCount(x); + } + return c; + } + + /** + * Returns the value of the bit with the specified index. + * + * @param bitIndex the bit index (assumed to be positive) + * @return the value of the bit with the specified index + */ + public boolean get(int bitIndex) { + // WARNING: No range checks !!! + final int index = bitIndex - left; + final int i = getLongIndex(index); + final long m = getLongBit(index); + return (data[i] & m) != 0; + } + + /** + * Sets the bit at the specified index to {@code true}. + * + *

Warning: This has no range checks. Use {@link #add(int)} to add an index that + * may be outside the support. + * + * @param bitIndex the bit index (assumed to be positive) + */ + public void set(int bitIndex) { + // WARNING: No range checks !!! + final int index = bitIndex - left; + final int i = getLongIndex(index); + final long m = getLongBit(index); + data[i] |= m; + } + + /** + * Sets the bits from the specified {@code leftIndex} (inclusive) to the specified + * {@code rightIndex} (inclusive) to {@code true}. + * + *

If {@code rightIndex - leftIndex < 0} the behavior is not defined.

+ * + *

Note: In contrast to the BitSet API, this uses an inclusive end as this + * is the main use case for the class. + * + *

Warning: This has no range checks. Use {@link #add(int, int)} to range that + * may be outside the support. + * + * @param leftIndex the left index + * @param rightIndex the right index + */ + public void set(int leftIndex, int rightIndex) { + final int l = leftIndex - left; + final int r = rightIndex - left; + + // WARNING: No range checks !!! + int i = getLongIndex(l); + final int j = getLongIndex(r); + + // Fill in bits using (big-endian mask): + // end middle start + // 00011111 11111111 11111100 + + // start = -1L << (left % 64) + // end = -1L >>> (64 - ((right+1) % 64)) + final long start = LONG_MASK << l; + final long end = LONG_MASK >>> -(r + 1); + if (i == j) { + // Special case where the two masks overlap at the same long index + // 11111100 & 00011111 => 00011100 + data[i] |= start & end; + } else { + // 11111100 + data[i] |= start; + while (++i < j) { + // 11111111 + // Note: -1L is all bits set + data[i] = -1L; + } + // 00011111 + data[j] |= end; + } + } + + /** + * Returns the index of the nearest bit that is set to {@code true} that occurs on or + * before the specified starting index. If no such bit exists, then {@code -1} is returned. + * + * @param fromIndex Index to start checking from (inclusive). + * @return the index of the previous set bit, or {@code -1} if there is no such bit + */ + public int previousSetBit(int fromIndex) { + return previousSetBitOrElse(fromIndex, -1); + } + + /** + * Returns the index of the nearest bit that is set to {@code true} that occurs on or + * before the specified starting index. If no such bit exists, then + * {@code defaultValue} is returned. + * + * @param fromIndex Index to start checking from (inclusive). + * @param defaultValue Default value. + * @return the index of the previous set bit, or {@code defaultValue} if there is no such bit + */ + int previousSetBitOrElse(int fromIndex, int defaultValue) { + if (fromIndex < left) { + // index is in an unknown range + return defaultValue; + } + final int index = fromIndex - left; + int i = getLongIndex(index); + + long bits = data[i]; + + // Repeat logic of get(int) to check the bit + if ((bits & getLongBit(index)) != 0) { + return fromIndex; + } + + // Mask bits before the bit index + // mask = 00011111 = -1L >>> (64 - ((index + 1) % 64)) + bits &= LONG_MASK >>> -(index + 1); + for (;;) { + if (bits != 0) { + //(i+1) i + // | index | + // | | | + // 0 001010000 + return (i + 1) * Long.SIZE - Long.numberOfLeadingZeros(bits) - 1 + left; + } + if (i == 0) { + return defaultValue; + } + bits = data[--i]; + } + } + + /** + * Returns the index of the first bit that is set to {@code true} that occurs on or + * after the specified starting index. If no such bit exists then {@code -1} is + * returned. + * + * @param fromIndex Index to start checking from (inclusive). + * @return the index of the next set bit, or {@code -1} if there is no such bit + */ + public int nextSetBit(int fromIndex) { + return nextSetBitOrElse(fromIndex, -1); + } + + /** + * Returns the index of the first bit that is set to {@code true} that occurs on or + * after the specified starting index. If no such bit exists then {@code defaultValue} is + * returned. + * + * @param fromIndex Index to start checking from (inclusive). + * @param defaultValue Default value. + * @return the index of the next set bit, or {@code defaultValue} if there is no such bit + */ + int nextSetBitOrElse(int fromIndex, int defaultValue) { + // Support searching forward through the known range + final int index = fromIndex < left ? 0 : fromIndex - left; + + int i = getLongIndex(index); + + // Mask bits after the bit index + // mask = 11111000 = -1L << (index % 64) + long bits = data[i] & (LONG_MASK << index); + for (;;) { + if (bits != 0) { + //(i+1) i + // | index | + // | | | + // 0 001010000 + return i * Long.SIZE + Long.numberOfTrailingZeros(bits) + left; + } + if (++i == data.length) { + return defaultValue; + } + bits = data[i]; + } + } + + /** + * Returns the index of the first bit that is set to {@code false} that occurs on or + * after the specified starting index within the supported range. If no such + * bit exists then the {@code capacity} is returned where {@code capacity = index + 1} + * with {@code index} the largest index that can be added to the set without an error. + * + *

If the starting index is less than the supported range the result is {@code fromIndex}. + * + * @param fromIndex Index to start checking from (inclusive). + * @return the index of the next unset bit, or the {@code capacity} if there is no such bit + */ + public int nextClearBit(int fromIndex) { + if (fromIndex < left) { + return fromIndex; + } + // Support searching forward through the known range + final int index = fromIndex - left; + + int i = getLongIndex(index); + + // Note: This method is conceptually the same as nextSetBit with the exception + // that: all the data is bit-flipped; the capacity is returned when the + // scan reaches the end. + + // Mask bits after the bit index + // mask = 11111000 = -1L << (fromIndex % 64) + long bits = ~data[i] & (LONG_MASK << index); + for (;;) { + if (bits != 0) { + return i * Long.SIZE + Long.numberOfTrailingZeros(bits) + left; + } + if (++i == data.length) { + // Capacity + return data.length * Long.SIZE + left; + } + bits = ~data[i]; + } + } + + /** + * Returns the index of the first bit that is set to {@code false} that occurs on or + * before the specified starting index within the supported range. If no such + * bit exists then {@code -1} is returned. + * + *

If the starting index is less than the supported range the result is {@code fromIndex}. + * This can return {@code -1} only if the support begins at {@code index == 0}. + * + * @param fromIndex Index to start checking from (inclusive). + * @return the index of the previous unset bit, or {@code -1} if there is no such bit + */ + public int previousClearBit(int fromIndex) { + if (fromIndex < left) { + // index is in an unknown range + return fromIndex; + } + final int index = fromIndex - left; + int i = getLongIndex(index); + + // Note: This method is conceptually the same as previousSetBit with the exception + // that: all the data is bit-flipped; the offset - 1 is returned when the + // scan reaches the end. + + // Mask bits before the bit index + // mask = 00011111 = -1L >>> (64 - ((index + 1) % 64)) + long bits = ~data[i] & (LONG_MASK >>> -(index + 1)); + for (;;) { + if (bits != 0) { + //(i+1) i + // | index | + // | | | + // 0 001010000 + return (i + 1) * Long.SIZE - Long.numberOfLeadingZeros(bits) - 1 + left; + } + if (i == 0) { + return left - 1; + } + bits = ~data[--i]; + } + } + + /** + * Perform the {@code action} for each index in the set. + * + * @param action Action. + */ + public void forEach(IntConsumer action) { + // Adapted from o.a.c.collections4.IndexProducer + int wordIdx = left; + for (int i = 0; i < data.length; i++) { + long bits = data[i]; + int index = wordIdx; + while (bits != 0) { + if ((bits & 1L) == 1L) { + action.accept(index); + } + bits >>>= 1; + index++; + } + wordIdx += Long.SIZE; + } + } + + /** + * Write each index in the set into the provided array. + * Returns the number of indices. + * + *

The caller must ensure the output array has sufficient capacity. + * For example the array used to construct the IndexSet. + * + * @param a Output array. + * @return count of indices + * @see #of(int[]) + * @see #of(int[], int) + */ + public int toArray(int[] a) { + // This benchmarks as faster for index sorting than toArray2 even at + // high density (average separation of 2). + int n = -1; + int offset = left; + for (long bits : data) { + while (bits != 0) { + final int index = Long.numberOfTrailingZeros(bits); + a[++n] = index + offset; + bits &= ~(1L << index); + } + offset += Long.SIZE; + } + return n + 1; + } + + /** + * Write each index in the set into the provided array. + * Returns the number of indices. + * + *

The caller must ensure the output array has sufficient capacity. + * For example the array used to construct the IndexSet. + * + * @param a Output array. + * @return count of indices + * @see #of(int[]) + * @see #of(int[], int) + */ + public int toArray2(int[] a) { + // Adapted from o.a.c.collections4.IndexProducer + int n = -1; + for (int i = 0, offset = left; i < data.length; i++, offset += Long.SIZE) { + long bits = data[i]; + int index = offset; + while (bits != 0) { + if ((bits & 1L) == 1L) { + a[++n] = index; + } + bits >>>= 1; + index++; + } + } + return n + 1; + } + + // PivotCache interface + + @Override + public int left() { + return left; + } + + @Override + public int right() { + return right; + } + + @Override + public boolean sparse() { + // Can store all pivots between [left, right] + return false; + } + + @Override + public boolean contains(int k) { + // Assume [left <= k <= right] + return get(k); + } + + @Override + public void add(int index) { + // Update the floating pivots if outside the support + if (index < left) { + lowerPivot = Math.max(index, lowerPivot); + } else if (index > right) { + upperPivot = Math.min(index, upperPivot); + } else { + set(index); + } + } + + @Override + public void add(int fromIndex, int toIndex) { + // Optimisation for the main use case of the PivotCache + if (fromIndex == toIndex) { + add(fromIndex); + return; + } + + // Note: + // Storing all pivots allows regions of identical values + // and sorted regions to be skipped in subsequent partitioning. + // Repeat sorting these regions is typically more expensive + // than caching them and moving over them during partitioning. + // An alternative is to: store fromIndex and only store + // toIndex if they are well separated, optionally storing + // regions between. If they are not well separated (e.g. < 10) + // then using a single pivot is an alternative to investigate + // with performance benchmarks on a range of input data. + + // Pivots are required to bracket [L, R]: + // LP-----L--------------R------UP + // If the range [i, j] overlaps either L or R then + // the floating pivots are no longer required: + // i-j Set lower pivot + // i--------j Ignore lower pivot + // i---------------------j Ignore lower & upper pivots (no longer required) + // i-------j Ignore lower & upper pivots + // i---------------j Ignore upper pivot + // i-j Set upper pivot + if (fromIndex <= right && toIndex >= left) { + // Clip the range between [left, right] + final int i = Math.max(fromIndex, left); + final int j = Math.min(toIndex, right); + set(i, j); + } else if (toIndex < left) { + lowerPivot = Math.max(toIndex, lowerPivot); + } else { + // fromIndex > right + upperPivot = Math.min(fromIndex, upperPivot); + } + } + + @Override + public int previousPivot(int k) { + // Assume scanning in [left <= k <= right] + return previousSetBitOrElse(k, lowerPivot); + } + + @Override + public int nextPivotOrElse(int k, int other) { + // Assume scanning in [left <= k <= right] + final int p = upperPivot == UPPER_DEFAULT ? other : upperPivot; + return nextSetBitOrElse(k, p); + } + + // IndexInterval + + @Override + public int previousIndex(int k) { + // Re-implement previousSetBitOrElse without index checks + // as this supports left <= k <= right + + final int index = k - left; + int i = getLongIndex(index); + + // Mask bits before the bit index + // mask = 00011111 = -1L >>> (64 - ((index + 1) % 64)) + long bits = data[i] & (LONG_MASK >>> -(index + 1)); + for (;;) { + if (bits != 0) { + //(i+1) i + // | index | + // | | | + // 0 001010000 + return (i + 1) * Long.SIZE - Long.numberOfLeadingZeros(bits) - 1 + left; + } + // Unsupported: the interval should contain k + //if (i == 0) { + // return left - 1; + //} + bits = data[--i]; + } + } + + @Override + public int nextIndex(int k) { + // Re-implement nextSetBitOrElse without index checks + // as this supports left <= k <= right + + final int index = k - left; + int i = getLongIndex(index); + + // Mask bits after the bit index + // mask = 11111000 = -1L << (index % 64) + long bits = data[i] & (LONG_MASK << index); + for (;;) { + if (bits != 0) { + //(i+1) i + // | index | + // | | | + // 0 001010000 + return i * Long.SIZE + Long.numberOfTrailingZeros(bits) + left; + } + // Unsupported: the interval should contain k + //if (++i == data.length) { + // return right + 1; + //} + bits = data[++i]; + } + } + + // No override for split. + // This requires searching for previousIndex(k - 1) and nextIndex(k + 1). + // The only shared code is getLongIndex(x - left). Since argument indices are 2 apart + // these will map to a different long with a probability of 1/32. + + // IndexInterval2 + // This is exactly the same as IndexInterval as the pointers i are the same as the keys k + + @Override + public int start() { + return left(); + } + + @Override + public int end() { + return right(); + } + + @Override + public int index(int i) { + return i; + } + + @Override + public int previous(int i, int k) { + return previousIndex(k); + } + + @Override + public int next(int i, int k) { + return nextIndex(k); + } + + /** + * Return a {@link ScanningPivotCache} implementation re-using the same internal storage. + * + *

Note that the range for the {@link ScanningPivotCache} must fit inside the current + * supported range of indices. + * + *

Warning: This operation clears all set bits within the range. + * + *

Support + * + *

The returned {@link ScanningPivotCache} is suitable for storing all pivot points between + * {@code [left, right]} and the closest bounding pivots outside that range. It can be + * used for bracketing partition keys processed in a random order by storing pivots + * found during each successive partition search. + * + *

The returned {@link ScanningPivotCache} is suitable for use when iterating over + * partition keys in ascending order. + * + *

The cache allows incrementing the {@code left} support using + * {@link ScanningPivotCache#moveLeft(int)}. Any calls to decrement the {@code left} support + * at any time will result in an {@link UnsupportedOperationException}; this prevents reseting + * to within the original support used to create the cache. If the {@code left} is + * moved beyond the {@code right} then the move is rejected. + * + * @param lower Lower bound (inclusive). + * @param upper Upper bound (inclusive). + * @return the pivot cache + * @throws IllegalArgumentException if {@code right < left} or the range cannot be + * supported. + */ + ScanningPivotCache asScanningPivotCache(int lower, int upper) { + return asScanningPivotCache(lower, upper, true); + } + + /** + * Return a {@link ScanningPivotCache} implementation to support the range + * {@code [left, right]}. + * + *

See {@link #asScanningPivotCache(int, int)} for the details of the cache implementation. + * + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @return the pivot cache + * @throws IllegalArgumentException if {@code right < left} + */ + static ScanningPivotCache createScanningPivotCache(int left, int right) { + final IndexSet set = ofRange(left, right); + return set.asScanningPivotCache(left, right, false); + } + + /** + * Return a {@link ScanningPivotCache} implementation to support the range + * {@code [left, right]} re-using the same internal storage. + * + * @param lower Lower bound (inclusive). + * @param upper Upper bound (inclusive). + * @param initialize Perform validation checks and initialize the storage. + * @return the pivot cache + * @throws IllegalArgumentException if {@code right < left} or the range cannot be + * supported. + */ + private ScanningPivotCache asScanningPivotCache(int lower, int upper, boolean initialize) { + if (initialize) { + checkRange(lower, upper); + final int capacity = data.length * Long.SIZE + lower; + if (lower < left || upper >= capacity) { + throw new IllegalArgumentException( + String.format("Unsupported range: [%d, %d] is not within [%d, %d]", lower, upper, + left, capacity - 1)); + } + // Clear existing data + Arrays.fill(data, 0); + } + return new IndexPivotCache(lower, upper); + } + + /** + * Return an {@link UpdatingInterval} implementation to support the range + * {@code [left, right]} re-using the same internal storage. + * + * @return the interval + */ + UpdatingInterval interval() { + return new IndexSetUpdatingInterval(left, right); + } + + /** + * Check the range is valid. + * + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @throws IllegalArgumentException if {@code right < left} + */ + private static void checkRange(int left, int right) { + if (right < left) { + throw new IllegalArgumentException( + String.format("Invalid range: [%d, %d]", left, right)); + } + } + + /** + * Implementation of the {@link ScanningPivotCache} using the {@link IndexSet}. + * + *

Stores all pivots between the support {@code [left, right]}. Uses two + * floating pivots which are the closest known pivots surrounding this range. + * + *

This class is bound to the enclosing {@link IndexSet} instance to provide + * the functionality to read, write and search indexes. + * + *

Note: This duplicates functionality of the parent IndexSet. Differences + * are that it uses a movable left bound and implements the scanning functionality + * of the {@link ScanningPivotCache} interface. It can also be created for + * a smaller {@code [left, right]} range than the enclosing class. + * + *

Creation of this class typically invalidates the use of the outer class. + * Creation will zero the underlying storage and the range may be different. + */ + private class IndexPivotCache implements ScanningPivotCache { + /** Left bound of the support. */ + private int left; + /** Right bound of the support. */ + private final int right; + /** The upstream pivot closest to the left bound of the support. + * Provides a lower search bound for the range [left, right]. */ + private int lowerPivot = -1; + /** The downstream pivot closest to the right bound of the support. + * Provides an upper search bound for the range [left, right]. */ + private int upperPivot = UPPER_DEFAULT; + + /** + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + IndexPivotCache(int left, int right) { + this.left = left; + this.right = right; + } + + @Override + public int left() { + return left; + } + + @Override + public int right() { + return right; + } + + @Override + public boolean sparse() { + // Can store all pivots between [left, right] + return false; + } + + @Override + public boolean moveLeft(int newLeft) { + if (newLeft > right) { + // Signal that this cache can no longer be used in that range + return false; + } + if (newLeft < left) { + throw new UnsupportedOperationException( + String.format("New left is outside current support: %d < %d", newLeft, left)); + } + // Here [left <= newLeft <= right] + // Move the upstream pivot + lowerPivot = previousPivot(newLeft); + left = newLeft; + return true; + } + + @Override + public boolean contains(int k) { + // Assume [left <= k <= right] + return IndexSet.this.get(k); + } + + @Override + public int previousPivot(int k) { + // Assume scanning in [left <= k <= right] + // Here left is moveable and lower pivot holds the last pivot below it. + // The cache will not store any bits below left so if it has moved + // searching may find stale bits below the current lower pivot. + // So we return the max of the found bit or the lower pivot. + if (k < left) { + return lowerPivot; + } + return Math.max(lowerPivot, IndexSet.this.previousSetBitOrElse(k, lowerPivot)); + } + + @Override + public int nextPivotOrElse(int k, int other) { + // Assume scanning in [left <= k <= right] + final int p = upperPivot == UPPER_DEFAULT ? other : upperPivot; + return IndexSet.this.nextSetBitOrElse(k, p); + } + + @Override + public int nextNonPivot(int k) { + // Assume scanning in [left <= k <= right] + return IndexSet.this.nextClearBit(k); + } + + @Override + public int previousNonPivot(int k) { + // Assume scanning in [left <= k <= right] + return IndexSet.this.previousClearBit(k); + } + + @Override + public void add(int index) { + // Update the floating pivots if outside the support + if (index < left) { + lowerPivot = Math.max(index, lowerPivot); + } else if (index > right) { + upperPivot = Math.min(index, upperPivot); + } else { + IndexSet.this.set(index); + } + } + + @Override + public void add(int fromIndex, int toIndex) { + if (fromIndex == toIndex) { + add(fromIndex); + return; + } + // Note: + // Storing all pivots allows regions of identical values + // and sorted regions to be skipped in subsequent partitioning. + // Repeat sorting these regions is typically more expensive + // than caching them and moving over them during partitioning. + // An alternative is to: store fromIndex and only store + // toIndex if they are well separated, optionally storing + // regions between. If they are not well separated (e.g. < 10) + // then using a single pivot is an alternative to investigate + // with performance benchmarks on a range of input data. + + // Pivots are required to bracket [L, R]: + // LP-----L--------------R------UP + // If the range [i, j] overlaps either L or R then + // the floating pivots are no longer required: + // i-j Set lower pivot + // i--------j Ignore lower pivot + // i---------------------j Ignore lower & upper pivots (no longer required) + // i-------j Ignore lower & upper pivots + // i---------------j Ignore upper pivot + // i-j Set upper pivot + if (fromIndex <= right && toIndex >= left) { + // Clip the range between [left, right] + final int i = Math.max(fromIndex, left); + final int j = Math.min(toIndex, right); + IndexSet.this.set(i, j); + } else if (toIndex < left) { + lowerPivot = Math.max(toIndex, lowerPivot); + } else { + // fromIndex > right + upperPivot = Math.min(fromIndex, upperPivot); + } + } + } + + /** + * Implementation of the {@link UpdatingInterval} using the {@link IndexSet}. + * + *

This class is bound to the enclosing {@link IndexSet} instance to provide + * the functionality to search indexes. + */ + private class IndexSetUpdatingInterval implements UpdatingInterval { + /** Left bound of the interval. */ + private int left; + /** Right bound of the interval. */ + private int right; + + /** + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + IndexSetUpdatingInterval(int left, int right) { + this.left = left; + this.right = right; + } + + @Override + public int left() { + return left; + } + + @Override + public int right() { + return right; + } + + @Override + public int updateLeft(int k) { + // Assume left < k= < right + return left = nextIndex(k); + } + + @Override + public int updateRight(int k) { + // Assume left <= k < right + return right = previousIndex(k); + } + + @Override + public UpdatingInterval splitLeft(int ka, int kb) { + // Assume left < ka <= kb < right + final int lower = left; + left = nextIndex(kb + 1); + return new IndexSetUpdatingInterval(lower, previousIndex(ka - 1)); + } + + @Override + public UpdatingInterval splitRight(int ka, int kb) { + // Assume left < ka <= kb < right + final int upper = right; + right = previousIndex(ka - 1); + return new IndexSetUpdatingInterval(nextIndex(kb + 1), upper); + } + } +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/IndexSortingPerformance.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/IndexSortingPerformance.java new file mode 100644 index 000000000..c7f9330af --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/IndexSortingPerformance.java @@ -0,0 +1,243 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import org.apache.commons.rng.UniformRandomProvider; +import org.apache.commons.rng.simple.RandomSource; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** + * Executes a benchmark of sorting array indices to a unique ascending sequence. + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@State(Scope.Benchmark) +@Fork(value = 1, jvmArgs = {"-server", "-Xms512M", "-Xmx4096M"}) +public class IndexSortingPerformance { + /** Sort using a modified insertion sort that ignores duplicates. */ + private static final String INSERTION = "Insertion"; + /** Sort using a binary search into the unique indices. */ + private static final String BINARY_SEARCH = "BinarySearch"; + /** Sort using a modified heap sort that ignores duplicates. */ + private static final String HEAP = "Heap"; + /** Sort using a full sort and a second pass to ignore duplicates. */ + private static final String SORT_UNIQUE = "SortUnique"; + /** Sort using an {@link IndexSet} to ignore duplicates; + * sorted array extracted from the {@link IndexSet} storage. */ + private static final String INDEX_SET = "IndexSet"; + /** Sort using an {@link HashIndexSet} to ignore duplicates and full sort the unique values. */ + private static final String HASH_INDEX_SET = "HashIndexSet"; + /** Sort using a hybrid method using heuristics to choose the sort. */ + private static final String HYBRID = "Hybrid"; + + /** + * Interface to test sorting unique indices. + */ + interface IndexSort { + /** + * Sort the indices into unique ascending order. + * + * @param a Indices. + * @param n Number of indices. + * @return number of unique indices. + */ + int sort(int[] a, int n); + } + + /** + * Source of {@code int} index array data. + */ + @State(Scope.Benchmark) + public static class IndexDataSource { + /** Number of indices. */ + @Param({ + "10", + "100", + //"1000" + }) + private int n; + /** Range factor (spread of indices). */ + @Param({ + //"1", + "10", + //"100" + }) + private double range; + /** Duplication factor. */ + @Param({ + //"0", + "1", + "2" + }) + private double duplication; + /** Number of samples. */ + @Param({"100"}) + private int samples; + /** True if the indices should be sorted into ascending order. + * This would be the case if multiple quantiles are requested + * using an ascending sequence of p in [0, 1]. */ + @Param({"false"}) + private boolean ascending; + + + /** Data. */ + private int[][] data; + + /** + * @return the data + */ + public int[][] getData() { + return data; + } + + /** + * Create the data. + */ + @Setup(Level.Iteration) + public void setup() { + // Data will be randomized per iteration + final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create(); + + // length of data: index in [0, length) + final int length = (int) Math.floor(n * range); + // extra duplicates + final int extra = (int) Math.floor(n * duplication); + + data = new int[samples][]; + for (int i = 0; i < samples; i++) { + final int[] indices = new int[n + extra]; + // Randomly spread indices in the range (this may create duplicates anyway) + for (int j = 0; j < n; j++) { + indices[j] = rng.nextInt(length); + } + // Sample from the indices to create duplicates. + for (int j = 0; j < extra; j++) { + indices[j + n] = indices[rng.nextInt(n)]; + } + // Ensure the full range is present. Otherwise it is hard to fairly assess + // the performance of the IndexSet when the data is so sparse that + // the min/max is far from the edge of the range and it can use less memory. + // Pick a random place to put the min. + final int i1 = rng.nextInt(indices.length); + // Put the max somewhere else. + final int i2 = (i1 + rng.nextInt(indices.length - 1)) % indices.length; + indices[i1] = 0; + indices[i2] = length - 1; + if (ascending) { + Arrays.sort(indices); + } + data[i] = indices; + } + } + } + + /** + * Source of a {@link IndexSort}. + */ + @State(Scope.Benchmark) + public static class IndexSortSource { + /** Name of the source. */ + @Param({ + // Fast when size is small (<10) + INSERTION, + // Slow (too many System.arraycopy calls) + //BINARY_SEARCH, + // Slow ~ n log(n) + //HEAP, + // Fast sort but does not scale well with duplicates + //SORT_UNIQUE, + // Scale well with duplicates. + // IndexSet has poor high memory requirements when keys are spread out. + // HashIndexSet has predictable memory usage. + INDEX_SET, HASH_INDEX_SET, + // Should pick the best option most of the time + HYBRID}) + private String name; + + /** The sort function. */ + private IndexSort function; + + /** + * @return the function + */ + public IndexSort getFunction() { + return function; + } + + /** + * Create the function. + */ + @Setup + public void setup() { + // Note: Functions defensively copy the data by default + // Note: KeyStratgey does not matter for single / paired keys but + // we set it anyway for completeness. + Objects.requireNonNull(name); + if (INSERTION.equals(name)) { + function = Sorting::sortIndicesInsertionSort; + } else if (BINARY_SEARCH.equals(name)) { + function = Sorting::sortIndicesBinarySearch; + } else if (HEAP.equals(name)) { + function = Sorting::sortIndicesHeapSort; + } else if (SORT_UNIQUE.equals(name)) { + function = Sorting::sortIndicesSort; + } else if (INDEX_SET.equals(name)) { + function = Sorting::sortIndicesIndexSet; + } else if ((INDEX_SET + "2").equals(name)) { + function = Sorting::sortIndicesIndexSet2; + } else if (HASH_INDEX_SET.equals(name)) { + function = Sorting::sortIndicesHashIndexSet; + } else if (HYBRID.equals(name)) { + function = Sorting::sortIndices; + } else { + throw new IllegalStateException("Unknown sort function: " + name); + } + } + } + + /** + * Sort the unique indices. + * + * @param function Source of the function. + * @param source Source of the data. + * @param bh Data sink. + */ + @Benchmark + public void indexSort(IndexSortSource function, IndexDataSource source, Blackhole bh) { + for (final int[] a : source.getData()) { + bh.consume(function.getFunction().sort(a.clone(), a.length)); + } + } +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/IntervalAnalysis.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/IntervalAnalysis.java new file mode 100644 index 000000000..393fbbc1e --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/IntervalAnalysis.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * An interval that provides analysis of indices within the range. + * + * @since 1.2 + */ +interface IntervalAnalysis { + /** + * Test if the interval is saturated at the specified {@code separation}. The + * separation distance is provided as a power of 2. + * + *

{@code distance = 1 << separation}
+ * + *

A saturated interval will have all neighbouring indices separated + * approximately within the maximum separation distance. + * + *

Implementations may: + *

    + *
  1. Use approximations for performance, for example + * compressing indices into blocks of the defined separation. + *
    {@code c = (i - left) >> separation}
    + *
  2. Support only a range of the possible + * {@code separation} values in {@code [0, 30]}. Unsupported {@code separation} + * values should return {@code false}. + *
+ * + * @param separation Log2 of the maximum separation between indices. + * @return true if saturated + */ + boolean saturated(int separation); +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/KeyIndexIterator.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/KeyIndexIterator.java new file mode 100644 index 000000000..bb7eaec48 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/KeyIndexIterator.java @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * An {@link IndexIterator} backed by an array of ordered keys. + * + * @since 1.2 + */ +final class KeyIndexIterator implements IndexIterator { + /** The ordered keys. */ + private final int[] keys; + /** The original number of keys minus 1. */ + private final int nm1; + + /** Iterator left. */ + private int left; + /** Iterator right position. Never advanced beyond {@code n - 1}. */ + private int hi = -1; + + /** + * Create an instance with the provided keys. + * + * @param indices Indices. + * @param n Number of indices. + */ + KeyIndexIterator(int[] indices, int n) { + keys = indices; + this.nm1 = n - 1; + next(); + } + + /** + * Initialise an instance with {@code n} initial {@code indices}. The indices are used in place. + * + * @param indices Indices. + * @param n Number of indices. + * @return the iterator + * @throws IllegalArgumentException if the indices are not unique and ordered; or not + * in the range {@code [0, 2^31-1)}; or {@code n <= 0} + */ + static KeyIndexIterator of(int[] indices, int n) { + // Check the indices are uniquely ordered + if (n <= 0) { + throw new IllegalArgumentException("No indices to define the range"); + } + int p = indices[0]; + for (int i = 0; ++i < n;) { + final int c = indices[i]; + if (c <= p) { + throw new IllegalArgumentException("Indices are not unique and ordered"); + } + p = c; + } + if (indices[0] < 0) { + throw new IllegalArgumentException("Unsupported min value: " + indices[0]); + } + if (indices[n - 1] == Integer.MAX_VALUE) { + throw new IllegalArgumentException("Unsupported max value: " + Integer.MAX_VALUE); + } + return new KeyIndexIterator(indices, n); + } + + @Override + public int left() { + return left; + } + + @Override + public int right() { + return keys[hi]; + } + + @Override + public int end() { + return keys[nm1]; + } + + @Override + public boolean next() { + int i = hi; + if (i < nm1) { + // Blocks [left, right] use a maximum separation of 2 between indices + int k = keys[++i]; + left = k; + while (++i <= nm1 && k + 2 >= keys[i]) { + k = keys[i]; + } + hi = i - 1; + return true; + } + return false; + } + + @Override + public boolean positionAfter(int index) { + int i = hi; + int r = keys[i]; + if (r <= index && i < nm1) { + // fast-forward right + while (++i <= nm1) { + r = keys[i]; + if (r > index) { + // Advance to match the output of next() + while (++i <= nm1 && r + 2 >= keys[i]) { + r = keys[i]; + } + break; + } + } + hi = --i; + // calculate left + // Blocks [left, right] use a maximum separation of 2 between indices + int k = r; + while (--i >= 0 && keys[i] + 2 >= k) { + k = keys[i]; + // indices <= index are not required + if (k <= index) { + left = index + 1; + return r > index; + } + } + left = k; + } + return r > index; + } + + @Override + public boolean nextAfter(int index) { + if (hi < nm1) { + // test if the next left is after the index + return keys[hi + 1] > index; + } + // no more indices + return true; + } +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/KeyUpdatingInterval.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/KeyUpdatingInterval.java new file mode 100644 index 000000000..33620dd3f --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/KeyUpdatingInterval.java @@ -0,0 +1,241 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * An {@link UpdatingInterval} and {@link SplittingInterval} backed by an array of ordered keys. + * + * @since 1.2 + */ +final class KeyUpdatingInterval implements UpdatingInterval, SplittingInterval { + /** Size to use a scan of the keys when splitting instead of binary search. + * Note binary search has an overhead on small size due to the random left/right + * branching per iteration. It is much faster on very large sizes. */ + private static final int SCAN_SIZE = 256; + + /** The ordered keys. */ + private final int[] keys; + /** Index of the left key. */ + private int l; + /** Index of the right key. */ + private int r; + + /** + * Create an instance with the provided {@code indices}. + * Indices must be sorted. + * + * @param indices Indices. + * @param n Number of indices. + */ + KeyUpdatingInterval(int[] indices, int n) { + this(indices, 0, n - 1); + } + + /** + * @param indices Indices. + * @param l Index of left key. + * @param r Index of right key. + */ + private KeyUpdatingInterval(int[] indices, int l, int r) { + keys = indices; + this.l = l; + this.r = r; + } + + /** + * Initialise an instance with the {@code indices}. The indices are used in place. + * + * @param indices Indices. + * @param n Number of indices. + * @return the interval + * @throws IllegalArgumentException if the indices are not unique and ordered; + * or {@code n <= 0} + */ + static KeyUpdatingInterval of(int[] indices, int n) { + // Check the indices are uniquely ordered + if (n <= 0) { + throw new IllegalArgumentException("No indices to define the range"); + } + int p = indices[0]; + for (int i = 0; ++i < n;) { + final int c = indices[i]; + if (c <= p) { + throw new IllegalArgumentException("Indices are not unique and ordered"); + } + p = c; + } + if (indices[0] < 0) { + throw new IllegalArgumentException("Unsupported min value: " + indices[0]); + } + if (indices[n - 1] == Integer.MAX_VALUE) { + throw new IllegalArgumentException("Unsupported max value: " + Integer.MAX_VALUE); + } + return new KeyUpdatingInterval(indices, n); + } + + @Override + public int left() { + return keys[l]; + } + + @Override + public int right() { + return keys[r]; + } + + @Override + public int updateLeft(int k) { + // Assume left < k <= right (i.e. we must move left at least 1) + // Search using a scan on the assumption that k is close to the end + int i = l; + do { + ++i; + } while (keys[i] < k); + l = i; + return keys[i]; + } + + @Override + public int updateRight(int k) { + // Assume left <= k < right (i.e. we must move right at least 1) + // Search using a scan on the assumption that k is close to the end + int i = r; + do { + --i; + } while (keys[i] > k); + r = i; + return keys[i]; + } + + @Override + public UpdatingInterval splitLeft(int ka, int kb) { + // left < ka <= kb < right + + // Find the new left bound for the upper interval. + // Switch to a linear scan if length is small. + int i; + if (r - l < SCAN_SIZE) { + i = r; + do { + --i; + } while (keys[i] > kb); + } else { + // Binary search + i = Partition.searchLessOrEqual(keys, l, r, kb); + } + final int lowerLeft = l; + l = i + 1; + + // Find the new right bound for the lower interval using a scan since a + // typical use case has ka == kb and this is faster than a second binary search. + while (keys[i] >= ka) { + --i; + } + // return left + return new KeyUpdatingInterval(keys, lowerLeft, i); + } + + @Override + public UpdatingInterval splitRight(int ka, int kb) { + // left < ka <= kb < right + + // Find the new left bound for the upper interval. + // Switch to a linear scan if length is small. + int i; + if (r - l < SCAN_SIZE) { + i = r; + do { + --i; + } while (keys[i] > kb); + } else { + // Binary search + i = Partition.searchLessOrEqual(keys, l, r, kb); + } + final int upperLeft = i + 1; + + // Find the new right bound for the lower interval using a scan since a + // typical use case has ka == kb and this is faster than a second binary search. + while (keys[i] >= ka) { + --i; + } + final int upperRight = r; + r = i; + // return right + return new KeyUpdatingInterval(keys, upperLeft, upperRight); + } + + /** + * Return the current number of indices in the interval. + * This is undefined when {@link #empty()}. + * + * @return the size + */ + int size() { + return r - l + 1; + } + + @Override + public boolean empty() { + // Empty when the interval is invalid. Signalled by a negative right index. + return r < 0; + } + + @Override + public SplittingInterval split(int ka, int kb) { + if (ka <= left()) { + // No left interval + if (kb >= right()) { + // No right interval + invalidate(); + } else if (kb >= left()) { + // Update the left bound. + // Search using a scan on the assumption that kb is close to the end + // given that ka is less then the end. + int i = l; + do { + ++i; + } while (keys[i] < kb); + l = i; + } + return null; + } + if (kb >= right()) { + // No right interval. + // Find new right bound for the left-side. + // Search using a scan on the assumption that ka is close to the end + // given that kb is greater then the end. + int i = r; + if (ka <= keys[i]) { + do { + --i; + } while (keys[i] > ka); + } + invalidate(); + return new KeyUpdatingInterval(keys, l, i); + } + // Split + return (SplittingInterval) splitLeft(ka, kb); + } + + /** + * Invalidate the interval and mark as empty. + */ + private void invalidate() { + r = -1; + } +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/KthSelector.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/KthSelector.java new file mode 100644 index 000000000..ef7ab5f59 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/KthSelector.java @@ -0,0 +1,2365 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.util.Arrays; +import java.util.BitSet; + +/** + * A Kth selector implementation to pick up the Kth ordered element + * from a data array containing the input numbers. Uses a partial sort of the data. + * + *

Note: The search may use a cache of pivots. A pivot is a Kth index that + * corresponds to a value correctly positioned within the equivalent fully sorted data array. + * Each step of the algorithm partitions the array between a lower and upper bound into + * values above or below a value chosen from the interval. The index of this value after the + * partition is stored as a pivot. A subsequent search into the array can use the pivots to + * quickly bracket the interval for the next target index. + * + *

The maximum supported cache size is 2^30 - 1. This ensures that any valid index i + * into the cache can be increased for the next level using 2 * i + 2 without overflow. + * + *

The method for choosing the value within the interval to pivot around is specified by + * the {@link PivotingStrategy}. Ideally this should provide a guess of the middle (median) + * of the interval. The value must be within the interval, thus using for example the + * mean of the end values is not an option. The default uses a guess for the median from + * 3 values that are likely to be representative of the range of values. + * + *

This implementation provides the option to return the (K+1) ordered element along with + * the Kth. This uses the position of K and the most recent upper bound on the + * bracket known to contain values greater than the Kth. This prevents using a + * second search into the array for the (K+1) element. + * + * @since 1.2 + */ +class KthSelector { + /** Empty pivots array. */ + static final int[] NO_PIVOTS = {}; + /** Minimum selection size for insertion sort rather than selection. + * Dual-pivot quicksort used 27 in the original paper. */ + private static final int MIN_SELECT_SIZE = 17; + + /** A {@link PivotingStrategy} used for pivoting. */ + private final PivotingStrategy pivotingStrategy; + + /** Minimum selection size for insertion sort rather than selection. */ + private final int minSelectSize; + + /** + * Constructor with default {@link PivotingStrategy#MEDIAN_OF_3 median of 3} pivoting + * strategy. + */ + KthSelector() { + this(PivotingStrategy.MEDIAN_OF_3); + } + + /** + * Constructor with specified pivoting strategy. + * + * @param pivotingStrategy Pivoting strategy to use. + */ + KthSelector(PivotingStrategy pivotingStrategy) { + this(pivotingStrategy, MIN_SELECT_SIZE); + } + + /** + * Constructor with specified pivoting strategy and select size. + * + * @param pivotingStrategy Pivoting strategy to use. + * @param minSelectSize Minimum selection size for insertion sort rather than selection. + */ + KthSelector(PivotingStrategy pivotingStrategy, int minSelectSize) { + this.pivotingStrategy = pivotingStrategy; + this.minSelectSize = minSelectSize; + } + + /** + * Select Kth value in the array. Optionally select the next value after K. + * + *

Note: If K+1 is requested this method assumes it is a valid index into the array. + * + *

Uses a single-pivot partition method. + * + * @param data Data array to use to find out the Kth value. + * @param k Index whose value in the array is of interest. + * @param kp1 K+1th value (if not null) + * @return Kth value + */ + double selectSP(double[] data, int k, double[] kp1) { + int begin = 0; + int end = data.length; + while (end - begin > minSelectSize) { + // Select a pivot and partition data array around it + final int pivot = partitionSP(data, begin, end, + pivotingStrategy.pivotIndex(data, begin, end - 1, k)); + if (k == pivot) { + // The pivot was exactly the element we wanted + return finalSelection(data, k, kp1, end); + } else if (k < pivot) { + // The element is in the left partition + end = pivot; + } else { + // The element is in the right partition + begin = pivot + 1; + } + } + sortRange(data, begin, end); + if (kp1 != null) { + // Either end == data.length and k+1 is sorted; or + // end == pivot where data[k] <= data[pivot] <= data[pivot+j] for all j + kp1[0] = data[k + 1]; + } + return data[k]; + } + + /** + * Select Kth value in the array. Optionally select the next value after K. + * + *

Note: If K+1 is requested this method assumes it is a valid index into the array. + * + *

Uses a single-pivot partition method with special handling of NaN and signed zeros. + * Correction of signed zeros requires a sweep across the entire range. + * + * @param data Data array to use to find out the Kth value. + * @param k Index whose value in the array is of interest. + * @param kp1 K+1th value (if not null) + * @return Kth value + */ + double selectSPN(double[] data, int k, double[] kp1) { + // Handle NaN + final int length = sortNaN(data); + if (k >= length) { + if (kp1 != null) { + kp1[0] = Double.NaN; + } + return Double.NaN; + } + + int begin = 0; + int end = length; + while (end - begin > minSelectSize) { + // Select a pivot and partition data array around it + final int pivot = partitionSPN(data, begin, end, + pivotingStrategy.pivotIndex(data, begin, end - 1, k)); + if (k == pivot) { + // The pivot was exactly the element we wanted + if (data[k] == 0) { + orderSignedZeros(data, 0, length); + } + return finalSelection(data, k, kp1, end); + } else if (k < pivot) { + // The element is in the left partition + end = pivot; + } else { + // The element is in the right partition + begin = pivot + 1; + } + } + insertionSort(data, begin, end, begin != 0); + if (data[k] == 0) { + orderSignedZeros(data, 0, length); + } + if (kp1 != null) { + // Either end == data.length and k+1 is sorted; or + // end == pivot where data[k] <= data[pivot] <= data[pivot+j] for all j + kp1[0] = data[k + 1]; + } + return data[k]; + } + + /** + * Select Kth value in the array. Optionally select the next value after K. + * + *

Note: If K+1 is requested this method assumes it is a valid index into the array. + * + *

Uses a single-pivot partition method with a heap cache. + * + *

This method can be used for repeat calls to identify Kth values in + * the same array by caching locations of pivots. Maximum supported heap size is 2^30 - 1. + * + * @param data Data array to use to find out the Kth value. + * @param pivotsHeap Cached pivots heap that can be used for efficient estimation. + * @param k Index whose value in the array is of interest. + * @param kp1 K+1th value (if not null) + * @return Kth value + */ + double selectSPH(double[] data, int[] pivotsHeap, int k, double[] kp1) { + final int heapLength = pivotsHeap.length; + if (heapLength == 0) { + // No pivots + return selectSP(data, k, kp1); + } + int begin = 0; + int end = data.length; + int node = 0; + while (end - begin > minSelectSize) { + int pivot; + + if (node < heapLength && pivotsHeap[node] >= 0) { + // The pivot has already been found in a previous call + // and the array has already been partitioned around it + pivot = pivotsHeap[node]; + } else { + // Select a pivot and partition data array around it + pivot = partitionSP(data, begin, end, + pivotingStrategy.pivotIndex(data, begin, end - 1, k)); + if (node < heapLength) { + pivotsHeap[node] = pivot; + } + } + + if (k == pivot) { + // The pivot was exactly the element we wanted + return finalSelection(data, k, kp1, end); + } else if (k < pivot) { + // The element is in the left partition + end = pivot; + if (node < heapLength) { + node = Math.min((node << 1) + 1, heapLength); + } + } else { + // The element is in the right partition + begin = pivot + 1; + if (node < heapLength) { + node = Math.min((node << 1) + 2, heapLength); + } + } + } + sortRange(data, begin, end); + if (kp1 != null) { + // Either end == data.length and k+1 is sorted; or + // end == pivot where data[k] <= data[pivot] <= data[pivot+j] for all j + kp1[0] = data[k + 1]; + } + return data[k]; + } + + /** + * Select Kth value in the array. Optionally select the next value after K. + * + *

Note: If K+1 is requested this method assumes it is a valid index into the array + * (i.e. K is not the last index in the array). + * + * @param data Data array to use to find out the Kth value. + * @param k Index whose value in the array is of interest. + * @param kp1 K+1th value (if not null) + * @param end Upper bound (exclusive) of the interval containing K. + * This should be either a pivot point {@code data[k] <= data[end]} or the length + * of the data array. + * @return Kth value + */ + private static double finalSelection(double[] data, int k, double[] kp1, int end) { + if (kp1 != null) { + // After partitioning all elements above k are greater than or equal to k. + // Find the minimum of the elements above. + // Set the k+1 limit as either a pivot or the end of the data. + final int limit = Math.min(end, data.length - 1); + double min = data[k + 1]; + for (int i = k + 2; i <= limit; i++) { + if (DoubleMath.lessThan(data[i], min)) { + min = data[i]; + } + } + kp1[0] = min; + } + return data[k]; + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

Uses a single-pivot partition method. + * + * @param data Values. + * @param k Indices. + */ + void partitionSP(double[] data, int... k) { + final int n = k.length; + if (n <= 1) { + if (n == 1) { + selectSP(data, k[0], null); + } + return; + } + // Multiple pivots + final int length = data.length; + final BitSet pivots = new BitSet(length); + + for (int i = 0; i < n; i++) { + final int kk = k[i]; + if (pivots.get(kk)) { + // Already sorted + continue; + } + int begin; + int end; + if (i == 0) { + begin = 0; + end = length; + } else { + // Start inclusive + begin = pivots.previousSetBit(kk) + 1; + end = pivots.nextSetBit(kk + 1); + if (end < 0) { + end = length; + } + } + partitionSP(data, begin, end, pivots, kk); + } + } + + /** + * Partition around the Kth value in the array. + * + *

This method can be used for repeat calls to identify Kth values in + * the same array by caching locations of pivots (correctly sorted indices). + * + *

Uses a single-pivot partition method. + * + * @param data Data array to use to find out the Kth value. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + * @param pivots Cache of pivot points. + * @param k Index whose value in the array is of interest. + */ + private void partitionSP(double[] data, int begin, int end, BitSet pivots, int k) { + // Find the unsorted range containing k + while (end - begin > minSelectSize) { + // Select a value and partition data array around it + final int pivot = partitionSP(data, begin, end, + pivotingStrategy.pivotIndex(data, begin, end - 1, k)); + pivots.set(pivot); + if (k == pivot) { + // The pivot was exactly the element we wanted + return; + } else if (k < pivot) { + // The element is in the left partition + end = pivot; + } else { + // The element is in the right partition + begin = pivot + 1; + } + } + setPivots(begin, end, pivots); + sortRange(data, begin, end); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Uses a single-pivot partition method. + * + * @param data Data array. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + * @param pivot Initial index of the pivot. + * @return index of the pivot after partition + */ + private static int partitionSP(double[] data, int begin, int end, int pivot) { + final double value = data[pivot]; + data[pivot] = data[begin]; + + int i = begin + 1; + int j = end - 1; + while (i < j) { + while (i < j && DoubleMath.greaterThan(data[j], value)) { + --j; + } + while (i < j && DoubleMath.lessThan(data[i], value)) { + ++i; + } + if (i < j) { + final double tmp = data[i]; + data[i++] = data[j]; + data[j--] = tmp; + } + } + + if (i >= end || DoubleMath.greaterThan(data[i], value)) { + --i; + } + data[begin] = data[i]; + data[i] = value; + return i; + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

Uses a single-pivot partition method. + * + * @param data Values. + * @param k Indices. + */ + void partitionSPN(double[] data, int... k) { + final int n = k.length; + if (n <= 1) { + if (n == 1) { + selectSPN(data, k[0], null); + } + return; + } + // Multiple pivots + + // Handle NaN + final int length = sortNaN(data); + if (length < 1) { + return; + } + + final BitSet pivots = new BitSet(length); + + // Flag any pivots that are zero + boolean zeros = false; + for (int i = 0; i < n; i++) { + final int kk = k[i]; + if (kk >= length || pivots.get(kk)) { + // Already sorted + continue; + } + int begin; + int end; + if (i == 0) { + begin = 0; + end = length; + } else { + // Start inclusive + begin = pivots.previousSetBit(kk) + 1; + end = pivots.nextSetBit(kk + 1); + if (end < 0) { + end = length; + } + } + partitionSPN(data, begin, end, pivots, kk); + zeros = zeros || data[kk] == 0; + } + + // Handle signed zeros + if (zeros) { + orderSignedZeros(data, 0, length); + } + } + + /** + * Partition around the Kth value in the array. + * + *

This method can be used for repeat calls to identify Kth values in + * the same array by caching locations of pivots (correctly sorted indices). + * + *

Note: Requires that the range contains no NaN values. Does not partition + * around signed zeros. + * + *

Uses a single-pivot partition method. + * + * @param data Data array to use to find out the Kth value. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + * @param pivots Cache of pivot points. + * @param k Index whose value in the array is of interest. + */ + private void partitionSPN(double[] data, int begin, int end, BitSet pivots, int k) { + // Find the unsorted range containing k + while (end - begin > minSelectSize) { + // Select a value and partition data array around it + final int pivot = partitionSPN(data, begin, end, + pivotingStrategy.pivotIndex(data, begin, end - 1, k)); + pivots.set(pivot); + if (k == pivot) { + // The pivot was exactly the element we wanted + return; + } else if (k < pivot) { + // The element is in the left partition + end = pivot; + } else { + // The element is in the right partition + begin = pivot + 1; + } + } + setPivots(begin, end, pivots); + sortRange(data, begin, end); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Note: Requires that the range contains no NaN values. Does not partition + * around signed zeros. + * + *

Uses a single-pivot partition method. + * + * @param data Data array. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + * @param pivot Initial index of the pivot. + * @return index of the pivot after partition + */ + private static int partitionSPN(double[] data, int begin, int end, int pivot) { + final double value = data[pivot]; + data[pivot] = data[begin]; + + int i = begin + 1; + int j = end - 1; + while (i < j) { + while (i < j && data[j] > value) { + --j; + } + while (i < j && data[i] < value) { + ++i; + } + if (i < j) { + final double tmp = data[i]; + data[i++] = data[j]; + data[j--] = tmp; + } + } + + if (i >= end || data[i] > value) { + --i; + } + data[begin] = data[i]; + data[i] = value; + return i; + } + + /** + * Sort an array range. + * + * @param data Data array. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + */ + private static void sortRange(double[] data, int begin, int end) { + Arrays.sort(data, begin, end); + } + + /** + * Sorts an array using an insertion sort. + * + *

Note: Requires that the range contains no NaN values. It does not respect the + * order of signed zeros. + * + *

This method is fast up to approximately 40 - 80 values. + * + *

The {@code internal} flag indicates that the value at {@code data[begin - 1]} + * is sorted. + * + * @param data Data array. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + * @param internal Internal flag. + */ + private static void insertionSort(double[] data, int begin, int end, boolean internal) { + Sorting.sort(data, begin, end - 1, internal); + } + + /** + * Move NaN values to the end of the array. + * This allows all other values to be compared using {@code <, ==, >} operators (with + * the exception of signed zeros). + * + * @param data Values. + * @return end of non-NaN values + */ + private static int sortNaN(double[] data) { + int end = data.length; + // Avoid unnecessary moves + while (--end > 0) { + if (!Double.isNaN(data[end])) { + break; + } + } + end++; + for (int i = end; i > 0;) { + final double v = data[--i]; + if (Double.isNaN(v)) { + data[i] = data[--end]; + data[end] = v; + } + } + return end; + } + + /** + * Detect and fix the sort order of signed zeros. Assumes the data may have been + * partially ordered around zero. + * + *

Searches for zeros if {@code data[begin] <= 0} and {@code data[end - 1] >= 0}. + * If zeros are discovered in the range then they are assumed to be continuous. + * + * @param data Values. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + */ + private static void fixSignedZeros(double[] data, int begin, int end) { + int j; + if (data[begin] <= 0 && data[end - 1] >= 0) { + int i = begin; + while (data[i] < 0) { + i++; + } + j = end - 1; + while (data[j] > 0) { + j--; + } + sortZero(data, i, j + 1); + } + } + + /** + * Count the number of signed zeros in the range and order them to be correctly + * sorted. This checks all values in the range. It does not assume zeros are continuous. + * + * @param data Values. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + */ + private static void orderSignedZeros(double[] data, int begin, int end) { + int c = countSignedZeros(data, begin, end); + if (c != 0) { + int i = begin - 1; + while (++i < end) { + if (data[i] == 0) { + data[i] = -0.0; + if (--c == 0) { + break; + } + } + } + while (++i < end) { + if (data[i] == 0) { + data[i] = 0.0; + } + } + } + } + + /** + * Count the number of signed zeros (-0.0). + * + * @param data Values. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + * @return the count + */ + static int countSignedZeros(double[] data, int begin, int end) { + // Count negative zeros + int c = 0; + for (int i = begin; i < end; i++) { + if (data[i] == 0 && Double.doubleToRawLongBits(data[i]) < 0) { + c++; + } + } + return c; + } + + /** + * Sort a range of all zero values. + * This orders -0.0 before 0.0. + * + * @param data Values. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + */ + static void sortZero(double[] data, int begin, int end) { + // Count negative zeros + int c = 0; + for (int i = begin; i < end; i++) { + if (Double.doubleToRawLongBits(data[i]) < 0) { + c++; + } + } + // Replace + if (c != 0) { + int i = begin; + while (c-- > 0) { + data[i++] = -0.0; + } + while (i < end) { + data[i++] = 0.0; + } + } + } + + /** + * Sets the pivots. + * + * @param from Start (inclusive) + * @param to End (exclusive) + * @param pivots the pivots + */ + private static void setPivots(int from, int to, BitSet pivots) { + if (from + 1 == to) { + pivots.set(from); + } else { + pivots.set(from, to); + } + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

Uses a Bentley-McIlroy quicksort partition method by Sedgewick. + * + * @param data Values. + * @param k Indices. + */ + void partitionSBM(double[] data, int... k) { + final int n = k.length; + if (n < 1) { + return; + } + + // Handle NaN + final int length = sortNaN(data); + if (length < 1) { + return; + } + + if (n == 1) { + if (k[0] < length) { + partitionSBM(data, 0, length, k[0]); + } + return; + } + // Special case for partition around adjacent indices (for interpolation) + if (n == 2 && k[0] + 1 == k[1]) { + if (k[0] < length) { + final int p = partitionSBM(data, 0, length, k[0]); + if (p > k[1]) { + partitionMin(data, k[1], p); + } + } + return; + } + + // To partition all k requires not moving any pivot k after it has been + // processed. This is supported using two strategies: + // + // 1. Processing k in sorted order: + // (k1, end), (k2, end), (k3, end), ... , k1 <= k2 <= k3 + // This can reorder each region during processing without destroying sorted k. + // + // 2. Processing unique k and visiting array regions only once: + // Pre-process the pivots to make them unique and store the entire sorted + // region between the end pivots (k1, kn) in a BitSet type structure: + // |k1|......|k2|....|p|k3|k4|pppp|......|kn| + // k can be processed in any order, e.g. k3. We use already sorted regions + // |p| to bracket the search for each k, and skip k that are already sorted (k4). + // Worst case storage cost is Order(N / 64). + // The advantage is never visiting any part of the array twice. If the pivots + // saturate the entire range then performance degrades to the speed of + // the sort of the entire array. + + // Multiple pivots + final BitSet pivots = new BitSet(length); + + for (int i = 0; i < n; i++) { + final int kk = k[i]; + if (kk >= length || pivots.get(kk)) { + // Already sorted + continue; + } + int begin; + int end; + if (i == 0) { + begin = 0; + end = length; + } else { + // Start inclusive + begin = pivots.previousSetBit(kk) + 1; + end = pivots.nextSetBit(kk + 1); + if (end < 0) { + end = length; + } + } + partitionSBM(data, begin, end, pivots, kk); + } + } + + /** + * Move the minimum value to the start of the range. + * + *

Note: Requires that the range contains no NaN values. + * Does not respect the ordering of signed zeros. + * + * @param data Data array to use to find out the Kth value. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + */ + static void partitionMin(double[] data, int begin, int end) { + int i = begin; + double min = data[i]; + int j = i; + while (++i < end) { + if (data[i] < min) { + min = data[i]; + j = i; + } + } + //swap(data, begin, j) + data[j] = data[begin]; + data[begin] = min; + } + + /** + * Move the maximum value to the end of the range. + * + *

Note: Requires that the range contains no NaN values. + * Does not respect the ordering of signed zeros. + * + * @param data Data array to use to find out the Kth value. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + */ + static void partitionMax(double[] data, int begin, int end) { + int i = end - 1; + double max = data[i]; + int j = i; + while (--i >= begin) { + if (data[i] > max) { + max = data[i]; + j = i; + } + } + //swap(data, end - 1, j) + data[j] = data[end - 1]; + data[end - 1] = max; + } + + /** + * Partition around the Kth value in the array. + * + *

This method can be used for repeat calls to identify Kth values in + * the same array by caching locations of pivots (correctly sorted indices). + * + *

Note: Requires that the range contains no NaN values. + * + *

Uses a Bentley-McIlroy quicksort partition method by Sedgewick. + * + * @param data Data array to use to find out the Kth value. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + * @param pivots Cache of pivot points. + * @param k Index whose value in the array is of interest. + */ + private void partitionSBM(double[] data, int begin, int end, BitSet pivots, int k) { + // Find the unsorted range containing k + final int[] upper = {0}; + while (end - begin > minSelectSize) { + // Select a value and partition data array around it + final int from = partitionSBM(data, begin, end, + pivotingStrategy.pivotIndex(data, begin, end - 1, k), upper); + final int to = upper[0]; + setPivots(from, to, pivots); + if (k >= to) { + // The element is in the right partition + begin = to; + } else if (k < from) { + // The element is in the left partition + end = from; + } else { + // The range contains the element we wanted + return; + } + } + setPivots(begin, end, pivots); + insertionSort(data, begin, end, begin != 0); + fixSignedZeros(data, begin, end); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Note: Requires that the range contains no NaN values. + * + *

Uses a Bentley-McIlroy quicksort partition method by Sedgewick. + * + * @param data Data array. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + * @param pivot Initial index of the pivot. + * @param upper Upper bound (exclusive) of the pivot range. + * @return Lower bound (inclusive) of the pivot range. + */ + private static int partitionSBM(double[] data, int begin, int end, int pivot, int[] upper) { + // Single-pivot Bentley-McIlroy quicksort handling equal keys (Sedgewick's algorithm). + // + // Partition data using pivot P into less-than, greater-than or equal. + // P is placed at the end to act as a sentinal. + // k traverses the unknown region ??? and values moved if equal (l) or greater (g): + // + // left p i j q right + // | ==P |

P | ==P |P| + // + // At the end P and additional equal values are swapped back to the centre. + // + // |

P | + // + // Adapted from Sedgewick "Quicksort is optimal" + // https://sedgewick.io/wp-content/themes/sedgewick/talks/2002QuicksortIsOptimal.pdf + // + // The algorithm has been changed so that: + // - A pivot point must be provided. + // - An edge case where the search meets in the middle is handled. + // - Equal value data is not swapped to the end. Since the value is fixed then + // only the less than / greater than value must be moved from the end inwards. + // The end is then assumed to be the equal value. This would not work with + // object references. Equivalent swap calls are commented. + // - Added a fast-forward over initial range containing the pivot. + + final int l = begin; + final int r = end - 1; + + int p = l; + int q = r; + + // Use the pivot index to set the upper sentinal value + final double v = data[pivot]; + data[pivot] = data[r]; + data[r] = v; + + // Special case: count signed zeros + int c = 0; + if (v == 0) { + c = countSignedZeros(data, begin, end); + } + + // Fast-forward over equal regions to reduce swaps + while (data[p] == v) { + if (++p == q) { + // Edge-case: constant value + if (c != 0) { + sortZero(data, begin, end); + } + upper[0] = end; + return begin; + } + } + // Cannot overrun as the prior scan using p stopped before the end + while (data[q - 1] == v) { + q--; + } + + int i = p - 1; + int j = q; + + for (;;) { + do { + ++i; + } while (data[i] < v); + while (v < data[--j]) { + // Stop at l (not i) allows scan loops to be independent + if (j == l) { + break; + } + } + if (i >= j) { + // Edge-case if search met on an internal pivot value + // (not at the greater equal region, i.e. i < q). + // Move this to the lower-equal region. + if (i == j && v == data[i]) { + //swap(data, i++, p++) + //data[i++] = data[p++]; + data[i++] = data[p]; + data[p++] = v; + } + break; + } + //swap(data, i, j) + final double vj = data[i]; + final double vi = data[j]; + data[i] = vi; + data[j] = vj; + if (vi == v) { + //swap(data, i, p++) + //data[i] = data[p++]; + data[i] = data[p]; + data[p++] = v; + } + if (vj == v) { + //swap(data, j, --q) + data[j] = data[--q]; + data[q] = v; + } + } + // i is at the end (exclusive) of the less-than region + + // Place pivot value in centre + //swap(data, r, i) + data[r] = data[i]; + data[i] = v; + + // Move equal regions to the centre. + // Set the pivot range [j, i) and move this outward for equal values. + j = i++; + + // less-equal: + // for (int k = l; k < p; k++): + // swap(data, k, --j) + // greater-equal: + // for (int k = r; k-- > q; i++) { + // swap(data, k, i) + + // Move the minimum of less-equal or less-than + int move = Math.min(p - l, j - p); + final int lower = j - (p - l); + for (int k = l; move-- > 0; k++) { + data[k] = data[--j]; + data[j] = v; + } + // Move the minimum of greater-equal or greater-than + move = Math.min(r - q, q - i); + upper[0] = i + (r - q); + for (int k = r; move-- > 0; i++) { + data[--k] = data[i]; + data[i] = v; + } + + // Special case: fixed signed zeros + if (c != 0) { + p = lower; + while (c-- > 0) { + data[p++] = -0.0; + } + while (p < upper[0]) { + data[p++] = 0.0; + } + } + + return lower; + } + + /** + * Partition around the Kth value in the array. + * + *

This method can be used for repeat calls to identify Kth values in + * the same array by caching locations of pivots (correctly sorted indices). + * + *

Note: Requires that the range contains no NaN values. + * + *

Uses a Bentley-McIlroy quicksort partition method by Sedgewick. + * + *

Returns the last known pivot location adjacent to K. + * If {@code p <= k} the range [p, min{k+2, data.length}) is sorted. + * If {@code p > k} then p is a pivot. + * + * @param data Data array to use to find out the Kth value. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + * @param k Index whose value in the array is of interest. + * @return the bound index + */ + private int partitionSBM(double[] data, int begin, int end, int k) { + // Find the unsorted range containing k + final int[] upper = {0}; + while (end - begin > minSelectSize) { + // Select a value and partition data array around it + final int from = partitionSBM(data, begin, end, + pivotingStrategy.pivotIndex(data, begin, end - 1, k), upper); + final int to = upper[0]; + if (k >= to) { + // The element is in the right partition + begin = to; + } else if (k < from) { + // The element is in the left partition + end = from; + } else { + // The range contains the element we wanted + return end; + } + } + insertionSort(data, begin, end, begin != 0); + fixSignedZeros(data, begin, end); + // Either end == data.length and k+1 is sorted; or + // end == pivot and k+1 is sorted + return begin; + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

Uses a Bentley-McIlroy quicksort partition method. + * + * @param data Values. + * @param k Indices. + */ + void partitionBM(double[] data, int... k) { + final int n = k.length; + if (n < 1) { + return; + } + + // Handle NaN + final int length = sortNaN(data); + if (length < 1) { + return; + } + + if (n == 1) { + partitionBM(data, 0, length, k[0]); + return; + } + // Special case for partition around adjacent indices (for interpolation) + if (n == 2 && k[0] + 1 == k[1]) { + final int p = partitionBM(data, 0, length, k[0]); + if (p > k[1]) { + partitionMin(data, k[1], p); + } + return; + } + + // Multiple pivots + final BitSet pivots = new BitSet(length); + + for (int i = 0; i < n; i++) { + final int kk = k[i]; + if (kk >= length || pivots.get(kk)) { + // Already sorted + continue; + } + int begin; + int end; + if (i == 0) { + begin = 0; + end = length; + } else { + // Start inclusive + begin = pivots.previousSetBit(kk) + 1; + end = pivots.nextSetBit(kk + 1); + if (end < 0) { + end = length; + } + } + partitionBM(data, begin, end, pivots, kk); + } + } + + /** + * Partition around the Kth value in the array. + * + *

This method can be used for repeat calls to identify Kth values in + * the same array by caching locations of pivots (correctly sorted indices). + * + *

Note: Requires that the range contains no NaN values. + * + *

Uses a Bentley-McIlroy quicksort partition method. + * + * @param data Data array to use to find out the Kth value. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + * @param pivots Cache of pivot points. + * @param k Index whose value in the array is of interest. + */ + private void partitionBM(double[] data, int begin, int end, BitSet pivots, int k) { + // Find the unsorted range containing k + final int[] upper = {0}; + while (end - begin > minSelectSize) { + // Select a value and partition data array around it + final int from = partitionBM(data, begin, end, + pivotingStrategy.pivotIndex(data, begin, end - 1, k), upper); + final int to = upper[0]; + setPivots(from, to, pivots); + if (k >= to) { + // The element is in the right partition + begin = to; + } else if (k < from) { + // The element is in the left partition + end = from; + } else { + // The range contains the element we wanted + return; + } + } + setPivots(begin, end, pivots); + insertionSort(data, begin, end, begin != 0); + fixSignedZeros(data, begin, end); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Note: Requires that the range contains no NaN values. + * + *

Uses a Bentley-McIlroy quicksort partition method. + * + * @param data Data array. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + * @param pivot Initial index of the pivot. + * @param upper Upper bound (exclusive) of the pivot range. + * @return Lower bound (inclusive) of the pivot range. + */ + private static int partitionBM(double[] data, int begin, int end, int pivot, int[] upper) { + // Partition method handling equal keys, Bentley-McIlroy quicksort. + // + // Adapted from program 7 in Bentley-McIlroy (1993) + // Engineering a sort function + // SOFTWARE—PRACTICE AND EXPERIENCE, VOL.23(11), 1249–1265 + // + // 3-way partition of the data using a pivot value into + // less-than, equal or greater-than. + // + // First partition data into 4 reqions by scanning the unknown region from + // left (i) and right (j) and moving equal values to the ends: + // i-> <-j end + // l p | | q r| + // | equal | less | unknown | greater | equal || + // + // <-j end + // l p i q r| + // | equal | less | greater | equal || + // + // Then the equal values are copied from the ends to the centre: + // | less | equal | greater | + + final int l = begin; + final int r = end - 1; + + int i = l; + int j = r; + int p = l; + int q = r; + + final double v = data[pivot]; + + // Special case: count signed zeros + int c = 0; + if (v == 0) { + c = countSignedZeros(data, begin, end); + } + + for (;;) { + while (i <= j && data[i] <= v) { + if (data[i] == v) { + //swap(data, i, p++) + data[i] = data[p]; + data[p++] = v; + } + i++; + } + while (j >= i && data[j] >= v) { + if (v == data[j]) { + //swap(data, j, q--) + data[j] = data[q]; + data[q--] = v; + } + j--; + } + if (i > j) { + break; + } + swap(data, i++, j--); + } + + // Move equal regions to the centre. + int s = Math.min(p - l, i - p); + for (int k = l; s > 0; k++, s--) { + //swap(data, k, i - s) + data[k] = data[i - s]; + data[i - s] = v; + } + s = Math.min(q - j, r - q); + for (int k = i; s > 0; k++, s--) { + //swap(data, end - s, k) + data[end - s] = data[k]; + data[k] = v; + } + + // Set output range + i = i - p + l; + j = j - q + end; + upper[0] = j; + + // Special case: fixed signed zeros + if (c != 0) { + p = i; + while (c-- > 0) { + data[p++] = -0.0; + } + while (p < j) { + data[p++] = 0.0; + } + } + + return i; + } + + /** + * Partition around the Kth value in the array. + * + *

This method can be used for repeat calls to identify Kth values in + * the same array by caching locations of pivots (correctly sorted indices). + * + *

Note: Requires that the range contains no NaN values. + * + *

Uses a Bentley-McIlroy quicksort partition method. + * + *

Returns the last known pivot location adjacent to K. + * If {@code p <= k} the range [p, min{k+2, data.length}) is sorted. + * If {@code p > k} then p is a pivot. + * + * @param data Data array to use to find out the Kth value. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + * @param k Index whose value in the array is of interest. + * @return the bound index + */ + private int partitionBM(double[] data, int begin, int end, int k) { + // Find the unsorted range containing k + final int[] upper = {0}; + while (end - begin > minSelectSize) { + // Select a value and partition data array around it + final int from = partitionBM(data, begin, end, + pivotingStrategy.pivotIndex(data, begin, end - 1, k), upper); + final int to = upper[0]; + if (k >= to) { + // The element is in the right partition + begin = to; + } else if (k < from) { + // The element is in the left partition + end = from; + } else { + // The range contains the element we wanted + return end; + } + } + insertionSort(data, begin, end, begin != 0); + fixSignedZeros(data, begin, end); + // Either end == data.length and k+1 is sorted; or + // end == pivot and k+1 is sorted + return begin; + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

Uses a dual-pivot quicksort method by Vladimir Yaroslavskiy. + * + * @param data Values. + * @param k Indices. + */ + void partitionDP(double[] data, int... k) { + final int n = k.length; + if (n < 1) { + return; + } + + // Handle NaN + final int length = sortNaN(data); + if (length < 1) { + return; + } + + if (n == 1) { + partitionDP(data, 0, length, (BitSet) null, k[0]); + return; + } + // Special case for partition around adjacent indices (for interpolation) + if (n == 2 && k[0] + 1 == k[1]) { + final int p = partitionDP(data, 0, length, (BitSet) null, k[0]); + if (p > k[1]) { + partitionMin(data, k[1], p); + } + return; + } + + // Multiple pivots + final BitSet pivots = new BitSet(length); + + for (int i = 0; i < n; i++) { + final int kk = k[i]; + if (kk >= length || pivots.get(kk)) { + // Already sorted + continue; + } + int begin; + int end; + if (i == 0) { + begin = 0; + end = length; + } else { + // Start inclusive + begin = pivots.previousSetBit(kk) + 1; + end = pivots.nextSetBit(kk + 1); + if (end < 0) { + end = length; + } + } + partitionDP(data, begin, end, pivots, kk); + } + } + + /** + * Partition around the Kth value in the array. + * + *

This method can be used for repeat calls to identify Kth values in + * the same array by caching locations of pivots (correctly sorted indices). + * + *

Note: Requires that the range contains no NaN values. + * + *

Uses a dual-pivot quicksort method by Vladimir Yaroslavskiy. + * + *

Returns the pivot location adjacent to K to signal if K+1 is sorted. + * If {@code p <= k} the range [p, min{k+2, data.length}) is sorted. + * If {@code p > k} then p is a pivot. + * + * @param data Data array to use to find out the Kth value. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + * @param pivots Cache of pivot points. + * @param k Index whose value in the array is of interest. + * @return the bound index + */ + private int partitionDP(double[] data, int begin, int end, BitSet pivots, int k) { + // Find the unsorted range containing k + final int[] bounds = new int[4]; + int div = 3; + while (end - begin > minSelectSize) { + div = partitionDP(data, begin, end, bounds, div); + final int k0 = bounds[0]; + final int k1 = bounds[1]; + final int k2 = bounds[2]; + final int k3 = bounds[3]; + // sorted in [k0, k1) and (k2, k3] + if (pivots != null) { + setPivots(k0, k1, pivots); + setPivots(k2 + 1, k3 + 1, pivots); + } + if (k > k3) { + // The element is in the right partition + begin = k3 + 1; + } else if (k < k0) { + // The element is in the left partition + end = k0; + } else if (k >= k1 && k <= k2) { + // Internal unsorted region + begin = k1; + end = k2 + 1; + } else { + // The sorted ranges contain the element we wanted. + // Return a pivot (k0; k2+1) to signal if k+1 is sorted. + if (k + 1 < k1) { + return k0; + } + if (k + 1 < k3) { + return k2 + 1; + } + return end; + } + } + if (pivots != null) { + setPivots(begin, end, pivots); + } + insertionSort(data, begin, end, begin != 0); + fixSignedZeros(data, begin, end); + // Either end == data.length and k+1 is sorted; or + // end == pivot and k+1 is sorted + return begin; + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Note: Requires that the range contains no NaN values. If the range contains + * signed zeros and one is chosen as a pivot point the sort order of zeros is correct. + * + *

This method returns 4 points: the lower and upper pivots and bounds for + * the internal range of unsorted values. + *

+ * + *

Bounds are set so {@code [k0, k1)} and {@code (k2, k3]} are fully sorted. + * When the range {@code [k0, k3]} contains fully sorted elements + * the result is set to {@code k1 = k3+1} and {@code k2 = k3}. + * + *

Uses a dual-pivot quicksort method by Vladimir Yaroslavskiy. + * + * @param a Data array. + * @param left Lower bound (inclusive). + * @param end Upper bound (exclusive). + * @param bounds Points [k0, k1, k2, k3]. + * @param div Divisor for the range to pick medians. + * @return div + */ + private int partitionDP(double[] a, int left, int end, int[] bounds, int div) { + // Dual-pivot quicksort method by Vladimir Yaroslavskiy. + // + // Partition data using pivots P1 and P2 into less-than, greater-than or between. + // Pivot values P1 & P2 are placed at the end. If P1 < P2, P2 acts as a sentinal. + // k traverses the unknown region ??? and values moved if less (l) or greater (g): + // + // left l k g right + // |P1| P2 |P2| + // + // At the end pivots are swapped back to behind the l and g pointers. + // + // | P2 | + // + // Adapted from Yaroslavskiy + // http://codeblab.com/wp-content/uploads/2009/09/DualPivotQuicksort.pdf + // + // Modified to allow partial sorting (partitioning): + // - Ignore insertion sort for tiny array (handled by calling code) + // - Ignore recursive calls for a full sort (handled by calling code) + // - Check for equal elements if a pivot is a signed zero + // - Fix signed zeros within the region between pivots + // - Change to fast-forward over initial ascending / descending runs + // - Change to a single-pivot partition method if the pivots are equal + + final int right = end - 1; + final int len = right - left; + + // Find pivots: + + // Original method: Guess medians using 1/3 and 2/3 of range + final int third = len / div; + int m1 = left + third; + int m2 = right - third; + if (m1 <= left) { + m1 = left + 1; + } + if (m2 >= right) { + m2 = right - 1; + } + if (a[m1] < a[m2]) { + swap(a, m1, left); + swap(a, m2, right); + } else { + swap(a, m1, right); + swap(a, m2, left); + } + // pivots + final double pivot1 = a[left]; + final double pivot2 = a[right]; + + // Single pivot sort + if (pivot1 == pivot2) { + final int lower = partitionSBM(a, left, end, m1, bounds); + final int upper = bounds[0]; + // Set dual pivot range + bounds[0] = lower; + bounds[3] = upper - 1; + // Fully sorted internally + bounds[1] = upper; + bounds[2] = upper - 1; + return div; + } + + // Special case: Handle signed zeros + int c = 0; + if (pivot1 == 0 || pivot2 == 0) { + c = countSignedZeros(a, left, end); + } + + // pointers + int less = left + 1; + int great = right - 1; + + // Fast-forward ascending / descending runs to reduce swaps + while (a[less] < pivot1) { + less++; + } + while (a[great] > pivot2) { + great--; + } + + // sorting + SORTING: + for (int k = less; k <= great; k++) { + final double v = a[k]; + if (v < pivot1) { + //swap(a, k, less++) + a[k] = a[less]; + a[less] = v; + less++; + } else if (v > pivot2) { + // Original + //while (k < great && a[great] > pivot2) { + // great--; + //} + while (a[great] > pivot2) { + if (great-- == k) { + // Done + break SORTING; + } + } + // swap(a, k, great--) + // if (a[k] < pivot1) + // swap(a, k, less++) + final double w = a[great]; + a[great] = v; + great--; + // a[k] = w + if (w < pivot1) { + a[k] = a[less]; + a[less] = w; + less++; + } else { + a[k] = w; + } + } + } + // swaps + final int dist = great - less; + // Original paper: If middle partition (dist) is less than 13 + // then increase 'div' by 1. This means that the two outer partitions + // contained most of the data and choosing medians should take + // values closer to the edge. The middle will be sorted by quicksort. + // 13 = 27 / 2 where 27 is the threshold for quicksort. + if (dist < (minSelectSize >>> 1)) { + // TODO: Determine if this is needed? The original paper + // does not comment its purpose. + div++; + } + //swap(a, less - 1, left) + //swap(a, great + 1, right) + a[left] = a[less - 1]; + a[less - 1] = pivot1; + a[right] = a[great + 1]; + a[great + 1] = pivot2; + + // unsorted in [less, great] + + // Set the pivots + bounds[0] = less - 1; + bounds[3] = great + 1; + //partitionDP(a, left, less - 2, div) + //partitionDP(a, great + 2, right, div) + + // equal elements + // Original paper: If middle partition (dist) is bigger + // than (length - 13) then check for equal elements, i.e. + // if the middle was very large there may be many repeated elements. + // 13 = 27 / 2 where 27 is the threshold for quicksort. + // We always do this if the pivots are signed zeros. + if ((dist > len - (minSelectSize >>> 1) || c != 0) && pivot1 != pivot2) { + // Fast-forward to reduce swaps + while (a[less] == pivot1) { + less++; + } + while (a[great] == pivot2) { + great--; + } + // This copies the logic in the sorting loop using == comparisons + EQUAL: + for (int k = less; k <= great; k++) { + final double v = a[k]; + if (v == pivot1) { + //swap(a, k, less++) + a[k] = a[less]; + a[less] = v; + less++; + } else if (v == pivot2) { + while (a[great] == pivot2) { + if (great-- == k) { + // Done + break EQUAL; + } + } + final double w = a[great]; + a[great] = v; + great--; + if (w == pivot1) { + a[k] = a[less]; + a[less] = w; + less++; + } else { + a[k] = w; + } + } + } + } + // unsorted in [less, great] + if (pivot1 < pivot2 && less < great) { + //partitionDP(a, less, great, div) + bounds[1] = less; + bounds[2] = great; + } else { + // Fully sorted + bounds[1] = bounds[3] + 1; + bounds[2] = bounds[3]; + } + + // Fix signed zeros + if (c != 0) { + int i; + if (pivot1 == 0) { + i = bounds[0]; + while (c-- > 0) { + a[i++] = -0.0; + } + while (i < end && a[i] == 0) { + a[i++] = 0.0; + } + } else { + i = bounds[3]; + while (a[i] == 0) { + a[i--] = 0.0; + if (i == left) { + break; + } + } + while (c-- > 0) { + a[++i] = -0.0; + } + } + } + + return div; + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

Uses a dual-pivot quicksort method by Vladimir Yaroslavskiy. + * + * @param data Values. + * @param k Indices. + */ + void partitionDP5(double[] data, int... k) { + final int n = k.length; + if (n < 1) { + return; + } + + // Handle NaN + final int length = sortNaN(data); + if (length < 1) { + return; + } + + if (n == 1) { + partitionDP5(data, 0, length, (BitSet) null, k[0]); + return; + } + // Special case for partition around adjacent indices (for interpolation) + if (n == 2 && k[0] + 1 == k[1]) { + final int p = partitionDP5(data, 0, length, (BitSet) null, k[0]); + if (p > k[1]) { + partitionMin(data, k[1], p); + } + return; + } + + // Multiple pivots + final BitSet pivots = new BitSet(length); + + for (int i = 0; i < n; i++) { + final int kk = k[i]; + if (kk >= length || pivots.get(kk)) { + // Already sorted + continue; + } + int begin; + int end; + if (i == 0) { + begin = 0; + end = length; + } else { + // Start inclusive + begin = pivots.previousSetBit(kk) + 1; + end = pivots.nextSetBit(kk + 1); + if (end < 0) { + end = length; + } + } + partitionDP5(data, begin, end, pivots, kk); + } + } + + /** + * Partition around the Kth value in the array. + * + *

This method can be used for repeat calls to identify Kth values in + * the same array by caching locations of pivots (correctly sorted indices). + * + *

Note: Requires that the range contains no NaN values. + * + *

Uses a dual-pivot quicksort method by Vladimir Yaroslavskiy. + * + *

Returns the pivot location adjacent to K to signal if K+1 is sorted. + * If {@code p <= k} the range [p, min{k+2, data.length}) is sorted. + * If {@code p > k} then p is a pivot. + * + * @param data Data array to use to find out the Kth value. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + * @param pivots Cache of pivot points. + * @param k Index whose value in the array is of interest. + * @return the bound index + */ + private int partitionDP5(double[] data, int begin, int end, BitSet pivots, int k) { + // Find the unsorted range containing k + final int[] bounds = new int[4]; + while (end - begin > minSelectSize) { + partitionDP5(data, begin, end, bounds); + final int k0 = bounds[0]; + final int k1 = bounds[1]; + final int k2 = bounds[2]; + final int k3 = bounds[3]; + // sorted in [k0, k1) and (k2, k3] + if (pivots != null) { + setPivots(k0, k1, pivots); + setPivots(k2 + 1, k3 + 1, pivots); + } + if (k > k3) { + // The element is in the right partition + begin = k3 + 1; + } else if (k < k0) { + // The element is in the left partition + end = k0; + } else if (k >= k1 && k <= k2) { + // Internal unsorted region + begin = k1; + end = k2 + 1; + } else { + // The sorted ranges contain the element we wanted. + // Return a pivot (k0; k2+1) to signal if k+1 is sorted. + if (k + 1 < k1) { + return k0; + } + if (k + 1 < k3) { + return k2 + 1; + } + return end; + } + } + if (pivots != null) { + setPivots(begin, end, pivots); + } + insertionSort(data, begin, end, begin != 0); + fixSignedZeros(data, begin, end); + // Either end == data.length and k+1 is sorted; or + // end == pivot and k+1 is sorted + return begin; + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Note: Requires that the range contains no NaN values. If the range contains + * signed zeros and one is chosen as a pivot point the sort order of zeros is correct. + * + *

This method returns 4 points: the lower and upper pivots and bounds for + * the internal range of unsorted values. + *

+ * + *

Bounds are set so {@code [k0, k1)} and {@code (k2, k3]} are fully sorted. + * When the range {@code [k0, k3]} contains fully sorted elements + * the result is set to {@code k1 = k3+1} and {@code k2 = k3}. + * + *

Uses a dual-pivot quicksort method by Vladimir Yaroslavskiy. + * + * @param a Data array. + * @param left Lower bound (inclusive). + * @param end Upper bound (exclusive). + * @param bounds Points [k0, k1, k2, k3]. + */ + private static void partitionDP5(double[] a, int left, int end, int[] bounds) { + // Dual-pivot quicksort method by Vladimir Yaroslavskiy. + // + // Adapted from: + // + // http://codeblab.com/wp-content/uploads/2009/09/DualPivotQuicksort.pdf + // + // Modified to allow partial sorting (partitioning): + // - Choose a pivot using 5 sorted points from the range. + // - Ignore insertion sort for tiny array (handled by calling code) + // - Ignore recursive calls for a full sort (handled by calling code) + // - Check for equal elements if a pivot is a signed zero + // - Fix signed zeros within the region between pivots + // - Change to fast-forward over initial ascending / descending runs + // - Change to a single-pivot partition method if the pivots are equal + + final int right = end - 1; + final int len = right - left; + + // Find pivots: + + // Original method: Guess medians using 1/3 and 2/3 of range. + // Here we sort 5 points and choose 2 and 4 as the pivots: 1/6, 1/3, 1/2, 2/3, 5/6 + // 1/6 ~ 1/8 + 1/32. Ensure the value is above zero to choose different points! + // This is safe is len >= 4. + final int sixth = 1 + (len >>> 3) + (len >>> 5); + final int p3 = left + (len >>> 1); + final int p2 = p3 - sixth; + final int p1 = p2 - sixth; + final int p4 = p3 + sixth; + final int p5 = p4 + sixth; + Sorting.sort5(a, p1, p2, p3, p4, p5); + + // For testing + //p2 = DualPivotingStrategy.SORT_5.pivotIndex(a, left, end - 1, bounds); + //p4 = bounds[0]; + + final double pivot1 = a[p2]; + final double pivot2 = a[p4]; + + // Add property to control this switch so we can benchmark not using it. + + if (pivot1 == pivot2) { + // pivots == median ! + // Switch to a single pivot sort around the estimated median + final int lower = partitionSBM(a, left, end, p3, bounds); + final int upper = bounds[0]; + // Set dual pivot range + bounds[0] = lower; + bounds[3] = upper - 1; + // No unsorted internal region + bounds[1] = upper; + bounds[2] = upper - 1; + return; + } + + // Special case: Handle signed zeros + int c = 0; + if (pivot1 == 0 || pivot2 == 0) { + c = countSignedZeros(a, left, end); + } + + // Move ends to the pivot locations. + // After sorting the final pivot locations are overwritten. + a[p2] = a[left]; + a[p4] = a[right]; + // It is assumed + //a[left] = pivot1 + //a[right] = pivot2 + + // pointers + int less = left + 1; + int great = right - 1; + + // Fast-forward ascending / descending runs to reduce swaps + while (a[less] < pivot1) { + less++; + } + while (a[great] > pivot2) { + great--; + } + + // sorting + SORTING: + for (int k = less; k <= great; k++) { + final double v = a[k]; + if (v < pivot1) { + //swap(a, k, less++) + a[k] = a[less]; + a[less] = v; + less++; + } else if (v > pivot2) { + // Original + //while (k < great && a[great] > pivot2) { + // great--; + //} + while (a[great] > pivot2) { + if (great-- == k) { + // Done + break SORTING; + } + } + // swap(a, k, great--) + // if (a[k] < pivot1) + // swap(a, k, less++) + final double w = a[great]; + a[great] = v; + great--; + // a[k] = w + if (w < pivot1) { + a[k] = a[less]; + a[less] = w; + less++; + } else { + a[k] = w; + } + } + } + // swaps + //swap(a, less - 1, left) + //swap(a, great + 1, right) + a[left] = a[less - 1]; + a[less - 1] = pivot1; + a[right] = a[great + 1]; + a[great + 1] = pivot2; + + // unsorted in [less, great] + + // Set the pivots + bounds[0] = less - 1; + bounds[3] = great + 1; + //partitionDP5(a, left, less - 2) + //partitionDP5(a, great + 2, right) + + // equal elements + // Original paper: If middle partition (dist) is bigger + // than (length - 13) then check for equal elements, i.e. + // if the middle was very large there may be many repeated elements. + // 13 = 27 / 2 where 27 is the threshold for quicksort. + + // Look for equal elements if the centre is more than 2/3 the length + // We always do this if the pivots are signed zeros. + if ((less < p1 && great > p5 || c != 0) && pivot1 != pivot2) { + + // Fast-forward to reduce swaps + while (a[less] == pivot1) { + less++; + } + while (a[great] == pivot2) { + great--; + } + + // This copies the logic in the sorting loop using == comparisons + EQUAL: + for (int k = less; k <= great; k++) { + final double v = a[k]; + if (v == pivot1) { + //swap(a, k, less++) + a[k] = a[less]; + a[less] = v; + less++; + } else if (v == pivot2) { + while (a[great] == pivot2) { + if (great-- == k) { + // Done + break EQUAL; + } + } + final double w = a[great]; + a[great] = v; + great--; + if (w == pivot1) { + a[k] = a[less]; + a[less] = w; + less++; + } else { + a[k] = w; + } + } + } + } + // unsorted in [less, great] + if (pivot1 < pivot2 && less < great) { + //partitionDP5(a, less, great) + bounds[1] = less; + bounds[2] = great; + } else { + // Fully sorted + bounds[1] = bounds[3] + 1; + bounds[2] = bounds[3]; + } + + // Fix signed zeros + if (c != 0) { + int i; + if (pivot1 == 0) { + i = bounds[0]; + while (c-- > 0) { + a[i++] = -0.0; + } + while (i < end && a[i] == 0) { + a[i++] = 0.0; + } + } else { + i = bounds[3]; + while (a[i] == 0) { + a[i--] = 0.0; + if (i == left) { + break; + } + } + while (c-- > 0) { + a[++i] = -0.0; + } + } + } + } + + /** + * Swaps the two specified elements in the array. + * + * @param array Array. + * @param i First index. + * @param j Second index. + */ + private static void swap(double[] array, int i, int j) { + final double tmp = array[i]; + array[i] = array[j]; + array[j] = tmp; + } + + /** + * Sort the data. + * + *

Uses a single-pivot quicksort partition method. + * + * @param data Values. + */ + void sortSP(double[] data) { + sortSP(data, 0, data.length); + } + + /** + * Sort the data. + * + * @param data Values. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + */ + private void sortSP(double[] data, int begin, int end) { + if (end - begin <= 1) { + return; + } + final int i = partitionSP(data, begin, end, + pivotingStrategy.pivotIndex(data, begin, end - 1, begin)); + sortSP(data, begin, i); + sortSP(data, i + 1, end); + } + + /** + * Sort the data. + * + *

Uses a Bentley-McIlroy quicksort partition method. + * + * @param data Values. + */ + void sortSBM(double[] data) { + sortSBM(data, 0, sortNaN(data)); + } + + /** + * Sort the data. Requires no NaN values in the range. + * + * @param data Values. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + */ + private void sortSBM(double[] data, int begin, int end) { + if (end - begin <= minSelectSize) { + insertionSort(data, begin, end, begin != 0); + if (begin < end) { + fixSignedZeros(data, begin, end); + } + return; + } + final int[] to = {0}; + final int from = partitionSBM(data, begin, end, + pivotingStrategy.pivotIndex(data, begin, end - 1, begin), to); + sortSBM(data, begin, from); + sortSBM(data, to[0], end); + } + + /** + * Sort the data. + * + *

Uses a Bentley-McIlroy quicksort partition method. + * + * @param data Values. + */ + void sortBM(double[] data) { + sortBM(data, 0, sortNaN(data)); + } + + /** + * Sort the data. Requires no NaN values in the range. + * + * @param data Values. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + */ + private void sortBM(double[] data, int begin, int end) { + if (end - begin <= minSelectSize) { + insertionSort(data, begin, end, begin != 0); + if (begin < end) { + fixSignedZeros(data, begin, end); + } + return; + } + final int[] to = {0}; + final int from = partitionBM(data, begin, end, + pivotingStrategy.pivotIndex(data, begin, end - 1, begin), to); + sortBM(data, begin, from); + sortBM(data, to[0], end); + } + + /** + * Sort the data. + * + *

Uses a dual-pivot quicksort method by Vladimir Yaroslavskiy. + * + * @param data Values. + */ + void sortDP(double[] data) { + sortDP(data, 0, sortNaN(data), 3); + } + + /** + * Sort the data. Requires no NaN values in the range. + * + * @param data Values. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + * @param div Divisor for the range to pick medians. + */ + private void sortDP(double[] data, int begin, int end, int div) { + if (end - begin <= minSelectSize) { + insertionSort(data, begin, end, begin != 0); + if (begin < end) { + fixSignedZeros(data, begin, end); + } + return; + } + final int[] bounds = new int[4]; + div = partitionDP(data, begin, end, bounds, div); + final int k0 = bounds[0]; + final int k1 = bounds[1]; + final int k2 = bounds[2]; + final int k3 = bounds[3]; + sortDP(data, begin, k0, div); + sortDP(data, k3 + 1, end, div); + sortDP(data, k1, k2 + 1, div); + } + + /** + * Sort the data. + * + *

Uses a dual-pivot quicksort method by Vladimir Yaroslavskiy optimised + * to choose the pivots using 5 sorted points. + * + * @param data Values. + */ + void sortDP5(double[] data) { + sortDP5(data, 0, sortNaN(data)); + } + + /** + * Sort the data. Requires no NaN values in the range. + * + * @param data Values. + * @param begin Lower bound (inclusive). + * @param end Upper bound (exclusive). + */ + private void sortDP5(double[] data, int begin, int end) { + if (end - begin <= minSelectSize) { + insertionSort(data, begin, end, begin != 0); + if (begin < end) { + fixSignedZeros(data, begin, end); + } + return; + } + final int[] bounds = new int[4]; + partitionDP5(data, begin, end, bounds); + final int k0 = bounds[0]; + final int k1 = bounds[1]; + final int k2 = bounds[2]; + final int k3 = bounds[3]; + sortDP5(data, begin, k0); + sortDP5(data, k3 + 1, end); + sortDP5(data, k1, k2 + 1); + } + + /** + * Creates the pivots heap for a data array of the specified {@code length}. + * If the array is too small to use the pivots heap then an empty array is returned. + * + * @param length Length. + * @return the pivots heap + */ + static int[] createPivotsHeap(int length) { + if (length <= MIN_SELECT_SIZE) { + return NO_PIVOTS; + } + // Size should be x^2 - 1, where x is the layers in the heap. + // Do not create more pivots than the array length. When partitions are small + // the pivots are no longer used so this does not have to contain all indices. + // Default size in Commons Math Percentile class was 1023 (10 layers). + final int n = nextPow2(length >>> 1); + final int[] pivotsHeap = new int[Math.min(n, 1 << 10) - 1]; + Arrays.fill(pivotsHeap, -1); + return pivotsHeap; + } + + /** + * Returns the closest power-of-two number greater than or equal to value. + * + *

Warning: This will return {@link Integer#MIN_VALUE} for any value above + * {@code 1 << 30}. This is the next power of 2 as an unsigned integer. + * + * @param value the value (must be positive) + * @return the closest power-of-two number greater than or equal to value + */ + private static int nextPow2(int value) { + // shift by -x is equal to shift by (32 - x) as only the low 5-bits are used. + return 1 << -Integer.numberOfLeadingZeros(value - 1); + } +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/NaNPolicy.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/NaNPolicy.java new file mode 100644 index 000000000..197374dd7 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/NaNPolicy.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * Defines the policy for {@link Double#NaN NaN} values found in data. + * + * @since 1.2 + */ +public enum NaNPolicy { + /** NaNs are included in the data. */ + INCLUDE, + /** NaNs are excluded from the data. */ + EXCLUDE, + /** NaNs result in an exception. */ + ERROR +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/Partition.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/Partition.java new file mode 100644 index 000000000..2377254b9 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/Partition.java @@ -0,0 +1,10368 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.util.Arrays; +import java.util.Objects; +import java.util.SplittableRandom; +import java.util.function.IntConsumer; +import java.util.function.IntUnaryOperator; +import java.util.function.Supplier; +import org.apache.commons.rng.UniformRandomProvider; +import org.apache.commons.rng.simple.RandomSource; + +/** + * Partition array data. + * + *

Arranges elements such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+ * data[i < k] <= data[k] <= data[k < i]
+ * }
+ * + *

Examples: + * + *

+ * data    [0, 1, 2, 1, 2, 5, 2, 3, 3, 6, 7, 7, 7, 7]
+ *
+ *
+ * k=4   : [0, 1, 2, 1], [2], [5, 2, 3, 3, 6, 7, 7, 7, 7]
+ * k=4,8 : [0, 1, 2, 1], [2], [3, 3, 2], [5], [6, 7, 7, 7, 7]
+ * 
+ * + *

Note: Unless otherwise stated, methods in this class require that the floating-point data + * contains no NaN values; ordering does not respect the order of signed zeros imposed by + * {@link Double#compare(double, double)}. + * + *

References + * + *

Quickselect is introduced in Hoare [1]. This selects an element {@code k} from {@code n} + * using repeat division of the data around a partition element, recursing into the + * partition that contains {@code k}. + * + *

Introselect is introduced in Musser [2]. This detects excess recursion in quickselect + * and reverts to heapselect to achieve an improved worst case bound on selection. + * + *

Use of dual-pivot quickselect is analysed in Wild et al [3] and shown to require + * marginally more comparisons than single-pivot quickselect on a uniformly chosen order + * statistic {@code k} and extremal order statistic (see table 1, page 19). This analysis + * is reflected in the current implementation where dual-pivot quickselect is marginally + * slower when {@code k} is close to the end of the data. However the dual-pivot quickselect + * outperforms single-pivot quickselect when using multiple {@code k}; often significantly + * when {@code k} or {@code n} are large. + * + *

Use of sampling to identify a pivot that places {@code k} in the smaller partition is + * performed in the SELECT algorithm of Floyd and Rivest [4]. The original algorithm partitions + * on a single pivot. This was extended by Kiwiel to partition using two pivots either side + * of {@code k} with high probability [5]. + * + *

Confidence bounds for the number of iterations to reduce a partition length by 2-x + * are provided in Valois [6]. + * + *

A worst-case linear time algorithm PICK is described in Blum et al [7]. This uses the + * median-of-medians as a partition element for selection which ensures a minimum fraction of the + * elements are eliminated per iteration. This was extended to use an asymmetric pivot choice + * with efficient reuse of the medians sample in the QuickselectAdpative algorithm of + * Alexandrescu [8]. + * + *

    + *
  1. + * Hoare (1961) + * Algorithm 65: Find + * Comm. ACM. 4 (7): 321–322 + *
  2. + * Musser (1999) + * Introspective Sorting and Selection Algorithms + * + * Software: Practice and Experience 27, 983-993. + *
  3. + * Wild, Nebel and Mahmoud (2013) + * Analysis of Quickselect under Yaroslavskiy's Dual-Pivoting Algorithm + * arXiv:1306.3819 + *
  4. Floyd and Rivest (1975) + * Algorithm 489: The Algorithm SELECT - for Finding the ith Smallest of n elements. + * Comm. ACM. 18 (3): 173. + *
  5. Kiwiel (2005) + * On Floyd and Rivest's SELECT algorithm. + * + * Theoretical Computer Science 347, 214-238. + *
  6. Valois (2000) + * Introspective sorting and selection revisited + * + * Software: Practice and Experience 30, 617-638. + *
  7. Blum, Floyd, Pratt, Rivest, and Tarjan (1973) + * Time bounds for selection. + * + * Journal of Computer and System Sciences. 7 (4): 448–461. + *
  8. Alexandrescu (2016) + * Fast Deterministic Selection + * arXiv:1606.00484. + *
  9. Quickselect (Wikipedia) + *
  10. Introsort (Wikipedia) + *
  11. Introselect (Wikipedia) + *
  12. Median of medians (Wikipedia) + *
+ * + * @since 1.2 + */ +final class Partition { + // This class contains implementations for use in benchmarking. + + /** Default pivoting strategy. Note: Using the dynamic strategy avoids excess recursion + * on the Bentley and McIlroy test data vs the MEDIAN_OF_3 strategy. */ + static final PivotingStrategy PIVOTING_STRATEGY = PivotingStrategy.DYNAMIC; + /** + * Default pivoting strategy. Choosing from 5 points is unbiased on random data and + * has a lower standard deviation around the thirds than choosing 2 points + * (Yaroslavskiy's original method, see {@link DualPivotingStrategy#MEDIANS}). It + * performs well across various test data. + * + *

There are 3 variants using spacings of approximately 1/6, 1/7, and 1/8 computed + * using shifts to create 0.1719, 0.1406, and 0.125; with middle thirds on large + * lengths of 0.342, 0.28 and 0.25. The spacing using 1/7 is marginally faster when + * performing a full sort than the others; thus favouring a smaller middle third, but + * not too small, appears to be most performant. + */ + static final DualPivotingStrategy DUAL_PIVOTING_STRATEGY = DualPivotingStrategy.SORT_5B; + /** Minimum selection size for quickselect/quicksort. + * Below this switch to sortselect/insertion sort rather than selection. + * Dual-pivot quicksort used 27 in Yaroslavskiy's original paper. + * Changes to this value are only noticeable when the input array is small. + * + *

This is a legacy setting from when insertion sort was used as the stopper. + * This has been replaced by edge selection functions. Using insertion sort + * is slower as n must be sorted compared to an edge select that only sorts + * up to n/2 from the edge. It is disabled by default but can be used for + * benchmarking. + * + *

If using insertion sort as the stopper for quickselect: + *

*/ + static final int MIN_QUICKSELECT_SIZE = 0; + /** Minimum size for heapselect. + * Below this switch to insertion sort rather than selection. This is used to avoid + * heap select on tiny data. */ + static final int MIN_HEAPSELECT_SIZE = 5; + /** Minimum size for sortselect. + * Below this switch to insertion sort rather than selection. This is used to avoid + * sort select on tiny data. */ + static final int MIN_SORTSELECT_SIZE = 4; + /** Default selection constant for edgeselect. Any k closer than this to the left/right + * bound will be selected using the configured edge selection function. */ + static final int EDGESELECT_CONSTANT = 20; + /** Default sort selection constant for linearselect. Note that linear select variants + * recursively call quickselect so very small lengths are included with an initial + * medium length. Using lengths of 1023-5 and 2043-53 indicate optimum performance around + * 80 for median-of-medians when placing the sample on the left. Adaptive linear methods + * are faster and so this value is reduced. Quickselect adaptive has a value around 20-30. + * Note: When using {@link ExpandStrategy#T2} the input length must create a sample of at + * least length 2 as each end of the sample is used as a sentinel. With a sample length of + * 1/12 of the data this requires edge select of at least 12. */ + static final int LINEAR_SORTSELECT_SIZE = 24; + /** Default sub-sampling size to identify a single pivot. Sub-sampling is performed if the + * length is above this value thus using MAX_VALUE sets it off by default. + * The SELECT algorithm of Floyd-Rivest uses 600. */ + static final int SUBSAMPLING_SIZE = Integer.MAX_VALUE; + /** Default key strategy. */ + static final KeyStrategy KEY_STRATEGY = KeyStrategy.INDEX_SET; + /** Default 1 or 2 key strategy. */ + static final PairedKeyStrategy PAIRED_KEY_STRATEGY = PairedKeyStrategy.SEARCHABLE_INTERVAL; + /** Default recursion multiple. */ + static final int RECURSION_MULTIPLE = 2; + /** Default recursion constant. */ + static final int RECURSION_CONSTANT = 0; + /** Default compression. */ + static final int COMPRESSION_LEVEL = 1; + /** Default control flags. */ + static final int CONTROL_FLAGS = 0; + /** Default option flags. */ + static final int OPTION_FLAGS = 0; + /** Default single-pivot partition strategy. */ + static final SPStrategy SP_STRATEGY = SPStrategy.KBM; + /** Default expand partition strategy. A ternary method is faster on equal elements and no + * slower on unique elements. */ + static final ExpandStrategy EXPAND_STRATEGY = ExpandStrategy.T2; + /** Default single-pivot linear select strategy. */ + static final LinearStrategy LINEAR_STRATEGY = LinearStrategy.RSA; + /** Default edge select strategy. */ + static final EdgeSelectStrategy EDGE_STRATEGY = EdgeSelectStrategy.ESS; + /** Default single-pivot stopper strategy. */ + static final StopperStrategy STOPPER_STRATEGY = StopperStrategy.SQA; + /** Default quickselect adaptive mode. */ + static final AdaptMode ADAPT_MODE = AdaptMode.ADAPT3; + + /** Sampling mode using Floyd-Rivest sampling. */ + static final int MODE_FR_SAMPLING = -1; + /** Sampling mode. */ + static final int MODE_SAMPLING = 0; + /** No sampling but use adaption of the target k. */ + static final int MODE_ADAPTION = 1; + /** No sampling and no adaption of target k (strict margins). */ + static final int MODE_STRICT = 2; + + // Floyd-Rivest flags + + /** Control flag for random sampling. */ + static final int FLAG_RANDOM_SAMPLING = 0x2; + /** Control flag for vector swap of the sample. */ + static final int FLAG_MOVE_SAMPLE = 0x4; + /** Control flag for random subset sampling. This creates the sample at the end + * of the data and requires moving regions to reposition around the target k. */ + static final int FLAG_SUBSET_SAMPLING = 0x8; + + // RNG flags + + /** Control flag for biased nextInt(n) RNG. */ + static final int FLAG_BIASED_RANDOM = 0x1000; + /** Control flag for SplittableRandom RNG. */ + static final int FLAG_SPLITTABLE_RANDOM = 0x2000; + /** Control flag for MSWS RNG. */ + static final int FLAG_MSWS = 0x4000; + + // Quickselect adaptive flags. Must not clash with the Floyd-Rivest/RNG flags + // that are supported for sample mode. + + /** Control flag for quickselect adaptive to propagate the no sampling mode recursively. */ + static final int FLAG_QA_PROPAGATE = 0x1; + /** Control flag for quickselect adaptive variant of Floyd-Rivest random sampling. */ + static final int FLAG_QA_RANDOM_SAMPLING = 0x4; + /** Control flag for quickselect adaptive to use a different far left/right step + * using min of 4; then median of 3 into the 2nd 12th-tile. The default (original) uses + * lower median of 4; then min of 3 into 4th 12th-tile). The default has a larger + * upper margin of 3/8 vs 1/3 for the new method. The new method is better + * with the original k mapping for far left/right and similar speed to the original + * far left/right step using the new k mapping. When sampling is off it is marginally + * faster, may be due to improved layout of the sample closer to the strict 1/12 lower margin. + * There is no compelling evidence to indicate is it better so the default uses + * the original far step method. */ + static final int FLAG_QA_FAR_STEP = 0x8; + /** Control flag for quickselect adaptive to map k using the same k mapping for all + * repeated steps. This enables the original algorithm behaviour. + * + *

Note that the original mapping can create a lower margin that + * does not contain k. This makes it possible to put k into the larger partition. + * For the middle and step left methods this heuristic is acceptable as the bias in + * margins is shifted but the smaller margin is at least 1/12 of the data and a choice + * of this side is not a severe penalty. For the far step left the original mapping + * will always create a smaller margin that does not contain k. Removing this + * adaptive k and using the median of the 12th-tile shows a measurable speed-up + * as the smaller margin always contains k. This result has been extended to change + * the mapping for the far step to ensure the smaller + * margin always contains at least k elements. This is faster and so enabled by default. */ + static final int FLAG_QA_FAR_STEP_ADAPT_ORIGINAL = 0x10; + /** Use a 12th-tile for the sampling mode in the middle repeated step method. + * The default uses a 9th-tile which is a larger sample than the 12th-tile used in + * the step left/far left methods. */ + static final int FLAG_QA_MIDDLE_12 = 0x20; + /** Position the sample for quickselect adaptive to place the mapped k' at the target index k. + * This is not possible for the far step methods as it can generated a bounds error as + * k approaches the edge. */ + static final int FLAG_QA_SAMPLE_K = 0x40; + + /** Threshold to use sub-sampling of the range to identify the single pivot. + * Sub-sampling uses the Floyd-Rivest algorithm to partition a sample of the data to + * identify a pivot so that the target element is in the smaller set after partitioning. + * The original FR paper used 600 otherwise reverted to the target index as the pivot. + * This implementation uses a sample to identify a median pivot which increases robustness + * at small size on a variety of data and allows raising the original FR threshold. + * At 600, FR has no speed-up; at double this the speed-up can be measured. */ + static final int SELECT_SUB_SAMPLING_SIZE = 1200; + + /** Message for an unsupported introselect configuration. */ + private static final String UNSUPPORTED_INTROSELECT = "Unsupported introselect: "; + + /** Transformer factory for double data with the behaviour of a JDK sort. + * Moves NaN to the end of the data and handles signed zeros. Works on the data in-place. */ + private static final Supplier SORT_TRANSFORMER = + DoubleDataTransformers.createFactory(NaNPolicy.INCLUDE, false); + + /** Minimum length between 2 pivots {@code p2 - p1} that requires a full sort. */ + private static final int SORT_BETWEEN_SIZE = 2; + /** log2(e). Used for conversions: log2(x) = ln(x) * log2(e) */ + private static final double LOG2_E = 1.4426950408889634; + + /** Threshold to use repeated step left: 7 / 16. */ + private static final double STEP_LEFT = 0.4375; + /** Threshold to use repeated step right: 9 / 16. */ + private static final double STEP_RIGHT = 0.5625; + /** Threshold to use repeated step far-left: 1 / 12. */ + private static final double STEP_FAR_LEFT = 0.08333333333333333; + /** Threshold to use repeated step far-right: 11 / 12. */ + private static final double STEP_FAR_RIGHT = 0.9166666666666666; + + /** Default quickselect adaptive mode. Start with FR sampling. */ + private static int qaMode = MODE_FR_SAMPLING; + /** Default quickselect adaptive mode increment. */ + private static int qaIncrement = 1; + + // Use final for settings/objects used within partitioning functions + + /** A {@link PivotingStrategy} used for pivoting. */ + private final PivotingStrategy pivotingStrategy; + /** A {@link DualPivotingStrategy} used for pivoting. */ + private final DualPivotingStrategy dualPivotingStrategy; + + /** Minimum size for quickselect when partitioning multiple keys. + * Below this threshold partitioning using quickselect is stopped and a sort selection + * is performed. + * + *

This threshold is also used in the sort methods to switch to insertion sort; + * and in legacy partition methods which do not use edge selection. These may perform + * key analysis using this value to determine saturation. */ + private final int minQuickSelectSize; + /** Constant for edgeselect. */ + private final int edgeSelectConstant; + /** Size for sortselect in the linearselect function. Optimal value for this is higher + * than for regular quickselect as the median-of-medians pivot strategy is expensive. + * For convenience (limit overrides for the constructor) this is not final. */ + private int linearSortSelectSize = LINEAR_SORTSELECT_SIZE; + /** Threshold to use sub-sampling of the range to identify the single pivot. + * Sub-sampling uses the Floyd-Rivest algorithm to partition a sample of the data. This + * identifies a pivot so that the target element is in the smaller set after partitioning. + * The algorithm applies to searching for a single k. + * Not all single-pivot {@link PairedKeyStrategy} methods support sub-sampling. It is + * available to test in {@link #introselect(SPEPartition, double[], int, int, int, int)}. + * + *

Sub-sampling can provide up to a 2-fold performance gain on large random data. + * It can have a 2-fold slowdown on some structured data (e.g. large shuffle data from + * the Bentley and McIlroy test data). Large shuffle data also observes a larger performance + * drop when using the SBM/BM/DNF partition methods (collect equal values) verses a + * simple SP method ignoring equal values. Here large ~500,000; the behaviour + * is observed at smaller sizes and becomes increasingly obvious at larger sizes. + * + *

The algorithm relies on partitioning of a subset to be representative of partitioning + * of the entire data. Values in a small range partitioned around a pivot P + * should create P in a similar location to its position in the entire fully sorted array, + * ideally closer to the middle so ensuring elimination of the larger side. + * E.g. ordering around P in [ll, rr] will be similar to P's order in [l, r]: + *

+     * target:                       k
+     * subset:                  ll---P-------rr
+     * sorted: l----------------------P-------------------------------------------r
+     *                                Good pivot
+     * 
+ * + *

If the data in [ll, rr] is not representative then pivot selection based on a + * subset creates bad pivot choices and performance is worse than using a + * {@link PivotingStrategy}. + *

+     * target:                       k
+     * subset:                 ll----P-------rr
+     * sorted: l------------------------------------------P----------------------r
+     *                                                    Bad pivot
+     * 
+ * + *

Use of the Floyd-Rivest subset sampling is not always an improvement and is data + * dependent. The type of data cannot be known by the partition algorithm before processing. + * Thus the Floyd-Rivest subset sampling is more suitable as an option to be enabled by + * user settings. + * + *

A random sub-sample can mitigate issues with non-representative data. This can + * be done by sampling with/without replacement into a new array; or shuffling in-place + * to part of the array. This implementation supports the later option. + * + *

See + * Floyd-Rivest Algorithm (Wikipedia). + * + *

+     * Floyd and Rivest (1975)
+     * Algorithm 489: The Algorithm SELECT—for Finding the ith Smallest of n elements.
+     * Comm. ACM. 18 (3): 173.
+     * 
*/ + private final int subSamplingSize; + + // Use non-final members for settings used to configure partitioning functions + + /** Setting to indicate strategy for processing of multiple keys. */ + private KeyStrategy keyStrategy = KEY_STRATEGY; + /** Setting to indicate strategy for processing of 1 or 2 keys. */ + private PairedKeyStrategy pairedKeyStrategy = PAIRED_KEY_STRATEGY; + + /** Multiplication factor {@code m} applied to the length based recursion factor {@code x}. + * The recursion is set using {@code m * x + c}. + *

Also used for the multiple of the original length to check the sum of the partition length + * for poor quickselect partitions. + *

Also used for the number of iterations before checking the partition length has been + * reduced by a given factor of 2 (in iselect). */ + private double recursionMultiple = RECURSION_MULTIPLE; + /** Constant {@code c} added to the length based recursion factor {@code x}. + * The recursion is set using {@code m * x + c}. + *

Also used to specify the factor of two to reduce the partition length after a set + * number of iterations (in iselect). */ + private int recursionConstant = RECURSION_CONSTANT; + /** Compression level for a {@link CompressedIndexSet} (in [1, 31]). */ + private int compression = COMPRESSION_LEVEL; + /** Control flags level for Floyd-Rivest sub-sampling. */ + private int controlFlags = CONTROL_FLAGS; + /** Consumer for the recursion level reached during partitioning. Used to analyse + * the distribution of the recursion for different input data. */ + private IntConsumer recursionConsumer = i -> { /* no-op */ }; + + /** The single-pivot partition function. */ + private SPEPartition spFunction; + /** The expand partition function. */ + private ExpandPartition expandFunction; + /** The single-pivot linear partition function. */ + private SPEPartition linearSpFunction; + /** Selection function used when {@code k} is close to the edge of the range. */ + private SelectFunction edgeSelection; + /** Selection function used when quickselect progress is poor. */ + private SelectFunction stopperSelection; + /** Quickselect adaptive mode. */ + private AdaptMode adaptMode = ADAPT_MODE; + + /** Quickselect adaptive mapping function applied when sampling-mode is on. */ + private MapDistance samplingAdapt; + /** Quickselect adaptive mapping function applied when sampling-mode is on for + * distances close to the edge (i.e. the far-step functions). */ + private MapDistance samplingEdgeAdapt; + /** Quickselect adaptive mapping function applied when sampling-mode is off. */ + private MapDistance noSamplingAdapt; + /** Quickselect adaptive mapping function applied when sampling-mode is off for + * distances close to the edge (i.e. the far-step functions). */ + private MapDistance noSamplingEdgeAdapt; + + /** + * Define the strategy for processing multiple keys. + */ + enum KeyStrategy { + /** Sort unique keys, collate ranges and process in ascending order. */ + SEQUENTIAL, + /** Process in input order using an {@link IndexSet} to cover the entire range. + * Introselect implementations will use a {@link SearchableInterval}. */ + INDEX_SET, + /** Process in input order using a {@link CompressedIndexSet} to cover the entire range. + * Introselect implementations will use a {@link SearchableInterval}. */ + COMPRESSED_INDEX_SET, + /** Process in input order using a {@link PivotCache} to cover the minimum range. */ + PIVOT_CACHE, + /** Sort unique keys and process using recursion with division of the keys + * for each sub-partition. */ + ORDERED_KEYS, + /** Sort unique keys and process using recursion with a {@link ScanningKeyInterval}. */ + SCANNING_KEY_SEARCHABLE_INTERVAL, + /** Sort unique keys and process using recursion with a {@link BinarySearchKeyInterval}. */ + SEARCH_KEY_SEARCHABLE_INTERVAL, + /** Sort unique keys and process using recursion with a {@link KeyIndexIterator}. */ + INDEX_ITERATOR, + /** Process in input order using an {@link IndexIterator} of a {@link CompressedIndexSet}. */ + COMPRESSED_INDEX_ITERATOR, + /** Process using recursion with an {@link IndexSet}-based {@link UpdatingInterval}. */ + INDEX_SET_UPDATING_INTERVAL, + /** Sort unique keys and process using recursion with an {@link UpdatingInterval}. */ + KEY_UPDATING_INTERVAL, + /** Process using recursion with an {@link IndexSet}-based {@link SplittingInterval}. */ + INDEX_SET_SPLITTING_INTERVAL, + /** Sort unique keys and process using recursion with a {@link SplittingInterval}. */ + KEY_SPLITTING_INTERVAL; + } + + /** + * Define the strategy for processing 1 key or 2 keys: (k, k+1). + */ + enum PairedKeyStrategy { + /** Use a dedicated single key method that returns information about (k+1). + * Use recursion depth to trigger the stopper select. */ + PAIRED_KEYS, + /** Use a dedicated single key method that returns information about (k+1). + * Recursion is monitored by checking the partition is reduced by 2-x after + * {@code c} iterations where {@code x} is the + * {@link #setRecursionConstant(int) recursion constant} and {@code c} is the + * {@link #setRecursionMultiple(double) recursion multiple} */ + PAIRED_KEYS_2, + /** Use a dedicated single key method that returns information about (k+1). + * Use a multiple of the sum of the length of all partitions to trigger the stopper select. */ + PAIRED_KEYS_LEN, + /** Use a method that accepts two separate keys. The keys do not define a range + * and are independent. */ + TWO_KEYS, + /** Use a method that accepts two keys to define a range. + * Recursion is monitored by checking the partition is reduced by 2-x after + * {@code c} iterations where {@code x} is the + * {@link #setRecursionConstant(int) recursion constant} and {@code c} is the + * {@link #setRecursionMultiple(double) recursion multiple} */ + KEY_RANGE, + /** Use an {@link SearchableInterval} covering the keys. This will reuse a multi-key + * strategy with keys that are a very small range. */ + SEARCHABLE_INTERVAL, + /** Use an {@link UpdatingInterval} covering the keys. This will reuse a multi-key + * strategy with keys that are a very small range. */ + UPDATING_INTERVAL; + } + + /** + * Define the strategy for single-pivot partitioning. Partitioning may be binary + * ({@code <, >}), or ternary ({@code <, ==, >}) by collecting values equal to the + * pivot value. Typically partitioning will use two pointers i and j to traverse the + * sequence from either end; or a single pointer i for a single pass. + * + *

Binary partitioning will be faster for quickselect when no equal elements are + * present. As duplicates become increasingly likely a ternary partition will be + * faster for quickselect to avoid repeat processing of values (that matched the + * previous pivot) on the next iteration. The type of ternary partition with the best + * performance depends on the number of duplicates. In the extreme case of 1 or 2 + * unique elements it is more likely to match the {@code ==, !=} comparison to the + * pivot than {@code <, >} (see {@link #DNF3}). An ideal ternary scheme should have + * little impact on data with no repeats, and significantly improve performance as the + * number of repeat elements increases. + * + *

Binary partitioning will skip over values already {@code <, >}, or + * {@code <=, =>} to the pivot value; otherwise values at the pointers i and j are + * swapped. If using {@code <, >} then values can be placed at either end of the + * sequence that are {@code >=, <=} respectively to act as sentinels during the scan. + * This is always possible in binary partitioning as the pivot can be one sentinel; + * any other value will be either {@code <=, =>} to the pivot and so can be used at + * one or the other end as appropriate. Note: Many schemes omit using sentinels. Modern + * processor branch prediction nullifies the cost of checking indices remain within + * the {@code [left, right]} bounds. However placing sentinels is a negligible cost + * and at least simplifies the code for the region traversal. + * + *

Bentley-McIlroy ternary partitioning schemes move equal values to the ends + * during the traversal, these are moved to the centre after the pass. This may use + * minimal swaps based on region sizes. Note that values already {@code <, >} are not + * moved during traversal allowing moves to be minimised. + * + *

Dutch National Flag schemes move non-equal values to either end and finish with + * the equal value region in the middle. This requires that every element is moved + * during traversal, even if already {@code <, >}. This can be mitigated by fast-forward + * of pointers at the current {@code <, >} end points until the condition is not true. + * + * @see SPEPartition + */ + enum SPStrategy { + /** + * Single-pivot partitioning. Uses a method adapted from Floyd and Rivest (1975) + * which uses sentinels to avoid bounds checks on the i and j pointers. + * This is a baseline for the maximum speed when no equal elements are present. + */ + SP, + /** + * Bentley-McIlroy ternary partitioning. Requires bounds checks on the i and j + * pointers during traversal. Comparisons to the pivot use {@code <=, =>} and a + * second check for {@code ==} if the first is true. + */ + BM, + /** + * Sedgewick's Bentley-McIlroy ternary partitioning. Requires bounds checks on the + * j pointer during traversal. Comparisons to the pivot use {@code <, >} and a + * second check for {@code ==} when both i and j have stopped. + */ + SBM, + /** + * Kiwiel's Bentley-McIlroy ternary partitioning. Similar to Sedgewick's BM but + * avoids bounds checks on both pointers during traversal using sentinels. + * Comparisons to the pivot use {@code <, >} and a second check for {@code ==} + * when both i and j have stopped. Handles i and j meeting at the pivot without a + * swap. + */ + KBM, + /** + * Dutch National Flag partitioning. Single pointer iteration using {@code <, >} + * comparisons to move elements to the edges. Fast-forwards any initial {@code <} + * region. The {@code ==} region is filled with the pivot after region traversal. + */ + DNF1, + /** + * Dutch National Flag partitioning. Single pointer iteration using {@code <, >} + * comparisons to move elements to the edges. Fast-forwards any initial {@code <} + * region. The {@code >} region uses fast-forward to reduce swaps. The {@code ==} + * region is filled with the pivot after region traversal. + */ + DNF2, + /** + * Dutch National Flag partitioning. Single pointer iteration using {@code !=} + * comparison to identify elements to move to the edges, then {@code <, >} + * comparisons. Fast-forwards any initial {@code <} region. The {@code >} region + * uses fast-forward to reduce swaps. The {@code ==} region is filled during + * traversal. + */ + DNF3; + } + + /** + * Define the strategy for expanding a partition. This function is used when + * partitioning has used a sample located within the range to find the pivot. + * The remaining range below and above the sample can be partitioned without + * re-processing the sample. + * + *

Schemes may be binary ({@code <, >}), or ternary ({@code <, ==, >}) by + * collecting values equal to the pivot value. Schemes may process the + * unpartitioned range below and above the partitioned middle using a sweep + * outwards towards the ends; or start at the ends and sweep inwards towards + * the partitioned middle. + * + * @see ExpandPartition + */ + enum ExpandStrategy { + /** Use the current {@link SPStrategy} partition method. This will not expand + * the partition but will Partition the Entire Range (PER). This can be used + * to test if the implementations of expand are efficient. */ + PER, + /** Ternary partition method 1. Sweeps outwards and uses sentinels at the ends + * to avoid pointer range checks. Equal values are moved directly into the + * central pivot range. */ + T1, + /** Ternary partition method 2. Similar to {@link #T1} with different method + * to set the sentinels. */ + T2, + /** Binary partition method 1. Sweeps outwards and uses sentinels at the ends + * to avoid pointer range checks. */ + B1, + /** Binary partition method 2. Similar to {@link #B1} with different method + * to set the sentinels. */ + B2, + } + + /** + * Define the strategy for the linear select single-pivot partition function. Linear + * select functions use a deterministic sample to find a pivot value that will + * eliminate at least a set fraction of the range (fixed borders/margins). After the + * sample has been processed to find a pivot the entire range is partitioned. This can + * be done by re-processing the entire range, or expanding the partition. + * + *

Adaption (selecting a non-central index in the median-of-medians sample) creates + * asymmetric borders; in practice the larger border is typically eliminated per + * iteration which improves performance. + * + * @see SPStrategy + * @see ExpandStrategy + * @see SPEPartition + * @see ExpandPartition + */ + enum LinearStrategy { + /** Uses the Blum, Floyd, Pratt, Rivest, and Tarjan (BFPRT) median-of-medians algorithm + * with medians of 5. This is the baseline version that creates the median sample + * at the left end and repartitions the entire range using the pivot. + * Fixed borders of 3/10. */ + BFPRT, + /** Uses the Chen and Dumitrescu repeated step median-of-medians-of-medians algorithm + * with medians of 3. This is the baseline version that creates the median sample + * at the left end and repartitions the entire range using the pivot. + * Fixed borders of 2/9. */ + RS, + /** Uses the Blum, Floyd, Pratt, Rivest, and Tarjan (BFPRT) median-of-medians algorithm + * with medians of 5. This is the improved version that creates the median sample + * in the centre and expands the partition around the pivot sample. + * Fixed borders of 3/10. */ + BFPRT_IM, + /** Uses the Chen and Dumitrescu repeated step median-of-medians-of-medians algorithm + * with medians of 3. This is the improved version that creates the median sample + * in the centre and expands the partition around the pivot sample. + * Fixed borders of 2/9. */ + RS_IM, + /** Uses the Blum, Floyd, Pratt, Rivest, and Tarjan (BFPRT) median-of-medians algorithm + * with medians of 5. This is the improved version that creates the median sample + * in the centre and expands the partition around the pivot sample; the adaption + * is to use k to define the pivot in the sample instead of using the median. + * This will not have fixed borders. */ + BFPRTA, + /** Uses the Chen and Dumitrescu repeated step median-of-medians-of-medians algorithm + * with medians of 3. This is the adaptive version that creates the median sample + * in the centre and expands the partition around the pivot sample; the adaption + * is to use k to define the pivot in the sample instead of using the median. + * This will not have fixed borders. */ + RSA; + } + + /** + * Define the strategy for selecting {@code k} close to the edge. + *

These are named to allow regex identification for dynamic configuration + * in benchmarking using the name; this uses the E (Edge) prefix. + */ + enum EdgeSelectStrategy { + /** Use heapselect version 1. Selects {@code k} and an additional + * {@code c} elements closer to the edge than {@code k} using a heap + * structure. */ + ESH, + /** Use heapselect version 2. Differs from {@link #ESH} in the + * final unwinding of the heap to sort the range {@code [ka, kb]}; + * the heap construction is identical. */ + ESH2, + /** Use sortselect version 1. Uses an insertion sort to maintain {@code k} + * and all elements closer to the edge as sorted. */ + ESS, + /** Use sortselect version 2. Differs from {@link #ESS} by a using pointer + * into the sorted range to improve insertion speed. In practice the more + * complex code is not more performant. */ + ESS2; + } + + /** + * Define the strategy for selecting {@code k} when quickselect progress is poor + * (worst case is quadratic). This should be a method providing good worst-case + * performance. + *

These are named to allow regex identification for dynamic configuration + * in benchmarking using the name; this uses the S (Stopper) prefix. + */ + enum StopperStrategy { + /** Use heapselect version 1. Selects {@code k} and an additional + * {@code c} elements closer to the edge than {@code k}. Heapselect + * provides increasingly slower performance with distance from the edge. + * It has better worst-case performance than quickselect. */ + SSH, + /** Use heapselect version 2. Differs from {@link #SSH} in the + * final unwinding of the heap to sort the range {@code [ka, kb]}; + * the heap construction is identical. */ + SSH2, + /** Use a linear selection algorithm with Order(n) worst-case performance. + * This is a median-of-medians using medians of size 5. This is the base + * implementation using a median sample into the first 20% of the data + * and not the improved version (with sample in the centre). */ + SLS, + /** Use the quickselect adaptive algorithm with Order(n) worst-case performance. */ + SQA; + } + + /** + * Partition function. Used to benchmark different implementations. + * + *

Note: The function is applied within a {@code [left, right]} bound. This bound + * is set using the entire range of the data to process, or it may be a sub-range + * due to previous partitioning. In this case the value at {@code left - 1} and/or + * {@code right + 1} can be a pivot. The value at these pivot points will be {@code <=} or + * {@code >=} respectively to all values within the range. This information is valuable + * during recursive partitioning and is passed as flags to the partition method. + */ + private interface PartitionFunction { + + /** + * Partition (partially sort) the array in the range {@code [left, right]} around + * a central region {@code [ka, kb]}. The central region should be entirely + * sorted. + * + *

{@code
+         * data[i < ka] <= data[ka] <= data[kb] <= data[kb < i]
+         * }
+ * + * @param a Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param ka Lower bound (inclusive) of the central region. + * @param kb Upper bound (inclusive) of the central region. + * @param leftInner Flag to indicate {@code left - 1} is a pivot. + * @param rightInner Flag to indicate {@code right + 1} is a pivot. + */ + void partition(double[] a, int left, int right, int ka, int kb, + boolean leftInner, boolean rightInner); + + /** + * Partition (partially sort) the array in the range {@code [left, right]} around + * a central region {@code [ka, kb]}. The central region should be entirely + * sorted. + * + *
{@code
+         * data[i < ka] <= data[ka] <= data[kb] <= data[kb < i]
+         * }
+ * + *

The {@link PivotStore} is only required to record pivots after {@code kb}. + * This is to support sequential ascending order processing of regions to partition. + * + * @param a Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param ka Lower bound (inclusive) of the central region. + * @param kb Upper bound (inclusive) of the central region. + * @param leftInner Flag to indicate {@code left - 1} is a pivot. + * @param rightInner Flag to indicate {@code right + 1} is a pivot. + * @param pivots Used to store sorted regions. + */ + void partitionSequential(double[] a, int left, int right, int ka, int kb, + boolean leftInner, boolean rightInner, PivotStore pivots); + + /** + * Partition (partially sort) the array in the range {@code [left, right]} around + * a central region {@code [ka, kb]}. The central region should be entirely + * sorted. + * + *

{@code
+         * data[i < ka] <= data[ka] <= data[kb] <= data[kb < i]
+         * }
+ * + *

The {@link PivotStore} records all pivots and sorted regions. + * + * @param a Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param ka Lower bound (inclusive) of the central region. + * @param kb Upper bound (inclusive) of the central region. + * @param leftInner Flag to indicate {@code left - 1} is a pivot. + * @param rightInner Flag to indicate {@code right + 1} is a pivot. + * @param pivots Used to store sorted regions. + */ + void partition(double[] a, int left, int right, int ka, int kb, + boolean leftInner, boolean rightInner, PivotStore pivots); + + /** + * Sort the array in the range {@code [left, right]}. + * + * @param a Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param leftInner Flag to indicate {@code left - 1} is a pivot. + * @param rightInner Flag to indicate {@code right + 1} is a pivot. + */ + void sort(double[] a, int left, int right, boolean leftInner, boolean rightInner); + } + + /** + * Single-pivot partition method handling equal values. + */ + @FunctionalInterface + private interface SPEPartitionFunction extends PartitionFunction { + /** + * Partition an array slice around a single pivot. Partitioning exchanges array + * elements such that all elements smaller than pivot are before it and all + * elements larger than pivot are after it. + * + *

This method returns 2 points describing the pivot range of equal values. + *

{@code
+         *                     |k0 k1|
+         * |         

P | + * }

+ * + * + * @param a Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param upper Upper bound (inclusive) of the pivot range [k1]. + * @param leftInner Flag to indicate {@code left - 1} is a pivot. + * @param rightInner Flag to indicate {@code right + 1} is a pivot. + * @return Lower bound (inclusive) of the pivot range [k0]. + */ + int partition(double[] a, int left, int right, int[] upper, + boolean leftInner, boolean rightInner); + + // Add support to have a pivot cache. Assume it is to store pivots after kb. + // Switch to not using it when right < kb, or doing a full sort between + // left and right (pivots are irrelevant). + + @Override + default void partition(double[] a, int left, int right, int ka, int kb, + boolean leftInner, boolean rightInner) { + // Skip when [left, right] does not overlap [ka, kb] + if (right - left < 1) { + return; + } + // Assume: left <= right && ka <= kb + // Ranges may overlap either way: + // left ---------------------- right + // ka --- kb + // + // Requires full sort: + // ka ------------------------- kb + // left ---- right + // + // This will naturally perform a full sort when ka < left and kb > right + + // Edge case for a single point + if (ka == right) { + selectMax(a, left, ka); + } else if (kb == left) { + selectMin(a, kb, right); + } else { + final int[] upper = {0}; + final int k0 = partition(a, left, right, upper, leftInner, rightInner); + final int k1 = upper[0]; + // Sorted in [k0, k1] + // Unsorted in [left, k0) and (k1, right] + if (ka < k0) { + partition(a, left, k0 - 1, ka, kb, leftInner, true); + } + if (kb > k1) { + partition(a, k1 + 1, right, ka, kb, true, rightInner); + } + } + } + + @Override + default void partitionSequential(double[] a, int left, int right, int ka, int kb, + boolean leftInner, boolean rightInner, PivotStore pivots) { + // This method is a copy of the above method except: + // - It records all sorted ranges to the cache + // - It switches to the above method when the cache is not required + if (right - left < 1) { + return; + } + if (ka == right) { + selectMax(a, left, ka); + pivots.add(ka); + } else if (kb == left) { + selectMin(a, kb, right); + pivots.add(kb); + } else { + final int[] upper = {0}; + final int k0 = partition(a, left, right, upper, leftInner, rightInner); + final int k1 = upper[0]; + // Sorted in [k0, k1] + // Unsorted in [left, k0) and (k1, right] + pivots.add(k0, k1); + + if (ka < k0) { + if (k0 - 1 < kb) { + // Left branch entirely below kb - no cache required + partition(a, left, k0 - 1, ka, kb, leftInner, true); + } else { + partitionSequential(a, left, k0 - 1, ka, kb, leftInner, true, pivots); + } + } + if (kb > k1) { + partitionSequential(a, k1 + 1, right, ka, kb, true, rightInner, pivots); + } + } + } + + @Override + default void partition(double[] a, int left, int right, int ka, int kb, + boolean leftInner, boolean rightInner, PivotStore pivots) { + // This method is a copy of the above method except: + // - It records all sorted ranges to the cache + // - It switches to the above method when the cache is not required + if (right - left < 1) { + return; + } + if (ka == right) { + selectMax(a, left, ka); + pivots.add(ka); + } else if (kb == left) { + selectMin(a, kb, right); + pivots.add(kb); + } else { + final int[] upper = {0}; + final int k0 = partition(a, left, right, upper, leftInner, rightInner); + final int k1 = upper[0]; + // Sorted in [k0, k1] + // Unsorted in [left, k0) and (k1, right] + pivots.add(k0, k1); + + if (ka < k0) { + partition(a, left, k0 - 1, ka, kb, leftInner, true, pivots); + } + if (kb > k1) { + partition(a, k1 + 1, right, ka, kb, true, rightInner, pivots); + } + } + } + + @Override + default void sort(double[] a, int left, int right, boolean leftInner, boolean rightInner) { + // Skip when [left, right] is sorted + // Note: This has no insertion sort for small lengths (so is less performant). + // It can be used to test the partition algorithm across the entire data. + if (right - left < 1) { + return; + } + final int[] upper = {0}; + final int k0 = partition(a, left, right, upper, leftInner, rightInner); + final int k1 = upper[0]; + // Sorted in [k0, k1] + // Unsorted in [left, k0) and (k1, right] + sort(a, left, k0 - 1, leftInner, true); + sort(a, k1 + 1, right, true, rightInner); + } + } + + /** + * Single-pivot partition method handling equal values. + */ + @FunctionalInterface + interface SPEPartition { + /** + * Partition an array slice around a single pivot. Partitioning exchanges array + * elements such that all elements smaller than pivot are before it and all + * elements larger than pivot are after it. + * + *

This method returns 2 points describing the pivot range of equal values. + *

{@code
+         *                     |k0 k1|
+         * |         

P | + * }

+ * + * + * @param a Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param upper Upper bound (inclusive) of the pivot range [k1]. + * @param pivot Pivot location. + * @return Lower bound (inclusive) of the pivot range [k0]. + */ + int partition(double[] a, int left, int right, int pivot, int[] upper); + } + + /** + * Dual-pivot partition method handling equal values. + */ + @FunctionalInterface + interface DPPartition { + /** + * Partition an array slice around two pivots. Partitioning exchanges array + * elements such that all elements smaller than pivot are before it and all + * elements larger than pivot are after it. + * + *

This method returns 4 points describing the pivot ranges of equal values. + *

{@code
+         *         |k0  k1|                |k2  k3|
+         * |   

P | + * }

+ * + * + *

Bounds are set so {@code i < k0}, {@code i > k3} and {@code k1 < i < k2} are + * unsorted. When the range {@code [k0, k3]} contains fully sorted elements the result + * is set to {@code k1 = k3; k2 == k0}. This can occur if + * {@code P1 == P2} or there are zero or 1 value between the pivots + * {@code P1 < v < P2}. Any sort/partition of ranges [left, k0-1], [k1+1, k2-1] and + * [k3+1, right] must check the length is {@code > 1}. + * + * @param a Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param bounds Points [k1, k2, k3]. + * @param pivot1 Pivot1 location. + * @param pivot2 Pivot2 location. + * @return Lower bound (inclusive) of the pivot range [k0]. + */ + int partition(double[] a, int left, int right, int pivot1, int pivot2, int[] bounds); + } + + /** + * Select function. + * + *

Used to define the function to call when {@code k} is close + * to the edge; or when quickselect progress is poor. This allows + * the edge-select or stopper-function to be configured using parameters. + */ + @FunctionalInterface + interface SelectFunction { + /** + * Partition the elements between {@code ka} and {@code kb}. + * It is assumed {@code left <= ka <= kb <= right}. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param ka Lower index to select. + * @param kb Upper index to select. + */ + void partition(double[] a, int left, int right, int ka, int kb); + } + + /** + * Single-pivot partition method handling a pre-partitioned range in the centre. + */ + @FunctionalInterface + interface ExpandPartition { + /** + * Expand a partition around a single pivot. Partitioning exchanges array + * elements such that all elements smaller than pivot are before it and all + * elements larger than pivot are after it. The central region is already + * partitioned. + * + *

{@code
+         * |l             |s   |p0 p1|   e|                r|
+         * |    ???       | 

P | ??? | + * }

+ * + *

This method returns 2 points describing the pivot range of equal values. + *

{@code
+         * |l                  |k0 k1|                     r|
+         * |         

P | + * }

+ * + * + * @param a Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param start Start of the partition range (inclusive). + * @param end End of the partitioned range (inclusive). + * @param pivot0 Lower pivot location (inclusive). + * @param pivot1 Upper pivot location (inclusive). + * @param upper Upper bound (inclusive) of the pivot range [k1]. + * @return Lower bound (inclusive) of the pivot range [k0]. + */ + int partition(double[] a, int left, int right, int start, int end, + int pivot0, int pivot1, int[] upper); + } + + /** + * Function to map the distance from the edge of a range {@code [l, r]} to a smaller + * range. The mapping is used in the quickselect adaptive method to adapt {@code k} + * based on the position in the sample: {@code kf'/|A|}; k is the index to partition; + * |A| is the size of the data to partition; f' is the size of the sample. + */ + enum MapDistance { + /** Use the median of the new range. */ + MEDIAN { + @Override + int mapDistance(int d, int l, int r, int n) { + return n >>> 1; + } + }, + /** Map the distance using a fraction of the original range: {@code d / (r - l)}. */ + ADAPT { + @Override + int mapDistance(int d, int l, int r, int n) { + // If distance==r-l this returns n-1 + return (int) (d * (n - 1.0) / (r - l)); + } + }, + /** Use the midpoint between the adaption computed by the {@link #MEDIAN} and {@link #ADAPT} methods. */ + HALF_ADAPT { + @Override + int mapDistance(int d, int l, int r, int n) { + // Half-adaption: compute the full adaption + final int x = ADAPT.mapDistance(d, l, r, n); + // Compute the median between the x and the middle + final int m = n >>> 1; + return (m + x) >>> 1; + } + }, + /** + * Map the distance assuming the distance to the edge is small. This method is + * used when the sample has a lower margin (minimum number of elements) in the + * original range of {@code 2(d+1)}. That is each element in the sample has at + * least 1 element below it in the original range. This occurs for example when + * the sample is built using a median-of-3. In this case setting the mapped + * distance as {@code d/2} will ensure that the lower margin in the original data + * is at least {@code d} and consequently {@code d} is inside the lower margin. It + * will generate a bounds error if called with {@code d > 2(r - l)}. + */ + EDGE_ADAPT { + @Override + int mapDistance(int d, int l, int r, int n) { + return d >>> 1; + } + }; + + /** + * Map the distance from the edge of {@code [l, r]} to a new distance in {@code [0, n)}. + * + *

For convenience this accepts the input range {@code [l, r]} instead of the length + * of the original range. The implementation may use the range or ignore it and only + * use the new range size {@code n}. + * + * @param d Distance from the edge in {@code [0, r - l]}. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param n Size of the new range. + * @return the mapped distance in [0, n) + */ + abstract int mapDistance(int d, int l, int r, int n); + } + + /** + * Encapsulate the state of adaption in the quickselect adaptive algorithm. + * + *

To ensure linear runtime performance a fixed size of data must be eliminated at each + * step. This requires a median-of-median-of-medians pivot sample generated from all the data + * with the target {@code k} mapped to the middle of the sample so that the margins of the + * possible partitions are a minimum size. + * Note that random selection of a pivot will achieve the same margins with some probability + * and is less expensive to compute; runtime performance may be better or worse due to average + * quality of the pivot. The adaption in quickselect adaptive is two-fold: + *

+ * + *

The quickselect adaptive paper suggests sampling mode is turned off when margins are not + * achieved. That is the size after partitioning is not as small as expected. + * However there is no detail on whether to turn off adaption, and the margins that are + * targeted. This provides the following possible state transitions: + *

{@code
+     * 1: sampling + adaption    --> no-sampling + adaption
+     * 2: sampling + adaption    --> no-sampling + no-adaption
+     * 3: sampling + adaption    --> no-sampling + adaption     --> no-sampling + no-adaption
+     * 4: sampling + no-adaption --> no-sampling + no-adaption
+     * }
+ * + *

The behaviour is captured in this enum as a state-machine. The finite state + * is dependent on the start state. The transition from one state to the next may require a + * count of failures to achieve; this is not captured in this state machine. + * + *

Note that use of no-adaption when sampling (case 4) is unlikely to work unless the sample + * median is representative of the location of the pivot sample. This is true for + * median-of-median-of-medians but not the offset pivot samples used in quickselect adaptive; + * this is supported for completeness and can be used to demonstrate its inefficiency. + */ + enum AdaptMode { + /** No sampling and no adaption (fixed margins) for worst-case linear runtime performance. + * This is a terminal state. */ + FIXED { + @Override + boolean isSampleMode() { + return false; + } + @Override + boolean isAdapt() { + return false; + } + @Override + AdaptMode update(int size, int l, int r) { + // No further states + return this; + } + }, + /** Sampling and adaption. Failure to achieve the expected partition size + * will revert to no sampling but retain adaption. */ + ADAPT1 { + @Override + boolean isSampleMode() { + return true; + } + @Override + boolean isAdapt() { + return true; + } + @Override + AdaptMode update(int size, int l, int r) { + return r - l <= size ? this : ADAPT1B; + } + }, + /** No sampling and use adaption. This is a terminal state. */ + ADAPT1B { + @Override + boolean isSampleMode() { + return false; + } + @Override + boolean isAdapt() { + return true; + } + @Override + AdaptMode update(int size, int l, int r) { + // No further states + return this; + } + }, + /** Sampling and adaption. Failure to achieve the expected partition size + * will revert to no sampling and no adaption. */ + ADAPT2 { + @Override + boolean isSampleMode() { + return true; + } + @Override + boolean isAdapt() { + return true; + } + @Override + AdaptMode update(int size, int l, int r) { + return r - l <= size ? this : FIXED; + } + }, + /** Sampling and adaption. Failure to achieve the expected partition size + * will revert to no sampling but retain adaption. */ + ADAPT3 { + @Override + boolean isSampleMode() { + return true; + } + @Override + boolean isAdapt() { + return true; + } + @Override + AdaptMode update(int size, int l, int r) { + return r - l <= size ? this : ADAPT3B; + } + }, + /** No sampling and use adaption. Failure to achieve the expected partition size + * will disable adaption (revert to fixed margins). */ + ADAPT3B { + @Override + boolean isSampleMode() { + return false; + } + @Override + boolean isAdapt() { + return true; + } + @Override + AdaptMode update(int size, int l, int r) { + return r - l <= size ? this : FIXED; + } + }, + /** Sampling and no adaption. Failure to achieve the expected partition size + * will disabled sampling (revert to fixed margins). */ + ADAPT4 { + @Override + boolean isSampleMode() { + return true; + } + @Override + boolean isAdapt() { + return false; + } + @Override + AdaptMode update(int size, int l, int r) { + return r - l <= size ? this : FIXED; + } + }; + + /** + * Checks if sample-mode is enabled. + * + * @return true if sample mode is enabled + */ + abstract boolean isSampleMode(); + + /** + * Checks if adaption is enabled. + * + * @return true if adaption is enabled + */ + abstract boolean isAdapt(); + + /** + * Update the state using the expected {@code size} of the partition and the actual size. + * + *

For convenience this accepts the range {@code [l, r]} instead of the actual size. + * The implementation may use the range or ignore it. + * + * @param size Expected size of the partition. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @return the new state + */ + abstract AdaptMode update(int size, int l, int r); + } + + /** + * Constructor with defaults. + */ + Partition() { + this(PIVOTING_STRATEGY, DUAL_PIVOTING_STRATEGY, MIN_QUICKSELECT_SIZE, + EDGESELECT_CONSTANT, SUBSAMPLING_SIZE); + } + + /** + * Constructor with specified pivoting strategy and quickselect size. + * + *

Used to test single-pivot quicksort. + * + * @param pivotingStrategy Pivoting strategy to use. + * @param minQuickSelectSize Minimum size for quickselect. + */ + Partition(PivotingStrategy pivotingStrategy, int minQuickSelectSize) { + this(pivotingStrategy, DUAL_PIVOTING_STRATEGY, minQuickSelectSize, + EDGESELECT_CONSTANT, SUBSAMPLING_SIZE); + } + + /** + * Constructor with specified pivoting strategy and quickselect size. + * + *

Used to test dual-pivot quicksort. + * + * @param dualPivotingStrategy Dual pivoting strategy to use. + * @param minQuickSelectSize Minimum size for quickselect. + */ + Partition(DualPivotingStrategy dualPivotingStrategy, int minQuickSelectSize) { + this(PIVOTING_STRATEGY, dualPivotingStrategy, minQuickSelectSize, + EDGESELECT_CONSTANT, SUBSAMPLING_SIZE); + } + + /** + * Constructor with specified pivoting strategy; quickselect size; and edgeselect configuration. + * + *

Used to test single-pivot quickselect. + * + * @param pivotingStrategy Pivoting strategy to use. + * @param minQuickSelectSize Minimum size for quickselect. + * @param edgeSelectConstant Length constant used for edge select distance from end threshold. + * @param subSamplingSize Size threshold to use sub-sampling for single-pivot selection. + */ + Partition(PivotingStrategy pivotingStrategy, + int minQuickSelectSize, int edgeSelectConstant, int subSamplingSize) { + this(pivotingStrategy, DUAL_PIVOTING_STRATEGY, minQuickSelectSize, edgeSelectConstant, + subSamplingSize); + } + + /** + * Constructor with specified dual-pivoting strategy; quickselect size; and edgeselect configuration. + * + *

Used to test dual-pivot quickselect. + * + * @param dualPivotingStrategy Dual pivoting strategy to use. + * @param minQuickSelectSize Minimum size for quickselect. + * @param edgeSelectConstant Length constant used for edge select distance from end threshold. + */ + Partition(DualPivotingStrategy dualPivotingStrategy, + int minQuickSelectSize, int edgeSelectConstant) { + this(PIVOTING_STRATEGY, dualPivotingStrategy, minQuickSelectSize, + edgeSelectConstant, SUBSAMPLING_SIZE); + } + + /** + * Constructor with specified pivoting strategy; quickselect size; and edgeselect configuration. + * + * @param pivotingStrategy Pivoting strategy to use. + * @param dualPivotingStrategy Dual pivoting strategy to use. + * @param minQuickSelectSize Minimum size for quickselect. + * @param edgeSelectConstant Length constant used for distance from end threshold. + * @param subSamplingSize Size threshold to use sub-sampling for single-pivot selection. + */ + Partition(PivotingStrategy pivotingStrategy, DualPivotingStrategy dualPivotingStrategy, + int minQuickSelectSize, int edgeSelectConstant, int subSamplingSize) { + this.pivotingStrategy = pivotingStrategy; + this.dualPivotingStrategy = dualPivotingStrategy; + this.minQuickSelectSize = minQuickSelectSize; + this.edgeSelectConstant = edgeSelectConstant; + this.subSamplingSize = subSamplingSize; + // Default strategies + setSPStrategy(SP_STRATEGY); + setEdgeSelectStrategy(EDGE_STRATEGY); + setStopperStrategy(STOPPER_STRATEGY); + setExpandStrategy(EXPAND_STRATEGY); + setLinearStrategy(LINEAR_STRATEGY); + // Called to initialise state + setControlFlags(0); + } + + /** + * Sets the single-pivot partition strategy. + * + * @param v Value. + * @return {@code this} for chaining + */ + Partition setSPStrategy(SPStrategy v) { + switch (v) { + case BM: + spFunction = Partition::partitionBM; + break; + case DNF1: + spFunction = Partition::partitionDNF1; + break; + case DNF2: + spFunction = Partition::partitionDNF2; + break; + case DNF3: + spFunction = Partition::partitionDNF3; + break; + case KBM: + spFunction = Partition::partitionKBM; + break; + case SBM: + spFunction = Partition::partitionSBM; + break; + case SP: + spFunction = Partition::partitionSP; + break; + default: + throw new IllegalArgumentException("Unknown single-pivot strategy: " + v); + } + return this; + } + + /** + * Sets the single-pivot partition expansion strategy. + * + * @param v Value. + * @return {@code this} for chaining + */ + Partition setExpandStrategy(ExpandStrategy v) { + switch (v) { + case PER: + // Partition the entire range using the single-pivot partition strategy + expandFunction = (a, left, right, start, end, pivot0, pivot1, upper) -> + spFunction.partition(a, left, right, (pivot0 + pivot1) >>> 1, upper); + break; + case T1: + expandFunction = Partition::expandPartitionT1; + break; + case B1: + expandFunction = Partition::expandPartitionB1; + break; + case T2: + expandFunction = Partition::expandPartitionT2; + break; + case B2: + expandFunction = Partition::expandPartitionB2; + break; + default: + throw new IllegalArgumentException("Unknown expand strategy: " + v); + } + return this; + } + + /** + * Sets the single-pivot linear select strategy. + * + *

Note: The linear select strategy will partition remaining range after computing + * a pivot from a sample by single-pivot partitioning or by expanding the partition + * (see {@link #setExpandStrategy(ExpandStrategy)}). + * + * @param v Value. + * @return {@code this} for chaining + * @see #setExpandStrategy(ExpandStrategy) + */ + Partition setLinearStrategy(LinearStrategy v) { + switch (v) { + case BFPRT: + linearSpFunction = this::linearBFPRTBaseline; + break; + case RS: + linearSpFunction = this::linearRepeatedStepBaseline; + break; + case BFPRT_IM: + noSamplingAdapt = MapDistance.MEDIAN; + linearSpFunction = this::linearBFPRTImproved; + break; + case BFPRTA: + // Here we re-use the same method as the only difference is adaption of k + noSamplingAdapt = MapDistance.ADAPT; + linearSpFunction = this::linearBFPRTImproved; + break; + case RS_IM: + noSamplingAdapt = MapDistance.MEDIAN; + linearSpFunction = this::linearRepeatedStepImproved; + break; + case RSA: + // Here we re-use the same method as the only difference is adaption of k + noSamplingAdapt = MapDistance.ADAPT; + linearSpFunction = this::linearRepeatedStepImproved; + break; + default: + throw new IllegalArgumentException("Unknown linear strategy: " + v); + } + return this; + } + + /** + * Sets the edge-select strategy. + * + * @param v Value. + * @return {@code this} for chaining + */ + Partition setEdgeSelectStrategy(EdgeSelectStrategy v) { + switch (v) { + case ESH: + edgeSelection = Partition::heapSelectRange; + break; + case ESH2: + edgeSelection = Partition::heapSelectRange2; + break; + case ESS: + edgeSelection = Partition::sortSelectRange; + break; + case ESS2: + edgeSelection = Partition::sortSelectRange2; + break; + default: + throw new IllegalArgumentException("Unknown edge select: " + v); + } + return this; + } + + /** + * Sets the stopper strategy (when quickselect progress is poor). + * + * @param v Value. + * @return {@code this} for chaining + */ + Partition setStopperStrategy(StopperStrategy v) { + switch (v) { + case SSH: + stopperSelection = Partition::heapSelectRange; + break; + case SSH2: + stopperSelection = Partition::heapSelectRange2; + break; + case SLS: + // Linear select does not match the interface as it: + // - requires the single-pivot partition function + // - uses a bounds array to allow minimising the partition region size after pivot selection + stopperSelection = (a, l, r, ka, kb) -> linearSelect(getSPFunction(), + a, l, r, ka, kb, new int[2]); + break; + case SQA: + // Linear select does not match the interface as it: + // - uses a bounds array to allow minimising the partition region size after pivot selection + // - uses control flags to set sampling mode on/off + stopperSelection = (a, l, r, ka, kb) -> quickSelectAdaptive(a, l, r, ka, kb, new int[1], + adaptMode); + break; + default: + throw new IllegalArgumentException("Unknown stopper: " + v); + } + return this; + } + + /** + * Sets the key strategy. + * + * @param v Value. + * @return {@code this} for chaining + */ + Partition setKeyStrategy(KeyStrategy v) { + this.keyStrategy = v; + return this; + } + + /** + * Sets the paired key strategy. + * + * @param v Value. + * @return {@code this} for chaining + */ + Partition setPairedKeyStrategy(PairedKeyStrategy v) { + this.pairedKeyStrategy = v; + return this; + } + + /** + * Sets the recursion multiple. + * + * @param v Value. + * @return {@code this} for chaining + */ + Partition setRecursionMultiple(double v) { + this.recursionMultiple = v; + return this; + } + + /** + * Sets the recursion constant. + * + * @param v Value. + * @return {@code this} for chaining + */ + Partition setRecursionConstant(int v) { + this.recursionConstant = v; + return this; + } + + /** + * Sets the compression for a {@link CompressedIndexSet}. + * + * @param v Value. + * @return {@code this} for chaining + */ + Partition setCompression(int v) { + if (v < 1 || v > Integer.SIZE - 1) { + throw new IllegalArgumentException("Bad compression: " + v); + } + this.compression = v; + return this; + } + + /** + * Sets the control flags for Floyd-Rivest sub-sampling. + * + * @param v Value. + * @return {@code this} for chaining + */ + Partition setControlFlags(int v) { + this.controlFlags = v; + // Quickselect adaptive requires functions to map k to the sample. + // These functions must be set based on the margins in the repeated step method. + // These will differ due to the implementation and whether the first step is + // skipped (sampling mode on). + if ((v & FLAG_QA_FAR_STEP_ADAPT_ORIGINAL) != 0) { + // Use the same mapping for all repeated step functions. + // This is the original behaviour from Alexandrescu (2016). + samplingAdapt = samplingEdgeAdapt = noSamplingAdapt = noSamplingEdgeAdapt = MapDistance.ADAPT; + } else { + // Default behaviour. This optimises the adaption for the algorithm. + samplingAdapt = MapDistance.ADAPT; + if ((v & FLAG_QA_FAR_STEP) != 0) { + // Switches the far-step to minimum-of-4, median-of-3. + // When sampling mode is on all samples are from median-of-3 and we + // use the same adaption. + samplingEdgeAdapt = MapDistance.ADAPT; + } else { + // Original far-step of lower-median-of-4, minimum-of-3 + // When sampling mode is on the sample is a minimum-of-3. This halves the + // lower margin from median-of-3. Change the adaption to avoid + // a tiny lower margin (and possibility of k falling in a very large partition). + // Note: The only way we can ensure that k is inside the lower margin is by using + // (r-l) as the sample k. Compromise by using the midpoint for a 50% chance that + // k is inside the lower margin. + samplingEdgeAdapt = MapDistance.MEDIAN; + } + noSamplingAdapt = MapDistance.ADAPT; + // Force edge margin to contain the target index + noSamplingEdgeAdapt = MapDistance.EDGE_ADAPT; + } + return this; + } + + /** + * Sets the size for sortselect for the linearselect algorithm. + * Must be above 0 for the algorithm to return (else an infinite loop occurs). + * The minimum size required depends on the expand partition function, and the + * same size relative to the range (e.g. 1/5, 1/9 or 1/12). + * + * @param v Value. + * @return {@code this} for chaining + */ + Partition setLinearSortSelectSize(int v) { + if (v < 1) { + throw new IllegalArgumentException("Bad linear sortselect size: " + v); + } + this.linearSortSelectSize = v; + return this; + } + + /** + * Sets the quickselect adaptive mode. + * + * @param v Value. + * @return {@code this} for chaining + */ + Partition setAdaptMode(AdaptMode v) { + this.adaptMode = v; + return this; + } + + /** + * Sets the recursion consumer. This is called with the value of the recursion + * counter immediately before the introselect routine returns. It is used to + * analyse recursion depth on various input data. + * + * @param v Value. + */ + void setRecursionConsumer(IntConsumer v) { + this.recursionConsumer = Objects.requireNonNull(v); + } + + /** + * Gets the single-pivot partition function. + * + * @return the single-pivot partition function + */ + SPEPartition getSPFunction() { + return spFunction; + } + + /** + * Configure the properties used by the static quickselect adaptive algorithm. + * The increment is used to update the current mode when the margins are not achieved. + * + * @param mode Initial mode. + * @param increment Flag increment + */ + static void configureQaAdaptive(int mode, int increment) { + qaMode = mode; + qaIncrement = increment; + } + + /** + * Move the minimum value to the start of the range. + * + *

Note: Respects the ordering of signed zeros. + * + *

Assumes {@code left <= right}. + * + * @param data Data. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + static void selectMin(double[] data, int left, int right) { + selectMinIgnoreZeros(data, left, right); + // Edge-case: if min was 0.0, check for a -0.0 above and swap. + if (data[left] == 0) { + minZero(data, left, right); + } + } + + /** + * Move the maximum value to the end of the range. + * + *

Note: Respects the ordering of signed zeros. + * + *

Assumes {@code left <= right}. + * + * @param data Data. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + static void selectMax(double[] data, int left, int right) { + selectMaxIgnoreZeros(data, left, right); + // Edge-case: if max was -0.0, check for a 0.0 below and swap. + if (data[right] == 0) { + maxZero(data, left, right); + } + } + + /** + * Place a negative signed zero at {@code left} before any positive signed zero in the range, + * {@code -0.0 < 0.0}. + * + *

Warning: Only call when {@code data[left]} is zero. + * + * @param data Data. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + private static void minZero(double[] data, int left, int right) { + // Assume data[left] is zero and check the sign bit + if (Double.doubleToRawLongBits(data[left]) >= 0) { + // Check for a -0.0 above and swap. + // We only require 1 swap as this is not a full sort of zeros. + for (int k = left; ++k <= right;) { + if (data[k] == 0 && Double.doubleToRawLongBits(data[k]) < 0) { + data[k] = 0.0; + data[left] = -0.0; + break; + } + } + } + } + + /** + * Place a positive signed zero at {@code right} after any negative signed zero in the range, + * {@code -0.0 < 0.0}. + * + *

Warning: Only call when {@code data[right]} is zero. + * + * @param data Data. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + private static void maxZero(double[] data, int left, int right) { + // Assume data[right] is zero and check the sign bit + if (Double.doubleToRawLongBits(data[right]) < 0) { + // Check for a 0.0 below and swap. + // We only require 1 swap as this is not a full sort of zeros. + for (int k = right; --k >= left;) { + if (data[k] == 0 && Double.doubleToRawLongBits(data[k]) >= 0) { + data[k] = -0.0; + data[right] = 0.0; + break; + } + } + } + } + + /** + * Move the minimum value to the start of the range. + * + *

Assumes {@code left <= right}. + * + * @param data Data. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + static void selectMinIgnoreZeros(double[] data, int left, int right) { + // Mitigate worst case performance on descending data by backward sweep + double min = data[left]; + for (int i = right + 1; --i > left;) { + final double v = data[i]; + if (v < min) { + data[i] = min; + min = v; + } + } + data[left] = min; + } + + /** + * Move the two smallest values to the start of the range. + * + *

Assumes {@code left < right}. + * + * @param data Data. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + static void selectMin2IgnoreZeros(double[] data, int left, int right) { + double min1 = data[left + 1]; + if (min1 < data[left]) { + min1 = data[left]; + data[left] = data[left + 1]; + } + // Mitigate worst case performance on descending data by backward sweep + for (int i = right + 1, end = left + 1; --i > end;) { + final double v = data[i]; + if (v < min1) { + data[i] = min1; + if (v < data[left]) { + min1 = data[left]; + data[left] = v; + } else { + min1 = v; + } + } + } + data[left + 1] = min1; + } + + /** + * Move the maximum value to the end of the range. + * + *

Assumes {@code left <= right}. + * + * @param data Data. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + static void selectMaxIgnoreZeros(double[] data, int left, int right) { + // Mitigate worst case performance on descending data by backward sweep + double max = data[right]; + for (int i = left - 1; ++i < right;) { + final double v = data[i]; + if (v > max) { + data[i] = max; + max = v; + } + } + data[right] = max; + } + + /** + * Move the two largest values to the end of the range. + * + *

Assumes {@code left < right}. + * + * @param data Data. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + static void selectMax2IgnoreZeros(double[] data, int left, int right) { + double max1 = data[right - 1]; + if (max1 > data[right]) { + max1 = data[right]; + data[right] = data[right - 1]; + } + // Mitigate worst case performance on descending data by backward sweep + for (int i = left - 1, end = right - 1; ++i < end;) { + final double v = data[i]; + if (v > max1) { + data[i] = max1; + if (v > data[right]) { + max1 = data[right]; + data[right] = v; + } else { + max1 = v; + } + } + } + data[right - 1] = max1; + } + + /** + * Sort the elements using a heap sort algorithm. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + static void heapSort(double[] a, int left, int right) { + // We could make a choice here between select left or right + heapSelectLeft(a, left, right, right, right - left); + } + + /** + * Partition the elements {@code ka} and {@code kb} using a heap select algorithm. It + * is assumed {@code left <= ka <= kb <= right}. Any range between the two elements is + * not ensured to be sorted. + * + *

If there is no range between the two point, i.e. {@code ka == kb} or + * {@code ka + 1 == kb}, it is preferred to use + * {@link #heapSelectRange(double[], int, int, int, int)}. The result is the same but + * the decision choice is simpler for the range function. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param ka Lower index to select. + * @param kb Upper index to select. + * @see #heapSelectRange(double[], int, int, int, int) + */ + static void heapSelectPair(double[] a, int left, int right, int ka, int kb) { + // Avoid the overhead of heap select on tiny data (supports right <= left). + if (right - left < MIN_HEAPSELECT_SIZE) { + Sorting.sort(a, left, right); + return; + } + // Call the appropriate heap partition function based on + // building a heap up to 50% of the length + // |l|-----|ka|--------|kb|------|r| + // ---d1---- + // -----d3---- + // ---------d2---------- + // ----------d4----------- + final int d1 = ka - left; + final int d2 = kb - left; + final int d3 = right - kb; + final int d4 = right - ka; + if (d1 + d3 < Math.min(d2, d4)) { + // Partition both ends. + // Note: Not possible if ka == kb. + // s1 + s3 == r - l and >= than the smallest + // distance to one of the ends + heapSelectLeft(a, left, right, ka, 0); + // Repeat for the other side above ka + heapSelectRight(a, ka + 1, right, kb, 0); + } else if (d2 < d4) { + heapSelectLeft(a, left, right, kb, kb - ka); + } else { + // s4 + heapSelectRight(a, left, right, ka, kb - ka); + } + } + + /** + * Partition the elements between {@code ka} and {@code kb} using a heap select + * algorithm. It is assumed {@code left <= ka <= kb <= right}. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param ka Lower index to select. + * @param kb Upper index to select. + * @see #heapSelectPair(double[], int, int, int, int) + */ + static void heapSelectRange(double[] a, int left, int right, int ka, int kb) { + // Combine the test for right <= left with + // avoiding the overhead of heap select on tiny data. + if (right - left < MIN_HEAPSELECT_SIZE) { + Sorting.sort(a, left, right); + return; + } + // Call the appropriate heap partition function based on + // building a heap up to 50% of the length + // |l|-----|ka|--------|kb|------|r| + // |---------d1-----------| + // |----------d2-----------| + // Note: Optimisation for small heap size (n=1,2) is negligible. + // The main overhead is the test for insertion against the current top of the heap + // which grows increasingly unlikely as the range is scanned. + if (kb - left < right - ka) { + heapSelectLeft(a, left, right, kb, kb - ka); + } else { + heapSelectRight(a, left, right, ka, kb - ka); + } + } + + /** + * Partition the minimum {@code n} elements below {@code k} where + * {@code n = k - left + 1}. Uses a heap select algorithm. + * + *

Works with any {@code k} in the range {@code left <= k <= right} + * and can be used to perform a full sort of the range below {@code k} + * using the {@code count} parameter. + * + *

For best performance this should be called with + * {@code k - left < right - k}, i.e. + * to partition a value in the lower half of the range. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param k Index to select. + * @param count Size of range to sort below k. + */ + static void heapSelectLeft(double[] a, int left, int right, int k, int count) { + // Create a max heap in-place in [left, k], rooted at a[left] = max + // |l|-max-heap-|k|--------------| + // Build the heap using Floyd's heap-construction algorithm for heap size n. + // Start at parent of the last element in the heap (k), + // i.e. start = parent(n-1) : parent(c) = floor((c - 1) / 2) : c = k - left + int end = k + 1; + for (int p = left + ((k - left - 1) >> 1); p >= left; p--) { + maxHeapSiftDown(a, a[p], p, left, end); + } + // Scan the remaining data and insert + // Mitigate worst case performance on descending data by backward sweep + double max = a[left]; + for (int i = right + 1; --i > k;) { + final double v = a[i]; + if (v < max) { + a[i] = max; + maxHeapSiftDown(a, v, left, left, end); + max = a[left]; + } + } + + // To partition elements k (and below) move the top of the heap to the position + // immediately after the end of the reduced size heap; the previous end + // of the heap [k] is placed at the top + // |l|-max-heap-|k|--------------| + // | <-swap-> | + // The heap can be restored by sifting down the new top. + + // Always require the top 1 + a[left] = a[k]; + a[k] = max; + + if (count > 0) { + --end; + // Sifting limited to heap size of 2 (i.e. don't sift heap n==1) + for (int c = Math.min(count, end - left - 1); --c >= 0;) { + maxHeapSiftDown(a, a[left], left, left, end--); + // Move top of heap to the sorted end + max = a[left]; + a[left] = a[end]; + a[end] = max; + } + } + } + + /** + * Sift the element down the max heap. + * + *

Assumes {@code root <= p < end}, i.e. the max heap is above root. + * + * @param a Heap data. + * @param v Value to sift. + * @param p Start position. + * @param root Root of the heap. + * @param end End of the heap (exclusive). + */ + private static void maxHeapSiftDown(double[] a, double v, int p, int root, int end) { + // child2 = root + 2 * (parent - root) + 2 + // = 2 * parent - root + 2 + while (true) { + // Right child + int c = (p << 1) - root + 2; + if (c > end) { + // No left child + break; + } + // Use the left child if right doesn't exist, or it is greater + if (c == end || a[c] < a[c - 1]) { + --c; + } + if (v >= a[c]) { + // Parent greater than largest child - done + break; + } + // Swap and descend + a[p] = a[c]; + p = c; + } + a[p] = v; + } + + /** + * Partition the maximum {@code n} elements above {@code k} where + * {@code n = right - k + 1}. Uses a heap select algorithm. + * + *

Works with any {@code k} in the range {@code left <= k <= right} + * and can be used to perform a full sort of the range above {@code k} + * using the {@code count} parameter. + * + *

For best performance this should be called with + * {@code k - left > right - k}, i.e. + * to partition a value in the upper half of the range. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param k Index to select. + * @param count Size of range to sort below k. + */ + static void heapSelectRight(double[] a, int left, int right, int k, int count) { + // Create a min heap in-place in [k, right], rooted at a[right] = min + // |--------------|k|-min-heap-|r| + // Build the heap using Floyd's heap-construction algorithm for heap size n. + // Start at parent of the last element in the heap (k), + // i.e. start = parent(n-1) : parent(c) = floor((c - 1) / 2) : c = right - k + int end = k - 1; + for (int p = right - ((right - k - 1) >> 1); p <= right; p++) { + minHeapSiftDown(a, a[p], p, right, end); + } + // Scan the remaining data and insert + // Mitigate worst case performance on descending data by backward sweep + double min = a[right]; + for (int i = left - 1; ++i < k;) { + final double v = a[i]; + if (v > min) { + a[i] = min; + minHeapSiftDown(a, v, right, right, end); + min = a[right]; + } + } + + // To partition elements k (and above) move the top of the heap to the position + // immediately before the end of the reduced size heap; the previous end + // of the heap [k] is placed at the top. + // |--------------|k|-min-heap-|r| + // | <-swap-> | + // The heap can be restored by sifting down the new top. + + // Always require the top 1 + a[right] = a[k]; + a[k] = min; + + if (count > 0) { + ++end; + // Sifting limited to heap size of 2 (i.e. don't sift heap n==1) + for (int c = Math.min(count, right - end - 1); --c >= 0;) { + minHeapSiftDown(a, a[right], right, right, end++); + // Move top of heap to the sorted end + min = a[right]; + a[right] = a[end]; + a[end] = min; + } + } + } + + /** + * Sift the element down the min heap. + * + *

Assumes {@code root >= p > end}, i.e. the max heap is below root. + * + * @param a Heap data. + * @param v Value to sift. + * @param p Start position. + * @param root Root of the heap. + * @param end End of the heap (exclusive). + */ + private static void minHeapSiftDown(double[] a, double v, int p, int root, int end) { + // child2 = root - 2 * (root - parent) - 2 + // = 2 * parent - root - 2 + while (true) { + // Right child + int c = (p << 1) - root - 2; + if (c < end) { + // No left child + break; + } + // Use the left child if right doesn't exist, or it is less + if (c == end || a[c] > a[c + 1]) { + ++c; + } + if (v <= a[c]) { + // Parent less than smallest child - done + break; + } + // Swap and descend + a[p] = a[c]; + p = c; + } + a[p] = v; + } + + /** + * Partition the elements between {@code ka} and {@code kb} using a heap select + * algorithm. It is assumed {@code left <= ka <= kb <= right}. + * + *

Differs from {@link #heapSelectRange(double[], int, int, int, int)} by using + * a different extraction of the sorted elements from the heap. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param ka Lower index to select. + * @param kb Upper index to select. + * @see #heapSelectPair(double[], int, int, int, int) + */ + static void heapSelectRange2(double[] a, int left, int right, int ka, int kb) { + // Combine the test for right <= left with + // avoiding the overhead of heap select on tiny data. + if (right - left < MIN_HEAPSELECT_SIZE) { + Sorting.sort(a, left, right); + return; + } + // Use the smallest heap + if (kb - left < right - ka) { + heapSelectLeft2(a, left, right, ka, kb); + } else { + heapSelectRight2(a, left, right, ka, kb); + } + } + + /** + * Partition the elements between {@code ka} and {@code kb} using a heap select + * algorithm. It is assumed {@code left <= ka <= kb <= right}. + * + *

For best performance this should be called with {@code k} in the lower + * half of the range. + * + *

Differs from {@link #heapSelectLeft(double[], int, int, int, int)} by using + * a different extraction of the sorted elements from the heap. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param ka Lower index to select. + * @param kb Upper index to select. + */ + static void heapSelectLeft2(double[] a, int left, int right, int ka, int kb) { + // Create a max heap in-place in [left, k], rooted at a[left] = max + // |l|-max-heap-|k|--------------| + // Build the heap using Floyd's heap-construction algorithm for heap size n. + // Start at parent of the last element in the heap (k), + // i.e. start = parent(n-1) : parent(c) = floor((c - 1) / 2) : c = k - left + int end = kb + 1; + for (int p = left + ((kb - left - 1) >> 1); p >= left; p--) { + maxHeapSiftDown(a, a[p], p, left, end); + } + // Scan the remaining data and insert + // Mitigate worst case performance on descending data by backward sweep + double max = a[left]; + for (int i = right + 1; --i > kb;) { + final double v = a[i]; + if (v < max) { + a[i] = max; + maxHeapSiftDown(a, v, left, left, end); + max = a[left]; + } + } + // Partition [ka, kb] + // |l|-max-heap-|k|--------------| + // | <-swap-> | then sift down reduced size heap + // Avoid sifting heap of size 1 + final int last = Math.max(left, ka - 1); + while (--end > last) { + maxHeapSiftDown(a, a[end], left, left, end); + a[end] = max; + max = a[left]; + } + } + + /** + * Partition the elements between {@code ka} and {@code kb} using a heap select + * algorithm. It is assumed {@code left <= ka <= kb <= right}. + * + *

For best performance this should be called with {@code k} in the upper + * half of the range. + * + *

Differs from {@link #heapSelectRight(double[], int, int, int, int)} by using + * a different extraction of the sorted elements from the heap. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param ka Lower index to select. + * @param kb Upper index to select. + */ + static void heapSelectRight2(double[] a, int left, int right, int ka, int kb) { + // Create a min heap in-place in [k, right], rooted at a[right] = min + // |--------------|k|-min-heap-|r| + // Build the heap using Floyd's heap-construction algorithm for heap size n. + // Start at parent of the last element in the heap (k), + // i.e. start = parent(n-1) : parent(c) = floor((c - 1) / 2) : c = right - k + int end = ka - 1; + for (int p = right - ((right - ka - 1) >> 1); p <= right; p++) { + minHeapSiftDown(a, a[p], p, right, end); + } + // Scan the remaining data and insert + // Mitigate worst case performance on descending data by backward sweep + double min = a[right]; + for (int i = left - 1; ++i < ka;) { + final double v = a[i]; + if (v > min) { + a[i] = min; + minHeapSiftDown(a, v, right, right, end); + min = a[right]; + } + } + // Partition [ka, kb] + // |--------------|k|-min-heap-|r| + // | <-swap-> | then sift down reduced size heap + // Avoid sifting heap of size 1 + final int last = Math.min(right, kb + 1); + while (++end < last) { + minHeapSiftDown(a, a[end], right, right, end); + a[end] = min; + min = a[right]; + } + } + + /** + * Partition the elements between {@code ka} and {@code kb} using a sort select + * algorithm. It is assumed {@code left <= ka <= kb <= right}. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param ka Lower index to select. + * @param kb Upper index to select. + */ + static void sortSelectRange(double[] a, int left, int right, int ka, int kb) { + // Combine the test for right <= left with + // avoiding the overhead of sort select on tiny data. + if (right - left <= MIN_SORTSELECT_SIZE) { + Sorting.sort(a, left, right); + return; + } + // Sort the smallest side + if (kb - left < right - ka) { + sortSelectLeft(a, left, right, kb); + } else { + sortSelectRight(a, left, right, ka); + } + } + + /** + * Partition the minimum {@code n} elements below {@code k} where + * {@code n = k - left + 1}. Uses an insertion sort algorithm. + * + *

Works with any {@code k} in the range {@code left <= k <= right} + * and performs a full sort of the range below {@code k}. + * + *

For best performance this should be called with + * {@code k - left < right - k}, i.e. + * to partition a value in the lower half of the range. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param k Index to select. + */ + static void sortSelectLeft(double[] a, int left, int right, int k) { + // Sort + for (int i = left; ++i <= k;) { + final double v = a[i]; + // Move preceding higher elements above (if required) + if (v < a[i - 1]) { + int j = i; + while (--j >= left && v < a[j]) { + a[j + 1] = a[j]; + } + a[j + 1] = v; + } + } + // Scan the remaining data and insert + // Mitigate worst case performance on descending data by backward sweep + double m = a[k]; + for (int i = right + 1; --i > k;) { + final double v = a[i]; + if (v < m) { + a[i] = m; + int j = k; + while (--j >= left && v < a[j]) { + a[j + 1] = a[j]; + } + a[j + 1] = v; + m = a[k]; + } + } + } + + /** + * Partition the maximum {@code n} elements above {@code k} where + * {@code n = right - k + 1}. Uses an insertion sort algorithm. + * + *

Works with any {@code k} in the range {@code left <= k <= right} + * and can be used to perform a full sort of the range above {@code k}. + * + *

For best performance this should be called with + * {@code k - left > right - k}, i.e. + * to partition a value in the upper half of the range. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param k Index to select. + */ + static void sortSelectRight(double[] a, int left, int right, int k) { + // Sort + for (int i = right; --i >= k;) { + final double v = a[i]; + // Move succeeding lower elements below (if required) + if (v > a[i + 1]) { + int j = i; + while (++j <= right && v > a[j]) { + a[j - 1] = a[j]; + } + a[j - 1] = v; + } + } + // Scan the remaining data and insert + // Mitigate worst case performance on descending data by backward sweep + double m = a[k]; + for (int i = left - 1; ++i < k;) { + final double v = a[i]; + if (v > m) { + a[i] = m; + int j = k; + while (++j <= right && v > a[j]) { + a[j - 1] = a[j]; + } + a[j - 1] = v; + m = a[k]; + } + } + } + + /** + * Partition the elements between {@code ka} and {@code kb} using a sort select + * algorithm. It is assumed {@code left <= ka <= kb <= right}. + * + *

Differs from {@link #sortSelectRange(double[], int, int, int, int)} by using + * a pointer to a position in the sorted array to skip ahead during insertion. + * This extra complexity does not improve performance. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param ka Lower index to select. + * @param kb Upper index to select. + */ + static void sortSelectRange2(double[] a, int left, int right, int ka, int kb) { + // Combine the test for right <= left with + // avoiding the overhead of sort select on tiny data. + if (right - left <= MIN_SORTSELECT_SIZE) { + Sorting.sort(a, left, right); + return; + } + // Sort the smallest side + if (kb - left < right - ka) { + sortSelectLeft2(a, left, right, kb); + } else { + sortSelectRight2(a, left, right, ka); + } + } + + /** + * Partition the minimum {@code n} elements below {@code k} where + * {@code n = k - left + 1}. Uses an insertion sort algorithm. + * + *

Works with any {@code k} in the range {@code left <= k <= right} + * and performs a full sort of the range below {@code k}. + * + *

For best performance this should be called with + * {@code k - left < right - k}, i.e. + * to partition a value in the lower half of the range. + * + *

Differs from {@link #sortSelectLeft(double[], int, int, int)} by using + * a pointer to a position in the sorted array to skip ahead during insertion. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param k Index to select. + */ + static void sortSelectLeft2(double[] a, int left, int right, int k) { + // Sort + for (int i = left; ++i <= k;) { + final double v = a[i]; + // Move preceding higher elements above (if required) + if (v < a[i - 1]) { + int j = i; + while (--j >= left && v < a[j]) { + a[j + 1] = a[j]; + } + a[j + 1] = v; + } + } + // Scan the remaining data and insert + // Mitigate worst case performance on descending data by backward sweep + double m = a[k]; + // Pointer to a position in the sorted array + final int p = (left + k) >>> 1; + for (int i = right + 1; --i > k;) { + final double v = a[i]; + if (v < m) { + a[i] = m; + int j = k; + if (v < a[p]) { + // Skip ahead + //System.arraycopy(a, p, a, p + 1, k - p); + while (j > p) { + // left index is evaluated before right decrement + a[j] = a[--j]; + } + // j == p + while (--j >= left && v < a[j]) { + a[j + 1] = a[j]; + } + } else { + // No bounds check on left: a[p] <= v < a[k] + while (v < a[--j]) { + a[j + 1] = a[j]; + } + } + a[j + 1] = v; + m = a[k]; + } + } + } + + /** + * Partition the maximum {@code n} elements above {@code k} where + * {@code n = right - k + 1}. Uses an insertion sort algorithm. + * + *

Works with any {@code k} in the range {@code left <= k <= right} + * and can be used to perform a full sort of the range above {@code k}. + * + *

For best performance this should be called with + * {@code k - left > right - k}, i.e. + * to partition a value in the upper half of the range. + * + *

Differs from {@link #sortSelectRight(double[], int, int, int)} by using + * a pointer to a position in the sorted array to skip ahead during insertion. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param k Index to select. + */ + static void sortSelectRight2(double[] a, int left, int right, int k) { + // Sort + for (int i = right; --i >= k;) { + final double v = a[i]; + // Move succeeding lower elements below (if required) + if (v > a[i + 1]) { + int j = i; + while (++j <= right && v > a[j]) { + a[j - 1] = a[j]; + } + a[j - 1] = v; + } + } + // Scan the remaining data and insert + // Mitigate worst case performance on descending data by backward sweep + double m = a[k]; + // Pointer to a position in the sorted array + final int p = (right + k) >>> 1; + for (int i = left - 1; ++i < k;) { + final double v = a[i]; + if (v > m) { + a[i] = m; + int j = k; + if (v > a[p]) { + // Skip ahead + //System.arraycopy(a, p, a, p - 1, p - k); + while (j < p) { + // left index is evaluated before right increment + a[j] = a[++j]; + } + // j == p + while (++j <= right && v > a[j]) { + a[j - 1] = a[j]; + } + } else { + // No bounds check on right: a[k] < v <= a[p] + while (v > a[++j]) { + a[j - 1] = a[j]; + } + } + a[j - 1] = v; + m = a[k]; + } + } + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

Note: This is the only quickselect method in this class not based on introselect. + * This is a legacy method containing alternatives for iterating over + * multiple keys that are not supported by introselect, namely: + * + *

+ * + *

Note: In each method indices are processed independently. Thus each bracket around an + * index to partition does not know the number of recursion steps used to obtain the start + * pivots defining the bracket. Excess recursion cannot be efficiently tracked for each + * partition. This is unlike introselect which tracks recursion and can switch to algorithm + * if quickselect convergence is slow. + * + *

Benchmarking can be used to show these alternatives are slower. + * + * @param part Partition function. + * @param data Values. + * @param right Upper bound of data (inclusive). + * @param k Indices (may be destructively modified). + * @param count Count of indices. + */ + private void partition(PartitionFunction part, double[] data, int right, int[] k, int count) { + if (count < 1 || right < 1) { + return; + } + // Validate indices. Excludes indices > right. + final int n = countIndices(k, count, right); + if (n < 1) { + return; + } + if (n == 1) { + part.partition(data, 0, right, k[0], k[0], false, false); + } else if (n == 2 && Math.abs(k[0] - k[1]) <= (minQuickSelectSize >>> 1)) { + final int ka = Math.min(k[0], k[1]); + final int kb = Math.max(k[0], k[1]); + part.partition(data, 0, right, ka, kb, false, false); + } else { + // Allow non-sequential / sequential processing to be selected + if (keyStrategy == KeyStrategy.SEQUENTIAL) { + // Sequential processing + final ScanningPivotCache pivots = keyAnalysis(right + 1, k, n, minQuickSelectSize >>> 1); + if (k[0] == Integer.MIN_VALUE) { + // Full-sort recommended. Assume the partition function + // can choose to switch to using Arrays.sort. + part.sort(data, 0, right, false, false); + } else { + partitionSequential(part, data, k, n, right, pivots); + } + } else if (keyStrategy == KeyStrategy.INDEX_SET) { + // Non-sequential processing using non-optimised storage + final IndexSet pivots = IndexSet.ofRange(0, right); + // First index must partition the entire range + part.partition(data, 0, right, k[0], k[0], false, false, pivots); + for (int i = 1; i < n; i++) { + final int ki = k[i]; + if (pivots.get(ki)) { + continue; + } + final int l = pivots.previousSetBit(ki); + int r = pivots.nextSetBit(ki); + if (r < 0) { + r = right + 1; + } + part.partition(data, l + 1, r - 1, ki, ki, l >= 0, r <= right, pivots); + } + } else if (keyStrategy == KeyStrategy.PIVOT_CACHE) { + // Non-sequential processing using a pivot cache to optimise storage + final PivotCache pivots = createPivotCacheForIndices(k, n); + + // Handle single-point or tiny range + if ((pivots.right() - pivots.left()) <= (minQuickSelectSize >>> 1)) { + part.partition(data, 0, right, pivots.left(), pivots.right(), false, false); + return; + } + + // Bracket the range so the rest is internal. + // Note: Partition function handles min/max searching if ka/kb are + // at the end of the range. + final int ka = pivots.left(); + part.partition(data, 0, right, ka, ka, false, false, pivots); + final int kb = pivots.right(); + int l = pivots.previousPivot(kb); + int r = pivots.nextPivot(kb); + if (r < 0) { + // Partition did not visit downstream + r = right + 1; + } + part.partition(data, l + 1, r - 1, kb, kb, true, r <= right, pivots); + for (int i = 0; i < n; i++) { + final int ki = k[i]; + if (pivots.contains(ki)) { + continue; + } + l = pivots.previousPivot(ki); + r = pivots.nextPivot(ki); + part.partition(data, l + 1, r - 1, ki, ki, true, true, pivots); + } + } else { + throw new IllegalStateException("Unsupported: " + keyStrategy); + } + } + } + + /** + * Return a {@link PivotCache} implementation to support the range + * {@code [left, right]} as defined by minimum and maximum index. + * + * @param indices Indices. + * @param n Count of indices (must be strictly positive). + * @return the pivot cache + */ + private static PivotCache createPivotCacheForIndices(int[] indices, int n) { + int min = indices[0]; + int max = min; + for (int i = 1; i < n; i++) { + final int k = indices[i]; + min = Math.min(min, k); + max = Math.max(max, k); + } + return PivotCaches.ofFullRange(min, max); + } + + /** + * Analysis of keys to partition. The indices k are updated in-place. The keys are + * processed to eliminate duplicates and sorted in ascending order. Close points are + * joined into ranges using the minimum separation. A zero or negative separation + * prevents creating ranges. + * + *

On output the indices contain ranges or single points to partition in ascending + * order. Single points are identified as negative values and should be bit-flipped + * to the index value. + * + *

If compression occurs the result will contain fewer indices than {@code n}. + * The end of the compressed range is marked using {@link Integer#MIN_VALUE}. This + * is outside the valid range for any single index and signals to stop processing + * the ordered indices. + * + *

A {@link PivotCache} implementation is returned for optimal bracketing + * of indices in the range after the first target range / point. + * + *

Examples: + * + *

{@code
+     *                                                 [L, R] PivotCache
+     * [3]                -> [3]                       -
+     *
+     * // min separation 0
+     * [3, 4, 5]          -> [~3, ~4, ~5]              [4, 5]
+     * [3, 4, 7, 8]       -> [~3, ~4, ~7, ~8]          [4, 8]
+     *
+     * // min separation 1
+     * [3, 4, 5]          -> [3, 5, MIN_VALUE]         -
+     * [3, 4, 5, 8]       -> [3, 5, ~8, MIN_VALUE]     [8]
+     * [3, 4, 5, 6, 7, 8] -> [3, 8, MIN_VALUE, ...]    -
+     * [3, 4, 7, 8]       -> [3, 4, 7, 8]              [7, 8]
+     * [3, 4, 7, 8, 99]   -> [3, 4, 7, 8, ~99]         [7, 99]
+     * }
+ * + *

The length of data to partition can be used to determine if processing is + * required. A full sort of the data is recommended by returning + * {@code k[0] == Integer.MIN_VALUE}. This occurs if the length is sufficiently small + * or the first range to partition covers the entire data. + * + *

Note: The signal marker {@code Integer.MIN_VALUE} is {@code Integer.MAX_VALUE} + * bit flipped. It this is outside the range of any valid index into an array. + * + * @param size Length of the data to partition. + * @param k Indices. + * @param n Count of indices (must be strictly positive). + * @param minSeparation Minimum separation between points (set to zero to disable ranges). + * @return the pivot cache + */ + // package-private for testing + ScanningPivotCache keyAnalysis(int size, int[] k, int n, int minSeparation) { + // Tiny data, signal to sort it + if (size < minQuickSelectSize) { + k[0] = Integer.MIN_VALUE; + return null; + } + // Sort the keys + final IndexSet indices = Sorting.sortUnique(Math.max(6, minQuickSelectSize), k, n); + // Find the max index + int right = k[n - 1]; + if (right < 0) { + right = ~right; + } + // Join up close keys using the min separation distance. + final int left = compressRange(k, n, minSeparation); + if (left < 0) { + // Nothing to partition after the first target. + // Recommend full sort if the range is effectively complete. + // A range requires n > 1 and positive indices. + if (n != 1 && k[0] >= 0 && size - (k[1] - k[0]) < minQuickSelectSize) { + k[0] = Integer.MIN_VALUE; + } + return null; + } + // Return an optimal PivotCache to process keys in sorted order + if (indices != null) { + // Reuse storage from sorting large number of indices + return indices.asScanningPivotCache(left, right); + } + return IndexSet.createScanningPivotCache(left, right); + } + + /** + * Compress sorted indices into ranges using the minimum separation. + * Single points are identified by bit flipping to negative. The + * first unused position after compression is set to {@link Integer#MIN_VALUE}, + * unless this is outside the array length (i.e. no compression). + * + * @param k Unique indices (sorted). + * @param n Count of indices (must be strictly positive). + * @param minSeparation Minimum separation between points. + * @return the first index after the initial pair / point (or -1) + */ + private static int compressRange(int[] k, int n, int minSeparation) { + if (n == 1) { + // Single point, mark the first unused position + if (k.length > 1) { + k[1] = Integer.MIN_VALUE; + } + return -1; + } + // Start of range is in k[j]; end in p2 + int j = 0; + int p2 = k[0]; + int secondTarget = -1; + for (int i = 0; ++i < n;) { + if (k[i] < 0) { + // Start of duplicate indices + break; + } + if (k[i] <= p2 + minSeparation) { + // Extend range + p2 = k[i]; + } else { + // Store range or point (bit flipped) + if (k[j] == p2) { + k[j] = ~p2; + } else { + k[++j] = p2; + } + j++; + // Next range is k[j] to p2 + k[j] = p2 = k[i]; + // Set the position of the second target + if (secondTarget < 0) { + secondTarget = p2; + } + } + } + // Store range or point (bit flipped) + // Note: If there is only 1 range then the second target is -1 + if (k[j] == p2) { + k[j] = ~p2; + } else { + k[++j] = p2; + } + j++; + // Add a marker at the end of the compressed indices + if (k.length > j) { + k[j] = Integer.MIN_VALUE; + } + return secondTarget; + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

The keys must have been pre-processed by {@link #keyAnalysis(int, int[], int, int)} + * to structure them for sequential processing. + * + * @param part Partition function. + * @param data Values. + * @param k Indices (created by key analysis). + * @param n Count of indices. + * @param right Upper bound (inclusive). + * @param pivots Cache of pivots (created by key analysis). + */ + private static void partitionSequential(PartitionFunction part, double[] data, int[] k, int n, + int right, ScanningPivotCache pivots) { + // Sequential processing of [s, s] single points / [s, e] pairs (regions). + // Single-points are identified as negative indices. + // The partition algorithm must run so each [s, e] is sorted: + // lower---se----------------s---e---------upper + // Pivots are stored to allow lower / upper to be set for the next region: + // lower---se-------p--------s-p-e-----p---upper + int i = 1; + int s = k[0]; + int e; + if (s < 0) { + e = s = ~s; + } else { + e = k[i++]; + } + + // Key analysis has configured the pivot cache correctly for the first region. + // If there is no cache, there is only 1 region. + if (pivots == null) { + part.partition(data, 0, right, s, e, false, false); + return; + } + + part.partitionSequential(data, 0, right, s, e, false, false, pivots); + + // Process remaining regions + while (i < n) { + s = k[i++]; + if (s < 0) { + e = s = ~s; + } else { + e = k[i++]; + } + if (s > right) { + // End of indices + break; + } + // Cases: + // 1. l------s-----------r Single point (s==e) + // 2. l------se----------r An adjacent pair of points + // 3. l------s------e----r A range of points (may contain internal pivots) + // Find bounding region of range: [l, r) + // Left (inclusive) is always above 0 as we have partitioned upstream already. + // Right (exclusive) may not have been searched yet so we check right bounds. + final int l = pivots.previousPivot(s); + final int r = pivots.nextPivotOrElse(e, right + 1); + + // Create regions: + // Partition: l------s--p1 + // Sort: p1-----p2 + // Partition: p2-----e-----r + // Look for internal pivots. + int p1 = -1; + int p2 = -1; + if (e - s > 1) { + final int p = pivots.nextPivot(s + 1); + if (p > s && p < e) { + p1 = p; + p2 = pivots.previousPivot(e - 1); + if (p2 - p1 > SORT_BETWEEN_SIZE) { + // Special-case: multiple internal pivots + // Full-sort of (p1, p2). Walk the unsorted regions: + // l------s--p1 p2----e-----r + // ppppp-----pppp----pppp--------- + // s1-e1 s1e1 s1-----e1 + int e1 = pivots.previousNonPivot(p2); + while (p1 < e1) { + final int s1 = pivots.previousPivot(e1); + part.sort(data, s1 + 1, e1, true, true); + e1 = pivots.previousNonPivot(s1); + } + } + } + } + + // Pivots are only required for the next downstream region + int sn = right + 1; + if (i < n) { + sn = k[i]; + if (sn < 0) { + sn = ~sn; + } + } + // Current implementations will signal if this is outside the support. + // Occurs on the last region the cache was created to support (i.e. sn > right). + final boolean unsupportedCacheRange = !pivots.moveLeft(sn); + + // Note: The partition function uses inclusive left and right bounds + // so use +/- 1 from pivot values. If r is not a pivot it is right + 1 + // which is a valid exclusive upper bound. + + if (p1 > s) { + // At least 1 internal pivot: + // l <= s < p1 and p2 < e <= r + // If l == s or r == e these calls should fully sort the respective range + part.partition(data, l + 1, p1 - 1, s, p1 - 1, true, p1 <= right); + if (unsupportedCacheRange) { + part.partition(data, p2 + 1, r - 1, p2 + 1, e, true, r <= right); + } else { + part.partitionSequential(data, p2 + 1, r - 1, p2 + 1, e, true, r <= right, pivots); + } + } else { + // Single range + if (unsupportedCacheRange) { + part.partition(data, l + 1, r - 1, s, e, true, r <= right); + } else { + part.partitionSequential(data, l + 1, r - 1, s, e, true, r <= right, pivots); + } + } + } + } + + /** + * Sort the data. + * + *

Uses a Bentley-McIlroy quicksort partition method. Signed zeros + * are corrected when encountered during processing. + * + * @param data Values. + */ + void sortSBM(double[] data) { + // Handle NaN + final int right = sortNaN(data); + sort((SPEPartitionFunction) this::partitionSBMWithZeros, data, right); + } + + /** + * Sort the data by recursive partitioning (quicksort). + * + * @param part Partition function. + * @param data Values. + * @param right Upper bound (inclusive). + */ + private static void sort(PartitionFunction part, double[] data, int right) { + if (right < 1) { + return; + } + // Signal entire range + part.sort(data, 0, right, false, false); + } + + /** + * Sort the data using an introsort. + * + *

Uses the configured single-pivot partition method; falling back + * to heapsort when quicksort recursion is slow. + * + * @param data Values. + */ + void sortISP(double[] data) { + // NaN processing is done in the introsort method + introsort(getSPFunction(), data); + } + + /** + * Sort the array using an introsort. The single-pivot partition method is provided as an argument. + * Switches to heapsort when recursive partitioning reaches a maximum depth. + * + *

The partition method is not required to handle signed zeros. + * + * @param part Partition function. + * @param a Values. + * @see Introsort (Wikipedia) + */ + private void introsort(SPEPartition part, double[] a) { + // Handle NaN / signed zeros + final DoubleDataTransformer t = SORT_TRANSFORMER.get(); + // Assume this is in-place + t.preProcess(a); + final int end = t.length(); + if (end > 1) { + introsort(part, a, 0, end - 1, createMaxDepthSinglePivot(end)); + } + // Restore signed zeros + t.postProcess(a); + } + + /** + * Sort the array. + * + *

Uses an introsort. The single-pivot partition method is provided as an argument. + * + * @param part Partition function. + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param maxDepth Maximum depth for recursion. + * @see Introsort (Wikipedia) + */ + private void introsort(SPEPartition part, double[] a, int left, int right, int maxDepth) { + // Only one side requires recursion. The other side + // can remain within this function call. + final int l = left; + int r = right; + final int[] upper = {0}; + while (true) { + // Full sort of small data + if (r - l < minQuickSelectSize) { + Sorting.sort(a, l, r); + return; + } + if (maxDepth == 0) { + // Too much recursion + heapSort(a, l, r); + return; + } + + // Pick a pivot and partition + final int p0 = part.partition(a, l, r, + pivotingStrategy.pivotIndex(a, l, r, l), + upper); + final int p1 = upper[0]; + + // Recurse right side + introsort(part, a, p1 + 1, r, --maxDepth); + // Continue on the left side + r = p0 - 1; + } + } + + /** + * Sort the data using an introsort. + * + *

Uses a dual-pivot quicksort method; falling back + * to heapsort when quicksort recursion is slow. + * + * @param data Values. + */ + void sortIDP(double[] data) { + // NaN processing is done in the introsort method + introsort((DPPartition) Partition::partitionDP, data); + } + + /** + * Sort the array using an introsort. The dual-pivot partition method is provided as an argument. + * Switches to heapsort when recursive partitioning reaches a maximum depth. + * + *

The partition method is not required to handle signed zeros. + * + * @param part Partition function. + * @param a Values. + * @see Introsort (Wikipedia) + */ + private void introsort(DPPartition part, double[] a) { + // Handle NaN / signed zeros + final DoubleDataTransformer t = SORT_TRANSFORMER.get(); + // Assume this is in-place + t.preProcess(a); + final int end = t.length(); + if (end > 1) { + introsort(part, a, 0, end - 1, createMaxDepthDualPivot(end)); + } + // Restore signed zeros + t.postProcess(a); + } + + /** + * Sort the array. + * + *

Uses an introsort. The dual-pivot partition method is provided as an argument. + * + * @param part Partition function. + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param maxDepth Maximum depth for recursion. + * @see Introsort (Wikipedia) + */ + private void introsort(DPPartition part, double[] a, int left, int right, int maxDepth) { + // Only two regions require recursion. The third region + // can remain within this function call. + final int l = left; + int r = right; + final int[] upper = {0, 0, 0}; + while (true) { + // Full sort of small data + if (r - l < minQuickSelectSize) { + Sorting.sort(a, l, r); + return; + } + if (maxDepth == 0) { + // Too much recursion + heapSort(a, l, r); + return; + } + + // Pick 2 pivots and partition + int p0 = dualPivotingStrategy.pivotIndex(a, l, r, upper); + p0 = part.partition(a, l, r, p0, upper[0], upper); + final int p1 = upper[0]; + final int p2 = upper[1]; + final int p3 = upper[2]; + + // Recurse middle and right sides + --maxDepth; + introsort(part, a, p3 + 1, r, maxDepth); + introsort(part, a, p1 + 1, p2 - 1, maxDepth); + // Continue on the left side + r = p0 - 1; + } + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

All indices are assumed to be within {@code [0, right]}. + * + *

Uses an introselect variant. The single-pivot quickselect is provided as an argument; + * the fall-back on poor convergence of the quickselect is controlled by + * current configuration. + * + *

The partition method is not required to handle signed zeros. + * + * @param part Partition function. + * @param a Values. + * @param k Indices (may be destructively modified). + * @param count Count of indices (assumed to be strictly positive). + */ + private void introselect(SPEPartition part, double[] a, int[] k, int count) { + // Handle NaN / signed zeros + final DoubleDataTransformer t = SORT_TRANSFORMER.get(); + // Assume this is in-place + t.preProcess(a); + final int end = t.length(); + int n = count; + if (end > 1) { + // Filter indices invalidated by NaN check + if (end < a.length) { + for (int i = n; --i >= 0;) { + final int v = k[i]; + if (v >= end) { + // swap(k, i, --n) + k[i] = k[--n]; + k[n] = v; + } + } + } + introselect(part, a, end - 1, k, n); + } + // Restore signed zeros + t.postProcess(a, k, n); + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

All indices are assumed to be within {@code [0, right]}. + * + *

Uses an introselect variant. The quickselect is provided as an argument; + * the fall-back on poor convergence of the quickselect is controlled by + * current configuration. + * + * @param part Partition function. + * @param a Values. + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param k Indices (may be destructively modified). + * @param n Count of indices (assumed to be strictly positive). + */ + private void introselect(SPEPartition part, double[] a, int right, int[] k, int n) { + if (n < 1) { + return; + } + final int maxDepth = createMaxDepthSinglePivot(right + 1); + // Handle cases without multiple keys + if (n == 1) { + // Dedicated methods for a single key. These use different strategies + // to trigger the stopper on quickselect recursion + if (pairedKeyStrategy == PairedKeyStrategy.PAIRED_KEYS) { + introselect(part, a, 0, right, k[0], maxDepth); + } else if (pairedKeyStrategy == PairedKeyStrategy.PAIRED_KEYS_2) { + // This uses the configured recursion constant c. + // The length must halve every c iterations. + introselect2(part, a, 0, right, k[0]); + } else if (pairedKeyStrategy == PairedKeyStrategy.PAIRED_KEYS_LEN) { + introselect(part, a, 0, right, k[0]); + } else if (pairedKeyStrategy == PairedKeyStrategy.TWO_KEYS) { + // Dedicated method for two separate keys using the same key + introselect(part, a, 0, right, k[0], k[0], maxDepth); + } else if (pairedKeyStrategy == PairedKeyStrategy.KEY_RANGE) { + // Dedicated method for a range of keys using the same key + introselect2(part, a, 0, right, k[0], k[0]); + } else if (pairedKeyStrategy == PairedKeyStrategy.SEARCHABLE_INTERVAL) { + // Reuse the SearchableInterval method using the same key + introselect(part, a, 0, right, IndexIntervals.anyIndex(), k[0], k[0], maxDepth); + } else if (pairedKeyStrategy == PairedKeyStrategy.UPDATING_INTERVAL) { + // Reuse the UpdatingInterval method using a single key + introselect(part, a, 0, right, IndexIntervals.interval(k[0]), maxDepth); + } else { + throw new IllegalStateException(UNSUPPORTED_INTROSELECT + pairedKeyStrategy); + } + return; + } + // Special case for partition around adjacent indices (for interpolation) + if (n == 2 && k[0] + 1 == k[1]) { + // Dedicated method for a single key, returns information about k+1 + if (pairedKeyStrategy == PairedKeyStrategy.PAIRED_KEYS) { + final int p = introselect(part, a, 0, right, k[0], maxDepth); + // p <= k to signal k+1 is unsorted, or p+1 is a pivot. + // if k is sorted, and p+1 is sorted, k+1 is sorted if k+1 == p. + if (p > k[1]) { + selectMinIgnoreZeros(a, k[1], p); + } + } else if (pairedKeyStrategy == PairedKeyStrategy.PAIRED_KEYS_2) { + final int p = introselect2(part, a, 0, right, k[0]); + if (p > k[1]) { + selectMinIgnoreZeros(a, k[1], p); + } + } else if (pairedKeyStrategy == PairedKeyStrategy.PAIRED_KEYS_LEN) { + final int p = introselect(part, a, 0, right, k[0]); + if (p > k[1]) { + selectMinIgnoreZeros(a, k[1], p); + } + } else if (pairedKeyStrategy == PairedKeyStrategy.TWO_KEYS) { + // Dedicated method for two separate keys + // Note: This can handle keys that are not adjacent + // e.g. keys near opposite ends without a partition step. + final int ka = Math.min(k[0], k[1]); + final int kb = Math.max(k[0], k[1]); + introselect(part, a, 0, right, ka, kb, maxDepth); + } else if (pairedKeyStrategy == PairedKeyStrategy.KEY_RANGE) { + // Dedicated method for a range of keys using the same key + final int ka = Math.min(k[0], k[1]); + final int kb = Math.max(k[0], k[1]); + introselect2(part, a, 0, right, ka, kb); + } else if (pairedKeyStrategy == PairedKeyStrategy.SEARCHABLE_INTERVAL) { + // Reuse the SearchableInterval method using a range of two keys + introselect(part, a, 0, right, IndexIntervals.anyIndex(), k[0], k[1], maxDepth); + } else if (pairedKeyStrategy == PairedKeyStrategy.UPDATING_INTERVAL) { + // Reuse the UpdatingInterval method using a range of two keys + introselect(part, a, 0, right, IndexIntervals.interval(k[0], k[1]), maxDepth); + } else { + throw new IllegalStateException(UNSUPPORTED_INTROSELECT + pairedKeyStrategy); + } + return; + } + + // Note: Sorting to unique keys is an overhead. This can be eliminated + // by requesting the caller passes sorted keys. + + // Note: Attempts to perform key analysis here to detect a full sort + // add an overhead for sparse keys and do not increase performance + // for saturated keys unless data is structured with ascending/descending + // runs so that it is fast with JDK's merge sort algorithm in Arrays.sort. + + if (keyStrategy == KeyStrategy.ORDERED_KEYS) { + final int unique = Sorting.sortIndices(k, n); + introselect(part, a, 0, right, k, 0, unique - 1, maxDepth); + } else if (keyStrategy == KeyStrategy.SCANNING_KEY_SEARCHABLE_INTERVAL) { + final int unique = Sorting.sortIndices(k, n); + final SearchableInterval keys = ScanningKeyInterval.of(k, unique); + introselect(part, a, 0, right, keys, keys.left(), keys.right(), maxDepth); + } else if (keyStrategy == KeyStrategy.SEARCH_KEY_SEARCHABLE_INTERVAL) { + final int unique = Sorting.sortIndices(k, n); + final SearchableInterval keys = BinarySearchKeyInterval.of(k, unique); + introselect(part, a, 0, right, keys, keys.left(), keys.right(), maxDepth); + } else if (keyStrategy == KeyStrategy.COMPRESSED_INDEX_SET) { + final SearchableInterval keys = CompressedIndexSet.of(compression, k, n); + introselect(part, a, 0, right, keys, keys.left(), keys.right(), maxDepth); + } else if (keyStrategy == KeyStrategy.INDEX_SET) { + final SearchableInterval keys = IndexSet.of(k, n); + introselect(part, a, 0, right, keys, keys.left(), keys.right(), maxDepth); + } else if (keyStrategy == KeyStrategy.KEY_UPDATING_INTERVAL) { + final int unique = Sorting.sortIndices(k, n); + final UpdatingInterval keys = KeyUpdatingInterval.of(k, unique); + introselect(part, a, 0, right, keys, maxDepth); + } else if (keyStrategy == KeyStrategy.INDEX_SET_UPDATING_INTERVAL) { + final UpdatingInterval keys = BitIndexUpdatingInterval.of(k, n); + introselect(part, a, 0, right, keys, maxDepth); + } else if (keyStrategy == KeyStrategy.KEY_SPLITTING_INTERVAL) { + final int unique = Sorting.sortIndices(k, n); + final SplittingInterval keys = KeyUpdatingInterval.of(k, unique); + introselect(part, a, 0, right, keys, maxDepth); + } else if (keyStrategy == KeyStrategy.INDEX_SET_SPLITTING_INTERVAL) { + final SplittingInterval keys = BitIndexUpdatingInterval.of(k, n); + introselect(part, a, 0, right, keys, maxDepth); + } else if (keyStrategy == KeyStrategy.INDEX_ITERATOR) { + final int unique = Sorting.sortIndices(k, n); + final IndexIterator keys = KeyIndexIterator.of(k, unique); + introselect(part, a, 0, right, keys, keys.left(), keys.right(), maxDepth); + } else if (keyStrategy == KeyStrategy.COMPRESSED_INDEX_ITERATOR) { + final IndexIterator keys = CompressedIndexSet.iterator(compression, k, n); + introselect(part, a, 0, right, keys, keys.left(), keys.right(), maxDepth); + } else { + throw new IllegalStateException(UNSUPPORTED_INTROSELECT + keyStrategy); + } + } + + /** + * Partition the array such that index {@code k} corresponds to its + * correctly sorted value in the equivalent fully sorted array. + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

Uses an introselect variant. The quickselect is provided as an argument; + * the fall-back on poor convergence of the quickselect is controlled by + * current configuration. + * + *

Returns information {@code p} on whether {@code k+1} is sorted. + * If {@code p <= k} then {@code k+1} is sorted. + * If {@code p > k} then {@code p+1} is a pivot. + * + * @param part Partition function. + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param k Index. + * @param maxDepth Maximum depth for recursion. + * @return the index {@code p} + */ + private int introselect(SPEPartition part, double[] a, int left, int right, + int k, int maxDepth) { + int l = left; + int r = right; + final int[] upper = {0}; + while (true) { + // It is possible to use edgeselect when k is close to the end + // |l|-----|k|---------|k|--------|r| + // ---d1---- + // -----d2---- + final int d1 = k - l; + final int d2 = r - k; + if (Math.min(d1, d2) < edgeSelectConstant) { + edgeSelection.partition(a, l, r, k, k); + // Last known unsorted value >= k + return r; + } + + if (maxDepth == 0) { + // Too much recursion + // Note: For testing the Floyd-Rivest algorithm we trigger the recursion + // consumer as a signal that FR failed due to a non-representative sample. + recursionConsumer.accept(maxDepth); + stopperSelection.partition(a, l, r, k, k); + // Last known unsorted value >= k + return r; + } + + // Pick a pivot and partition + int pivot; + // length - 1 + int n = r - l; + if (n > subSamplingSize) { + // Floyd-Rivest: use SELECT recursively on a sample of size S to get an estimate + // for the (k-l+1)-th smallest element into a[k], biased slightly so that the + // (k-l+1)-th element is expected to lie in the smaller set after partitioning. + ++n; + final int ith = k - l + 1; + final double z = Math.log(n); + final double s = 0.5 * Math.exp(0.6666666666666666 * z); + final double sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * Integer.signum(ith - (n >> 1)); + final int ll = Math.max(l, (int) (k - ith * s / n + sd)); + final int rr = Math.min(r, (int) (k + (n - ith) * s / n + sd)); + // Optional random sampling + if ((controlFlags & FLAG_RANDOM_SAMPLING) != 0) { + final IntUnaryOperator rng = createRNG(n, k); + // Shuffle [ll, k) from [l, k) + if (ll > l) { + for (int i = k; i > ll;) { + // l + rand [0, i - l + 1) : i is currently i+1 + final int j = l + rng.applyAsInt(i - l); + final double t = a[--i]; + a[i] = a[j]; + a[j] = t; + } + } + // Shuffle (k, rr] from (k, r] + if (rr < r) { + for (int i = k; i < rr;) { + // r - rand [0, r - i + 1) : i is currently i-1 + final int j = r - rng.applyAsInt(r - i); + final double t = a[++i]; + a[i] = a[j]; + a[j] = t; + } + } + } + introselect(part, a, ll, rr, k, lnNtoMaxDepthSinglePivot(z)); + pivot = k; + } else { + // default pivot strategy + pivot = pivotingStrategy.pivotIndex(a, l, r, k); + } + + final int p0 = part.partition(a, l, r, pivot, upper); + final int p1 = upper[0]; + + maxDepth--; + if (k < p0) { + // The element is in the left partition + r = p0 - 1; + } else if (k > p1) { + // The element is in the right partition + l = p1 + 1; + } else { + // The range contains the element we wanted. + // Signal if k+1 is sorted. + // This can be true if the pivot was a range [p0, p1] + return k < p1 ? k : r; + } + } + } + + /** + * Partition the array such that index {@code k} corresponds to its + * correctly sorted value in the equivalent fully sorted array. + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

Uses an introselect variant. The quickselect is provided as an argument; + * the fall-back on poor convergence of the quickselect is controlled by + * current configuration. + * + *

Returns information {@code p} on whether {@code k+1} is sorted. + * If {@code p <= k} then {@code k+1} is sorted. + * If {@code p > k} then {@code p+1} is a pivot. + * + *

Recursion is monitored by checking the partition is reduced by 2-x after + * {@code c} iterations where {@code x} is the + * {@link #setRecursionConstant(int) recursion constant} and {@code c} is the + * {@link #setRecursionMultiple(double) recursion multiple} (variables reused for convenience). + * Confidence bounds for dividing a length by 2-x are provided in Valois (2000) + * as {@code c = floor((6/5)x) + b}: + *

+     * b  confidence (%)
+     * 2  76.56
+     * 3  92.92
+     * 4  97.83
+     * 5  99.33
+     * 6  99.79
+     * 
+ *

Ideally {@code c >= 3} using {@code x = 1}. E.g. We can use 3 iterations to be 76% + * confident the sequence will divide in half; or 7 iterations to be 99% confident the + * sequence will divide into a quarter. A larger factor {@code b} reduces the sensitivity + * of introspection. + * + * @param part Partition function. + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param k Index. + * @return the index {@code p} + */ + private int introselect2(SPEPartition part, double[] a, int left, int right, int k) { + int l = left; + int r = right; + final int[] upper = {0}; + int counter = (int) recursionMultiple; + int threshold = (right - left) >>> recursionConstant; + int depth = singlePivotMaxDepth(right - left); + while (true) { + // It is possible to use edgeselect when k is close to the end + // |l|-----|k|---------|k|--------|r| + // ---d1---- + // -----d2---- + final int d1 = k - l; + final int d2 = r - k; + if (Math.min(d1, d2) < edgeSelectConstant) { + edgeSelection.partition(a, l, r, k, k); + // Last known unsorted value >= k + return r; + } + + // length - 1 + int n = r - l; + depth--; + if (--counter < 0) { + if (n > threshold) { + // Did not reduce the length after set number of iterations. + // Here riselect (Valois (2000)) would use random points to choose the pivot + // to inject entropy and restart. This continues until the sum of the partition + // lengths is too high (twice the original length). Here we just switch. + + // Note: For testing we trigger the recursion consumer + recursionConsumer.accept(depth); + stopperSelection.partition(a, l, r, k, k); + // Last known unsorted value >= k + return r; + } + // Once the confidence has been achieved we use (6/5)x with x=1. + // So check every 5/6 iterations that the length is halving. + if (counter == -5) { + counter = 1; + } + threshold >>>= 1; + } + + // Pick a pivot and partition + int pivot; + if (n > subSamplingSize) { + // Floyd-Rivest: use SELECT recursively on a sample of size S to get an estimate + // for the (k-l+1)-th smallest element into a[k], biased slightly so that the + // (k-l+1)-th element is expected to lie in the smaller set after partitioning. + ++n; + final int ith = k - l + 1; + final double z = Math.log(n); + final double s = 0.5 * Math.exp(0.6666666666666666 * z); + final double sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * Integer.signum(ith - (n >> 1)); + final int ll = Math.max(l, (int) (k - ith * s / n + sd)); + final int rr = Math.min(r, (int) (k + (n - ith) * s / n + sd)); + // Optional random sampling + if ((controlFlags & FLAG_RANDOM_SAMPLING) != 0) { + final IntUnaryOperator rng = createRNG(n, k); + // Shuffle [ll, k) from [l, k) + if (ll > l) { + for (int i = k; i > ll;) { + // l + rand [0, i - l + 1) : i is currently i+1 + final int j = l + rng.applyAsInt(i - l); + final double t = a[--i]; + a[i] = a[j]; + a[j] = t; + } + } + // Shuffle (k, rr] from (k, r] + if (rr < r) { + for (int i = k; i < rr;) { + // r - rand [0, r - i + 1) : i is currently i-1 + final int j = r - rng.applyAsInt(r - i); + final double t = a[++i]; + a[i] = a[j]; + a[j] = t; + } + } + } + // Sample recursion restarts from [ll, rr] + introselect2(part, a, ll, rr, k); + pivot = k; + } else { + // default pivot strategy + pivot = pivotingStrategy.pivotIndex(a, l, r, k); + } + + final int p0 = part.partition(a, l, r, pivot, upper); + final int p1 = upper[0]; + + if (k < p0) { + // The element is in the left partition + r = p0 - 1; + } else if (k > p1) { + // The element is in the right partition + l = p1 + 1; + } else { + // The range contains the element we wanted. + // Signal if k+1 is sorted. + // This can be true if the pivot was a range [p0, p1] + return k < p1 ? k : r; + } + } + } + + /** + * Partition the array such that index {@code k} corresponds to its + * correctly sorted value in the equivalent fully sorted array. + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

Uses an introselect variant. The quickselect is provided as an argument; + * the fall-back on poor convergence of the quickselect is controlled by + * current configuration. + * + *

Returns information {@code p} on whether {@code k+1} is sorted. + * If {@code p <= k} then {@code k+1} is sorted. + * If {@code p > k} then {@code p+1} is a pivot. + * + *

Recursion is monitored by checking the sum of partition lengths is less than + * {@code m * (r - l)} where {@code m} is the + * {@link #setRecursionMultiple(double) recursion multiple}. + * Ideally {@code c} should be a value above 1. + * + * @param part Partition function. + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param k Index. + * @return the index {@code p} + */ + private int introselect(SPEPartition part, double[] a, int left, int right, int k) { + int l = left; + int r = right; + final int[] upper = {0}; + // Set the limit on the sum of the length. Since the length is subtracted at the start + // of the loop use (1 + recursionMultiple). + long limit = (long) ((1 + recursionMultiple) * (right - left)); + int depth = singlePivotMaxDepth(right - left); + while (true) { + // It is possible to use edgeselect when k is close to the end + // |l|-----|k|---------|k|--------|r| + // ---d1---- + // -----d2---- + final int d1 = k - l; + final int d2 = r - k; + if (Math.min(d1, d2) < edgeSelectConstant) { + edgeSelection.partition(a, l, r, k, k); + // Last known unsorted value >= k + return r; + } + + // length - 1 + int n = r - l; + limit -= n; + depth--; + + if (limit < 0) { + // Excess total partition length + // Note: For testing we trigger the recursion consumer + recursionConsumer.accept(depth); + stopperSelection.partition(a, l, r, k, k); + // Last known unsorted value >= k + return r; + } + + // Pick a pivot and partition + int pivot; + if (n > subSamplingSize) { + // Floyd-Rivest: use SELECT recursively on a sample of size S to get an estimate + // for the (k-l+1)-th smallest element into a[k], biased slightly so that the + // (k-l+1)-th element is expected to lie in the smaller set after partitioning. + ++n; + final int ith = k - l + 1; + final double z = Math.log(n); + final double s = 0.5 * Math.exp(0.6666666666666666 * z); + final double sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * Integer.signum(ith - (n >> 1)); + final int ll = Math.max(l, (int) (k - ith * s / n + sd)); + final int rr = Math.min(r, (int) (k + (n - ith) * s / n + sd)); + // Optional random sampling + if ((controlFlags & FLAG_RANDOM_SAMPLING) != 0) { + final IntUnaryOperator rng = createRNG(n, k); + // Shuffle [ll, k) from [l, k) + if (ll > l) { + for (int i = k; i > ll;) { + // l + rand [0, i - l + 1) : i is currently i+1 + final int j = l + rng.applyAsInt(i - l); + final double t = a[--i]; + a[i] = a[j]; + a[j] = t; + } + } + // Shuffle (k, rr] from (k, r] + if (rr < r) { + for (int i = k; i < rr;) { + // r - rand [0, r - i + 1) : i is currently i-1 + final int j = r - rng.applyAsInt(r - i); + final double t = a[++i]; + a[i] = a[j]; + a[j] = t; + } + } + } + // Sample recursion restarts from [ll, rr] + introselect(part, a, ll, rr, k); + pivot = k; + } else { + // default pivot strategy + pivot = pivotingStrategy.pivotIndex(a, l, r, k); + } + + final int p0 = part.partition(a, l, r, pivot, upper); + final int p1 = upper[0]; + + if (k < p0) { + // The element is in the left partition + r = p0 - 1; + } else if (k > p1) { + // The element is in the right partition + l = p1 + 1; + } else { + // The range contains the element we wanted. + // Signal if k+1 is sorted. + // This can be true if the pivot was a range [p0, p1] + return k < p1 ? k : r; + } + } + } + + /** + * Partition the array such that indices {@code ka} and {@code kb} correspond to their + * correctly sorted value in the equivalent fully sorted array. + * + *

For all indices {@code k} and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

Note: Requires {@code ka <= kb}. The use of two indices is to support processing + * of pairs of indices {@code (k, k+1)}. However the indices are treated independently + * and partitioned by recursion. They may be equal, neighbours or well separated. + * + *

Uses an introselect variant. The quickselect is provided as an argument; the + * fall-back on poor convergence of the quickselect is a heapselect. + * + * @param part Partition function. + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param ka Index. + * @param kb Index. + * @param maxDepth Maximum depth for recursion. + */ + private void introselect(SPEPartition part, double[] a, int left, int right, + int ka, int kb, int maxDepth) { + // Only one side requires recursion. The other side + // can remain within this function call. + int l = left; + int r = right; + int ka1 = ka; + int kb1 = kb; + final int[] upper = {0}; + while (true) { + // length - 1 + final int n = r - l; + + if (n < minQuickSelectSize) { + // Sort selection on small data + sortSelectRange(a, l, r, ka1, kb1); + return; + } + + // It is possible to use heapselect when ka1 and kb1 are close to the ends + // |l|-----|ka1|--------|kb1|------|r| + // ---d1---- + // -----d3---- + // ---------d2----------- + // ----------d4----------- + final int d1 = ka1 - l; + final int d2 = kb1 - l; + final int d3 = r - kb1; + final int d4 = r - ka1; + if (maxDepth == 0 || + Math.min(d1 + d3, Math.min(d2, d4)) < edgeSelectConstant) { + // Too much recursion, or ka1 and kb1 are both close to the ends + // Note: Does not use the edgeSelection function as the indices are not a range + heapSelectPair(a, l, r, ka1, kb1); + return; + } + + // Pick a pivot and partition + final int p0 = part.partition(a, l, r, + pivotingStrategy.pivotIndex(a, l, r, ka), + upper); + final int p1 = upper[0]; + + // Recursion to max depth + // Note: Here we possibly branch left and right with multiple keys. + // It is possible that the partition has split the pair + // and the recursion proceeds with a single point. + maxDepth--; + // Recurse left side if required + if (ka1 < p0) { + if (kb1 <= p1) { + // Entirely on left side + r = p0 - 1; + kb1 = r < kb1 ? ka1 : kb1; + continue; + } + introselect(part, a, l, p0 - 1, ka1, ka1, maxDepth); + ka1 = kb1; + } + if (kb1 <= p1) { + // No right side + return; + } + // Continue on the right side + l = p1 + 1; + ka1 = ka1 < l ? kb1 : ka1; + } + } + + /** + * Partition the array such that index {@code k} corresponds to its + * correctly sorted value in the equivalent fully sorted array. + * + *

For all indices {@code [ka, kb]} and any index {@code i}: + * + *

{@code
+     * data[i < ka] <= data[ka] <= data[kb] <= data[kb < i]
+     * }
+ * + *

This function accepts indices {@code [ka, kb]} that define the + * range of indices to partition. It is expected that the range is small. + * + *

Uses an introselect variant. The quickselect is provided as an argument; + * the fall-back on poor convergence of the quickselect is controlled by + * current configuration. + * + *

Recursion is monitored by checking the partition is reduced by 2-x after + * {@code c} iterations where {@code x} is the + * {@link #setRecursionConstant(int) recursion constant} and {@code c} is the + * {@link #setRecursionMultiple(double) recursion multiple} (variables reused for convenience). + * Confidence bounds for dividing a length by 2-x are provided in Valois (2000) + * as {@code c = floor((6/5)x) + b}: + *

+     * b  confidence (%)
+     * 2  76.56
+     * 3  92.92
+     * 4  97.83
+     * 5  99.33
+     * 6  99.79
+     * 
+ *

Ideally {@code c >= 3} using {@code x = 1}. E.g. We can use 3 iterations to be 76% + * confident the sequence will divide in half; or 7 iterations to be 99% confident the + * sequence will divide into a quarter. A larger factor {@code b} reduces the sensitivity + * of introspection. + * + * @param part Partition function. + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param ka First key of interest. + * @param kb Last key of interest. + */ + private void introselect2(SPEPartition part, double[] a, int left, int right, int ka, int kb) { + int l = left; + int r = right; + final int[] upper = {0}; + int counter = (int) recursionMultiple; + int threshold = (right - left) >>> recursionConstant; + while (true) { + // It is possible to use edgeselect when k is close to the end + // |l|-----|ka|kkkkkkkk|kb|------|r| + if (Math.min(kb - l, r - ka) < edgeSelectConstant) { + edgeSelection.partition(a, l, r, ka, kb); + return; + } + + // length - 1 + int n = r - l; + if (--counter < 0) { + if (n > threshold) { + // Did not reduce the length after set number of iterations. + // Here riselect (Valois (2000)) would use random points to choose the pivot + // to inject entropy and restart. This continues until the sum of the partition + // lengths is too high (twice the original length). Here we just switch. + + // Note: For testing we trigger the recursion consumer with the remaining length + recursionConsumer.accept(r - l); + stopperSelection.partition(a, l, r, ka, kb); + return; + } + // Once the confidence has been achieved we use (6/5)x with x=1. + // So check every 5/6 iterations that the length is halving. + if (counter == -5) { + counter = 1; + } + threshold >>>= 1; + } + + // Pick a pivot and partition + int pivot; + if (n > subSamplingSize) { + // Floyd-Rivest: use SELECT recursively on a sample of size S to get an estimate + // for the (k-l+1)-th smallest element into a[k], biased slightly so that the + // (k-l+1)-th element is expected to lie in the smaller set after partitioning. + ++n; + final int ith = ka - l + 1; + final double z = Math.log(n); + final double s = 0.5 * Math.exp(0.6666666666666666 * z); + final double sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * Integer.signum(ith - (n >> 1)); + final int ll = Math.max(l, (int) (ka - ith * s / n + sd)); + final int rr = Math.min(r, (int) (ka + (n - ith) * s / n + sd)); + // Optional random sampling + if ((controlFlags & FLAG_RANDOM_SAMPLING) != 0) { + final IntUnaryOperator rng = createRNG(n, ka); + // Shuffle [ll, k) from [l, k) + if (ll > l) { + for (int i = ka; i > ll;) { + // l + rand [0, i - l + 1) : i is currently i+1 + final int j = l + rng.applyAsInt(i - l); + final double t = a[--i]; + a[i] = a[j]; + a[j] = t; + } + } + // Shuffle (k, rr] from (k, r] + if (rr < r) { + for (int i = ka; i < rr;) { + // r - rand [0, r - i + 1) : i is currently i-1 + final int j = r - rng.applyAsInt(r - i); + final double t = a[++i]; + a[i] = a[j]; + a[j] = t; + } + } + } + // Sample recursion restarts from [ll, rr] + introselect2(part, a, ll, rr, ka, ka); + pivot = ka; + } else { + // default pivot strategy + pivot = pivotingStrategy.pivotIndex(a, l, r, ka); + } + + final int p0 = part.partition(a, l, r, pivot, upper); + final int p1 = upper[0]; + + // Note: Here we expect [ka, kb] to be small and splitting is unlikely. + // p0 p1 + // |l|--|ka|kkkk|kb|--|P|-------------------|r| + // |l|----------------|P|--|ka|kkk|kb|------|r| + // |l|-----------|ka|k|P|k|kb|--------------|r| + if (kb < p0) { + // The element is in the left partition + r = p0 - 1; + } else if (ka > p1) { + // The element is in the right partition + l = p1 + 1; + } else { + // Pivot splits [ka, kb]. Expect ends to be close to the pivot and finish. + if (ka < p0) { + sortSelectRight(a, l, p0 - 1, ka); + } + if (kb > p1) { + sortSelectLeft(a, p1 + 1, r, kb); + } + return; + } + } + } + + /** + * Partition the array such that indices {@code k} correspond to their + * correctly sorted value in the equivalent fully sorted array. + * + *

For all indices {@code k} and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

This function accepts an ordered array of indices {@code k} and pointers + * to the first and last positions in {@code k} that define the range indices + * to partition. + * + *

{@code
+     * left <= k[ia] <= k[ib] <= right  : ia <= ib
+     * }
+ * + *

A binary search is used to search for keys in {@code [ia, ib]} + * to create {@code [ia, ib1]} and {@code [ia1, ib]} if partitioning splits the range. + * + *

Uses an introselect variant. The quickselect is provided as an argument; + * the fall-back on poor convergence of the quickselect is a heapselect. + * + * @param part Partition function. + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param k Indices to partition (ordered). + * @param ia Index of first key. + * @param ib Index of last key. + * @param maxDepth Maximum depth for recursion. + */ + private void introselect(SPEPartition part, double[] a, int left, int right, + int[] k, int ia, int ib, int maxDepth) { + // Only one side requires recursion. The other side + // can remain within this function call. + int l = left; + int r = right; + int ia1 = ia; + int ib1 = ib; + final int[] upper = {0}; + while (true) { + // Switch to paired key implementation if possible. + // Note: adjacent indices can refer to well separated keys. + // This is the major difference between this implementation + // and an implementation using an IndexInterval (which does not + // have a fast way to determine if there are any keys within the range). + if (ib1 - ia1 <= 1) { + introselect(part, a, l, r, k[ia1], k[ib1], maxDepth); + return; + } + + // length - 1 + final int n = r - l; + int ka = k[ia1]; + final int kb = k[ib1]; + + if (n < minQuickSelectSize) { + // Sort selection on small data + sortSelectRange(a, l, r, ka, kb); + return; + } + + // It is possible to use heapselect when ka and kb are close to the same end + // |l|-----|ka|--------|kb|------|r| + // ---------s2---------- + // ----------s4----------- + if (Math.min(kb - l, r - ka) < edgeSelectConstant) { + edgeSelection.partition(a, l, r, ka, kb); + return; + } + + if (maxDepth == 0) { + // Too much recursion + heapSelectRange(a, l, r, ka, kb); + return; + } + + // Pick a pivot and partition + final int p0 = part.partition(a, l, r, + pivotingStrategy.pivotIndex(a, l, r, ka), + upper); + final int p1 = upper[0]; + + // Recursion to max depth + // Note: Here we possibly branch left and right with multiple keys. + // It is possible that the partition has split the keys + // and the recursion proceeds with a reduced set on either side. + // p0 p1 + // |l|--|ka|--k----k--|P|------k--|kb|------|r| + // ia1 iba | ia1 ib1 + // Search less/greater is bounded at ia1/ib1 + maxDepth--; + // Recurse left side if required + if (ka < p0) { + if (kb <= p1) { + // Entirely on left side + r = p0 - 1; + if (r < kb) { + ib1 = searchLessOrEqual(k, ia1, ib1, r); + } + continue; + } + // Require a split here + introselect(part, a, l, p0 - 1, k, ia1, searchLessOrEqual(k, ia1, ib1, p0 - 1), maxDepth); + ia1 = searchGreaterOrEqual(k, ia1, ib1, l); + ka = k[ia1]; + } + if (kb <= p1) { + // No right side + recursionConsumer.accept(maxDepth); + return; + } + // Continue on the right side + l = p1 + 1; + if (ka < l) { + ia1 = searchGreaterOrEqual(k, ia1, ib1, l); + } + } + } + + /** + * Partition the array such that indices {@code k} correspond to their + * correctly sorted value in the equivalent fully sorted array. + * + *

For all indices {@code k} and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

This function accepts a {@link SearchableInterval} of indices {@code k} and the + * first index {@code ka} and last index {@code kb} that define the range of indices + * to partition. The {@link SearchableInterval} is used to search for keys in {@code [ka, kb]} + * to create {@code [ka, kb1]} and {@code [ka1, kb]} if partitioning splits the range. + * + *

{@code
+     * left <= ka <= kb <= right
+     * }
+ * + *

Uses an introselect variant. The quickselect is provided as an argument; + * the fall-back on poor convergence of the quickselect is a heapselect. + * + * @param part Partition function. + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param k Interval of indices to partition (ordered). + * @param ka First key. + * @param kb Last key. + * @param maxDepth Maximum depth for recursion. + */ + // package-private for benchmarking + void introselect(SPEPartition part, double[] a, int left, int right, + SearchableInterval k, int ka, int kb, int maxDepth) { + // Only one side requires recursion. The other side + // can remain within this function call. + int l = left; + int r = right; + int ka1 = ka; + int kb1 = kb; + final int[] upper = {0}; + while (true) { + // length - 1 + int n = r - l; + + if (n < minQuickSelectSize) { + // Sort selection on small data + sortSelectRange(a, l, r, ka1, kb1); + recursionConsumer.accept(maxDepth); + return; + } + + // It is possible to use heapselect when kaa and kb1 are close to the same end + // |l|-----|ka1|--------|kb1|------|r| + // ---------s2---------- + // ----------s4----------- + if (Math.min(kb1 - l, r - ka1) < edgeSelectConstant) { + edgeSelection.partition(a, l, r, ka1, kb1); + recursionConsumer.accept(maxDepth); + return; + } + + if (maxDepth == 0) { + // Too much recursion + heapSelectRange(a, l, r, ka1, kb1); + recursionConsumer.accept(maxDepth); + return; + } + + // Pick a pivot and partition + int pivot; + if (n > subSamplingSize) { + // Floyd-Rivest: use SELECT recursively on a sample of size S to get an estimate + // for the (k-l+1)-th smallest element into a[k], biased slightly so that the + // (k-l+1)-th element is expected to lie in the smaller set after partitioning. + // Note: This targets ka1 and ignores kb1 for pivot selection. + ++n; + final int ith = ka1 - l + 1; + final double z = Math.log(n); + final double s = 0.5 * Math.exp(0.6666666666666666 * z); + final double sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * Integer.signum(ith - (n >> 1)); + final int ll = Math.max(l, (int) (ka1 - ith * s / n + sd)); + final int rr = Math.min(r, (int) (ka1 + (n - ith) * s / n + sd)); + // Optional random sampling + if ((controlFlags & FLAG_RANDOM_SAMPLING) != 0) { + final IntUnaryOperator rng = createRNG(n, ka1); + // Shuffle [ll, k) from [l, k) + if (ll > l) { + for (int i = ka1; i > ll;) { + // l + rand [0, i - l + 1) : i is currently i+1 + final int j = l + rng.applyAsInt(i - l); + final double t = a[--i]; + a[i] = a[j]; + a[j] = t; + } + } + // Shuffle (k, rr] from (k, r] + if (rr < r) { + for (int i = ka1; i < rr;) { + // r - rand [0, r - i + 1) : i is currently i-1 + final int j = r - rng.applyAsInt(r - i); + final double t = a[++i]; + a[i] = a[j]; + a[j] = t; + } + } + } + introselect(part, a, ll, rr, k, ka1, ka1, lnNtoMaxDepthSinglePivot(z)); + pivot = ka1; + } else { + // default pivot strategy + pivot = pivotingStrategy.pivotIndex(a, l, r, ka1); + } + + final int p0 = part.partition(a, l, r, pivot, upper); + final int p1 = upper[0]; + + // Recursion to max depth + // Note: Here we possibly branch left and right with multiple keys. + // It is possible that the partition has split the keys + // and the recursion proceeds with a reduced set on either side. + // p0 p1 + // |l|--|ka1|--k----k--|P|------k--|kb1|------|r| + // kb1 | ka1 + // Search previous/next is bounded at ka1/kb1 + maxDepth--; + // Recurse left side if required + if (ka1 < p0) { + if (kb1 <= p1) { + // Entirely on left side + r = p0 - 1; + if (r < kb1) { + kb1 = k.previousIndex(r); + } + continue; + } + introselect(part, a, l, p0 - 1, k, ka1, k.split(p0, p1, upper), maxDepth); + ka1 = upper[0]; + } + if (kb1 <= p1) { + // No right side + recursionConsumer.accept(maxDepth); + return; + } + // Continue on the right side + l = p1 + 1; + if (ka1 < l) { + ka1 = k.nextIndex(l); + } + } + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. + * + *

For all indices {@code k} and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

This function accepts a {@link UpdatingInterval} of indices {@code k} that define the + * range of indices to partition. The {@link UpdatingInterval} can be narrowed or split as + * partitioning divides the range. + * + *

Uses an introselect variant. The quickselect is provided as an argument; + * the fall-back on poor convergence of the quickselect is controlled by + * current configuration. + * + * @param part Partition function. + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param k Interval of indices to partition (ordered). + * @param maxDepth Maximum depth for recursion. + */ + // package-private for benchmarking + void introselect(SPEPartition part, double[] a, int left, int right, + UpdatingInterval k, int maxDepth) { + // Only one side requires recursion. The other side + // can remain within this function call. + int l = left; + int r = right; + int ka = k.left(); + int kb = k.right(); + final int[] upper = {0}; + while (true) { + // length - 1 + final int n = r - l; + + if (n < minQuickSelectSize) { + // Sort selection on small data + sortSelectRange(a, l, r, ka, kb); + recursionConsumer.accept(maxDepth); + return; + } + + // It is possible to use heapselect when ka and kb are close to the same end + // |l|-----|ka|--------|kb|------|r| + // ---------s2---------- + // ----------s4----------- + if (Math.min(kb - l, r - ka) < edgeSelectConstant) { + edgeSelection.partition(a, l, r, ka, kb); + recursionConsumer.accept(maxDepth); + return; + } + + if (maxDepth == 0) { + // Too much recursion + heapSelectRange(a, l, r, ka, kb); + recursionConsumer.accept(maxDepth); + return; + } + + // Pick a pivot and partition + final int p0 = part.partition(a, l, r, + pivotingStrategy.pivotIndex(a, l, r, ka), + upper); + final int p1 = upper[0]; + + // Recursion to max depth + // Note: Here we possibly branch left and right with multiple keys. + // It is possible that the partition has split the keys + // and the recursion proceeds with a reduced set on either side. + // p0 p1 + // |l|--|ka|--k----k--|P|------k--|kb|------|r| + // kb | ka + maxDepth--; + // Recurse left side if required + if (ka < p0) { + if (kb <= p1) { + // Entirely on left side + r = p0 - 1; + if (r < kb) { + kb = k.updateRight(r); + } + continue; + } + introselect(part, a, l, p0 - 1, k.splitLeft(p0, p1), maxDepth); + ka = k.left(); + } else if (kb <= p1) { + // No right side + recursionConsumer.accept(maxDepth); + return; + } else if (ka <= p1) { + ka = k.updateLeft(p1 + 1); + } + // Continue on the right side + l = p1 + 1; + } + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. + * + *

For all indices {@code k} and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

This function accepts a {@link SplittingInterval} of indices {@code k} that define the + * range of indices to partition. The {@link SplittingInterval} is split as + * partitioning divides the range. + * + *

Uses an introselect variant. The quickselect is provided as an argument; + * the fall-back on poor convergence of the quickselect is a heapselect. + * + * @param part Partition function. + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param keys Interval of indices to partition (ordered). + * @param maxDepth Maximum depth for recursion. + */ + // package-private for benchmarking + void introselect(SPEPartition part, double[] a, int left, int right, + SplittingInterval keys, int maxDepth) { + // Only one side requires recursion. The other side + // can remain within this function call. + int l = left; + int r = right; + SplittingInterval k = keys; + int ka = k.left(); + int kb = k.right(); + final int[] upper = {0}; + while (true) { + // length - 1 + final int n = r - l; + + if (n < minQuickSelectSize) { + // Sort selection on small data + sortSelectRange(a, l, r, ka, kb); + recursionConsumer.accept(maxDepth); + return; + } + + // It is possible to use heapselect when ka and kb are close to the same end + // |l|-----|ka|--------|kb|------|r| + // ---------s2---------- + // ----------s4----------- + if (Math.min(kb - l, r - ka) < edgeSelectConstant) { + edgeSelection.partition(a, l, r, ka, kb); + recursionConsumer.accept(maxDepth); + return; + } + + if (maxDepth == 0) { + // Too much recursion + heapSelectRange(a, l, r, ka, kb); + recursionConsumer.accept(maxDepth); + return; + } + + // Pick a pivot and partition + final int p0 = part.partition(a, l, r, + pivotingStrategy.pivotIndex(a, l, r, ka), + upper); + final int p1 = upper[0]; + + // Recursion to max depth + // Note: Here we possibly branch left and right with multiple keys. + // It is possible that the partition has split the keys + // and the recursion proceeds with a reduced set on either side. + // p0 p1 + // |l|--|ka|--k----k--|P|------k--|kb|------|r| + // kb | ka + maxDepth--; + final SplittingInterval lk = k.split(p0, p1); + // Recurse left side if required + if (lk != null) { + // Avoid recursive method calls + if (k.empty()) { + // Entirely on left side + r = p0 - 1; + kb = lk.right(); + k = lk; + continue; + } + introselect(part, a, l, p0 - 1, lk, maxDepth); + } + if (k.empty()) { + // No right side + recursionConsumer.accept(maxDepth); + return; + } + // Continue on the right side + l = p1 + 1; + ka = k.left(); + } + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. + * + *

For all indices {@code k} and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

This function accepts an {@link IndexIterator} of indices {@code k}; for + * convenience the lower and upper indices of the current interval are passed as the + * first index {@code ka} and last index {@code kb} of the closed interval of indices + * to partition. These may be within the lower and upper indices if the interval was + * split during recursion: {@code lower <= ka <= kb <= upper}. + * + *

The data is recursively partitioned using left-most ordering. When the current + * interval has been partitioned the {@link IndexIterator} is used to advance to the + * next interval to partition. + * + *

Uses an introselect variant. The quickselect is provided as an argument; the + * fall-back on poor convergence of the quickselect is a heapselect. + * + * @param part Partition function. + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param k Interval of indices to partition (ordered). + * @param ka First key. + * @param kb Last key. + * @param maxDepth Maximum depth for recursion. + */ + // package-private for benchmarking + void introselect(SPEPartition part, double[] a, int left, int right, + IndexIterator k, int ka, int kb, int maxDepth) { + // Left side requires recursion; right side remains within this function + // When this function returns all indices in [left, right] must be processed. + int l = left; + int lo = ka; + int hi = kb; + final int[] upper = {0}; + while (true) { + if (maxDepth == 0) { + // Too much recursion. + // Advance the iterator to the end of the current range. + // Note: heapSelectRange handles hi > right. + // Single API method: advanceBeyond(right): return hi <= right + while (hi < right && k.next()) { + hi = k.right(); + } + heapSelectRange(a, l, right, lo, hi); + recursionConsumer.accept(maxDepth); + return; + } + + // length - 1 + final int n = right - l; + + // If interval is close to one end then edgeselect. + // Only elect left if there are no further indices in the range. + // |l|-----|lo|--------|hi|------|right| + // ---------d1---------- + // --------------d2----------- + if (Math.min(hi - l, right - lo) < edgeSelectConstant) { + if (hi - l > right - lo) { + // Right end. Do not check above hi, just select to the end + edgeSelection.partition(a, l, right, lo, right); + recursionConsumer.accept(maxDepth); + return; + } else if (k.nextAfter(right)) { + // Left end + // Only if no further indices in the range. + // If false this branch will continue to be triggered until + // a partition is made to separate the next indices. + edgeSelection.partition(a, l, right, lo, hi); + recursionConsumer.accept(maxDepth); + // Advance iterator + l = hi + 1; + if (!k.positionAfter(hi) || Math.max(k.left(), l) > right) { + // No more keys, or keys beyond the current bounds + return; + } + lo = Math.max(k.left(), l); + hi = Math.min(right, k.right()); + // Continue right (allows a second heap select for the right side) + continue; + } + } + + // If interval is close to both ends then full sort + // |l|-----|lo|--------|hi|------|right| + // ---d1---- + // ----d2-------- + // (lo - l) + (right - hi) == (right - l) - (hi - lo) + if (n - (hi - lo) < minQuickSelectSize) { + // Handle small data. This is done as the JDK sort will + // use insertion sort for small data. For double data it + // will also pre-process the data for NaN and signed + // zeros which is an overhead to avoid. + if (n < minQuickSelectSize) { + // Must not use sortSelectRange in [lo, hi] as the iterator + // has not been advanced to check after hi + sortSelectRight(a, l, right, lo); + } else { + // Note: This disregards the current level of recursion + // but can exploit the JDK's more advanced sort algorithm. + Arrays.sort(a, l, right + 1); + } + recursionConsumer.accept(maxDepth); + return; + } + + // Here: l <= lo <= hi <= right + // Pick a pivot and partition + final int p0 = part.partition(a, l, right, + pivotingStrategy.pivotIndex(a, l, right, ka), + upper); + final int p1 = upper[0]; + + maxDepth--; + // Recursion left + if (lo < p0) { + introselect(part, a, l, p0 - 1, k, lo, Math.min(hi, p0 - 1), maxDepth); + // Advance iterator + // Single API method: fastForwardAndLeftWithin(p1, right) + if (!k.positionAfter(p1) || k.left() > right) { + // No more keys, or keys beyond the current bounds + return; + } + lo = k.left(); + hi = Math.min(right, k.right()); + } + if (hi <= p1) { + // Advance iterator + if (!k.positionAfter(p1) || k.left() > right) { + // No more keys, or keys beyond the current bounds + return; + } + lo = k.left(); + hi = Math.min(right, k.right()); + } + // Continue right + l = p1 + 1; + lo = Math.max(lo, l); + } + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

All indices are assumed to be within {@code [0, right]}. + * + *

Uses an introselect variant. The dual pivot quickselect is provided as an argument; + * the fall-back on poor convergence of the quickselect is controlled by + * current configuration. + * + *

The partition method is not required to handle signed zeros. + * + * @param part Partition function. + * @param a Values. + * @param k Indices (may be destructively modified). + * @param count Count of indices (assumed to be strictly positive). + */ + void introselect(DPPartition part, double[] a, int[] k, int count) { + // Handle NaN / signed zeros + final DoubleDataTransformer t = SORT_TRANSFORMER.get(); + // Assume this is in-place + t.preProcess(a); + final int end = t.length(); + int n = count; + if (end > 1) { + // Filter indices invalidated by NaN check + if (end < a.length) { + for (int i = n; --i >= 0;) { + final int v = k[i]; + if (v >= end) { + // swap(k, i, --n) + k[i] = k[--n]; + k[n] = v; + } + } + } + introselect(part, a, end - 1, k, n); + } + // Restore signed zeros + t.postProcess(a, k, n); + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

All indices are assumed to be within {@code [0, right]}. + * + *

Uses an introselect variant. The dual pivot quickselect is provided as an argument; + * the fall-back on poor convergence of the quickselect is controlled by + * current configuration. + * + *

This function assumes {@code n > 0} and {@code right > 0}; otherwise + * there is nothing to do. + * + * @param part Partition function. + * @param a Values. + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param k Indices (may be destructively modified). + * @param n Count of indices (assumed to be strictly positive). + */ + private void introselect(DPPartition part, double[] a, int right, int[] k, int n) { + if (n < 1) { + return; + } + final int maxDepth = createMaxDepthDualPivot(right + 1); + // Handle cases without multiple keys + if (n == 1) { + if (pairedKeyStrategy == PairedKeyStrategy.PAIRED_KEYS) { + // Dedicated method for a single key + introselect(part, a, 0, right, k[0], maxDepth); + } else if (pairedKeyStrategy == PairedKeyStrategy.TWO_KEYS) { + // Dedicated method for two keys using the same key + introselect(part, a, 0, right, k[0], k[0], maxDepth); + } else if (pairedKeyStrategy == PairedKeyStrategy.SEARCHABLE_INTERVAL) { + // Reuse the IndexInterval method using the same key + introselect(part, a, 0, right, IndexIntervals.anyIndex(), k[0], k[0], maxDepth); + } else if (pairedKeyStrategy == PairedKeyStrategy.UPDATING_INTERVAL) { + // Reuse the Interval method using a single key + introselect(part, a, 0, right, IndexIntervals.interval(k[0]), maxDepth); + } else { + throw new IllegalStateException(UNSUPPORTED_INTROSELECT + pairedKeyStrategy); + } + return; + } + // Special case for partition around adjacent indices (for interpolation) + if (n == 2 && k[0] + 1 == k[1]) { + if (pairedKeyStrategy == PairedKeyStrategy.PAIRED_KEYS) { + // Dedicated method for a single key, returns information about k+1 + final int p = introselect(part, a, 0, right, k[0], maxDepth); + // p <= k to signal k+1 is unsorted, or p+1 is a pivot. + // if k is sorted, and p+1 is sorted, k+1 is sorted if k+1 == p. + if (p > k[1]) { + selectMinIgnoreZeros(a, k[1], p); + } + } else if (pairedKeyStrategy == PairedKeyStrategy.TWO_KEYS) { + // Dedicated method for two keys + // Note: This can handle keys that are not adjacent + // e.g. keys near opposite ends without a partition step. + final int ka = Math.min(k[0], k[1]); + final int kb = Math.max(k[0], k[1]); + introselect(part, a, 0, right, ka, kb, maxDepth); + } else if (pairedKeyStrategy == PairedKeyStrategy.SEARCHABLE_INTERVAL) { + // Reuse the IndexInterval method using a range of two keys + introselect(part, a, 0, right, IndexIntervals.anyIndex(), k[0], k[1], maxDepth); + } else if (pairedKeyStrategy == PairedKeyStrategy.UPDATING_INTERVAL) { + // Reuse the Interval method using a range of two keys + introselect(part, a, 0, right, IndexIntervals.interval(k[0], k[1]), maxDepth); + } else { + throw new IllegalStateException(UNSUPPORTED_INTROSELECT + pairedKeyStrategy); + } + return; + } + + // Detect possible saturated range. + // minimum keys = 10 + // min separation = 2^3 (could use log2(minQuickSelectSize) here) + // saturation = 0.95 + //if (keysAreSaturated(right + 1, k, n, 10, 3, 0.95)) { + // Arrays.sort(a, 0, right + 1); + // return; + //} + + // Note: Sorting to unique keys is an overhead. This can be eliminated + // by requesting the caller passes sorted keys (or quantiles in order). + + if (keyStrategy == KeyStrategy.ORDERED_KEYS) { + // DP does not offer ORDERED_KEYS implementation but we include the branch + // for completeness. + throw new IllegalStateException(UNSUPPORTED_INTROSELECT + keyStrategy); + } else if (keyStrategy == KeyStrategy.SCANNING_KEY_SEARCHABLE_INTERVAL) { + final int unique = Sorting.sortIndices(k, n); + final SearchableInterval keys = ScanningKeyInterval.of(k, unique); + introselect(part, a, 0, right, keys, keys.left(), keys.right(), maxDepth); + } else if (keyStrategy == KeyStrategy.SEARCH_KEY_SEARCHABLE_INTERVAL) { + final int unique = Sorting.sortIndices(k, n); + final SearchableInterval keys = BinarySearchKeyInterval.of(k, unique); + introselect(part, a, 0, right, keys, keys.left(), keys.right(), maxDepth); + } else if (keyStrategy == KeyStrategy.COMPRESSED_INDEX_SET) { + final SearchableInterval keys = CompressedIndexSet.of(compression, k, n); + introselect(part, a, 0, right, keys, keys.left(), keys.right(), maxDepth); + } else if (keyStrategy == KeyStrategy.INDEX_SET) { + final SearchableInterval keys = IndexSet.of(k, n); + introselect(part, a, 0, right, keys, keys.left(), keys.right(), maxDepth); + } else if (keyStrategy == KeyStrategy.KEY_UPDATING_INTERVAL) { + final int unique = Sorting.sortIndices(k, n); + final UpdatingInterval keys = KeyUpdatingInterval.of(k, unique); + introselect(part, a, 0, right, keys, maxDepth); + } else if (keyStrategy == KeyStrategy.INDEX_SET_UPDATING_INTERVAL) { + final UpdatingInterval keys = BitIndexUpdatingInterval.of(k, n); + introselect(part, a, 0, right, keys, maxDepth); + } else if (keyStrategy == KeyStrategy.KEY_SPLITTING_INTERVAL) { + final int unique = Sorting.sortIndices(k, n); + final SplittingInterval keys = KeyUpdatingInterval.of(k, unique); + introselect(part, a, 0, right, keys, maxDepth); + } else if (keyStrategy == KeyStrategy.INDEX_SET_SPLITTING_INTERVAL) { + final SplittingInterval keys = BitIndexUpdatingInterval.of(k, n); + introselect(part, a, 0, right, keys, maxDepth); + } else if (keyStrategy == KeyStrategy.INDEX_ITERATOR) { + final int unique = Sorting.sortIndices(k, n); + final IndexIterator keys = KeyIndexIterator.of(k, unique); + introselect(part, a, 0, right, keys, keys.left(), keys.right(), maxDepth); + } else if (keyStrategy == KeyStrategy.COMPRESSED_INDEX_ITERATOR) { + final IndexIterator keys = CompressedIndexSet.iterator(compression, k, n); + introselect(part, a, 0, right, keys, keys.left(), keys.right(), maxDepth); + } else { + throw new IllegalStateException(UNSUPPORTED_INTROSELECT + keyStrategy); + } + } + + /** + * Partition the array such that index {@code k} corresponds to its + * correctly sorted value in the equivalent fully sorted array. + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

Uses an introselect variant. The quickselect is provided as an argument; + * the fall-back on poor convergence of the quickselect is controlled by + * current configuration. + * + *

Returns information {@code p} on whether {@code k+1} is sorted. + * If {@code p <= k} then {@code k+1} is sorted. + * If {@code p > k} then {@code p+1} is a pivot. + * + * @param part Partition function. + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param k Index. + * @param maxDepth Maximum depth for recursion. + * @return the index {@code p} + */ + private int introselect(DPPartition part, double[] a, int left, int right, + int k, int maxDepth) { + int l = left; + int r = right; + final int[] upper = {0, 0, 0}; + while (true) { + // It is possible to use edgeselect when k is close to the end + // |l|-----|k|---------|k|--------|r| + // ---d1---- + // -----d3---- + final int d1 = k - l; + final int d3 = r - k; + if (Math.min(d1, d3) < edgeSelectConstant) { + edgeSelection.partition(a, l, r, k, k); + // Last known unsorted value >= k + return r; + } + + if (maxDepth == 0) { + // Too much recursion + stopperSelection.partition(a, l, r, k, k); + // Last known unsorted value >= k + return r; + } + + // Pick 2 pivots and partition + int p0 = dualPivotingStrategy.pivotIndex(a, l, r, upper); + p0 = part.partition(a, l, r, p0, upper[0], upper); + final int p1 = upper[0]; + final int p2 = upper[1]; + final int p3 = upper[2]; + + maxDepth--; + if (k < p0) { + // The element is in the left partition + r = p0 - 1; + continue; + } else if (k > p3) { + // The element is in the right partition + l = p3 + 1; + continue; + } + // Check the interval overlaps the middle; and the middle exists. + // p0 p1 p2 p3 + // |l|-----------------|P|------------------|P|----|r| + // Eliminate: ----kb1 ka1---- + if (k <= p1 || p2 <= k || p2 - p1 <= 2) { + // Signal if k+1 is sorted. + // This can be true if the pivots were ranges [p0, p1] or [p2, p3] + // This check will match *most* sorted k for the 3 eliminated cases. + // It will not identify p2 - p1 <= 2 when k == p1. In this case + // k+1 is sorted and a min-select for k+1 is a fast scan up to r. + return k != p1 && k < p3 ? k : r; + } + // Continue in the middle partition + l = p1 + 1; + r = p2 - 1; + } + } + + /** + * Partition the array such that indices {@code ka} and {@code kb} correspond to their + * correctly sorted value in the equivalent fully sorted array. + * + *

For all indices {@code k} and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

Note: Requires {@code ka <= kb}. The use of two indices is to support processing + * of pairs of indices {@code (k, k+1)}. However the indices are treated independently + * and partitioned by recursion. They may be equal, neighbours or well separated. + * + *

Uses an introselect variant. The quickselect is provided as an argument; the + * fall-back on poor convergence of the quickselect is a heapselect. + * + * @param part Partition function. + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param ka Index. + * @param kb Index. + * @param maxDepth Maximum depth for recursion. + */ + private void introselect(DPPartition part, double[] a, int left, int right, + int ka, int kb, int maxDepth) { + // Only one side requires recursion. The other side + // can remain within this function call. + int l = left; + int r = right; + int ka1 = ka; + int kb1 = kb; + final int[] upper = {0, 0, 0}; + while (true) { + // length - 1 + final int n = r - l; + + if (n < minQuickSelectSize) { + // Sort selection on small data + sortSelectRange(a, l, r, ka1, kb1); + return; + } + + // It is possible to use heapselect when ka1 and kb1 are close to the ends + // |l|-----|ka1|--------|kb1|------|r| + // ---s1---- + // -----s3---- + // ---------s2----------- + // ----------s4----------- + final int s1 = ka1 - l; + final int s2 = kb1 - l; + final int s3 = r - kb1; + final int s4 = r - ka1; + if (maxDepth == 0 || + Math.min(s1 + s3, Math.min(s2, s4)) < edgeSelectConstant) { + // Too much recursion, or ka1 and kb1 are both close to the ends + // Note: Does not use the edgeSelection function as the indices are not a range + heapSelectPair(a, l, r, ka1, kb1); + return; + } + + // Pick 2 pivots and partition + int p0 = dualPivotingStrategy.pivotIndex(a, l, r, upper); + p0 = part.partition(a, l, r, p0, upper[0], upper); + final int p1 = upper[0]; + final int p2 = upper[1]; + final int p3 = upper[2]; + + // Recursion to max depth + // Note: Here we possibly branch left and right with multiple keys. + // It is possible that the partition has split the pair + // and the recursion proceeds with a single point. + maxDepth--; + // Recurse left side if required + if (ka1 < p0) { + if (kb1 <= p1) { + // Entirely on left side + r = p0 - 1; + kb1 = r < kb1 ? ka1 : kb1; + continue; + } + introselect(part, a, l, p0 - 1, ka1, ka1, maxDepth); + // Here we must process middle and possibly right + ka1 = kb1; + } + // Recurse middle if required + // Check the either k is in the range (p1, p2) + // p0 p1 p2 p3 + // |l|-----------------|P|------------------|P|----|r| + if (ka1 < p2 && ka1 > p1 || kb1 < p2 && kb1 > p1) { + // Advance lower bound + l = p1 + 1; + ka1 = ka1 < l ? kb1 : ka1; + if (kb1 <= p3) { + // Entirely in middle + r = p2 - 1; + kb1 = r < kb1 ? ka1 : kb1; + continue; + } + introselect(part, a, l, p2 - 1, ka1, ka1, maxDepth); + // Here we must process right + ka1 = kb1; + } + if (kb1 <= p3) { + // No right side + return; + } + // Continue right + l = p3 + 1; + ka1 = ka1 < l ? kb1 : ka1; + } + } + + /** + * Partition the array such that indices {@code k} correspond to their + * correctly sorted value in the equivalent fully sorted array. + * + *

For all indices {@code k} and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

This function accepts a {@link SearchableInterval} of indices {@code k} and the + * first index {@code ka} and last index {@code kb} that define the range of indices + * to partition. The {@link SearchableInterval} is used to search for keys in {@code [ka, kb]} + * to create {@code [ka, kb1]} and {@code [ka1, kb]} if partitioning splits the range. + * + *

{@code
+     * left <= ka <= kb <= right
+     * }
+ * + *

Uses an introselect variant. The dual pivot quickselect is provided as an argument; + * the fall-back on poor convergence of the quickselect is a heapselect. + * + * @param part Partition function. + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param k Interval of indices to partition (ordered). + * @param ka First key. + * @param kb Last key. + * @param maxDepth Maximum depth for recursion. + */ + // package-private for benchmarking + void introselect(DPPartition part, double[] a, int left, int right, + SearchableInterval k, int ka, int kb, int maxDepth) { + // If partitioning splits the interval then recursion is used for left and/or + // right sides and the middle remains within this function. If partitioning does + // not split the interval then it remains within this function. + int l = left; + int r = right; + int ka1 = ka; + int kb1 = kb; + final int[] upper = {0, 0, 0}; + while (true) { + // length - 1 + final int n = r - l; + + if (n < minQuickSelectSize) { + // Sort selection on small data + sortSelectRange(a, l, r, ka1, kb1); + recursionConsumer.accept(maxDepth); + return; + } + + // It is possible to use heapselect when ka1 and kb1 are close to the same end + // |l|-----|ka1|--------|kb1|------|r| + // ---------s2----------- + // ----------s4----------- + if (Math.min(kb1 - l, r - ka1) < edgeSelectConstant) { + edgeSelection.partition(a, l, r, ka1, kb1); + recursionConsumer.accept(maxDepth); + return; + } + + if (maxDepth == 0) { + // Too much recursion + heapSelectRange(a, l, r, ka1, kb1); + recursionConsumer.accept(maxDepth); + return; + } + + // Pick 2 pivots and partition + int p0 = dualPivotingStrategy.pivotIndex(a, l, r, upper); + p0 = part.partition(a, l, r, p0, upper[0], upper); + final int p1 = upper[0]; + final int p2 = upper[1]; + final int p3 = upper[2]; + + // Recursion to max depth + // Note: Here we possibly branch left, middle and right with multiple keys. + // It is possible that the partition has split the keys + // and the recursion proceeds with a reduced set in each region. + // p0 p1 p2 p3 + // |l|--|ka1|--k----k--|P|------k--|kb1|----|P|----|r| + // kb1 | ka1 + // Search previous/next is bounded at ka1/kb1 + maxDepth--; + // Recurse left side if required + if (ka1 < p0) { + if (kb1 <= p1) { + // Entirely on left side + r = p0 - 1; + if (r < kb1) { + kb1 = k.previousIndex(r); + } + continue; + } + introselect(part, a, l, p0 - 1, k, ka1, k.split(p0, p1, upper), maxDepth); + ka1 = upper[0]; + } + // Recurse right side if required + if (kb1 > p3) { + if (ka1 >= p2) { + // Entirely on right-side + l = p3 + 1; + if (ka1 < l) { + ka1 = k.nextIndex(l); + } + continue; + } + final int lo = k.split(p2, p3, upper); + introselect(part, a, p3 + 1, r, k, upper[0], kb1, maxDepth); + kb1 = lo; + } + // Check the interval overlaps the middle; and the middle exists. + // p0 p1 p2 p3 + // |l|-----------------|P|------------------|P|----|r| + // Eliminate: ----kb1 ka1---- + if (kb1 <= p1 || p2 <= ka1 || p2 - p1 <= 2) { + // No middle + recursionConsumer.accept(maxDepth); + return; + } + l = p1 + 1; + r = p2 - 1; + // Interval [ka1, kb1] overlaps the middle but there may be nothing in the interval. + // |l|-----------------|P|------------------|P|----|r| + // Eliminate: ka1 kb1 + // Detect this if ka1 is advanced too far. + if (ka1 < l) { + ka1 = k.nextIndex(l); + if (ka1 > r) { + // No middle + recursionConsumer.accept(maxDepth); + return; + } + } + if (r < kb1) { + kb1 = k.previousIndex(r); + } + } + } + + /** + * Partition the array such that indices {@code k} correspond to their + * correctly sorted value in the equivalent fully sorted array. + * + *

For all indices {@code k} and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

This function accepts a {@link UpdatingInterval} of indices {@code k} that define the + * range of indices to partition. The {@link UpdatingInterval} can be narrowed or split as + * partitioning divides the range. + * + *

Uses an introselect variant. The dual pivot quickselect is provided as an argument; + * the fall-back on poor convergence of the quickselect is a heapselect. + * + * @param part Partition function. + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param k Interval of indices to partition (ordered). + * @param maxDepth Maximum depth for recursion. + */ + // package-private for benchmarking + void introselect(DPPartition part, double[] a, int left, int right, + UpdatingInterval k, int maxDepth) { + // If partitioning splits the interval then recursion is used for left and/or + // right sides and the middle remains within this function. If partitioning does + // not split the interval then it remains within this function. + int l = left; + int r = right; + int ka = k.left(); + int kb = k.right(); + final int[] upper = {0, 0, 0}; + while (true) { + // length - 1 + final int n = r - l; + + if (n < minQuickSelectSize) { + // Sort selection on small data + sortSelectRange(a, l, r, ka, kb); + recursionConsumer.accept(maxDepth); + return; + } + + // It is possible to use heapselect when ka and kb are close to the same end + // |l|-----|ka|--------|kb|------|r| + // ---------s2----------- + // ----------s4----------- + if (Math.min(kb - l, r - ka) < edgeSelectConstant) { + edgeSelection.partition(a, l, r, ka, kb); + recursionConsumer.accept(maxDepth); + return; + } + + if (maxDepth == 0) { + // Too much recursion + heapSelectRange(a, l, r, ka, kb); + recursionConsumer.accept(maxDepth); + return; + } + + // Pick 2 pivots and partition + int p0 = dualPivotingStrategy.pivotIndex(a, l, r, upper); + p0 = part.partition(a, l, r, p0, upper[0], upper); + final int p1 = upper[0]; + final int p2 = upper[1]; + final int p3 = upper[2]; + + // Recursion to max depth + // Note: Here we possibly branch left, middle and right with multiple keys. + // It is possible that the partition has split the keys + // and the recursion proceeds with a reduced set in each region. + // p0 p1 p2 p3 + // |l|--|ka|--k----k--|P|------k--|kb|----|P|----|r| + // kb | ka + // Search previous/next is bounded at ka/kb + maxDepth--; + // Recurse left side if required + if (ka < p0) { + if (kb <= p1) { + // Entirely on left side + r = p0 - 1; + if (r < kb) { + kb = k.updateRight(r); + } + continue; + } + introselect(part, a, l, p0 - 1, k.splitLeft(p0, p1), maxDepth); + ka = k.left(); + } else if (kb <= p1) { + // No middle/right side + return; + } else if (ka <= p1) { + // Advance lower bound + ka = k.updateLeft(p1 + 1); + } + // Recurse middle if required + if (ka < p2) { + l = p1 + 1; + if (kb <= p3) { + // Entirely in middle + r = p2 - 1; + if (r < kb) { + kb = k.updateRight(r); + } + continue; + } + introselect(part, a, l, p2 - 1, k.splitLeft(p2, p3), maxDepth); + ka = k.left(); + } else if (kb <= p3) { + // No right side + return; + } else if (ka <= p3) { + ka = k.updateLeft(p3 + 1); + } + // Continue right + l = p3 + 1; + } + } + + /** + * Partition the array such that indices {@code k} correspond to their + * correctly sorted value in the equivalent fully sorted array. + * + *

For all indices {@code k} and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

This function accepts a {@link SplittingInterval} of indices {@code k} that define the + * range of indices to partition. The {@link SplittingInterval} is split as + * partitioning divides the range. + * + *

Uses an introselect variant. The dual pivot quickselect is provided as an argument; + * the fall-back on poor convergence of the quickselect is a heapselect. + * + * @param part Partition function. + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param k Interval of indices to partition (ordered). + * @param maxDepth Maximum depth for recursion. + */ + // package-private for benchmarking + void introselect(DPPartition part, double[] a, int left, int right, + SplittingInterval k, int maxDepth) { + // If partitioning splits the interval then recursion is used for left and/or + // right sides and the middle remains within this function. If partitioning does + // not split the interval then it remains within this function. + int l = left; + int r = right; + int ka = k.left(); + int kb = k.right(); + final int[] upper = {0, 0, 0}; + while (true) { + // length - 1 + final int n = r - l; + + if (n < minQuickSelectSize) { + // Sort selection on small data + sortSelectRange(a, l, r, ka, kb); + recursionConsumer.accept(maxDepth); + return; + } + + // It is possible to use heapselect when ka and kb are close to the same end + // |l|-----|ka|--------|kb|------|r| + // ---------s2----------- + // ----------s4----------- + if (Math.min(kb - l, r - ka) < edgeSelectConstant) { + edgeSelection.partition(a, l, r, ka, kb); + recursionConsumer.accept(maxDepth); + return; + } + + if (maxDepth == 0) { + // Too much recursion + heapSelectRange(a, l, r, ka, kb); + recursionConsumer.accept(maxDepth); + return; + } + + // Pick 2 pivots and partition + int p0 = dualPivotingStrategy.pivotIndex(a, l, r, upper); + p0 = part.partition(a, l, r, p0, upper[0], upper); + final int p1 = upper[0]; + final int p2 = upper[1]; + final int p3 = upper[2]; + + // Recursion to max depth + // Note: Here we possibly branch left, middle and right with multiple keys. + // It is possible that the partition has split the keys + // and the recursion proceeds with a reduced set in each region. + // p0 p1 p2 p3 + // |l|--|ka|--k----k--|P|------k--|kb|----|P|----|r| + // kb | ka + // Search previous/next is bounded at ka/kb + maxDepth--; + SplittingInterval lk = k.split(p0, p1); + // Recurse left side if required + if (lk != null) { + // Avoid recursive method calls + if (k.empty()) { + // Entirely on left side + r = p0 - 1; + kb = lk.right(); + k = lk; + continue; + } + introselect(part, a, l, p0 - 1, lk, maxDepth); + } + if (k.empty()) { + // No middle/right side + recursionConsumer.accept(maxDepth); + return; + } + lk = k.split(p2, p3); + // Recurse middle side if required + if (lk != null) { + // Avoid recursive method calls + if (k.empty()) { + // Entirely in middle side + l = p1 + 1; + r = p2 - 1; + ka = lk.left(); + kb = lk.right(); + k = lk; + continue; + } + introselect(part, a, p1 + 1, p2 - 1, lk, maxDepth); + } + if (k.empty()) { + // No right side + recursionConsumer.accept(maxDepth); + return; + } + // Continue right + l = p3 + 1; + ka = k.left(); + } + } + + /** + * Partition the array such that indices {@code k} correspond to their + * correctly sorted value in the equivalent fully sorted array. + * + *

For all indices {@code k} and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + * + *

This function accepts an {@link IndexIterator} of indices {@code k}; for + * convenience the lower and upper indices of the current interval are passed as the + * first index {@code ka} and last index {@code kb} of the closed interval of indices + * to partition. These may be within the lower and upper indices if the interval was + * split during recursion: {@code lower <= ka <= kb <= upper}. + * + *

The data is recursively partitioned using left-most ordering. When the current + * interval has been partitioned the {@link IndexIterator} is used to advance to the + * next interval to partition. + * + *

Uses an introselect variant. The dual pivot quickselect is provided as an argument; + * the fall-back on poor convergence of the quickselect is a heapselect. + * + * @param part Partition function. + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param k Interval of indices to partition (ordered). + * @param ka First key. + * @param kb Last key. + * @param maxDepth Maximum depth for recursion. + */ + // package-private for benchmarking + void introselect(DPPartition part, double[] a, int left, int right, + IndexIterator k, int ka, int kb, int maxDepth) { + // If partitioning splits the interval then recursion is used for left and/or + // right sides and the middle remains within this function. If partitioning does + // not split the interval then it remains within this function. + int l = left; + final int r = right; + int lo = ka; + int hi = kb; + final int[] upper = {0, 0, 0}; + while (true) { + if (maxDepth == 0) { + // Too much recursion. + // Advance the iterator to the end of the current range. + // Note: heapSelectRange handles hi > right. + // Single API method: advanceBeyond(right): return hi <= right + while (hi < right && k.next()) { + hi = k.right(); + } + heapSelectRange(a, l, right, lo, hi); + recursionConsumer.accept(maxDepth); + return; + } + + // length - 1 + final int n = right - l; + + // If interval is close to one end then heapselect. + // Only heapselect left if there are no further indices in the range. + // |l|-----|lo|--------|hi|------|right| + // ---------d1---------- + // --------------d2----------- + if (Math.min(hi - l, right - lo) < edgeSelectConstant) { + if (hi - l > right - lo) { + // Right end. Do not check above hi, just select to the end + edgeSelection.partition(a, l, right, lo, right); + recursionConsumer.accept(maxDepth); + return; + } else if (k.nextAfter(right)) { + // Left end + // Only if no further indices in the range. + // If false this branch will continue to be triggered until + // a partition is made to separate the next indices. + edgeSelection.partition(a, l, right, lo, hi); + recursionConsumer.accept(maxDepth); + // Advance iterator + l = hi + 1; + if (!k.positionAfter(hi) || Math.max(k.left(), l) > right) { + // No more keys, or keys beyond the current bounds + return; + } + lo = Math.max(k.left(), l); + hi = Math.min(right, k.right()); + // Continue right (allows a second heap select for the right side) + continue; + } + } + + // If interval is close to both ends then sort + // |l|-----|lo|--------|hi|------|right| + // ---d1---- + // ----d2-------- + // (lo - l) + (right - hi) == (right - l) - (hi - lo) + if (n - (hi - lo) < minQuickSelectSize) { + // Handle small data. This is done as the JDK sort will + // use insertion sort for small data. For double data it + // will also pre-process the data for NaN and signed + // zeros which is an overhead to avoid. + if (n < minQuickSelectSize) { + // Must not use sortSelectRange in [lo, hi] as the iterator + // has not been advanced to check after hi + sortSelectRight(a, l, right, lo); + } else { + // Note: This disregards the current level of recursion + // but can exploit the JDK's more advanced sort algorithm. + Arrays.sort(a, l, right + 1); + } + recursionConsumer.accept(maxDepth); + return; + } + + // Here: l <= lo <= hi <= right + // Pick 2 pivots and partition + int p0 = dualPivotingStrategy.pivotIndex(a, l, r, upper); + p0 = part.partition(a, l, r, p0, upper[0], upper); + final int p1 = upper[0]; + final int p2 = upper[1]; + final int p3 = upper[2]; + + maxDepth--; + // Recursion left + if (lo < p0) { + introselect(part, a, l, p0 - 1, k, lo, Math.min(hi, p0 - 1), maxDepth); + // Advance iterator + if (!k.positionAfter(p1) || k.left() > right) { + // No more keys, or keys beyond the current bounds + return; + } + lo = k.left(); + hi = Math.min(right, k.right()); + } + if (hi <= p1) { + // Advance iterator + if (!k.positionAfter(p1) || k.left() > right) { + // No more keys, or keys beyond the current bounds + return; + } + lo = k.left(); + hi = Math.min(right, k.right()); + } + + // Recursion middle + l = p1 + 1; + lo = Math.max(lo, l); + if (lo < p2) { + introselect(part, a, l, p2 - 1, k, lo, Math.min(hi, p2 - 1), maxDepth); + // Advance iterator + if (!k.positionAfter(p3) || k.left() > right) { + // No more keys, or keys beyond the current bounds + return; + } + lo = k.left(); + hi = Math.min(right, k.right()); + } + if (hi <= p3) { + // Advance iterator + if (!k.positionAfter(p3) || k.left() > right) { + // No more keys, or keys beyond the current bounds + return; + } + lo = k.left(); + hi = Math.min(right, k.right()); + } + + // Continue right + l = p3 + 1; + lo = Math.max(lo, l); + } + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

Uses a Bentley-McIlroy quicksort partition method by Sedgewick. + * + * @param data Values. + * @param k Indices (may be destructively modified). + * @param n Count of indices. + */ + void partitionSBM(double[] data, int[] k, int n) { + // Handle NaN (this does assume n > 0) + final int right = sortNaN(data); + partition((SPEPartitionFunction) this::partitionSBMWithZeros, data, right, k, n); + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

The method assumes all {@code k} are valid indices into the data. + * It handles NaN and signed zeros in the data. + * + *

Uses an introselect variant. Uses the configured single-pivot quicksort method; + * the fall-back on poor convergence of the quickselect is controlled by + * current configuration. + * + * @param data Values. + * @param k Indices (may be destructively modified). + * @param n Count of indices. + */ + void partitionISP(double[] data, int[] k, int n) { + introselect(getSPFunction(), data, k, n); + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

The method assumes all {@code k} are valid indices into the data. + * It handles NaN and signed zeros in the data. + * + *

Uses an introselect variant. The quickselect is a dual-pivot quicksort + * partition method by Vladimir Yaroslavskiy; the fall-back on poor convergence of + * the quickselect is controlled by current configuration. + * + * @param data Values. + * @param k Indices (may be destructively modified). + * @param n Count of indices. + */ + void partitionIDP(double[] data, int[] k, int n) { + introselect((DPPartition) Partition::partitionDP, data, k, n); + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

The method assumes all {@code k} are valid indices into the data. + * It handles NaN and signed zeros in the data. + * + *

Uses the + * Floyd-Rivest Algorithm (Wikipedia) + * + *

WARNING: Currently this only supports a single {@code k}. For parity with other + * select methods this accepts an array {@code k} and pre/post processes the data for + * NaN and signed zeros. + * + * @param a Values. + * @param k Indices (may be destructively modified). + * @param count Count of indices. + */ + void partitionFR(double[] a, int[] k, int count) { + // Handle NaN / signed zeros + final DoubleDataTransformer t = SORT_TRANSFORMER.get(); + // Assume this is in-place + t.preProcess(a); + final int end = t.length(); + int n = count; + if (end > 1) { + // Filter indices invalidated by NaN check + if (end < a.length) { + for (int i = n; --i >= 0;) { + final int v = k[i]; + if (v >= end) { + // swap(k, i, --n) + k[i] = k[--n]; + k[n] = v; + } + } + } + // Only handles a single k + if (n != 0) { + selectFR(a, 0, end - 1, k[0], controlFlags); + } + } + // Restore signed zeros + t.postProcess(a, k, n); + } + + /** + * Select the k-th element of the array. + * + *

Uses the + * Floyd-Rivest Algorithm (Wikipedia). + * + *

This code has been adapted from: + *

+     * Floyd and Rivest (1975)
+     * Algorithm 489: The Algorithm SELECT—for Finding the ith Smallest of n elements.
+     * Comm. ACM. 18 (3): 173.
+     * 
+ * + * @param a Values. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param k Key of interest. + * @param flags Control behaviour. + */ + private void selectFR(double[] a, int left, int right, int k, int flags) { + int l = left; + int r = right; + while (true) { + // The following edgeselect modifications are additions to the + // FR algorithm. These have been added for testing and only affect the finishing + // selection of small lengths. + + // It is possible to use edgeselect when k is close to the end + // |l|-----|ka|--------|kb|------|r| + // ---------s2---------- + // ----------s4----------- + if (Math.min(k - l, r - k) < edgeSelectConstant) { + edgeSelection.partition(a, l, r, k, k); + return; + } + + // use SELECT recursively on a sample of size S to get an estimate for the + // (k-l+1)-th smallest element into a[k], biased slightly so that the (k-l+1)-th + // element is expected to lie in the smaller set after partitioning. + int pivot = k; + int p = l; + int q = r; + // length - 1 + int n = r - l; + if (n > 600) { + ++n; + final int ith = k - l + 1; + final double z = Math.log(n); + final double s = 0.5 * Math.exp(0.6666666666666666 * z); + final double sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * Integer.signum(ith - (n >> 1)); + final int ll = Math.max(l, (int) (k - ith * s / n + sd)); + final int rr = Math.min(r, (int) (k + (n - ith) * s / n + sd)); + // Optional: sample [l, r] into [l, rs] + if ((flags & FLAG_SUBSET_SAMPLING) != 0) { + // Create a random sample at the left end. + // This creates an unbiased random sample. + // This method is not as fast as sampling into [ll, rr] (see below). + final IntUnaryOperator rng = createRNG(n, k); + final int rs = l + rr - ll; + for (int i = l - 1; i < rs;) { + // r - rand [0, r - i + 1) : i is currently i-1 + final int j = r - rng.applyAsInt(r - i); + final double t = a[++i]; + a[i] = a[j]; + a[j] = t; + } + selectFR(a, l, rs, k - ll + l, flags); + // Current: + // |l |k-ll+l| rs| r| + // | < v | v | > v | ??? | + // Move partitioned data + // |l |p |k| q| r| + // | < v | ??? |v| ??? | > v | + p = k - ll + l; + q = r - rs + p; + vectorSwap(a, p + 1, rs, r); + vectorSwap(a, p, p, k); + } else { + // Note: Random sampling is a redundant overhead on fully random data + // and will part destroy sorted data. On data that is: partially partitioned; + // has many repeat elements; or is structured with repeat patterns, the + // shuffle removes side-effects of patterns and stabilises performance. + if ((flags & FLAG_RANDOM_SAMPLING) != 0) { + // This is not a random sample from [l, r] when k is not exactly + // in the middle. By sampling either side of k the sample + // will maintain the value of k if the data is already partitioned + // around k. However sorted data will be part scrambled by the shuffle. + // This sampling has the best performance overall across datasets. + final IntUnaryOperator rng = createRNG(n, k); + // Shuffle [ll, k) from [l, k) + if (ll > l) { + for (int i = k; i > ll;) { + // l + rand [0, i - l + 1) : i is currently i+1 + final int j = l + rng.applyAsInt(i - l); + final double t = a[--i]; + a[i] = a[j]; + a[j] = t; + } + } + // Shuffle (k, rr] from (k, r] + if (rr < r) { + for (int i = k; i < rr;) { + // r - rand [0, r - i + 1) : i is currently i-1 + final int j = r - rng.applyAsInt(r - i); + final double t = a[++i]; + a[i] = a[j]; + a[j] = t; + } + } + } + selectFR(a, ll, rr, k, flags); + // Current: + // |l |ll |k| rr| r| + // | ??? | < v |v| > v | ??? | + // Optional: move partitioned data + // Unlikely to make a difference as the partitioning will skip + // over v. + // |l |p |k| q| r| + // | < v | ??? |v| ??? | > v | + if ((flags & FLAG_MOVE_SAMPLE) != 0) { + vectorSwap(a, l, ll - 1, k - 1); + vectorSwap(a, k + 1, rr, r); + p += k - ll; + q -= rr - k; + } + } + } else { + // Optional: use pivot strategy + pivot = pivotingStrategy.pivotIndex(a, l, r, k); + } + + // This uses the original binary partition of FR. + // FR sub-sampling can be used in some introselect methods; this + // allows the original FR to be compared with introselect. + + // Partition a[p : q] about t. + // Sub-script range checking has been eliminated by appropriate placement of t + // at the p or q end. + final double t = a[pivot]; + // swap(left, pivot) + a[pivot] = a[p]; + if (a[q] > t) { + // swap(right, left) + a[p] = a[q]; + a[q] = t; + // Here after the first swap: a[p] = t; a[q] > t + } else { + a[p] = t; + // Here after the first swap: a[p] <= t; a[q] = t + } + int i = p; + int j = q; + while (i < j) { + // swap(i, j) + final double temp = a[i]; + a[i] = a[j]; + a[j] = temp; + do { + ++i; + } while (a[i] < t); + do { + --j; + } while (a[j] > t); + } + if (a[p] == t) { + // data[j] <= t : swap(left, j) + a[p] = a[j]; + a[j] = t; + } else { + // data[j+1] > t : swap(j+1, right) + a[q] = a[++j]; + a[j] = t; + } + // Continue on the correct side + if (k < j) { + r = j - 1; + } else if (k > j) { + l = j + 1; + } else { + return; + } + } + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *
{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

The method assumes all {@code k} are valid indices into the data. + * It handles NaN and signed zeros in the data. + * + *

Uses the + * Floyd-Rivest Algorithm (Wikipedia), modified by Kiwiel. + * + *

WARNING: Currently this only supports a single {@code k}. For parity with other + * select methods this accepts an array {@code k} and pre/post processes the data for + * NaN and signed zeros. + * + * @param a Values. + * @param k Indices (may be destructively modified). + * @param count Count of indices. + */ + void partitionKFR(double[] a, int[] k, int count) { + // Handle NaN / signed zeros + final DoubleDataTransformer t = SORT_TRANSFORMER.get(); + // Assume this is in-place + t.preProcess(a); + final int end = t.length(); + int n = count; + if (end > 1) { + // Filter indices invalidated by NaN check + if (end < a.length) { + for (int i = n; --i >= 0;) { + final int v = k[i]; + if (v >= end) { + // swap(k, i, --n) + k[i] = k[--n]; + k[n] = v; + } + } + } + // Only handles a single k + if (n != 0) { + final int[] bounds = new int[5]; + selectKFR(a, 0, end - 1, k[0], bounds, null); + } + } + // Restore signed zeros + t.postProcess(a, k, n); + } + + /** + * Select the k-th element of the array. + * + *

Uses the + * Floyd-Rivest Algorithm (Wikipedia), modified by Kiwiel. + * + *

References: + *

+ * + * @param x Values. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param k Key of interest. + * @param bounds Inclusive bounds {@code [k-, k+]} containing {@code k}. + * @param rng Random generator for samples in {@code [0, n)}. + */ + private void selectKFR(double[] x, int left, int right, int k, int[] bounds, + IntUnaryOperator rng) { + int l = left; + int r = right; + while (true) { + // The following edgeselect modifications are additions to the + // KFR algorithm. These have been added for testing and only affect the finishing + // selection of small lengths. + + // It is possible to use edgeselect when k is close to the end + // |l|-----|ka|--------|kb|------|r| + // ---------s2---------- + // ----------s4----------- + if (Math.min(k - l, r - k) < edgeSelectConstant) { + edgeSelection.partition(x, l, r, k, k); + bounds[0] = bounds[1] = k; + return; + } + + // length - 1 + int n = r - l; + if (n < 600) { + // Switch to quickselect + final int p0 = partitionKBM(x, l, r, + pivotingStrategy.pivotIndex(x, l, r, k), bounds); + final int p1 = bounds[0]; + if (k < p0) { + // The element is in the left partition + r = p0 - 1; + } else if (k > p1) { + // The element is in the right partition + l = p1 + 1; + } else { + // The range contains the element we wanted. + bounds[0] = p0; + bounds[1] = p1; + return; + } + continue; + } + + // Floyd-Rivest sub-sampling + ++n; + // Step 1: Choose sample size s <= n-1 and gap g > 0 + final double z = Math.log(n); + // sample size = alpha * n^(2/3) * ln(n)^1/3 (4.1) + // sample size = alpha * n^(2/3) (4.17; original Floyd-Rivest size) + final double s = 0.5 * Math.exp(0.6666666666666666 * z) * Math.cbrt(z); + //final double s = 0.5 * Math.exp(0.6666666666666666 * z); + // gap = sqrt(beta * s * ln(n)) + final double g = Math.sqrt(0.25 * s * z); + final int rs = (int) (l + s - 1); + // Step 2: Sample selection + // Convenient to place the random sample in [l, rs] + if (rng == null) { + rng = createRNG(n, k); + } + for (int i = l - 1; i < rs;) { + // r - rand [0, r - i + 1) : i is currently i-1 + final int j = r - rng.applyAsInt(r - i); + final double t = x[++i]; + x[i] = x[j]; + x[j] = t; + } + + // Step 3: pivot selection + final double isn = (k - l + 1) * s / n; + final int ku = (int) Math.max(Math.floor(l - 1 + isn - g), l); + final int kv = (int) Math.min(Math.ceil(l - 1 + isn + g), rs); + // Find u and v by recursion + selectKFR(x, l, rs, ku, bounds, rng); + final int kum = bounds[0]; + int kup = bounds[1]; + int kvm; + int kvp; + if (kup >= kv) { + kvm = kv; + kvp = kup; + kup = kv - 1; + // u == v will use single-pivot ternary partitioning + } else { + selectKFR(x, kup + 1, rs, kv, bounds, rng); + kvm = bounds[0]; + kvp = bounds[1]; + } + + // Step 4: Partitioning + final double u = x[kup]; + final double v = x[kvm]; + // |l |ku- ku+| |kv- kv+| rs| r| (6.4) + // | x < u | x = u | u < x < v | x = v | x > v | ??? | + final int ll = kum; + int pp = kup; + final int rr = r - rs + kvp; + int qq = rr - kvp + kvm; + vectorSwap(x, kvp + 1, rs, r); + vectorSwap(x, kvm, kvp, rr); + // |l |ll pp| |kv- |qq rr| r| (6.5) + // | x < u | x = u | u < x < v | ??? | x = v | x > v | + + int a; + int b; + int c; + int d; + + // Note: The quintary partitioning is as specified in Kiwiel. + // Moving each branch to methods had no effect on performance. + + if (u == v) { + // Can be optimised by omitting step A1 (moving of sentinels). Here the + // size of ??? is large and initialisation is insignificant. + a = partitionKBM(x, ll, rr, pp, bounds); + d = bounds[0]; + // Make ternary and quintary partitioning compatible + b = d + 1; + c = a - 1; + } else if (k < (r + l) >>> 1) { + // Left k: u < x[k] < v --> expects x > v. + // Quintary partitioning using the six-part array: + // |ll pp| p| |i j| |q rr| (6.6) + // | x = u | u < x < v | x < u | ??? | x > v | x = v | + // + // |ll pp| p| j|i |q rr| (6.7) + // | x = u | u < x < v | x < u | x > v | x = v | + // + // Swap the second and third part: + // |ll pp| |b c|i |q rr| (6.8) + // | x = u | x < u | u < x < v | x > v | x = v | + // + // Swap the extreme parts with their neighbours: + // |ll |a |b c| d| rr| (6.9) + // | x < u | x = u | u < x < v | x = v | x > v | + int p = kvm - 1; + int q = qq; + int i = p; + int j = q; + for (;;) { + while (x[++i] < v) { + if (x[i] < u) { + continue; + } + // u <= xi < v + final double xi = x[i]; + x[i] = x[++p]; + if (xi > u) { + x[p] = xi; + } else { + x[p] = x[++pp]; + x[pp] = xi; + } + } + while (x[--j] >= v) { + if (x[j] == v) { + final double xj = x[j]; + x[j] = x[--q]; + x[q] = xj; + } + } + // Here x[j] < v <= x[i] + if (i >= j) { + break; + } + //swap(x, i, j) + final double xi = x[j]; + final double xj = x[i]; + x[i] = xi; + x[j] = xj; + if (xi > u) { + x[i] = x[++p]; + x[p] = xi; + } else if (xi == u) { + x[i] = x[++p]; + x[p] = x[++pp]; + x[pp] = xi; + } + if (xj == v) { + x[j] = x[--q]; + x[q] = xj; + } + } + a = ll + i - p - 1; + b = a + pp + 1 - ll; + d = rr - q + 1 + j; + c = d - rr + q - 1; + vectorSwap(x, pp + 1, p, j); + //vectorSwap(x, ll, pp, b - 1); + //vectorSwap(x, i, q - 1, rr); + vectorSwapL(x, ll, pp, b - 1, u); + vectorSwapR(x, i, q - 1, rr, v); + } else { + // Right k: u < x[k] < v --> expects x < u. + // Symmetric quintary partitioning replacing 6.6-6.8 with: + // |ll p| |i j| |q |qq rr| (6.10) + // | x = u | x < u | ??? | x > v | u < x < v | x = v | + // + // |ll p| j|i |q |qq rr| (6.11) + // | x = u | x < u | x > v | u < x < v | x = v | + // + // |ll p| j|b c| |qq rr| (6.12) + // | x = u | x < u | u < x < v | x > v | x = v | + // + // |ll |a |b c| d| rr| (6.9) + // | x < u | x = u | u < x < v | x = v | x > v | + int p = pp; + int q = qq - kvm + kup + 1; + int i = p; + int j = q; + vectorSwap(x, pp + 1, kvm - 1, qq - 1); + for (;;) { + while (x[++i] <= u) { + if (x[i] == u) { + final double xi = x[i]; + x[i] = x[++p]; + x[p] = xi; + } + } + while (x[--j] > u) { + if (x[j] > v) { + continue; + } + // u < xj <= v + final double xj = x[j]; + x[j] = x[--q]; + if (xj < v) { + x[q] = xj; + } else { + x[q] = x[--qq]; + x[qq] = xj; + } + } + // Here x[j] < v <= x[i] + if (i >= j) { + break; + } + //swap(x, i, j) + final double xi = x[j]; + final double xj = x[i]; + x[i] = xi; + x[j] = xj; + if (xi == u) { + x[i] = x[++p]; + x[p] = xi; + } + if (xj < v) { + x[j] = x[--q]; + x[q] = xj; + } else if (xj == v) { + x[j] = x[--q]; + x[q] = x[--qq]; + x[qq] = xj; + } + } + a = ll + i - p - 1; + b = a + p + 1 - ll; + d = rr - q + 1 + j; + c = d - rr + qq - 1; + vectorSwap(x, i, q - 1, qq - 1); + //vectorSwap(x, ll, p, j); + //vectorSwap(x, c + 1, qq - 1, rr); + vectorSwapL(x, ll, p, j, u); + vectorSwapR(x, c + 1, qq - 1, rr, v); + } + + // Step 5/6/7: Stopping test, reduction and recursion + // |l |a |b c| d| r| + // | x < u | x = u | u < x < v | x = v | x > v | + if (a <= k) { + l = b; + } + if (c < k) { + l = d + 1; + } + if (k <= d) { + r = c; + } + if (k < b) { + r = a - 1; + } + if (l >= r) { + if (l == r) { + // [b, c] + bounds[0] = bounds[1] = k; + } else { + // l > r + bounds[0] = r + 1; + bounds[1] = l - 1; + } + return; + } + } + } + + /** + * Vector swap x[a:b] <-> x[b+1:c] means the first m = min(b+1-a, c-b) + * elements of the array x[a:c] are exchanged with its last m elements. + * + * @param x Array. + * @param a Index. + * @param b Index. + * @param c Index. + */ + private static void vectorSwap(double[] x, int a, int b, int c) { + for (int i = a - 1, j = c + 1, m = Math.min(b + 1 - a, c - b); --m >= 0;) { + final double v = x[++i]; + x[i] = x[--j]; + x[j] = v; + } + } + + /** + * Vector swap x[a:b] <-> x[b+1:c] means the first m = min(b+1-a, c-b) + * elements of the array x[a:c] are exchanged with its last m elements. + * + *

This is a specialisation of {@link #vectorSwap(double[], int, int, int)} + * where the current left-most value is a constant {@code v}. + * + * @param x Array. + * @param a Index. + * @param b Index. + * @param c Index. + * @param v Constant value in [a, b] + */ + private static void vectorSwapL(double[] x, int a, int b, int c, double v) { + for (int i = a - 1, j = c + 1, m = Math.min(b + 1 - a, c - b); --m >= 0;) { + x[++i] = x[--j]; + x[j] = v; + } + } + + /** + * Vector swap x[a:b] <-> x[b+1:c] means the first m = min(b+1-a, c-b) + * elements of the array x[a:c] are exchanged with its last m elements. + * + *

This is a specialisation of {@link #vectorSwap(double[], int, int, int)} + * where the current right-most value is a constant {@code v}. + * + * @param x Array. + * @param a Index. + * @param b Index. + * @param c Index. + * @param v Constant value in (b, c] + */ + private static void vectorSwapR(double[] x, int a, int b, int c, double v) { + for (int i = a - 1, j = c + 1, m = Math.min(b + 1 - a, c - b); --m >= 0;) { + x[--j] = x[++i]; + x[i] = v; + } + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

The method assumes all {@code k} are valid indices into the data in {@code [0, length)}. + * It assumes no NaNs or signed zeros in the data. Data must be pre- and post-processed. + * + *

Uses the configured single-pivot quicksort method; + * and median-of-medians algorithm for pivot selection with medians-of-5. + * + *

Note: + *

This method is not configurable with the exception of the single-pivot quickselect method + * and the size to stop quickselect recursion and finish using sort select. It has been superceded by + * {@link #partitionLinear(double[], int[], int)} which has configurable deterministic + * pivot selection including those using partition expansion in-place of full partitioning. + * + * @param data Values. + * @param k Indices (may be destructively modified). + * @param n Count of indices. + */ + void partitionLSP(double[] data, int[] k, int n) { + linearSelect(getSPFunction(), data, k, n); + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

The method assumes all {@code k} are valid indices into the data. + * It handles NaN and signed zeros in the data. + * + *

Uses the median-of-medians algorithm for pivot selection. + * + *

WARNING: Currently this only supports a single or range of {@code k}. + * For parity with other select methods this accepts an array {@code k} and pre/post + * processes the data for NaN and signed zeros. + * + * @param part Partition function. + * @param a Values. + * @param k Indices (may be destructively modified). + * @param count Count of indices. + */ + private void linearSelect(SPEPartition part, double[] a, int[] k, int count) { + // Handle NaN / signed zeros + final DoubleDataTransformer t = SORT_TRANSFORMER.get(); + // Assume this is in-place + t.preProcess(a); + final int end = t.length(); + int n = count; + if (end > 1) { + // Filter indices invalidated by NaN check + if (end < a.length) { + for (int i = n; --i >= 0;) { + final int v = k[i]; + if (v >= end) { + // swap(k, i, --n) + k[i] = k[--n]; + k[n] = v; + } + } + } + if (n != 0) { + final int ka = Math.min(k[0], k[n - 1]); + final int kb = Math.max(k[0], k[n - 1]); + linearSelect(part, a, 0, end - 1, ka, kb, new int[2]); + } + } + // Restore signed zeros + t.postProcess(a, k, n); + } + + /** + * Partition the array such that indices {@code k} correspond to their + * correctly sorted value in the equivalent fully sorted array. + * + *

For all indices {@code [ka, kb]} and any index {@code i}: + * + *

{@code
+     * data[i < ka] <= data[ka] <= data[kb] <= data[kb < i]
+     * }
+ * + *

This function accepts indices {@code [ka, kb]} that define the + * range of indices to partition. It is expected that the range is small. + * + *

Uses quickselect with median-of-medians pivot selection to provide Order(n) + * performance. + * + *

Returns the bounds containing {@code [ka, kb]}. These may be lower/higher + * than the keys if equal values are present in the data. This is to be used by + * {@link #pivotMedianOfMedians(SPEPartition, double[], int, int, int[])} to identify + * the equal value range of the pivot. + * + * @param part Partition function. + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param ka First key of interest. + * @param kb Last key of interest. + * @param bounds Bounds of the range containing {@code [ka, kb]} (inclusive). + * @see Median of medians (Wikipedia) + */ + private void linearSelect(SPEPartition part, double[] a, int left, int right, int ka, int kb, + int[] bounds) { + int l = left; + int r = right; + while (true) { + // Select when ka and kb are close to the same end + // |l|-----|ka|kkkkkkkk|kb|------|r| + // Optimal value for this is much higher than standard quickselect due + // to the high cost of median-of-medians pivot computation and reuse via + // mutual recursion so we have a different value. + if (Math.min(kb - l, r - ka) < linearSortSelectSize) { + sortSelectRange(a, l, r, ka, kb); + // We could scan left/right to extend the bounds here after the sort. + // Since the move_sample strategy is not generally useful we do not bother. + bounds[0] = ka; + bounds[1] = kb; + return; + } + int p0 = pivotMedianOfMedians(part, a, l, r, bounds); + if ((controlFlags & FLAG_MOVE_SAMPLE) != 0) { + // Note: medians with 5 elements creates a sample size of 20%. + // Avoid partitioning the sample known to be above the pivot. + // The pivot identified the lower pivot (lp) and upper pivot (p). + // This strategy is not faster unless there are a large number of duplicates + // (e.g. less than 10 unique values). + // On random data with no duplicates this is slower. + // Note: The methods based on quickselect adaptive create the sample in + // a region corresponding to expected k and expand the partition (faster). + // + // |l |lp p0| rr| r| + // | < | == | > | ??? | + // + // Move region above P to r + // + // |l |pp p0| r| + // | < | == | ??? | > | + final int lp = bounds[0]; + final int rr = bounds[1]; + vectorSwap(a, p0 + 1, rr, r); + // 20% less to partition + final int p = part.partition(a, p0, r - rr + p0, p0, bounds); + // |l |pp |p0 |p u| r| + // | < | == | < | == | > | + // + // Move additional equal pivot region to the centre: + // |l |p0 u| r| + // | < | == | > | + vectorSwapL(a, lp, p0 - 1, p - 1, a[p]); + p0 = p - p0 + lp; + } else { + p0 = part.partition(a, l, r, p0, bounds); + } + final int p1 = bounds[0]; + + // Note: Here we expect [ka, kb] to be small and splitting is unlikely. + // p0 p1 + // |l|--|ka|kkkk|kb|--|P|-------------------|r| + // |l|----------------|P|--|ka|kkk|kb|------|r| + // |l|-----------|ka|k|P|k|kb|--------------|r| + if (kb < p0) { + // Entirely on left side + r = p0 - 1; + } else if (ka > p1) { + // Entirely on right side + l = p1 + 1; + } else { + // Pivot splits [ka, kb]. Expect ends to be close to the pivot and finish. + // Here we set the bounds for use after median-of-medians pivot selection. + // In the event there are many equal values this allows collecting those + // known to be equal together when moving around the medians sample. + bounds[0] = p0; + bounds[1] = p1; + if (ka < p0) { + sortSelectRight(a, l, p0 - 1, ka); + bounds[0] = ka; + } + if (kb > p1) { + sortSelectLeft(a, p1 + 1, r, kb); + bounds[1] = kb; + } + return; + } + } + } + + /** + * Compute the median-of-medians pivot. Divides the length {@code n} into groups + * of at most 5 elements, computes the median of each group, and the median of the + * {@code n/5} medians. Assumes {@code l <= r}. + * + *

The median-of-medians in computed in-place at the left end. The range containing + * the medians is {@code [l, rr]} with the right bound {@code rr} returned. + * In the event the pivot is a region of equal values, the range of the pivot values + * is {@code [lp, p]}, with the {@code p} returned and {@code lp} set in the output bounds. + * + * @param part Partition function. + * @param a Values. + * @param l Lower bound of data (inclusive, assumed to be strictly positive). + * @param r Upper bound of data (inclusive, assumed to be strictly positive). + * @param bounds Bounds {@code [lp, rr]}. + * @return the pivot index {@code p} + */ + private int pivotMedianOfMedians(SPEPartition part, double[] a, int l, int r, int[] bounds) { + // Process blocks of 5. + // Moves the median of each block to the left of the array. + int rr = l - 1; + for (int e = l + 5;; e += 5) { + if (e > r) { + // Final block of size 1-5 + Sorting.sort(a, e - 5, r); + final int m = (e - 5 + r) >>> 1; + final double v = a[m]; + a[m] = a[++rr]; + a[rr] = v; + break; + } + + // Various methods for time-critical step. + // Each must be compiled and run on the same benchmark data. + // Decision tree is fastest. + //final int m = Sorting.median5(a, e - 5); + //final int m = Sorting.median5(a, e - 5, e - 4, e - 3, e - 2, e - 1); + // Bigger decision tree (same as median5) + //final int m = Sorting.median5b(a, e - 5); + // Sorting network of 4 + insertion (3-4% slower) + //final int m = Sorting.median5c(a, e - 5); + // In-place median: Sorting of 5, or median of 5 + final int m = e - 3; + //Sorting.sort(a, e - 5, e - 1); // insertion sort + //Sorting.sort5(a, e - 5, e - 4, e - 3, e - 2, e - 1); + Sorting.median5d(a, e - 5, e - 4, e - 3, e - 2, e - 1); + + final double v = a[m]; + a[m] = a[++rr]; + a[rr] = v; + } + + int m = (l + rr + 1) >>> 1; + // mutual recursion + linearSelect(part, a, l, rr, m, m, bounds); + // bounds contains the range of the pivot. + // return the upper pivot and record the end of the range. + m = bounds[1]; + bounds[1] = rr; + return m; + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

The method assumes all {@code k} are valid indices into the data in {@code [0, length)}. + * It assumes no NaNs or signed zeros in the data. Data must be pre- and post-processed. + * + *

Uses the median-of-medians algorithm to provide Order(n) performance. + * This method has configurable deterministic pivot selection including those using + * partition expansion in-place of full partitioning. The methods are based on the + * QuickselectAdaptive method of Alexandrescu. + * + * @param data Values. + * @param k Indices (may be destructively modified). + * @param n Count of indices. + * @see #setLinearStrategy(LinearStrategy) + */ + void partitionLinear(double[] data, int[] k, int n) { + quickSelect(linearSpFunction, data, k, n); + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

The method assumes all {@code k} are valid indices into the data. + * It handles NaN and signed zeros in the data. + * + *

This method assumes that the partition function can compute a pivot. + * It is used for variants of the median-of-medians algorithm which use mutual + * recursion for pivot selection. + * + *

WARNING: Currently this only supports a single or range of {@code k}. + * For parity with other select methods this accepts an array {@code k} and pre/post + * processes the data for NaN and signed zeros. + * + * @param part Partition function. + * @param a Values. + * @param k Indices (may be destructively modified). + * @param count Count of indices. + * @see #setLinearStrategy(LinearStrategy) + */ + private void quickSelect(SPEPartition part, double[] a, int[] k, int count) { + // Handle NaN / signed zeros + final DoubleDataTransformer t = SORT_TRANSFORMER.get(); + // Assume this is in-place + t.preProcess(a); + final int end = t.length(); + int n = count; + if (end > 1) { + // Filter indices invalidated by NaN check + if (end < a.length) { + for (int i = n; --i >= 0;) { + final int v = k[i]; + if (v >= end) { + // swap(k, i, --n) + k[i] = k[--n]; + k[n] = v; + } + } + } + if (n != 0) { + final int ka = Math.min(k[0], k[n - 1]); + final int kb = Math.max(k[0], k[n - 1]); + quickSelect(part, a, 0, end - 1, ka, kb, new int[2]); + } + } + // Restore signed zeros + t.postProcess(a, k, n); + } + + /** + * Partition the array such that indices {@code k} correspond to their + * correctly sorted value in the equivalent fully sorted array. + * + *

For all indices {@code [ka, kb]} and any index {@code i}: + * + *

{@code
+     * data[i < ka] <= data[ka] <= data[kb] <= data[kb < i]
+     * }
+ * + *

This function accepts indices {@code [ka, kb]} that define the + * range of indices to partition. It is expected that the range is small. + * + *

This method assumes that the partition function can compute a pivot. + * It is used for variants of the median-of-medians algorithm which use mutual + * recursion for pivot selection. This method is based on the improvements + * for median-of-medians algorithms in Alexandrescu (2016) (median-of-medians + * and median-of-median-of-medians). + * + *

Returns the bounds containing {@code [ka, kb]}. These may be lower/higher + * than the keys if equal values are present in the data. + * + * @param part Partition function. + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param ka First key of interest. + * @param kb Last key of interest. + * @param bounds Bounds of the range containing {@code [ka, kb]} (inclusive). + * @see #setLinearStrategy(LinearStrategy) + */ + private void quickSelect(SPEPartition part, double[] a, int left, int right, int ka, int kb, + int[] bounds) { + int l = left; + int r = right; + while (true) { + // Select when ka and kb are close to the same end + // |l|-----|ka|kkkkkkkk|kb|------|r| + // Optimal value for this is much higher than standard quickselect due + // to the high cost of median-of-medians pivot computation and reuse via + // mutual recursion so we have a different value. + // Note: Use of this will not break the Order(n) performance for worst + // case data, i.e. data where all values require full insertion. + // This will be Order(n * k) == Order(n); k becomes a multiplier as long as + // k << n; otherwise worst case is Order(n^2 / 2) when k=n/2. + if (Math.min(kb - l, r - ka) < linearSortSelectSize) { + sortSelectRange(a, l, r, ka, kb); + // We could scan left/right to extend the bounds here after the sort. + // Attempts to do this were not measurable in benchmarking. + bounds[0] = ka; + bounds[1] = kb; + return; + } + // Only target ka; kb is assumed to be close + final int p0 = part.partition(a, l, r, ka, bounds); + final int p1 = bounds[0]; + + // Note: Here we expect [ka, kb] to be small and splitting is unlikely. + // p0 p1 + // |l|--|ka|kkkk|kb|--|P|-------------------|r| + // |l|----------------|P|--|ka|kkk|kb|------|r| + // |l|-----------|ka|k|P|k|kb|--------------|r| + if (kb < p0) { + // Entirely on left side + r = p0 - 1; + } else if (ka > p1) { + // Entirely on right side + l = p1 + 1; + } else { + // Pivot splits [ka, kb]. Expect ends to be close to the pivot and finish. + // Here we set the bounds for use after median-of-medians pivot selection. + // In the event there are many equal values this allows collecting those + // known to be equal together when moving around the medians sample. + bounds[0] = p0; + bounds[1] = p1; + if (ka < p0) { + sortSelectRight(a, l, p0 - 1, ka); + bounds[0] = ka; + } + if (kb > p1) { + sortSelectLeft(a, p1 + 1, r, kb); + bounds[1] = kb; + } + return; + } + } + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

The method assumes all {@code k} are valid indices into the data in {@code [0, length)}. + * It assumes no NaNs or signed zeros in the data. Data must be pre- and post-processed. + * + *

Uses the QuickselectAdaptive method of Alexandrescu. This is based on the + * median-of-medians algorithm. The median sample strategy is chosen based on + * the target index. + * + * @param data Values. + * @param k Indices (may be destructively modified). + * @param n Count of indices. + */ + void partitionQA(double[] data, int[] k, int n) { + quickSelectAdaptive(data, k, n); + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

The method assumes all {@code k} are valid indices into the data. + * It handles NaN and signed zeros in the data. + * + *

WARNING: Currently this only supports a single or range of {@code k}. + * For parity with other select methods this accepts an array {@code k} and pre/post + * processes the data for NaN and signed zeros. + * + * @param a Values. + * @param k Indices (may be destructively modified). + * @param count Count of indices. + */ + private void quickSelectAdaptive(double[] a, int[] k, int count) { + // Handle NaN / signed zeros + final DoubleDataTransformer t = SORT_TRANSFORMER.get(); + // Assume this is in-place + t.preProcess(a); + final int end = t.length(); + int n = count; + if (end > 1) { + // Filter indices invalidated by NaN check + if (end < a.length) { + for (int i = n; --i >= 0;) { + final int v = k[i]; + if (v >= end) { + // swap(k, i, --n) + k[i] = k[--n]; + k[n] = v; + } + } + } + if (n != 0) { + final int ka = Math.min(k[0], k[n - 1]); + final int kb = Math.max(k[0], k[n - 1]); + quickSelectAdaptive(a, 0, end - 1, ka, kb, new int[1], adaptMode); + } + } + // Restore signed zeros + t.postProcess(a, k, n); + } + + /** + * Partition the array such that indices {@code k} correspond to their + * correctly sorted value in the equivalent fully sorted array. + * + *

For all indices {@code [ka, kb]} and any index {@code i}: + * + *

{@code
+     * data[i < ka] <= data[ka] <= data[kb] <= data[kb < i]
+     * }
+ * + *

This function accepts indices {@code [ka, kb]} that define the + * range of indices to partition. It is expected that the range is small. + * + *

Uses the QuickselectAdaptive method of Alexandrescu. This is based on the + * median-of-medians algorithm. The median sample is strategy is chosen based on + * the target index. + * + *

The adaption {@code mode} is used to control the sampling mode and adaption of + * the index within the sample. + * + *

Returns the bounds containing {@code [ka, kb]}. These may be lower/higher + * than the keys if equal values are present in the data. + * + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param ka First key of interest. + * @param kb Last key of interest. + * @param bounds Upper bound of the range containing {@code [ka, kb]} (inclusive). + * @param mode Adaption mode. + * @return Lower bound of the range containing {@code [ka, kb]} (inclusive). + */ + private int quickSelectAdaptive(double[] a, int left, int right, int ka, int kb, + int[] bounds, AdaptMode mode) { + int l = left; + int r = right; + AdaptMode m = mode; + while (true) { + // Select when ka and kb are close to the same end + // |l|-----|ka|kkkkkkkk|kb|------|r| + // Optimal value for this is much higher than standard quickselect due + // to the high cost of median-of-medians pivot computation and reuse via + // mutual recursion so we have a different value. + if (Math.min(kb - l, r - ka) < linearSortSelectSize) { + sortSelectRange(a, l, r, ka, kb); + bounds[0] = kb; + return ka; + } + + // Only target ka; kb is assumed to be close + int p0; + int n = r - l; + // f in [0, 1] + final double f = (double) (ka - l) / n; + // Note: Margins for fraction left/right of pivot L : R. + // Subtract the larger margin to create the estimated size + // after partitioning. If the new size subtracted from + // the estimated size is negative (partition did not meet + // the margin guarantees) then adaption mode is changed (to + // a method more likely to achieve the margins). + if (f <= STEP_LEFT) { + if (m.isSampleMode() && n > subSamplingSize) { + // Floyd-Rivest sampling. Expect to eliminate the same as QA steps. + if (f <= STEP_FAR_LEFT) { + n -= (n >> 2) + (n >> 5) + (n >> 6); + } else { + n -= (n >> 2) + (n >> 3); + } + p0 = sampleStep(a, l, r, ka, bounds, m); + } else if (f <= STEP_FAR_LEFT) { + if ((controlFlags & FLAG_QA_FAR_STEP) != 0) { + // 1/12 : 1/3 (use 1/4 + 1/32 + 1/64 ~ 0.328) + n -= (n >> 2) + (n >> 5) + (n >> 6); + p0 = repeatedStepFarLeft(a, l, r, ka, bounds, m); + } else { + // 1/12 : 3/8 + n -= (n >> 2) + (n >> 3); + p0 = repeatedStepLeft(a, l, r, ka, bounds, m, true); + } + } else { + // 1/6 : 1/4 + n -= n >> 2; + p0 = repeatedStepLeft(a, l, r, ka, bounds, m, false); + } + } else if (f >= STEP_RIGHT) { + if (m.isSampleMode() && n > subSamplingSize) { + // Floyd-Rivest sampling. Expect to eliminate the same as QA steps. + if (f >= STEP_FAR_RIGHT) { + n -= (n >> 2) + (n >> 5) + (n >> 6); + } else { + n -= (n >> 2) + (n >> 3); + } + p0 = sampleStep(a, l, r, ka, bounds, m); + } else if (f >= STEP_FAR_RIGHT) { + if ((controlFlags & FLAG_QA_FAR_STEP) != 0) { + // 1/12 : 1/3 (use 1/4 + 1/32 + 1/64 ~ 0.328) + n -= (n >> 2) + (n >> 5) + (n >> 6); + p0 = repeatedStepFarRight(a, l, r, ka, bounds, m); + } else { + // 3/8 : 1/12 + n -= (n >> 2) + (n >> 3); + p0 = repeatedStepRight(a, l, r, ka, bounds, m, true); + } + } else { + // 1/4 : 1/6 + n -= n >> 2; + p0 = repeatedStepRight(a, l, r, ka, bounds, m, false); + } + } else { + if (m.isSampleMode() && n > subSamplingSize) { + // Floyd-Rivest sampling. Expect to eliminate the same as QA steps. + p0 = sampleStep(a, l, r, ka, bounds, m); + } else { + p0 = repeatedStep(a, l, r, ka, bounds, m); + } + // 2/9 : 2/9 (use 1/4 - 1/32 ~ 0.219) + n -= (n >> 2) - (n >> 5); + } + + // Note: Here we expect [ka, kb] to be small and splitting is unlikely. + // p0 p1 + // |l|--|ka|kkkk|kb|--|P|-------------------|r| + // |l|----------------|P|--|ka|kkk|kb|------|r| + // |l|-----------|ka|k|P|k|kb|--------------|r| + final int p1 = bounds[0]; + if (kb < p0) { + // Entirely on left side + r = p0 - 1; + } else if (ka > p1) { + // Entirely on right side + l = p1 + 1; + } else { + // Pivot splits [ka, kb]. Expect ends to be close to the pivot and finish. + // Here we set the bounds for use after median-of-medians pivot selection. + // In the event there are many equal values this allows collecting those + // known to be equal together when moving around the medians sample. + if (kb > p1) { + sortSelectLeft(a, p1 + 1, r, kb); + bounds[0] = kb; + } + if (ka < p0) { + sortSelectRight(a, l, p0 - 1, ka); + p0 = ka; + } + return p0; + } + // Update sampling mode + m = m.update(n, l, r); + } + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

The method assumes all {@code k} are valid indices into the data in {@code [0, length)}. + * It assumes no NaNs or signed zeros in the data. Data must be pre- and post-processed. + * + *

Uses the QuickselectAdaptive method of Alexandrescu. This is based on the + * median-of-medians algorithm. The median sample is strategy is chosen based on + * the target index. + * + *

Differences to QA + * + *

This function is not as configurable as {@link #partitionQA(double[], int[], int)}; + * it is composed of the best performing configuration from benchmarking. + * + *

A key difference is that this method allows starting with Floyd-Rivest sub-sampling, + * then progression to QuickselectAdaptive sampling, before disabling of sampling. This + * method can thus have two attempts at sampling (FR, then QA) before disabling sampling. The + * method can also be configured to start at QA sampling, or skip QA sampling when starting + * with FR sampling depending on the configured starting mode and increment + * (see {@link #configureQaAdaptive(int, int)}). + * + * @param data Values. + * @param k Indices (may be destructively modified). + * @param n Count of indices. + */ + static void partitionQA2(double[] data, int[] k, int n) { + quickSelectAdaptive2(data, k, n, qaMode); + } + + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} and + * any index {@code i}: + * + *

{@code
+     * data[i < k] <= data[k] <= data[k < i]
+     * }
+ * + *

The method assumes all {@code k} are valid indices into the data. It handles NaN + * and signed zeros in the data. + * + *

WARNING: Currently this only supports a single or range of {@code k}. For parity + * with other select methods this accepts an array {@code k} and pre/post processes + * the data for NaN and signed zeros. + * + * @param a Values. + * @param k Indices (may be destructively modified). + * @param count Count of indices. + * @param flags Adaption flags. + */ + static void quickSelectAdaptive2(double[] a, int[] k, int count, int flags) { + // Handle NaN / signed zeros + final DoubleDataTransformer t = SORT_TRANSFORMER.get(); + // Assume this is in-place + t.preProcess(a); + final int end = t.length(); + int n = count; + if (end > 1) { + // Filter indices invalidated by NaN check + if (end < a.length) { + for (int i = n; --i >= 0;) { + final int v = k[i]; + if (v >= end) { + // swap(k, i, --n) + k[i] = k[--n]; + k[n] = v; + } + } + } + if (n != 0) { + final int ka = Math.min(k[0], k[n - 1]); + final int kb = Math.max(k[0], k[n - 1]); + quickSelectAdaptive2(a, 0, end - 1, ka, kb, new int[1], flags); + } + } + // Restore signed zeros + t.postProcess(a, k, n); + } + + /** + * Partition the array such that indices {@code k} correspond to their + * correctly sorted value in the equivalent fully sorted array. + * + *

For all indices {@code [ka, kb]} and any index {@code i}: + * + *

{@code
+     * data[i < ka] <= data[ka] <= data[kb] <= data[kb < i]
+     * }
+ * + *

This function accepts indices {@code [ka, kb]} that define the + * range of indices to partition. It is expected that the range is small. + * + *

Uses the QuickselectAdaptive method of Alexandrescu. This is based on the + * median-of-medians algorithm. The median sample is strategy is chosen based on + * the target index. + * + *

The control {@code flags} are used to control the sampling mode and adaption of + * the index within the sample. + * + *

Returns the bounds containing {@code [ka, kb]}. These may be lower/higher + * than the keys if equal values are present in the data. + * + * @param a Values. + * @param left Lower bound of data (inclusive, assumed to be strictly positive). + * @param right Upper bound of data (inclusive, assumed to be strictly positive). + * @param ka First key of interest. + * @param kb Last key of interest. + * @param bounds Upper bound of the range containing {@code [ka, kb]} (inclusive). + * @param flags Adaption flags. + * @return Lower bound of the range containing {@code [ka, kb]} (inclusive). + */ + private static int quickSelectAdaptive2(double[] a, int left, int right, int ka, int kb, + int[] bounds, int flags) { + int l = left; + int r = right; + int m = flags; + while (true) { + // Select when ka and kb are close to the same end + // |l|-----|ka|kkkkkkkk|kb|------|r| + if (Math.min(kb - l, r - ka) < LINEAR_SORTSELECT_SIZE) { + sortSelectRange(a, l, r, ka, kb); + bounds[0] = kb; + return ka; + } + + // Only target ka; kb is assumed to be close + int p0; + final int n = r - l; + // f in [0, 1] + final double f = (double) (ka - l) / n; + // Record the larger margin (start at 1/4) to create the estimated size. + // step L R + // far left 1/12 1/3 (use 1/4 + 1/32 + 1/64 ~ 0.328) + // left 1/6 1/4 + // middle 2/9 2/9 (use 1/4 - 1/32 ~ 0.219) + int margin = n >> 2; + if (m < MODE_SAMPLING && r - l > SELECT_SUB_SAMPLING_SIZE) { + // Floyd-Rivest sample step uses the same margins + p0 = sampleStep(a, l, r, ka, bounds, flags); + if (f <= STEP_FAR_LEFT || f >= STEP_FAR_RIGHT) { + margin += (n >> 5) + (n >> 6); + } else if (f > STEP_LEFT && f < STEP_RIGHT) { + margin -= n >> 5; + } + } else if (f <= STEP_LEFT) { + if (f <= STEP_FAR_LEFT) { + margin += (n >> 5) + (n >> 6); + p0 = repeatedStepFarLeft(a, l, r, ka, bounds, m); + } else { + p0 = repeatedStepLeft(a, l, r, ka, bounds, m); + } + } else if (f >= STEP_RIGHT) { + if (f >= STEP_FAR_RIGHT) { + margin += (n >> 5) + (n >> 6); + p0 = repeatedStepFarRight(a, l, r, ka, bounds, m); + } else { + p0 = repeatedStepRight(a, l, r, ka, bounds, m); + } + } else { + margin -= n >> 5; + p0 = repeatedStep(a, l, r, ka, bounds, m); + } + + // Note: Here we expect [ka, kb] to be small and splitting is unlikely. + // p0 p1 + // |l|--|ka|kkkk|kb|--|P|-------------------|r| + // |l|----------------|P|--|ka|kkk|kb|------|r| + // |l|-----------|ka|k|P|k|kb|--------------|r| + final int p1 = bounds[0]; + if (kb < p0) { + // Entirely on left side + r = p0 - 1; + } else if (ka > p1) { + // Entirely on right side + l = p1 + 1; + } else { + // Pivot splits [ka, kb]. Expect ends to be close to the pivot and finish. + // Here we set the bounds for use after median-of-medians pivot selection. + // In the event there are many equal values this allows collecting those + // known to be equal together when moving around the medians sample. + if (kb > p1) { + sortSelectLeft(a, p1 + 1, r, kb); + bounds[0] = kb; + } + if (ka < p0) { + sortSelectRight(a, l, p0 - 1, ka); + p0 = ka; + } + return p0; + } + // Update mode based on target partition size + m += r - l > n - margin ? qaIncrement : 0; + } + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Implements {@link SPEPartitionFunction}. This method is not static as the + * pivot strategy and minimum quick select size are used within the method. + * + *

Note: Handles signed zeros. + * + *

Uses a Bentley-McIlroy quicksort partition method by Sedgewick. + * + * @param data Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param upper Upper bound (inclusive) of the pivot range. + * @param leftInner Flag to indicate {@code left - 1} is a pivot. + * @param rightInner Flag to indicate {@code right + 1} is a pivot. + * @return Lower bound (inclusive) of the pivot range. + */ + private int partitionSBMWithZeros(double[] data, int left, int right, int[] upper, + boolean leftInner, boolean rightInner) { + // Single-pivot Bentley-McIlroy quicksort handling equal keys (Sedgewick's algorithm). + // + // Partition data using pivot P into less-than, greater-than or equal. + // P is placed at the end to act as a sentinel. + // k traverses the unknown region ??? and values moved if equal (l) or greater (g): + // + // left p i j q right + // | ==P |

P | ==P |P| + // + // At the end P and additional equal values are swapped back to the centre. + // + // |

P | + // + // Adapted from Sedgewick "Quicksort is optimal" + // https://sedgewick.io/wp-content/themes/sedgewick/talks/2002QuicksortIsOptimal.pdf + // + // The algorithm has been changed so that: + // - A pivot point must be provided. + // - An edge case where the search meets in the middle is handled. + // - Equal value data is not swapped to the end. Since the value is fixed then + // only the less than / greater than value must be moved from the end inwards. + // The end is then assumed to be the equal value. This would not work with + // object references. Equivalent swap calls are commented. + // - Added a fast-forward over initial range containing the pivot. + + // Switch to insertion sort for small range + if (right - left <= minQuickSelectSize) { + Sorting.sort(data, left, right, leftInner); + fixContinuousSignedZeros(data, left, right); + upper[0] = right; + return left; + } + + final int l = left; + final int r = right; + + int p = l; + int q = r; + + // Use the pivot index to set the upper sentinel value. + // Pass -1 as the target k (should trigger an IOOBE if strategy uses it). + final int pivot = pivotingStrategy.pivotIndex(data, left, right, -1); + final double v = data[pivot]; + data[pivot] = data[r]; + data[r] = v; + + // Special case: count signed zeros + int c = 0; + if (v == 0) { + c = countMixedSignedZeros(data, left, right); + } + + // Fast-forward over equal regions to reduce swaps + while (data[p] == v) { + if (++p == q) { + // Edge-case: constant value + if (c != 0) { + sortZero(data, left, right); + } + upper[0] = right; + return left; + } + } + // Cannot overrun as the prior scan using p stopped before the end + while (data[q - 1] == v) { + q--; + } + + int i = p - 1; + int j = q; + + for (;;) { + do { + ++i; + } while (data[i] < v); + while (v < data[--j]) { + if (j == l) { + break; + } + } + if (i >= j) { + // Edge-case if search met on an internal pivot value + // (not at the greater equal region, i.e. i < q). + // Move this to the lower-equal region. + if (i == j && v == data[i]) { + //swap(data, i++, p++) + data[i] = data[p]; + data[p] = v; + i++; + p++; + } + break; + } + //swap(data, i, j) + final double vi = data[j]; + final double vj = data[i]; + data[i] = vi; + data[j] = vj; + // Move the equal values to the ends + if (vi == v) { + //swap(data, i, p++) + data[i] = data[p]; + data[p] = v; + p++; + } + if (vj == v) { + //swap(data, j, --q) + data[j] = data[--q]; + data[q] = v; + } + } + // i is at the end (exclusive) of the less-than region + + // Place pivot value in centre + //swap(data, r, i) + data[r] = data[i]; + data[i] = v; + + // Move equal regions to the centre. + // Set the pivot range [j, i) and move this outward for equal values. + j = i++; + + // less-equal: + // for (int k = l; k < p; k++): + // swap(data, k, --j) + // greater-equal: + // for (int k = r; k-- > q; i++) { + // swap(data, k, i) + + // Move the minimum of less-equal or less-than + int move = Math.min(p - l, j - p); + final int lower = j - (p - l); + for (int k = l; move-- > 0; k++) { + data[k] = data[--j]; + data[j] = v; + } + // Move the minimum of greater-equal or greater-than + move = Math.min(r - q, q - i); + upper[0] = i + (r - q) - 1; + for (int k = r; move-- > 0; i++) { + data[--k] = data[i]; + data[i] = v; + } + + // Special case: fixed signed zeros + if (c != 0) { + p = lower; + while (c-- > 0) { + data[p++] = -0.0; + } + while (p <= upper[0]) { + data[p++] = 0.0; + } + } + + // Equal in [lower, upper] + return lower; + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Uses a single pivot partition method. This method does not handle equal values + * at the pivot location: {@code lower == upper}. The method conforms to the + * {@link SPEPartition} interface to allow use with the single-pivot introselect method. + * + * @param data Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param pivot Pivot index. + * @param upper Upper bound (inclusive) of the pivot range. + * @return Lower bound (inclusive) of the pivot range. + */ + private static int partitionSP(double[] data, int l, int r, int pivot, int[] upper) { + // Partition data using pivot P into less-than or greater-than. + // + // Adapted from Floyd and Rivest (1975) + // Algorithm 489: The Algorithm SELECT—for Finding the ith Smallest of n elements. + // Comm. ACM. 18 (3): 173. + // + // Sub-script range checking has been eliminated by appropriate placement + // of values at the ends to act as sentinels. + // + // left i j right + // |<=P|

P |>=P| + // + // At the end P is swapped back to the centre. + // + // |

P | + final double v = data[pivot]; + // swap(left, pivot) + data[pivot] = data[l]; + if (data[r] > v) { + // swap(right, left) + data[l] = data[r]; + data[r] = v; + // Here after the first swap: a[l] = v; a[r] > v + } else { + data[l] = v; + // Here after the first swap: a[l] <= v; a[r] = v + } + int i = l; + int j = r; + while (i < j) { + // swap(i, j) + final double temp = data[i]; + data[i] = data[j]; + data[j] = temp; + do { + ++i; + } while (data[i] < v); + do { + --j; + } while (data[j] > v); + } + // Move pivot back to the correct location from either l or r + if (data[l] == v) { + // data[j] <= v : swap(left, j) + data[l] = data[j]; + data[j] = v; + } else { + // data[j+1] > v : swap(j+1, right) + data[r] = data[++j]; + data[j] = v; + } + upper[0] = j; + return j; + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Uses a Bentley-McIlroy quicksort partition method. + * + * @param data Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param pivot Initial index of the pivot. + * @param upper Upper bound (inclusive) of the pivot range. + * @return Lower bound (inclusive) of the pivot range. + */ + private static int partitionBM(double[] data, int l, int r, int pivot, int[] upper) { + // Single-pivot Bentley-McIlroy quicksort handling equal keys. + // + // Adapted from program 7 in Bentley-McIlroy (1993) + // Engineering a sort function + // SOFTWARE—PRACTICE AND EXPERIENCE, VOL.23(11), 1249–1265 + // + // 3-way partition of the data using a pivot value into + // less-than, equal or greater-than. + // + // First partition data into 4 reqions by scanning the unknown region from + // left (i) and right (j) and moving equal values to the ends: + // i-> <-j + // l p | | q r + // | == | < | ??? | > | == | + // + // <-j + // l p i q r + // | == | < | > | == | + // + // Then the equal values are copied from the ends to the centre: + // | less | equal | greater | + + int i = l; + int j = r; + int p = l; + int q = r; + + final double v = data[pivot]; + + for (;;) { + while (i <= j && data[i] <= v) { + if (data[i] == v) { + //swap(data, i, p++) + data[i] = data[p]; + data[p] = v; + p++; + } + i++; + } + while (j >= i && data[j] >= v) { + if (v == data[j]) { + //swap(data, j, q--) + data[j] = data[q]; + data[q] = v; + q--; + } + j--; + } + if (i > j) { + break; + } + //swap(data, i++, j--) + final double tmp = data[j]; + data[j] = data[i]; + data[i] = tmp; + } + + // Move equal regions to the centre. + int s = Math.min(p - l, i - p); + for (int k = l; s > 0; k++, s--) { + //swap(data, k, i - s) + data[k] = data[i - s]; + data[i - s] = v; + } + s = Math.min(q - j, r - q); + for (int k = i; --s >= 0; k++) { + //swap(data, r - s, k) + data[r - s] = data[k]; + data[k] = v; + } + + // Set output range + i = i - p + l; + j = j - q + r; + upper[0] = j; + + return i; + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Uses a Bentley-McIlroy quicksort partition method by Sedgewick. + * + * @param data Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param pivot Pivot index. + * @param upper Upper bound (inclusive) of the pivot range. + * @return Lower bound (inclusive) of the pivot range. + */ + static int partitionSBM(double[] data, int l, int r, int pivot, int[] upper) { + // Single-pivot Bentley-McIlroy quicksort handling equal keys (Sedgewick's algorithm). + // + // Partition data using pivot P into less-than, greater-than or equal. + // P is placed at the end to act as a sentinel. + // k traverses the unknown region ??? and values moved if equal (l) or greater (g): + // + // left p i j q right + // | ==P |

P | ==P |P| + // + // At the end P and additional equal values are swapped back to the centre. + // + // |

P | + // + // Adapted from Sedgewick "Quicksort is optimal" + // https://sedgewick.io/wp-content/themes/sedgewick/talks/2002QuicksortIsOptimal.pdf + // + // Note: The difference between this and the original BM partition is the use of + // < or > rather than <= and >=. This allows the pivot to act as a sentinel and removes + // the requirement for checks on i; and j can be checked against an unlikely condition. + // This method will swap runs of equal values. + // + // The algorithm has been changed so that: + // - A pivot point must be provided. + // - An edge case where the search meets in the middle is handled. + // - Added a fast-forward over any initial range containing the pivot. + // - Changed the final move to perform the minimum moves. + + // Use the pivot index to set the upper sentinel value + final double v = data[pivot]; + data[pivot] = data[r]; + data[r] = v; + + int p = l; + int q = r; + + // Fast-forward over equal regions to reduce swaps + while (data[p] == v) { + if (++p == q) { + // Edge-case: constant value + upper[0] = r; + return l; + } + } + // Cannot overrun as the prior scan using p stopped before the end + while (data[q - 1] == v) { + q--; + } + + int i = p - 1; + int j = q; + + for (;;) { + do { + ++i; + } while (data[i] < v); + while (v < data[--j]) { + // Cannot use j == i in the event that i == q (already passed j) + if (j == l) { + break; + } + } + if (i >= j) { + // Edge-case if search met on an internal pivot value + // (not at the greater equal region, i.e. i < q). + // Move this to the lower-equal region. + if (i == j && v == data[i]) { + //swap(data, i++, p++) + data[i] = data[p]; + data[p] = v; + i++; + p++; + } + break; + } + //swap(data, i, j) + final double vi = data[j]; + final double vj = data[i]; + data[i] = vi; + data[j] = vj; + // Move the equal values to the ends + if (vi == v) { + //swap(data, i, p++) + data[i] = data[p]; + data[p] = v; + p++; + } + if (vj == v) { + //swap(data, j, --q) + data[j] = data[--q]; + data[q] = v; + } + } + // i is at the end (exclusive) of the less-than region + + // Place pivot value in centre + //swap(data, r, i) + data[r] = data[i]; + data[i] = v; + + // Move equal regions to the centre. + // Set the pivot range [j, i) and move this outward for equal values. + j = i++; + + // less-equal: + // for k = l; k < p; k++ + // swap(data, k, --j) + // greater-equal: + // for k = r; k-- > q; i++ + // swap(data, k, i) + + // Move the minimum of less-equal or less-than + int move = Math.min(p - l, j - p); + final int lower = j - (p - l); + for (int k = l; --move >= 0; k++) { + data[k] = data[--j]; + data[j] = v; + } + // Move the minimum of greater-equal or greater-than + move = Math.min(r - q, q - i); + upper[0] = i + (r - q) - 1; + for (int k = r; --move >= 0; i++) { + data[--k] = data[i]; + data[i] = v; + } + + // Equal in [lower, upper] + return lower; + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Uses a Bentley-McIlroy quicksort partition method by Kiwiel. + * + * @param x Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param pivot Pivot index. + * @param upper Upper bound (inclusive) of the pivot range. + * @return Lower bound (inclusive) of the pivot range. + */ + static int partitionKBM(double[] x, int l, int r, int pivot, int[] upper) { + // Single-pivot Bentley-McIlroy quicksort handling equal keys. + // + // Partition data using pivot v into less-than, greater-than or equal. + // The basic idea is to work with the 5 inner parts of the array [ll, rr] + // by positioning sentinels at l and r: + // + // |l |ll p| |i j| |q rr| r| (6.1) + // |v | ==v |>v| + // + // until the middle part is empty or just contains an element equal to the pivot: + // + // |ll p| j| |i |q rr| (6.2) + // | ==v | v | ==v | + // + // i.e. j = i-1 or i-2, then swap the ends into the middle: + // + // |ll |a d| rr| (6.3) + // | v | + // + // Adapted from Kiwiel (2005) "On Floyd and Rivest's SELECT algorithm" + // Theoretical Computer Science 347, 214-238. + // This is the safeguarded ternary partition Scheme E with modification to + // prevent vacuous swaps of equal keys (section 5.6) in Kiwiel (2003) + // Partitioning schemes for quicksort and quickselect, + // Technical report, Systems Research Institute, Warsaw. + // http://arxiv.org/abs/cs.DS/0312054 + // + // Note: The difference between this and Sedgewick's BM is the use of sentinels + // at either end to remove index checks at both ends and changing the behaviour + // when i and j meet on a pivot value. + // + // The listing in Kiwiel (2005) has been updated: + // - p and q mark the *inclusive* end of ==v regions. + // - Added a fast-forward over initial range containing the pivot. + // - Vector swap is optimised given one side of the exchange is v. + + final double v = x[pivot]; + x[pivot] = x[l]; + x[l] = v; + + int ll = l; + int rr = r; + + // Ensure x[l] <= v <= x[r] + if (v < x[r]) { + --rr; + } else if (v > x[r]) { + x[l] = x[r]; + x[r] = v; + ++ll; + } + + // Position p and q for pre-in/decrement to write into edge pivot regions + // Fast-forward over equal regions to reduce swaps + int p = l; + while (x[p + 1] == v) { + if (++p == rr) { + // Edge-case: constant value in [ll, rr] + // Return the full range [l, r] as a single edge element + // will also be partitioned. + upper[0] = r; + return l; + } + } + // Cannot overrun as the prior scan using p stopped before the end + int q = r; + while (x[q - 1] == v) { + --q; + } + + // [ll, p] and [q, rr] are pivot + // Position for pre-in/decrement + int i = p; + int j = q; + + for (;;) { + do { + ++i; + } while (x[i] < v); + do { + --j; + } while (x[j] > v); + // Here x[j] <= v <= x[i] + if (i >= j) { + if (i == j) { + // x[i]=x[j]=v; update to leave the pivot in between (j, i) + ++i; + --j; + } + break; + } + //swap(x, i, j) + final double vi = x[j]; + final double vj = x[i]; + x[i] = vi; + x[j] = vj; + // Move the equal values to the ends + if (vi == v) { + x[i] = x[++p]; + x[p] = v; + } + if (vj == v) { + x[j] = x[--q]; + x[q] = v; + } + } + + // Set [a, d] (p and q are offset by 1 from Kiwiel) + final int a = ll + j - p; + upper[0] = rr - q + i; + + // Vector swap x[a:b] <-> x[b+1:c] means the first m = min(b+1-a, c-b) + // elements of the array x[a:c] are exchanged with its last m elements. + //vectorSwapL(x, ll, p, j, v); + //vectorSwapR(x, i, q - 1, rr, v); + // x[ll:p] <-> x[p+1:j] + for (int m = Math.min(p + 1 - ll, j - p); --m >= 0; ++ll, --j) { + x[ll] = x[j]; + x[j] = v; + } + // x[i:q-1] <-> x[q:rr] + for (int m = Math.min(q - i, rr - q + 1); --m >= 0; ++i, --rr) { + x[rr] = x[i]; + x[i] = v; + } + return a; + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Uses a Dutch-National-Flag method handling equal keys (version 1). + * + * @param data Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param pivot Pivot index. + * @param upper Upper bound (inclusive) of the pivot range. + * @return Lower bound (inclusive) of the pivot range. + */ + private static int partitionDNF1(double[] data, int left, int right, int pivot, int[] upper) { + // Dutch National Flag partitioning: + // https://www.baeldung.com/java-sorting-arrays-with-repeated-entries + // https://en.wikipedia.org/wiki/Dutch_national_flag_problem + + // Partition data using pivot P into less-than, greater-than or equal. + // i traverses the unknown region ??? and values moved to the correct end. + // + // left lt i gt right + // | < P | P | ??? | > P | + // + // We can delay filling in [lt, gt) with P until the end and only + // move values in the wrong place. + + final double value = data[pivot]; + + // Fast-forward initial less-than region + int lt = left; + while (data[lt] < value) { + lt++; + } + + // Pointers positioned to use pre-increment/decrement + lt--; + int gt = right + 1; + + // DNF partitioning which inspects one position per loop iteration + for (int i = lt; ++i < gt;) { + final double v = data[i]; + if (v < value) { + data[++lt] = v; + } else if (v > value) { + data[i] = data[--gt]; + data[gt] = v; + // Ensure data[i] is inspected next time + i--; + } + // else v == value and is in the central region to fill at the end + } + + // Equal in (lt, gt) so adjust to [lt, gt] + ++lt; + upper[0] = --gt; + + // Fill the equal values gap + for (int i = lt; i <= gt; i++) { + data[i] = value; + } + + return lt; + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Uses a Dutch-National-Flag method handling equal keys (version 2). + * + * @param data Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param pivot Pivot index. + * @param upper Upper bound (inclusive) of the pivot range. + * @return Lower bound (inclusive) of the pivot range. + */ + private static int partitionDNF2(double[] data, int left, int right, int pivot, int[] upper) { + // Dutch National Flag partitioning: + // https://www.baeldung.com/java-sorting-arrays-with-repeated-entries + // https://en.wikipedia.org/wiki/Dutch_national_flag_problem + + // Partition data using pivot P into less-than, greater-than or equal. + // i traverses the unknown region ??? and values moved to the correct end. + // + // left lt i gt right + // | < P | P | ??? | > P | + // + // We can delay filling in [lt, gt) with P until the end and only + // move values in the wrong place. + + final double value = data[pivot]; + + // Fast-forward initial less-than region + int lt = left; + while (data[lt] < value) { + lt++; + } + + // Pointers positioned to use pre-increment/decrement: ++x / --x + lt--; + int gt = right + 1; + + // Modified DNF partitioning with fast-forward of the greater-than + // pointer. Note the fast-forward must check bounds. + for (int i = lt; ++i < gt;) { + final double v = data[i]; + if (v < value) { + data[++lt] = v; + } else if (v > value) { + // Fast-forward here: + do { + --gt; + } while (gt > i && data[gt] > value); + // here data[gt] <= value + // if data[gt] == value we can skip over it + if (data[gt] < value) { + data[++lt] = data[gt]; + } + // Move v to the >P side + data[gt] = v; + } + // else v == value and is in the central region to fill at the end + } + + // Equal in (lt, gt) so adjust to [lt, gt] + ++lt; + upper[0] = --gt; + + // Fill the equal values gap + for (int i = lt; i <= gt; i++) { + data[i] = value; + } + + return lt; + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Uses a Dutch-National-Flag method handling equal keys (version 3). + * + * @param data Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param pivot Pivot index. + * @param upper Upper bound (inclusive) of the pivot range. + * @return Lower bound (inclusive) of the pivot range. + */ + private static int partitionDNF3(double[] data, int left, int right, int pivot, int[] upper) { + // Dutch National Flag partitioning: + // https://www.baeldung.com/java-sorting-arrays-with-repeated-entries + // https://en.wikipedia.org/wiki/Dutch_national_flag_problem + + // Partition data using pivot P into less-than, greater-than or equal. + // i traverses the unknown region ??? and values moved to the correct end. + // + // left lt i gt right + // | < P | P | ??? | > P | + // + // This version writes in the value of P as it traverses. Any subsequent + // less-than values will overwrite P values trailing behind i. + + final double value = data[pivot]; + + // Fast-forward initial less-than region + int lt = left; + while (data[lt] < value) { + lt++; + } + + // Pointers positioned to use pre-increment/decrement: ++x / --x + lt--; + int gt = right + 1; + + // Note: + // This benchmarks as faster than DNF1 and equal to DNF2 on random data. + // On data with (many) repeat values it is faster than DNF2. + // Both DNF2 & 3 have fast-forward of the gt pointer. + + // Modified DNF partitioning with fast-forward of the greater-than + // pointer. Here we write in the pivot value at i during the sweep. + // This acts as a sentinel when fast-forwarding greater-than. + // It is over-written by any future

pivot + for (int i = lt; ++i < gt;) { + final double v = data[i]; + if (v != value) { + // Overwrite with the pivot value + data[i] = value; + if (v < value) { + // Move v to the

value) + do { + --gt; + } while (data[gt] > value); + // Now data[gt] <= value + // if data[gt] == value we can skip over it + if (data[gt] < value) { + data[++lt] = data[gt]; + } + // Move v to the >P side + data[gt] = v; + } + } + } + + // Equal in (lt, gt) so adjust to [lt, gt] + ++lt; + upper[0] = --gt; + + // In contrast to version 1 and 2 there is no requirement to fill the central + // region with the pivot value as it was filled during the sweep + + return lt; + } + + /** + * Partition an array slice around 2 pivots. Partitioning exchanges array elements + * such that all elements smaller than pivot are before it and all elements larger + * than pivot are after it. + * + *

Uses a dual-pivot quicksort method by Vladimir Yaroslavskiy. + * + *

This method assumes {@code a[pivot1] <= a[pivot2]}. + * If {@code pivot1 == pivot2} this triggers a switch to a single-pivot method. + * It is assumed this indicates that choosing two pivots failed due to many equal + * values. In this case the single-pivot method uses a Dutch National Flag algorithm + * suitable for many equal values. + * + *

This method returns 4 points describing the pivot ranges of equal values. + * + *

{@code
+     *         |k0  k1|                |k2  k3|
+     * |   

P | + * }

+ * + * + * + *

Bounds are set so {@code i < k0}, {@code i > k3} and {@code k1 < i < k2} are + * unsorted. When the range {@code [k0, k3]} contains fully sorted elements the result + * is set to {@code k1 = k3; k2 == k0}. This can occur if + * {@code P1 == P2} or there are zero or 1 value between the pivots + * {@code P1 < v < P2}. Any sort/partition of ranges [left, k0-1], [k1+1, k2-1] and + * [k3+1, right] must check the length is {@code > 1}. + * + * @param a Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param bounds Points [k1, k2, k3]. + * @param pivot1 Pivot1 location. + * @param pivot2 Pivot2 location. + * @return Lower bound (inclusive) of the pivot range [k0]. + */ + static int partitionDP(double[] a, int left, int right, int pivot1, int pivot2, int[] bounds) { + // Allow caller to choose a single-pivot + if (pivot1 == pivot2) { + // Switch to a single pivot sort. This is used when there are + // estimated to be many equal values so use the fastest equal + // value single pivot method. + final int lower = partitionDNF3(a, left, right, pivot1, bounds); + // Set dual pivot range + bounds[2] = bounds[0]; + // No unsorted internal region (set k1 = k3; k2 = k0) + // Note: It is extra work for the caller to detect that this region can be skipped. + bounds[1] = lower; + return lower; + } + + // Dual-pivot quicksort method by Vladimir Yaroslavskiy. + // + // Partition data using pivots P1 and P2 into less-than, greater-than or between. + // Pivot values P1 & P2 are placed at the end. If P1 < P2, P2 acts as a sentinel. + // k traverses the unknown region ??? and values moved if less-than (lt) or + // greater-than (gt): + // + // left lt k gt right + // |P1| P2 |P2| + // + // P2 (gt, right) + // + // At the end pivots are swapped back to behind the lt and gt pointers. + // + // | P2 | + // + // Adapted from Yaroslavskiy + // http://codeblab.com/wp-content/uploads/2009/09/DualPivotQuicksort.pdf + // + // Modified to allow partial sorting (partitioning): + // - Allow the caller to supply the pivot indices + // - Ignore insertion sort for tiny array (handled by calling code) + // - Ignore recursive calls for a full sort (handled by calling code) + // - Change to fast-forward over initial ascending / descending runs + // - Change to a single-pivot partition method if the pivots are equal + // - Change to fast-forward great when v > v2 and either break the sorting + // loop, or move a[great] direct to the correct location. + // - Change to remove the 'div' parameter used to control the pivot selection + // using the medians method (div initialises as 3 for 1/3 and 2/3 and increments + // when the central region is too large). + // - Identify a large central region using ~5/8 of the length. + + final double v1 = a[pivot1]; + final double v2 = a[pivot2]; + + // Swap ends to the pivot locations. + a[pivot1] = a[left]; + a[pivot2] = a[right]; + a[left] = v1; + a[right] = v2; + + // pointers + int less = left; + int great = right; + + // Fast-forward ascending / descending runs to reduce swaps. + // Cannot overrun as end pivots (v1 <= v2) act as sentinels. + do { + ++less; + } while (a[less] < v1); + do { + --great; + } while (a[great] > v2); + + // a[less - 1] < P1 : a[great + 1] > P2 + // unvisited in [less, great] + SORTING: + for (int k = less - 1; ++k <= great;) { + final double v = a[k]; + if (v < v1) { + // swap(a, k, less++) + a[k] = a[less]; + a[less] = v; + less++; + } else if (v > v2) { + // while k < great and a[great] > v2: + // great-- + while (a[great] > v2) { + if (great-- == k) { + // Done + break SORTING; + } + } + // swap(a, k, great--) + // if a[k] < v1: + // swap(a, k, less++) + final double w = a[great]; + a[great] = v; + great--; + // delay a[k] = w + if (w < v1) { + a[k] = a[less]; + a[less] = w; + less++; + } else { + a[k] = w; + } + } + } + + // Change to inclusive ends : a[less] < P1 : a[great] > P2 + less--; + great++; + // Move the pivots to correct locations + a[left] = a[less]; + a[less] = v1; + a[right] = a[great]; + a[great] = v2; + + // Record the pivot locations + final int lower = less; + bounds[2] = great; + + // equal elements + // Original paper: If middle partition is bigger than a threshold + // then check for equal elements. + + // Note: This is extra work. When performing partitioning the region of interest + // may be entirely above or below the central region and this could be skipped. + // Versions that do this are not measurably faster. Skipping this may be faster + // if this step can be skipped on the initial largest region. The 5/8 size occurs + // approximately ~7% of the time on random data (verified using collated statistics). + + // Here we look for equal elements if the centre is more than 5/8 the length. + // 5/8 = 1/2 + 1/8. Pivots must be different. + if ((great - less) > ((right - left) >>> 1) + ((right - left) >>> 3) && v1 != v2) { + + // Fast-forward to reduce swaps. Changes inclusive ends to exclusive ends. + // Since v1 != v2 these act as sentinels to prevent overrun. + do { + ++less; + } while (a[less] == v1); + do { + --great; + } while (a[great] == v2); + + // This copies the logic in the sorting loop using == comparisons + EQUAL: + for (int k = less - 1; ++k <= great;) { + final double v = a[k]; + if (v == v1) { + a[k] = a[less]; + a[less] = v; + less++; + } else if (v == v2) { + while (a[great] == v2) { + if (great-- == k) { + // Done + break EQUAL; + } + } + final double w = a[great]; + a[great] = v; + great--; + if (w == v1) { + a[k] = a[less]; + a[less] = w; + less++; + } else { + a[k] = w; + } + } + } + + // Change to inclusive ends + less--; + great++; + } + + // Between pivots in (less, great) + if (v1 < v2 && less < great - 1) { + // Record the pivot end points + bounds[0] = less; + bounds[1] = great; + } else { + // No unsorted internal region (set k1 = k3; k2 = k0) + bounds[0] = bounds[2]; + bounds[1] = lower; + } + + return lower; + } + + /** + * Expand a partition around a single pivot. Partitioning exchanges array + * elements such that all elements smaller than pivot are before it and all + * elements larger than pivot are after it. The central region is already + * partitioned. + * + *

{@code
+     * |l             |s   |p0 p1|   e|                r|
+     * |    ???       | 

P | ??? | + * }

+ * + *

This method requires that {@code left < start && end < right}. It supports + * {@code start == end}. + * + * @param a Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param start Start of the partition range (inclusive). + * @param end End of the partitioned range (inclusive). + * @param pivot0 Lower pivot location (inclusive). + * @param pivot1 Upper pivot location (inclusive). + * @param upper Upper bound (inclusive) of the pivot range [k1]. + * @return Lower bound (inclusive) of the pivot range [k0]. + */ + private static int expandPartitionT1(double[] a, int left, int right, int start, int end, + int pivot0, int pivot1, int[] upper) { + // 3-way partition of the data using a pivot value into + // less-than, equal or greater-than. + // Based on Sedgewick's Bentley-McIroy partitioning: always swap i<->j then + // check for equal to the pivot and move again. + // + // Move sentinels from start and end to left and right. Scan towards the + // sentinels until >=,<=. Swap then move == to the pivot region. + // <-i j-> + // |l | | |p0 p1| | | r| + // |>=| ??? | < | == | > | ??? |<=| + // + // When either i or j reach the edge perform finishing loop. + // Finish loop for a[j] <= v replaces j with p1+1, moves value + // to p0 for < and updates the pivot range p1 (and optionally p0): + // j-> + // |l |p0 p1| | | r| + // | < | == | > | ??? |<=| + + // Positioned for pre-in/decrement to write to pivot region + int p0 = pivot0; + int p1 = pivot1; + final double v = a[p0]; + if (a[left] < v) { + // a[left] is not a sentinel + final double w = a[left]; + if (a[right] > v) { + // Most likely case: ends can be sentinels + a[left] = a[right]; + a[right] = w; + } else { + // a[right] is a sentinel; use pivot for left + a[left] = v; + a[p0] = w; + p0++; + } + } else if (a[right] > v) { + // a[right] is not a sentinel; use pivot + a[p1] = a[right]; + p1--; + a[right] = v; + } + + // Required to avoid index bound error first use of i/j + assert left < start && end < right; + int i = start; + int j = end; + while (true) { + do { + --i; + } while (a[i] < v); + do { + ++j; + } while (a[j] > v); + final double vj = a[i]; + final double vi = a[j]; + a[i] = vi; + a[j] = vj; + // Move the equal values to pivot region + if (vi == v) { + a[i] = a[--p0]; + a[p0] = v; + } + if (vj == v) { + a[j] = a[++p1]; + a[p1] = v; + } + // Termination check and finishing loops. + // Note: this works even if pivot region is zero length (p1 == p0-1) + // due to pivot use as a sentinel on one side because we pre-inc/decrement + // one side and post-inc/decrement the other side. + if (i == left) { + while (j < right) { + do { + ++j; + } while (a[j] > v); + final double w = a[j]; + // Move upper bound of pivot region + a[j] = a[++p1]; + a[p1] = v; + if (w != v) { + // Move lower bound of pivot region + a[p0] = w; + p0++; + } + } + break; + } + if (j == right) { + while (i > left) { + do { + --i; + } while (a[i] < v); + final double w = a[i]; + // Move lower bound of pivot region + a[i] = a[--p0]; + a[p0] = v; + if (w != v) { + // Move upper bound of pivot region + a[p1] = w; + p1--; + } + } + break; + } + } + + upper[0] = p1; + return p0; + } + + /** + * Expand a partition around a single pivot. Partitioning exchanges array + * elements such that all elements smaller than pivot are before it and all + * elements larger than pivot are after it. The central region is already + * partitioned. + * + *

{@code
+     * |l             |s   |p0 p1|   e|                r|
+     * |    ???       | 

P | ??? | + * }

+ * + *

This is similar to {@link #expandPartitionT1(double[], int, int, int, int, int, int, int[])} + * with a change to binary partitioning. It requires that {@code left < start && end < right}. + * It supports {@code start == end}. + * + * @param a Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param start Start of the partition range (inclusive). + * @param end End of the partitioned range (inclusive). + * @param pivot0 Lower pivot location (inclusive). + * @param pivot1 Upper pivot location (inclusive). + * @param upper Upper bound (inclusive) of the pivot range [k1]. + * @return Lower bound (inclusive) of the pivot range [k0]. + */ + private static int expandPartitionB1(double[] a, int left, int right, int start, int end, + int pivot0, int pivot1, int[] upper) { + // 2-way partition of the data using a pivot value into + // less-than, or greater-than. + // + // Move sentinels from start and end to left and right. Scan towards the + // sentinels until >=,<= then swap. + // <-i j-> + // |l | | | p| | | r| + // |>=| ??? | < |==| > | ??? |<=| + // + // When either i or j reach the edge perform finishing loop. + // Finish loop for a[j] <= v replaces j with p1+1, moves value to p + // and moves the pivot up: + // j-> + // |l | p| | | r| + // | < |==| > | ??? |<=| + + // Pivot may be moved to use as a sentinel + int p = pivot0; + final double v = a[p]; + if (a[left] < v) { + // a[left] is not a sentinel + final double w = a[left]; + if (a[right] > v) { + // Most likely case: ends can be sentinels + a[left] = a[right]; + a[right] = w; + } else { + // a[right] is a sentinel; use pivot for left + a[left] = v; + a[p] = w; + p++; + } + } else if (a[right] > v) { + // a[right] is not a sentinel; use pivot + a[p] = a[right]; + p--; + a[right] = v; + } + + // Required to avoid index bound error first use of i/j + assert left < start && end < right; + int i = start; + int j = end; + while (true) { + do { + --i; + } while (a[i] < v); + do { + ++j; + } while (a[j] > v); + final double vj = a[i]; + final double vi = a[j]; + a[i] = vi; + a[j] = vj; + // Termination check and finishing loops. + // These reset the pivot if it was moved then slide it as required. + if (i == left) { + // Reset the pivot and sentinel + if (p < pivot0) { + // Pivot is in right; a[p] <= v + a[right] = a[p]; + a[p] = v; + } else if (p > pivot0) { + // Pivot was in left (now swapped to j); a[p] >= v + a[j] = a[p]; + a[p] = v; + } + if (j == right) { + break; + } + while (j < right) { + do { + ++j; + } while (a[j] > v); + // Move pivot + a[p] = a[j]; + a[j] = a[++p]; + a[p] = v; + } + break; + } + if (j == right) { + // Reset the pivot and sentinel + if (p < pivot0) { + // Pivot was in right (now swapped to i); a[p] <= v + a[i] = a[p]; + a[p] = v; + } else if (p > pivot0) { + // Pivot is in left; a[p] >= v + a[left] = a[p]; + a[p] = v; + } + if (i == left) { + break; + } + while (i > left) { + do { + --i; + } while (a[i] < v); + // Move pivot + a[p] = a[i]; + a[i] = a[--p]; + a[p] = v; + } + break; + } + } + + upper[0] = p; + return p; + } + + /** + * Expand a partition around a single pivot. Partitioning exchanges array + * elements such that all elements smaller than pivot are before it and all + * elements larger than pivot are after it. The central region is already + * partitioned. + * + *

{@code
+     * |l             |s   |p0 p1|   e|                r|
+     * |    ???       | 

P | ??? | + * }

+ * + *

This is similar to {@link #expandPartitionT1(double[], int, int, int, int, int, int, int[])} + * with a change to how the end-point sentinels are created. It does not use the pivot + * but uses values at start and end. This increases the length of the lower/upper ranges + * by 1 for the main scan. It requires that {@code start != end}. However it handles + * {@code left == start} and/or {@code end == right}. + * + * @param a Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param start Start of the partition range (inclusive). + * @param end End of the partitioned range (inclusive). + * @param pivot0 Lower pivot location (inclusive). + * @param pivot1 Upper pivot location (inclusive). + * @param upper Upper bound (inclusive) of the pivot range [k1]. + * @return Lower bound (inclusive) of the pivot range [k0]. + */ + private static int expandPartitionT2(double[] a, int left, int right, int start, int end, + int pivot0, int pivot1, int[] upper) { + // 3-way partition of the data using a pivot value into + // less-than, equal or greater-than. + // Based on Sedgewick's Bentley-McIroy partitioning: always swap i<->j then + // check for equal to the pivot and move again. + // + // Move sentinels from start and end to left and right. Scan towards the + // sentinels until >=,<=. Swap then move == to the pivot region. + // <-i j-> + // |l | | |p0 p1| | | r| + // |>=| ??? | < | == | > | ??? |<=| + // + // When either i or j reach the edge perform finishing loop. + // Finish loop for a[j] <= v replaces j with p1+1, optionally moves value + // to p0 for < and updates the pivot range p1 (and optionally p0): + // j-> + // |l |p0 p1| | | r| + // | < | == | > | ??? |<=| + + final double v = a[pivot0]; + // Use start/end as sentinels. + // This requires start != end + assert start != end; + double vi = a[start]; + double vj = a[end]; + a[start] = a[left]; + a[end] = a[right]; + a[left] = vj; + a[right] = vi; + + int i = start + 1; + int j = end - 1; + + // Positioned for pre-in/decrement to write to pivot region + int p0 = pivot0 == start ? i : pivot0; + int p1 = pivot1 == end ? j : pivot1; + + while (true) { + do { + --i; + } while (a[i] < v); + do { + ++j; + } while (a[j] > v); + vj = a[i]; + vi = a[j]; + a[i] = vi; + a[j] = vj; + // Move the equal values to pivot region + if (vi == v) { + a[i] = a[--p0]; + a[p0] = v; + } + if (vj == v) { + a[j] = a[++p1]; + a[p1] = v; + } + // Termination check and finishing loops. + // Note: this works even if pivot region is zero length (p1 == p0-1 + // due to single length pivot region at either start/end) because we + // pre-inc/decrement one side and post-inc/decrement the other side. + if (i == left) { + while (j < right) { + do { + ++j; + } while (a[j] > v); + final double w = a[j]; + // Move upper bound of pivot region + a[j] = a[++p1]; + a[p1] = v; + // Move lower bound of pivot region + //p0 += w != v ? 1 : 0; + if (w != v) { + a[p0] = w; + p0++; + } + } + break; + } + if (j == right) { + while (i > left) { + do { + --i; + } while (a[i] < v); + final double w = a[i]; + // Move lower bound of pivot region + a[i] = a[--p0]; + a[p0] = v; + // Move upper bound of pivot region + //p1 -= w != v ? 1 : 0; + if (w != v) { + a[p1] = w; + p1--; + } + } + break; + } + } + + upper[0] = p1; + return p0; + } + + /** + * Expand a partition around a single pivot. Partitioning exchanges array + * elements such that all elements smaller than pivot are before it and all + * elements larger than pivot are after it. The central region is already + * partitioned. + * + *

{@code
+     * |l             |s   |p0 p1|   e|                r|
+     * |    ???       | 

P | ??? | + * }

+ * + *

This is similar to {@link #expandPartitionT2(double[], int, int, int, int, int, int, int[])} + * with a change to binary partitioning. It is simpler than + * {@link #expandPartitionB1(double[], int, int, int, int, int, int, int[])} as the pivot is + * not moved. It requires that {@code start != end}. However it handles + * {@code left == start} and/or {@code end == right}. + * + * @param a Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param start Start of the partition range (inclusive). + * @param end End of the partitioned range (inclusive). + * @param pivot0 Lower pivot location (inclusive). + * @param pivot1 Upper pivot location (inclusive). + * @param upper Upper bound (inclusive) of the pivot range [k1]. + * @return Lower bound (inclusive) of the pivot range [k0]. + */ + private static int expandPartitionB2(double[] a, int left, int right, int start, int end, + int pivot0, int pivot1, int[] upper) { + // 2-way partition of the data using a pivot value into + // less-than, or greater-than. + // + // Move sentinels from start and end to left and right. Scan towards the + // sentinels until >=,<= then swap. + // <-i j-> + // |l | | | p| | | r| + // |>=| ??? | < |==| > | ??? |<=| + // + // When either i or j reach the edge perform finishing loop. + // Finish loop for a[j] <= v replaces j with p1+1, moves value to p + // and moves the pivot up: + // j-> + // |l | p| | | r| + // | < |==| > | ??? |<=| + + // Pivot + int p = pivot0; + final double v = a[p]; + // Use start/end as sentinels. + // This requires start != end + assert start != end; + // Note: Must not move pivot as this invalidates the finishing loops. + // See logic in method B1 to see added complexity of pivot location. + // This method is not better than T2 for data with no repeat elements + // and is slower for repeat elements when used with the improved + // versions (e.g. linearBFPRTImproved). So for this edge case just use B1. + if (p == start || p == end) { + return expandPartitionB1(a, left, right, start, end, pivot0, pivot1, upper); + } + double vi = a[start]; + double vj = a[end]; + a[start] = a[left]; + a[end] = a[right]; + a[left] = vj; + a[right] = vi; + + int i = start + 1; + int j = end - 1; + while (true) { + do { + --i; + } while (a[i] < v); + do { + ++j; + } while (a[j] > v); + vj = a[i]; + vi = a[j]; + a[i] = vi; + a[j] = vj; + // Termination check and finishing loops + if (i == left) { + while (j < right) { + do { + ++j; + } while (a[j] > v); + // Move pivot + a[p] = a[j]; + a[j] = a[++p]; + a[p] = v; + } + break; + } + if (j == right) { + while (i > left) { + do { + --i; + } while (a[i] < v); + // Move pivot + a[p] = a[i]; + a[i] = a[--p]; + a[p] = v; + } + break; + } + } + + upper[0] = p; + return p; + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

The index {@code k} is the target element. This method ignores this value. + * The value is included to match the method signature of the {@link SPEPartition} interface. + * Assumes the range {@code r - l >= 4}; the caller is responsible for selection on a smaller + * range. + * + *

Uses the Blum, Floyd, Pratt, Rivest, and Tarjan (BFPRT) median-of-medians algorithm + * with medians of 5 with the sample medians computed in the first quintile. + * + * @param a Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param k Target index. + * @param upper Upper bound (inclusive) of the pivot range. + * @return Lower bound (inclusive) of the pivot range. + */ + private int linearBFPRTBaseline(double[] a, int l, int r, int k, int[] upper) { + // Adapted from Alexandrescu (2016), algorithm 3. + // Moves the responsibility for selection when r-l <= 4 to the caller. + // Compute the median of each contiguous set of 5 to the first quintile. + int rr = l - 1; + for (int e = l + 4; e <= r; e += 5) { + Sorting.median5d(a, e - 4, e - 3, e - 2, e - 1, e); + // Median to first quintile + final double v = a[e - 2]; + a[e - 2] = a[++rr]; + a[rr] = v; + } + final int m = (l + rr + 1) >>> 1; + // mutual recursion + quickSelect(this::linearBFPRTBaseline, a, l, rr, m, m, upper); + // Note: repartions already partitioned data [l, rr] + return spFunction.partition(a, l, r, m, upper); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

The index {@code k} is the target element. This method ignores this value. + * The value is included to match the method signature of the {@link SPEPartition} interface. + * Assumes the range {@code r - l >= 8}; the caller is responsible for selection on a smaller + * range. + * + *

Uses the Chen and Dumitrescu repeated step median-of-medians-of-medians algorithm + * with medians of 3 with the samples computed in the first tertile and 9th-tile. + * + * @param a Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param k Target index. + * @param upper Upper bound (inclusive) of the pivot range. + * @return Lower bound (inclusive) of the pivot range. + */ + private int linearRepeatedStepBaseline(double[] a, int l, int r, int k, int[] upper) { + // Adapted from Alexandrescu (2016), algorithm 5. + // Moves the responsibility for selection when r-l <= 8 to the caller. + // Compute the median of each contiguous set of 3 to the first tertile, and repeat. + int j = l - 1; + for (int e = l + 2; e <= r; e += 3) { + Sorting.sort3(a, e - 2, e - 1, e); + // Median to first tertile + final double v = a[e - 1]; + a[e - 1] = a[++j]; + a[j] = v; + } + int rr = l - 1; + for (int e = l + 2; e <= j; e += 3) { + Sorting.sort3(a, e - 2, e - 1, e); + // Median to first 9th-tile + final double v = a[e - 1]; + a[e - 1] = a[++rr]; + a[rr] = v; + } + final int m = (l + rr + 1) >>> 1; + // mutual recursion + quickSelect(this::linearRepeatedStepBaseline, a, l, rr, m, m, upper); + // Note: repartions already partitioned data [l, rr] + return spFunction.partition(a, l, r, m, upper); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

The index {@code k} is the target element. This method ignores this value. + * The value is included to match the method signature of the {@link SPEPartition} interface. + * Assumes the range {@code r - l >= 4}; the caller is responsible for selection on a smaller + * range. + * + *

Uses the Blum, Floyd, Pratt, Rivest, and Tarjan (BFPRT) median-of-medians algorithm + * with medians of 5 with the sample medians computed in the central quintile. + * + * @param a Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param k Target index. + * @param upper Upper bound (inclusive) of the pivot range. + * @return Lower bound (inclusive) of the pivot range. + */ + private int linearBFPRTImproved(double[] a, int l, int r, int k, int[] upper) { + // Adapted from Alexandrescu (2016), algorithm 6. + // Moves the responsibility for selection when r-l <= 4 to the caller. + // Compute the median of each non-contiguous set of 5 to the middle quintile. + final int f = (r - l + 1) / 5; + final int f3 = 3 * f; + // middle quintile: [2f:3f) + final int s = l + (f << 1); + final int e = s + f - 1; + for (int i = l, j = s; i < s; i += 2, j++) { + Sorting.median5d(a, i, i + 1, j, f3 + i, f3 + i + 1); + } + // Adaption to target kf/|A| + //final int p = s + mapDistance(k - l, l, r, f); + final int p = s + noSamplingAdapt.mapDistance(k - l, l, r, f); + // mutual recursion + quickSelect(this::linearBFPRTImproved, a, s, e, p, p, upper); + return expandFunction.partition(a, l, r, s, e, upper[0], upper[1], upper); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

The index {@code k} is the target element. This method ignores this value. + * The value is included to match the method signature of the {@link SPEPartition} interface. + * Assumes the range {@code r - l >= 8}; the caller is responsible for selection on a smaller + * range. + * + *

Uses the Chen and Dumitrescu repeated step median-of-medians-of-medians algorithm + * with medians of 3 with the samples computed in the middle tertile and 9th-tile. + * + * @param a Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param k Target index. + * @param upper Upper bound (inclusive) of the pivot range. + * @return Lower bound (inclusive) of the pivot range. + */ + private int linearRepeatedStepImproved(double[] a, int l, int r, int k, int[] upper) { + // Adapted from Alexandrescu (2016), algorithm 7. + // Moves the responsibility for selection when r-l <= 8 to the caller. + // Compute the median of each non-contiguous set of 3 to the middle tertile, and repeat. + final int f = (r - l + 1) / 9; + final int f3 = 3 * f; + // i in middle tertile [3f:6f) + for (int i = l + f3, e = l + (f3 << 1); i < e; i++) { + Sorting.sort3(a, i - f3, i, i + f3); + } + // i in middle 9th-tile: [4f:5f) + final int s = l + (f << 2); + final int e = s + f - 1; + for (int i = s; i <= e; i++) { + Sorting.sort3(a, i - f, i, i + f); + } + // Adaption to target kf/|A| + //final int p = s + mapDistance(k - l, l, r, f); + final int p = s + noSamplingAdapt.mapDistance(k - l, l, r, f); + // mutual recursion + quickSelect(this::linearRepeatedStepImproved, a, s, e, p, p, upper); + return expandFunction.partition(a, l, r, s, e, upper[0], upper[1], upper); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Assumes the range {@code r - l >= 8}; the caller is responsible for selection on a smaller + * range. If using a 12th-tile for sampling then assumes {@code r - l >= 11}. + * + *

Uses the Chen and Dumitrescu repeated step median-of-medians-of-medians algorithm + * with the median of 3 then median of 3; the final sample is placed in the + * 5th 9th-tile; the pivot chosen from the sample is adaptive using the input {@code k}. + * + * @param a Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param k Target index. + * @param upper Upper bound (inclusive) of the pivot range. + * @param mode Adaption mode. + * @return Lower bound (inclusive) of the pivot range. + */ + private int repeatedStep(double[] a, int l, int r, int k, int[] upper, AdaptMode mode) { + // Adapted from Alexandrescu (2016), algorithm 8. + // Moves the responsibility for selection when r-l <= 8 to the caller. + int f; + int s; + int p; + if (!mode.isSampleMode()) { + // i in tertile [3f:6f) + f = (r - l + 1) / 9; + final int f3 = 3 * f; + for (int i = l + f3, end = l + (f3 << 1); i < end; i++) { + Sorting.sort3(a, i - f3, i, i + f3); + } + // 5th 9th-tile: [4f:5f) + s = l + (f << 2); + p = s + (mode.isAdapt() ? noSamplingAdapt.mapDistance(k - l, l, r, f) : (f >>> 1)); + } else { + if ((controlFlags & FLAG_QA_MIDDLE_12) != 0) { + // Switch to a 12th-tile as used in the other methods. + f = (r - l + 1) / 12; + // middle - f/2 + s = ((r + l) >>> 1) - (f >> 1); + } else { + f = (r - l + 1) / 9; + s = l + (f << 2); + } + // Adaption to target kf'/|A| + int kp = mode.isAdapt() ? samplingAdapt.mapDistance(k - l, l, r, f) : (f >>> 1); + // Centre the sample at k + if ((controlFlags & FLAG_QA_SAMPLE_K) != 0) { + s = k - kp; + } + p = s + kp; + } + final int e = s + f - 1; + for (int i = s; i <= e; i++) { + Sorting.sort3(a, i - f, i, i + f); + } + p = quickSelectAdaptive(a, s, e, p, p, upper, + (controlFlags & FLAG_QA_PROPAGATE) != 0 ? mode : adaptMode); + return expandFunction.partition(a, l, r, s, e, p, upper[0], upper); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Assumes the range {@code r - l >= 11}; the caller is responsible for selection on a smaller + * range. + * + *

Uses the Chen and Dumitrescu repeated step median-of-medians-of-medians algorithm + * with the lower median of 4 then either median of 3 with the final sample placed in the + * 5th 12th-tile, or min of 3 with the final sample in the 4th 12th-tile; + * the pivot chosen from the sample is adaptive using the input {@code k}. + * + * @param a Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param k Target index. + * @param upper Upper bound (inclusive) of the pivot range. + * @param mode Adaption mode. + * @param far Set to {@code true} to perform repeatedStepFarLeft. + * @return Lower bound (inclusive) of the pivot range. + */ + private int repeatedStepLeft(double[] a, int l, int r, int k, int[] upper, AdaptMode mode, + boolean far) { + // Adapted from Alexandrescu (2016), algorithm 9 and 10. + // Moves the responsibility for selection when r-l <= 11 to the caller. + final int f = (r - l + 1) >> 2; + if (!mode.isSampleMode()) { + // i in 2nd quartile + final int f2 = f + f; + for (int i = l + f, e = l + f2; i < e; i++) { + Sorting.lowerMedian4(a, i - f, i, i + f, i + f2); + } + } + int fp = f / 3; + int s; + int e; + int p; + if (far) { + // i in 4th 12th-tile + s = l + f; + // Variable adaption + int kp; + if (!mode.isSampleMode()) { + kp = mode.isAdapt() ? noSamplingEdgeAdapt.mapDistance(k - l, l, r, fp) : fp >>> 1; + } else { + kp = mode.isAdapt() ? samplingEdgeAdapt.mapDistance(k - l, l, r, fp) : fp >>> 1; + // Note: Not possible to centre the sample at k on the far step + } + e = s + fp - 1; + p = s + kp; + final int fp2 = fp << 1; + for (int i = s; i <= e; i++) { + // min into i + if (a[i] > a[i + fp]) { + final double u = a[i]; + a[i] = a[i + fp]; + a[i + fp] = u; + } + if (a[i] > a[i + fp2]) { + final double v = a[i]; + a[i] = a[i + fp2]; + a[i + fp2] = v; + } + } + } else { + // i in 5th 12th-tile + s = l + f + fp; + // Variable adaption + int kp; + if (!mode.isSampleMode()) { + kp = mode.isAdapt() ? noSamplingAdapt.mapDistance(k - l, l, r, fp) : fp >>> 1; + } else { + kp = mode.isAdapt() ? samplingAdapt.mapDistance(k - l, l, r, fp) : fp >>> 1; + // Centre the sample at k + if ((controlFlags & FLAG_QA_SAMPLE_K) != 0) { + // Avoid bounds error due to rounding as (k-l)/(r-l) -> 1/12 + s = Math.max(k - kp, l + fp); + } + } + e = s + fp - 1; + p = s + kp; + for (int i = s; i <= e; i++) { + Sorting.sort3(a, i - fp, i, i + fp); + } + } + p = quickSelectAdaptive(a, s, e, p, p, upper, + (controlFlags & FLAG_QA_PROPAGATE) != 0 ? mode : adaptMode); + return expandFunction.partition(a, l, r, s, e, p, upper[0], upper); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Assumes the range {@code r - l >= 11}; the caller is responsible for selection on a smaller + * range. + * + *

Uses the Chen and Dumitrescu repeated step median-of-medians-of-medians algorithm + * with the upper median of 4 then either median of 3 with the final sample placed in the + * 8th 12th-tile, or max of 3 with the final sample in the 9th 12th-tile; + * the pivot chosen from the sample is adaptive using the input {@code k}. + * + * @param a Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param k Target index. + * @param upper Upper bound (inclusive) of the pivot range. + * @param mode Adaption mode. + * @param far Set to {@code true} to perform repeatedStepFarRight. + * @return Lower bound (inclusive) of the pivot range. + */ + private int repeatedStepRight(double[] a, int l, int r, int k, int[] upper, AdaptMode mode, + boolean far) { + // Mirror image repeatedStepLeft using upper median into 3rd quartile + final int f = (r - l + 1) >> 2; + if (!mode.isSampleMode()) { + // i in 3rd quartile + final int f2 = f + f; + for (int i = r - f, e = r - f2; i > e; i--) { + Sorting.upperMedian4(a, i - f2, i - f, i, i + f); + } + } + int fp = f / 3; + int s; + int e; + int p; + if (far) { + // i in 9th 12th-tile + e = r - f; + // Variable adaption + int kp; + if (!mode.isSampleMode()) { + kp = mode.isAdapt() ? noSamplingEdgeAdapt.mapDistance(r - k, l, r, fp) : fp >>> 1; + } else { + kp = mode.isAdapt() ? samplingEdgeAdapt.mapDistance(r - k, l, r, fp) : fp >>> 1; + // Note: Not possible to centre the sample at k on the far step + } + s = e - fp + 1; + p = e - kp; + final int fp2 = fp << 1; + for (int i = s; i <= e; i++) { + // max into i + if (a[i] < a[i - fp]) { + final double u = a[i]; + a[i] = a[i - fp]; + a[i - fp] = u; + } + if (a[i] < a[i - fp2]) { + final double v = a[i]; + a[i] = a[i - fp2]; + a[i - fp2] = v; + } + } + } else { + // i in 8th 12th-tile + e = r - f - fp; + // Variable adaption + int kp; + if (!mode.isSampleMode()) { + kp = mode.isAdapt() ? noSamplingAdapt.mapDistance(r - k, l, r, fp) : fp >>> 1; + } else { + kp = mode.isAdapt() ? samplingAdapt.mapDistance(r - k, l, r, fp) : fp >>> 1; + // Centre the sample at k + if ((controlFlags & FLAG_QA_SAMPLE_K) != 0) { + // Avoid bounds error due to rounding as (r-k)/(r-l) -> 11/12 + e = Math.min(k + kp, r - fp); + } + } + s = e - fp + 1; + p = e - kp; + for (int i = s; i <= e; i++) { + Sorting.sort3(a, i - fp, i, i + fp); + } + } + p = quickSelectAdaptive(a, s, e, p, p, upper, + (controlFlags & FLAG_QA_PROPAGATE) != 0 ? mode : adaptMode); + return expandFunction.partition(a, l, r, s, e, p, upper[0], upper); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Assumes the range {@code r - l >= 11}; the caller is responsible for selection on a smaller + * range. + * + *

Uses the Chen and Dumitrescu repeated step median-of-medians-of-medians algorithm + * with the minimum of 4 then median of 3; the final sample is placed in the + * 2nd 12th-tile; the pivot chosen from the sample is adaptive using the input {@code k}. + * + *

Given a pivot in the middle of the sample this has margins of 1/12 and 1/3. + * + * @param a Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param k Target index. + * @param upper Upper bound (inclusive) of the pivot range. + * @param mode Adaption mode. + * @return Lower bound (inclusive) of the pivot range. + */ + private int repeatedStepFarLeft(double[] a, int l, int r, int k, int[] upper, AdaptMode mode) { + // Moves the responsibility for selection when r-l <= 11 to the caller. + final int f = (r - l + 1) >> 2; + int fp = f / 3; + // 2nd 12th-tile + int s = l + fp; + final int e = s + fp - 1; + int p; + if (!mode.isSampleMode()) { + p = s + (mode.isAdapt() ? noSamplingEdgeAdapt.mapDistance(k - l, l, r, fp) : fp >>> 1); + // i in 2nd quartile; min into i-f (1st quartile) + final int f2 = f + f; + for (int i = l + f, end = l + f2; i < end; i++) { + if (a[i + f] < a[i - f]) { + final double u = a[i + f]; + a[i + f] = a[i - f]; + a[i - f] = u; + } + if (a[i + f2] < a[i]) { + final double v = a[i + f2]; + a[i + f2] = a[i]; + a[i] = v; + } + if (a[i] < a[i - f]) { + final double u = a[i]; + a[i] = a[i - f]; + a[i - f] = u; + } + } + } else { + int kp = mode.isAdapt() ? samplingEdgeAdapt.mapDistance(k - l, l, r, fp) : fp >>> 1; + p = s + kp; + } + for (int i = s; i <= e; i++) { + Sorting.sort3(a, i - fp, i, i + fp); + } + p = quickSelectAdaptive(a, s, e, p, p, upper, + (controlFlags & FLAG_QA_PROPAGATE) != 0 ? mode : adaptMode); + return expandFunction.partition(a, l, r, s, e, p, upper[0], upper); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Assumes the range {@code r - l >= 11}; the caller is responsible for selection on a smaller + * range. + * + *

Uses the Chen and Dumitrescu repeated step median-of-medians-of-medians algorithm + * with the maximum of 4 then median of 3; the final sample is placed in the + * 11th 12th-tile; the pivot chosen from the sample is adaptive using the input {@code k}. + * + *

Given a pivot in the middle of the sample this has margins of 1/3 and 1/12. + * + * @param a Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param k Target index. + * @param upper Upper bound (inclusive) of the pivot range. + * @param mode Adaption mode. + * @return Lower bound (inclusive) of the pivot range. + */ + private int repeatedStepFarRight(double[] a, int l, int r, int k, int[] upper, AdaptMode mode) { + // Mirror image repeatedStepFarLeft + final int f = (r - l + 1) >> 2; + int fp = f / 3; + // 11th 12th-tile + int e = r - fp; + final int s = e - fp + 1; + int p; + if (!mode.isSampleMode()) { + p = e - (mode.isAdapt() ? noSamplingEdgeAdapt.mapDistance(r - k, l, r, fp) : fp >>> 1); + // i in 3rd quartile; max into i+f (4th quartile) + final int f2 = f + f; + for (int i = r - f, end = r - f2; i > end; i--) { + if (a[i - f] > a[i + f]) { + final double u = a[i - f]; + a[i - f] = a[i + f]; + a[i + f] = u; + } + if (a[i - f2] > a[i]) { + final double v = a[i - f2]; + a[i - f2] = a[i]; + a[i] = v; + } + if (a[i] > a[i + f]) { + final double u = a[i]; + a[i] = a[i + f]; + a[i + f] = u; + } + } + } else { + int kp = mode.isAdapt() ? samplingEdgeAdapt.mapDistance(r - k, l, r, fp) : fp >>> 1; + p = e - kp; + } + for (int i = s; i <= e; i++) { + Sorting.sort3(a, i - fp, i, i + fp); + } + p = quickSelectAdaptive(a, s, e, p, p, upper, + (controlFlags & FLAG_QA_PROPAGATE) != 0 ? mode : adaptMode); + return expandFunction.partition(a, l, r, s, e, p, upper[0], upper); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Partitions a Floyd-Rivest sample around a pivot offset so that the input {@code k} will + * fall in the smaller partition when the entire range is partitioned. + * + *

Assumes the range {@code r - l} is large; the original Floyd-Rivest size for sampling + * was 600. + * + * @param a Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param k Target index. + * @param upper Upper bound (inclusive) of the pivot range. + * @param mode Adaption mode. + * @return Lower bound (inclusive) of the pivot range. + */ + private int sampleStep(double[] a, int l, int r, int k, int[] upper, AdaptMode mode) { + // Floyd-Rivest: use SELECT recursively on a sample of size S to get an estimate + // for the (k-l+1)-th smallest element into a[k], biased slightly so that the + // (k-l+1)-th element is expected to lie in the smaller set after partitioning. + final int n = r - l + 1; + final int ith = k - l + 1; + final double z = Math.log(n); + // sample size = 0.5 * n^(2/3) + final double s = 0.5 * Math.exp(0.6666666666666666 * z); + final double sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * Integer.signum(ith - (n >> 1)); + final int ll = Math.max(l, (int) (k - ith * s / n + sd)); + final int rr = Math.min(r, (int) (k + (n - ith) * s / n + sd)); + // Optional random sampling. + // Support two variants. + if ((controlFlags & FLAG_QA_RANDOM_SAMPLING) != 0) { + final IntUnaryOperator rng = createRNG(n, k); + if (ll == l) { + // Shuffle [l, rr] from [l, r] + for (int i = l - 1; i < rr;) { + // r - rand [0, r - i] : i is currently i-1 + final int j = r - rng.applyAsInt(r - i); + final double t = a[++i]; + a[i] = a[j]; + a[j] = t; + } + } else if (rr == r) { + // Shuffle [ll, r] from [l, r] + for (int i = r + 1; i > ll;) { + // l + rand [0, i - l] : i is currently i+1 + final int j = l + rng.applyAsInt(i - l); + final double t = a[--i]; + a[i] = a[j]; + a[j] = t; + } + } else { + // Sample range [ll, rr] is internal + // Shuffle [ll, k) from [l, k) + for (int i = k; i > ll;) { + // l + rand [0, i - l + 1) : i is currently i+1 + final int j = l + rng.applyAsInt(i - l); + final double t = a[--i]; + a[i] = a[j]; + a[j] = t; + } + // Shuffle (k, rr] from (k, r] + for (int i = k; i < rr;) { + // r - rand [0, r - i + 1) : i is currently i-1 + final int j = r - rng.applyAsInt(r - i); + final double t = a[++i]; + a[i] = a[j]; + a[j] = t; + } + } + } else if ((controlFlags & FLAG_RANDOM_SAMPLING) != 0) { + final IntUnaryOperator rng = createRNG(n, k); + // Shuffle [ll, k) from [l, k) + if (ll > l) { + for (int i = k; i > ll;) { + // l + rand [0, i - l + 1) : i is currently i+1 + final int j = l + rng.applyAsInt(i - l); + final double t = a[--i]; + a[i] = a[j]; + a[j] = t; + } + } + // Shuffle (k, rr] from (k, r] + if (rr < r) { + for (int i = k; i < rr;) { + // r - rand [0, r - i + 1) : i is currently i-1 + final int j = r - rng.applyAsInt(r - i); + final double t = a[++i]; + a[i] = a[j]; + a[j] = t; + } + } + } + // Sample recursion restarts from [ll, rr] + final int p = quickSelectAdaptive(a, ll, rr, k, k, upper, + (controlFlags & FLAG_QA_PROPAGATE) != 0 ? mode : adaptMode); + + // Expect a small sample and repartition the entire range... + // Does not support a pivot range so use the centre + //return spFunction.partition(a, l, r, (p + upper[0]) >>> 1, upper); + + return expandFunction.partition(a, l, r, ll, rr, p, upper[0], upper); + } + + /** + * Map the distance from the edge of {@code [l, r]} to a new distance in {@code [0, n)}. + * + *

The provides the adaption {@code kf'/|A|} from Alexandrescu (2016) where + * {@code k == d}, {@code f' == n} and {@code |A| == r-l+1}. + * + *

For convenience this accepts the input range {@code [l, r]}. + * + * @param d Distance from the edge in {@code [0, r - l]}. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param n Size of the new range. + * @return the mapped distance in [0, n) + */ + private static int mapDistance(int d, int l, int r, int n) { + return (int) (d * (n - 1.0) / (r - l)); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Assumes the range {@code r - l >= 8}; the caller is responsible for selection on a smaller + * range. If using a 12th-tile for sampling then assumes {@code r - l >= 11}. + * + *

Uses the Chen and Dumitrescu repeated step median-of-medians-of-medians algorithm + * with the median of 3 then median of 3; the final sample is placed in the + * 5th 9th-tile; the pivot chosen from the sample is adaptive using the input {@code k}. + * + *

Given a pivot in the middle of the sample this has margins of 2/9 and 2/9. + * + * @param a Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param k Target index. + * @param upper Upper bound (inclusive) of the pivot range. + * @param flags Control flags. + * @return Lower bound (inclusive) of the pivot range. + */ + private static int repeatedStep(double[] a, int l, int r, int k, int[] upper, int flags) { + // Adapted from Alexandrescu (2016), algorithm 8. + int fp; + int s; + int p; + if (flags <= MODE_SAMPLING) { + // Median into a 12th-tile + fp = (r - l + 1) / 12; + // Position the sample around the target k + s = k - mapDistance(k - l, l, r, fp); + p = k; + } else { + // i in tertile [3f':6f') + fp = (r - l + 1) / 9; + final int f3 = 3 * fp; + for (int i = l + f3, end = l + (f3 << 1); i < end; i++) { + Sorting.sort3(a, i - f3, i, i + f3); + } + // 5th 9th-tile: [4f':5f') + s = l + (fp << 2); + // No adaption uses the middle to enforce strict margins + p = s + (flags == MODE_ADAPTION ? mapDistance(k - l, l, r, fp) : (fp >>> 1)); + } + final int e = s + fp - 1; + for (int i = s; i <= e; i++) { + Sorting.sort3(a, i - fp, i, i + fp); + } + p = quickSelectAdaptive2(a, s, e, p, p, upper, qaMode); + return expandPartitionT2(a, l, r, s, e, p, upper[0], upper); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Assumes the range {@code r - l >= 11}; the caller is responsible for selection on a smaller + * range. + * + *

Uses the Chen and Dumitrescu repeated step median-of-medians-of-medians algorithm + * with the lower median of 4 then either median of 3 with the final sample placed in the + * 5th 12th-tile, or min of 3 with the final sample in the 4th 12th-tile; + * the pivot chosen from the sample is adaptive using the input {@code k}. + * + *

Given a pivot in the middle of the sample this has margins of 1/6 and 1/4. + * + * @param a Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param k Target index. + * @param upper Upper bound (inclusive) of the pivot range. + * @param flags Control flags. + * @return Lower bound (inclusive) of the pivot range. + */ + private static int repeatedStepLeft(double[] a, int l, int r, int k, int[] upper, int flags) { + // Adapted from Alexandrescu (2016), algorithm 9. + int fp; + int s; + int p; + if (flags <= MODE_SAMPLING) { + // Median into a 12th-tile + fp = (r - l + 1) / 12; + // Position the sample around the target k + // Avoid bounds error due to rounding as (k-l)/(r-l) -> 1/12 + s = Math.max(k - mapDistance(k - l, l, r, fp), l + fp); + p = k; + } else { + // i in 2nd quartile + final int f = (r - l + 1) >> 2; + final int f2 = f + f; + for (int i = l + f, end = l + f2; i < end; i++) { + Sorting.lowerMedian4(a, i - f, i, i + f, i + f2); + } + // i in 5th 12th-tile + fp = f / 3; + s = l + f + fp; + // No adaption uses the middle to enforce strict margins + p = s + (flags == MODE_ADAPTION ? mapDistance(k - l, l, r, fp) : (fp >>> 1)); + } + final int e = s + fp - 1; + for (int i = s; i <= e; i++) { + Sorting.sort3(a, i - fp, i, i + fp); + } + p = quickSelectAdaptive2(a, s, e, p, p, upper, qaMode); + return expandPartitionT2(a, l, r, s, e, p, upper[0], upper); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Assumes the range {@code r - l >= 11}; the caller is responsible for selection on a smaller + * range. + * + *

Uses the Chen and Dumitrescu repeated step median-of-medians-of-medians algorithm + * with the upper median of 4 then either median of 3 with the final sample placed in the + * 8th 12th-tile, or max of 3 with the final sample in the 9th 12th-tile; + * the pivot chosen from the sample is adaptive using the input {@code k}. + * + *

Given a pivot in the middle of the sample this has margins of 1/4 and 1/6. + * + * @param a Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param k Target index. + * @param upper Upper bound (inclusive) of the pivot range. + * @param flags Control flags. + * @return Lower bound (inclusive) of the pivot range. + */ + private static int repeatedStepRight(double[] a, int l, int r, int k, int[] upper, int flags) { + // Mirror image repeatedStepLeft using upper median into 3rd quartile + int fp; + int e; + int p; + if (flags <= MODE_SAMPLING) { + // Median into a 12th-tile + fp = (r - l + 1) / 12; + // Position the sample around the target k + // Avoid bounds error due to rounding as (r-k)/(r-l) -> 11/12 + e = Math.min(k + mapDistance(r - k, l, r, fp), r - fp); + p = k; + } else { + // i in 3rd quartile + final int f = (r - l + 1) >> 2; + final int f2 = f + f; + for (int i = r - f, end = r - f2; i > end; i--) { + Sorting.upperMedian4(a, i - f2, i - f, i, i + f); + } + // i in 8th 12th-tile + fp = f / 3; + e = r - f - fp; + // No adaption uses the middle to enforce strict margins + p = e - (flags == MODE_ADAPTION ? mapDistance(r - k, l, r, fp) : (fp >>> 1)); + } + final int s = e - fp + 1; + for (int i = s; i <= e; i++) { + Sorting.sort3(a, i - fp, i, i + fp); + } + p = quickSelectAdaptive2(a, s, e, p, p, upper, qaMode); + return expandPartitionT2(a, l, r, s, e, p, upper[0], upper); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Assumes the range {@code r - l >= 11}; the caller is responsible for selection on a smaller + * range. + * + *

Uses the Chen and Dumitrescu repeated step median-of-medians-of-medians algorithm + * with the minimum of 4 then median of 3; the final sample is placed in the + * 2nd 12th-tile; the pivot chosen from the sample is adaptive using the input {@code k}. + * + *

Given a pivot in the middle of the sample this has margins of 1/12 and 1/3. + * + * @param a Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param k Target index. + * @param upper Upper bound (inclusive) of the pivot range. + * @param flags Control flags. + * @return Lower bound (inclusive) of the pivot range. + */ + private static int repeatedStepFarLeft(double[] a, int l, int r, int k, int[] upper, int flags) { + // Far step has been changed from the Alexandrescu (2016) step of lower-median-of-4, min-of-3 + // into the 4th 12th-tile to a min-of-4, median-of-3 into the 2nd 12th-tile. + // The differences are: + // - The upper margin when not sampling is 8/24 vs. 9/24; the lower margin remains at 1/12. + // - The position of the sample is closer to the expected location of k < |A| / 12. + // - Sampling mode uses a median-of-3 with adaptive k, matching the other step methods. + // A min-of-3 sample can create a pivot too small if used with adaption of k leaving + // k in the larger partition and a wasted iteration. + // - Adaption is adjusted to force use of the lower margin when not sampling. + int fp; + int s; + int p; + if (flags <= MODE_SAMPLING) { + // 2nd 12th-tile + fp = (r - l + 1) / 12; + s = l + fp; + // Use adaption + p = s + mapDistance(k - l, l, r, fp); + } else { + // i in 2nd quartile; min into i-f (1st quartile) + final int f = (r - l + 1) >> 2; + final int f2 = f + f; + for (int i = l + f, end = l + f2; i < end; i++) { + if (a[i + f] < a[i - f]) { + final double u = a[i + f]; + a[i + f] = a[i - f]; + a[i - f] = u; + } + if (a[i + f2] < a[i]) { + final double v = a[i + f2]; + a[i + f2] = a[i]; + a[i] = v; + } + if (a[i] < a[i - f]) { + final double u = a[i]; + a[i] = a[i - f]; + a[i - f] = u; + } + } + // 2nd 12th-tile + fp = f / 3; + s = l + fp; + // Lower margin has 2(d+1) elements; d == (position in sample) - s + // Force k into the lower margin + p = s + ((k - l) >>> 1); + } + final int e = s + fp - 1; + for (int i = s; i <= e; i++) { + Sorting.sort3(a, i - fp, i, i + fp); + } + p = quickSelectAdaptive2(a, s, e, p, p, upper, qaMode); + return expandPartitionT2(a, l, r, s, e, p, upper[0], upper); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Assumes the range {@code r - l >= 11}; the caller is responsible for selection on a smaller + * range. + * + *

Uses the Chen and Dumitrescu repeated step median-of-medians-of-medians algorithm + * with the maximum of 4 then median of 3; the final sample is placed in the + * 11th 12th-tile; the pivot chosen from the sample is adaptive using the input {@code k}. + * + *

Given a pivot in the middle of the sample this has margins of 1/3 and 1/12. + * + * @param a Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param k Target index. + * @param upper Upper bound (inclusive) of the pivot range. + * @param flags Control flags. + * @return Lower bound (inclusive) of the pivot range. + */ + private static int repeatedStepFarRight(double[] a, int l, int r, int k, int[] upper, int flags) { + // Mirror image repeatedStepFarLeft + int fp; + int e; + int p; + if (flags <= MODE_SAMPLING) { + // 11th 12th-tile + fp = (r - l + 1) / 12; + e = r - fp; + // Use adaption + p = e - mapDistance(r - k, l, r, fp); + } else { + // i in 3rd quartile; max into i+f (4th quartile) + final int f = (r - l + 1) >> 2; + final int f2 = f + f; + for (int i = r - f, end = r - f2; i > end; i--) { + if (a[i - f] > a[i + f]) { + final double u = a[i - f]; + a[i - f] = a[i + f]; + a[i + f] = u; + } + if (a[i - f2] > a[i]) { + final double v = a[i - f2]; + a[i - f2] = a[i]; + a[i] = v; + } + if (a[i] > a[i + f]) { + final double u = a[i]; + a[i] = a[i + f]; + a[i + f] = u; + } + } + // 11th 12th-tile + fp = f / 3; + e = r - fp; + // Upper margin has 2(d+1) elements; d == e - (position in sample) + // Force k into the upper margin + p = e - ((r - k) >>> 1); + } + final int s = e - fp + 1; + for (int i = s; i <= e; i++) { + Sorting.sort3(a, i - fp, i, i + fp); + } + p = quickSelectAdaptive2(a, s, e, p, p, upper, qaMode); + return expandPartitionT2(a, l, r, s, e, p, upper[0], upper); + } + + /** + * Partition an array slice around a pivot. Partitioning exchanges array elements such + * that all elements smaller than pivot are before it and all elements larger than + * pivot are after it. + * + *

Partitions a Floyd-Rivest sample around a pivot offset so that the input {@code k} will + * fall in the smaller partition when the entire range is partitioned. + * + *

Assumes the range {@code r - l} is large. + * + * @param a Data array. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @param k Target index. + * @param upper Upper bound (inclusive) of the pivot range. + * @param flags Control flags. + * @return Lower bound (inclusive) of the pivot range. + */ + private static int sampleStep(double[] a, int l, int r, int k, int[] upper, int flags) { + // Floyd-Rivest: use SELECT recursively on a sample of size S to get an estimate + // for the (k-l+1)-th smallest element into a[k], biased slightly so that the + // (k-l+1)-th element is expected to lie in the smaller set after partitioning. + final int n = r - l + 1; + final int ith = k - l + 1; + final double z = Math.log(n); + // sample size = 0.5 * n^(2/3) + final double s = 0.5 * Math.exp(0.6666666666666666 * z); + final double sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * Integer.signum(ith - (n >> 1)); + final int ll = Math.max(l, (int) (k - ith * s / n + sd)); + final int rr = Math.min(r, (int) (k + (n - ith) * s / n + sd)); + // Note: Random sampling is not supported. + // Sample recursion restarts from [ll, rr] + final int p = quickSelectAdaptive2(a, ll, rr, k, k, upper, qaMode); + return expandPartitionT2(a, l, r, ll, rr, p, upper[0], upper); + } + + /** + * Move NaN values to the end of the array. + * This allows all other values to be compared using {@code <, ==, >} operators (with + * the exception of signed zeros). + * + * @param data Values. + * @return index of last non-NaN value (or -1) + */ + static int sortNaN(double[] data) { + int end = data.length; + // Find first non-NaN + while (--end >= 0) { + if (!Double.isNaN(data[end])) { + break; + } + } + for (int i = end; --i >= 0;) { + final double v = data[i]; + if (Double.isNaN(v)) { + // swap(data, i, end--) + data[i] = data[end]; + data[end] = v; + end--; + } + } + return end; + } + + /** + * Move invalid indices to the end of the array. + * + * @param indices Values. + * @param right Upper bound of data (inclusive). + * @param count Count of indices. + * @return count of valid indices + */ + static int countIndices(int[] indices, int count, int right) { + int end = count; + // Find first valid index + while (--end >= 0) { + if (indices[end] <= right) { + break; + } + } + for (int i = end; --i >= 0;) { + final int k = indices[i]; + if (k > right) { + // swap(indices, i, end--) + indices[i] = indices[end]; + indices[end] = k; + end--; + } + } + return end + 1; + } + + /** + * Count the number of signed zeros (-0.0) if the range contains a mix of positive and + * negative zeros. If all positive, or all negative then this returns 0. + * + *

This method can be used when a pivot value is zero during partitioning when the + * method uses the pivot value to replace values matched as equal using {@code ==}. + * This may destroy a mixture of signed zeros by overwriting them as all 0.0 or -0.0 + * depending on the pivot value. + * + * @param data Values. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @return the count of signed zeros if some positive zeros are also present + */ + static int countMixedSignedZeros(double[] data, int left, int right) { + // Count negative zeros + int c = 0; + int cn = 0; + for (int i = left; i <= right; i++) { + if (data[i] == 0) { + c++; + if (Double.doubleToRawLongBits(data[i]) < 0) { + cn++; + } + } + } + return c == cn ? 0 : cn; + } + + /** + * Sort a range of all zero values. + * This orders -0.0 before 0.0. + * + *

Warning: The range must contain only zeros. + * + * @param data Values. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + static void sortZero(double[] data, int left, int right) { + // Count negative zeros + int c = 0; + for (int i = left; i <= right; i++) { + if (Double.doubleToRawLongBits(data[i]) < 0) { + c++; + } + } + // Replace + if (c != 0) { + int i = left; + while (c-- > 0) { + data[i++] = -0.0; + } + while (i <= right) { + data[i++] = 0.0; + } + } + } + + /** + * Detect and fix the sort order of signed zeros. Assumes the data may have been + * partially ordered around zero. + * + *

Searches for zeros if {@code data[left] <= 0} and {@code data[right] >= 0}. + * If zeros are discovered in the range then they are assumed to be continuous. + * + * @param data Values. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + private static void fixContinuousSignedZeros(double[] data, int left, int right) { + int j; + if (data[left] <= 0 && data[right] >= 0) { + int i = left; + while (data[i] < 0) { + i++; + } + j = right; + while (data[j] > 0) { + j--; + } + sortZero(data, i, j); + } + } + + /** + * Creates the maximum recursion depth for single-pivot quickselect recursion. + * + *

Warning: A length of zero will create a negative recursion depth. + * In practice this does not matter as the sort / partition of a length + * zero array should ignore the data. + * + * @param n Length of data (must be strictly positive). + * @return the maximum recursion depth + */ + private int createMaxDepthSinglePivot(int n) { + // Ideal single pivot recursion will take log2(n) steps as data is + // divided into length (n/2) at each iteration. + final int maxDepth = floorLog2(n); + // This factor should be tuned for practical performance + return (int) Math.floor(maxDepth * recursionMultiple) + recursionConstant; + } + + /** + * Compute the maximum recursion depth for single pivot recursion. + * Uses {@code 2 * floor(log2 (x))}. + * + * @param x Value. + * @return {@code log3(x))} + */ + private static int singlePivotMaxDepth(int x) { + return (31 - Integer.numberOfLeadingZeros(x)) << 1; + } + + /** + * Compute {@code floor(log 2 (x))}. This is valid for all strictly positive {@code x}. + * + *

Returns -1 for {@code x = 0} in place of -infinity. + * + * @param x Value. + * @return {@code floor(log 2 (x))} + */ + static int floorLog2(int x) { + return 31 - Integer.numberOfLeadingZeros(x); + } + + /** + * Convert {@code ln(n)} to the single-pivot max depth. + * + * @param x ln(n) + * @return the maximum recursion depth + */ + private int lnNtoMaxDepthSinglePivot(double x) { + final double maxDepth = x * LOG2_E; + return (int) Math.floor(maxDepth * recursionMultiple) + recursionConstant; + } + + /** + * Creates the maximum recursion depth for dual-pivot quickselect recursion. + * + *

Warning: A length of zero will create a high recursion depth. + * In practice this does not matter as the sort / partition of a length + * zero array should ignore the data. + * + * @param n Length of data (must be strictly positive). + * @return the maximum recursion depth + */ + private int createMaxDepthDualPivot(int n) { + // Ideal dual pivot recursion will take log3(n) steps as data is + // divided into length (n/3) at each iteration. + final int maxDepth = log3(n); + // This factor should be tuned for practical performance + return (int) Math.floor(maxDepth * recursionMultiple) + recursionConstant; + } + + /** + * Compute an approximation to {@code log3 (x)}. + * + *

The result is between {@code floor(log3(x))} and {@code ceil(log3(x))}. + * The result is correctly rounded when {@code x +/- 1} is a power of 3. + * + * @param x Value. + * @return {@code log3(x))} + */ + static int log3(int x) { + // log3(2) ~ 1.5849625 + // log3(x) ~ log2(x) * 0.630929753... ~ log2(x) * 323 / 512 (0.630859375) + // Use (floor(log2(x))+1) * 323 / 512 + // This result is always between floor(log3(x)) and ceil(log3(x)). + // It is correctly rounded when x +/- 1 is a power of 3. + return ((32 - Integer.numberOfLeadingZeros(x)) * 323) >>> 9; + } + + /** + * Search the data for the largest index {@code i} where {@code a[i]} is + * less-than-or-equal to the {@code key}; else return {@code left - 1}. + *

+     * a[i] <= k    :   left <= i <= right, or (left - 1)
+     * 
+ * + *

The data is assumed to be in ascending order, otherwise the behaviour is undefined. + * If the range contains multiple elements with the {@code key} value, the result index + * may be any that match. + * + *

This is similar to using {@link java.util.Arrays#binarySearch(int[], int, int, int) + * Arrays.binarySearch}. The method differs in: + *

+ * + *

An equivalent use of binary search is: + *

{@code
+     * int i = Arrays.binarySearch(a, left, right + 1, k);
+     * if (i < 0) {
+     *     i = ~i - 1;
+     * }
+     * }
+ * + *

This specialisation avoids the caller checking the binary search result for the use + * case when the presence or absence of a key is not important; only that the returned + * index for an absence of a key is the largest index. When used on unique keys this + * method can be used to update an upper index so all keys are known to be below a key: + * + *

{@code
+     * int[] keys = ...
+     * // [i0, i1] contains all keys
+     * int i0 = 0;
+     * int i1 = keys.length - 1;
+     * // Update: [i0, i1] contains all keys <= k
+     * i1 = searchLessOrEqual(keys, i0, i1, k);
+     * }
+ * + * @param a Data. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param k Key. + * @return largest index {@code i} such that {@code a[i] <= k}, or {@code left - 1} if no + * such index exists + */ + static int searchLessOrEqual(int[] a, int left, int right, int k) { + int l = left; + int r = right; + while (l <= r) { + // Middle value + final int m = (l + r) >>> 1; + final int v = a[m]; + // Test: + // l------m------r + // v k update left + // k v update right + + // Full binary search + // Run time is up to log2(n) (fast exit on a match) but has more comparisons + if (v < k) { + l = m + 1; + } else if (v > k) { + r = m - 1; + } else { + // Equal + return m; + } + + // Modified search that does not expect a match + // Run time is log2(n). Benchmarks as the same speed. + //if (v > k) { + // r = m - 1; + //} else { + // l = m + 1; + //} + } + // Return largest known value below: + // r is always moved downward when a middle index value is too high + return r; + } + + /** + * Search the data for the smallest index {@code i} where {@code a[i]} is + * greater-than-or-equal to the {@code key}; else return {@code right + 1}. + *
+     * a[i] >= k      :   left <= i <= right, or (right + 1)
+     * 
+ * + *

The data is assumed to be in ascending order, otherwise the behaviour is undefined. + * If the range contains multiple elements with the {@code key} value, the result index + * may be any that match. + * + *

This is similar to using {@link java.util.Arrays#binarySearch(int[], int, int, int) + * Arrays.binarySearch}. The method differs in: + *

+ * + *

An equivalent use of binary search is: + *

{@code
+     * int i = Arrays.binarySearch(a, left, right + 1, k);
+     * if (i < 0) {
+     *     i = ~i;
+     * }
+     * }
+ * + *

This specialisation avoids the caller checking the binary search result for the use + * case when the presence or absence of a key is not important; only that the returned + * index for an absence of a key is the smallest index. When used on unique keys this + * method can be used to update a lower index so all keys are known to be above a key: + * + *

{@code
+     * int[] keys = ...
+     * // [i0, i1] contains all keys
+     * int i0 = 0;
+     * int i1 = keys.length - 1;
+     * // Update: [i0, i1] contains all keys >= k
+     * i0 = searchGreaterOrEqual(keys, i0, i1, k);
+     * }
+ * + * @param a Data. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param k Key. + * @return largest index {@code i} such that {@code a[i] >= k}, or {@code right + 1} if no + * such index exists + */ + static int searchGreaterOrEqual(int[] a, int left, int right, int k) { + int l = left; + int r = right; + while (l <= r) { + // Middle value + final int m = (l + r) >>> 1; + final int v = a[m]; + // Test: + // l------m------r + // v k update left + // k v update right + + // Full binary search + // Run time is up to log2(n) (fast exit on a match) but has more comparisons + if (v < k) { + l = m + 1; + } else if (v > k) { + r = m - 1; + } else { + // Equal + return m; + } + + // Modified search that does not expect a match + // Run time is log2(n). Benchmarks as the same speed. + //if (v < k) { + // l = m + 1; + //} else { + // r = m - 1; + //} + } + // Smallest known value above + // l is always moved upward when a middle index value is too low + return l; + } + + /** + * Creates the source of random numbers in {@code [0, n)}. + * This is configurable via the control flags. + * + * @param n Data length. + * @param k Target index. + * @return the RNG + */ + private IntUnaryOperator createRNG(int n, int k) { + // Configurable + if ((controlFlags & FLAG_MSWS) != 0) { + // Middle-Square Weyl Sequence is fastest int generator + final UniformRandomProvider rng = RandomSource.MSWS.create(n * 31L + k); + if ((controlFlags & FLAG_BIASED_RANDOM) != 0) { + // result = i * [0, 2^32) / 2^32 + return i -> (int) ((i * Integer.toUnsignedLong(rng.nextInt())) >>> Integer.SIZE); + } + return rng::nextInt; + } + if ((controlFlags & FLAG_SPLITTABLE_RANDOM) != 0) { + final SplittableRandom rng = new SplittableRandom(n * 31L + k); + if ((controlFlags & FLAG_BIASED_RANDOM) != 0) { + // result = i * [0, 2^32) / 2^32 + return i -> (int) ((i * Integer.toUnsignedLong(rng.nextInt())) >>> Integer.SIZE); + } + return rng::nextInt; + } + return createFastRNG(n, k); + } + + /** + * Creates the source of random numbers in {@code [0, n)}. + * + *

This uses a RNG based on a linear congruential generator with biased numbers + * in {@code [0, n)}, favouring speed over statitsical robustness. + * + * @param n Data length. + * @param k Target index. + * @return the RNG + */ + static IntUnaryOperator createFastRNG(int n, int k) { + return new Gen(n * 31L + k); + } + + /** + * Random generator for numbers in {@code [0, n)}. + * The random sample should be fast in preference to statistically robust. + * Here we implement a biased sampler for the range [0, n) + * as n * f with f a fraction with base 2^32. + * Source of randomness is a 64-bit LCG using the constants from MMIX by Donald Knuth. + * https://en.wikipedia.org/wiki/Linear_congruential_generator + */ + private static final class Gen implements IntUnaryOperator { + /** LCG state. */ + private long s; + + /** + * @param seed Seed. + */ + Gen(long seed) { + // Update state + this.s = seed * 6364136223846793005L + 1442695040888963407L; + } + + @Override + public int applyAsInt(int n) { + final long x = s; + // Update state + s = s * 6364136223846793005L + 1442695040888963407L; + // Use the upper 32-bits from the state as the random 32-bit sample + // result = n * [0, 2^32) / 2^32 + return (int) ((n * (x >>> Integer.SIZE)) >>> Integer.SIZE); + } + } +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/PartitionFactory.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/PartitionFactory.java new file mode 100644 index 000000000..3ec33d64e --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/PartitionFactory.java @@ -0,0 +1,420 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.commons.numbers.examples.jmh.arrays.Partition.AdaptMode; +import org.apache.commons.numbers.examples.jmh.arrays.Partition.EdgeSelectStrategy; +import org.apache.commons.numbers.examples.jmh.arrays.Partition.ExpandStrategy; +import org.apache.commons.numbers.examples.jmh.arrays.Partition.KeyStrategy; +import org.apache.commons.numbers.examples.jmh.arrays.Partition.LinearStrategy; +import org.apache.commons.numbers.examples.jmh.arrays.Partition.PairedKeyStrategy; +import org.apache.commons.numbers.examples.jmh.arrays.Partition.SPStrategy; +import org.apache.commons.numbers.examples.jmh.arrays.Partition.StopperStrategy; + +/** + * Create instances of partition algorithms. The configuration of the algorithm + * is obtained by harvesting parameters from the name. + * + * @see Partition + * @see KthSelector + * @since 1.2 + */ +final class PartitionFactory { + /** Pattern for the minimum quickselect size. */ + private static final Pattern QS_PATTERN = Pattern.compile("QS(\\d+)"); + /** Pattern for the edgeselect constant. */ + private static final Pattern EC_PATTERN = Pattern.compile("EC(\\d+)"); + /** Pattern for the edgeselect constant for linear select. */ + private static final Pattern LC_PATTERN = Pattern.compile("LC(\\d+)"); + /** Pattern for the sub-sampling size. */ + private static final Pattern SU_PATTERN = Pattern.compile("SU(\\d+)"); + /** Pattern for the recursion multiple (simple float format). */ + private static final Pattern RM_PATTERN = Pattern.compile("RM(\\d+\\.?\\d*)"); + /** Pattern for the recursion constant. */ + private static final Pattern RC_PATTERN = Pattern.compile("RC(\\d+)"); + /** Pattern for the compression level. */ + private static final Pattern CL_PATTERN = Pattern.compile("CL(\\d+)"); + /** Pattern for the control flags. Allow negative flags. */ + private static final Pattern CF_PATTERN = Pattern.compile("CF(-?\\d+)"); + /** Pattern for the option flags. */ + private static final Pattern OF_PATTERN = Pattern.compile("OF(-?\\d+)"); + + /** No instances. */ + private PartitionFactory() {} + + /** + * Creates the {@link KthSelector}. Parameters are derived from the {@code name}. + * + *

After parameters are harvested the only allowed characters are underscores, + * otherwise an exception is thrown. This ensures the parameters in the name were + * correct. + * + * @param name Name. + * @param prefix Method prefix. + * @return the {@link KthSelector} instance + */ + static KthSelector createKthSelector(String name, String prefix) { + return createKthSelector(name, prefix, 0); + } + + /** + * Creates the {@link KthSelector}. Parameters are derived from the {@code name}. This + * uses regex matching or enum name matching. Regex uses a prefix of two characters + * and then a number. Enum name matching finds the longest enum name match from all + * enum values. Ideally enum names from different enums that can be used together + * should be distinct. Enum names in the {@code name} must be prefixed using an underscore. + * + *

Any matches are removed from the {@code name}. After parameters are harvested + * the only allowed characters are underscores, otherwise an exception is thrown. This + * ensures the parameters in the {@code name} were correct. + * + *

Harvests: + *

+ * + * @param name Name. + * @param prefix Method prefix. + * @param qs Minimum quickselect size (if non-zero). + * @return the {@link KthSelector} instance + */ + static KthSelector createKthSelector(String name, String prefix, int qs) { + final String[] s = {name}; + final PivotingStrategy sp = getEnumOrElse(s, PivotingStrategy.class, Partition.PIVOTING_STRATEGY); + final int minQuickSelectSize = qs != 0 ? qs : getMinQuickSelectSize(s); + // Check for unharvested parameters + for (int i = prefix.length(); i < s[0].length(); i++) { + if (s[0].charAt(i) != '_') { + throw new IllegalStateException( + String.format("Unharvested KthSelector parameters: %s -> %s", name, s[0])); + } + } + return new KthSelector(sp, minQuickSelectSize); + } + + /** + * Creates the {@link Partition}. Parameters are derived from the {@code name}. + * + *

After parameters are harvested the only allowed characters are underscores, + * otherwise an exception is thrown. This ensures the parameters in the name were + * correct. + * + * @param name Name. + * @param prefix Method prefix. + * @return the {@link Partition} instance + * @see #createPartition(String, String, int, int) + */ + static Partition createPartition(String name, String prefix) { + return createPartition(name, prefix, 0, 0); + } + + /** + * Creates the {@link Partition}. Parameters are derived from the {@code name}. This + * uses regex matching or enum name matching. Regex uses a prefix of two characters + * and then a number. Enum name matching finds the longest enum name match from all + * enum values. Ideally enum names from different enums that can be used together + * should be distinct. Enum names in the {@code name} must be prefixed using an underscore. + * + *

Any matches are removed from the {@code name}. After parameters are harvested + * the only allowed characters are underscores, otherwise an exception is thrown. This + * ensures the parameters in the {@code name} were correct. + * + *

Harvests: + *

+ * + * @param name Name. + * @param prefix Method prefix. + * @param qs Minimum quickselect size (if non-zero). + * @param ec Minimum edgeselect constant (if non-zero); also used for linear sort select size. + * @return the {@link Partition} instance + */ + static Partition createPartition(String name, String prefix, int qs, int ec) { + if (!name.startsWith(prefix)) { + throw new IllegalArgumentException("Invalid prefix: " + prefix + " for " + name); + } + final String[] s = {name.substring(prefix.length())}; + final PivotingStrategy sp = getEnumOrElse(s, PivotingStrategy.class, Partition.PIVOTING_STRATEGY); + final DualPivotingStrategy dp = getEnumOrElse(s, DualPivotingStrategy.class, Partition.DUAL_PIVOTING_STRATEGY); + final int minQuickSelectSize = qs != 0 ? qs : getMinQuickSelectSize(s); + final int edgeSelectConstant = ec != 0 ? ec : getEdgeSelectConstant(s); + final int linearSortSelectConstant = ec != 0 ? ec : getLinearSortSelectConstant(s); + final int subSamplingSize = getSubSamplingSize(s); + final KeyStrategy keyStrategy = getEnumOrElse(s, KeyStrategy.class, Partition.KEY_STRATEGY); + final PairedKeyStrategy pairedKeyStrategy = + getEnumOrElse(s, PairedKeyStrategy.class, Partition.PAIRED_KEY_STRATEGY); + final double recursionMultiple = getRecursionMultiple(s); + final int recursionConstant = getRecursionConstant(s); + final int compressionLevel = getCompressionLevel(s); + final int controlFlags = getControlFlags(s); + final SPStrategy spStrategy = getEnumOrElse(s, SPStrategy.class, Partition.SP_STRATEGY); + final ExpandStrategy expandStrategy = getEnumOrElse(s, ExpandStrategy.class, Partition.EXPAND_STRATEGY); + final LinearStrategy linearStrategy = getEnumOrElse(s, LinearStrategy.class, Partition.LINEAR_STRATEGY); + final EdgeSelectStrategy esStrategy = getEnumOrElse(s, EdgeSelectStrategy.class, Partition.EDGE_STRATEGY); + final StopperStrategy stopStrategy = getEnumOrElse(s, StopperStrategy.class, Partition.STOPPER_STRATEGY); + final AdaptMode adaptMode = getEnumOrElse(s, AdaptMode.class, Partition.ADAPT_MODE); + // Check for unharvested parameters + for (int i = s[0].length(); --i >= 0;) { + if (s[0].charAt(i) != '_') { + throw new IllegalStateException( + String.format("Unharvested Partition parameters: %s -> %s", name, prefix + s[0])); + } + } + final Partition p = new Partition(sp, dp, minQuickSelectSize, + edgeSelectConstant, subSamplingSize); + // Some values do not have to be final as they are not used within optimised + // partitioning code. + p.setKeyStrategy(keyStrategy); + p.setPairedKeyStrategy(pairedKeyStrategy); + p.setRecursionMultiple(recursionMultiple); + p.setRecursionConstant(recursionConstant); + p.setCompression(compressionLevel); + p.setControlFlags(controlFlags); + p.setSPStrategy(spStrategy); + p.setExpandStrategy(expandStrategy); + p.setLinearStrategy(linearStrategy); + p.setEdgeSelectStrategy(esStrategy); + p.setStopperStrategy(stopStrategy); + p.setLinearSortSelectSize(linearSortSelectConstant); + p.setAdaptMode(adaptMode); + + return p; + } + + /** + * Gets the minimum size for the recursive quickselect partition algorithm. + * Below this size the algorithm will change strategy for partitioning, + * e.g. change to a full sort. + * + * @param name Algorithm name (updated in-place to remove the parameter). + * @return the minimum quickselect size + */ + static int getMinQuickSelectSize(String[] name) { + final Matcher m = QS_PATTERN.matcher(name[0]); + if (m.find()) { + final int i = Integer.parseInt(name[0], m.start(1), m.end(1), 10); + name[0] = name[0].substring(0, m.start()) + name[0].substring(m.end(), name[0].length()); + return i; + } + return Partition.MIN_QUICKSELECT_SIZE; + } + + /** + * Gets the constant for the edgeselect distance-from-end computation. + * + * @param name Algorithm name (updated in-place to remove the parameter). + * @return the edgeselect constant + */ + static int getEdgeSelectConstant(String[] name) { + final Matcher m = EC_PATTERN.matcher(name[0]); + if (m.find()) { + final int i = Integer.parseInt(name[0], m.start(1), m.end(1), 10); + name[0] = name[0].substring(0, m.start()) + name[0].substring(m.end(), name[0].length()); + return i; + } + return Partition.EDGESELECT_CONSTANT; + } + + /** + * Gets the constant for the sortselect distance-from-end computation for linearselect. + * + * @param name Algorithm name (updated in-place to remove the parameter). + * @return the sortselect constant + */ + static int getLinearSortSelectConstant(String[] name) { + final Matcher m = LC_PATTERN.matcher(name[0]); + if (m.find()) { + final int i = Integer.parseInt(name[0], m.start(1), m.end(1), 10); + name[0] = name[0].substring(0, m.start()) + name[0].substring(m.end(), name[0].length()); + return i; + } + return Partition.LINEAR_SORTSELECT_SIZE; + } + + /** + * Gets the minimum size for single-pivot sub-sampling (using the Floyd-Rivest algorithm). + * + * @param name Algorithm name (updated in-place to remove the parameter). + * @return the sub-sampling size + */ + static int getSubSamplingSize(String[] name) { + final Matcher m = SU_PATTERN.matcher(name[0]); + if (m.find()) { + final int i = Integer.parseInt(name[0], m.start(1), m.end(1), 10); + name[0] = name[0].substring(0, m.start()) + name[0].substring(m.end(), name[0].length()); + return i; + } + return Partition.SUBSAMPLING_SIZE; + } + + /** + * Gets the recursion multiplication factor. + * + * @param name Algorithm name (updated in-place to remove the parameter). + * @return the recursion multiple + */ + static double getRecursionMultiple(String[] name) { + final Matcher m = RM_PATTERN.matcher(name[0]); + if (m.find()) { + final double d = Double.parseDouble(m.group(1)); + name[0] = name[0].substring(0, m.start()) + name[0].substring(m.end(), name[0].length()); + return d; + } + return Partition.RECURSION_MULTIPLE; + } + + /** + * Gets the recursion constant. + * + * @param name Algorithm name (updated in-place to remove the parameter). + * @return the recursion constant + */ + static int getRecursionConstant(String[] name) { + final Matcher m = RC_PATTERN.matcher(name[0]); + if (m.find()) { + final int i = Integer.parseInt(name[0], m.start(1), m.end(1), 10); + name[0] = name[0].substring(0, m.start()) + name[0].substring(m.end(), name[0].length()); + return i; + } + return Partition.RECURSION_CONSTANT; + } + + /** + * Gets the compression level for {@link CompressedIndexSet}. + * + * @param name Algorithm name (updated in-place to remove the parameter). + * @return the compression + */ + static int getCompressionLevel(String[] name) { + final Matcher m = CL_PATTERN.matcher(name[0]); + if (m.find()) { + final int i = Integer.parseInt(name[0], m.start(1), m.end(1), 10); + name[0] = name[0].substring(0, m.start()) + name[0].substring(m.end(), name[0].length()); + return i; + } + return Partition.COMPRESSION_LEVEL; + } + + /** + * Gets the control flags. These are used to enable additional features, for example + * random sampling in the Floyd-Rivest algorithm. + * + * @param name Algorithm name (updated in-place to remove the parameter). + * @return the control flags + */ + static int getControlFlags(String[] name) { + return getControlFlags(name, Partition.CONTROL_FLAGS); + } + + /** + * Gets the control flags. These are used to enable additional features, for example + * random sampling in the Floyd-Rivest algorithm. + * + * @param name Algorithm name (updated in-place to remove the parameter). + * @param defaultValue Default value. + * @return the control flags + */ + static int getControlFlags(String[] name, int defaultValue) { + final Matcher m = CF_PATTERN.matcher(name[0]); + if (m.find()) { + final int i = Integer.parseInt(name[0], m.start(1), m.end(1), 10); + name[0] = name[0].substring(0, m.start()) + name[0].substring(m.end(), name[0].length()); + return i; + } + return defaultValue; + } + + /** + * Gets the option flags. These are used to enable additional features, and can be + * used separately to the control flags. + * + * @param name Algorithm name (updated in-place to remove the parameter). + * @return the option flags + */ + static int getOptionFlags(String[] name) { + return getOptionFlags(name, Partition.OPTION_FLAGS); + } + + /** + * Gets the option flags. These are used to enable additional features, and can be + * used separately to the control flags. + * + * @param name Algorithm name (updated in-place to remove the parameter). + * @param defaultValue Default value. + * @return the option flags + */ + static int getOptionFlags(String[] name, int defaultValue) { + final Matcher m = OF_PATTERN.matcher(name[0]); + if (m.find()) { + final int i = Integer.parseInt(name[0], m.start(1), m.end(1), 10); + name[0] = name[0].substring(0, m.start()) + name[0].substring(m.end(), name[0].length()); + return i; + } + return defaultValue; + } + + /** + * Gets the enum from the name. The enum name must be prefixed with an underscore. + * + * @param Enum type. + * @param name Algorithm name (updated in-place to remove the parameter). + * @param cls Class of the enum. + * @param defaultValue Default value. + * @return the enum value + */ + static > E getEnumOrElse(String[] name, Class cls, E defaultValue) { + // Names can have partial matches. Match the longest name + int index = -1; + int len = 0; + E result = defaultValue; + for (final E s : cls.getEnumConstants()) { + // Use the index so we can mandate that the enum is prefixed by underscore + final int i = name[0].indexOf(s.name()); + if ((i > 0 && name[0].charAt(i - 1) == '_' || i == 0) && s.name().length() > len) { + index = i; + len = s.name().length(); + result = s; + } + } + if (index >= 0) { + name[0] = name[0].substring(0, index) + name[0].substring(index + len); + } + return result; + } +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/PivotCache.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/PivotCache.java new file mode 100644 index 000000000..89953c240 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/PivotCache.java @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * A cache of pivot indices used for partitioning an array into multiple regions. + * + *

A pivot is an index position that contains a value equal to the value in a fully + * sorted array. + * + *

For a pivot {@code p}: + * + *

{@code
+ * i < p < j
+ * data[i] <= data[p] <= data[j]
+ * }
+ * + *

Partitioning moves data in a range {@code [lower, upper]} so that the pivot + * {@code p} partitions the data. During this process many pivots may be found + * before the search ends. A pivot cache supports storing these pivots so that + * they can be used to bracket further searches in the array. + * + *

A pivot cache supports finding a search bracket to partition an index {@code k} + * within an array of length {@code n}. The bracket should be an enclosing bound + * of known pivots. All data can be rearranged within this bracket without destroying + * other regions of the partitioned array. The support for {@code k} is provided within + * an inclusive range {@code [left, right]} where {@code 0 <= left <= right < n}. + * Thus {@code [left, right]} denotes the region containing all target indices {@code k} + * for multi-region partitioning. + * + *

The cache provides the following functionality: + * + *

    + *
  • Test if an index {@code [left <= k <= right]} is a known pivot. + *
  • Return a {@code lower} bounding pivot for a partition of an index {@code [left <= k <= right]}. + *
  • Return an {@code upper} bounding pivot for a partition of an index {@code [left <= k <= right]}. + *
+ * + *

Note that searching with the bound {@code [lower, upper]} will reorder data + * and pivots within this range may be invalidated by moving of data. To prevent + * error the bound provided by a cache must use the closest bracketing pivots. + * + *

At least two strategies can be used: + * + *

    + *
  1. Process {@code k} indices in any order. Store all pivots during the partitioning. + * Each subsequent search after the first can use adjacent pivots to bracket the search. + *
  2. Process {@code k} indices in sorted order. The {@code lower} bound for {@code k+1} + * will be {@code k <= lower}. This does not require a cache as {@code upper} can be set + * using the end of the data {@code n}. For this case a cache can store pivots which can + * be used to bracket the search for {@code k+1}. + *
+ * + *

Implementations may assume indices are positive. + * + * @since 1.2 + */ +interface PivotCache extends PivotStore { + /** + * The start (inclusive) of the range of indices supported. + * + * @return start of the supported range + */ + int left(); + + /** + * The end (inclusive) of the range of indices supported. + * + * @return end of the supported range + */ + int right(); + + /** + * Returns {@code true} if the cache supports storing some of the pivots in the supported + * range. A sparse cache can provide approximate bounds for partitioning. These bounds may be + * smaller than using the bounds of the entire array. Note that partitioning may destroy + * previous pivots within a range. Thus a sparse cache should be used to partition indices + * in sorted order so that bounds generated by each iteration do not overlap the bounds + * of a previous partition. This can be done by using the previous {@code k} as the left + * bound. + * + *

A sparse cache can be created to store 1 pivot between all {@code k} of interest + * after the first {@code k}, and optionally two pivots that bracket the entire supported + * range. In the following example the partition of {@code k1} stores pivots {@code p}. + * These can be used to bracket {@code k2, k3}. An alternative scheme + * where no pivots are stored is shown for comparison: + * + *

+     * Partition:
+     * 0------k1----------k2------k3---------N
+     *
+     * Iteration 1:
+     * 0------k1------p--------p---------p---N
+     *
+     * Iteration 2:
+     *                l---k2---r
+     * or:    l-----------k2-----------------N
+     *
+     * Iteration 3:
+     *                         l--k3-----r
+     * or:                l-------k3---------N
+     * 
+ * + *

If false then the cache will store all pivots within the supported range and + * ideally provide the closest bounding pivot around the supported range. + * + * @return true if sparse + */ + boolean sparse(); + + /** + * Test if the index {@code k} is a pivot. + * + *

If {@code index < left} or {@code index > right} the behavior is not + * defined.

+ * + * @param k Index. + * @return true if the index is a pivot within the supported range + */ + boolean contains(int k); + + /** + * Returns the nearest pivot index that occurs on or before the specified starting + * index. If none exist then {@code -1} is returned. + * + * @param k Index to start checking from (inclusive). + * @return the index of the previous pivot, or {@code -1} if there is no index + */ + int previousPivot(int k); + + /** + * Returns the nearest pivot index that occurs on or after the specified starting + * index. If none exist then {@code -1} is returned. + * + * @param k Index to start checking from (inclusive). + * @return the index of the next pivot, or {@code -1} if there is no index + */ + default int nextPivot(int k) { + return nextPivotOrElse(k, -1); + } + + /** + * Returns the nearest pivot index that occurs on or after the specified starting + * index. If none exist then {@code other} is returned. + * + * @param k Index to start checking from (inclusive). + * @param other Other value. + * @return the index of the next pivot, or {@code other} if there is no index + */ + int nextPivotOrElse(int k, int other); +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/PivotCaches.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/PivotCaches.java new file mode 100644 index 000000000..e7ccf7323 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/PivotCaches.java @@ -0,0 +1,374 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * Support for creating {@link PivotCache} implementations. + * + * @since 1.2 + */ +final class PivotCaches { + /** Default value for an unset upper floating pivot. + * Set as a value higher than any valid array index. */ + private static final int UPPER_DEFAULT = Integer.MAX_VALUE; + + /** No instances. */ + private PivotCaches() {} + + /** + * Return a {@link PivotCache} for a single {@code k}. + * + * @param k Index. + * @return the pivot cache + */ + static PivotCache ofIndex(int k) { + return new PointPivotCache(k); + } + + /** + * Return a {@link PivotCache} for a single {@code k}, + * or a pair of indices {@code (k, k+1)}. A pair is + * signalled using the sign bit. + * + * @param k Paired index. + * @return the pivot cache + */ + static PivotCache ofPairedIndex(int k) { + if (k >= 0) { + return new PointPivotCache(k); + } + // Remove sign bit + final int ka = k & Integer.MAX_VALUE; + return new RangePivotCache(ka, ka + 1); + } + + /** + * Return a {@link PivotCache} for the range {@code [left, right]}. + * + *

If the range contains internal indices, the {@link PivotCache} will not + * store them and will be {@link PivotCache#sparse() sparse}. + * + *

The range returned instance may implement {@link ScanningPivotCache}. + * It should only be cast to a {@link ScanningPivotCache} and used for scanning + * if it reports itself as non-{@link PivotCache#sparse() sparse}. + * + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @return the pivot cache + * @see #ofFullRange(int, int) + */ + static PivotCache ofRange(int left, int right) { + validateRange(left, right); + return left == right ? + new PointPivotCache(left) : + new RangePivotCache(left, right); + } + + /** + * Return a {@link PivotCache} for the full-range {@code [left, right]}. + * The returned implementation will be non-{@link PivotCache#sparse() sparse}. + * + *

The range returned instance may implement {@link ScanningPivotCache}. + * + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @return the pivot cache + */ + static PivotCache ofFullRange(int left, int right) { + validateRange(left, right); + if (right - left <= 1) { + return left == right ? + new PointPivotCache(left) : + new RangePivotCache(left, right); + } + return IndexSet.ofRange(left, right); + } + + /** + * Validate the range {@code left <= right}. + * + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + private static void validateRange(int left, int right) { + if (right < left) { + throw new IllegalArgumentException("Invalid range"); + } + } + + /** + * PivotCache for range {@code [left, right]} consisting of a single point. + */ + private static class PointPivotCache implements ScanningPivotCache { + /** The target point. */ + private final int target; + /** The upstream pivot closest to the left bound of the support. + * Provides a lower search bound for the range [left, right]. */ + private int lowerPivot = -1; + /** The downstream pivot closest to the right bound of the support. + * Provides an upper search bound for the range [left, right]. */ + private int upperPivot = UPPER_DEFAULT; + + /** + * @param index Index defining {@code [left, right]}. + */ + PointPivotCache(int index) { + this.target = index; + } + + @Override + public void add(int index) { + // Update the floating pivots + if (index <= target) { + // This does not update upperPivot if index == target. + // This case is checked in nextPivot(int). + lowerPivot = Math.max(index, lowerPivot); + } else { + upperPivot = Math.min(index, upperPivot); + } + } + + @Override + public void add(int fromIndex, int toIndex) { + // Update the floating pivots + if (toIndex <= target) { + // This does not update upperPivot if toIndex == target. + // This case is checked in nextPivot(int). + lowerPivot = Math.max(toIndex, lowerPivot); + } else if (fromIndex > target) { + upperPivot = Math.min(fromIndex, upperPivot); + } else { + // Range brackets the target + lowerPivot = upperPivot = target; + } + } + + @Override + public int left() { + return target; + } + + @Override + public int right() { + return target; + } + + @Override + public boolean sparse() { + // Not sparse between [left, right] + return false; + } + + @Override + public boolean moveLeft(int newLeft) { + // Unsupported + return false; + } + + @Override + public boolean contains(int k) { + return lowerPivot == k; + } + + @Override + public int previousPivot(int k) { + // Only support scanning within [left, right] => assume k == target + return lowerPivot; + } + + // Do not override: int nextPivot(int k) + + @Override + public int nextPivotOrElse(int k, int other) { + // Only support scanning within [left, right] + // assume lowerPivot <= left <= k <= right <= upperPivot + if (lowerPivot == target) { + return target; + } + return upperPivot == UPPER_DEFAULT ? other : upperPivot; + } + + @Override + public int nextNonPivot(int k) { + // Only support scanning within [left, right] => assume k == target + return lowerPivot == target ? target + 1 : target; + } + + @Override + public int previousNonPivot(int k) { + // Only support scanning within [left, right] => assume k == target + return lowerPivot == target ? target - 1 : target; + } + } + + /** + * PivotCache for range {@code [left, right]} consisting of a bracketing range + * {@code lower <= left < right <= upper}. + * + *

Behaviour is undefined if {@code left == right}. This cache is intended to + * bracket a range [left, right] that can be entirely sorted, e.g. if the separation + * between left and right is small. + */ + private static class RangePivotCache implements ScanningPivotCache { + /** Left bound of the support. */ + private final int left; + /** Right bound of the support. */ + private final int right; + /** The upstream pivot closest to the left bound of the support. + * Provides a lower search bound for the range [left, right]. */ + private int lowerPivot = -1; + /** The downstream pivot closest to the right bound of the support. + * Provides an upper search bound for the range [left, right]. */ + private int upperPivot = UPPER_DEFAULT; + + /** + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + RangePivotCache(int left, int right) { + this.left = left; + this.right = right; + } + + @Override + public void add(int index) { + // Update the floating pivots + if (index <= left) { + lowerPivot = Math.max(index, lowerPivot); + } else if (index >= right) { + upperPivot = Math.min(index, upperPivot); + } + } + + @Override + public void add(int fromIndex, int toIndex) { + // Update the floating pivots + if (toIndex <= left) { + // l-------------r + // f---t + lowerPivot = Math.max(toIndex, lowerPivot); + } else if (fromIndex >= right) { + // l-------------r + // f---t + upperPivot = Math.min(fromIndex, upperPivot); + } else { + // Range [left, right] overlaps [from, to] + // toIndex > left && fromIndex < right + // l-------------r + // f---t + // f----t + // f----t + if (fromIndex <= left) { + lowerPivot = left; + } + if (toIndex >= right) { + upperPivot = right; + } + } + } + + @Override + public int left() { + return left; + } + + @Override + public int right() { + return right; + } + + @Override + public boolean sparse() { + // Sparse if there are internal points between [left, right] + return right - left > 1; + } + + @Override + public boolean moveLeft(int newLeft) { + // Unsupported + return false; + } + + @Override + public boolean contains(int k) { + return lowerPivot == k || upperPivot == k; + } + + @Override + public int previousPivot(int k) { + // Only support scanning within [left, right] + // assume lowerPivot <= left <= k <= right <= upperPivot + return k == upperPivot ? k : lowerPivot; + } + + // Do not override: int nextPivot(int k) + + @Override + public int nextPivotOrElse(int k, int other) { + // Only support scanning within [left, right] + // assume lowerPivot <= left <= k <= right <= upperPivot + if (k == lowerPivot) { + return k; + } + return upperPivot == UPPER_DEFAULT ? other : upperPivot; + } + + @Override + public int nextNonPivot(int k) { + // Only support scanning within [left, right] + // assume lowerPivot <= left <= k <= right <= upperPivot + if (sparse()) { + throw new UnsupportedOperationException(); + } + // range of size 2 + // scan right + int i = k; + if (i == left) { + if (lowerPivot != left) { + return left; + } + i++; + } + if (i == right && upperPivot == right) { + i++; + } + return i; + } + + @Override + public int previousNonPivot(int k) { + // Only support scanning within [left, right] + // assume lowerPivot <= left <= k <= right <= upperPivot + if (sparse()) { + throw new UnsupportedOperationException(); + } + // range of size 2 + // scan left + int i = k; + if (i == right) { + if (upperPivot != right) { + return right; + } + i--; + } + if (i == left && lowerPivot == left) { + i--; + } + return i; + } + } +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/PivotStore.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/PivotStore.java new file mode 100644 index 000000000..60ad416f7 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/PivotStore.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * Storage for pivot indices used for partitioning an array into multiple regions. + * + *

A pivot is an index position that contains a value equal to the value in a fully + * sorted array. + * + *

For a pivot {@code p}: + * + *

{@code
+ * i < p < j
+ * data[i] <= data[p] <= data[j]
+ * }
+ * + *

Implementations may assume indices are positive. Implementations are not required to + * store all indices, and may discard previously stored indices during operation. Behaviour + * should be documented. + * + *

This interface is used by methods that create pivots. Methods that use pivots should + * use the {@link PivotCache} interface. + * + * @since 1.2 + */ +interface PivotStore { + /** + * Add the pivot index to the store. + * + * @param index Index. + */ + void add(int index); + + /** + * Add a range of pivot indices to the store. + * + *

If {@code fromIndex == toIndex} this is equivalent to {@link #add(int)}. + * + *

If {@code fromIndex > toIndex} the behavior is not defined.

+ * + * @param fromIndex Start index of the range (inclusive). + * @param toIndex End index of the range (inclusive). + */ + void add(int fromIndex, int toIndex); +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/PivotingStrategy.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/PivotingStrategy.java new file mode 100644 index 000000000..59f2c0f59 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/PivotingStrategy.java @@ -0,0 +1,345 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * A strategy to pick a pivoting index of an array for partitioning. + * + *

An ideal strategy will pick [1/2, 1/2] across a variety of data. + * + * @since 1.2 + */ +enum PivotingStrategy { + /** + * Pivot around the centre of the range. + */ + CENTRAL { + @Override + int pivotIndex(double[] data, int left, int right, int ignored) { + return med(left, right); + } + + @Override + int[] getSampledIndices(int left, int right, int ignored) { + return new int[] {med(left, right)}; + } + + @Override + int samplingEffect() { + return UNCHANGED; + } + }, + /** + * Pivot around the median of 3 values within the range: the first; the centre; and the last. + */ + MEDIAN_OF_3 { + @Override + int pivotIndex(double[] data, int left, int right, int ignored) { + return med3(data, left, med(left, right), right); + } + + @Override + int[] getSampledIndices(int left, int right, int ignored) { + return new int[] {left, med(left, right), right}; + } + + @Override + int samplingEffect() { + return UNCHANGED; + } + }, + /** + * Pivot around the median of 9 values within the range. + * Uses the median of 3 medians of 3. The returned value + * is ranked 4, 5, or 6 out of the 9 values. + * This is also known in the literature as Tukey’s "ninther" pivot. + */ + MEDIAN_OF_9 { + @Override + int pivotIndex(double[] data, int left, int right, int ignored) { + final int s = (right - left) >>> 3; + final int m = med(left, right); + final int x = med3(data, left, left + s, left + (s << 1)); + final double a = data[x]; + final int y = med3(data, m - s, m, m + s); + final double b = data[y]; + final int z = med3(data, right - (s << 1), right - s, right); + return med3(a, b, data[z], x, y, z); + } + + @Override + int[] getSampledIndices(int left, int right, int ignored) { + final int s = (right - left) >>> 3; + final int m = med(left, right); + return new int[] { + left, left + s, left + (s << 1), + m - s, m, m + s, + right - (s << 1), right - s, right + }; + } + + @Override + int samplingEffect() { + return UNCHANGED; + } + }, + /** + * Pivot around the median of 3 or 9 values within the range. + * + *

Note: Bentley & McIlroy (1993) choose a size of 40 to pivot around 9 values; + * and a lower size of 7 to use the central; otherwise the median of 3. + * This method does not switch to the central method for small sizes. + */ + DYNAMIC { + @Override + int pivotIndex(double[] data, int left, int right, int ignored) { + if (right - left >= MED_9) { + return MEDIAN_OF_9.pivotIndex(data, left, right, ignored); + } + return MEDIAN_OF_3.pivotIndex(data, left, right, ignored); + } + + @Override + int[] getSampledIndices(int left, int right, int ignored) { + if (right - left >= MED_9) { + return MEDIAN_OF_9.getSampledIndices(left, right, ignored); + } + return MEDIAN_OF_3.getSampledIndices(left, right, ignored); + } + + @Override + int samplingEffect() { + return UNCHANGED; + } + }, + /** + * Pivot around the median of 5 values within the range. + * Requires that {@code right - left >= 4}. + * + *

Warning: This has the side effect that the 5 values are also partially sorted. + * + *

Uses the same spacing as {@link DualPivotingStrategy#SORT_5}. + */ + MEDIAN_OF_5 { + @Override + int pivotIndex(double[] data, int left, int right, int ignored) { + // 1/6 = 5/30 ~ 1/8 + 1/32 + 1/64 : 0.1666 ~ 0.1719 + // Ensure the value is above zero to choose different points! + // This is safe if len >= 4. + final int len = right - left; + final int sixth = 1 + (len >>> 3) + (len >>> 5) + (len >>> 6); + // Note: No use of median(left, right). This is not targeted by median of 3 killer + // input as it does not use the end points left and right. + final int p3 = left + (len >>> 1); + final int p2 = p3 - sixth; + final int p1 = p2 - sixth; + final int p4 = p3 + sixth; + final int p5 = p4 + sixth; + return Sorting.median5(data, p1, p2, p3, p4, p5); + } + + @Override + int[] getSampledIndices(int left, int right, int ignored) { + final int len = right - left; + final int sixth = 1 + (len >>> 3) + (len >>> 5) + (len >>> 6); + final int p3 = left + (len >>> 1); + final int p2 = p3 - sixth; + final int p1 = p2 - sixth; + final int p4 = p3 + sixth; + final int p5 = p4 + sixth; + return new int[] {p1, p2, p3, p4, p5}; + } + + @Override + int samplingEffect() { + return PARTIAL_SORT; + } + }, + /** + * Pivot around the median of 5 values within the range. + * Requires that {@code right - left >= 4}. + * + *

Warning: This has the side effect that the 5 values are also partially sorted. + * + *

Uses the same spacing as {@link DualPivotingStrategy#SORT_5B}. + */ + MEDIAN_OF_5B { + @Override + int pivotIndex(double[] data, int left, int right, int ignored) { + // 1/7 = 5/35 ~ 1/8 + 1/64 : 0.1429 ~ 0.1406 + // Ensure the value is above zero to choose different points! + // This is safe if len >= 4. + final int len = right - left; + final int seventh = 1 + (len >>> 3) + (len >>> 6); + final int p3 = left + (len >>> 1); + final int p2 = p3 - seventh; + final int p1 = p2 - seventh; + final int p4 = p3 + seventh; + final int p5 = p4 + seventh; + Sorting.sort4(data, p1, p2, p4, p5); + // p2 and p4 are sorted: check if p3 is between them + if (data[p3] < data[p2]) { + return p2; + } + return data[p3] > data[p4] ? p4 : p3; + } + + @Override + int[] getSampledIndices(int left, int right, int ignored) { + final int len = right - left; + final int seventh = 1 + (len >>> 3) + (len >>> 6); + final int p3 = left + (len >>> 1); + final int p2 = p3 - seventh; + final int p1 = p2 - seventh; + final int p4 = p3 + seventh; + final int p5 = p4 + seventh; + return new int[] {p1, p2, p3, p4, p5}; + } + + @Override + int samplingEffect() { + return PARTIAL_SORT; + } + }, + /** + * Pivot around the target index. + */ + TARGET { + @Override + int pivotIndex(double[] data, int left, int right, int k) { + return k; + } + + @Override + int[] getSampledIndices(int left, int right, int k) { + return new int[] {k}; + } + + @Override + int samplingEffect() { + return UNCHANGED; + } + }; + + /** Sampled points are unchanged. */ + static final int UNCHANGED = 0; + /** Sampled points are partially sorted. */ + static final int PARTIAL_SORT = 0x1; + /** Sampled points are sorted. */ + static final int SORT = 0x2; + /** Size to pivot around the median of 9. */ + private static final int MED_9 = 40; + + /** + * Compute the median index. + * + *

Note: This intentionally uses the median as {@code left + (right - left + 1) / 2}. + * If the median is {@code left + (right - left) / 2} then the median is 1 position lower + * for even length due to using an inclusive right bound. This median is not as affected + * by median-of-3 killer sequences. For benchmarking it is useful to maintain the classic + * median-of-3 behaviour to be able to trigger worst case performance on input + * used in the literature. + * + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @return the median index + */ + private static int med(int left, int right) { + return (left + right + 1) >>> 1; + } + + /** + * Find the median index of 3. + * + * @param data Values. + * @param i Index. + * @param j Index. + * @param k Index. + * @return the median index + */ + private static int med3(double[] data, int i, int j, int k) { + return med3(data[i], data[j], data[k], i, j, k); + } + + /** + * Find the median index of 3 values. + * + * @param a Value. + * @param b Value. + * @param c Value. + * @param ia Index of a. + * @param ib Index of b. + * @param ic Index of c. + * @return the median index + */ + private static int med3(double a, double b, double c, int ia, int ib, int ic) { + if (a < b) { + if (b < c) { + return ib; + } + return a < c ? ic : ia; + } + if (b > c) { + return ib; + } + return a > c ? ic : ia; + } + + /** + * Find a pivot index of the array so that partitioning into 2-regions can be made. + * + *

{@code
+     * left <= p <= right
+     * }
+ * + *

The argument {@code k} is the target index in {@code [left, right]}. Strategies + * may use this to help select the pivot index. If not available (e.g. selecting a pivot + * for quicksort) then choose a value in {@code [left, right]} to be safe. + * + * @param data Array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param k Target index. + * @return pivot + */ + abstract int pivotIndex(double[] data, int left, int right, int k); + + // The following methods allow the strategy and side effects to be tested + + /** + * Get the indices of points that will be sampled. + * + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param k Target index. + * @return the indices + */ + abstract int[] getSampledIndices(int left, int right, int k); + + /** + * Get the effect on the sampled points. + *

    + *
  • 0 - Unchanged + *
  • 1 - Partially sorted + *
  • 2 - Sorted + *
+ * + * @return the effect + */ + abstract int samplingEffect(); +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/ScanningKeyInterval.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/ScanningKeyInterval.java new file mode 100644 index 000000000..d5b491652 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/ScanningKeyInterval.java @@ -0,0 +1,223 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * An {@link SearchableInterval} backed by an array of ordered keys. The interval is searched using + * a linear scan of the data. The scan start point is chosen from reference points within the data. + * + *

The scan is fast when the number of keys is small. + * + * @since 1.2 + */ +final class ScanningKeyInterval implements SearchableInterval, SearchableInterval2 { + // Note: + // Using 4 markers into the data allows this class to return the same + // performance as using a binary search within the data when n < 1600. + // Benchmarked by searching once for next and previous from median points between k. + + /** The ordered keys. */ + private final int[] keys; + /** The original number of keys. */ + private final int n; + /** Index into the keys (used for fast-forward). */ + private final int i1; + /** Index into the keys (used for fast-forward). */ + private final int i2; + /** Index into the keys (used for fast-forward). */ + private final int i3; + + /** + * Create an instance with the provided keys. + * + * @param indices Indices. + * @param n Number of indices. + */ + ScanningKeyInterval(int[] indices, int n) { + keys = indices; + this.n = n; + // Divide into quarters for fast-forward + i1 = n >>> 2; + i2 = n >>> 1; + i3 = i1 + i2; + } + + /** + * Initialise an instance with {@code n} initial {@code indices}. The indices are used in place. + * + * @param indices Indices. + * @param n Number of indices. + * @return the interval + * @throws IllegalArgumentException if the indices are not unique and ordered; or not + * in the range {@code [0, 2^31-1)}; or {@code n <= 0} + */ + static ScanningKeyInterval of(int[] indices, int n) { + // Check the indices are uniquely ordered + if (n <= 0) { + throw new IllegalArgumentException("No indices to define the range"); + } + int p = indices[0]; + for (int i = 0; ++i < n;) { + final int c = indices[i]; + if (c <= p) { + throw new IllegalArgumentException("Indices are not unique and ordered"); + } + p = c; + } + if (indices[0] < 0) { + throw new IllegalArgumentException("Unsupported min value: " + indices[0]); + } + if (indices[n - 1] == Integer.MAX_VALUE) { + throw new IllegalArgumentException("Unsupported max value: " + Integer.MAX_VALUE); + } + return new ScanningKeyInterval(indices, n); + } + + @Override + public int left() { + return keys[0]; + } + + @Override + public int right() { + return keys[n - 1]; + } + + @Override + public int previousIndex(int k) { + return keys[previous(k)]; + } + + @Override + public int nextIndex(int k) { + return keys[next(k)]; + } + + @Override + public int split(int ka, int kb, int[] upper) { + int i = next(kb + 1); + upper[0] = keys[i]; + // Find the lower + do { + --i; + } while (keys[i] >= ka); + return keys[i]; + } + + /** + * Find the key index {@code i} of {@code keys[i] <= k}. + * + * @param k Target key. + * @return the key index + */ + private int previous(int k) { + // Scan the sorted keys from the end. + // Assume left <= k <= right thus no index checks required. + // IndexOutOfBoundsException indicates incorrect usage by the caller. + + // Attempt fast-forward + int i; + if (keys[i2] > k) { + i = keys[i1] > k ? i1 : i2; + } else { + i = keys[i3] > k ? i3 : n; + } + do { + --i; + } while (keys[i] > k); + return i; + } + + /** + * Find the key index {@code i} of {@code keys[i] >= k}. + * + * @param k Target key. + * @return the key index + */ + private int next(int k) { + // Scan the sorted keys from the start. + // Assume left <= k <= right thus no index checks required. + // IndexOutOfBoundsException indicates incorrect usage by the caller. + + // Attempt fast-forward + int i; + if (keys[i2] < k) { + i = keys[i3] < k ? i3 : i2; + } else { + i = keys[i1] < k ? i1 : -1; + } + do { + ++i; + } while (keys[i] < k); + return i; + } + + @Override + public int start() { + return 0; + } + + @Override + public int end() { + return n - 1; + } + + @Override + public int index(int i) { + return keys[i]; + } + + @Override + public int previous(int i, int k) { + // index(start) <= k < index(i) + int j = i; + do { + --j; + } while (keys[j] > k); + return j; + } + + @Override + public int next(int i, int k) { + // index(i) < k <= index(end) + int j = i; + do { + ++j; + } while (keys[j] < k); + return j; + } + + @Override + public int split(int lo, int hi, int ka, int kb, int[] upper) { + // index(lo) < ka <= kb < index(hi) + + // We could test if ka/kb is above or below the + // median (keys[lo] + keys[hi]) >>> 1 to pick the side to search + + int j = hi; + do { + --j; + } while (keys[j] > kb); + upper[0] = j + 1; + // Find the lower + while (keys[j] >= ka) { + --j; + } + return j; + } +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/ScanningPivotCache.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/ScanningPivotCache.java new file mode 100644 index 000000000..01cb3aa68 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/ScanningPivotCache.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * A cache of pivot indices used for partitioning an array into multiple regions. + * + *

This extends the {@link PivotCache} interface to add support to traverse + * a region between pivot and non-pivot indices. It is intended to be used to + * allow unsorted gaps between pivots to be targeted using a full sort when + * the partition function is configured to partition several regions [ka, kb]. + * + *

In the following example the partition algorithm must fully sort [k1, k2] + * and [k3, k4]. The first iteration detects pivots downstream from [k1, k2]. + * The second iteration can fill in the gaps when processing [k3, k4]: + * + *

+ * Partition:
+ * 0------k1---k2-------k3--------------k4-N
+ *
+ * Iteration 1:
+ * 0------ppppppp----p------p--p-------p---N
+ *
+ * Iteration 2:
+ *                   -------               Partition (p, k3, p)
+ *                           ss sssssss    Sort these regions
+ *                                      -- Partition (p, k4, N)
+ * 
+ * + * @since 1.2 + */ +interface ScanningPivotCache extends PivotCache { + /** + * Move the start (inclusive) of the range of indices supported. + * + *

Implementations may discard previously stored indices during this operation, for + * example all indices below {@code newLeft}. + * + *

This method can be used when partitioning keys {@code k1, k2, ...} in ascending + * order to indicate the next {@code k} that will be processed. The cache can optimise + * pivot storage to help partition downstream keys. + * + *

Note: If {@code newLeft < left} then the updated range is outside the current + * support. Implementations may choose to: move {@code left} so the new support is + * {@code [newLeft, right]}; return {@code false} to indicate the support was not changed; + * or to throw an exception (which should be documented). + * + *

Note: If {@code newLeft > right} then the updated range is outside the current + * support. Implementations may choose to: move {@code right} so the new support is + * {@code [newLeft, newleft]}; return {@code false} to indicate the support was not changed; + * or to throw an exception (which should be documented). + * + * @param newLeft Start of the supported range. + * @return true if the support was successfully modified + */ + boolean moveLeft(int newLeft); + + /** + * Returns the nearest non-pivot index that occurs on or after the specified starting + * index within the supported range. If no such + * indices exists then {@code right + n} is returned, where {@code n} is strictly positive. + * + *

If the starting index is less than the supported range {@code left} + * the result is undefined. + * + *

This method is intended to allow traversing the unsorted ranges between sorted + * pivot regions within the range {@code [left, right]}. + * + * @param k Index to start checking from (inclusive). + * @return the index of the next non-pivot, or {@code right + n} if there is no index + */ + int nextNonPivot(int k); + + /** + * Returns the nearest non-pivot index that occurs on or before the specified starting + * index within the supported range. If no such + * indices exists then {@code left - n} is returned, where {@code n} is strictly positive. + * + *

If the starting index is greater than the supported range {@code right} + * the result is undefined. + * + *

This method is intended to allow traversing the unsorted ranges between sorted + * pivot regions within the range {@code [left, right]}. + * + * @param k Index to start checking from (inclusive). + * @return the index of the next non-pivot, or {@code left - n} if there is no index + */ + int previousNonPivot(int k); +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/SearchableInterval.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/SearchableInterval.java new file mode 100644 index 000000000..541fac7c5 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/SearchableInterval.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * A searchable interval that contains indices used for partitioning an array into multiple regions. + * + *

The interval provides the following functionality: + * + *

    + *
  • Return the supported bounds of the search {@code [left <= right]}. + *
  • Return the previous index contained in the interval from a search point {@code k}. + *
  • Return the next index contained in the interval from a search point {@code k}. + *
+ * + *

Note that the interval provides the supported bounds. If a search begins outside + * the supported bounds the result is undefined. + * + *

Implementations may assume indices are positive. + * + * @see SearchableInterval2 + * @since 1.2 + */ +interface SearchableInterval { + /** + * The start (inclusive) of the range of indices supported. + * + * @return start of the supported range + */ + int left(); + + /** + * The end (inclusive) of the range of indices supported. + * + * @return end of the supported range + */ + int right(); + + /** + * Returns the nearest index that occurs on or before the specified starting + * index. + * + *

If {@code k < left} or {@code k > right} the result is undefined. + * + * @param k Index to start checking from (inclusive). + * @return the previous index + */ + int previousIndex(int k); + + /** + * Returns the nearest index that occurs on or after the specified starting + * index. + * + *

If {@code k < left} or {@code k > right} the result is undefined. + * + * @param k Index to start checking from (inclusive). + * @return the next index + */ + int nextIndex(int k); + + /** + * Split the interval using two splitting indices. Returns the nearest index that occurs + * before the specified split index {@code ka}, and the nearest index that occurs after the + * specified split index {@code kb}. + * + *

Note: Requires {@code left < ka <= kb < right}, i.e. there exists a valid interval + * above and below the split indices. + * + *

{@code
+     * l-----------ka-kb----------r
+     *   lower <--|
+     *                  |--> upper
+     *
+     * lower < ka
+     * upper > kb
+     * }
+ * + *

The default implementation uses: + * + *

{@code
+     * upper = nextIndex(kb + 1);
+     * lower = previousIndex(ka - 1);
+     * }
+ * + *

Implementations may override this method if both indices can be obtained together. + * + *

If {@code ka <= left} or {@code kb >= right} the result is undefined. + * + * @param ka Split index. + * @param kb Split index. + * @param upper Upper index. + * @return the lower index + */ + default int split(int ka, int kb, int[] upper) { + upper[0] = nextIndex(kb + 1); + return previousIndex(ka - 1); + } +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/SearchableInterval2.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/SearchableInterval2.java new file mode 100644 index 000000000..13ae9c3e2 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/SearchableInterval2.java @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * A searchable interval that contains indices used for partitioning an array into multiple regions. + * + *

The interval provides pointers to indices in the interval. These pointers are used + * to assist in searching the interval and can be used to access the index value. + * + *

The interval provides the following functionality: + * + *

    + *
  • Return the supported bounds of the search pointers {@code [start <= end]}. + *
  • Return a pointer {@code j} to the previous index contained in the interval from a search point {@code i}. + *
  • Return a pointer {@code j} to the next index contained in the interval from a search point {@code i}. + *
  • Split the interval around two indices given search points {@code i1} and {@code i2}. + *
+ * + *

Note that the interval provides the supported bounds. If a search begins outside + * the supported bounds the result is undefined. + * + *

Implementations may assume indices are positive. + * + *

This differs from {@link SearchableInterval} by providing pointers into the interval to + * assist the search. + * + * @see SearchableInterval + * @since 1.2 + */ +interface SearchableInterval2 { + /** + * Start pointer of the interval. + * + * @return the start pointer + */ + int start(); + + /** + * End pointer of the interval. + * + * @return the end pointer + */ + int end(); + + /** + * Return the index value {@code k} for the pointer. + * + *

If {@code i < start} or {@code i > end} the result is undefined. + * + * @param i Pointer. + * @return the index value of {@code i} + */ + int index(int i); + + /** + * Returns a pointer to the nearest index that occurs on or before the specified starting index. + * + *

Assumes {@code index(start) <= k < index(i)}. + * + * @param i Pointer. + * @param k Index to start checking from (inclusive). + * @return the previous pointer + */ + int previous(int i, int k); + + /** + * Returns a pointer to the nearest index that occurs on or after the specified starting + * index. + * + *

Assumes {@code index(i) < k <= index(end)}. + * + * @param i Pointer. + * @param k Index to start checking from (inclusive). + * @return the next index + */ + int next(int i, int k); + + /** + * Split the interval using two splitting indices. Returns a pointer to the the + * nearest index that occurs before the specified split index {@code ka}, and a + * pointer to the nearest index that occurs after the specified split index + * {@code kb}. + * + *

Requires {@code index(lo) < ka <= kb < index(hi)}, i.e. there exists a + * valid interval above and below the split indices. + * + *

{@code
+     * lo-----------ka-kb----------hi
+     *      lower <--|
+     *                 |--> upper
+     *
+     * index(lower) < ka
+     * index(upper) > kb
+     * }
+ * + *

The default implementation uses: + * + *

{@code
+     * upper = next(hi, kb + 1);
+     * lower = previous(lo, ka - 1);
+     * }
+ * + *

Implementations may override this method if both pointers can be obtained + * together. + * + *

If {@code lo < start} or {@code hi > end} the result is undefined. + * + * @param lo Lower pointer. + * @param hi Upper pointer. + * @param ka Split index. + * @param kb Split index. + * @param upper Upper pointer. + * @return the lower pointer + */ + default int split(int lo, int hi, int ka, int kb, int[] upper) { + upper[0] = next(hi, kb + 1); + return previous(lo, ka - 1); + } +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/SelectionPerformance.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/SelectionPerformance.java new file mode 100644 index 000000000..ade2484ea --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/SelectionPerformance.java @@ -0,0 +1,3198 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import org.apache.commons.numbers.arrays.Selection; +import org.apache.commons.rng.UniformRandomProvider; +import org.apache.commons.rng.sampling.PermutationSampler; +import org.apache.commons.rng.sampling.distribution.DiscreteUniformSampler; +import org.apache.commons.rng.sampling.distribution.SharedStateDiscreteSampler; +import org.apache.commons.rng.simple.RandomSource; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** + * Executes a benchmark of the selection of indices from {@code double} array data. + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@State(Scope.Benchmark) +@Fork(value = 1, jvmArgs = {"-server", "-Xms512M", "-Xmx8192M"}) +public class SelectionPerformance { + /** Use the JDK sort function. */ + private static final String JDK = "JDK"; + /** Use a sort function. */ + private static final String SORT = "Sort"; + /** Baseline for the benchmark. */ + private static final String BASELINE = "Baseline"; + /** Selection method using a heap. */ + private static final String HEAP_SELECT = "HeapSelect"; + /** Selection method using a sort. */ + private static final String SORT_SELECT = "SortSelect"; + + // First generation partition functions. + // These are based on the KthSelector class used in Commons Math: + // - Single k or a pair of indices (k,k+1) are selected in a single + // call; multiple indices cache pivots in a heap structure or use a BitSet. + // - They dynamically correct signed zeros when they are encountered. + + /** Single-pivot partitioning. This method uses a special comparison of double + * values similar to {@link Double#compare(double, double)}. This handles + * NaN and signed zeros. */ + private static final String SP = "SP"; + /** Single-pivot partitioning; uses a BitSet to cache pivots. */ + private static final String SPN = "SPN"; + /** Single-pivot partitioning using a heap to cache pivots. + * This method is copied from Commons Math. */ + private static final String SPH = "SPH"; + /** Bentley-McIlroy partitioning (Sedgewick); uses a BitSet to cache pivots. */ + private static final String SBM = "SBM"; + /** Bentley-McIlroy partitioning (original); uses a BitSet to cache pivots. */ + private static final String BM = "BM"; + /** Dual-pivot partitioning; uses a BitSet to cache pivots. */ + private static final String DP = "DP"; + /** Dual-pivot partitioning with 5 sorted points to choose pivots; uses a BitSet to cache pivots. */ + private static final String DP5 = "5DP"; + + // Second generation partition functions. + // These pre-process data to sort NaN to the end and count signed zeros; + // post-processing is performed to restore signed zeros in order. + // The exception is SBM2 which dynamically corrects signed zeros. + + /** Bentley-McIlroy partitioning (Sedgewick). This second generation function + * dynamically corrects signed zeros when they are encountered. It is based on + * the fastest first generation method with changes to allow different pivot + * store strategies: SEQUENTIAL, INDEX_SET, PIVOT_CACHE. */ + private static final String SBM2 = "2SBM"; + + /** Floyd-Rivest partitioning. Only for single k. */ + private static final String FR = "FR"; + /** Floyd-Rivest partitioning (Kiwiel). Only for a single k. */ + private static final String KFR = "KFR"; + + // Introselect functions - switch to a stopper function when progress is poor. + // Allow specification of configuration using the name parameter: + // single-pivot strategy (Partition.SPStrategy); + // multiple key strategy (Partition.KeyStrategy); + // paired (close) keys strategy (Partition.PairedKeyStrategy); + // edge-selection strategy (Partition.EdgeSelectStrategy); + // stopper strategy (Partition.StopperStrategy). + // Parameters to control strategies and introspection are set using the name parameter. + // See PartitionFactory for details. + + /** Introselect implementation with single-pivot partitioning. */ + private static final String ISP = "ISP"; + /** Introselect implementation with dual-pivot partitioning. */ + private static final String IDP = "IDP"; + + // Single k selection using various methods which provide linear runtime (Order(n)). + + /** Linearselect implementation with single pivot partitioning using median-of-medians-of-5 + * for pivot selection. */ + private static final String LSP = "LSP"; + /** Linearselect implementation with single pivot partitioning using optimised + * median-of-medians. */ + private static final String LINEAR = "Linear"; + /** Quickselect adaptive implementation. Has configuration of the far-step method and some + * adaption modes. */ + private static final String QA = "QA"; + /** Quickselect adaptive implementation. Uses the best performing far-step method and + * has configurable adaption control allowing starting at and skipping over adaption modes. */ + private static final String QA2 = "QA2"; + + /** Commons Numbers select implementation. This method is built using the best performing + * select function across a range of input data. This algorithm cannot be configured. */ + private static final String SELECT = "SELECT"; + + /** Random source. */ + private static final RandomSource RANDOM_SOURCE = RandomSource.XO_RO_SHI_RO_128_PP; + + /** + * Source of {@code double} array data. + * + *

By default this uses the adverse input test suite from figure 1 in Bentley and McIlroy + * (1993) Engineering a sort function, Software, practice and experience, Vol.23(11), + * 1249–1265. + * + *

An alternative set of data is from Valois (2000) Introspective sorting and selection + * revisited, Software, practice and experience, Vol.30(6), 617-638. + * + *

Note + * + *

This class has setter methods to allow re-use in unit testing without requiring + * use of reflection to set fields. Parameters set by JMH are initialized to their + * defaults for convenience. Re-use requires: + * + *

    + *
  1. Creating an instance of the abstract class that provides the data length + *
  2. Calling {@link #setup()} to create the data + *
  3. Iterating over the data + *
+ * + *
+     * AbstractDataSource s = new AbstractDataSource() {
+     *     protected int getLength() {
+     *         return 123;
+     *     }
+     * };
+     * s.setDistribution(Distribution.SAWTOOTH, Distribution.SHUFFLE);
+     * s.setModification(Modification.REVERSE_FRONT);
+     * s.setRange(2);
+     * s.setup();
+     * for (int i = 0; i < s.size(); i++) {
+     *     s.getData(i);
+     * }
+     * 
+ * + *

Random distribution mode + * + *

The default BM configuration includes random samples generated as a family of + * single samples created from ranges that are powers of two [0, 2^i). This small set + * of samples is only a small representation of randomness. For small lengths this may + * only be a few random samples. + * + *

The data source can be changed to generate a fixed number of random samples + * using a uniform distribution [0, n]. For this purpose the distribution must be set + * to {@link Distribution#RANDOM} and the {@link #setSamples(int) samples} set above + * zero. The inclusive upper bound {@code n} is set using the {@link #setSeed(int) seed}. + * If this is zero then the default is {@link Integer#MAX_VALUE}. + * + *

Order + * + *

Data are created in distribution families. If these are passed in order to a + * partition method the JVM can change behaviour of the algorithm as branch prediction + * statistics stabilise for the family. To mitigate this effect the order is permuted + * per invocation of the benchmark (see {@link #createOrder()}. This stabilises the + * average timing results from JMH. Using per-invocation data generation requires + * the benchmark execution time is higher than 1 millisecond. Benchmarks that use + * tiny data (e.g. sort 5 elements) must use several million samples. + */ + @State(Scope.Benchmark) + public abstract static class AbstractDataSource { + /** All distributions / modifications. */ + private static final String ALL = "all"; + /** All distributions / modifications in the Bentley and McIlroy test suite. */ + private static final String BM = "bm"; + /** All distributions in the Valois test suite. These currently ignore the seed. + * To replicate Valois used a fixed seed and the copy modification. */ + private static final String VALOIS = "valois"; + /** Flag to determine if the data size should be logged. This is useful to be + * able to determine the execution time per sample when the number of samples + * is dynamically created based on the data length, range and seed. */ + private static final AtomicInteger LOG_SIZE = new AtomicInteger(); + + /** + * The type of distribution. + */ + enum Distribution { + // B&M (1993) + + /** Sawtooth distribution. Ascending data from 0 to m, that repeats. */ + SAWTOOTH, + /** Random distribution. Uniform random data in [0, m] */ + RANDOM, + /** Stagger distribution. Multiple interlaced ascending sequences. */ + STAGGER, + /** Plateau distribution. Ascending data from 0 to m, then constant. */ + PLATEAU, + /** Shuffle distribution. Two randomly interlaced ascending sequences of different lengths. */ + SHUFFLE, + + /** Sharktooth distribution. Alternating ascending then descending data from 0 + * to m and back. This is an addition to the original suite of BM + * and is not included in the test suite by default and must be specified. + * + *

An ascending then descending sequence is also known as organpipe in + * Valois (2000). This version allows multiple ascending/descending runs in the + * same length. */ + SHARKTOOTH, + + // Valois (2000) + + /** Sorted. */ + SORTED, + /** Permutation of ones and zeros. */ + ONEZERO, + /** Musser's median-of-3 killer. This elicits worst case performance for a median-of-3 + * pivot selection strategy. */ + M3KILLER, + /** A sorted sequence rotated left once. */ + ROTATED, + /** Musser's two-faced sequence (the median-of-3 killer with two random permutations). */ + TWOFACED, + /** An ascending then descending sequence. */ + ORGANPIPE; + } + + /** + * The type of data modification. + */ + enum Modification { + /** Copy modification. */ + COPY, + /** Reverse modification. */ + REVERSE, + /** Reverse front-half modification. */ + REVERSE_FRONT, + /** Reverse back-half modification. */ + REVERSE_BACK, + /** Sort modification. */ + SORT, + /** Descending modification (this is an addition to the original suite of BM). + * It is useful for testing worst case performance, e.g. insertion sort performs + * poorly on descending data. Heapselect using a max heap (to find k minimum elements) + * would perform poorly if data is processed in the forward direction as all elements + * must be inserted. + * + *

This is not included in the test suite by default and must be specified. + * Note that the Shuffle distribution with a very large seed 'm' is effectively an + * ascending sequence and will be reversed to descending as part of the original + * B&M suite of data. */ + DESCENDING, + /** Dither modification. Add i % 5 to the data element i. */ + DITHER; + } + + /** + * Sample information. Used to obtain information about samples that may be slow + * for a particular partition method, e.g. they use excessive recursion during quickselect. + * This is used for testing: each sample from the data source can provide the + * information to create the sample distribution. + */ + public static final class SampleInfo { + /** Distribution. */ + private final Distribution dist; + /** Modification. */ + private final Modification mod; + /** Length. */ + private final int n; + /** Seed. */ + private final int m; + /** Offset. */ + private final int o; + + /** + * @param dist Distribution. + * @param mod Modification. + * @param n Length. + * @param m Seed. + * @param o Offset. + */ + SampleInfo(Distribution dist, Modification mod, int n, int m, int o) { + this.dist = dist; + this.mod = mod; + this.n = n; + this.m = m; + this.o = o; + } + + /** + * Create an instance with the specified distribution. + * + * @param v Value. + * @return the instance + */ + SampleInfo with(Distribution v) { + return new SampleInfo(v, mod, n, m, o); + } + + /** + * Create an instance with the specified modification. + * + * @param v Value. + * @return the instance + */ + SampleInfo with(Modification v) { + return new SampleInfo(dist, v, n, m, o); + } + + /** + * @return the distribution + */ + Distribution getDistribution() { + return dist; + } + + /** + * @return the modification + */ + Modification getModification() { + return mod; + } + + /** + * @return the data length + */ + int getN() { + return n; + } + + /** + * @return the distribution seed + */ + int getM() { + return m; + } + + /** + * @return the distribution offset + */ + int getO() { + return o; + } + + @Override + public String toString() { + return String.format("%s, %s, n=%d, m=%d, o=%d", dist, mod, n, m, o); + } + } + + /** Order. This is randomized to ensure that successive calls do not partition + * similar distributions. Randomized per invocation to avoid the JVM 'learning' + * branch decisions on small data sets. */ + protected int[] order; + /** Cached source of randomness. */ + protected UniformRandomProvider rng; + + /** Type of data. Multiple types can be specified in the same string using + * lower/upper case, delimited using ':'. */ + @Param({BM}) + private String distribution = BM; + + /** Type of data modification. Multiple types can be specified in the same string using + * lower/upper case, delimited using ':'. */ + @Param({BM}) + private String modification = BM; + + /** Extra range to add to the data length. + * E.g. Use 1 to force use of odd and even length samples. */ + @Param({"1"}) + private int range = 1; + + /** Sample 'seed'. This is {@code m} in Bentley and McIlroy's test suite. + * If set to zero the default is to use powers of 2 based on sample size. */ + @Param({"0"}) + private int seed; + + /** Sample offset. This is used to shift each distribution to create different data. + * It is advanced on each invocation of {@link #setup()}. */ + @Param({"0"}) + private int offset; + + /** Number of samples. Applies only to the random distribution. In this case + * the length of the data is randomly chosen in {@code [length, length + range)}. */ + @Param({"0"}) + private int samples; + + /** RNG seed. Created using ThreadLocalRandom.current().nextLong(). This is advanced + * for the random distribution mode per iteration. Each benchmark executed by + * JMH will use the same random data, even across JVMs. + * + *

If this is zero then a random seed is chosen. */ + @Param({"-7450238124206088695"}) + private long rngSeed = -7450238124206088695L; + + /** Data. This is stored as integer data which saves memory. Note that when ranking + * data it is not necessary to have the full range of the double data type; the same + * number of unique values can be recorded in an array using an integer type. + * Returning a double[] forces a copy to be generated for destructive sorting / + * partitioning methods. */ + private int[][] data; + + /** Sample information. */ + private List sampleInfo; + + /** + * Gets the sample for the given {@code index}. + * + *

This is returned in a randomized order per iteration. + * + * @param index Index. + * @return the data sample + */ + public double[] getData(int index) { + return getDataSample(order[index]); + } + + /** + * Gets the sample for the given {@code index}. + * + * @param index Index. + * @return the data sample + */ + protected double[] getDataSample(int index) { + final int[] a = data[index]; + final double[] x = new double[a.length]; + for (int i = -1; ++i < a.length;) { + x[i] = a[i]; + } + return x; + } + + /** + * Gets the sample size for the given {@code index}. + * + * @param index Index. + * @return the data sample size + */ + public int getDataSize(int index) { + return data[index].length; + } + + /** + * Gets the sample information for the given {@code index}. + * Matches the (native) order returned by {@link #getDataSample(int)}. + * + * @param index Index. + * @return the data sample information + */ + SampleInfo getDataSampleInfo(int index) { + return sampleInfo.get(index); + } + + /** + * Get the number of data samples. + * + *

Note: This data source will create a permutation order per invocation based on + * this size. Per-invocation control in JMH is recommended for methods that take + * more than 1 millisecond to execute. For very small data and/or fast methods + * this may not be achievable. Child classes may override this value to create + * a large number of repeats of the same data per invocation. Any class performing + * this should also override {@link #getData(int)} to prevent index out of bound errors. + * This can be done by mapping the index to the original index using the number of repeats + * e.g. {@code original index = index / repeats}. + * + * @return the number of samples + */ + public int size() { + return data.length; + } + + /** + * Create the data. + */ + @Setup(Level.Iteration) + public void setup() { + Objects.requireNonNull(distribution); + Objects.requireNonNull(modification); + + // Set-up using parameters (may throw) + final EnumSet dist = getDistributions(); + final int length = getLength(); + if (length < 1) { + throw new IllegalStateException("Unsupported length: " + length); + } + // Note: Bentley-McIlroy use n in {100, 1023, 1024, 1025}. + // Here we only support a continuous range. + final int r = range > 0 ? range : 0; + if (length + (long) r > Integer.MAX_VALUE) { + throw new IllegalStateException("Unsupported upper length: " + length); + } + final int length2 = length + r; + + // Allow pseudorandom seeding + if (rngSeed == 0) { + rngSeed = RandomSource.createLong(); + } + if (rng == null) { + // First call, create objects + rng = RANDOM_SOURCE.create(rngSeed); + } + + // Special case for random distribution mode + if (dist.contains(Distribution.RANDOM) && dist.size() == 1 && samples > 0) { + data = new int[samples][]; + sampleInfo = new ArrayList<>(samples); + final int upper = seed > 0 ? seed : Integer.MAX_VALUE; + final SharedStateDiscreteSampler s1 = DiscreteUniformSampler.of(rng, 0, upper); + final SharedStateDiscreteSampler s2 = DiscreteUniformSampler.of(rng, length, length2); + for (int i = 0; i < data.length; i++) { + final int[] a = new int[s2.sample()]; + for (int j = a.length; --j >= 0;) { + a[j] = s1.sample(); + } + data[i] = a; + sampleInfo.add(new SampleInfo(Distribution.RANDOM, Modification.COPY, a.length, 0, 0)); + } + return; + } + + // New data per iteration + data = null; + final int o = offset; + offset = rng.nextInt(); + + final EnumSet mod = getModifications(); + + // Data using the RNG will be randomized only once. + // Here we use the same seed for parity across benchmark methods. + // Note that most distributions do not use the source of randomness. + final ArrayList sampleData = new ArrayList<>(); + sampleInfo = new ArrayList<>(); + final List info = new ArrayList<>(); + for (int n = length; n <= length2; n++) { + // Note: Large lengths may wish to limit the range of m to limit + // the memory required to store the samples. Currently a single + // m is supported via the seed parameter. + // Default seed will create ceil(log2(2*n)) * 5 dist * 6 mods samples: + // MAX = 32 * 5 * 7 * (2^31-1) * 4 bytes == 7679 GiB + // HUGE = 31 * 5 * 7 * 2^30 * 4 bytes == 3719 GiB + // BIG = 21 * 5 * 7 * 2^20 * 4 bytes == 2519 MiB <-- within configured JVM -Xmx + // MED = 11 * 5 * 7 * 2^10 * 4 bytes == 1318 KiB + // (This is for the B&M data.) + // It is possible to create lengths above 2^30 using a single distribution, + // modification, and seed: + // MAX1 = 1 * 1 * 1 * (2^31-1) * 4 bytes == 8191 MiB + // However this is then used to create double[] data thus requiring an extra + // ~16GiB memory for the sample to partition. + for (final int m : createSeeds(seed, n)) { + final List d = createDistributions(dist, rng, n, m, o, info); + for (int i = 0; i < d.size(); i++) { + final int[] x = d.get(i); + final SampleInfo si = info.get(i); + if (mod.contains(Modification.COPY)) { + // Don't copy! All other methods generate copies + // so we can use this in-place. + sampleData.add(x); + sampleInfo.add(si.with(Modification.COPY)); + } + if (mod.contains(Modification.REVERSE)) { + sampleData.add(reverse(x, 0, n)); + sampleInfo.add(si.with(Modification.REVERSE)); + } + if (mod.contains(Modification.REVERSE_FRONT)) { + sampleData.add(reverse(x, 0, n >>> 1)); + sampleInfo.add(si.with(Modification.REVERSE_FRONT)); + } + if (mod.contains(Modification.REVERSE_BACK)) { + sampleData.add(reverse(x, n >>> 1, n)); + sampleInfo.add(si.with(Modification.REVERSE_BACK)); + } + // Only sort once + if (mod.contains(Modification.SORT) || + mod.contains(Modification.DESCENDING)) { + final int[] y = x.clone(); + Arrays.sort(y); + if (mod.contains(Modification.DESCENDING)) { + sampleData.add(reverse(y, 0, n)); + sampleInfo.add(si.with(Modification.DESCENDING)); + } + if (mod.contains(Modification.SORT)) { + sampleData.add(y); + sampleInfo.add(si.with(Modification.SORT)); + } + } + if (mod.contains(Modification.DITHER)) { + sampleData.add(dither(x)); + sampleInfo.add(si.with(Modification.DITHER)); + } + } + } + } + data = sampleData.toArray(new int[0][]); + if (LOG_SIZE.getAndSet(length) != length) { + Logger.getLogger(getClass().getName()).info( + () -> String.format("Data length: [%d, %d] n=%d", length, length2, data.length)); + } + } + + /** + * Create the order to process the indices. + * + *

JMH recommends that invocations should take at + * least 1 millisecond for timings to be usable. In practice there should be + * enough data that processing takes much longer than a few milliseconds. + */ + @Setup(Level.Invocation) + public void createOrder() { + if (order == null) { + // First call, create objects + order = PermutationSampler.natural(size()); + } + PermutationSampler.shuffle(rng, order); + } + + /** + * @return the distributions + */ + private EnumSet getDistributions() { + EnumSet dist; + if (BM.equals(distribution)) { + dist = EnumSet.of( + Distribution.SAWTOOTH, + Distribution.RANDOM, + Distribution.STAGGER, + Distribution.PLATEAU, + Distribution.SHUFFLE); + } else if (VALOIS.equals(distribution)) { + dist = EnumSet.of( + Distribution.RANDOM, + Distribution.SORTED, + Distribution.ONEZERO, + Distribution.M3KILLER, + Distribution.ROTATED, + Distribution.TWOFACED, + Distribution.ORGANPIPE); + } else { + dist = getEnumFromParam(Distribution.class, distribution); + } + return dist; + } + + /** + * @return the modifications + */ + private EnumSet getModifications() { + EnumSet mod; + if (BM.equals(modification)) { + // Modifications are from Bentley and McIlroy + mod = EnumSet.allOf(Modification.class); + // ... except descending + mod.remove(Modification.DESCENDING); + } else if (VALOIS.equals(modification)) { + // For convenience alias Valois to copy + mod = EnumSet.of(Modification.COPY); + } else { + mod = getEnumFromParam(Modification.class, modification); + } + return mod; + } + + /** + * Gets all the enum values of the given class from the parameters. + * + * @param Enum type. + * @param cls Class of the enum. + * @param parameters Parameters (multiple values delimited by ':') + * @return the enum values + */ + static > EnumSet getEnumFromParam(Class cls, String parameters) { + if (ALL.equals(parameters)) { + return EnumSet.allOf(cls); + } + final EnumSet set = EnumSet.noneOf(cls); + final String s = parameters.toUpperCase(Locale.ROOT); + for (final E e : cls.getEnumConstants()) { + // Scan for the name + for (int i = s.indexOf(e.name(), 0); i >= 0; i = s.indexOf(e.name(), i)) { + // Ensure a full match to the name: + // either at the end of the string, or followed by the delimiter + i += e.name().length(); + if (i == s.length() || s.charAt(i) == ':') { + set.add(e); + break; + } + } + } + if (set.isEmpty()) { + throw new IllegalStateException("Unknown parameters: " + parameters); + } + return set; + } + + /** + * Creates the seeds. + * + *

This can be pasted into a JShell terminal to verify it works for any size + * {@code 1 <= n < 2^31}. With the default behaviour all seeds {@code m} are unsigned + * strictly positive powers of 2 and the highest seed should be below {@code 2*n}. + * + * @param seed Seed (use 0 for default; or provide a strictly positive {@code 1 <= m <= 2^31}). + * @param n Sample size. + * @return the seeds + */ + private static int[] createSeeds(int seed, int n) { + // Allow [1, 2^31] (note 2^31 is negative but handled as a power of 2) + if (seed - 1 >= 0) { + return new int[] {seed}; + } + // Bentley-McIlroy use: + // for: m = 1; m < 2 * n; m *= 2 + // This has been modified here to handle n up to MAX_VALUE + // by knowing the count of m to generate as the power of 2 >= n. + + // ceil(log2(n)) + 1 == ceil(log2(2*n)) but handles MAX_VALUE + int c = 33 - Integer.numberOfLeadingZeros(n - 1); + final int[] seeds = new int[c]; + c = 0; + for (int m = 1; c != seeds.length; m *= 2) { + seeds[c++] = m; + } + return seeds; + } + + /** + * Creates the distribution samples. Handles {@code m = 2^31} using + * {@link Integer#MIN_VALUE}. + * + *

The offset is used to adjust each distribution to generate a different + * output. Only applies to distributions that do not use the source of randomness. + * + *

Distributions are generated in enum order and recorded in the output {@code info}. + * Distributions that are a constant value at {@code m == 1} are not generated. + * This case is handled by the plateau distribution which will be a constant value + * except one occurrence of zero. + * + * @param dist Distributions. + * @param rng Source of randomness. + * @param n Length of the sample. + * @param m Sample seed (in [1, 2^31]) + * @param o Offset. + * @param info Sample information. + * @return the samples + */ + private static List createDistributions(EnumSet dist, + UniformRandomProvider rng, int n, int m, int o, List info) { + final ArrayList distData = new ArrayList<>(6); + int[] x; + info.clear(); + SampleInfo si = new SampleInfo(null, null, n, m, o); + // B&M (1993) + if (dist.contains(Distribution.SAWTOOTH) && m != 1) { + x = createSample(distData, info, si.with(Distribution.SAWTOOTH)); + // i % m + // Typical case m is a power of 2 so we can use a mask + // Use the offset. + final int mask = m - 1; + if ((m & mask) == 0) { + for (int i = -1; ++i < n;) { + x[i] = (i + o) & mask; + } + } else { + // User input seed. Start at the offset. + int j = Integer.remainderUnsigned(o, m); + for (int i = -1; ++i < n;) { + j = j % m; + x[i] = j++; + } + } + } + if (dist.contains(Distribution.RANDOM) && m != 1) { + x = createSample(distData, info, si.with(Distribution.RANDOM)); + // rand() % m + // A sampler is faster than rng.nextInt(m); the sampler has an inclusive upper. + final SharedStateDiscreteSampler s = DiscreteUniformSampler.of(rng, 0, m - 1); + for (int i = -1; ++i < n;) { + x[i] = s.sample(); + } + } + if (dist.contains(Distribution.STAGGER)) { + x = createSample(distData, info, si.with(Distribution.STAGGER)); + // Overflow safe: (i * m + i) % n + final long nn = n; + final long oo = Integer.toUnsignedLong(o); + for (int i = -1; ++i < n;) { + final long j = i + oo; + x[i] = (int) ((j * m + j) % nn); + } + } + if (dist.contains(Distribution.PLATEAU)) { + x = createSample(distData, info, si.with(Distribution.PLATEAU)); + // min(i, m) + for (int i = Math.min(n, m); --i >= 0;) { + x[i] = i; + } + for (int i = m - 1; ++i < n;) { + x[i] = m; + } + // Rotate + final int n1 = Integer.remainderUnsigned(o, n); + if (n1 != 0) { + final int[] a = x.clone(); + final int n2 = n - n1; + System.arraycopy(a, 0, x, n1, n2); + System.arraycopy(a, n2, x, 0, n1); + } + } + if (dist.contains(Distribution.SHUFFLE) && m != 1) { + x = createSample(distData, info, si.with(Distribution.SHUFFLE)); + // rand() % m ? (j += 2) : (k += 2) + final SharedStateDiscreteSampler s = DiscreteUniformSampler.of(rng, 0, m - 1); + for (int i = -1, j = 0, k = 1; ++i < n;) { + x[i] = s.sample() != 0 ? (j += 2) : (k += 2); + } + } + // Extra - based on organpipe with a variable ascending/descending length + if (dist.contains(Distribution.SHARKTOOTH) && m != 1) { + x = createSample(distData, info, si.with(Distribution.SHARKTOOTH)); + // ascending-descending runs + int i = -1; + int j = (o & Integer.MAX_VALUE) % m - 1; + OUTER: + for (;;) { + while (++j < m) { + if (++i == n) { + break OUTER; + } + x[i] = j; + } + while (--j >= 0) { + if (++i == n) { + break OUTER; + } + x[i] = j; + } + } + } + // Valois (2000) + if (dist.contains(Distribution.SORTED)) { + x = createSample(distData, info, si.with(Distribution.SORTED)); + for (int i = -1; ++i < n;) { + x[i] = i; + } + } + if (dist.contains(Distribution.ONEZERO)) { + x = createSample(distData, info, si.with(Distribution.ONEZERO)); + // permutation of floor(n/2) ones and ceil(n/2) zeroes. + // For convenience this uses random ones and zeros to avoid a shuffle + // and simply reads bits from integers. The distribution will not + // be exactly 50:50. + final int end = n & ~31; + for (int i = 0; i < end; i += 32) { + int z = rng.nextInt(); + for (int j = -1; ++j < 32;) { + x[i + j] = z & 1; + z >>>= 1; + } + } + for (int i = end; ++i < n;) { + x[i] = rng.nextBoolean() ? 1 : 0; + } + } + if (dist.contains(Distribution.M3KILLER)) { + x = createSample(distData, info, si.with(Distribution.M3KILLER)); + medianOf3Killer(x); + } + if (dist.contains(Distribution.ROTATED)) { + x = createSample(distData, info, si.with(Distribution.ROTATED)); + // sorted sequence rotated left once + // 1, 2, 3, ..., n-1, 0 + for (int i = 1; i < n; i++) { + x[i - 1] = i; + } + } + if (dist.contains(Distribution.TWOFACED)) { + x = createSample(distData, info, si.with(Distribution.TWOFACED)); + // Musser's two faced randomly permutes a median-of-3 killer in + // 4 floor(log2(n)) through n/2 and n/2 + 4 floor(log2(n)) through n + medianOf3Killer(x); + final int j = 4 * (31 - Integer.numberOfLeadingZeros(n)); + final int n2 = n >>> 1; + shuffle(rng, x, j, n2); + shuffle(rng, x, n2 + j, n); + } + if (dist.contains(Distribution.ORGANPIPE)) { + x = createSample(distData, info, si.with(Distribution.ORGANPIPE)); + // 0, 1, 2, 3, ..., 3, 2, 1, 0 + // n should be even to leave two equal values in the middle, otherwise a single + for (int i = -1, j = n; ++i <= --j;) { + x[i] = i; + x[j] = i; + } + } + return distData; + } + + /** + * Create the sample array and add it to the {@code data}; add the information to the {@code info}. + * + * @param data Data samples. + * @param info Sample information. + * @param s Sample information. + * @return the new sample array + */ + private static int[] createSample(ArrayList data, List info, + SampleInfo s) { + final int[] x = new int[s.getN()]; + data.add(x); + info.add(s); + return x; + } + + /** + * Create Musser's median-of-3 killer sequence (in-place). + * + * @param x Data. + */ + private static void medianOf3Killer(int[] x) { + // This uses the original K_2k sequence from Musser (1997) + // Introspective sorting and selection algorithms, + // Software—Practice and Experience, 27(8), 983–993. + // A true median-of-3 killer requires n to be an even integer divisible by 4, + // i.e. k is an even positive integer. This causes a median-of-3 partition + // strategy to produce a sequence of n/4 partitions into sub-sequences of + // length 2 and n-2, 2 and n-4, ..., 2 and n/2. + // 1 2 3 4 5 k-2 k-1 k k+1 k+2 k+3 2k-1 2k + // 1, k+1, 3, k+3, 5, ..., 2k-3, k-1 2k-1 2 4 6 ... 2k-2 2k + final int n = x.length; + final int k = n >>> 1; + for (int i = 0; i < k; i++) { + x[i] = ++i; + x[i] = k + i; + } + for (int i = k - 1, j = 2; ++i < n; j += 2) { + x[i] = j; + } + } + + /** + * Return a (part) reversed copy of the data. + * + * @param x Data. + * @param from Start index to reverse (inclusive). + * @param to End index to reverse (exclusive). + * @return the copy + */ + private static int[] reverse(int[] x, int from, int to) { + final int[] a = x.clone(); + for (int i = from - 1, j = to; ++i < --j;) { + final int v = a[i]; + a[i] = a[j]; + a[j] = v; + } + return a; + } + + /** + * Return a dithered copy of the data. + * + * @param x Data. + * @return the copy + */ + private static int[] dither(int[] x) { + final int[] a = x.clone(); + for (int i = a.length; --i >= 0;) { + // Bentley-McIlroy use i % 5. + // It is important this is not a power of 2 so it will not coincide + // with patterns created in the data using the default m powers-of-2. + a[i] += i % 5; + } + return a; + } + + /** + * Shuffles the entries of the given array. + * + * @param rng Source of randomness. + * @param array Array whose entries will be shuffled (in-place). + * @param from Lower-bound (inclusive) of the sub-range. + * @param to Upper-bound (exclusive) of the sub-range. + */ + private static void shuffle(UniformRandomProvider rng, int[] array, int from, int to) { + final int length = to - from; + for (int i = length; i > 1; i--) { + swap(array, from + i - 1, from + rng.nextInt(i)); + } + } + + /** + * Swaps the two specified elements in the array. + * + * @param array Array. + * @param i First index. + * @param j Second index. + */ + private static void swap(int[] array, int i, int j) { + final int tmp = array[i]; + array[i] = array[j]; + array[j] = tmp; + } + + /** + * Gets the minimum length of the data. + * The actual length is enumerated in {@code [length, length + range]}. + * + * @return the length + */ + protected abstract int getLength(); + + /** + * Gets the range. + * + * @return the range + */ + final int getRange() { + return range; + } + + /** + * Sets the distribution(s) of the data. + * If the input is an empty array or the first enum value is null, + * then all distributions are used. + * + * @param v Values. + */ + void setDistribution(Distribution... v) { + if (v.length == 0 || v[0] == null) { + distribution = ALL; + } else { + final EnumSet s = EnumSet.of(v[0], v); + distribution = s.stream().map(Enum::name).collect(Collectors.joining(":")); + } + } + + /** + * Sets the modification of the data. + * If the input is an empty array or the first enum value is null, + * then all distributions are used. + * + * @param v Value. + */ + void setModification(Modification... v) { + if (v.length == 0 || v[0] == null) { + modification = ALL; + } else { + final EnumSet s = EnumSet.of(v[0], v); + modification = s.stream().map(Enum::name).collect(Collectors.joining(":")); + } + } + + /** + * Sets the maximum addition to extend the length of each sample of data. + * The actual length is enumerated in {@code [length, length + range]}. + * + * @param v Value. + */ + void setRange(int v) { + range = v; + } + + /** + * Sets the sample 'seed' used to generate distributions. + * If set to zero the default is to use powers of 2 based on sample size. + * + *

Supports positive values and the edge case of {@link Integer#MIN_VALUE} + * which is treated as an unsigned power of 2. + * + * @param v Value (ignored if not within {@code [1, 2^31]}). + */ + void setSeed(int v) { + seed = v; + } + + /** + * Sets the sample 'offset' used to generate distributions. Advanced to a new + * random integer on each invocation of {@link #setup()}. + * + * @param v Value. + */ + void setOffset(int v) { + offset = v; + } + + /** + * Sets the number of samples to use for the random distribution mode. + * See {@link AbstractDataSource} for details. + * + * @param v Value. + */ + void setSamples(int v) { + samples = v; + } + + /** + * Sets the seed for the random number generator. + * + * @param v Value. + */ + void setRngSeed(long v) { + this.rngSeed = v; + } + } + + /** + * Source of {@code double} array data to sort. + */ + @State(Scope.Benchmark) + public static class SortSource extends AbstractDataSource { + /** Data length. */ + @Param({"1023"}) + private int length; + /** Number of repeats. This is used to control the number of times the data is processed + * per invocation. Note that each invocation randomises the order. For very small data + * and/or fast methods there may not be enough data to achieve the target of 1 + * millisecond per invocation. Use this value to increase the length of each invocation. + * For example the insertion sort on tiny data, or the sort5 methods, may require this + * to be 1,000,000 or higher. */ + @Param({"1"}) + private int repeats; + + /** {@inheritDoc} */ + @Override + protected int getLength() { + return length; + } + + /** {@inheritDoc} */ + @Override + public int size() { + return super.size() * repeats; + } + + /** {@inheritDoc} */ + @Override + public double[] getData(int index) { + // order = (data index) * repeats + repeat + // data index = order / repeats + return super.getDataSample(order[index] / repeats); + } + } + + /** + * Source of k-th indices to partition. + * + *

This class provides both data to partition and the indices to partition. + * The indices and data are created per iteration. The order to process them + * is created per invocation. + */ + @State(Scope.Benchmark) + public static class KSource extends AbstractDataSource { + /** Data length. */ + @Param({"1023"}) + private int length; + /** Number of indices to select. */ + @Param({"1", "2", "3", "5", "10"}) + private int k; + /** Number of repeats. */ + @Param({"10"}) + private int repeats; + /** Distribution mode. K indices can be distributed randomly or uniformly. + *

    + *
  • "random": distribute k indices randomly + *
  • "uniform": distribute k indices uniformly but with a random start point + *
  • "index": Use a single index at k + *
  • "single": Use a single index at k uniformly spaced points. This mode + * first generates the spacing for the indices. Then samples from that spacing + * using the configured repeats. Common usage of k=10 will have 10 samples with a + * single index, each in a different position. + *
+ *

If the mode ends with a "s" then the indices are sorted. For example "randoms" + * will sort the random indices. + */ + @Param({"random"}) + private String mode; + /** Separation. K can be single indices (s=0) or paired (s!=0). Paired indices are + * separated using the specified separation. When running in paired mode the + * number of k is doubled and duplicates may occur. This method is used for + * testing sparse or uniform distributions of paired indices that may occur when + * interpolating quantiles. Since the separation is allowed to be above 1 it also + * allows testing configurations for close indices. */ + @Param({"0"}) + private int s; + + /** Indices. */ + private int[][] indices; + /** Cache permutation samplers. */ + private PermutationSampler[] samplers; + + /** {@inheritDoc} */ + @Override + protected int getLength() { + return length; + } + + /** {@inheritDoc} */ + @Override + public int size() { + return super.size() * repeats; + } + + /** {@inheritDoc} */ + @Override + public double[] getData(int index) { + // order = (data index) * repeats + repeat + // data index = order / repeats + return super.getDataSample(order[index] / repeats); + } + + /** + * Gets the indices for the given {@code index}. + * + * @param index Index. + * @return the data indices + */ + public int[] getIndices(int index) { + // order = (data index) * repeats + repeat + // Directly look-up the indices for this repeat. + return indices[order[index]]; + } + + /** + * Create the indices. + */ + @Override + @Setup(Level.Iteration) + public void setup() { + if (s < 0 || s >= getLength()) { + throw new IllegalStateException("Invalid separation: " + s); + } + super.setup(); + + // Data will be randomized per iteration + if (indices == null) { + // First call, create objects + indices = new int[size()][]; + // Cache samplers. These hold an array which is randomized + // per call to obtain a permutation. + if (k > 1) { + samplers = new PermutationSampler[getRange() + 1]; + } + } + + // Create indices in the data sample length. + // If a separation is provided then the length is reduced by the separation + // to make space for a second index. + + int index = 0; + final int noOfSamples = super.size(); + if (mode.startsWith("random")) { + // random mode creates a permutation of k indices in the length + if (k > 1) { + final int baseLength = getLength(); + for (int i = 0; i < noOfSamples; i++) { + final int len = getDataSize(i); + // Create permutation sampler for the length + PermutationSampler sampler = samplers[len - baseLength]; + if (sampler == null) { + // Reduce length by the separation + final int n = len - s; + samplers[len - baseLength] = sampler = new PermutationSampler(rng, n, k); + } + for (int j = repeats; --j >= 0;) { + indices[index++] = sampler.sample(); + } + } + } else { + // k=1: No requirement for a permutation + for (int i = 0; i < noOfSamples; i++) { + // Reduce length by the separation + final int n = getDataSize(i) - s; + for (int j = repeats; --j >= 0;) { + indices[index++] = new int[] {rng.nextInt(n)}; + } + } + } + } else if (mode.startsWith("uniform")) { + // uniform indices with a random start + for (int i = 0; i < noOfSamples; i++) { + // Reduce length by the separation + final int n = getDataSize(i) - s; + final int step = Math.max(1, (int) Math.round((double) n / k)); + for (int j = repeats; --j >= 0;) { + final int[] k1 = new int[k]; + int p = rng.nextInt(n); + for (int m = 0; m < k; m++) { + p = (p + step) % n; + k1[m] = p; + } + indices[index++] = k1; + } + } + } else if (mode.startsWith("single")) { + // uniform indices with a random start + for (int i = 0; i < noOfSamples; i++) { + // Reduce length by the separation + final int n = getDataSize(i) - s; + int[] samples; + // When k approaches n then a linear spacing covers every part + // of the array and we sample. Do this when n < k/4. This handles + // k > n (saturation). + if (n < (k >> 2)) { + samples = rng.ints(k, 0, n).toArray(); + } else { + // Linear spacing + final int step = n / k; + samples = new int[k]; + for (int j = 0, x = step >> 1; j < k; j++, x += step) { + samples[j] = x; + } + } + for (int j = 0; j < repeats; j++) { + final int ii = j % k; + if (ii == 0) { + PermutationSampler.shuffle(rng, samples); + } + indices[index++] = new int[] {samples[ii]}; + } + } + } else if ("index".equals(mode)) { + // Same single or paired indices for all samples. + // Check the index is valid. + for (int i = 0; i < noOfSamples; i++) { + // Reduce length by the separation + final int n = getDataSize(i) - s; + if (k >= n) { + throw new IllegalStateException("Invalid k: " + k + " >= " + n); + } + } + final int[] kk = s > 0 ? new int[] {k, k + s} : new int[] {k}; + Arrays.fill(indices, kk); + return; + } else { + throw new IllegalStateException("Unknown index mode: " + mode); + } + // Add paired indices + if (s > 0) { + for (int i = 0; i < indices.length; i++) { + final int[] k1 = indices[i]; + final int[] k2 = new int[k1.length << 1]; + for (int j = 0; j < k1.length; j++) { + k2[j << 1] = k1[j]; + k2[(j << 1) + 1] = k1[j] + s; + } + indices[i] = k2; + } + } + // Optionally sort + if (mode.endsWith("s")) { + for (int i = 0; i < indices.length; i++) { + Arrays.sort(indices[i]); + } + } + } + } + + /** + * Source of k-th indices. This does not extend the {@link AbstractDataSource} to provide + * data to partition. It is to be used to test processing of indices without partition + * overhead. + */ + @State(Scope.Benchmark) + public static class IndexSource { + /** Indices. */ + protected int[][] indices; + /** Upper bound (exclusive) on the indices. */ + @Param({"1000", "1000000", "1000000000"}) + private int length; + /** Number of indices to select. */ + @Param({"10", "20", "40", "80", "160"}) + private int k; + /** Number of repeats. */ + @Param({"1000"}) + private int repeats; + /** RNG seed. Created using ThreadLocalRandom.current().nextLong(). Each benchmark + * executed by JMH will use the same random data, even across JVMs. + * + *

If this is zero then a random seed is chosen. */ + @Param({"-7450238124206088695"}) + private long rngSeed; + /** Ordered keys. */ + @Param({"false"}) + private boolean ordered; + /** Minimum separation between keys. */ + @Param({"32"}) + private int separation; + + /** + * @return the indices + */ + public int[][] getIndices() { + return indices; + } + + /** + * Gets the minimum separation between keys. This is used by benchmarks + * to ignore splitting/search keys below a threshold. + * + * @return the minimum separation + */ + public int getMinSeparation() { + return separation; + } + + /** + * Create the indices and search points. + */ + @Setup(Level.Iteration) + public void setup() { + if (k < 2) { + throw new IllegalStateException("Require multiple indices"); + } + // Data will be randomized per iteration. It is the same sequence across + // benchmarks and JVM instances and allows benchmarking across JVM platforms + // with the same data. + // Allow pseudorandom seeding + if (rngSeed == 0) { + rngSeed = RandomSource.createLong(); + } + final UniformRandomProvider rng = RANDOM_SOURCE.create(rngSeed); + // Advance the seed for the next iteration. + rngSeed = rng.nextLong(); + + final SharedStateDiscreteSampler s = DiscreteUniformSampler.of(rng, 0, length - 1); + + indices = new int[repeats][]; + + for (int i = repeats; --i >= 0;) { + // Indices with possible repeats + final int[] x = new int[k]; + for (int j = k; --j >= 0;) { + x[j] = s.sample(); + } + indices[i] = x; + if (ordered) { + Sorting.sortIndices(x, x.length); + } + } + } + + /** + * @return the RNG seed + */ + long getRngSeed() { + return rngSeed; + } + } + + /** + * Source of k-th indices to be searched/split. + * Can be used to split the same indices multiple times, or split a set of indices + * a single time, e.g. split indices k at point p. + */ + @State(Scope.Benchmark) + public static class SplitIndexSource extends IndexSource { + /** Division mode. */ + @Param({"RANDOM", "BINARY"}) + private DivisionMode mode; + + /** Search points. */ + private int[][] points; + /** The look-up samples. These are used to identify a set of indices, and a single point to + * find in the range of the indices, e.g. split indices k at point p. The long packs + * two integers: the index of the indices k; and the search point p. These are packed + * as a long to enable easy shuffling of samples and access to the two indices. */ + private long[] samples; + + /** Options for the division mode. */ + public enum DivisionMode { + /** Randomly divide. */ + RANDOM, + /** Divide using binary division with recursion left then right. */ + BINARY; + } + + /** + * Return the search points. They are the median index points between adjacent + * indices. These are in the order specified by the division mode. + * + * @return the search points + */ + public int[][] getPoints() { + return points; + } + + /** + * @return the sample size + */ + int samples() { + return samples.length; + } + + /** + * Gets the indices for the random sample. + * + * @param index the index + * @return the indices + */ + int[] getIndices(int index) { + return indices[(int) (samples[index] >>> Integer.SIZE)]; + } + + /** + * Gets the search point for the random sample. + * + * @param index the index + * @return the search point + */ + int getPoint(int index) { + return (int) samples[index]; + } + + /** + * Create the indices and search points. + */ + @Override + @Setup(Level.Iteration) + public void setup() { + super.setup(); + + final UniformRandomProvider rng = RANDOM_SOURCE.create(getRngSeed()); + + final int[][] indices = getIndices(); + points = new int[indices.length][]; + + final int s = getMinSeparation(); + + // Set the division mode + final boolean random = Objects.requireNonNull(mode) == DivisionMode.RANDOM; + + int size = 0; + + for (int i = points.length; --i >= 0;) { + // Get the sorted unique indices + final int[] y = indices[i].clone(); + final int unique = Sorting.sortIndices(y, y.length); + + // Create the cut points between each unique index + int[] p = new int[unique - 1]; + if (random) { + int c = 0; + for (int j = 0; j < p.length; j++) { + // Ignore dense keys + if (y[j] + s < y[j + 1]) { + p[c++] = (y[j] + y[j + 1]) >>> 1; + } + } + p = Arrays.copyOf(p, c); + PermutationSampler.shuffle(rng, p); + points[i] = p; + } else { + // binary division + final int c = divide(y, 0, unique - 1, p, 0, s); + points[i] = Arrays.copyOf(p, c); + } + size += points[i].length; + } + + // Create the samples: pack indices index+point into a long + samples = new long[size]; + for (int i = points.length; --i >= 0;) { + final long l = ((long) i) << Integer.SIZE; + for (final int p : points[i]) { + samples[--size] = l | p; + } + } + shuffle(rng, samples); + } + + /** + * Divide the indices using binary division with recursion left then right. + * If a division is possible store the division point and update the count. + * + * @param indices Indices to divide + * @param lo Lower index in indices (inclusive). + * @param hi Upper index in indices (inclusive). + * @param p Division points. + * @param c Count of division points. + * @param s Minimum separation between indices. + * @return the updated count of division points. + */ + private static int divide(int[] indices, int lo, int hi, int[] p, int c, int s) { + if (lo < hi) { + // Divide the interval in half + final int m = (lo + hi) >>> 1; + // Create a division point at approximately the midpoint + final int m1 = m + 1; + // Ignore dense keys + if (indices[m] + s < indices[m1]) { + final int k = (indices[m] + indices[m1]) >>> 1; + p[c++] = k; + } + // Recurse left then right. + // Does nothing if lo + 1 == hi as m == lo and m1 == hi. + c = divide(indices, lo, m, p, c, s); + c = divide(indices, m1, hi, p, c, s); + } + return c; + } + + /** + * Shuffles the entries of the given array. + * + * @param rng Source of randomness. + * @param array Array whose entries will be shuffled (in-place). + */ + private static void shuffle(UniformRandomProvider rng, long[] array) { + for (int i = array.length; i > 1; i--) { + swap(array, i - 1, rng.nextInt(i)); + } + } + + /** + * Swaps the two specified elements in the array. + * + * @param array Array. + * @param i First index. + * @param j Second index. + */ + private static void swap(long[] array, int i, int j) { + final long tmp = array[i]; + array[i] = array[j]; + array[j] = tmp; + } + } + + /** + * Source of an {@link SearchableInterval}. + */ + @State(Scope.Benchmark) + public static class SearchableIntervalSource { + /** Name of the source. */ + @Param({"ScanningKeyInterval", + "BinarySearchKeyInterval", + "IndexSetInterval", + "CompressedIndexSet", + // Same speed as the CompressedIndexSet_2 + //"CompressedIndexSet2", + }) + private String name; + + /** The factory. */ + private Function factory; + + /** + * @param indices Indices. + * @return {@link SearchableInterval} + */ + public SearchableInterval create(int[] indices) { + return factory.apply(indices); + } + + /** + * Create the function. + */ + @Setup + public void setup() { + Objects.requireNonNull(name); + if ("ScanningKeyInterval".equals(name)) { + factory = k -> { + k = k.clone(); + final int unique = Sorting.sortIndices(k, k.length); + return ScanningKeyInterval.of(k, unique); + }; + } else if ("BinarySearchKeyInterval".equals(name)) { + factory = k -> { + k = k.clone(); + final int unique = Sorting.sortIndices(k, k.length); + return BinarySearchKeyInterval.of(k, unique); + }; + } else if ("IndexSetInterval".equals(name)) { + factory = IndexSet::of; + } else if (name.equals("CompressedIndexSet2")) { + factory = CompressedIndexSet2::of; + } else if (name.startsWith("CompressedIndexSet")) { + // To use compression 2 requires CompressedIndexSet_2 otherwise + // a fixed compression set will be returned + final int c = getCompression(name); + factory = k -> CompressedIndexSet.of(c, k); + } else { + throw new IllegalStateException("Unknown SearchableInterval: " + name); + } + } + + /** + * Gets the compression from the last character of the name. + * + * @param name Name. + * @return the compression + */ + private static int getCompression(String name) { + final char c = name.charAt(name.length() - 1); + if (Character.isDigit(c)) { + return Character.digit(c, 10); + } + return 1; + } + } + + /** + * Source of an {@link UpdatingInterval}. + */ + @State(Scope.Benchmark) + public static class UpdatingIntervalSource { + /** Name of the source. */ + @Param({"KeyUpdatingInterval", + // Same speed as BitIndexUpdatingInterval + //"IndexSet", + "BitIndexUpdatingInterval", + }) + private String name; + + /** The factory. */ + private Function factory; + + /** + * @param indices Indices. + * @return {@link UpdatingInterval} + */ + public UpdatingInterval create(int[] indices) { + return factory.apply(indices); + } + + /** + * Create the function. + */ + @Setup + public void setup() { + Objects.requireNonNull(name); + if ("KeyUpdatingInterval".equals(name)) { + factory = k -> { + k = k.clone(); + final int unique = Sorting.sortIndices(k, k.length); + return KeyUpdatingInterval.of(k, unique); + }; + } else if ("IndexSet".equals(name)) { + factory = k -> IndexSet.of(k).interval(); + } else if (name.equals("BitIndexUpdatingInterval")) { + factory = k -> BitIndexUpdatingInterval.of(k, k.length); + } else { + throw new IllegalStateException("Unknown UpdatingInterval: " + name); + } + } + } + + /** + * Source of a range of positions to partition. These are positioned away from the edge + * using a power of 2 shift. + * + *

This is a specialised class to allow benchmarking the switch from using + * quickselect partitioning to using an edge selection. + * + *

This class provides both data to partition and the indices to partition. + * The indices and data are created per iteration. The order to process them + * is created per invocation. + */ + @State(Scope.Benchmark) + public static class EdgeSource extends AbstractDataSource { + /** Data length. */ + @Param({"1023"}) + private int length; + /** Mode. */ + @Param({"SHIFT"}) + private Mode mode; + /** Parameter to find k. Configured for 'shift' of the length. */ + @Param({"1", "2", "3", "4", "5", "6", "7", "8", "9"}) + private int p; + /** Target indices (as pairs of {@code [ka, kb]} defining a range to select). */ + private int[][] indices; + + /** Define the method used to generated the edge k. */ + public enum Mode { + /** Create {@code k} using a right-shift {@code >>>} applied to the length. */ + SHIFT, + /** Use the parameter {@code p} as an index. */ + INDEX; + } + + /** {@inheritDoc} */ + @Override + public int size() { + return super.size() * 2; + } + + /** {@inheritDoc} */ + @Override + public double[] getData(int index) { + // order = (data index) * repeats + repeat + // data index = order / repeats; repeats=2 divide by using a shift + return super.getDataSample(order[index] >> 1); + } + + /** + * Gets the sample indices for the given {@code index}. + * Returns a range to partition {@code [k1, kn]}. + * + * @param index Index. + * @return the target indices + */ + public int[] getIndices(int index) { + // order = (data index) * repeats + repeat + // Directly look-up the indices for this repeat. + return indices[order[index]]; + } + + /** {@inheritDoc} */ + @Override + protected int getLength() { + return length; + } + + /** + * Create the data and check the indices are not at the end. + */ + @Override + @Setup(Level.Iteration) + public void setup() { + // Data will be randomized per iteration + super.setup(); + // Error for a bad configuration. Allow k=0 but not smaller. + // Uses the lower bound on the length. + int k; + if (mode == Mode.SHIFT) { + k = length >>> p; + if (k == 0 && length >>> (p - 1) == 0) { + throw new IllegalStateException(length + " >>> (" + p + " - 1) == 0"); + } + } else if (mode == Mode.INDEX) { + k = p; + if (k < 0 || k >= length) { + throw new IllegalStateException("Invalid index [0, " + length + "): " + p); + } + } else { + throw new IllegalStateException("Unknown mode: " + mode); + } + + if (indices == null) { + // First call, create objects + indices = new int[size()][]; + } + + // Create a single index at both ends. + // Note: Data has variable length so we have to compute the upper end for each sample. + // Re-use the constant lower but we do not bother to cache repeats of the upper. + final int[] lower = {k, k}; + final int noOfSamples = super.size(); + for (int i = 0; i < noOfSamples; i++) { + final int len = getDataSize(i); + final int k1 = len - 1 - k; + indices[i << 1] = lower; + indices[(i << 1) + 1] = new int[] {k1, k1}; + } + } + } + + /** + * Source of a sort function. + */ + @State(Scope.Benchmark) + public static class SortFunctionSource { + /** Name of the source. */ + @Param({JDK, SP, BM, SBM, DP, DP5, + SBM2, + // Not run by default as it is slow on large data + //"InsertionSortIF", "InsertionSortIT", "InsertionSort", "InsertionSortB" + // Introsort methods with defaults, can configure using the name + // e.g. ISP_SBM_QS50. + ISP, IDP, + }) + private String name; + + /** Override of minimum quickselect size. */ + @Param({"0"}) + private int qs; + + /** The action. */ + private Consumer function; + + /** + * @return the function + */ + public Consumer getFunction() { + return function; + } + + /** + * Create the function. + */ + @Setup + public void setup() { + Objects.requireNonNull(name); + if (JDK.equals(name)) { + function = Arrays::sort; + // First generation kth-selector functions (not configurable) + } else if (name.startsWith(SP)) { + function = PartitionFactory.createKthSelector(name, SP, qs)::sortSP; + } else if (name.startsWith(SBM)) { + function = PartitionFactory.createKthSelector(name, SBM, qs)::sortSBM; + } else if (name.startsWith(BM)) { + function = PartitionFactory.createKthSelector(name, BM, qs)::sortBM; + } else if (name.startsWith(DP)) { + function = PartitionFactory.createKthSelector(name, DP, qs)::sortDP; + } else if (name.startsWith(DP5)) { + function = PartitionFactory.createKthSelector(name, DP5, qs)::sortDP5; + // 2nd generation partition function + } else if (name.startsWith(SBM2)) { + function = PartitionFactory.createPartition(name, SBM2, qs, 0)::sortSBM; + // Introsort + } else if (name.startsWith(ISP)) { + function = PartitionFactory.createPartition(name, ISP, qs, 0)::sortISP; + } else if (name.startsWith(IDP)) { + function = PartitionFactory.createPartition(name, IDP, qs, 0)::sortIDP; + // Insertion sort variations. + // For parity with the internal version these all use the same (shorter) data + } else if ("InsertionSortIF".equals(name)) { + function = x -> { + // Ignored sentinal + x[0] = Double.NEGATIVE_INFINITY; + Sorting.sort(x, 1, x.length - 1, false); + }; + } else if ("InsertionSortIT".equals(name)) { + // Internal version + function = x -> { + // Add a sentinal + x[0] = Double.NEGATIVE_INFINITY; + Sorting.sort(x, 1, x.length - 1, true); + }; + } else if ("InsertionSort".equals(name)) { + function = x -> { + x[0] = Double.NEGATIVE_INFINITY; + Sorting.sort(x, 1, x.length - 1); + }; + } else if (name.startsWith("PairedInsertionSort")) { + if (name.endsWith("1")) { + function = x -> { + x[0] = Double.NEGATIVE_INFINITY; + Sorting.sortPairedInternal1(x, 1, x.length - 1); + }; + } else if (name.endsWith("2")) { + function = x -> { + x[0] = Double.NEGATIVE_INFINITY; + Sorting.sortPairedInternal2(x, 1, x.length - 1); + }; + } else if (name.endsWith("3")) { + function = x -> { + x[0] = Double.NEGATIVE_INFINITY; + Sorting.sortPairedInternal3(x, 1, x.length - 1); + }; + } else if (name.endsWith("4")) { + function = x -> { + x[0] = Double.NEGATIVE_INFINITY; + Sorting.sortPairedInternal4(x, 1, x.length - 1); + }; + } + } else if ("InsertionSortB".equals(name)) { + function = x -> { + x[0] = Double.NEGATIVE_INFINITY; + Sorting.sortb(x, 1, x.length - 1); + }; + // Not actually a sort. This is used to benchmark the speed of heapselect + // for a single k as a stopper function against a full sort of small data. + } else if (name.startsWith(HEAP_SELECT)) { + final char c = name.charAt(name.length() - 1); + // This offsets the start by 1 for comparison with insertion sort + final int k = Character.isDigit(c) ? Character.digit(c, 10) + 1 : 1; + function = x -> Partition.heapSelectLeft(x, 1, x.length - 1, k, 0); + } + if (function == null) { + throw new IllegalStateException("Unknown sort function: " + name); + } + } + } + + /** + * Source of a sort function to sort 5 points. + */ + @State(Scope.Benchmark) + public static class Sort5FunctionSource { + /** Name of the source. */ + @Param({"sort5", "sort5b", "sort5c", + //"sort", "sort5head" + }) + private String name; + + /** The action. */ + private Consumer function; + + /** + * @return the function + */ + public Consumer getFunction() { + return function; + } + + /** + * Create the function. + */ + @Setup + public void setup() { + Objects.requireNonNull(name); + // Note: We do not run this on input of length 5. We can run it on input of + // any length above 5. So we choose indices using a spacing of 1/4 of the range. + // Since we do this for all methods it is a fixed overhead. This allows use + // of a variety of data sizes. + if ("sort5".equals(name)) { + function = x -> { + final int s = x.length >> 2; + Sorting.sort5(x, 0, s, s << 1, x.length - 1 - s, x.length - 1); + }; + } else if ("sort5b".equals(name)) { + function = x -> { + final int s = x.length >> 2; + Sorting.sort5b(x, 0, s, s << 1, x.length - 1 - s, x.length - 1); + }; + } else if ("sort5c".equals(name)) { + function = x -> { + final int s = x.length >> 2; + Sorting.sort5c(x, 0, s, s << 1, x.length - 1 - s, x.length - 1); + }; + } else if ("sort".equals(name)) { + function = x -> Sorting.sort(x, 0, 4); + } else if ("sort5head".equals(name)) { + function = x -> Sorting.sort5(x, 0, 1, 2, 3, 4); + // Median of 5. Ensure the median index is computed by storing it in x + } else if ("median5".equals(name)) { + function = x -> { + final int s = x.length >> 2; + x[0] = Sorting.median5(x, 0, s, s << 1, x.length - 1 - s, x.length - 1); + }; + } else if ("median5head".equals(name)) { + function = x -> x[0] = Sorting.median5(x, 0, 1, 2, 3, 4); + // median of 5 continuous elements + } else if ("med5".equals(name)) { + function = x -> x[0] = Sorting.median5(x, 0); + } else if ("med5b".equals(name)) { + function = x -> x[0] = Sorting.median5b(x, 0); + } else if ("med5c".equals(name)) { + function = x -> x[0] = Sorting.median5c(x, 0); + } else if ("med5d".equals(name)) { + function = x -> Sorting.median5d(x, 0, 1, 2, 3, 4); + } else { + throw new IllegalStateException("Unknown sort5 function: " + name); + } + } + } + + /** + * Source of a function to compute the median of 4 points. + */ + @State(Scope.Benchmark) + public static class Median4FunctionSource { + /** Name of the source. */ + @Param({"lower4", "lower4b", "lower4c", "lower4d", "lower4e", + "upper4", "upper4c", "upper4d", + // Full sort is slower + //"sort4" + }) + private String name; + + /** The action. */ + private Consumer function; + + /** + * @return the function + */ + public Consumer getFunction() { + return function; + } + + /** + * Create the function. + */ + @Setup + public void setup() { + Objects.requireNonNull(name); + // Note: We run this across the entire input array to simulate a pass + // of the quickselect adaptive algorithm. + if ("lower4".equals(name)) { + function = x -> { + final int f = x.length >>> 2; + final int f2 = f + f; + for (int i = f; i < f2; i++) { + Sorting.lowerMedian4(x, i - f, i, i + f, i + f2); + } + }; + } else if ("lower4b".equals(name)) { + function = x -> { + final int f = x.length >>> 2; + final int f2 = f + f; + for (int i = f; i < f2; i++) { + Sorting.lowerMedian4b(x, i - f, i, i + f, i + f2); + } + }; + } else if ("lower4c".equals(name)) { + function = x -> { + final int f = x.length >>> 2; + final int f2 = f + f; + for (int i = f; i < f2; i++) { + Sorting.lowerMedian4c(x, i - f, i, i + f, i + f2); + } + }; + } else if ("lower4d".equals(name)) { + function = x -> { + final int f = x.length >>> 2; + final int f2 = f + f; + for (int i = f; i < f2; i++) { + Sorting.lowerMedian4d(x, i - f, i, i + f, i + f2); + } + }; + } else if ("lower4e".equals(name)) { + function = x -> { + final int f = x.length >>> 2; + final int f2 = f + f; + for (int i = f; i < f2; i++) { + Sorting.lowerMedian4e(x, i - f, i, i + f, i + f2); + } + }; + } else if ("upper4".equals(name)) { + function = x -> { + final int f = x.length >>> 2; + final int f2 = f + f; + for (int i = f; i < f2; i++) { + Sorting.upperMedian4(x, i - f, i, i + f, i + f2); + } + }; + } else if ("upper4c".equals(name)) { + function = x -> { + final int f = x.length >>> 2; + final int f2 = f + f; + for (int i = f; i < f2; i++) { + Sorting.upperMedian4c(x, i - f, i, i + f, i + f2); + } + }; + } else if ("upper4d".equals(name)) { + function = x -> { + final int f = x.length >>> 2; + final int f2 = f + f; + for (int i = f; i < f2; i++) { + Sorting.upperMedian4d(x, i - f, i, i + f, i + f2); + } + }; + } else if ("sort4".equals(name)) { + function = x -> { + final int f = x.length >>> 2; + final int f2 = f + f; + for (int i = f; i < f2; i++) { + Sorting.sort4(x, i - f, i, i + f, i + f2); + } + }; + } else { + throw new IllegalStateException("Unknown median4 function: " + name); + } + } + } + + /** + * Source of a function to compute the median of 3 points. + */ + @State(Scope.Benchmark) + public static class Median3FunctionSource { + /** Name of the source. */ + @Param({"sort3", "sort3b", "sort3c"}) + private String name; + + /** The action. */ + private Consumer function; + + /** + * @return the function + */ + public Consumer getFunction() { + return function; + } + + /** + * Create the function. + */ + @Setup + public void setup() { + Objects.requireNonNull(name); + // Note: We run this across the entire input array to simulate a pass + // of the quickselect adaptive algorithm. + if ("sort3".equals(name)) { + function = x -> { + final int f = x.length / 3; + final int f2 = f + f; + for (int i = f; i < f2; i++) { + Sorting.sort3(x, i - f, i, i + f); + } + }; + } else if ("sort3b".equals(name)) { + function = x -> { + final int f = x.length / 3; + final int f2 = f + f; + for (int i = f; i < f2; i++) { + Sorting.sort3b(x, i - f, i, i + f); + } + }; + } else if ("sort3c".equals(name)) { + function = x -> { + final int f = x.length / 3; + final int f2 = f + f; + for (int i = f; i < f2; i++) { + Sorting.sort3c(x, i - f, i, i + f); + } + }; + } else { + throw new IllegalStateException("Unknown sort3 function: " + name); + } + } + } + + /** + * Source of a k-th selector function. + */ + @State(Scope.Benchmark) + public static class KFunctionSource { + /** Name of the source. */ + @Param({SORT + JDK, SPH, + SP, BM, SBM, + DP, DP5, + SBM2, + ISP, IDP, + LSP, LINEAR, SELECT}) + private String name; + + /** Override of minimum quickselect size. */ + @Param({"0"}) + private int qs; + + /** Override of minimum edgeselect constant. */ + @Param({"0"}) + private int ec; + + /** The action. */ + private BiFunction function; + + /** + * @return the function + */ + public BiFunction getFunction() { + return function; + } + + /** + * Create the function. + */ + @Setup + public void setup() { + Objects.requireNonNull(name); + // Note: For parity in the test, each partition method that accepts the keys as any array + // receives a clone of the indices. + if (name.equals(BASELINE)) { + function = (data, indices) -> extractIndices(data, indices.clone()); + } else if (name.startsWith(SORT)) { + // Sort variants (do not clone the keys) + if (name.contains(ISP)) { + final Partition part = PartitionFactory.createPartition(name.substring(SORT.length()), ISP, qs, ec); + function = (data, indices) -> { + part.sortISP(data); + return extractIndices(data, indices); + }; + } else if (name.contains(IDP)) { + final Partition part = PartitionFactory.createPartition(name.substring(SORT.length()), IDP, qs, ec); + function = (data, indices) -> { + part.sortIDP(data); + return extractIndices(data, indices); + }; + } else if (name.contains(JDK)) { + function = (data, indices) -> { + Arrays.sort(data); + return extractIndices(data, indices); + }; + } + // First generation kth-selector functions + } else if (name.startsWith(SPH)) { + // Ported CM implementation with a heap + final KthSelector selector = PartitionFactory.createKthSelector(name, SPH, qs); + function = (data, indices) -> { + final int n = indices.length; + // Note: Pivots heap is not optimal here but should be enough for most cases. + // The size matches that in the Commons Math Percentile class + final int[] pivots = n <= 1 ? + KthSelector.NO_PIVOTS : + new int[1023]; + final double[] x = new double[indices.length]; + for (int i = 0; i < indices.length; i++) { + x[i] = selector.selectSPH(data, pivots, indices[i], null); + } + return x; + }; + // The following methods clone the indices to avoid destructive modification + } else if (name.startsWith(SPN)) { + final KthSelector selector = PartitionFactory.createKthSelector(name, SPN, qs); + function = (data, indices) -> { + selector.partitionSPN(data, indices.clone()); + return extractIndices(data, indices); + }; + } else if (name.startsWith(SP)) { + final KthSelector selector = PartitionFactory.createKthSelector(name, SP, qs); + function = (data, indices) -> { + selector.partitionSP(data, indices.clone()); + return extractIndices(data, indices); + }; + } else if (name.startsWith(BM)) { + final KthSelector selector = PartitionFactory.createKthSelector(name, BM, qs); + function = (data, indices) -> { + selector.partitionBM(data, indices.clone()); + return extractIndices(data, indices); + }; + } else if (name.startsWith(SBM)) { + final KthSelector selector = PartitionFactory.createKthSelector(name, SBM, qs); + function = (data, indices) -> { + selector.partitionSBM(data, indices.clone()); + return extractIndices(data, indices); + }; + } else if (name.startsWith(DP)) { + final KthSelector selector = PartitionFactory.createKthSelector(name, DP, qs); + function = (data, indices) -> { + selector.partitionDP(data, indices.clone()); + return extractIndices(data, indices); + }; + } else if (name.startsWith(DP5)) { + final KthSelector selector = PartitionFactory.createKthSelector(name, DP5, qs); + function = (data, indices) -> { + selector.partitionDP5(data, indices.clone()); + return extractIndices(data, indices); + }; + // Second generation partition function with configurable key strategy + } else if (name.startsWith(SBM2)) { + final Partition part = PartitionFactory.createPartition(name, SBM2, qs, ec); + function = (data, indices) -> { + part.partitionSBM(data, indices.clone(), indices.length); + return extractIndices(data, indices); + }; + // Floyd-Rivest partition functions + } else if (name.startsWith(FR)) { + final Partition part = PartitionFactory.createPartition(name, FR, qs, ec); + function = (data, indices) -> { + part.partitionFR(data, indices.clone(), indices.length); + return extractIndices(data, indices); + }; + } else if (name.startsWith(KFR)) { + final Partition part = PartitionFactory.createPartition(name, KFR, qs, ec); + function = (data, indices) -> { + part.partitionKFR(data, indices.clone(), indices.length); + return extractIndices(data, indices); + }; + // Introselect implementations which are highly configurable + } else if (name.startsWith(ISP)) { + final Partition part = PartitionFactory.createPartition(name, ISP, qs, ec); + function = (data, indices) -> { + part.partitionISP(data, indices.clone(), indices.length); + return extractIndices(data, indices); + }; + } else if (name.startsWith(IDP)) { + final Partition part = PartitionFactory.createPartition(name, IDP, qs, ec); + function = (data, indices) -> { + part.partitionIDP(data, indices.clone(), indices.length); + return extractIndices(data, indices); + }; + } else if (name.startsWith(SELECT)) { + // Not configurable + function = (data, indices) -> { + Selection.select(data, indices.clone()); + return extractIndices(data, indices); + }; + // Linearselect (median-of-medians) implementation (stopper for quickselect) + } else if (name.startsWith(LSP)) { + final Partition part = PartitionFactory.createPartition(name, LSP, qs, ec); + function = (data, indices) -> { + part.partitionLSP(data, indices.clone(), indices.length); + return extractIndices(data, indices); + }; + // Linearselect (optimised median-of-medians) implementation (stopper for quickselect) + } else if (name.startsWith(LINEAR)) { + final Partition part = PartitionFactory.createPartition(name, LINEAR, qs, ec); + function = (data, indices) -> { + part.partitionLinear(data, indices.clone(), indices.length); + return extractIndices(data, indices); + }; + } else if (name.startsWith(QA2)) { + // Configurable only by static properties. + // Default to FR sampling for the initial mode. + final int mode = PartitionFactory.getControlFlags(new String[] {name}, -1); + final int inc = PartitionFactory.getOptionFlags(new String[] {name}, 1); + Partition.configureQaAdaptive(mode, inc); + function = (data, indices) -> { + Partition.partitionQA2(data, indices.clone(), indices.length); + return extractIndices(data, indices); + }; + } else if (name.startsWith(QA)) { + final Partition part = PartitionFactory.createPartition(name, QA, qs, ec); + function = (data, indices) -> { + part.partitionQA(data, indices.clone(), indices.length); + return extractIndices(data, indices); + }; + // Heapselect implementation (stopper for quickselect) + } else if (name.startsWith(HEAP_SELECT)) { + function = (data, indices) -> { + int min = indices[indices.length - 1]; + int max = min; + for (int i = indices.length - 1; --i >= 0;) { + min = Math.min(min, indices[i]); + max = Math.max(max, indices[i]); + } + Partition.heapSelectRange(data, 0, data.length - 1, min, max); + return extractIndices(data, indices); + }; + } + if (function == null) { + throw new IllegalStateException("Unknown selector function: " + name); + } + } + + /** + * Extract the data at the specified indices. + * + * @param data Data. + * @param indices Indices. + * @return the data + */ + private static double[] extractIndices(double[] data, int[] indices) { + final double[] x = new double[indices.length]; + for (int i = 0; i < indices.length; i++) { + x[i] = data[indices[i]]; + } + return x; + } + } + + /** + * Source of a function that pre-processes NaN and signed zeros (-0.0). + * + *

Detection of signed zero using direct conversion of raw bits and + * comparison with the bit representation is noticeably faster than comparison + * using {@code == 0.0}. + */ + @State(Scope.Benchmark) + public static class SortNaNFunctionSource { + /** Name of the source. */ + @Param({"RawZeroNaN", "ZeroSignNaN", "NaNRawZero", "NaNZeroSign"}) + private String name; + + /** The action. */ + private BiConsumer function; + + /** + * @return the function + */ + public BiConsumer getFunction() { + return function; + } + + /** + * Create the function. + */ + @Setup + public void setup() { + // Functions sort NaN and detect signed zeros. + // For convenience the function accepts the blackhole to handle any + // output from the processing. + if ("RawZeroNaN".equals(name)) { + function = (a, bh) -> { + int cn = 0; + int end = a.length; + for (int i = end; --i >= 0;) { + final double v = a[i]; + if (Double.doubleToRawLongBits(v) == Long.MIN_VALUE) { + cn++; + a[i] = 0.0; + } else if (v != v) { + a[i] = a[--end]; + a[end] = v; + } + } + bh.consume(cn); + bh.consume(end); + }; + } else if ("ZeroSignNaN".equals(name)) { + function = (a, bh) -> { + int cn = 0; + int end = a.length; + for (int i = end; --i >= 0;) { + final double v = a[i]; + if (v == 0.0 && Double.doubleToRawLongBits(v) < 0) { + cn++; + a[i] = 0.0; + } else if (v != v) { + a[i] = a[--end]; + a[end] = v; + } + } + bh.consume(cn); + bh.consume(end); + }; + } else if ("NaNRawZero".equals(name)) { + function = (a, bh) -> { + int cn = 0; + int end = a.length; + for (int i = end; --i >= 0;) { + final double v = a[i]; + if (v != v) { + a[i] = a[--end]; + a[end] = v; + } else if (Double.doubleToRawLongBits(v) == Long.MIN_VALUE) { + cn++; + a[i] = 0.0; + } + } + bh.consume(cn); + bh.consume(end); + }; + } else if ("NaNZeroSign".equals(name)) { + function = (a, bh) -> { + int cn = 0; + int end = a.length; + for (int i = end; --i >= 0;) { + final double v = a[i]; + if (v != v) { + a[i] = a[--end]; + a[end] = v; + } else if (v == 0.0 && Double.doubleToRawLongBits(v) < 0) { + cn++; + a[i] = 0.0; + } + } + bh.consume(cn); + bh.consume(end); + }; + } else { + throw new IllegalStateException("Unknown sort NaN function: " + name); + } + } + } + + /** + * Source of a edge selector function. This is a function that selects indices + * that are clustered close to the edge of the data. + * + *

This is a specialised class to allow benchmarking the switch from using + * quickselect partitioning to using edgeselect. + */ + @State(Scope.Benchmark) + public static class EdgeFunctionSource { + /** Name of the source. + * For introselect methods this should effectively turn-off edgeselect. */ + @Param({HEAP_SELECT, ISP + "_EC0", IDP + "_EC0", + // Only use for small length as sort insertion is worst case Order(k * (right - left)) + // vs heap select() is O(k - left) + O((right - k) * log(k - left)) + //SORT_SELECT + }) + private String name; + + /** The action. */ + private BiFunction function; + + /** + * @return the function + */ + public BiFunction getFunction() { + return function; + } + + /** + * Create the function. + */ + @Setup + public void setup() { + Objects.requireNonNull(name); + // Direct use of heapselect. This has variations which use different + // optimisations for small heaps. + // Note: Optimisation for small heap size (n=1,2) is not observable on large data. + // It requires the use of small data (e.g. len=[16, 32)) to observe differences. + // The main overhead is the test for insertion against the current top of the + // heap which grows increasingly unlikely as the range is scanned. + // Optimisation for n=1 is negligible; for n=2 it is up to 10%. However using only + // heapSelectRange2 is not as fast as the non-optimised heapSelectRange0 + // when the heap is size 1. For n=1 the heap insertion branch prediction + // can learn the heap has no children and skip descending the heap, whereas + // heap size n=2 can descend 1 level if the child is smaller/bigger. This is not + // as fast as dedicated code for the single child case. + // This benchmark requires repeating with variable heap size to avoid branch + // prediction learning what to do, i.e. use with an index source that has variable + // distance from the edge. + if (HEAP_SELECT.equals(name)) { + function = (data, indices) -> { + heapSelectRange0(data, 0, data.length - 1, indices[0], indices[1]); + return extractIndices(data, indices[0], indices[1]); + }; + } else if ((HEAP_SELECT + "1").equals(name)) { + function = (data, indices) -> { + heapSelectRange1(data, 0, data.length - 1, indices[0], indices[1]); + return extractIndices(data, indices[0], indices[1]); + }; + } else if ((HEAP_SELECT + "2").equals(name)) { + function = (data, indices) -> { + heapSelectRange2(data, 0, data.length - 1, indices[0], indices[1]); + return extractIndices(data, indices[0], indices[1]); + }; + } else if ((HEAP_SELECT + "12").equals(name)) { + function = (data, indices) -> { + heapSelectRange12(data, 0, data.length - 1, indices[0], indices[1]); + return extractIndices(data, indices[0], indices[1]); + }; + // Only use on small edge as insertion is Order(k) + } else if (SORT_SELECT.equals(name)) { + function = (data, indices) -> { + Partition.sortSelectRange(data, 0, data.length - 1, indices[0], indices[1]); + return extractIndices(data, indices[0], indices[1]); + }; + // Introselect methods - these should be configured to not use edgeselect. + // These directly call the introselect method to skip NaN/signed zero processing. + } else if (name.startsWith(ISP)) { + final Partition part = PartitionFactory.createPartition(name, ISP); + function = (data, indices) -> { + part.introselect(part.getSPFunction(), data, + 0, data.length - 1, IndexIntervals.interval(indices[0], indices[1]), 10000); + return extractIndices(data, indices[0], indices[1]); + }; + } else if (name.startsWith(IDP)) { + final Partition part = PartitionFactory.createPartition(name, IDP); + function = (data, indices) -> { + part.introselect(Partition::partitionDP, data, + 0, data.length - 1, IndexIntervals.interval(indices[0], indices[1]), 10000); + return extractIndices(data, indices[0], indices[1]); + }; + } else { + throw new IllegalStateException("Unknown edge selector function: " + name); + } + } + + /** + * Partition the elements between {@code ka} and {@code kb} using a heap select + * algorithm. It is assumed {@code left <= ka <= kb <= right}. + * + *

Note: + * + *

This is a copy of {@link Partition#heapSelectRange(double[], int, int, int, int)}. + * It uses no optimised versions for small heaps. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param ka Lower index to select. + * @param kb Upper index to select. + */ + static void heapSelectRange0(double[] a, int left, int right, int ka, int kb) { + if (right - left < Partition.MIN_HEAPSELECT_SIZE) { + Sorting.sort(a, left, right); + return; + } + if (kb - left < right - ka) { + Partition.heapSelectLeft(a, left, right, kb, kb - ka); + } else { + Partition.heapSelectRight(a, left, right, ka, kb - ka); + } + } + + /** + * Partition the elements between {@code ka} and {@code kb} using a heap select + * algorithm. It is assumed {@code left <= ka <= kb <= right}. + * + *

Note: + * + *

This is a copy of {@link Partition#heapSelectRange(double[], int, int, int, int)}. + * It uses no optimised versions for small heap of size 1. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param ka Lower index to select. + * @param kb Upper index to select. + */ + static void heapSelectRange1(double[] a, int left, int right, int ka, int kb) { + if (right - left < Partition.MIN_HEAPSELECT_SIZE) { + Sorting.sort(a, left, right); + return; + } + if (kb - left < right - ka) { + // Optimise + if (kb == left) { + Partition.selectMinIgnoreZeros(a, left, right); + } else { + Partition.heapSelectLeft(a, left, right, kb, kb - ka); + } + } else { + // Optimise + if (ka == right) { + Partition.selectMaxIgnoreZeros(a, left, right); + } else { + Partition.heapSelectRight(a, left, right, ka, kb - ka); + } + } + } + + /** + * Partition the elements between {@code ka} and {@code kb} using a heap select + * algorithm. It is assumed {@code left <= ka <= kb <= right}. + * + *

Note: + * + *

This is a copy of {@link Partition#heapSelectRange(double[], int, int, int, int)}. + * It uses optimised versions for small heap of size 2. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param ka Lower index to select. + * @param kb Upper index to select. + */ + static void heapSelectRange2(double[] a, int left, int right, int ka, int kb) { + if (right - left < Partition.MIN_HEAPSELECT_SIZE) { + Sorting.sort(a, left, right); + return; + } + if (kb - left < right - ka) { + // Optimise + if (kb - 1 <= left) { + Partition.selectMin2IgnoreZeros(a, left, right); + } else { + Partition.heapSelectLeft(a, left, right, kb, kb - ka); + } + } else { + // Optimise + if (ka + 1 >= right) { + Partition.selectMax2IgnoreZeros(a, left, right); + } else { + Partition.heapSelectRight(a, left, right, ka, kb - ka); + } + } + } + + /** + * Partition the elements between {@code ka} and {@code kb} using a heap select + * algorithm. It is assumed {@code left <= ka <= kb <= right}. + * + *

Note: + * + *

This is a copy of {@link Partition#heapSelectRange(double[], int, int, int, int)}. + * It uses optimised versions for small heap of size 1 and 2. + * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param ka Lower index to select. + * @param kb Upper index to select. + */ + static void heapSelectRange12(double[] a, int left, int right, int ka, int kb) { + if (right - left < Partition.MIN_HEAPSELECT_SIZE) { + Sorting.sort(a, left, right); + return; + } + if (kb - left < right - ka) { + // Optimise + if (kb - 1 <= left) { + if (kb == left) { + Partition.selectMinIgnoreZeros(a, left, right); + } else { + Partition.selectMin2IgnoreZeros(a, left, right); + } + } else { + Partition.heapSelectLeft(a, left, right, kb, kb - ka); + } + } else { + // Optimise + if (ka + 1 >= right) { + if (ka == right) { + Partition.selectMaxIgnoreZeros(a, left, right); + } else { + Partition.selectMax2IgnoreZeros(a, left, right); + } + } else { + Partition.heapSelectRight(a, left, right, ka, kb - ka); + } + } + } + + /** + * Extract the data at the specified indices. + * + * @param data Data. + * @param l Lower bound (inclusive). + * @param r Upper bound (inclusive). + * @return the data + */ + private static double[] extractIndices(double[] data, int l, int r) { + final double[] x = new double[r - l + 1]; + for (int i = l; i <= r; i++) { + x[i - l] = data[i]; + } + return x; + } + } + + /** + * Source of an search function. This is a function that find an index + * in a sorted list of indices, e.g. a binary search. + */ + @State(Scope.Benchmark) + public static class IndexSearchFunctionSource { + /** Name of the source. */ + @Param({"Binary", + //"binarySearch", + "Scan"}) + private String name; + + /** The action. */ + private SearchFunction function; + + /** + * Define a search function. + */ + public interface SearchFunction { + /** + * Find the index of the element {@code k}, or the closest index + * to the element (implementation definitions may vary). + * + * @param a Data. + * @param k Element. + * @return the index + */ + int find(int[] a, int k); + } + + /** + * @return the function + */ + public SearchFunction getFunction() { + return function; + } + + /** + * Create the function. + */ + @Setup + public void setup() { + Objects.requireNonNull(name); + if ("Binary".equals(name)) { + function = (keys, k) -> Partition.searchLessOrEqual(keys, 0, keys.length - 1, k); + } else if ("binarySearch".equals(name)) { + function = (keys, k) -> Arrays.binarySearch(keys, 0, keys.length, k); + } else if ("Scan".equals(name)) { + function = (keys, k) -> { + // Assume that k >= keys[0] + int i = keys.length; + do { + --i; + } while (keys[i] > k); + return i; + }; + } else { + throw new IllegalStateException("Unknown index search function: " + name); + } + } + } + + /** + * Benchmark a sort on the data. + * + * @param function Source of the function. + * @param source Source of the data. + * @param bh Data sink. + */ + @Benchmark + public void sort(SortFunctionSource function, SortSource source, Blackhole bh) { + final int size = source.size(); + final Consumer fun = function.getFunction(); + for (int j = -1; ++j < size;) { + final double[] y = source.getData(j); + fun.accept(y); + bh.consume(y); + } + } + + /** + * Benchmark a sort of 5 data values. + * This tests the pivot selection from 5 values used in dual-pivot partitioning. + * + * @param function Source of the function. + * @param source Source of the data. + * @param bh Data sink. + */ + @Benchmark + public void fiveSort(Sort5FunctionSource function, SortSource source, Blackhole bh) { + final int size = source.size(); + final Consumer fun = function.getFunction(); + for (int j = -1; ++j < size;) { + final double[] y = source.getData(j); + fun.accept(y); + bh.consume(y); + } + } + + /** + * Benchmark a pass over the entire data computing the medians of 4 data values. + * This simulates a QuickselectAdaptive step. + * + * @param function Source of the function. + * @param source Source of the data. + * @param bh Data sink. + */ + @Benchmark + public void fourMedian(Median4FunctionSource function, SortSource source, Blackhole bh) { + final int size = source.size(); + final Consumer fun = function.getFunction(); + for (int j = -1; ++j < size;) { + final double[] y = source.getData(j); + fun.accept(y); + bh.consume(y); + } + } + + /** + * Benchmark a pass over the entire data computing the medians of 3 data values. + * This simulates a QuickselectAdaptive step. + * + * @param function Source of the function. + * @param source Source of the data. + * @param bh Data sink. + */ + @Benchmark + public void threeMedian(Median3FunctionSource function, SortSource source, Blackhole bh) { + final int size = source.size(); + final Consumer fun = function.getFunction(); + for (int j = -1; ++j < size;) { + final double[] y = source.getData(j); + fun.accept(y); + bh.consume(y); + } + } + + /** + * Benchmark partitioning using k partition indices. + * + * @param function Source of the function. + * @param source Source of the data. + * @param bh Data sink. + */ + @Benchmark + public void partition(KFunctionSource function, KSource source, Blackhole bh) { + final int size = source.size(); + final BiFunction fun = function.getFunction(); + for (int j = -1; ++j < size;) { + // Note: This uses the indices without cloning. This is because some + // functions do not destructively modify the data. + bh.consume(fun.apply(source.getData(j), source.getIndices(j))); + } + } + + /** + * Benchmark partitioning of an interval of indices a set distance from the edge. + * This is used to benchmark the switch from quickselect partitioning to edgeselect. + * + * @param function Source of the function. + * @param source Source of the data. + * @param bh Data sink. + */ + @Benchmark + public void edgeSelect(EdgeFunctionSource function, EdgeSource source, Blackhole bh) { + final int size = source.size(); + final BiFunction fun = function.getFunction(); + for (int j = -1; ++j < size;) { + bh.consume(fun.apply(source.getData(j), source.getIndices(j))); + } + } + + /** + * Benchmark pre-processing of NaN and signed zeros (-0.0). + * + * @param function Source of the function. + * @param source Source of the data. + * @param bh Data sink. + */ + @Benchmark + public void nanZero(SortNaNFunctionSource function, SortSource source, Blackhole bh) { + final int size = source.size(); + final BiConsumer fun = function.getFunction(); + for (int j = -1; ++j < size;) { + fun.accept(source.getData(j), bh); + } + } + + /** + * Benchmark the search of an ordered set of indices. + * + * @param function Source of the search. + * @param source Source of the data. + * @return value to consume + */ + @Benchmark + public long indexSearch(IndexSearchFunctionSource function, SplitIndexSource source) { + final IndexSearchFunctionSource.SearchFunction fun = function.getFunction(); + // Ensure we have something to consume during the benchmark + long sum = 0; + for (int i = source.samples(); --i >= 0;) { + // Single point in the range + sum += fun.find(source.getIndices(i), source.getPoint(i)); + } + return sum; + } + + /** + * Benchmark the tracking of an interval of indices during a partition algorithm. + * + *

The {@link SearchableInterval} is created for the indices of interest. These are then + * cut at all points in the interval between indices to simulate a partition algorithm + * dividing the data and requiring a new interval to use in each part: + *

{@code
+     *            cut
+     *             |
+     * -------k1--------k2---------k3---- ... ---------kn--------
+     *          <-- scan previous
+     *    scan next -->
+     * }
+ * + *

Note: If a cut is made in the interval then the smallest region of data + * that was most recently partitioned was the length between the two flanking k. + * This involves a full scan (and partitioning) over the data of length (k2 - k1). + * A BitSet-type structure will require a scan over 1/64 of this length of data + * to find the next and previous index from a cut point. In practice + * the interval may be partitioned over a much larger length, e.g. (kn - k1). + * Thus the length of time for the partition algorithm is expected to be at least + * 64x the length of time for the BitSet-type scan. The disadvantage of the + * BitSet-type structure is memory consumption. For a small number of keys the + * structures that search the entire set of keys are fast enough. At very high + * density the BitSet-type structures are preferred. + * + * @param function Source of the interval. + * @param source Source of the data. + * @return value to consume + */ + @Benchmark + public long searchableIntervalNextPrevious(SearchableIntervalSource function, SplitIndexSource source) { + final int[][] indices = source.getIndices(); + final int[][] points = source.getPoints(); + // Ensure we have something to consume during the benchmark + long sum = 0; + for (int i = 0; i < indices.length; i++) { + final int[] x = indices[i]; + final int[] p = points[i]; + final SearchableInterval interval = function.create(x); + for (final int k : p) { + sum += interval.nextIndex(k); + sum += interval.previousIndex(k); + } + } + return sum; + } + + /** + * Benchmark the tracking of an interval of indices during a partition algorithm. + * + *

This is similar to + * {@link #searchableIntervalNextPrevious(SearchableIntervalSource, SplitIndexSource)}. + * It uses the {@link SearchableInterval#split(int, int, int[])} method. This requires + * {@code k} to be in an open interval. Some modes of the {@link IndexSource} do not + * ensure that {@code left < k < right} for all split points so we have to check this + * before calling the split method (it is a fixed overhead for the benchmark). + * + * @param function Source of the interval. + * @param source Source of the data. + * @return value to consume + */ + @Benchmark + public long searchableIntervalSplit(SearchableIntervalSource function, SplitIndexSource source) { + final int[][] indices = source.getIndices(); + final int[][] points = source.getPoints(); + // Ensure we have something to consume during the benchmark + long sum = 0; + final int[] bound = {0}; + for (int i = 0; i < indices.length; i++) { + final int[] x = indices[i]; + final int[] p = points[i]; + // Note: A partition algorithm would only call split if there are indices + // above and below the split point. + final SearchableInterval interval = function.create(x); + final int left = interval.left(); + final int right = interval.right(); + for (final int k : p) { + // Check k is in the open interval (left, right) + if (left < k && k < right) { + sum += interval.split(k, k, bound); + sum += bound[0]; + } + } + } + return sum; + } + + /** + * Benchmark the creation of an interval of indices for controlling a partition + * algorithm. + * + *

This baselines the + * {@link #searchableIntervalNextPrevious(SearchableIntervalSource, SplitIndexSource)} + * benchmark. For the BitSet-type structures a large overhead is the memory allocation + * to create the {@link SearchableInterval}. Note that this will be at most 1/64 the + * size of the array that is being partitioned and in practice this overhead is not + * significant. + * + * @param function Source of the interval. + * @param source Source of the data. + * @param bh Data sink. + */ + @Benchmark + public void createSearchableInterval(SearchableIntervalSource function, IndexSource source, Blackhole bh) { + final int[][] indices = source.getIndices(); + for (final int[] x : indices) { + bh.consume(function.create(x)); + } + } + + /** + * Benchmark the splitting of an interval of indices during a partition algorithm. + * + *

This is similar to + * {@link #searchableIntervalSplit(SearchableIntervalSource, SplitIndexSource)}. It + * uses the {@link UpdatingInterval#splitLeft(int, int)} method by recursive division + * of the indices. + * + * @param function Source of the interval. + * @param source Source of the data. + * @param bh Data sink. + */ + @Benchmark + public void updatingIntervalSplit(UpdatingIntervalSource function, IndexSource source, Blackhole bh) { + final int[][] indices = source.getIndices(); + final int s = source.getMinSeparation(); + for (int i = 0; i < indices.length; i++) { + split(function.create(indices[i]), s, bh); + } + } + + /** + * Recursively split the interval until the length is below the provided separation. + * Consume the interval when no more divides can occur. Simulates a single-pivot + * partition algorithm. + * + * @param interval Interval. + * @param s Minimum separation between left and right. + * @param bh Data sink. + */ + private static void split(UpdatingInterval interval, int s, Blackhole bh) { + int l = interval.left(); + final int r = interval.right(); + // Note: A partition algorithm would only call split if there are indices + // above and below the split point. + if (r - l > s) { + final int middle = (l + r) >>> 1; + // recurse left + split(interval.splitLeft(middle, middle), s, bh); + // continue on right side + l = interval.left(); + } + bh.consume(interval); + } +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/Sorting.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/Sorting.java new file mode 100644 index 000000000..3c2dfe3d7 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/Sorting.java @@ -0,0 +1,2541 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.util.Arrays; + +/** + * Support class for sorting arrays. + * + *

Optimal sorting networks are used for small fixed size array sorting. + * + *

Note: Requires that the floating-point data contains no NaN values; sorting + * does not respect the order of signed zeros imposed by {@link Double#compare(double, double)}. + * + * @see Sorting network (Wikipedia) + * @see Sorting Networks (Bert Dobbelaere) + * @since 1.2 + */ +final class Sorting { + /** The upper threshold to use a modified insertion sort to find unique indices. */ + private static final int UNIQUE_INSERTION_SORT = 20; + + /** No instances. */ + private Sorting() {} + + /** + * Sorts an array using an insertion sort. + * + *

This method is fast up to approximately 40 - 80 values. + * + *

The {@code internal} flag indicates that the value at {@code data[begin - 1]} + * is sorted. + * + * @param data Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param internal Internal flag. + */ + static void sort(double[] data, int left, int right, boolean internal) { + if (internal) { + // Assume data[begin - 1] is a pivot and acts as a sentinal on the range. + // => no requirement to check j >= left. + + // Note: + // Benchmarking fails to show that this is faster + // even though it is the same method with fewer instructions. + // There may be an issue with the benchmarking data, or noise in the timings. + + // There are also paired-insertion sort methods for internal regions + // which benchmark as slower on random data. + // On structured data with many ascending runs they are faster. + + for (int i = left; ++i <= right;) { + final double v = data[i]; + // Move preceding higher elements above (if required) + if (v < data[i - 1]) { + int j = i; + while (v < data[--j]) { + data[j + 1] = data[j]; + } + data[j + 1] = v; + } + } + + } else { + for (int i = left; ++i <= right;) { + final double v = data[i]; + // Move preceding higher elements above (if required) + if (v < data[i - 1]) { + int j = i; + while (--j >= left && v < data[j]) { + data[j + 1] = data[j]; + } + data[j + 1] = v; + } + } + } + } + + /** + * Sorts an array using an insertion sort. + * + *

This method is fast up to approximately 40 - 80 values. + * + * @param data Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + static void sort(double[] data, int left, int right) { + for (int i = left; ++i <= right;) { + final double v = data[i]; + // Move preceding higher elements above (if required) + if (v < data[i - 1]) { + int j = i; + while (--j >= left && v < data[j]) { + data[j + 1] = data[j]; + } + data[j + 1] = v; + } + } + } + + /** + * Sorts an array using an insertion sort. + * + *

This method is fast up to approximately 40 - 80 values. + * + * @param data Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + static void sortb(double[] data, int left, int right) { + for (int i = left; ++i <= right;) { + final double v = data[i]; + // Move preceding higher elements above. + // This method always uses a loop. It benchmarks slower than the + // method that uses an if statement to check the loop is required. + int j = i; + while (--j >= left && v < data[j]) { + data[j + 1] = data[j]; + } + data[j + 1] = v; + } + } + + /** + * Sorts an array using a paired insertion sort. + * + *

Warning: It is assumed that the value at {@code data[begin - 1]} is sorted. + * + * @param data Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + static void sortPairedInternal1(double[] data, int left, int right) { + // Assume data[begin - 1] is a pivot and acts as a sentinal on the range. + // => no requirement to check j >= left. + + // Paired insertion sort. Move largest of two elements down the array. + // When inserted move the smallest of the two elements down the rest of the array. + + // Pairs require an even length so start at left for even or left + 1 for odd. + // This will do nothing when right <= left. + + // Using one index which requires i += 2. + for (int i = left + ((right - left + 1) & 0x1); i < right; i += 2) { + double v1 = data[i]; + double v2 = data[i + 1]; + // Sort the pair + if (v2 < v1) { + v1 = v2; + v2 = data[i]; + } + // Move preceding higher elements above the largest value + int j = i; + while (v2 < data[--j]) { + data[j + 2] = data[j]; + } + // Insert at j + 2. Update j for the next scan down. + data[++j + 1] = v2; + // Move preceding higher elements above the smallest value + while (v1 < data[--j]) { + data[j + 1] = data[j]; + } + data[j + 1] = v1; + } + } + + /** + * Sorts an array using a paired insertion sort. + * + *

Warning: It is assumed that the value at {@code data[begin - 1]} is sorted. + * + * @param data Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + static void sortPairedInternal2(double[] data, int left, int right) { + // Assume data[begin - 1] is a pivot and acts as a sentinal on the range. + // => no requirement to check j >= left. + + // Paired insertion sort. Move largest of two elements down the array. + // When inserted move the smallest of the two elements down the rest of the array. + + // Pairs require an even length so start at left for even or left + 1 for odd. + // This will do nothing when right <= left. + + // Use pair (i, j) + for (int i = left + ((right - left + 1) & 0x1), j = i; ++j <= right; i = ++j) { + double v1 = data[i]; + double v2 = data[j]; + // Sort the pair + if (v2 < v1) { + v1 = v2; + v2 = data[i]; + } + // Move preceding higher elements above the largest value + while (v2 < data[--i]) { + data[i + 2] = data[i]; + } + // Insert at i + 2. Update i for the next scan down. + data[++i + 1] = v2; + // Move preceding higher elements above the smallest value + while (v1 < data[--i]) { + data[i + 1] = data[i]; + } + data[i + 1] = v1; + } + } + + /** + * Sorts an array using a paired insertion sort. + * + *

Warning: It is assumed that the value at {@code data[begin - 1]} is sorted. + * + * @param data Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + static void sortPairedInternal3(double[] data, int left, int right) { + // Assume data[begin - 1] is a pivot and acts as a sentinal on the range. + // => no requirement to check j >= left. + + // Paired insertion sort. Move largest of two elements down the array. + // When inserted move the smallest of the two elements down the rest of the array. + + // Pairs require an even length so start at left for even or left + 1 for odd. + // This will do nothing when right <= left. + + // As above but only move if required + for (int i = left + ((right - left + 1) & 0x1), j = i; ++j <= right; i = ++j) { + double v1 = data[i]; + double v2 = data[j]; + // Sort the pair + if (v2 < v1) { + v1 = v2; + v2 = data[i]; + // In the event of no move of v2 + data[j] = v2; + } + // Move preceding higher elements above the largest value (if required) + if (v2 < data[i - 1]) { + while (v2 < data[--i]) { + data[i + 2] = data[i]; + } + // Insert at i + 2. Update i for the next scan down. + data[++i + 1] = v2; + } + // Move preceding higher elements above the smallest value (if required) + if (v1 < data[i - 1]) { + while (v1 < data[--i]) { + data[i + 1] = data[i]; + } + // Insert at i+1 + i++; + } + // Always write v1 as v2 may have moved down + data[i] = v1; + } + } + + /** + * Sorts an array using a paired insertion sort. + * + *

Warning: It is assumed that the value at {@code data[begin - 1]} is sorted. + * + * @param data Data array. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + */ + static void sortPairedInternal4(double[] data, int left, int right) { + // Assume data[begin - 1] is a pivot and acts as a sentinal on the range. + // => no requirement to check j >= left. + + // Paired insertion sort. Move largest of two elements down the array. + // When inserted move the smallest of the two elements down the rest of the array. + + // Pairs require an even length so start at left for even or left + 1 for odd. + // This will do nothing when right <= left. + + // As above but only move if required + for (int i = left + ((right - left + 1) & 0x1), j = i; ++j <= right; i = ++j) { + double v1 = data[i]; + double v2 = data[j]; + // Sort the pair + if (v2 < v1) { + v1 = v2; + v2 = data[i]; + // In the event of no moves + data[j] = v2; + data[i] = v1; + } + // Move preceding higher elements (if required, only test the smallest) + if (v1 < data[i - 1]) { + // Move preceding higher elements above the largest value + while (v2 < data[--i]) { + data[i + 2] = data[i]; + } + // Insert at i + 2. Update i for the next scan down. + data[++i + 1] = v2; + // Move preceding higher elements above the smallest value + while (v1 < data[--i]) { + data[i + 1] = data[i]; + } + data[i + 1] = v1; + } + } + } + + /** + * Place the minimum of 3 elements in {@code a}; and the larger + * two elements in {@code b, c}. + * + * @param x Values + * @param a Index. + * @param b Index. + * @param c Index. + */ + static void min3(double[] x, int a, int b, int c) { + if (x[b] < x[a]) { + final double v = x[b]; + x[b] = x[a]; + x[a] = v; + } + if (x[c] < x[a]) { + final double v = x[c]; + x[c] = x[a]; + x[a] = v; + } + } + + /** + * Place the maximum of 3 elements in {@code c}; and the smaller + * two elements in {@code a, b}. + * + * @param x Values + * @param a Index. + * @param b Index. + * @param c Index. + */ + static void max3(double[] x, int a, int b, int c) { + if (x[c] < x[b]) { + final double u = x[c]; + x[c] = x[b]; + x[b] = u; + } + if (x[c] < x[a]) { + final double v = x[c]; + x[c] = x[a]; + x[a] = v; + } + } + + /** + * Sorts the given indices in an array. + * + *

Assumes all indices are valid and distinct. + * + *

Data are arranged such that: + *

{@code
+     * i0 != i1 != i2
+     * data[i0] < data[i1] < data[i2]
+     * }
+ * + *

If indices are duplicated elements will not be correctly ordered. + * However in this case data will contain the same values and may be partially ordered. + * + * @param data Data array. + * @param i0 Index. + * @param i1 Index. + * @param i2 Index. + */ + static void sort3(double[] data, int i0, int i1, int i2) { + // Decision tree avoiding swaps: + // Order [(0,2)] + // Move point 1 above point 2 or below point 0 + final double x = data[i0]; + final double y = data[i1]; + final double z = data[i2]; + if (z < x) { + if (y < z) { + data[i0] = y; + data[i1] = z; + data[i2] = x; + return; + } + if (x < y) { + data[i0] = z; + data[i1] = x; + data[i2] = y; + return; + } + // z < y < z + data[i0] = z; + data[i2] = x; + return; + } + if (y < x) { + // y < x < z + data[i0] = y; + data[i1] = x; + return; + } + if (z < y) { + // x < z < y + data[i1] = z; + data[i2] = y; + } + // x < y < z + } + + /** + * Sorts the given indices in an array. + * + *

Note: Requires that the range contains no NaN values. It does not respect the + * order of signed zeros. + * + *

Assumes all indices are valid and distinct. + * + *

Data are arranged such that: + *

{@code
+     * a != b != c
+     * data[a] < data[b] < data[c]
+     * }
+ * + *

If indices are duplicated elements will not be correctly ordered. + * However in this case data will contain the same values and may be partially ordered. + * + * @param data Data array. + * @param i0 Index. + * @param i1 Index. + * @param i2 Index. + */ + static void sort3b(double[] data, int i0, int i1, int i2) { + // Order pair: + //[(0,2)] + // Move point 1 above point 2 or below point 0 + if (data[i2] < data[i0]) { + final double v = data[i2]; + data[i2] = data[i0]; + data[i0] = v; + } + if (data[i2] < data[i1]) { + final double v = data[i2]; + data[i2] = data[i1]; + data[i1] = v; + } else if (data[i1] < data[i0]) { + final double v = data[i1]; + data[i1] = data[i0]; + data[i0] = v; + } + } + + /** + * Sorts the given indices in an array. + * + *

Note: Requires that the range contains no NaN values. It does not respect the + * order of signed zeros. + * + *

Assumes all indices are valid and distinct. + * + *

Data are arranged such that: + * + *

{@code
+     * a != b != c
+     * data[a] < data[b] < data[c]
+     * }
+ * + *

If indices are duplicated elements will not be correctly ordered. + * However in this case data will contain the same values and may be partially + * ordered. + * + * @param data Data array. + * @param i0 Index. + * @param i1 Index. + * @param i2 Index. + */ + static void sort3c(double[] data, int i0, int i1, int i2) { + // Order pairs: + // [(0,2)] + // [(0,1)] + // [(1,2)] + if (data[i2] < data[i0]) { + final double v = data[i2]; + data[i2] = data[i0]; + data[i0] = v; + } + if (data[i1] < data[i0]) { + final double v = data[i1]; + data[i1] = data[i0]; + data[i0] = v; + } + if (data[i2] < data[i1]) { + final double v = data[i2]; + data[i2] = data[i1]; + data[i1] = v; + } + } + + /** + * Sorts the given indices in an array using an insertion sort. + * + *

Assumes all indices are valid and distinct. + * + *

Data are arranged such that: + *

{@code
+     * i0 != i1 != i2 != i3
+     * data[i0] < data[i1] < data[i2] < data[i3]
+     * }
+ * + *

If indices are duplicated elements will not be correctly ordered. + * However in this case data will contain the same values and may be partially ordered. + * + * @param data Data array. + * @param i0 Index. + * @param i1 Index. + * @param i2 Index. + * @param i3 Index. + */ + static void sort4(double[] data, int i0, int i1, int i2, int i3) { + // Uses an optimal sorting network from Knuth's Art of Computer Programming. + // 5 comparisons. + // Order pairs: + //[(0,2),(1,3)] + //[(0,1),(2,3)] + //[(1,2)] + if (data[i3] < data[i1]) { + final double u = data[i3]; + data[i3] = data[i1]; + data[i1] = u; + } + if (data[i2] < data[i0]) { + final double v = data[i2]; + data[i2] = data[i0]; + data[i0] = v; + } + + if (data[i3] < data[i2]) { + final double u = data[i3]; + data[i3] = data[i2]; + data[i2] = u; + } + if (data[i1] < data[i0]) { + final double v = data[i1]; + data[i1] = data[i0]; + data[i0] = v; + } + + if (data[i2] < data[i1]) { + final double u = data[i2]; + data[i2] = data[i1]; + data[i1] = u; + } + } + + /** + * Sorts the given indices in an array. + * + *

Assumes all indices are valid and distinct. + * + *

Data are arranged such that: + *

{@code
+     * i0 != i1 != i2 != i3 != i4
+     * data[i0] < data[i1] < data[i2] < data[i3] < data[i4]
+     * }
+ * + *

If indices are duplicated elements will not be correctly ordered. + * However in this case data will contain the same values and may be partially ordered. + * + * @param data Data array. + * @param i0 Index. + * @param i1 Index. + * @param i2 Index. + * @param i3 Index. + * @param i4 Index. + */ + static void sort5(double[] data, int i0, int i1, int i2, int i3, int i4) { + // Uses an optimal sorting network from Knuth's Art of Computer Programming. + // 9 comparisons. + // Order pairs: + // [(0,3),(1,4)] + // [(0,2),(1,3)] + // [(0,1),(2,4)] + // [(1,2),(3,4)] + // [(2,3)] + if (data[i4] < data[i1]) { + final double u = data[i4]; + data[i4] = data[i1]; + data[i1] = u; + } + if (data[i3] < data[i0]) { + final double v = data[i3]; + data[i3] = data[i0]; + data[i0] = v; + } + + if (data[i3] < data[i1]) { + final double u = data[i3]; + data[i3] = data[i1]; + data[i1] = u; + } + if (data[i2] < data[i0]) { + final double v = data[i2]; + data[i2] = data[i0]; + data[i0] = v; + } + + if (data[i4] < data[i2]) { + final double u = data[i4]; + data[i4] = data[i2]; + data[i2] = u; + } + if (data[i1] < data[i0]) { + final double v = data[i1]; + data[i1] = data[i0]; + data[i0] = v; + } + + if (data[i4] < data[i3]) { + final double u = data[i4]; + data[i4] = data[i3]; + data[i3] = u; + } + if (data[i2] < data[i1]) { + final double v = data[i2]; + data[i2] = data[i1]; + data[i1] = v; + } + + if (data[i3] < data[i2]) { + final double u = data[i3]; + data[i3] = data[i2]; + data[i2] = u; + } + } + + /** + * Sorts the given indices in an array. + * + *

Assumes all indices are valid and distinct. + * + *

Data are arranged such that: + *

{@code
+     * i0 != i1 != i2 != i3 != i4
+     * data[i0] < data[i1] < data[i2] < data[i3] < data[i4]
+     * }
+ * + *

If indices are duplicated elements will not be correctly ordered. + * However in this case data will contain the same values and may be partially ordered. + * + * @param data Data array. + * @param i0 Index. + * @param i1 Index. + * @param i2 Index. + * @param i3 Index. + * @param i4 Index. + */ + static void sort5b(double[] data, int i0, int i1, int i2, int i3, int i4) { + // Sorting network for size 5 is 9 comparisons (see sort5). + // Sorting network for size 4 is 5 comparisons + 2 or 3 extra. + // This method benchmarks marginally faster (~1%) than the sorting network of size 5 + // on length 5 data. When the data is larger and the indices are uniformly + // spread across the range, the sorting network is faster. + + // Order quadruple: + //[(0,1,3,4)] + // Move point 2 above points 3,4 or below points 0,1 + sort4(data, i0, i1, i3, i4); + final double u = data[i2]; + if (u > data[i3]) { + data[i2] = data[i3]; + data[i3] = u; + if (u > data[i4]) { + data[i3] = data[i4]; + data[i4] = u; + } + } else if (u < data[i1]) { + data[i2] = data[i1]; + data[i1] = u; + if (u < data[i0]) { + data[i1] = data[i0]; + data[i0] = u; + } + } + } + + /** + * Sorts the given indices in an array. + * + *

Assumes all indices are valid and distinct. + * + *

Data are arranged such that: + *

{@code
+     * i0 != i1 != i2 != i3 != i4
+     * data[i0] < data[i1] < data[i2] < data[i3] < data[i4]
+     * }
+ * + *

If indices are duplicated elements will not be correctly ordered. + * However in this case data will contain the same values and may be partially ordered. + * + * @param data Data array. + * @param i0 Index. + * @param i1 Index. + * @param i2 Index. + * @param i3 Index. + * @param i4 Index. + */ + static void sort5c(double[] data, int i0, int i1, int i2, int i3, int i4) { + // Sorting of 5 elements in optimum 7 comparisons. + // Code adapted from Raphael, Computer Science Stack Exchange. + // https://cs.stackexchange.com/a/44982 + // https://gist.github.com/akerbos/5acb345ff3d41bc888c4 + + // 1. Sort the first two pairs. + if (data[i1] < data[i0]) { + final double u = data[i1]; + data[i1] = data[i0]; + data[i0] = u; + } + if (data[i3] < data[i2]) { + final double v = data[i3]; + data[i3] = data[i2]; + data[i2] = v; + } + + // 2. Order the pairs w.r.t. their respective larger element. + // Call the result [a,b,c,d,e]; we know a x[c]) { + final double u = x[c]; + x[c] = x[b]; + x[b] = u; + } else if (x[c] > x[d]) { + // a--c + // b--d + final double xc = x[d]; + x[d] = x[c]; + x[c] = xc; + // a--d + // b--c + if (x[a] > xc) { + x[c] = x[a]; + // Move a pair to maintain the sorted order + //x[a] = xc; + x[a] = x[b]; + x[b] = xc; + } + } + } + + /** + * Place the upper median of 4 elements in {@code c}; the smaller two elements in + * {@code a,b}; and the larger element in {@code d}. + * + * @param x Values + * @param a Index. + * @param b Index. + * @param c Index. + * @param d Index. + */ + static void upperMedian4c(double[] x, int a, int b, int c, int d) { + // 4 comparisons + if (x[d] < x[b]) { + final double u = x[d]; + x[d] = x[b]; + x[b] = u; + } + if (x[c] < x[a]) { + final double v = x[c]; + x[c] = x[a]; + x[a] = v; + } + // a--c + // b--d + if (x[d] < x[c]) { + final double v = x[a]; + final double u = x[c]; + x[a] = x[b]; + x[c] = x[d]; + x[b] = v; + x[d] = u; + } + // a--c + // b--d + if (x[c] < x[b]) { + final double u = x[c]; + x[c] = x[b]; + x[b] = u; + } + } + + /** + * Place the upper median of 4 elements in {@code c}; the smaller two elements in + * {@code a,b}; and the larger element in {@code d}. + * + * @param x Values + * @param a Index. + * @param b Index. + * @param c Index. + * @param d Index. + */ + static void upperMedian4d(double[] x, int a, int b, int c, int d) { + // 4 comparisons + if (x[d] < x[a]) { + final double u = x[d]; + x[d] = x[a]; + x[a] = u; + } + if (x[c] < x[b]) { + final double v = x[c]; + x[c] = x[b]; + x[b] = v; + } + // a--d + // b--c + if (x[d] < x[c]) { + final double xc = x[d]; + x[d] = x[c]; + x[c] = xc; + // a--c + // b--d + if (xc < x[b]) { + x[c] = x[b]; + x[b] = xc; + // fully sorted here + } + // else full sort requires a:b ordering + // Not fully sorted for 6 of 24 permutations + } else if (x[c] < x[a]) { + // a--d + // b--c + final double v = x[a]; + // Do a full sort for 1 additional swap + x[a] = x[b]; + x[b] = x[c]; + x[c] = v; + // minimum swaps to put the lower median at b + //x[d] = x[b]; + //x[b] = v; + } + } + + /** + * Return the median of a continuous block of 5 elements. + * Data may be partially reordered. + * + * @param a Values + * @param i1 First index. + * @return the median index + */ + static int median5(double[] a, int i1) { + final int i2 = i1 + 1; + final int i3 = i1 + 2; + final int i4 = i1 + 3; + final int i5 = i1 + 4; + // 6 comparison decision tree + // Possible median in parentheses + // (12345) + if (a[i2] < a[i1]) { + final double v = a[i2]; + a[i2] = a[i1]; + a[i1] = v; + } + if (a[i4] < a[i3]) { + final double v = a[i4]; + a[i4] = a[i3]; + a[i3] = v; + } + // (1<2 3<4 5) + if (a[i1] < a[i3]) { + // 1(2 3<4 5) + if (a[i5] < a[i2]) { + final double v = a[i5]; + a[i5] = a[i2]; + a[i2] = v; + } + // 1(2<5 3<4) + if (a[i2] < a[i3]) { + // 1,2(5 3<4) + return a[i5] < a[i3] ? i5 : i3; + } + // 1,3(2<5 4) + return a[i2] < a[i4] ? i2 : i4; + } + // 3(1<2 4 5) + if (a[i5] < a[i4]) { + final double v = a[i5]; + a[i5] = a[i4]; + a[i4] = v; + } + // 3(1<2 4<5) + if (a[i1] < a[i4]) { + // 3,1(2 4<5) + return a[i2] < a[i4] ? i2 : i4; + } + // 3,4(1<2 5) + return a[i1] < a[i5] ? i1 : i5; + } + + /** + * Return the median of 5 elements. Data may be partially reordered. + * + * @param a Values + * @param i1 Index. + * @param i2 Index. + * @param i3 Index. + * @param i4 Index. + * @param i5 Index. + * @return the median index + */ + static int median5(double[] a, int i1, int i2, int i3, int i4, int i5) { + // 6 comparison decision tree + // Possible median in parentheses + // (12345) + if (a[i2] < a[i1]) { + final double v = a[i2]; + a[i2] = a[i1]; + a[i1] = v; + } + if (a[i4] < a[i3]) { + final double v = a[i4]; + a[i4] = a[i3]; + a[i3] = v; + } + // (1<2 3<4 5) + if (a[i1] < a[i3]) { + // 1(2 3<4 5) + if (a[i5] < a[i2]) { + final double v = a[i5]; + a[i5] = a[i2]; + a[i2] = v; + } + // 1(2<5 3<4) + if (a[i2] < a[i3]) { + // 1,2(5 3<4) + return a[i5] < a[i3] ? i5 : i3; + } + // 1,3(2<5 4) + return a[i2] < a[i4] ? i2 : i4; + } + // 3(1<2 4 5) + if (a[i5] < a[i4]) { + final double v = a[i5]; + a[i5] = a[i4]; + a[i4] = v; + } + // 3(1<2 4<5) + if (a[i1] < a[i4]) { + // 3,1(2 4<5) + return a[i2] < a[i4] ? i2 : i4; + } + // 3,4(1<2 5) + return a[i1] < a[i5] ? i1 : i5; + } + + /** + * Return the median of a continuous block of 5 elements. + * Data may be partially reordered. + * + * @param a Values + * @param i1 First index. + * @return the median index + */ + static int median5b(double[] a, int i1) { + final int i2 = i1 + 1; + final int i3 = i1 + 2; + final int i4 = i1 + 3; + final int i5 = i1 + 4; + // 6 comparison decision tree + // Possible median in parentheses + // (12345) + if (a[i2] < a[i1]) { + final double v = a[i2]; + a[i2] = a[i1]; + a[i1] = v; + } + if (a[i4] < a[i3]) { + final double v = a[i4]; + a[i4] = a[i3]; + a[i3] = v; + } + // (1<2 3<4 5) + if (a[i1] < a[i3]) { + // 1(2 3<4 5) + if (a[i5] < a[i2]) { + // 1(5<2 3<4) + if (a[i5] < a[i3]) { + // 1,5(2 3<4) + return a[i2] < a[i3] ? i2 : i3; + } + // 1,3(2<5 4) + return a[i5] < a[i4] ? i5 : i4; + } + // 1(2<5 3<4) + if (a[i2] < a[i3]) { + // 1,2(5 3<4) + return a[i5] < a[i3] ? i5 : i3; + } + // 1,3(2<5 4) + return a[i2] < a[i4] ? i2 : i4; + } + // 3(1<2 4 5) + if (a[i5] < a[i4]) { + // 3(1<2 5<4) + if (a[i1] < a[i5]) { + // 3,1(2 5<4) + return a[i2] < a[i5] ? i2 : i5; + } + // 3,5(1<2 4) + return a[i1] < a[i4] ? i1 : i4; + } + // 3(1<2 4<5) + if (a[i1] < a[i4]) { + // 3,1(2 4<5) + return a[i2] < a[i4] ? i2 : i4; + } + // 3,4(1<2 5) + return a[i1] < a[i5] ? i1 : i5; + } + + /** + * Return the median of a continuous block of 5 elements. + * Data may be partially reordered. + * + * @param a Values + * @param i1 First index. + * @return the median index + */ + static int median5c(double[] a, int i1) { + // Sort 4 + Sorting.sort4(a, i1, i1 + 1, i1 + 3, i1 + 4); + // median of [e-4, e-3, e-2] + int m = i1 + 2; + if (a[m] < a[m - 1]) { + --m; + } else if (a[m] > a[m + 1]) { + ++m; + } + return m; + } + + /** + * Place the median of 5 elements in {@code c}; the smaller 2 elements in + * {@code a, b}; and the larger two elements in {@code d, e}. + * + * @param x Values + * @param a Index. + * @param b Index. + * @param c Index. + * @param d Index. + * @param e Index. + */ + static void median5d(double[] x, int a, int b, int c, int d, int e) { + // 6 comparison decision tree from: + // Alexandrescu (2016) Fast Deterministic Selection, arXiv:1606.00484, Algorithm 4 + // https://arxiv.org/abs/1606.00484 + if (x[c] < x[a]) { + final double v = x[c]; + x[c] = x[a]; + x[a] = v; + } + if (x[d] < x[b]) { + final double u = x[d]; + x[d] = x[b]; + x[b] = u; + } + if (x[d] < x[c]) { + final double v = x[d]; + x[d] = x[c]; + x[c] = v; + final double u = x[b]; + x[b] = x[a]; + x[a] = u; + } + if (x[e] < x[b]) { + final double v = x[e]; + x[e] = x[b]; + x[b] = v; + } + if (x[e] < x[c]) { + final double u = x[e]; + x[e] = x[c]; + x[c] = u; + if (u < x[a]) { + x[c] = x[a]; + x[a] = u; + } + } else { + if (x[c] < x[b]) { + final double u = x[c]; + x[c] = x[b]; + x[b] = u; + } + } + } + + /** + * Sorts the given indices in an array. + * + *

Assumes all indices are valid and distinct. + * + *

Data are arranged such that: + *

{@code
+     * i0 != i1 != i2 != i3 != i4 != i5 != i6
+     * data[i0] < data[i1] < data[i2] < data[i3] < data[i4] < data[i5] < data[i6]
+     * }
+ * + *

If indices are duplicated elements will not be correctly ordered. + * However in this case data will contain the same values and may be partially ordered. + * + * @param data Data array. + * @param i0 Index. + * @param i1 Index. + * @param i2 Index. + * @param i3 Index. + * @param i4 Index. + * @param i5 Index. + * @param i6 Index. + */ + static void sort7(double[] data, int i0, int i1, int i2, int i3, int i4, int i5, int i6) { + // Uses an optimal sorting network from Knuth's Art of Computer Programming. + // 16 comparisons. + // Order pairs: + //[(0,6),(2,3),(4,5)] + //[(0,2),(1,4),(3,6)] + //[(0,1),(2,5),(3,4)] + //[(1,2),(4,6)] + //[(2,3),(4,5)] + //[(1,2),(3,4),(5,6)] + if (data[i5] < data[i4]) { + final double u = data[i5]; + data[i5] = data[i4]; + data[i4] = u; + } + if (data[i3] < data[i2]) { + final double v = data[i3]; + data[i3] = data[i2]; + data[i2] = v; + } + if (data[i6] < data[i0]) { + final double w = data[i6]; + data[i6] = data[i0]; + data[i0] = w; + } + + if (data[i6] < data[i3]) { + final double u = data[i6]; + data[i6] = data[i3]; + data[i3] = u; + } + if (data[i4] < data[i1]) { + final double v = data[i4]; + data[i4] = data[i1]; + data[i1] = v; + } + if (data[i2] < data[i0]) { + final double w = data[i2]; + data[i2] = data[i0]; + data[i0] = w; + } + + if (data[i4] < data[i3]) { + final double u = data[i4]; + data[i4] = data[i3]; + data[i3] = u; + } + if (data[i5] < data[i2]) { + final double v = data[i5]; + data[i5] = data[i2]; + data[i2] = v; + } + if (data[i1] < data[i0]) { + final double w = data[i1]; + data[i1] = data[i0]; + data[i0] = w; + } + + if (data[i6] < data[i4]) { + final double u = data[i6]; + data[i6] = data[i4]; + data[i4] = u; + } + if (data[i2] < data[i1]) { + final double v = data[i2]; + data[i2] = data[i1]; + data[i1] = v; + } + + if (data[i5] < data[i4]) { + final double u = data[i5]; + data[i5] = data[i4]; + data[i4] = u; + } + if (data[i3] < data[i2]) { + final double v = data[i3]; + data[i3] = data[i2]; + data[i2] = v; + } + + if (data[i6] < data[i5]) { + final double u = data[i6]; + data[i6] = data[i5]; + data[i5] = u; + } + if (data[i4] < data[i3]) { + final double v = data[i4]; + data[i4] = data[i3]; + data[i3] = v; + } + if (data[i2] < data[i1]) { + final double w = data[i2]; + data[i2] = data[i1]; + data[i1] = w; + } + } + + /** + * Sorts the given indices in an array. + * + *

Assumes all indices are valid and distinct. + * + *

Data are arranged such that: + *

{@code
+     * i0 != i1 != i2 != i3 != i4 != i5 != i6 != i7
+     * data[i0] < data[i1] < data[i2] < data[i3] < data[i4] < data[i5] < data[i6] < data[i7]
+     * }
+ * + *

If indices are duplicated elements will not be correctly ordered. + * However in this case data will contain the same values and may be partially ordered. + * + * @param data Data array. + * @param i0 Index. + * @param i1 Index. + * @param i2 Index. + * @param i3 Index. + * @param i4 Index. + * @param i5 Index. + * @param i6 Index. + * @param i7 Index. + */ + static void sort8(double[] data, int i0, int i1, int i2, int i3, int i4, int i5, int i6, int i7) { + // Uses an optimal sorting network from Knuth's Art of Computer Programming. + // 19 comparisons. + // Order pairs: + //[(0,2),(1,3),(4,6),(5,7)] + //[(0,4),(1,5),(2,6),(3,7)] + //[(0,1),(2,3),(4,5),(6,7)] + //[(2,4),(3,5)] + //[(1,4),(3,6)] + //[(1,2),(3,4),(5,6)] + if (data[i7] < data[i5]) { + final double u = data[i7]; + data[i7] = data[i5]; + data[i5] = u; + } + if (data[i6] < data[i4]) { + final double v = data[i6]; + data[i6] = data[i4]; + data[i4] = v; + } + if (data[i3] < data[i1]) { + final double w = data[i3]; + data[i3] = data[i1]; + data[i1] = w; + } + if (data[i2] < data[i0]) { + final double x = data[i2]; + data[i2] = data[i0]; + data[i0] = x; + } + + if (data[i7] < data[i3]) { + final double u = data[i7]; + data[i7] = data[i3]; + data[i3] = u; + } + if (data[i6] < data[i2]) { + final double v = data[i6]; + data[i6] = data[i2]; + data[i2] = v; + } + if (data[i5] < data[i1]) { + final double w = data[i5]; + data[i5] = data[i1]; + data[i1] = w; + } + if (data[i4] < data[i0]) { + final double x = data[i4]; + data[i4] = data[i0]; + data[i0] = x; + } + + if (data[i7] < data[i6]) { + final double u = data[i7]; + data[i7] = data[i6]; + data[i6] = u; + } + if (data[i5] < data[i4]) { + final double v = data[i5]; + data[i5] = data[i4]; + data[i4] = v; + } + if (data[i3] < data[i2]) { + final double w = data[i3]; + data[i3] = data[i2]; + data[i2] = w; + } + if (data[i1] < data[i0]) { + final double x = data[i1]; + data[i1] = data[i0]; + data[i0] = x; + } + + if (data[i5] < data[i3]) { + final double u = data[i5]; + data[i5] = data[i3]; + data[i3] = u; + } + if (data[i4] < data[i2]) { + final double v = data[i4]; + data[i4] = data[i2]; + data[i2] = v; + } + + if (data[i6] < data[i3]) { + final double u = data[i6]; + data[i6] = data[i3]; + data[i3] = u; + } + if (data[i4] < data[i1]) { + final double v = data[i4]; + data[i4] = data[i1]; + data[i1] = v; + } + + if (data[i6] < data[i5]) { + final double u = data[i6]; + data[i6] = data[i5]; + data[i5] = u; + } + if (data[i4] < data[i3]) { + final double v = data[i4]; + data[i4] = data[i3]; + data[i3] = v; + } + if (data[i2] < data[i1]) { + final double w = data[i2]; + data[i2] = data[i1]; + data[i1] = w; + } + } + + + /** + * Sorts the given indices in an array. + * + *

Assumes all indices are valid and distinct. + * + *

Data are arranged such that: + *

{@code
+     * data[i] <= data[i + i] <= data[i + 2] ...
+     * }
+ * + *

If indices are duplicated elements will not be correctly ordered. + * However in this case data will contain the same values and may be partially ordered. + * + * @param data Data array. + * @param i0 Index. + * @param i1 Index. + * @param i2 Index. + * @param i3 Index. + * @param i4 Index. + * @param i5 Index. + * @param i6 Index. + * @param i7 Index. + * @param i8 Index. + * @param i9 Index. + * @param i10 Index. + */ + static void sort11(double[] data, int i0, int i1, int i2, int i3, int i4, int i5, int i6, int i7, + int i8, int i9, int i10) { + // Uses an optimal sorting network from Knuth's Art of Computer Programming. + // 35 comparisons. + // Order pairs: + //[(0,9),(1,6),(2,4),(3,7),(5,8)] + //[(0,1),(3,5),(4,10),(6,9),(7,8)] + //[(1,3),(2,5),(4,7),(8,10)] + //[(0,4),(1,2),(3,7),(5,9),(6,8)] + //[(0,1),(2,6),(4,5),(7,8),(9,10)] + //[(2,4),(3,6),(5,7),(8,9)] + //[(1,2),(3,4),(5,6),(7,8)] + //[(2,3),(4,5),(6,7)] + if (data[i8] < data[i5]) { + final double u = data[i8]; + data[i8] = data[i5]; + data[i5] = u; + } + if (data[i7] < data[i3]) { + final double v = data[i7]; + data[i7] = data[i3]; + data[i3] = v; + } + if (data[i4] < data[i2]) { + final double w = data[i4]; + data[i4] = data[i2]; + data[i2] = w; + } + if (data[i6] < data[i1]) { + final double x = data[i6]; + data[i6] = data[i1]; + data[i1] = x; + } + if (data[i9] < data[i0]) { + final double y = data[i9]; + data[i9] = data[i0]; + data[i0] = y; + } + + if (data[i8] < data[i7]) { + final double u = data[i8]; + data[i8] = data[i7]; + data[i7] = u; + } + if (data[i9] < data[i6]) { + final double v = data[i9]; + data[i9] = data[i6]; + data[i6] = v; + } + if (data[i10] < data[i4]) { + final double w = data[i10]; + data[i10] = data[i4]; + data[i4] = w; + } + if (data[i5] < data[i3]) { + final double x = data[i5]; + data[i5] = data[i3]; + data[i3] = x; + } + if (data[i1] < data[i0]) { + final double y = data[i1]; + data[i1] = data[i0]; + data[i0] = y; + } + + if (data[i10] < data[i8]) { + final double u = data[i10]; + data[i10] = data[i8]; + data[i8] = u; + } + if (data[i7] < data[i4]) { + final double v = data[i7]; + data[i7] = data[i4]; + data[i4] = v; + } + if (data[i5] < data[i2]) { + final double w = data[i5]; + data[i5] = data[i2]; + data[i2] = w; + } + if (data[i3] < data[i1]) { + final double x = data[i3]; + data[i3] = data[i1]; + data[i1] = x; + } + + if (data[i8] < data[i6]) { + final double u = data[i8]; + data[i8] = data[i6]; + data[i6] = u; + } + if (data[i9] < data[i5]) { + final double v = data[i9]; + data[i9] = data[i5]; + data[i5] = v; + } + if (data[i7] < data[i3]) { + final double w = data[i7]; + data[i7] = data[i3]; + data[i3] = w; + } + if (data[i2] < data[i1]) { + final double x = data[i2]; + data[i2] = data[i1]; + data[i1] = x; + } + if (data[i4] < data[i0]) { + final double y = data[i4]; + data[i4] = data[i0]; + data[i0] = y; + } + + if (data[i10] < data[i9]) { + final double u = data[i10]; + data[i10] = data[i9]; + data[i9] = u; + } + if (data[i8] < data[i7]) { + final double v = data[i8]; + data[i8] = data[i7]; + data[i7] = v; + } + if (data[i5] < data[i4]) { + final double w = data[i5]; + data[i5] = data[i4]; + data[i4] = w; + } + if (data[i6] < data[i2]) { + final double x = data[i6]; + data[i6] = data[i2]; + data[i2] = x; + } + if (data[i1] < data[i0]) { + final double y = data[i1]; + data[i1] = data[i0]; + data[i0] = y; + } + + if (data[i9] < data[i8]) { + final double u = data[i9]; + data[i9] = data[i8]; + data[i8] = u; + } + if (data[i7] < data[i5]) { + final double v = data[i7]; + data[i7] = data[i5]; + data[i5] = v; + } + if (data[i6] < data[i3]) { + final double w = data[i6]; + data[i6] = data[i3]; + data[i3] = w; + } + if (data[i4] < data[i2]) { + final double x = data[i4]; + data[i4] = data[i2]; + data[i2] = x; + } + + if (data[i8] < data[i7]) { + final double u = data[i8]; + data[i8] = data[i7]; + data[i7] = u; + } + if (data[i6] < data[i5]) { + final double v = data[i6]; + data[i6] = data[i5]; + data[i5] = v; + } + if (data[i4] < data[i3]) { + final double w = data[i4]; + data[i4] = data[i3]; + data[i3] = w; + } + if (data[i2] < data[i1]) { + final double x = data[i2]; + data[i2] = data[i1]; + data[i1] = x; + } + + if (data[i7] < data[i6]) { + final double u = data[i7]; + data[i7] = data[i6]; + data[i6] = u; + } + if (data[i5] < data[i4]) { + final double v = data[i5]; + data[i5] = data[i4]; + data[i4] = v; + } + if (data[i3] < data[i2]) { + final double w = data[i3]; + data[i3] = data[i2]; + data[i2] = w; + } + } + + /** + * Sort the unique indices in-place to the start of the array. Duplicates are moved + * to the end of the array and set to negative. For convenience the maximum + * index is set into the final position in the array. If this is a duplicate it is + * set to negative using the twos complement representation: + * + *

{@code
+     * int[] indices = ...
+     * IndexSet sortUnique(indices);
+     * int min = indices[0];
+     * int max = indices[indices.length - 1]
+     * if (max < 0) {
+     *     max = ~max;
+     * }
+     * }
+ * + *

A small number of indices is sorted in place. A large number will use an + * IndexSet which is returned for reuse by the caller. The threshold for this + * switch is provided by the caller. An index set is used when + * {@code indices.length > countThreshold} and there is more than 1 index. + * + *

This method assumes the {@code data} contains only positive integers. + * + * @param countThreshold Threshold to use an IndexSet. + * @param data Indices. + * @param n Number of indices. + * @return the index set (or null if not used) + */ + static IndexSet sortUnique(int countThreshold, int[] data, int n) { + if (n <= 1) { + return null; + } + if (n > countThreshold) { + return sortUnique(data, n); + } + int unique = 1; + int j; + // Do an insertion sort but only compare the current set of unique values. + for (int i = 0; ++i < n;) { + final int v = data[i]; + // Erase data + data[i] = -1; + j = unique; + if (v > data[j - 1]) { + // Insert at end + data[j] = v; + unique++; + } else if (v < data[j - 1]) { + // Find insertion point in the unique indices + do { + --j; + } while (j >= 0 && v < data[j]); + // Either insert at the start, or insert non-duplicate + if (j < 0 || v != data[j]) { + // Update j so it is the insertion position + j++; + // Process the delayed moves + // Move from [j, unique) to [j+1, unique+1) + // System.arraycopy(data, j, data, j + 1, unique - j) + for (int k = unique; k-- > j;) { + data[k + 1] = data[k]; + } + data[j] = v; + unique++; + } + } + } + // Set the max value at the end, bit-flipped + if (unique < n) { + data[n - 1] = ~data[unique - 1]; + } + return null; + } + + /** + * Sort the unique indices in-place to the start of the array. Duplicates are moved + * to the end of the array and set to negative. For convenience the maximum + * index is set into the final position in the array. If this is a duplicate it is + * set to negative using the twos complement representation: + * + *

{@code
+     * int[] indices = ...
+     * IndexSet sortUnique(indices);
+     * int min = indices[0];
+     * int max = indices[indices.length - 1]
+     * if (max < 0) {
+     *     max = ~max;
+     * }
+     * }
+ * + *

Uses an IndexSet which is returned to the caller. Assumes the indices + * are non-zero in length. + * + * @param data Indices. + * @param n Number of indices. + * @return the index set + */ + private static IndexSet sortUnique(int[] data, int n) { + final IndexSet set = IndexSet.of(data, n); + // Iterate + final int[] unique = {0}; + set.forEach(i -> data[unique[0]++] = i); + if (unique[0] < n) { + for (int i = unique[0]; i < n; i++) { + data[i] = -1; + } + // Set the max value at the end, bit flipped + data[n - 1] = ~data[unique[0] - 1]; + } + return set; + } + + /** + * Sort the unique indices in-place to the start of the array. The number of + * indices is returned. + * + *

{@code
+     * int[] indices = ...
+     * int n sortIndices(indices, indices.length);
+     * int min = indices[0];
+     * int max = indices[n - 1]
+     * }
+ * + *

This method assumes the {@code data} contains only positive integers. + * + * @param data Indices. + * @param n Number of indices. + * @return the number of indices + */ + static int sortIndices(int[] data, int n) { + // Simple cases + if (n < 3) { + if (n == 2) { + final int i0 = data[0]; + final int i1 = data[1]; + if (i0 > i1) { + data[0] = i1; + data[1] = i0; + } else if (i0 == i1) { + return 1; + } + } + // n=0,1,2 unique values + return n; + } + + // Strategy: Must be fast on already ascending data. + // Note: The recommended way to generate a lot of partition indices from + // many quantiles for interpolation is to generate in sequence. + + // n <= small: + // Modified insertion sort (naturally finds ascending data) + // n > small: + // Look for ascending sequence and compact + // else: + // Remove duplicates using an order(1) data structure and sort + + if (n <= UNIQUE_INSERTION_SORT) { + return sortIndicesInsertionSort(data, n); + } + + if (isAscending(data, n)) { + return compressDuplicates(data, n); + } + + // At least 20 indices that are partially unordered. + // Find min/max + int min = data[0]; + int max = min; + for (int i = 0; ++i < n;) { + min = Math.min(min, data[i]); + max = Math.max(max, data[i]); + } + + // Benchmarking shows the IndexSet is very fast when the long[] efficiently + // resides in cache memory. If the indices are very well separated the + // distribution is sparse and it is faster to use a HashIndexSet despite + // having to perform a sort after making keys unique. + // Both structures have Order(1) detection of unique keys (the HashIndexSet + // is configured with a load factor that should see low collision rates). + // IndexSet sort Order(n) (data is stored sorted and must be read) + // HashIndexSet sort Order(n log n) (unique data is sorted separately) + + // For now base the choice on memory consumption alone which is a fair + // approximation when n < 1000. + // Above 1000 indices we assume that sorting the indices is a small cost + // compared to sorting/partitioning the data that requires so many indices. + // If the input data is small upstream code could detect this, e.g. + // indices.length >> data.length, and choose to sort the data rather than + // partitioning so many indices. + + // If the HashIndexSet uses < 8x memory of IndexSet then prefer that. + // This detects obvious cases of sparse keys where the IndexSet is + // outperformed by the HashIndexSet. Otherwise we can assume the + // memory consumption of the IndexSet is small compared to the data to be + // partitioned at these target indices (max 1/64 for double[] data); any + // time taken here for sorting indices should be less than partitioning time. + + // This requires more analysis of performance crossover. + // Note: Expected behaviour under extreme use-cases should be documented. + + if (HashIndexSet.memoryFootprint(n) < (IndexSet.memoryFootprint(min, max) >>> 3)) { + return sortIndicesHashIndexSet(data, n); + } + + // Repeat code from IndexSet as we have the min/max + final IndexSet set = IndexSet.ofRange(min, max); + for (int i = -1; ++i < n;) { + set.set(data[i]); + } + return set.toArray(data); + } + + /** + * Sort the unique indices in-place to the start of the array. The number of + * indices is returned. + * + *

{@code
+     * int[] indices = ...
+     * int n sortIndices(indices, indices.length);
+     * int min = indices[0];
+     * int max = indices[n - 1]
+     * }
+ * + *

This method assumes the {@code data} contains only positive integers; + * and that {@code n} is small relative to the range of indices {@code [min, max]} such + * that storing all indices in an {@link IndexSet} is not memory efficient. + * + * @param data Indices. + * @param n Number of indices. + * @return the number of indices + */ + static int sortIndices2(int[] data, int n) { + // Simple cases + if (n < 3) { + if (n == 2) { + final int i0 = data[0]; + final int i1 = data[1]; + if (i0 > i1) { + data[0] = i1; + data[1] = i0; + } else if (i0 == i1) { + return 1; + } + } + // n=0,1,2 unique values + return n; + } + + // Strategy: Must be fast on already ascending data. + // Note: The recommended way to generate a lot of partition indices from + // many quantiles for interpolation is to generate in sequence. + + // n <= small: + // Modified insertion sort (naturally finds ascending data) + // n > small: + // Look for ascending sequence and compact + // else: + // Remove duplicates using an order(1) data structure and sort + + if (n <= UNIQUE_INSERTION_SORT) { + return sortIndicesInsertionSort(data, n); + } + + if (isAscending(data, n)) { + return compressDuplicates(data, n); + } + + return sortIndicesHashIndexSet(data, n); + } + + /** + * Test the data is in ascending order: {@code data[i] <= data[i+1]} for all {@code i}. + * Data is assumed to be at least length 1. + * + * @param data Data. + * @param n Length of data. + * @return true if ascending + */ + private static boolean isAscending(int[] data, int n) { + for (int i = 0; ++i < n;) { + if (data[i] < data[i - 1]) { + // descending + return false; + } + } + return true; + } + + /** + * Test the data is in ascending order: {@code data[i] <= data[i+1]} for all {@code i}. + * Data is assumed to be at least length 1. + * + * @param data Data. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @return true if ascending + */ + static boolean isAscending(double[] data, int left, int right) { + for (int i = left; ++i <= right;) { + if (data[i] < data[i - 1]) { + // descending + return false; + } + } + return true; + } + + // The following methods all perform the same function and are present + // for performance testing. + + /** + * Sort the unique indices in-place to the start of the array. The number of + * indices is returned. + * + *

Uses an insertion sort modified to ignore duplicates. + * + *

Warning: Requires {@code n > 0}. + * + * @param data Indices. + * @param n Number of indices. + * @return the number of indices + */ + static int sortIndicesInsertionSort(int[] data, int n) { + int unique = 1; + // Do an insertion sort but only compare the current set of unique values. + for (int i = 0; ++i < n;) { + final int v = data[i]; + int j = unique - 1; + if (v > data[j]) { + // Insert at end + data[unique] = v; + unique++; + } else if (v < data[j]) { + // Find insertion point in the unique indices + do { + --j; + } while (j >= 0 && v < data[j]); + // Insertion point = j + 1 + // Insert if at start or non-duplicate + if (j < 0 || v != data[j]) { + // Move (j, unique) to (j+1, unique+1) + for (int k = unique; --k > j;) { + data[k + 1] = data[k]; + } + data[j + 1] = v; + unique++; + } + } + } + return unique; + } + + /** + * Sort the unique indices in-place to the start of the array. The number of + * indices is returned. + * + *

Uses a binary search to find the insert point. + * + *

Warning: Requires {@code n > 1}. + * + * @param data Indices. + * @param n Number of indices. + * @return the number of indices + */ + static int sortIndicesBinarySearch(int[] data, int n) { + // Sort first 2 + if (data[1] < data[0]) { + final int v = data[0]; + data[0] = data[1]; + data[1] = v; + } + int unique = data[0] != data[1] ? 2 : 1; + // Insert the remaining indices if unique + OUTER: + for (int i = 1; ++i < n;) { + // Binary search with fast exit on match + int l = 0; + int r = unique - 1; + final int k = data[i]; + while (l <= r) { + // Middle value + final int m = (l + r) >>> 1; + final int v = data[m]; + // Test: + // l------m------r + // v k update left + // k v update right + if (v < k) { + l = m + 1; + } else if (v > k) { + r = m - 1; + } else { + // Equal + continue OUTER; + } + } + // key not found: insert at l + System.arraycopy(data, l, data, l + 1, unique - l); + data[l] = k; + unique++; + } + return unique; + } + + /** + * Sort the unique indices in-place to the start of the array. The number of + * indices is returned. + * + *

Uses a heap sort modified to ignore duplicates. + * + *

Warning: Requires {@code n > 0}. + * + * @param data Indices. + * @param n Number of indices. + * @return the number of indices + */ + static int sortIndicesHeapSort(int[] data, int n) { + // Build the min heap using Floyd's heap-construction algorithm + // Start at parent of the last element in the heap (n-1) + final int offset = n - 1; + for (int start = offset >> 1; start >= 0; start--) { + minHeapSiftDown(data, offset, start, n); + } + + // The min heap has been constructed in-place so a[n-1] is the min. + // To sort we have to move elements from the top of the + // heap to the position immediately before the end of the heap + // (which is below right), reducing the heap size each step: + // root + // |--------------|k|-min-heap-|r| + // | <-swap-> | + + // Move top of heap to the sorted end and move the end + // to the top. + int previous = data[offset]; + data[offset] = data[0]; + data[0] = previous; + int s = n - 1; + minHeapSiftDown(data, offset, 0, s); + + // Min heap is now 1 smaller + // Proceed with the remaining elements but do not write them + // to the sorted data unless different from the previous value. + int last = 0; + for (;;) { + s--; + // Move top of heap to the sorted end + final int v = data[offset]; + data[offset] = data[offset - s]; + if (previous != v) { + data[++last] = v; + previous = v; + } + if (s == 1) { + // end of heap + break; + } + minHeapSiftDown(data, offset, 0, s); + } + // Stopped sifting when the heap was size 1. + // Move the last (max) value to the sorted data. + if (previous != data[offset]) { + data[++last] = data[offset]; + } + return last + 1; + } + + /** + * Sift the top element down the min heap. + * + *

Note this creates the min heap in descending sequence so the + * heap is positioned below the root. + * + * @param a Heap data. + * @param offset Offset of the heap in the data. + * @param root Root of the heap. + * @param n Size of the heap. + */ + private static void minHeapSiftDown(int[] a, int offset, int root, int n) { + // For node i: + // left child: 2i + 1 + // right child: 2i + 2 + // parent: floor((i-1) / 2) + + // Value to sift + int p = root; + final int v = a[offset - p]; + // Left child of root: p * 2 + 1 + int c = (p << 1) + 1; + while (c < n) { + // Left child value + int cv = a[offset - c]; + // Use the right child if less + if (c + 1 < n && cv > a[offset - c - 1]) { + cv = a[offset - c - 1]; + c++; + } + // Min heap requires parent <= child + if (v <= cv) { + // Less than smallest child - done + break; + } + // Swap and descend + a[offset - p] = cv; + p = c; + c = (p << 1) + 1; + } + a[offset - p] = v; + } + + /** + * Sort the unique indices in-place to the start of the array. The number of + * indices is returned. + * + *

Uses a full sort and a second-pass to ignore duplicates. + * + *

Warning: Requires {@code n > 0}. + * + * @param data Indices. + * @param n Number of indices. + * @return the number of indices + */ + static int sortIndicesSort(int[] data, int n) { + java.util.Arrays.sort(data, 0, n); + return compressDuplicates(data, n); + } + + /** + * Compress duplicates in the ascending data. + * + *

Warning: Requires {@code n > 0}. + * + * @param data Indices. + * @param n Number of indices. + * @return the number of unique indices + */ + private static int compressDuplicates(int[] data, int n) { + // Compress to remove duplicates + int last = 0; + int top = data[0]; + for (int i = 0; ++i < n;) { + final int v = data[i]; + if (v == top) { + continue; + } + top = v; + data[++last] = v; + } + return last + 1; + } + + /** + * Sort the unique indices in-place to the start of the array. The number of + * indices is returned. + * + *

Uses an {@link IndexSet} to ignore duplicates. The sorted array is + * extracted from the {@link IndexSet} storage in order. + * + *

Warning: Requires {@code n > 0}. + * + * @param data Indices. + * @param n Number of indices. + * @return the number of indices + * @see IndexSet#toArray(int[]) + */ + static int sortIndicesIndexSet(int[] data, int n) { + // Delegate to IndexSet + // Storage (bytes) = 8 * ceil((max - min) / 64), irrespective of n. + // This can be use a lot of memory when the indices are spread out. + return IndexSet.of(data, n).toArray(data); + } + + /** + * Sort the unique indices in-place to the start of the array. The number of + * indices is returned. + * + *

Uses an {@link IndexSet} to ignore duplicates. The sorted array is + * extracted from the {@link IndexSet} storage in order. + * + *

Warning: Requires {@code n > 0}. + * + * @param data Indices. + * @param n Number of indices. + * @return the number of indices + * @see IndexSet#toArray2(int[]) + */ + static int sortIndicesIndexSet2(int[] data, int n) { + // Delegate to IndexSet + // Storage (bytes) = 8 * ceil((max - min) / 64), irrespective of n. + // This can be use a lot of memory when the indices are spread out. + return IndexSet.of(data, n).toArray2(data); + } + + /** + * Sort the unique indices in-place to the start of the array. The number of + * indices is returned. + * + *

Uses a {@link HashIndexSet} to ignore duplicates and then performs + * a full sort of the unique values. + * + *

Warning: Requires {@code n > 0}. + * + * @param data Indices. + * @param n Number of indices. + * @return the number of indices + */ + static int sortIndicesHashIndexSet(int[] data, int n) { + // Compress to remove duplicates. + // Duplicates are checked using a HashIndexSet. + // Storage (bytes) = 4 * next-power-of-2(n*2) => 2-4 times n + final HashIndexSet set = new HashIndexSet(n); + int i = 0; + int last = 0; + set.add(data[0]); + while (++i < n) { + final int v = data[i]; + if (set.add(v)) { + data[++last] = v; + } + } + // Sort unique data. + // This can exploit the input already being sorted. + Arrays.sort(data, 0, ++last); + return last; + } +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/SplittingInterval.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/SplittingInterval.java new file mode 100644 index 000000000..1504f23cc --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/SplittingInterval.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * An interval that contains indices used for partitioning an array into multiple regions. + * + *

The interval provides the following functionality: + * + *

    + *
  • Return the supported bounds of the interval {@code [left <= right]}. + *
  • Split the interval around two indices {@code k1} and {@code k2}. + *
+ * + *

Note that the interval provides the supported bounds. If a split invalidates an interval + * the bounds are undefined and the interval is marked as {@link #empty()}. + * + *

Implementations may assume indices are positive. + * + * @since 1.2 + */ +interface SplittingInterval { + /** + * The start (inclusive) of the interval. + * + * @return start of the interval + */ + int left(); + + /** + * The end (inclusive) of the interval. + * + * @return end of the interval + */ + int right(); + + /** + * Signal this interval is empty. The left and right bounds are undefined. This results + * from a split where there is no right side. + * + * @return {@code true} if empty + */ + boolean empty(); + + /** + * Split the interval using two splitting indices. Returns the left interval that occurs + * before the specified split index {@code ka}, and updates the current interval left bound + * to after the specified split index {@code kb}. + * + *

{@code
+     * l-----------ka-kb----------r
+     *      ra <--|     |--> lb
+     *
+     * ra < ka
+     * lb > kb
+     * }
+ * + *

If {@code ka <= left} the returned left interval is {@code null}. + * + *

If {@code kb >= right} the current interval is invalidated and marked as empty. + * + * @param ka Split index. + * @param kb Split index. + * @return the left interval + * @see #empty() + */ + SplittingInterval split(int ka, int kb); +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/UpdatingInterval.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/UpdatingInterval.java new file mode 100644 index 000000000..7f2e6406f --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/UpdatingInterval.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.numbers.examples.jmh.arrays; + +/** + * An interval that contains indices used for partitioning an array into multiple regions. + * + *

The interval provides the following functionality: + * + *

    + *
  • Return the supported bounds of the interval {@code [left <= right]}. + *
  • Update the left or right bound of the interval using an index {@code k} inside the interval. + *
  • Split the interval around two indices {@code k1} and {@code k2}. + *
+ * + *

Note that the interval provides the supported bounds. If an index {@code k} is + * outside the supported bounds the result is undefined. + * + *

Implementations may assume indices are positive. + * + * @since 1.2 + */ +interface UpdatingInterval { + /** + * The start (inclusive) of the interval. + * + * @return start of the interval + */ + int left(); + + /** + * The end (inclusive) of the interval. + * + * @return end of the interval + */ + int right(); + + /** + * Update the interval so {@code k <= left}. + * + *

Note: Requires {@code left < k <= right}, i.e. there exists a valid interval + * above the index. + * + *

{@code
+     * l-----------k----------r
+     *             |--> l
+     * }
+ * + * @param k Index to start checking from (inclusive). + * @return the new left + */ + int updateLeft(int k); + + /** + * Update the interval so {@code right <= k}. + * + *

Note: Requires {@code left <= k < right}, i.e. there exists a valid interval + * below the index. + * + *

{@code
+     * l-----------k----------r
+     *        r <--|
+     * }
+ * + * @param k Index to start checking from (inclusive). + * @return the new right + */ + int updateRight(int k); + + /** + * Split the interval using two splitting indices. Returns the left interval that occurs + * before the specified split index {@code ka}, and updates the current interval left bound + * to after the specified split index {@code kb}. + * + *

Note: Requires {@code left < ka <= kb < right}, i.e. there exists a valid interval + * above and below the split indices. + * + *

{@code
+     * l-----------ka-kb----------r
+     *      r1 <--|     |--> l1
+     *
+     * r1 < ka
+     * l1 > kb
+     * }
+ * + *

If {@code ka <= left} or {@code kb >= right} the result is undefined. + * + * @param ka Split index. + * @param kb Split index. + * @return the left interval + */ + UpdatingInterval splitLeft(int ka, int kb); + + /** + * Split the interval using two splitting indices. Returns the right interval that occurs + * after the specified split index {@code kb}, and updates the current interval right bound + * to before the specified split index {@code ka}. + * + *

Note: Requires {@code left < ka <= kb < right}, i.e. there exists a valid interval + * above and below the split indices. + * + *

{@code
+     * l-----------ka-kb----------r
+     *      r1 <--|     |--> l1
+     *
+     * r1 < ka
+     * l1 > kb
+     * }
+ * + *

If {@code ka <= left} or {@code kb >= right} the result is undefined. + * + * @param ka Split index. + * @param kb Split index. + * @return the right interval + */ + UpdatingInterval splitRight(int ka, int kb); +} diff --git a/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/package-info.java b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/package-info.java new file mode 100644 index 000000000..1712ef336 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/main/java/org/apache/commons/numbers/examples/jmh/arrays/package-info.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Benchmarks for the {@code org.apache.commons.numbers.arrays} components. + * + * @since 1.2 + */ +package org.apache.commons.numbers.examples.jmh.arrays; diff --git a/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/CompressedIndexSetTest.java b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/CompressedIndexSetTest.java new file mode 100644 index 000000000..1ff5098ae --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/CompressedIndexSetTest.java @@ -0,0 +1,365 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.util.Arrays; +import java.util.BitSet; +import java.util.stream.Stream; +import org.apache.commons.rng.UniformRandomProvider; +import org.apache.commons.rng.simple.RandomSource; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Test for {@link CompressedIndexSet} and {@link CompressedIndexSet2}. + */ +class CompressedIndexSetTest { + /** Compression levels to test. */ + private static final int[] COMPRESSION = {1, 2, 3}; + + @Test + void testInvalidRangeThrows() { + // Valid compression + final int c = 1; + Assertions.assertThrows(IllegalArgumentException.class, () -> CompressedIndexSet.ofRange(c, -1, 3)); + Assertions.assertThrows(IllegalArgumentException.class, () -> CompressedIndexSet.ofRange(c, 0, -1)); + Assertions.assertThrows(IllegalArgumentException.class, () -> CompressedIndexSet.ofRange(c, 456, 123)); + Assertions.assertThrows(IllegalArgumentException.class, () -> CompressedIndexSet.of(c, new int[0])); + Assertions.assertThrows(IllegalArgumentException.class, () -> CompressedIndexSet.of(c, new int[] {-1})); + // Fixed compression + Assertions.assertThrows(IllegalArgumentException.class, () -> CompressedIndexSet2.ofRange(-1, 3)); + Assertions.assertThrows(IllegalArgumentException.class, () -> CompressedIndexSet2.ofRange(0, -1)); + Assertions.assertThrows(IllegalArgumentException.class, () -> CompressedIndexSet2.ofRange(456, 123)); + Assertions.assertThrows(IllegalArgumentException.class, () -> CompressedIndexSet2.of(new int[0])); + Assertions.assertThrows(IllegalArgumentException.class, () -> CompressedIndexSet2.of(new int[] {-1})); + } + + @Test + void testInvalidCompressionThrows() { + for (final int c : new int[] {0, -1, 32}) { + Assertions.assertThrows(IllegalArgumentException.class, () -> CompressedIndexSet.ofRange(c, 1, 3)); + Assertions.assertThrows(IllegalArgumentException.class, () -> CompressedIndexSet.of(c, new int[] {1})); + } + } + + @ParameterizedTest + @MethodSource + void testGetSet(int[] indices, int n) { + for (final int c : COMPRESSION) { + final CompressedIndexSet set = createCompressedIndexSet(c, indices); + final BitSet ref = new BitSet(n); + final int left = set.left(); + final int range = 1 << c; + for (final int i : indices) { + // The contains value is a probability due to the compression. + // It will be true if any of the indices in the compressed range are set. + final int mapped = getCompressedIndexLow(c, left, i); + boolean contains = ref.get(mapped); + for (int j = 1; !contains && j < range; j++) { + contains = ref.get(mapped + j); + } + Assertions.assertEquals(contains, set.get(i), () -> String.valueOf(i)); + set.set(i); + ref.set(i); + Assertions.assertTrue(set.get(i)); + } + } + } + + @ParameterizedTest + @MethodSource(value = {"testGetSet"}) + void testPreviousIndexOrLeftMinus1(int[] indices, int n) { + for (final int c : COMPRESSION) { + final CompressedIndexSet set = createCompressedIndexSet(c, indices); + final BitSet ref = new BitSet(n); + final int left = set.left(); + final int right = set.right(); + Arrays.sort(indices); + final int highBit = indices[indices.length - 1]; + Assertions.assertEquals(set.left() - 1, set.previousIndexOrLeftMinus1(0)); + Assertions.assertEquals(set.left() - 1, set.previousIndexOrLeftMinus1(highBit)); + Assertions.assertEquals(set.left() - 1, set.previousIndexOrLeftMinus1(highBit * 2)); + for (final int i : indices) { + final int lo = getCompressedIndexLow(c, left, i); + final int hi = getCompressedIndexHigh(c, left, right, i); + boolean contains = ref.get(lo); + for (int j = lo + 1; !contains && j <= hi; j++) { + contains = ref.get(j); + } + if (contains) { + for (int j = lo; j <= hi; j++) { + Assertions.assertEquals(j, set.previousIndexOrLeftMinus1(j), () -> "contains: " + i); + } + } else { + int prev = ref.previousSetBit(lo); + if (prev < 0) { + prev = left - 1; + } else { + prev = getCompressedIndexHigh(c, left, right, prev); + } + Assertions.assertEquals(prev, set.previousIndexOrLeftMinus1(i), () -> "previous upper: " + i); + } + set.set(i); + ref.set(i); + // Re-check within + for (int j = lo; j <= hi; j++) { + Assertions.assertEquals(j, set.previousIndexOrLeftMinus1(j), () -> "within: " + i); + } + // Check after + Assertions.assertEquals(hi, set.previousIndexOrLeftMinus1(hi + 1), () -> "after: " + i); + Assertions.assertEquals(hi, set.previousIndexOrLeftMinus1(hi + 42), () -> "after: " + i); + } + } + } + + @ParameterizedTest + @MethodSource(value = {"testGetSet"}) + void testNextIndexOrRightPlus1(int[] indices, int n) { + for (final int c : COMPRESSION) { + final CompressedIndexSet set = createCompressedIndexSet(c, indices); + final BitSet ref = new BitSet(n); + final int left = set.left(); + final int right = set.right(); + Arrays.sort(indices); + final int highBit = indices[indices.length - 1]; + Assertions.assertEquals(set.right() + 1, set.nextIndexOrRightPlus1(0)); + Assertions.assertEquals(set.right() + 1, set.nextIndexOrRightPlus1(highBit)); + Assertions.assertEquals(set.right() + 1, set.nextIndexOrRightPlus1(highBit * 2)); + // Process in descending order + for (int i = -1, j = indices.length; ++i < --j;) { + final int k = indices[i]; + indices[i] = indices[j]; + indices[j] = k; + } + for (final int i : indices) { + final int lo = getCompressedIndexLow(c, left, i); + final int hi = getCompressedIndexHigh(c, left, right, i); + boolean contains = ref.get(lo); + for (int j = lo + 1; !contains && j <= hi; j++) { + contains = ref.get(j); + } + if (contains) { + for (int j = lo; j <= hi; j++) { + Assertions.assertEquals(j, set.nextIndexOrRightPlus1(j), () -> "contains: " + i); + } + } else { + int next = ref.nextSetBit(lo); + if (next < 0) { + next = right + 1; + } else { + next = getCompressedIndexLow(c, left, next); + } + Assertions.assertEquals(next, set.nextIndexOrRightPlus1(i), () -> "next upper: " + i); + } + set.set(i); + ref.set(i); + // Re-check within + for (int j = lo; j <= hi; j++) { + Assertions.assertEquals(j, set.nextIndexOrRightPlus1(j), () -> "within: " + i); + } + // Check before + Assertions.assertEquals(lo, set.nextIndexOrRightPlus1(lo - 1), () -> "before: " + i); + Assertions.assertEquals(lo, set.nextIndexOrRightPlus1(lo - 42), () -> "before: " + i); + } + } + } + + @ParameterizedTest + @MethodSource(value = {"testGetSet"}) + void testNextPreviousIndex(int[] indices, int ignored) { + for (final int c : COMPRESSION) { + final CompressedIndexSet set = CompressedIndexSet.of(c, indices); + final int left = set.left(); + final int right = set.right(); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> set.previousIndex(left - 1)); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> set.previousIndex(right + Long.SIZE << c)); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> set.nextIndex(left - 1)); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> set.nextIndex(right + Long.SIZE << c)); + for (final int i : indices) { + // Test against validated method + final int lo = getCompressedIndexLow(c, left, i); + final int hi = getCompressedIndexHigh(c, left, right, i); + // Search with left <= k <= right + Assertions.assertTrue(lo >= left && hi <= right); + for (int j = lo; j <= hi; j++) { + Assertions.assertEquals(set.previousIndexOrLeftMinus1(j), set.previousIndex(j)); + Assertions.assertEquals(set.nextIndexOrRightPlus1(j), set.nextIndex(j)); + } + if (lo > left) { + Assertions.assertEquals(set.previousIndexOrLeftMinus1(lo - 1), set.previousIndex(lo - 1)); + } + if (hi < right) { + Assertions.assertEquals(set.nextIndexOrRightPlus1(hi + 1), set.nextIndex(hi + 1)); + } + } + } + } + + static Stream testGetSet() { + final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create(); + final Stream.Builder builder = Stream.builder(); + for (final int size : new int[] {5, 500}) { + final int[] a = rng.ints(10, 0, size).toArray(); + builder.accept(Arguments.of(a.clone(), size)); + // Force use of index 0 + a[0] = 0; + builder.accept(Arguments.of(a, size)); + } + // Large offset with an index at the end of the range + final int n = 513; + // Use 1, 2, or 3 longs for storage + builder.accept(Arguments.of(new int[] {n - 13, n - 1}, n)); + builder.accept(Arguments.of(new int[] {n - 78, n - 1}, n)); + builder.accept(Arguments.of(new int[] {n - 137, n - 1}, n)); + // Uses a capacity of 512. BitSet will increase this to 512 + 64 to store index + // 513. + builder.accept(Arguments.of(new int[] {1, n - 1}, n)); + return builder.build(); + } + + + @ParameterizedTest + @MethodSource(value = {"testGetSet"}) + void testGetSet2(int[] indices, int n) { + final int c = 1; + final CompressedIndexSet2 set = createCompressedIndexSet2(indices); + final BitSet ref = new BitSet(n); + final int left = set.left(); + final int range = 1 << c; + for (final int i : indices) { + // The contains value is a probability due to the compression. + // It will be true if any of the indices in the compressed range are set. + final int mapped = getCompressedIndexLow(c, left, i); + boolean contains = ref.get(mapped); + for (int j = 1; !contains && j < range; j++) { + contains = ref.get(mapped + j); + } + Assertions.assertEquals(contains, set.get(i), () -> String.valueOf(i)); + set.set(i); + ref.set(i); + Assertions.assertTrue(set.get(i)); + } + } + + @ParameterizedTest + @MethodSource(value = {"testGetSet"}) + void testNextPreviousIndex2(int[] indices, int ignored) { + final int c = 1; + final CompressedIndexSet ref = CompressedIndexSet.of(c, indices); + final CompressedIndexSet2 set = CompressedIndexSet2.of(indices); + final int left = set.left(); + final int right = set.right(); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> set.previousIndex(left - 1)); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> set.previousIndex(right + Long.SIZE << c)); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> set.nextIndex(left - 1)); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> set.nextIndex(right + Long.SIZE << c)); + for (final int i : indices) { + // Test against validated method + final int lo = getCompressedIndexLow(c, left, i); + final int hi = getCompressedIndexHigh(c, left, right, i); + // Search with left <= k <= right + Assertions.assertTrue(lo >= left && hi <= right); + for (int j = lo; j <= hi; j++) { + Assertions.assertEquals(ref.previousIndexOrLeftMinus1(j), set.previousIndex(j)); + Assertions.assertEquals(ref.nextIndexOrRightPlus1(j), set.nextIndex(j)); + } + if (lo > left) { + Assertions.assertEquals(ref.previousIndexOrLeftMinus1(lo - 1), set.previousIndex(lo - 1)); + } + if (hi < right) { + Assertions.assertEquals(ref.nextIndexOrRightPlus1(hi + 1), set.nextIndex(hi + 1)); + } + } + } + + /** + * Creates the compressed index set using the min/max of the indices. + * + * @param compression Compression level. + * @param indices Indices. + * @return the set + */ + private static CompressedIndexSet createCompressedIndexSet(int compression, int[] indices) { + final int min = Arrays.stream(indices).min().getAsInt(); + final int max = Arrays.stream(indices).max().getAsInt(); + final CompressedIndexSet set = CompressedIndexSet.ofRange(compression, min, max); + Assertions.assertEquals(min, set.left()); + Assertions.assertEquals(max, set.right()); + return set; + } + + /** + * Creates the compressed index set using the min/max of the indices. + * + * @param indices Indices. + * @return the set + */ + private static CompressedIndexSet2 createCompressedIndexSet2(int[] indices) { + final int min = Arrays.stream(indices).min().getAsInt(); + final int max = Arrays.stream(indices).max().getAsInt(); + final CompressedIndexSet2 set = CompressedIndexSet2.ofRange(min, max); + Assertions.assertEquals(min, set.left()); + Assertions.assertEquals(max, set.right()); + return set; + } + + /** + * Gets the lower bound of the index range covered by the compressed index. + * A compressed index covers a range of real indices. For example the + * indices i, j, k, and l are all represented by the same compressed index + * with a compression level of 2. + *

+     * -------ijkl------
+     * 
+ * + * @param c Compression. + * @param left Lower bound of the set of compressed indices. + * @param i Index. + * @return the lower bound of the index range + */ + private static int getCompressedIndexLow(int c, int left, int i) { + return (((i - left) >>> c) << c) + left; + } + + /** + * Gets the upper bound of the index range covered by the compressed index. + * A compressed index covers a range of real indices. For example the + * indices i, j, k, and l are all represented by the same compressed index + * with a compression level of 2. + *
+     *          right
+     *          |
+     * -------ijkl------
+     * 
+ * + *

This method returns l, unless clipped by the upper bound of the supported + * indices (right; in example above clipping would return k). + * + * @param c Compression. + * @param left Lower bound of the set of compressed indices. + * @param right Upper bound of the set of compressed indices. + * @param i Index. + * @return the upper bound of the index range + */ + private static int getCompressedIndexHigh(int c, int left, int right, int i) { + return Math.min(right, (((i - left) >>> c) << c) + left + (1 << c) - 1); + } +} diff --git a/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/DoubleDataTransformersTest.java b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/DoubleDataTransformersTest.java new file mode 100644 index 000000000..182b24071 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/DoubleDataTransformersTest.java @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.util.Arrays; +import java.util.stream.Stream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Test for {@link DoubleDataTransformers}. + */ +class DoubleDataTransformersTest { + @ParameterizedTest + @MethodSource(value = {"nanData"}) + void testNaNErrorWithNaN(double[] a) { + final DoubleDataTransformer t1 = DoubleDataTransformers.createFactory(NaNPolicy.ERROR, false).get(); + Assertions.assertThrows(IllegalArgumentException.class, () -> t1.preProcess(a)); + final DoubleDataTransformer t2 = DoubleDataTransformers.createFactory(NaNPolicy.ERROR, true).get(); + Assertions.assertThrows(IllegalArgumentException.class, () -> t2.preProcess(a)); + } + + @ParameterizedTest + @MethodSource(value = {"nonNanData"}) + void testNaNError(double[] a) { + assertSortTransformer(a, DoubleDataTransformers.createFactory(NaNPolicy.ERROR, false).get(), true, false); + assertSortTransformer(a, DoubleDataTransformers.createFactory(NaNPolicy.ERROR, true).get(), true, true); + } + + @ParameterizedTest + @MethodSource(value = {"nanData", "nonNanData"}) + void testNaNInclude(double[] a) { + assertSortTransformer(a, DoubleDataTransformers.createFactory(NaNPolicy.INCLUDE, false).get(), true, false); + assertSortTransformer(a, DoubleDataTransformers.createFactory(NaNPolicy.INCLUDE, true).get(), true, true); + } + + @ParameterizedTest + @MethodSource(value = {"nanData", "nonNanData"}) + void testNaNExclude(double[] a) { + assertSortTransformer(a, DoubleDataTransformers.createFactory(NaNPolicy.EXCLUDE, false).get(), false, false); + assertSortTransformer(a, DoubleDataTransformers.createFactory(NaNPolicy.EXCLUDE, true).get(), false, true); + } + + /** + * Assert the transformer allows partitioning the data as if sorting using + * {@link Arrays#sort(double[])}. NaN should be moved to the end; signed zeros + * should be correctly ordered. + * + * @param a Data. + * @param t Transformer. + * @param includeNaN True if the size should include NaN + * @param copy True if the pre-processed data should be a copy + */ + private static void assertSortTransformer(double[] a, DoubleDataTransformer t, + boolean includeNaN, boolean copy) { + final double[] original = a.clone(); + final double[] b = t.preProcess(a); + if (copy) { + Assertions.assertNotSame(a, b); + } else { + Assertions.assertSame(a, b); + } + // Count NaN + final int nanCount = (int) Arrays.stream(a).filter(Double::isNaN).count(); + Assertions.assertEquals(a.length - nanCount, t.length(), "Length to process"); + Assertions.assertEquals(a.length - (includeNaN ? 0 : nanCount), t.size(), "Size of data"); + // Partition / sort data up to the specified length + Arrays.sort(b, 0, t.length()); + // Full sort of the original + Arrays.sort(original); + // Correct data given partition indices + if (b.length > 0) { + // Use potentially invalid partition index + t.postProcess(b, new int[] {b.length - 1}, 1); + } + Assertions.assertArrayEquals(original, b); + } + + static Stream nanData() { + final Stream.Builder builder = Stream.builder(); + final double nan = Double.NaN; + builder.add(new double[] {nan}); + builder.add(new double[] {1, 2, nan}); + builder.add(new double[] {nan, 2, 3}); + builder.add(new double[] {1, nan, 3}); + builder.add(new double[] {nan, nan}); + builder.add(new double[] {nan, 2, nan}); + builder.add(new double[] {nan, nan, nan}); + builder.add(new double[] {1, 0.0, 0.0, nan, -1}); + builder.add(new double[] {1, 0.0, -0.0, nan, -1}); + builder.add(new double[] {1, -0.0, 0.0, nan, -1}); + builder.add(new double[] {1, -0.0, -0.0, nan, -1}); + builder.add(new double[] {1, 0.0, 0.0, nan, -1, 0.0, 0.0}); + builder.add(new double[] {1, 0.0, -0.0, nan, -1, 0.0, 0.0}); + builder.add(new double[] {1, 0.0, -0.0, nan, -1, 0.0, -0.0}); + builder.add(new double[] {nan, -0.0, 0.0, nan, -1, -0.0, -0.0}); + builder.add(new double[] {nan, -0.0, -0.0, nan, -1, -0.0, -0.0}); + return builder.build(); + } + + static Stream nonNanData() { + final Stream.Builder builder = Stream.builder(); + builder.add(new double[] {}); + builder.add(new double[] {3}); + builder.add(new double[] {3, 2, 1}); + builder.add(new double[] {1, 0.0, 0.0, 3, -1}); + builder.add(new double[] {1, 0.0, -0.0, 3, -1}); + builder.add(new double[] {1, -0.0, 0.0, 3, -1}); + builder.add(new double[] {1, -0.0, -0.0, 3, -1}); + builder.add(new double[] {1, 0.0, 0.0, 3, -1, 0.0, 0.0}); + builder.add(new double[] {1, 0.0, -0.0, 3, -1, 0.0, 0.0}); + builder.add(new double[] {1, 0.0, -0.0, 3, -1, 0.0, -0.0}); + builder.add(new double[] {1, -0.0, 0.0, 3, -1, -0.0, -0.0}); + builder.add(new double[] {1, -0.0, -0.0, 3, -1, -0.0, -0.0}); + return builder.build(); + } +} diff --git a/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/DoubleMathTest.java b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/DoubleMathTest.java new file mode 100644 index 000000000..e4166d896 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/DoubleMathTest.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Test for {@link DoubleMath}. + */ +class DoubleMathTest { + @Test + void testGreaterThanLessThan() { + final double[] values = {0.0, 1.0, Double.POSITIVE_INFINITY, Double.NaN}; + final int[] sign = {-1, 1}; + for (final double a : values) { + for (final double b : values) { + for (final int i : sign) { + final double x = i * a; + for (final int j : sign) { + final double y = j * b; + Assertions.assertEquals(Double.compare(x, y) > 0, DoubleMath.greaterThan(x, y), + () -> x + " > " + y); + Assertions.assertEquals(Double.compare(x, y) < 0, DoubleMath.lessThan(x, y), + () -> x + " < " + y); + } + } + } + } + } +} diff --git a/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/DualPivotingStrategyTest.java b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/DualPivotingStrategyTest.java new file mode 100644 index 000000000..c04e8b007 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/DualPivotingStrategyTest.java @@ -0,0 +1,409 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Formatter; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; +import org.apache.commons.rng.UniformRandomProvider; +import org.apache.commons.rng.simple.RandomSource; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Test for {@link DualPivotingStrategy}. + */ +class DualPivotingStrategyTest { + @ParameterizedTest + @MethodSource + void testMedians(double[] a) { + assertPivots(a, DualPivotingStrategy.MEDIANS); + } + + @ParameterizedTest + @MethodSource(value = {"testSort5", "testSort5IsSorted"}) + void testSort5(double[] a) { + assertPivots(a, DualPivotingStrategy.SORT_5); + } + + @ParameterizedTest + @MethodSource(value = {"testSort5", "testSort5IsSorted"}) + void testSort5B(double[] a) { + assertPivots(a, DualPivotingStrategy.SORT_5B); + } + + @ParameterizedTest + @MethodSource(value = {"testSort5", "testSort5IsSorted"}) + void testSort5BSP(double[] a) { + assertPivots(a, DualPivotingStrategy.SORT_5B_SP); + } + + @ParameterizedTest + @MethodSource(value = {"testSort5", "testSort5IsSorted"}) + void testSort5C(double[] a) { + assertPivots(a, DualPivotingStrategy.SORT_5C); + } + + @ParameterizedTest + @MethodSource(value = {"testSort5IsSorted"}) + void testSort5of3(double[] a) { + // Does not work for small length + Assumptions.assumeTrue(a.length > 14); + assertPivots(a, DualPivotingStrategy.SORT_5_OF_3); + } + + @ParameterizedTest + @MethodSource(value = {"testSort5IsSorted"}) + void testSort4of3(double[] a) { + // Does not work for small length + Assumptions.assumeTrue(a.length > 11); + assertPivots(a, DualPivotingStrategy.SORT_4_OF_3); + } + + @ParameterizedTest + @MethodSource(value = {"testSort5IsSorted"}) + void testSort3of3(double[] a) { + // Does not work for small length + Assumptions.assumeTrue(a.length > 8); + assertPivots(a, DualPivotingStrategy.SORT_3_OF_3); + } + + @ParameterizedTest + @MethodSource(value = {"testSort5IsSorted"}) + void testSort5of5(double[] a) { + // Does not work for small length + Assumptions.assumeTrue(a.length > 24); + assertPivots(a, DualPivotingStrategy.SORT_5_OF_5); + } + + @ParameterizedTest + @MethodSource(value = {"testSort5IsSorted"}) + void testSort7(double[] a) { + // Does not work for small length + Assumptions.assumeTrue(a.length > 6); + assertPivots(a, DualPivotingStrategy.SORT_7); + } + + @ParameterizedTest + @MethodSource(value = {"testSort5IsSorted"}) + void testSort8(double[] a) { + // Does not work for small length + Assumptions.assumeTrue(a.length > 7); + assertPivots(a, DualPivotingStrategy.SORT_8); + } + + @ParameterizedTest + @MethodSource(value = {"testSort5IsSorted"}) + void testSort11(double[] a) { + // Does not work for small length + Assumptions.assumeTrue(a.length > 10); + assertPivots(a, DualPivotingStrategy.SORT_11); + } + + private static void assertPivots(double[] a, DualPivotingStrategy s) { + final double[] copy = a.clone(); + final int[] k = s.getSampledIndices(0, a.length - 1); + // Extract data + final double[] x = new double[k.length]; + for (int i = 0; i < k.length; i++) { + x[i] = a[k[i]]; + } + final int[] pivot2 = {-1}; + final int p1 = s.pivotIndex(a, 0, a.length - 1, pivot2); + final int p2 = pivot2[0]; + Assertions.assertTrue(a[p1] <= a[p2], "pivots not sorted"); + // Extract data after + final double[] y = new double[k.length]; + for (int i = 0; i < k.length; i++) { + y[i] = a[k[i]]; + } + // Test the effect on the data + final int effect = s.samplingEffect(); + if (effect == DualPivotingStrategy.SORT) { + Arrays.sort(x); + Assertions.assertArrayEquals(x, y, "Data at indices not sorted"); + } else if (effect == DualPivotingStrategy.UNCHANGED) { + Assertions.assertArrayEquals(x, y, "Data at indices changed"); + } else if (effect == DualPivotingStrategy.PARTIAL_SORT) { + Arrays.sort(x); + Arrays.sort(y); + Assertions.assertArrayEquals(x, y, "Data destroyed"); + } + // Flip data, pivot values should be the same + for (int i = 0, j = k.length - 1; i < j; i++, j--) { + final double v = copy[k[i]]; + copy[k[i]] = copy[k[j]]; + copy[k[j]] = v; + } + final int p1a = s.pivotIndex(copy, 0, a.length - 1, pivot2); + final int p2a = pivot2[0]; + Assertions.assertEquals(a[p1], copy[p1a], "Pivot 1 changed"); + Assertions.assertEquals(a[p2], copy[p2a], "Pivot 2 changed"); + } + + @Test + void testMediansIndexing() { + assertIndexing(DualPivotingStrategy.MEDIANS, 2); + } + + @Test + void testSort5Indexing() { + assertIndexing(DualPivotingStrategy.SORT_5, 5); + } + + @Test + void testSort5BIndexing() { + assertIndexing(DualPivotingStrategy.SORT_5B, 5); + } + + @Test + void testSort5BSPIndexing() { + assertIndexing(DualPivotingStrategy.SORT_5B_SP, 5); + } + + @Test + void testSort5CIndexing() { + assertIndexing(DualPivotingStrategy.SORT_5C, 5); + } + + @Test + void testSort5of3Indexing() { + assertIndexing(DualPivotingStrategy.SORT_5_OF_3, 15); + } + + @Test + void testSort4of3Indexing() { + assertIndexing(DualPivotingStrategy.SORT_4_OF_3, 12); + } + + @Test + void testSort3of3Indexing() { + assertIndexing(DualPivotingStrategy.SORT_3_OF_3, 9); + } + + @Test + void testSort5of5Indexing() { + assertIndexing(DualPivotingStrategy.SORT_5_OF_5, 25); + } + + @Test + void testSort7Indexing() { + assertIndexing(DualPivotingStrategy.SORT_7, 7); + } + + @Test + void testSort8Indexing() { + assertIndexing(DualPivotingStrategy.SORT_8, 8); + } + + @Test + void testSort11Indexing() { + assertIndexing(DualPivotingStrategy.SORT_11, 11); + } + + private static void assertIndexing(DualPivotingStrategy s, int safeLength) { + final int[] pivot2 = {-1}; + final double[] a = new double[safeLength - 1]; + Assertions.assertThrows(ArrayIndexOutOfBoundsException.class, () -> s.pivotIndex(a, 0, a.length - 1, pivot2), + () -> "Length: " + (safeLength - 1)); + for (int i = safeLength; i < 50; i++) { + final int n = i; + final double[] b = new double[i]; + Assertions.assertDoesNotThrow(() -> s.pivotIndex(b, 0, b.length - 1, pivot2), () -> "Length: " + n); + } + } + + static Stream testMedians() { + final Stream.Builder builder = Stream.builder(); + // Require length 2. + builder.add(new double[] {42.0, 46.0}); + builder.add(new double[] {42.0, 46.0, 49.0}); + builder.add(new double[] {-3.0, -46.0, -2.0}); + builder.add(new double[] {-3.0, -46.0, -2.0, 8.0}); + builder.add(new double[] {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0}); + return builder.build(); + } + + static Stream testSort5() { + final Stream.Builder builder = Stream.builder(); + final double[] a = new double[5]; + // Permutations is 5! = 120 + final int shift = 42; + for (int i = 0; i < 5; i++) { + a[0] = i + shift; + for (int j = 0; j < 5; j++) { + if (j == i) { + continue; + } + a[1] = j + shift; + for (int k = 0; k < 5; k++) { + if (k == j || k == i) { + continue; + } + a[2] = k + shift; + for (int l = 0; l < 5; l++) { + if (l == k || l == j || l == i) { + continue; + } + a[3] = l + shift; + for (int m = 0; m < 5; m++) { + if (m == l || m == k || m == j || m == i) { + continue; + } + a[4] = m + shift; + builder.add(a.clone()); + } + } + } + } + } + return builder.build(); + } + + static Stream testSort5IsSorted() { + final Stream.Builder builder = Stream.builder(); + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(); + for (int n = 8; n < 256; n *= 2) { + for (int i = 0; i < 10; i++) { + final int length = rng.nextInt(n, n * 2); + builder.add(rng.doubles(length).toArray()); + } + } + return builder.build(); + } + + /** + * This is not a test. It creates data and runs the pivoting strategy. + * The true locations of the pivots are discovered in the data and this + * printed to file. Summary statistics are reported to the console; these + * can be added to the Javadoc for the strategy. + */ + @ParameterizedTest + @MethodSource + @Disabled("Used for testing") + void testDistribution(DualPivotingStrategy ps, int n, int samples) { + final String dir = System.getProperty("java.io.tmpdir"); + final Path path = Paths.get(dir, String.format("%s_%d_%d.txt", ps, n, samples)); + final DescriptiveStatistics[] s = new DescriptiveStatistics[] { + new DescriptiveStatistics(), new DescriptiveStatistics(), new DescriptiveStatistics() + }; + try (BufferedWriter bw = Files.newBufferedWriter(path); + Formatter f = new Formatter(bw)) { + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(); + final double[] data = new double[n]; + final int[] pivot2 = {0}; + final int right = n - 1; + for (int i = 0; i < samples; i++) { + for (int j = 0; j < n; j++) { + // Assume 2^32 possible items is enough to avoid large number of clashes. + // The alternative is to create a natural sequence and shuffle. + data[j] = rng.nextInt(); + } + final int pivot1 = ps.pivotIndex(data, 0, right, pivot2); + // Find pivot locations in the array + int lo = 0; + int hi = 0; + final double p1 = data[pivot1]; + final double p2 = data[pivot2[0]]; + for (final double x : data) { + if (x < p1) { + lo++; + } + if (x > p2) { + hi++; + } + } + final double third1 = (double) lo / n; + final double third3 = (double) hi / n; + final double third2 = 1 - third1 - third3; + f.format("%s %s %s%n", third1, third2, third3); + s[0].addValue(third1); + s[1].addValue(third2); + s[2].addValue(third3); + } + // Get the pivot locations on sorted data + final int[] p2 = {0}; + final int p1 = ps.pivotIndex( + IntStream.range(0, n).asDoubleStream().toArray(), 0, n - 1, p2); + TestUtils.printf("%s n=%d len=%d : %6.4f %6.4f%n", ps, samples, n, (p1 + 1.0) / n, (p2[0] + 1.0) / n); + TestUtils.printf(" * %8s %8s %8s %8s %8s %8s%n", "min", "max", "mean", "sd", "median", "skew"); + for (int i = 0; i < s.length; i++) { + final DescriptiveStatistics d = s[i]; + TestUtils.printf(" * [%d] %8.4f %8.4f %8.4f %8.4f %8.4f %8.4f%n", + i + 1, d.getMin(), d.getMax(), d.getMean(), + d.getStandardDeviation(), d.getPercentile(50), d.getSkewness()); + } + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + } + + static Stream testDistribution() { + final Stream.Builder builder = Stream.builder(); + // Use to build the tertiles statistics + for (final DualPivotingStrategy s : DualPivotingStrategy.values()) { + builder.add(Arguments.of(s, 1000, 100000)); + } + + // On small data the sort 5 method has skewed density. + //builder.add(Arguments.of(DualPivotingStrategy.MEDIANS, 30, 1000000)); + //builder.add(Arguments.of(DualPivotingStrategy.SORT_5, 30, 1000000)); + + return builder.build(); + } + + /** + * This is not a test. It prints out the indices used by the strategy and + * where they are located in an array. + */ + @ParameterizedTest + @EnumSource(value = DualPivotingStrategy.class, + // Methods with unbiased tertiles on random data + names = {"MEDIANS", "SORT_5", "SORT_5B", "SORT_5C", "SORT_8", "SORT_11"}) + @Disabled("Used for testing") + void testSampledIndices(DualPivotingStrategy ps) { + // All current strategies work with <=25 values + final int n = ps.getSampledIndices(0, 25).length; + TestUtils.printf("%s n=%d%n", ps, n); + //for (int i = n; i < n + 100; i++) { + for (int i = n; i <= 2048; i *= 2) { + final int[] indices = ps.getSampledIndices(0, i - 1); + final double d = i; + TestUtils.printf("%4d : %s : %s%n", i, + Arrays.stream(indices).mapToObj(p -> String.format("%4d", p)) + .collect(Collectors.joining(", ", "[", "]")), + Arrays.stream(indices).mapToObj(p -> String.format("%.3f", (p + 1) / d)) + .collect(Collectors.joining(", ", "[", "]"))); + } + } +} diff --git a/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/HashIndexSetTest.java b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/HashIndexSetTest.java new file mode 100644 index 000000000..37827143b --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/HashIndexSetTest.java @@ -0,0 +1,158 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.util.Arrays; +import java.util.BitSet; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.apache.commons.rng.UniformRandomProvider; +import org.apache.commons.rng.simple.RandomSource; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * Test for {@link HashIndexSet}. + */ +class HashIndexSetTest { + + @Test + void testInvalidCapacityThrows() { + final int maxCapacity = 1 << 29; + Assertions.assertThrows(IllegalArgumentException.class, () -> new HashIndexSet(maxCapacity + 1)); + Assertions.assertThrows(IllegalArgumentException.class, () -> new HashIndexSet(Integer.MAX_VALUE)); + } + + @Test + void testInvalidIndexThrows() { + final HashIndexSet set = new HashIndexSet(16); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> set.add(-1)); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> set.add(Integer.MIN_VALUE)); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> set.contains(-1)); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> set.contains(Integer.MIN_VALUE)); + } + + @ParameterizedTest + @ValueSource(ints = {10, 32}) + void testCapacityExceededThrows(int capacity) { + final HashIndexSet set = new HashIndexSet(capacity); + IntStream.range(0, capacity).forEach(set::add); + Assertions.assertEquals(capacity, set.size()); + // Now add more and expect an exception. + // With a load factor of 0.5 we can only add up to twice the requested + // capacity before an exception occurs. Add this and expect an exception. + Assertions.assertThrows(IllegalStateException.class, () -> { + for (int i = capacity, upperLimit = 2 * capacity; i < upperLimit; i++) { + set.add(i); + } + }); + } + + @Test + void testMemoryFootprint() { + // 16 is the minimum size + final long intBytes = Integer.BYTES; + Assertions.assertEquals(intBytes * 16, HashIndexSet.memoryFootprint(-1)); + Assertions.assertEquals(intBytes * 16, HashIndexSet.memoryFootprint(8)); + // Size is next-power-of-2(capacity * 2) + Assertions.assertEquals(intBytes * 32, HashIndexSet.memoryFootprint(16)); + Assertions.assertEquals(intBytes * 64, HashIndexSet.memoryFootprint(17)); + Assertions.assertEquals(intBytes * 64, HashIndexSet.memoryFootprint(31)); + Assertions.assertEquals(intBytes * 64, HashIndexSet.memoryFootprint(32)); + Assertions.assertEquals(intBytes * 128, HashIndexSet.memoryFootprint(33)); + // Maximum capacity + Assertions.assertEquals(intBytes * (1 << 30), HashIndexSet.memoryFootprint(1 << 29)); + // Too big for an int[] array + Assertions.assertEquals(intBytes * (1L << 31), HashIndexSet.memoryFootprint((1 << 29) + 1)); + Assertions.assertEquals(intBytes * (1L << 31), HashIndexSet.memoryFootprint(1 << 30)); + Assertions.assertEquals(intBytes * (1L << 32), HashIndexSet.memoryFootprint(Integer.MAX_VALUE)); + } + + @ParameterizedTest + @MethodSource(value = {"testIndices"}) + void testAddContains(int[] indices, int capacity) { + final HashIndexSet set = new HashIndexSet(capacity); + final BitSet ref = new BitSet(capacity); + for (final int i : indices) { + // Add returns true if not already present + Assertions.assertEquals(!ref.get(i), set.add(i), () -> String.valueOf(i)); + ref.set(i); + Assertions.assertTrue(set.contains(i), () -> String.valueOf(i)); + } + Assertions.assertEquals(ref.cardinality(), set.size(), "Size"); + } + + @ParameterizedTest + @MethodSource(value = {"testIndices"}) + void testToArray(int[] indices, int capacity) { + final HashIndexSet set = new HashIndexSet(capacity); + final BitSet ref = new BitSet(capacity); + Arrays.stream(indices).forEach(i -> { + set.add(i); + ref.set(i); + }); + final int[] e = ref.stream().toArray(); + Assertions.assertEquals(e.length, set.size(), "Size"); + final int[] a = new int[e.length]; + set.toArray(a); + Arrays.sort(a); + Assertions.assertArrayEquals(e, a); + + // Write to a longer array + int[] original = indices.clone(); + final int len = set.toArray(indices); + int[] x = Arrays.copyOf(indices, len); + Arrays.sort(x); + Assertions.assertArrayEquals(e, x); + // Check rest of the array is untouched + if (len < indices.length) { + x = Arrays.copyOfRange(indices, len, indices.length); + original = Arrays.copyOfRange(original, len, original.length); + Assertions.assertArrayEquals(original, x); + } + } + + static Stream testIndices() { + final Stream.Builder builder = Stream.builder(); + + builder.accept(Arguments.of(new int[] {1, 2}, 10)); + builder.accept(Arguments.of(new int[] {1, 2, 3, 4, 5}, 10)); + + // Add duplicates + builder.accept(Arguments.of(new int[] {1, 1, 1, 2, 3, 4, 5, 6, 7}, 10)); + builder.accept(Arguments.of(new int[] {5, 6, 2, 2, 3, 8, 1, 1, 4, 3}, 10)); + builder.accept(Arguments.of(new int[] {2, 2, 2, 2, 2}, 10)); + builder.accept(Arguments.of(new int[] {2000, 2001, 2000, 2001}, 2010)); + + final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create(); + for (final int size : new int[] {5, 500}) { + // Sparse + builder.accept(Arguments.of(rng.ints(10, 0, size).toArray(), size)); + // With duplicates + builder.accept(Arguments.of(rng.ints(size, 0, size).toArray(), size)); + builder.accept(Arguments.of(rng.ints(size, 0, size >> 1).toArray(), size)); + builder.accept(Arguments.of(rng.ints(size, 0, size >> 2).toArray(), size)); + } + + return builder.build(); + } +} diff --git a/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/IndexIteratorTest.java b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/IndexIteratorTest.java new file mode 100644 index 000000000..d2fb7aa3c --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/IndexIteratorTest.java @@ -0,0 +1,375 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.util.Arrays; +import java.util.BitSet; +import java.util.function.BiFunction; +import java.util.stream.Stream; +import org.apache.commons.rng.UniformRandomProvider; +import org.apache.commons.rng.simple.RandomSource; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * Test for {@link IndexIterator} implementations. + */ +class IndexIteratorTest { + @ParameterizedTest + @ValueSource(ints = {0, 1, 42, Integer.MAX_VALUE - 1}) + void testSingleIndex(int k) { + final IndexIterator iterator = IndexIterators.ofIndex(k); + Assertions.assertEquals(k, iterator.left()); + Assertions.assertEquals(k, iterator.right()); + Assertions.assertEquals(k, iterator.end()); + Assertions.assertFalse(iterator.next()); + Assertions.assertFalse(iterator.positionAfter(k + 1)); + Assertions.assertFalse(iterator.positionAfter(k)); + Assertions.assertTrue(iterator.positionAfter(k - 1)); + Assertions.assertEquals(k, iterator.left()); + Assertions.assertEquals(k, iterator.right()); + Assertions.assertEquals(k, iterator.end()); + } + + @ParameterizedTest + @CsvSource({ + "0, 0", + "0, 10", + "5615236, 1263818376", + }) + void testSingleInterval(int l, int r) { + final IndexIterator iterator = IndexIterators.ofInterval(l, r); + Assertions.assertEquals(l, iterator.left()); + Assertions.assertEquals(r, iterator.right()); + Assertions.assertEquals(r, iterator.end()); + Assertions.assertFalse(iterator.next()); + Assertions.assertFalse(iterator.positionAfter(r + 1)); + Assertions.assertFalse(iterator.positionAfter(r)); + Assertions.assertTrue(iterator.positionAfter(r - 1)); + Assertions.assertEquals(r > l, iterator.positionAfter(l)); + Assertions.assertEquals(r > l, iterator.positionAfter((l + r) >>> 1)); + Assertions.assertEquals(l, iterator.left()); + Assertions.assertEquals(r, iterator.right()); + Assertions.assertEquals(r, iterator.end()); + } + + @Test + void testKeyIndexIteratorInvalidIndicesThrows() { + assertInvalidIndicesThrows(KeyIndexIterator::of); + // Invalid indices: not in [0, Integer.MAX_VALUE) + Assertions.assertThrows(IllegalArgumentException.class, + () -> KeyIndexIterator.of(new int[] {-1, 2, 3}, 3)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> KeyIndexIterator.of(new int[] {1, 2, Integer.MAX_VALUE}, 3)); + } + + private static void assertInvalidIndicesThrows(BiFunction constructor) { + // Size zero + Assertions.assertThrows(IllegalArgumentException.class, () -> constructor.apply(new int[0], 0)); + Assertions.assertThrows(IllegalArgumentException.class, () -> constructor.apply(new int[10], 0)); + // Not sorted + Assertions.assertThrows(IllegalArgumentException.class, + () -> constructor.apply(new int[] {3, 2, 1}, 3)); + // Not unique + Assertions.assertThrows(IllegalArgumentException.class, + () -> constructor.apply(new int[] {1, 2, 2, 3}, 4)); + } + + @ParameterizedTest + @MethodSource(value = {"testIterator"}) + void testKeyIndexIterator(int[] indices) { + // This defaults to joining keys with a minimum separation of 2 + assertIterator(KeyIndexIterator::of, indices, 2, 0); + } + + @ParameterizedTest + @MethodSource(value = {"testIterator"}) + void testCompressedIndexIterator1(int[] indices) { + assertIterator((k, n) -> CompressedIndexSet.iterator(1, k, k.length), indices, 0, 1); + } + + @ParameterizedTest + @MethodSource(value = {"testIterator"}) + void testCompressedIndexIterator2(int[] indices) { + assertIterator((k, n) -> CompressedIndexSet.iterator(2, k, k.length), indices, 0, 2); + } + + @ParameterizedTest + @MethodSource(value = {"testIterator"}) + void testCompressedIndexIterator3(int[] indices) { + assertIterator((k, n) -> CompressedIndexSet.iterator(3, k, k.length), indices, 0, 3); + } + + @ParameterizedTest + @MethodSource(value = {"testIterator"}) + void testCompressedIndexIterator5(int[] indices) { + assertIterator((k, n) -> CompressedIndexSet.iterator(5, k, k.length), indices, 0, 5); + } + + /** + * Assert iterating along the indices. + * + *

Supported compressed indices. Each index is compressed by a power of 2, then + * decompressed to create a range of indices {@code [from, to)}. See + * {@link #createBitSet(int[], int)} for details of the indices that must be iterated + * over. + * + * @param constructor Iterator constructor. + * @param indices Indices. + * @param separation Minimum separation between uncompressed indices. + * @param compression Compression level (for compressed indices). + */ + private static void assertIterator(BiFunction constructor, + int[] indices, int separation, int compression) { + + // Reference + final BitSet set = createBitSet(indices, compression); + final int first = indices[0]; + final int last = indices[indices.length - 1]; + + IndexIterator iterator = constructor.apply(indices, indices.length); + Assertions.assertEquals(last, iterator.end()); + // Check invariants + Assertions.assertTrue(iterator.left() <= iterator.right()); + Assertions.assertTrue(iterator.right() <= iterator.end()); + + // Expected + int l = first; + int r; + if (compression == 0) { + r = l; + while (true) { + final int n = set.nextSetBit(r + 1); + if (n < 0 || r + separation < n) { + break; + } + r = n; + } + } else { + r = Math.min(last, set.nextClearBit(l) - 1); + } + Assertions.assertEquals(l, iterator.left(), "left"); + Assertions.assertEquals(r, iterator.right(), "right"); + + // Iterate + while (iterator.right() < iterator.end()) { + final int previous = iterator.right(); + + Assertions.assertTrue(iterator.next()); + Assertions.assertTrue(previous < iterator.left(), "Did not advance"); + // Check invariants + Assertions.assertTrue(iterator.left() <= iterator.right()); + Assertions.assertTrue(iterator.right() <= iterator.end()); + + // Expected + l = set.nextSetBit(previous + 1); + if (compression == 0) { + r = l; + while (true) { + final int n = set.nextSetBit(r + 1); + if (n < 0 || r + separation < n) { + break; + } + r = n; + } + } else { + r = Math.min(last, set.nextClearBit(l) - 1); + } + Assertions.assertEquals(l, iterator.left(), "left"); + Assertions.assertEquals(r, iterator.right(), "right"); + } + Assertions.assertEquals(last, iterator.right()); + Assertions.assertFalse(iterator.next()); + Assertions.assertEquals(last, iterator.right()); + Assertions.assertFalse(iterator.next()); + + // Test position after + iterator = constructor.apply(indices, indices.length); + Assertions.assertFalse(iterator.positionAfter(last + 1)); + Assertions.assertEquals(last, iterator.right()); + + for (final int jump : new int[] {1, 2, 3}) { + iterator = constructor.apply(indices, indices.length); + final IndexIterator iterator2 = constructor.apply(indices, indices.length); + + for (int i = jump; i < indices.length; i += jump) { + final int k = indices[i]; + if (k == last) { + Assertions.assertFalse(iterator.positionAfter(k)); + Assertions.assertEquals(k, iterator.right()); + } else { + Assertions.assertTrue(iterator.positionAfter(k)); + Assertions.assertTrue(k < iterator.right()); + } + // Iterate using next. Ensures the sequence output is the same. + boolean result = true; + while (result && iterator2.right() <= k) { + result = iterator2.next(); + } + // Allowed to be clipped to k+1 + if (iterator2.left() > k) { + Assertions.assertEquals(iterator2.left(), iterator.left(), () -> "left after " + k); + } else { + Assertions.assertTrue(iterator.left() <= k + 1, () -> "left after " + k); + } + Assertions.assertEquals(iterator2.right(), iterator.right(), () -> "right after " + k); + + // Expected + if (compression == 0) { + r = set.nextSetBit(Math.min(k + 1, last)); + while (true) { + final int n = set.nextSetBit(r + 1); + if (n < 0 || r + separation < n) { + break; + } + r = n; + } + l = r; + while (true) { + final int n = set.previousSetBit(l - 1); + if (n < 0 || l - separation > n) { + break; + } + l = n; + } + } else { + if (set.get(k + 1)) { + l = set.previousClearBit(k + 1) + 1; + r = Math.min(last, set.nextClearBit(k + 1) - 1); + } else { + if (k == last) { + r = last; + l = set.previousClearBit(last) + 1; + } else { + l = set.nextSetBit(k + 1); + r = Math.min(last, set.nextClearBit(l + 1) - 1); + } + } + } + // Allowed to be clipped to k+1 + if (l > k) { + Assertions.assertEquals(l, iterator.left(), "left"); + } else { + Assertions.assertTrue(iterator.left() <= k + 1, "left"); + } + Assertions.assertEquals(r, iterator.right(), "right"); + } + Assertions.assertFalse(iterator.positionAfter(last)); + Assertions.assertEquals(last, iterator.right()); + Assertions.assertFalse(iterator.positionAfter(last)); + Assertions.assertEquals(last, iterator.right()); + } + } + + static Stream testIterator() { + final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create(); + final Stream.Builder builder = Stream.builder(); + builder.accept(new int[] {4}); + builder.accept(new int[] {4, 78}); + builder.accept(new int[] {4, 78, 999}); + builder.accept(new int[] {4, 78, 79, 999}); + builder.accept(new int[] {4, 5, 6, 7, 8}); + for (final int size : new int[] {10, 50, 500}) { + for (final int n : new int[] {2, 5, 10}) { + final int[] a = rng.ints(n, 0, size).distinct().sorted().toArray(); + builder.accept(a.clone()); + // Force use of index 0 + a[0] = 0; + builder.accept(a); + } + } + return builder.build(); + } + + /** + * Creates the BitSet using the indices. + * + *

Compressed indices are created using {@code c = (i - min) >>> compression}. + * This is then decompressed {@code from = (c << compression) + min}. The BitSet + * has all bits set in {@code [from, from + (1 << compression))}. + * + * @param indices Indices. + * @param compression Compression level. + * @return the set + */ + private static BitSet createBitSet(int[] indices, int compression) { + final int max = indices[indices.length - 1] + 1 + (1 << compression); + final BitSet set = new BitSet(max); + if (compression == 0) { + Arrays.stream(indices).forEach(set::set); + } else { + final int min = indices[0]; + final int width = 1 << compression; + Arrays.stream(indices).forEach(i -> { + i = (((i - min) >>> compression) << compression) + min; + set.set(i, i + width); + }); + } + return set; + } + + /** + * Output the iterator intervals for the indices. + */ + @ParameterizedTest + @MethodSource + @Disabled("This is not a test") + void testIntervals(int compression, int[] indices) { + IndexIterator iterator; + if (compression == 0) { + final int unique = Sorting.sortIndices(indices, indices.length); + iterator = KeyIndexIterator.of(indices, unique); + } else { + iterator = CompressedIndexSet.iterator(compression, indices, indices.length); + } + TestUtils.printf("Compression %d%n", compression); + do { + final int l = iterator.left(); + final int r = iterator.right(); + TestUtils.printf("%d %d : %d%n", l, r, r - l + 1); + } while (iterator.next()); + } + + static Stream testIntervals() { + final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create(); + final Stream.Builder builder = Stream.builder(); + int[] a; + // Unsaturated: mean spacing = 200 + a = rng.ints(5, 0, 1000).toArray(); + for (final int c : new int[] {0, 2, 5}) { + builder.accept(Arguments.of(c, a)); + } + // Saturated: mean spacing = 10 + a = rng.ints(100, 0, 1000).toArray(); + for (final int c : new int[] {0, 2, 5}) { + builder.accept(Arguments.of(c, a)); + } + // Big data: mean spacing = 100 + a = rng.ints(1000, 0, 100000).toArray(); + for (final int c : new int[] {5, 8}) { + builder.accept(Arguments.of(c, a)); + } + return builder.build(); + } +} diff --git a/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/IndexSetTest.java b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/IndexSetTest.java new file mode 100644 index 000000000..0c49957c1 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/IndexSetTest.java @@ -0,0 +1,403 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.util.Arrays; +import java.util.BitSet; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.apache.commons.rng.UniformRandomProvider; +import org.apache.commons.rng.simple.RandomSource; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Test for {@link IndexSet}. + */ +class IndexSetTest { + + @Test + void testInvalidRangeThrows() { + Assertions.assertThrows(IllegalArgumentException.class, () -> IndexSet.ofRange(-1, 3)); + Assertions.assertThrows(IllegalArgumentException.class, () -> IndexSet.ofRange(0, -1)); + Assertions.assertThrows(IllegalArgumentException.class, () -> IndexSet.ofRange(456, 123)); + Assertions.assertThrows(IllegalArgumentException.class, () -> IndexSet.of(new int[0])); + } + + @Test + void testInvalidPivotCacheRangeThrows() { + final int left = 10; + final int right = 156; + final IndexSet set = IndexSet.ofRange(left, right); + Assertions.assertThrows(IllegalArgumentException.class, () -> set.asScanningPivotCache(left - 1, right)); + Assertions.assertThrows(IllegalArgumentException.class, () -> set.asScanningPivotCache(right, left)); + Assertions.assertDoesNotThrow(() -> set.asScanningPivotCache(left, right)); + Assertions.assertDoesNotThrow(() -> set.asScanningPivotCache(left + 1, right - 1)); + // We must know the capacity (the highest bit that can be stored). + final int noOfLongs = 1 + (right - left) / Long.SIZE; + final int highBit = left + noOfLongs * Long.SIZE - 1; + // Show this is correct + set.set(highBit); + final int capacity = set.nextClearBit(highBit); + Assertions.assertEquals(highBit + 1, capacity); + Assertions.assertDoesNotThrow(() -> set.asScanningPivotCache(left, highBit)); + Assertions.assertThrows(IllegalArgumentException.class, () -> set.asScanningPivotCache(left, capacity)); + } + + @Test + void testMemoryFootprint() { + // Memory footprint is number of bits that has to be stored, rounded up to + // a multiple of 64 + final long longBytes = Long.BYTES; + Assertions.assertEquals(longBytes * 1, IndexSet.memoryFootprint(0, 0)); + Assertions.assertEquals(longBytes * 1, IndexSet.memoryFootprint(0, 63)); + Assertions.assertEquals(longBytes * 2, IndexSet.memoryFootprint(0, 64)); + Assertions.assertEquals(longBytes * 2, IndexSet.memoryFootprint(0, 127)); + Assertions.assertEquals(longBytes * 3, IndexSet.memoryFootprint(0, 128)); + // Test the documented 64 * ceil((right - left + 1) / 64 + Assertions.assertEquals(longBytes * Math.ceil(128 / 64.0), IndexSet.memoryFootprint(0, 127)); + Assertions.assertEquals(longBytes * Math.ceil(129 / 64.0), IndexSet.memoryFootprint(0, 128)); + // Maximum capacity + Assertions.assertEquals(longBytes * (1 << 25), IndexSet.memoryFootprint(0, Integer.MAX_VALUE)); + // Offset from zero + final int left = 12563; + Assertions.assertEquals(longBytes * 1, IndexSet.memoryFootprint(left, left + 0)); + Assertions.assertEquals(longBytes * 1, IndexSet.memoryFootprint(left, left + 63)); + Assertions.assertEquals(longBytes * 2, IndexSet.memoryFootprint(left, left + 64)); + Assertions.assertEquals(longBytes * 2, IndexSet.memoryFootprint(left, left + 127)); + Assertions.assertEquals(longBytes * 3, IndexSet.memoryFootprint(left, left + 128)); + } + + @ParameterizedTest + @MethodSource + void testGetSet(int[] indices, int n) { + final IndexSet set = createIndexSet(indices); + final BitSet ref = new BitSet(n); + for (final int i : indices) { + Assertions.assertEquals(ref.get(i), set.get(i), () -> String.valueOf(i)); + set.set(i); + ref.set(i); + Assertions.assertTrue(set.get(i)); + } + } + + @ParameterizedTest + @MethodSource(value = {"testGetSet"}) + void testSetRange1(int[] indices, int n) { + final IndexSet set = createIndexSet(indices); + final BitSet ref = new BitSet(n); + for (final int i : indices) { + Assertions.assertEquals(ref.get(i), set.get(i)); + // inclusive end + set.set(i, i); + ref.set(i); + Assertions.assertTrue(set.get(i)); + } + } + + @ParameterizedTest + @MethodSource(value = {"testGetSet"}) + void testSetRange(int[] indices, int n) { + final IndexSet set = createIndexSet(indices); + Arrays.sort(indices); + for (int i = 1; i < indices.length; i++) { + final int from = indices[i - 1]; + final int to = indices[i]; + // inclusive end so skip duplicates + if (from == to) { + continue; + } + for (int j = from; j < to; j++) { + Assertions.assertFalse(set.get(j)); + } + set.set(from, to - 1); + for (int j = from; j < to; j++) { + Assertions.assertTrue(set.get(j)); + } + } + } + + @ParameterizedTest + @MethodSource(value = {"testGetSet"}) + void testPreviousNextSetBit(int[] indices, int n) { + final IndexSet set = createIndexSet(indices); + final BitSet ref = new BitSet(n); + Arrays.sort(indices); + Assertions.assertEquals(-1, set.previousSetBit(0)); + Assertions.assertEquals(-1, set.nextSetBit(0)); + final int highBit = indices[indices.length - 1]; + Assertions.assertEquals(-1, set.previousSetBit(highBit)); + Assertions.assertEquals(-1, set.nextSetBit(highBit)); + for (int i = 1; i < indices.length; i++) { + final int from = indices[i - 1]; + final int to = indices[i]; + final int middle = (from + to) >>> 1; + Assertions.assertEquals(ref.previousSetBit(middle), set.previousSetBit(middle)); + Assertions.assertEquals(ref.nextSetBit(middle), set.nextSetBit(middle)); + set.set(from); + set.set(to); + ref.set(from); + ref.set(to); + Assertions.assertEquals(ref.previousSetBit(middle), set.previousSetBit(middle)); + Assertions.assertEquals(ref.nextSetBit(middle), set.nextSetBit(middle)); + } + } + + @ParameterizedTest + @MethodSource(value = {"testGetSet"}) + void testPreviousNextClearBit(int[] indices, int n) { + final IndexSet set = createIndexSet(indices); + final BitSet ref = new BitSet(n); + Arrays.sort(indices); + // Note: Different from using a BitSet. The IndexSet does not support + // clear bits outside of the range used to construct it. + final int highBit = indices[indices.length - 1]; + + // When empty this should return the start index. Only call with indices <= right. + Assertions.assertEquals(0, set.nextClearBit(0)); + Assertions.assertEquals(indices[0] - 1, set.nextClearBit(indices[0] - 1)); + Assertions.assertEquals(indices[0], set.nextClearBit(indices[0])); + Assertions.assertEquals(highBit, set.nextClearBit(highBit)); + Assertions.assertEquals(0, set.previousClearBit(0)); + Assertions.assertEquals(indices[0] - 1, set.previousClearBit(indices[0] - 1)); + Assertions.assertEquals(indices[0], set.previousClearBit(indices[0])); + Assertions.assertEquals(highBit, set.previousClearBit(highBit)); + + for (int i = 1; i < indices.length; i++) { + final int from = indices[i - 1]; + final int to = indices[i]; + final int middle = (from + to) >>> 1; + Assertions.assertEquals(ref.nextClearBit(middle), set.nextClearBit(middle)); + Assertions.assertEquals(ref.previousClearBit(middle), set.previousClearBit(middle)); + // inclusive end + set.set(from, to); + ref.set(from, to + 1); + Assertions.assertEquals(ref.nextClearBit(middle), set.nextClearBit(middle)); + Assertions.assertEquals(ref.previousClearBit(middle), set.previousClearBit(middle)); + } + } + + @ParameterizedTest + @MethodSource(value = {"testGetSet", "testForEachToArray"}) + void testForEachToArray(int[] indices, int n) { + final IndexSet set = createIndexSet(indices); + final BitSet ref = new BitSet(n); + Arrays.stream(indices).forEach(i -> { + set.set(i); + ref.set(i); + }); + final int[] e = ref.stream().toArray(); + // Check the output is the same + final int[] a = new int[e.length]; + final int[] c = {0}; + set.forEach(i -> a[c[0]++] = i); + Assertions.assertArrayEquals(e, a); + + // Test toArray + + int[] original = indices.clone(); + + int len = set.toArray(indices); + int[] x = Arrays.copyOf(indices, len); + Assertions.assertArrayEquals(e, x); + // Check rest of the array is untouched + if (len < indices.length) { + final int[] y = Arrays.copyOfRange(indices, len, indices.length); + final int[] z = Arrays.copyOfRange(original, len, original.length); + Assertions.assertArrayEquals(z, y); + } + + // Repeat with toArray2 + + len = set.toArray2(indices); + x = Arrays.copyOf(indices, len); + Assertions.assertArrayEquals(e, x); + // Check rest of the array is untouched + if (len < indices.length) { + final int[] y = Arrays.copyOfRange(indices, len, indices.length); + final int[] z = Arrays.copyOfRange(original, len, original.length); + Assertions.assertArrayEquals(z, y); + } + } + + static Stream testForEachToArray() { + final Stream.Builder builder = Stream.builder(); + // Add duplicates + builder.accept(Arguments.of(new int[] {1, 1, 1, 2, 3, 4, 5, 6, 7}, 10)); + builder.accept(Arguments.of(new int[] {5, 6, 2, 2, 3, 8, 1, 1, 4, 3}, 10)); + builder.accept(Arguments.of(new int[] {2, 2, 2, 2, 2}, 10)); + builder.accept(Arguments.of(new int[] {2000, 2001, 2000, 2001}, 2010)); + return builder.build(); + } + + @ParameterizedTest + @MethodSource(value = {"testGetSet"}) + void testOfIndices(int[] indices) { + assertOfIndices(indices, -1); + } + + @ParameterizedTest + @MethodSource(value = {"testGetSet"}) + void testOfIndicesTruncated(int[] indices) { + Assumptions.assumeTrue(indices.length > 1); + final int n = ThreadLocalRandom.current().nextInt(1, indices.length); + assertOfIndices(indices, n); + } + + private static void assertOfIndices(int[] indices, int n) { + final IndexSet set = n < 0 ? IndexSet.of(indices) : IndexSet.of(indices, n); + final BitSet ref = new BitSet(); + final int upper = n < 0 ? indices.length : n; + for (int i = 0; i < upper; i++) { + ref.set(indices[i]); + Assertions.assertTrue(set.get(indices[i])); + } + final int[] e = ref.stream().toArray(); + final int[] a = new int[e.length]; + final int[] c = {0}; + set.forEach(i -> a[c[0]++] = i); + Assertions.assertArrayEquals(e, a); + } + + static Stream testGetSet() { + final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create(); + final Stream.Builder builder = Stream.builder(); + for (final int size : new int[] {5, 500}) { + final int[] a = rng.ints(10, 0, size).toArray(); + builder.accept(Arguments.of(a.clone(), size)); + // Force use of index 0 + a[0] = 0; + builder.accept(Arguments.of(a, size)); + } + // Large offset with an index at the end of the range + final int n = 513; + // Use 1, 2, or 3 longs for storage + builder.accept(Arguments.of(new int[] {n - 13, n - 1}, n)); + builder.accept(Arguments.of(new int[] {n - 78, n - 1}, n)); + builder.accept(Arguments.of(new int[] {n - 137, n - 1}, n)); + // Uses a capacity of 512. BitSet will increase this to 512 + 64 + // to store index 513. + builder.accept(Arguments.of(new int[] {1, n - 1}, n)); + return builder.build(); + } + + /** + * Creates the index set using the min/max of the indices. + * + * @param indices Indices. + * @return the set + */ + private static IndexSet createIndexSet(int[] indices) { + final int min = Arrays.stream(indices).min().getAsInt(); + final int max = Arrays.stream(indices).max().getAsInt(); + return IndexSet.ofRange(min, max); + } + + @Test + void testCardinalityEmpty() { + final IndexSet set = IndexSet.ofRange(34, 219); + Assertions.assertEquals(0, set.cardinality()); + Assertions.assertEquals(0, set.cardinality2()); + Assertions.assertEquals(0, set.cardinality4()); + Assertions.assertEquals(0, set.cardinality8()); + Assertions.assertEquals(0, set.cardinality16()); + Assertions.assertEquals(0, set.cardinality32()); + Assertions.assertEquals(0, set.cardinality64()); + } + + @ParameterizedTest + @MethodSource(value = {"testGetSet", "testCardinality"}) + void testCardinality(int[] indices) { + // No compression here but re-use the method for simplicity + Assertions.assertEquals(compressedCardinality(indices, 0), IndexSet.of(indices).cardinality()); + } + + @ParameterizedTest + @MethodSource(value = {"testGetSet", "testCardinality"}) + void testCardinality2(int[] indices) { + Assertions.assertEquals(compressedCardinality(indices, 1), IndexSet.of(indices).cardinality2()); + } + + @ParameterizedTest + @MethodSource(value = {"testGetSet", "testCardinality"}) + void testCardinality4(int[] indices) { + Assertions.assertEquals(compressedCardinality(indices, 2), IndexSet.of(indices).cardinality4()); + } + + @ParameterizedTest + @MethodSource(value = {"testGetSet", "testCardinality"}) + void testCardinality8(int[] indices) { + Assertions.assertEquals(compressedCardinality(indices, 3), IndexSet.of(indices).cardinality8()); + } + + @ParameterizedTest + @MethodSource(value = {"testGetSet", "testCardinality"}) + void testCardinality16(int[] indices) { + Assertions.assertEquals(compressedCardinality(indices, 4), IndexSet.of(indices).cardinality16()); + } + + @ParameterizedTest + @MethodSource(value = {"testGetSet", "testCardinality"}) + void testCardinality32(int[] indices) { + Assertions.assertEquals(compressedCardinality(indices, 5), IndexSet.of(indices).cardinality32()); + } + + @ParameterizedTest + @MethodSource(value = {"testGetSet", "testCardinality"}) + void testCardinality64(int[] indices) { + Assertions.assertEquals(compressedCardinality(indices, 6), IndexSet.of(indices).cardinality64()); + } + + private static int compressedCardinality(int[] indices, int compression) { + final int min = Arrays.stream(indices).min().orElse(0); + final int max = Arrays.stream(indices).max().orElse(64); + final BitSet ref = new BitSet((max - min) >>> compression); + for (final int i : indices) { + ref.set((i - min) >>> compression); + } + return ref.cardinality() << compression; + } + + static Stream testCardinality() { + final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create(); + final Stream.Builder builder = Stream.builder(); + for (int i = 0; i < 64; i++) { + builder.accept(new int[] {i}); + for (final int j = 0; i < 64; i++) { + builder.accept(new int[] {i, j}); + } + } + builder.accept(IntStream.range(0, 64).toArray()); + for (int i = 0; i < 50; i++) { + builder.accept(rng.ints(30, 0, 500).toArray()); + builder.accept(rng.ints(10, 499, 879).toArray()); + builder.accept(rng.ints(2, 0, 64).toArray()); + builder.accept(rng.ints(4, 0, 64).toArray()); + builder.accept(rng.ints(8, 0, 64).toArray()); + builder.accept(rng.ints(16, 0, 64).toArray()); + builder.accept(rng.ints(32, 0, 64).toArray()); + builder.accept(rng.ints(64, 0, 64).toArray()); + } + return builder.build(); + } +} diff --git a/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/KthSelectorTest.java b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/KthSelectorTest.java new file mode 100644 index 000000000..3f7315466 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/KthSelectorTest.java @@ -0,0 +1,410 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.util.Arrays; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.apache.commons.rng.UniformRandomProvider; +import org.apache.commons.rng.simple.RandomSource; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Test for {@link KthSelector}. + */ +class KthSelectorTest { + @ParameterizedTest + @MethodSource + void testPartitionMin(double[] values, int from, int to) { + final double[] sorted = values.clone(); + Arrays.sort(sorted, from, to); + KthSelector.partitionMin(values, from, to); + Assertions.assertEquals(sorted[from], values[from]); + // Check the data is the same + Arrays.sort(values, from, to); + Assertions.assertArrayEquals(sorted, values, "Data destroyed"); + } + + static Stream testPartitionMin() { + final Stream.Builder builder = Stream.builder(); + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(); + for (final int size : new int[] {5, 10}) { + final double[] values = rng.doubles(size).toArray(); + builder.add(Arguments.of(values.clone(), 0, size)); + builder.add(Arguments.of(values.clone(), size >>> 1, size)); + builder.add(Arguments.of(values.clone(), 1, size >>> 1)); + } + return builder.build(); + } + + @ParameterizedTest + @MethodSource + void testPartitionMax(double[] values, int from, int to) { + final double[] sorted = values.clone(); + Arrays.sort(sorted, from, to); + KthSelector.partitionMax(values, from, to); + Assertions.assertEquals(sorted[to - 1], values[to - 1]); + // Check the data is the same + Arrays.sort(values, from, to); + Assertions.assertArrayEquals(sorted, values, "Data destroyed"); + } + + static Stream testPartitionMax() { + final Stream.Builder builder = Stream.builder(); + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(); + for (final int size : new int[] {5, 10}) { + final double[] values = rng.doubles(size).toArray(); + builder.add(Arguments.of(values.clone(), 0, size)); + builder.add(Arguments.of(values.clone(), size >>> 1, size)); + builder.add(Arguments.of(values.clone(), 1, size >>> 1)); + } + return builder.build(); + } + + @ParameterizedTest + @MethodSource + void testSelect(double[] values) { + final double[] sorted = values.clone(); + Arrays.sort(sorted); + final KthSelector selector = new KthSelector(); + final double[] kp1 = new double[1]; + for (int i = 0; i < sorted.length; i++) { + final int k = i; + double[] x = values.clone(); + Assertions.assertEquals(sorted[k], selector.selectSP(x, k, null), () -> "k[" + k + "]"); + Arrays.sort(x); + Assertions.assertArrayEquals(sorted, x, () -> "Data destroyed: k[" + k + "]"); + if (k + 1 < sorted.length) { + x = values.clone(); + Assertions.assertEquals(sorted[k], selector.selectSP(x, k, kp1), () -> "k[" + k + "] with k+1"); + Assertions.assertEquals(sorted[k + 1], kp1[0], () -> "k+1[" + (k + 1) + "]"); + Arrays.sort(x); + Assertions.assertArrayEquals(sorted, x, () -> "Data destroyed: k[" + k + "] with k+1"); + } + } + } + + @ParameterizedTest + @MethodSource(value = {"testSelect"}) + void testSelectSPN(double[] values) { + final double[] sorted = values.clone(); + Arrays.sort(sorted); + final KthSelector selector = new KthSelector(); + final double[] kp1 = new double[1]; + for (int i = 0; i < sorted.length; i++) { + final int k = i; + double[] x = values.clone(); + Assertions.assertEquals(sorted[k], selector.selectSPN(x, k, null), () -> "k[" + k + "]"); + Arrays.sort(x); + Assertions.assertArrayEquals(sorted, x, () -> "Data destroyed: k[" + k + "]"); + if (k + 1 < sorted.length) { + x = values.clone(); + Assertions.assertEquals(sorted[k], selector.selectSPN(x, k, kp1), () -> "k[" + k + "] with k+1"); + Assertions.assertEquals(sorted[k + 1], kp1[0], () -> "k+1[" + (k + 1) + "]"); + Arrays.sort(x); + Assertions.assertArrayEquals(sorted, x, () -> "Data destroyed: k[" + k + "] with k+1"); + } + } + } + + @ParameterizedTest + @MethodSource(value = {"testSelect"}) + void testSelectSPWithHeap(double[] values) { + final double[] sorted = values.clone(); + Arrays.sort(sorted); + final KthSelector selector = new KthSelector(); + final double[] kp1 = new double[1]; + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(); + final int[] indices = IntStream.range(0, sorted.length).toArray(); + for (int n = 0; n < 3; n++) { + TestUtils.shuffle(rng, indices); + final double[] x = values.clone(); + final int[] pivotsHeap = KthSelector.createPivotsHeap(sorted.length); + for (int i = 0; i < sorted.length; i++) { + final int k = indices[i]; + Assertions.assertEquals(sorted[k], selector.selectSPH(x, pivotsHeap, k, null), () -> "k[" + k + "]"); + if (k + 1 < sorted.length) { + Assertions.assertEquals(sorted[k], selector.selectSPH(x, pivotsHeap, k, kp1), () -> "k[" + k + "] with k+1"); + Assertions.assertEquals(sorted[k + 1], kp1[0], () -> "k+1[" + (k + 1) + "]"); + } + } + Arrays.sort(x); + Assertions.assertArrayEquals(sorted, x, "Data destroyed"); + } + } + + static Stream testSelect() { + final Stream.Builder builder = Stream.builder(); + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(); + // Sizes above and below the threshold for partitioning + for (final int size : new int[] {5, 50}) { + final double[] values = IntStream.range(0, size).asDoubleStream().toArray(); + final double[] zeros = values.clone(); + final double[] nans = values.clone(); + Arrays.fill(zeros, 0, size >>> 2, -0.0); + Arrays.fill(zeros, size >>> 2, size >>> 1, 0.0); + Arrays.fill(nans, 0, 2, Double.NaN); + for (int i = 0; i < 25; i++) { + builder.add(TestUtils.shuffle(rng, values.clone())); + builder.add(TestUtils.shuffle(rng, zeros.clone())); + } + for (int i = 0; i < 5; i++) { + builder.add(TestUtils.shuffle(rng, nans.clone())); + } + } + return builder.build(); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionSP(double[] values, int[] indices) { + assertPartition(values, indices, new KthSelector()::partitionSP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionSPN(double[] values, int[] indices) { + assertPartition(values, indices, new KthSelector()::partitionSPN); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionSBM(double[] values, int[] indices) { + assertPartition(values, indices, new KthSelector()::partitionSBM); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionBM(double[] values, int[] indices) { + assertPartition(values, indices, new KthSelector()::partitionBM); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionDP(double[] values, int[] indices) { + assertPartition(values, indices, new KthSelector()::partitionDP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionDP5(double[] values, int[] indices) { + assertPartition(values, indices, new KthSelector()::partitionDP5); + } + + static void assertPartition(double[] values, int[] indices, BiConsumer function) { + final double[] data = values.clone(); + final double[] sorted = values.clone(); + Arrays.sort(sorted); + function.accept(data, indices); + if (indices.length == 0) { + return; + } + for (final int k : indices) { + Assertions.assertEquals(sorted[k], data[k], () -> "k[" + k + "]"); + } + // Check partial ordering + Arrays.sort(indices); + int i = 0; + for (final int k : indices) { + final double value = sorted[k]; + while (i < k) { + final int j = i; + Assertions.assertTrue(Double.compare(data[i], value) <= 0, + () -> j + " < " + k); + i++; + } + } + final int k = indices[indices.length - 1]; + final double value = sorted[k]; + while (i < data.length) { + final int j = i; + Assertions.assertTrue(Double.compare(data[i], value) >= 0, + () -> k + " < " + j); + i++; + } + Arrays.sort(data); + Assertions.assertArrayEquals(sorted, data, "Data destroyed"); + } + + static Stream testPartition() { + final Stream.Builder builder = Stream.builder(); + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(123); + // Sizes above and below the threshold for partitioning + for (final int size : new int[] {5, 50}) { + final double[] values = IntStream.range(0, size).asDoubleStream().toArray(); + final double[] zeros = values.clone(); + Arrays.fill(zeros, 0, size >>> 2, -0.0); + Arrays.fill(zeros, size >>> 2, size >>> 1, 0.0); + for (final int k : new int[] {1, 2, 3, size}) { + for (int i = 0; i < 25; i++) { + // Note: Duplicate indices do not matter + final int[] indices = rng.ints(k, 0, size).toArray(); + builder.add(Arguments.of( + TestUtils.shuffle(rng, values.clone()), + indices)); + builder.add(Arguments.of( + TestUtils.shuffle(rng, zeros.clone()), + indices)); + } + } + // min; max; min/max + builder.add(Arguments.of(values.clone(), new int[] {0})); + builder.add(Arguments.of(values.clone(), new int[] {size - 1})); + builder.add(Arguments.of(values.clone(), new int[] {0, size - 1})); + builder.add(Arguments.of(zeros.clone(), new int[] {0})); + builder.add(Arguments.of(zeros.clone(), new int[] {size - 1})); + builder.add(Arguments.of(zeros.clone(), new int[] {0, size - 1})); + } + builder.add(Arguments.of(new double[] {}, new int[0])); + builder.add(Arguments.of(new double[] {Double.NaN}, new int[] {0})); + builder.add(Arguments.of(new double[] {Double.NaN, Double.NaN, Double.NaN}, new int[] {2})); + builder.add(Arguments.of(new double[] {Double.NaN, 0.0, -0.0, Double.NaN}, new int[] {3})); + builder.add(Arguments.of(new double[] {Double.NaN, 0.0, -0.0}, new int[] {0, 2})); + builder.add(Arguments.of(new double[] {Double.NaN, 1.23, 0.0, -4.56, -0.0, Double.NaN}, new int[] {0, 1, 3})); + return builder.build(); + } + + @ParameterizedTest + @MethodSource(value = {"testSort"}) + void testSortSP(double[] values) { + assertSort(values, new KthSelector()::sortSP); + } + + @ParameterizedTest + @MethodSource(value = {"testSort"}) + void testSortBM(double[] values) { + assertSort(values, new KthSelector()::sortBM); + } + + @ParameterizedTest + @MethodSource(value = {"testSort"}) + void testSortSBM(double[] values) { + assertSort(values, new KthSelector(PivotingStrategy.DYNAMIC, 3)::sortSBM); + } + + @ParameterizedTest + @MethodSource(value = {"testSort"}) + void testSortDP(double[] values) { + assertSort(values, new KthSelector(PivotingStrategy.DYNAMIC, 3)::sortDP); + } + + @ParameterizedTest + @MethodSource(value = {"testSort"}) + void testSortDP5(double[] values) { + // Requires at least 5 points + assertSort(values, new KthSelector(PivotingStrategy.DYNAMIC, 5)::sortDP5); + } + + @Test + void testSortZero() { + final double a = -0.0; + final double b = 0.0; + final double[][] values = new double[][] { + {a, a}, + {a, b}, + {b, a}, + {b, b}, + {a, a, a}, + {a, a, b}, + {a, b, a}, + {a, b, b}, + {b, a, a}, + {b, a, b}, + {b, b, a}, + {b, b, b}, + {a, a, a, a}, + {a, a, a, b}, + {a, a, b, a}, + {a, a, b, b}, + {a, b, a, a}, + {a, b, a, b}, + {a, b, b, a}, + {a, b, b, b}, + {b, a, a, a}, + {b, a, a, b}, + {b, a, b, a}, + {b, a, b, b}, + {b, b, a, a}, + {b, b, a, b}, + {b, b, b, a}, + {b, b, b, b}, + }; + for (final double[] v : values) { + assertSort(v, x -> KthSelector.sortZero(x, 0, x.length)); + } + } + + private static void assertSort(double[] values, Consumer function) { + final double[] data = values.clone(); + final double[] sorted = values.clone(); + Arrays.sort(sorted); + function.accept(data); + Assertions.assertArrayEquals(sorted, data); + } + + static Stream testSort() { + final Stream.Builder builder = Stream.builder(); + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(123); + // Sizes above and below the threshold for partitioning + for (final int size : new int[] {5, 50}) { + double[] a = new double[size]; + Arrays.fill(a, 1.23); + builder.add(a.clone()); + for (int ii = 0; ii < size; ii++) { + a[ii] = ii; + } + builder.add(a.clone()); + for (int ii = 0; ii < size; ii++) { + a[ii] = size - ii; + } + builder.add(a.clone()); + for (int i = 0; i < 5; i++) { + a = rng.doubles(size).toArray(); + builder.add(a.clone()); + final int j = rng.nextInt(size); + final int k = rng.nextInt(size); + a[j] = Double.NaN; + a[k] = Double.NaN; + builder.add(a.clone()); + a[j] = -0.0; + a[k] = 0.0; + builder.add(a.clone()); + for (int z = 0; z < size; z++) { + a[z] = rng.nextBoolean() ? -0.0 : 0.0; + } + builder.add(a.clone()); + a[j] = -rng.nextDouble(); + a[k] = rng.nextDouble(); + builder.add(a.clone()); + } + } + builder.add(new double[] {}); + builder.add(new double[] {Double.NaN}); + builder.add(new double[] {Double.NaN, Double.NaN, Double.NaN}); + builder.add(new double[] {Double.NaN, 0.0, -0.0, Double.NaN}); + builder.add(new double[] {Double.NaN, 0.0, -0.0}); + builder.add(new double[] {Double.NaN, 1.23, 0.0, -4.56, -0.0, Double.NaN}); + return builder.build(); + } +} diff --git a/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/PartitionFactoryTest.java b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/PartitionFactoryTest.java new file mode 100644 index 000000000..b96677ff7 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/PartitionFactoryTest.java @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.util.EnumSet; +import java.util.function.Function; +import java.util.function.ToDoubleFunction; +import java.util.function.ToIntFunction; +import org.apache.commons.numbers.examples.jmh.arrays.Partition.AdaptMode; +import org.apache.commons.numbers.examples.jmh.arrays.Partition.KeyStrategy; +import org.apache.commons.numbers.examples.jmh.arrays.Partition.PairedKeyStrategy; +import org.apache.commons.numbers.examples.jmh.arrays.Partition.SPStrategy; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Executes tests for {@link PartitionFactory}. + */ +class PartitionFactoryTest { + @Test + void testGetMinQuickSelectSize() { + assertIntParameter(Partition.MIN_QUICKSELECT_SIZE, "QS", PartitionFactory::getMinQuickSelectSize); + } + + @Test + void testGetEdgeSelectConstant() { + assertIntParameter(Partition.EDGESELECT_CONSTANT, "EC", PartitionFactory::getEdgeSelectConstant); + } + + @Test + void testGetLinearSortSelectConstant() { + assertIntParameter(Partition.LINEAR_SORTSELECT_SIZE, "LC", PartitionFactory::getLinearSortSelectConstant); + } + + @Test + void testGetSubSamplingSize() { + assertIntParameter(Partition.SUBSAMPLING_SIZE, "SU", PartitionFactory::getSubSamplingSize); + } + + @Test + void testGetRecursionMultiple() { + assertDoubleParameter(Partition.RECURSION_MULTIPLE, "RM", PartitionFactory::getRecursionMultiple); + } + + @Test + void testGetRecursionConstant() { + assertIntParameter(Partition.RECURSION_CONSTANT, "RC", PartitionFactory::getRecursionConstant); + } + + @Test + void testGetCompressionLevel() { + assertIntParameter(Partition.COMPRESSION_LEVEL, "CL", PartitionFactory::getCompressionLevel); + } + + @Test + void testGetControlFlags() { + assertIntParameter(Partition.CONTROL_FLAGS, "CF", PartitionFactory::getControlFlags); + // Special support for negative flags + Assertions.assertEquals(-1, PartitionFactory.getControlFlags(new String[] {"CF-1"})); + Assertions.assertEquals(-2, PartitionFactory.getControlFlags(new String[] {"CF-2"})); + Assertions.assertEquals(-42, PartitionFactory.getControlFlags(new String[] {"none"}, -42)); + } + + @Test + void testGetOptionFlags() { + assertIntParameter(Partition.OPTION_FLAGS, "OF", PartitionFactory::getOptionFlags); + // Special support for negative flags + Assertions.assertEquals(-1, PartitionFactory.getOptionFlags(new String[] {"OF-1"})); + Assertions.assertEquals(-2, PartitionFactory.getOptionFlags(new String[] {"OF-2"})); + Assertions.assertEquals(-42, PartitionFactory.getOptionFlags(new String[] {"none"}, -42)); + } + + @Test + void testGetPivotingStrategy() { + assertEnumParameter(Partition.PIVOTING_STRATEGY, + s -> PartitionFactory.getEnumOrElse(s, PivotingStrategy.class, Partition.PIVOTING_STRATEGY)); + } + + @Test + void testGetDualPivotingStrategy() { + assertEnumParameter(Partition.DUAL_PIVOTING_STRATEGY, + s -> PartitionFactory.getEnumOrElse(s, DualPivotingStrategy.class, Partition.DUAL_PIVOTING_STRATEGY)); + } + + @Test + void testGetKeyStrategy() { + assertEnumParameter(Partition.KEY_STRATEGY, + s -> PartitionFactory.getEnumOrElse(s, KeyStrategy.class, Partition.KEY_STRATEGY)); + } + + @Test + void testGetPairedKeyStrategy() { + assertEnumParameter(Partition.PAIRED_KEY_STRATEGY, + s -> PartitionFactory.getEnumOrElse(s, PairedKeyStrategy.class, Partition.PAIRED_KEY_STRATEGY)); + } + + @Test + void testGetSPStrategy() { + assertEnumParameter(Partition.SP_STRATEGY, + s -> PartitionFactory.getEnumOrElse(s, SPStrategy.class, Partition.SP_STRATEGY)); + } + + @Test + void testGetAdaptMode() { + assertEnumParameter(Partition.ADAPT_MODE, + s -> PartitionFactory.getEnumOrElse(s, AdaptMode.class, Partition.ADAPT_MODE)); + } + + @Test + void testInvalidPrefix() { + Assertions.assertThrows(IllegalArgumentException.class, () -> + PartitionFactory.createPartition("IDP", "ISP", 0, 0)); + } + + private static void assertIntParameter(int defaultValue, String pattern, ToIntFunction fun) { + final String[] s = {"nothing"}; + Assertions.assertEquals(defaultValue, fun.applyAsInt(s)); + Assertions.assertEquals("nothing", s[0]); + // Prevent overflow when setting non-default values + if (defaultValue + 4 < 0) { + defaultValue = 0; + } + s[0] = pattern + (defaultValue + 1); + Assertions.assertEquals(defaultValue + 1, fun.applyAsInt(s)); + Assertions.assertEquals("", s[0]); + s[0] = "before" + pattern + (defaultValue + 2); + Assertions.assertEquals(defaultValue + 2, fun.applyAsInt(s)); + Assertions.assertEquals("before", s[0]); + s[0] = pattern + (defaultValue + 3) + "after"; + Assertions.assertEquals(defaultValue + 3, fun.applyAsInt(s)); + Assertions.assertEquals("after", s[0]); + s[0] = "before" + pattern + (defaultValue + 4) + "after"; + Assertions.assertEquals(defaultValue + 4, fun.applyAsInt(s)); + Assertions.assertEquals("beforeafter", s[0]); + } + + private static void assertDoubleParameter(double defaultValue, String pattern, ToDoubleFunction fun) { + final String[] s = {"nothing"}; + Assertions.assertEquals(defaultValue, fun.applyAsDouble(s)); + Assertions.assertEquals("nothing", s[0]); + s[0] = pattern + (defaultValue + 0.5); + Assertions.assertEquals(defaultValue + 0.5, fun.applyAsDouble(s)); + Assertions.assertEquals("", s[0]); + s[0] = "before" + pattern + (defaultValue + 1.5); + Assertions.assertEquals(defaultValue + 1.5, fun.applyAsDouble(s)); + Assertions.assertEquals("before", s[0]); + s[0] = pattern + (defaultValue + 2.5) + "after"; + Assertions.assertEquals(defaultValue + 2.5, fun.applyAsDouble(s)); + Assertions.assertEquals("after", s[0]); + s[0] = "before" + pattern + (defaultValue + 3.5) + "after"; + Assertions.assertEquals(defaultValue + 3.5, fun.applyAsDouble(s)); + Assertions.assertEquals("beforeafter", s[0]); + } + + private static > void assertEnumParameter(E defaultValue, Function fun) { + final String[] s = {"nothing"}; + Assertions.assertEquals(defaultValue, fun.apply(s)); + Assertions.assertEquals("nothing", s[0]); + EnumSet.allOf(defaultValue.getDeclaringClass()).forEach(e -> { + s[0] = e.toString(); + Assertions.assertEquals(e, fun.apply(s)); + Assertions.assertEquals("", s[0]); + s[0] = "before_" + e; + Assertions.assertEquals(e, fun.apply(s)); + Assertions.assertEquals("before_", s[0]); + s[0] = e + "after"; + Assertions.assertEquals(e, fun.apply(s)); + Assertions.assertEquals("after", s[0]); + s[0] = "before_" + e + "after"; + Assertions.assertEquals(e, fun.apply(s)); + Assertions.assertEquals("before_after", s[0]); + }); + } +} diff --git a/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/PartitionTest.java b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/PartitionTest.java new file mode 100644 index 000000000..f11d21b8f --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/PartitionTest.java @@ -0,0 +1,2461 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Formatter; +import java.util.LinkedList; +import java.util.function.Consumer; +import java.util.function.IntUnaryOperator; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.apache.commons.numbers.arrays.Selection; +import org.apache.commons.numbers.examples.jmh.arrays.Partition.AdaptMode; +import org.apache.commons.numbers.examples.jmh.arrays.Partition.EdgeSelectStrategy; +import org.apache.commons.numbers.examples.jmh.arrays.Partition.ExpandStrategy; +import org.apache.commons.numbers.examples.jmh.arrays.Partition.KeyStrategy; +import org.apache.commons.numbers.examples.jmh.arrays.Partition.LinearStrategy; +import org.apache.commons.numbers.examples.jmh.arrays.Partition.PairedKeyStrategy; +import org.apache.commons.numbers.examples.jmh.arrays.Partition.SPStrategy; +import org.apache.commons.numbers.examples.jmh.arrays.SelectionPerformance.AbstractDataSource; +import org.apache.commons.numbers.examples.jmh.arrays.SelectionPerformance.AbstractDataSource.Distribution; +import org.apache.commons.numbers.examples.jmh.arrays.SelectionPerformance.AbstractDataSource.Modification; +import org.apache.commons.rng.UniformRandomProvider; +import org.apache.commons.rng.simple.RandomSource; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Test for {@link Partition}. + */ +class PartitionTest { + /** Default single pivot strategy. */ + private static final PivotingStrategy SP = PivotingStrategy.MEDIAN_OF_3; + /** Default single pivot strategy. */ + private static final DualPivotingStrategy DP = DualPivotingStrategy.SORT_5; + /** Default minimum quick select length. */ + private static final int QS = 3; + /** Default minimum quick select length for dual pivot. */ + private static final int QS2 = 5; + /** Default heap select constant. */ + private static final int EC = 2; + /** Default sub-sampling size. */ + private static final int SU = 600; + + /** + * Partition function. Used to test different implementations. + */ + private interface DoubleRangePartitionFunction { + /** + * Partition the array such that range of indices {@code [ka, kb]} correspond to + * their correctly sorted value in the equivalent fully sorted array. For all + * indices {@code k} and any index {@code i}: + * + *

{@code
+         * data[i < k] <= data[k] <= data[k < i]
+         * }
+ * + * @param a Data array to use to find out the Kth value. + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param ka Lower index to select. + * @param kb Upper index to select. + */ + void partition(double[] a, int left, int right, int ka, int kb); + } + + /** + * Partition function. Used to test different implementations. + */ + private interface DoublePartitionFunction { + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *
{@code
+         * data[i < k] <= data[k] <= data[k < i]
+         * }
+ * + *

This method allows variable length indices using a count of the indices to + * process. + * + * @param a Values. + * @param k Indices. + * @param n Count of indices. + */ + void partition(double[] a, int[] k, int n); + } + + /** + * Partition function. Used to test different implementations. + */ + private interface DoublePartitionFunction2 { + /** + * Partition the array such that indices {@code k} correspond to their correctly + * sorted value in the equivalent fully sorted array. For all indices {@code k} + * and any index {@code i}: + * + *

{@code
+         * data[i < k] <= data[k] <= data[k < i]
+         * }
+ * + * @param a Values. + * @param k Indices. + */ + void partition(double[] a, int... k); + } + + @ParameterizedTest + @MethodSource + void testSortNaN(double[] values) { + final double[] sorted = sort(values); + final int last = Partition.sortNaN(values); + // index of last non-NaN + int i = sorted.length; + while (--i >= 0) { + if (!Double.isNaN(sorted[i])) { + break; + } + } + Assertions.assertEquals(i, last); + // Check the data is the same + Arrays.sort(values); + Assertions.assertArrayEquals(sorted, values, "Data destroyed"); + } + + static Stream testSortNaN() { + final Stream.Builder builder = Stream.builder(); + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(); + final double nan = Double.NaN; + builder.add(new double[0]); + builder.add(new double[] {1.23}); + builder.add(new double[] {nan}); + builder.add(new double[] {nan, nan}); + builder.add(new double[] {nan, nan, nan}); + for (final int size : new int[] {2, 5}) { + final double[] values = rng.doubles(size).toArray(); + builder.add(values.clone()); + // Random NaNs + for (int n = 1; n < size; n++) { + final double[] x = values.clone(); + Arrays.fill(x, 0, n, nan); + for (int i = 0; i < 5; i++) { + builder.add(TestUtils.shuffle(rng, x).clone()); + } + } + } + return builder.build(); + } + + @ParameterizedTest + @MethodSource(value = {"testSelectMinMax"}) + void testSelectMin(double[] values, int from, int to) { + assertPartitionRange(sort(values, from, to), + (a, l, r, ka, kb) -> Partition.selectMin(values, from, to), + values, from, to, from, from); + } + + @ParameterizedTest + @MethodSource(value = {"testSelectMinMax"}) + void testSelectMax(double[] values, int from, int to) { + assertPartitionRange(sort(values, from, to), + (a, l, r, ka, kb) -> Partition.selectMax(values, from, to), + values, from, to, to, to); + } + + static Stream testSelectMinMax() { + final Stream.Builder builder = Stream.builder(); + builder.add(Arguments.of(new double[] {1, 2, 3, 4, 5}, 0, 4)); + builder.add(Arguments.of(new double[] {5, 4, 3, 2, 1}, 0, 4)); + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(); + for (final int size : new int[] {5, 10}) { + final double[] values = rng.doubles(size).toArray(); + builder.add(Arguments.of(values.clone(), 0, size - 1)); + builder.add(Arguments.of(values.clone(), size >>> 1, size - 1)); + builder.add(Arguments.of(values.clone(), 1, size >>> 1)); + } + builder.add(Arguments.of(new double[] {-0.0, 0.0}, 0, 1)); + builder.add(Arguments.of(new double[] {0.0, -0.0}, 0, 1)); + builder.add(Arguments.of(new double[] {-0.0, -0.0}, 0, 1)); + builder.add(Arguments.of(new double[] {0.0, 0.0}, 0, 1)); + builder.add(Arguments.of(new double[] {0.0, -0.0, 0.0, -0.0}, 0, 3)); + builder.add(Arguments.of(new double[] {-0.0, 0.0, -0.0, 0.0}, 0, 3)); + builder.add(Arguments.of(new double[] {0.0, -0.0, -0.0, 0.0}, 0, 3)); + builder.add(Arguments.of(new double[] {-0.0, 0.0, 0.0, -0.0}, 0, 3)); + return builder.build(); + } + + @ParameterizedTest + @MethodSource(value = "testSelectMinMax2") + void testSelectMin2IgnoreZeros(double[] values, int from, int to) { + assertPartitionRange(sort(values, from, to), + (a, l, r, ka, kb) -> { + replaceNegativeZeros(values, from, to); + Partition.selectMin2IgnoreZeros(values, from, to); + restoreNegativeZeros(values, from, to); + }, + values, from, to, from, from + 1); + } + + @ParameterizedTest + @MethodSource(value = "testSelectMinMax2") + void testSelectMax2IgnoreZeros(double[] values, int from, int to) { + assertPartitionRange(sort(values, from, to), + (a, l, r, ka, kb) -> { + replaceNegativeZeros(values, from, to); + Partition.selectMax2IgnoreZeros(values, from, to); + restoreNegativeZeros(values, from, to); + }, + values, from, to, to - 1, to); + } + + static Stream testSelectMinMax2() { + final Stream.Builder builder = Stream.builder(); + final double[] values = {-0.0, 0.0, 1}; + final double x = Double.NaN; + final double y = 42; + for (final double a : values) { + for (final double b : values) { + builder.add(Arguments.of(new double[] {a, b}, 0, 1)); + builder.add(Arguments.of(new double[] {x, a, b, y}, 1, 2)); + for (final double c : values) { + builder.add(Arguments.of(new double[] {a, b, c}, 0, 2)); + builder.add(Arguments.of(new double[] {x, a, b, c, y}, 1, 3)); + for (final double d : values) { + builder.add(Arguments.of(new double[] {a, b, c, d}, 0, 3)); + builder.add(Arguments.of(new double[] {x, a, b, c, d, y}, 1, 4)); + } + } + } + } + builder.add(Arguments.of(new double[] {-1, -1, -1, 4, 3, 2, 1, y}, 3, 6)); + builder.add(Arguments.of(new double[] {1, 2, 3, 4, 5}, 0, 4)); + builder.add(Arguments.of(new double[] {5, 4, 3, 2, 1}, 0, 4)); + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(); + for (final int size : new int[] {5, 10}) { + final double[] a = rng.doubles(size).toArray(); + builder.add(Arguments.of(a.clone(), 0, size - 1)); + builder.add(Arguments.of(a.clone(), size >>> 1, size - 1)); + builder.add(Arguments.of(a.clone(), 1, size >>> 1)); + } + builder.add(Arguments.of(new double[] {-0.0, 0.0}, 0, 1)); + builder.add(Arguments.of(new double[] {0.0, -0.0}, 0, 1)); + builder.add(Arguments.of(new double[] {-0.0, -0.0}, 0, 1)); + builder.add(Arguments.of(new double[] {0.0, 0.0}, 0, 1)); + builder.add(Arguments.of(new double[] {0.0, -0.0, 0.0, -0.0}, 0, 3)); + builder.add(Arguments.of(new double[] {-0.0, 0.0, -0.0, 0.0}, 0, 3)); + builder.add(Arguments.of(new double[] {0.0, -0.0, -0.0, 0.0}, 0, 3)); + builder.add(Arguments.of(new double[] {-0.0, 0.0, 0.0, -0.0}, 0, 3)); + return builder.build(); + } + + @ParameterizedTest + @MethodSource(value = {"testHeapSelect", "testSelectMinMax", "testSelectMinMax2"}) + void testHeapSelectLeft(double[] values, int from, int to) { + final double[] sorted = sort(values, from, to); + + final double[] x = values.clone(); + replaceNegativeZeros(x, from, to); + final DoubleRangePartitionFunction fun = (a, l, r, ka, kb) -> { + Partition.heapSelectLeft(a, l, r, kb, kb - ka); + restoreNegativeZeros(a, l, r); + }; + + for (int k = from; k <= to; k++) { + assertPartitionRange(sorted, fun, x.clone(), from, to, k, k); + if (k > from) { + // Sort an extra 1 + assertPartitionRange(sorted, fun, x.clone(), from, to, k - 1, k); + if (k > from + 1) { + // Sort all + // Test clipping with k < from + assertPartitionRange(sorted, fun, x.clone(), from, to, from - 23, k); + } + } + } + } + + @ParameterizedTest + @MethodSource(value = {"testHeapSelect", "testSelectMinMax", "testSelectMinMax2"}) + void testHeapSelectRight(double[] values, int from, int to) { + final double[] sorted = sort(values, from, to); + + final double[] x = values.clone(); + replaceNegativeZeros(x, from, to); + final DoubleRangePartitionFunction fun = (a, l, r, ka, kb) -> { + Partition.heapSelectRight(a, l, r, ka, kb - ka); + restoreNegativeZeros(a, l, r); + }; + + for (int k = from; k <= to; k++) { + assertPartitionRange(sorted, fun, x.clone(), from, to, k, k); + if (k < to) { + // Sort an extra 1 + assertPartitionRange(sorted, fun, x.clone(), from, to, k, k + 1); + if (k < to - 1) { + // Sort all + // Test clipping with k > to + assertPartitionRange(sorted, fun, x.clone(), from, to, k, to + 23); + } + } + } + } + + static Stream testHeapSelect() { + final Stream.Builder builder = Stream.builder(); + builder.add(Arguments.of(new double[] {1}, 0, 0)); + builder.add(Arguments.of(new double[] {3, 2, 1}, 1, 1)); + builder.add(Arguments.of(new double[] {2, 1}, 0, 1)); + builder.add(Arguments.of(new double[] {4, 3, 2, 1}, 1, 2)); + builder.add(Arguments.of(new double[] {-1, 0.0, -0.0, -0.0, 1}, 0, 4)); + builder.add(Arguments.of(new double[] {-1, 0.0, -0.0, -0.0, 1}, 0, 2)); + builder.add(Arguments.of(new double[] {1, 0.0, -0.0, -0.0, -1}, 0, 4)); + builder.add(Arguments.of(new double[] {-1, 2, -3, 4, -4, 3, -2, 1}, 1, 6)); + return builder.build(); + } + + @ParameterizedTest + @MethodSource(value = {"testHeapSelect", "testSelectMinMax", "testSelectMinMax2"}) + void testHeapSelectLeft2(double[] values, int from, int to) { + final double[] sorted = sort(values, from, to); + + final double[] x = values.clone(); + replaceNegativeZeros(x, from, to); + final DoubleRangePartitionFunction fun = (a, l, r, ka, kb) -> { + Partition.heapSelectLeft2(a, l, r, ka, kb); + restoreNegativeZeros(a, l, r); + }; + + for (int k = from; k <= to; k++) { + assertPartitionRange(sorted, fun, x.clone(), from, to, k, k); + if (k > from) { + // Sort an extra 1 + assertPartitionRange(sorted, fun, x.clone(), from, to, k - 1, k); + if (k > from + 1) { + // Sort all + // Test clipping with k < from + assertPartitionRange(sorted, fun, x.clone(), from, to, from - 23, k); + } + } + } + } + + @ParameterizedTest + @MethodSource(value = {"testHeapSelect", "testSelectMinMax", "testSelectMinMax2"}) + void testHeapSelectRight2(double[] values, int from, int to) { + final double[] sorted = sort(values, from, to); + + final double[] x = values.clone(); + replaceNegativeZeros(x, from, to); + final DoubleRangePartitionFunction fun = (a, l, r, ka, kb) -> { + Partition.heapSelectRight2(a, l, r, ka, kb); + restoreNegativeZeros(a, l, r); + }; + + for (int k = from; k <= to; k++) { + assertPartitionRange(sorted, fun, x.clone(), from, to, k, k); + if (k < to) { + // Sort an extra 1 + assertPartitionRange(sorted, fun, x.clone(), from, to, k, k + 1); + if (k < to - 1) { + // Sort all + // Test clipping with k > to + assertPartitionRange(sorted, fun, x.clone(), from, to, k, to + 23); + } + } + } + } + + @ParameterizedTest + @MethodSource + void testHeapSelectPair(double[] values, int from, int to, int k1, int k2) { + final double[] sorted = sort(values, from, to); + Partition.heapSelectPair(values, from, to, k1, k2); + Assertions.assertEquals(sorted[k1], values[k1]); + Assertions.assertEquals(sorted[k2], values[k2]); + // Check the data is the same + Arrays.sort(values, from, to + 1); + Assertions.assertArrayEquals(sorted, values, "Data destroyed"); + } + + static Stream testHeapSelectPair() { + final Stream.Builder builder = Stream.builder(); + builder.add(Arguments.of(new double[] {-1, 2, -3, 4, -4, 3, -2, 1}, 0, 7, 1, 2)); + builder.add(Arguments.of(new double[] {-1, 2, -3, 4, -4, 3, -2, 1}, 0, 7, 2, 2)); + builder.add(Arguments.of(new double[] {-1, 2, -3, 4, -4, 3, -2, 1}, 0, 7, 5, 7)); + builder.add(Arguments.of(new double[] {-1, 2, -3, 4, -4, 3, -2, 1}, 0, 7, 1, 6)); + builder.add(Arguments.of(new double[] {-1, 2, -3, 4, -4, 3, -2, 1}, 0, 7, 4, 4)); + return builder.build(); + } + + @ParameterizedTest + @MethodSource(value = {"testHeapSelectRange"}) + void testHeapSelectRange(double[] values, int from, int to, int k1, int k2) { + assertPartitionRange(sort(values, from, to), + Partition::heapSelectRange, values, from, to, k1, k2); + } + + @ParameterizedTest + @MethodSource(value = {"testHeapSelectRange"}) + void testHeapSelectRange2(double[] values, int from, int to, int k1, int k2) { + assertPartitionRange(sort(values, from, to), + Partition::heapSelectRange2, values, from, to, k1, k2); + } + + static Stream testHeapSelectRange() { + final Stream.Builder builder = Stream.builder(); + builder.add(Arguments.of(new double[] {-1, 2, -3, 4, -4, 3, -2, 1}, 0, 7, 1, 2)); + builder.add(Arguments.of(new double[] {-1, 2, -3, 4, -4, 3, -2, 1}, 0, 7, 2, 2)); + builder.add(Arguments.of(new double[] {-1, 2, -3, 4, -4, 3, -2, 1}, 0, 7, 5, 7)); + builder.add(Arguments.of(new double[] {-1, 2, -3, 4, -4, 3, -2, 1}, 0, 7, 1, 6)); + builder.add(Arguments.of(new double[] {-1, 2, -3, 4, -4, 3, -2, 1}, 0, 7, 0, 3)); + builder.add(Arguments.of(new double[] {-1, 2, -3, 4, -4, 3, -2, 1}, 0, 7, 4, 7)); + return builder.build(); + } + + @ParameterizedTest + @MethodSource(value = {"testHeapSelect", "testSelectMinMax", "testSelectMinMax2"}) + void testSortSelectLeft(double[] values, int from, int to) { + final double[] sorted = sort(values, from, to); + + final double[] x = values.clone(); + replaceNegativeZeros(x, from, to); + final DoubleRangePartitionFunction fun = (a, l, r, ka, kb) -> { + Partition.sortSelectLeft(a, l, r, kb); + restoreNegativeZeros(a, l, r); + }; + + for (int k = from; k <= to; k++) { + assertPartitionRange(sorted, fun, x.clone(), from, to, from, k); + } + } + + @ParameterizedTest + @MethodSource(value = {"testHeapSelect", "testSelectMinMax", "testSelectMinMax2"}) + void testSortSelectRight(double[] values, int from, int to) { + final double[] sorted = sort(values, from, to); + + final double[] x = values.clone(); + replaceNegativeZeros(x, from, to); + final DoubleRangePartitionFunction fun = (a, l, r, ka, kb) -> { + Partition.sortSelectRight(a, l, r, ka); + restoreNegativeZeros(a, l, r); + }; + + for (int k = from; k <= to; k++) { + assertPartitionRange(sorted, fun, x.clone(), from, to, k, to); + } + } + + @ParameterizedTest + @MethodSource(value = {"testHeapSelectRange"}) + void testSortSelectRange(double[] values, int from, int to, int k1, int k2) { + assertPartitionRange(sort(values, from, to), + Partition::sortSelectRange, values, from, to, k1, k2); + } + + @ParameterizedTest + @MethodSource(value = {"testHeapSelectRange"}) + void testSortSelectRange2(double[] values, int from, int to, int k1, int k2) { + assertPartitionRange(sort(values, from, to), + Partition::sortSelectRange2, values, from, to, k1, k2); + } + + /** + * Return a copy of the {@code values} sorted. + * + * @param values Values. + * @return the copy + */ + private static double[] sort(double[] values) { + final double[] sorted = values.clone(); + Arrays.sort(sorted); + return sorted; + } + + /** + * Return a copy of the {@code values} sorted in the range {@code [from, to]}. + * + * @param values Values. + * @param from From (inclusive). + * @param to To (inclusive). + * @return the copy + */ + private static double[] sort(double[] values, int from, int to) { + final double[] sorted = values.clone(); + Arrays.sort(sorted, from, to + 1); + return sorted; + } + + /** + * Assert the function correctly partitions the range. + * + * @param sorted Expected sort result. + * @param fun Partition function. + * @param values Values. + * @param from From (inclusive). + * @param to To (inclusive). + * @param ka Lower index to select. + * @param kb Upper index to select. + */ + private static void assertPartitionRange(double[] sorted, + DoubleRangePartitionFunction fun, + double[] values, int from, int to, int ka, int kb) { + Arrays.sort(sorted, from, to + 1); + fun.partition(values, from, to, ka, kb); + // Clip + ka = ka < from ? from : ka; + kb = kb > to ? to : kb; + for (int i = ka; i <= kb; i++) { + final int index = i; + Assertions.assertEquals(sorted[i], values[i], () -> "index: " + index); + } + // Check the data is the same + Arrays.sort(values, from, to + 1); + Assertions.assertArrayEquals(sorted, values, "Data destroyed"); + } + + @Test + void testFloorLog2() { + // Here expected = -Infinity; actual = -1 + Assertions.assertEquals(-1, Partition.floorLog2(0)); + Assertions.assertEquals(0, Partition.floorLog2(1)); + // Create a series of powers of 2, start at 2^1 + long p = 1; + for (int i = 1;; i++) { + p *= 2; + if (p > Integer.MAX_VALUE) { + break; + } + final int x = (int) p; + Assertions.assertEquals(i - 1, Partition.floorLog2(x - 1)); + Assertions.assertEquals(i, Partition.floorLog2(x)); + Assertions.assertEquals(i, Partition.floorLog2(x + 1)); + } + } + + @Test + void testLog3() { + // Reasonable behaviour at small x + Assertions.assertEquals(0, Partition.log3(0)); + Assertions.assertEquals(0, Partition.log3(1)); + Assertions.assertEquals(1, Partition.log3(2)); + Assertions.assertEquals(1, Partition.log3(3)); + Assertions.assertEquals(1, Partition.log3(4)); + Assertions.assertEquals(1, Partition.log3(5)); + Assertions.assertEquals(1, Partition.log3(6)); + Assertions.assertEquals(1, Partition.log3(7)); + Assertions.assertEquals(2, Partition.log3(8)); + // log3(2^31-1) = 19.5588223... + Assertions.assertEquals(19, Partition.log3(Integer.MAX_VALUE)); + // Create a series of powers of 3, start at 2^3 + long p = 3; + for (int i = 2;; i++) { + p *= 3; + if (p > Integer.MAX_VALUE) { + break; + } + final int x = (int) p; + // Computes round(log3(x)) when x is close to a power of 3 + Assertions.assertEquals(i, Partition.log3(x - 1)); + Assertions.assertEquals(i, Partition.log3(x)); + Assertions.assertEquals(i, Partition.log3(x + 1)); + // Half-way point is within the bracket [i, i+1] + final int y = (int) Math.floor(Math.pow(3, i + 0.5)); + Assertions.assertTrue(Partition.log3(y) >= i); + Assertions.assertTrue(Partition.log3(y + 1) <= i + 1); + } + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionSBMIndexSet(double[] values, int[] indices) { + assertPartition(values, indices, + new Partition(SP, QS).setKeyStrategy(KeyStrategy.INDEX_SET)::partitionSBM); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionSBMPivotCache(double[] values, int[] indices) { + assertPartition(values, indices, + new Partition(SP, QS).setKeyStrategy(KeyStrategy.PIVOT_CACHE)::partitionSBM); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionSBMSequential(double[] values, int[] indices) { + assertPartition(values, indices, + new Partition(SP, QS).setKeyStrategy(KeyStrategy.SEQUENTIAL)::partitionSBM); + } + + // Introselect versions use standard select configuration. + // We test the different PairedKeyStrategy/EdgeSelectStrategy options alongside KeyStrategy options. + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionISP(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setKeyStrategy(KeyStrategy.ORDERED_KEYS) + .setPairedKeyStrategy(PairedKeyStrategy.PAIRED_KEYS) + .setEdgeSelectStrategy(EdgeSelectStrategy.ESH) + .setControlFlags(Partition.FLAG_RANDOM_SAMPLING) + .setSPStrategy(SPStrategy.SP) + ::partitionISP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionIBM(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setKeyStrategy(KeyStrategy.ORDERED_KEYS) + .setPairedKeyStrategy(PairedKeyStrategy.PAIRED_KEYS_2) + .setEdgeSelectStrategy(EdgeSelectStrategy.ESH2) + .setRecursionMultiple(5) + .setRecursionConstant(1) + .setSPStrategy(SPStrategy.BM) + ::partitionISP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionISBM(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setKeyStrategy(KeyStrategy.ORDERED_KEYS) + .setPairedKeyStrategy(PairedKeyStrategy.PAIRED_KEYS_LEN) + .setEdgeSelectStrategy(EdgeSelectStrategy.ESS) + .setRecursionMultiple(2) + .setSPStrategy(SPStrategy.SBM) + ::partitionISP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionISBMScanningKey(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setKeyStrategy(KeyStrategy.SCANNING_KEY_SEARCHABLE_INTERVAL) + .setPairedKeyStrategy(PairedKeyStrategy.PAIRED_KEYS) + .setEdgeSelectStrategy(EdgeSelectStrategy.ESS2) + .setSPStrategy(SPStrategy.SBM) + ::partitionISP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionISBMSearchKey(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setKeyStrategy(KeyStrategy.SEARCH_KEY_SEARCHABLE_INTERVAL) + .setPairedKeyStrategy(PairedKeyStrategy.TWO_KEYS) + .setEdgeSelectStrategy(EdgeSelectStrategy.ESH) + .setSPStrategy(SPStrategy.SBM) + ::partitionISP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionISBMIndexSet(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setKeyStrategy(KeyStrategy.INDEX_SET) + .setPairedKeyStrategy(PairedKeyStrategy.KEY_RANGE) + .setEdgeSelectStrategy(EdgeSelectStrategy.ESH2) + .setRecursionMultiple(5) + .setRecursionConstant(1) + .setControlFlags(Partition.FLAG_RANDOM_SAMPLING) + .setSPStrategy(SPStrategy.SBM) + ::partitionISP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionISBMKeyUpdating(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setKeyStrategy(KeyStrategy.KEY_UPDATING_INTERVAL) + .setPairedKeyStrategy(PairedKeyStrategy.SEARCHABLE_INTERVAL) + .setEdgeSelectStrategy(EdgeSelectStrategy.ESS) + .setSPStrategy(SPStrategy.SBM) + ::partitionISP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionISBMIndexSetUpdating(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setKeyStrategy(KeyStrategy.INDEX_SET_UPDATING_INTERVAL) + .setPairedKeyStrategy(PairedKeyStrategy.UPDATING_INTERVAL) + .setEdgeSelectStrategy(EdgeSelectStrategy.ESS2) + .setSPStrategy(SPStrategy.SBM) + ::partitionISP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionISBMKeySplitting(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setKeyStrategy(KeyStrategy.KEY_SPLITTING_INTERVAL) + .setPairedKeyStrategy(PairedKeyStrategy.SEARCHABLE_INTERVAL) + .setEdgeSelectStrategy(EdgeSelectStrategy.ESH) + .setSPStrategy(SPStrategy.SBM) + ::partitionISP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionISBMIndexSetSplitting(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setKeyStrategy(KeyStrategy.INDEX_SET_SPLITTING_INTERVAL) + .setPairedKeyStrategy(PairedKeyStrategy.UPDATING_INTERVAL) + .setEdgeSelectStrategy(EdgeSelectStrategy.ESH) + .setSPStrategy(SPStrategy.SBM) + ::partitionISP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionISBMCompressedIndexSet(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setKeyStrategy(KeyStrategy.COMPRESSED_INDEX_SET) + .setCompression(1) + .setPairedKeyStrategy(PairedKeyStrategy.KEY_RANGE) + .setEdgeSelectStrategy(EdgeSelectStrategy.ESH2) + .setRecursionMultiple(5) + .setRecursionConstant(1) + .setSPStrategy(SPStrategy.SBM) + ::partitionISP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionISBMCompressedIndexSet2(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setKeyStrategy(KeyStrategy.COMPRESSED_INDEX_SET) + .setCompression(2) + .setSPStrategy(SPStrategy.SBM) + ::partitionISP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionISBMIndexIterator(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setKeyStrategy(KeyStrategy.INDEX_ITERATOR) + .setSPStrategy(SPStrategy.SBM) + ::partitionISP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionISBMCompressedIndexIterator(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setKeyStrategy(KeyStrategy.COMPRESSED_INDEX_ITERATOR) + .setCompression(1) + .setSPStrategy(SPStrategy.SBM) + ::partitionISP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionISBMCompressedIndexIterator4(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setKeyStrategy(KeyStrategy.COMPRESSED_INDEX_ITERATOR) + .setCompression(4) + .setSPStrategy(SPStrategy.SBM) + ::partitionISP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionIKBM(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setKeyStrategy(KeyStrategy.ORDERED_KEYS) + .setPairedKeyStrategy(PairedKeyStrategy.PAIRED_KEYS) + .setSPStrategy(SPStrategy.KBM) + ::partitionISP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionIDNF1(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setSPStrategy(SPStrategy.DNF1) + ::partitionISP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionIDPScanningKey(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(DP, QS2, EC) + .setKeyStrategy(KeyStrategy.SCANNING_KEY_SEARCHABLE_INTERVAL) + .setPairedKeyStrategy(PairedKeyStrategy.PAIRED_KEYS) + .setEdgeSelectStrategy(EdgeSelectStrategy.ESH) + ::partitionIDP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionIDPSearchKey(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(DP, QS2, EC) + .setKeyStrategy(KeyStrategy.SEARCH_KEY_SEARCHABLE_INTERVAL) + .setPairedKeyStrategy(PairedKeyStrategy.TWO_KEYS) + .setEdgeSelectStrategy(EdgeSelectStrategy.ESH2) + ::partitionIDP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionIDPIndexSet(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(DP, QS2, EC) + .setKeyStrategy(KeyStrategy.INDEX_SET) + .setPairedKeyStrategy(PairedKeyStrategy.SEARCHABLE_INTERVAL) + .setEdgeSelectStrategy(EdgeSelectStrategy.ESS) + ::partitionIDP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionIDPKeyUpdating(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(DP, QS2, EC) + .setKeyStrategy(KeyStrategy.KEY_UPDATING_INTERVAL) + .setPairedKeyStrategy(PairedKeyStrategy.SEARCHABLE_INTERVAL) + .setEdgeSelectStrategy(EdgeSelectStrategy.ESS2) + ::partitionIDP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionIDPIndexSetUpdating(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(DP, QS2, EC) + .setKeyStrategy(KeyStrategy.INDEX_SET_UPDATING_INTERVAL) + .setPairedKeyStrategy(PairedKeyStrategy.SEARCHABLE_INTERVAL) + .setEdgeSelectStrategy(EdgeSelectStrategy.ESH) + ::partitionIDP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionIDPKeySplitting(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(DP, QS2, EC) + .setKeyStrategy(KeyStrategy.KEY_SPLITTING_INTERVAL) + .setPairedKeyStrategy(PairedKeyStrategy.SEARCHABLE_INTERVAL) + .setEdgeSelectStrategy(EdgeSelectStrategy.ESH2) + ::partitionIDP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionIDPIndexSetSplitting(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(DP, QS2, EC) + .setKeyStrategy(KeyStrategy.INDEX_SET_SPLITTING_INTERVAL) + .setPairedKeyStrategy(PairedKeyStrategy.SEARCHABLE_INTERVAL) + .setEdgeSelectStrategy(EdgeSelectStrategy.ESS) + ::partitionIDP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionIDPCompressedIndexSet(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(DP, QS2, EC) + .setKeyStrategy(KeyStrategy.COMPRESSED_INDEX_SET) + .setCompression(1)::partitionIDP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionIDPCompressedIndexSet2(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(DP, QS2, EC) + .setKeyStrategy(KeyStrategy.COMPRESSED_INDEX_SET) + .setCompression(2)::partitionIDP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionIDPIndexIterator(double[] values, int[] indices) { + assertPartition(values, indices, + new Partition(DP, QS2, EC).setKeyStrategy(KeyStrategy.INDEX_ITERATOR)::partitionIDP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionIDPCompressedIndexIterator(double[] values, int[] indices) { + assertPartition(values, indices, new Partition(DP, QS2, EC) + .setKeyStrategy(KeyStrategy.COMPRESSED_INDEX_ITERATOR) + .setCompression(1)::partitionIDP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition", "testPartitionBigData"}) + void testPartitionFR(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1); + assertPartition(values, indices, + new Partition(PivotingStrategy.TARGET, QS, EC, SU)::partitionFR); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition", "testPartitionBigData"}) + void testPartitionFRPivotingStrategy(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1); + assertPartition(values, indices, new Partition(SP, QS, EC, SU)::partitionFR); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition", "testPartitionBigData"}) + void testPartitionFRRandomSampling(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1); + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setControlFlags(Partition.FLAG_RANDOM_SAMPLING)::partitionFR); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition", "testPartitionBigData"}) + void testPartitionFRMoveSample(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1); + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setControlFlags(Partition.FLAG_MOVE_SAMPLE)::partitionFR); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition", "testPartitionBigData"}) + void testPartitionFRSubsetSampling(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1); + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setControlFlags(Partition.FLAG_SUBSET_SAMPLING)::partitionFR); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition", "testPartitionBigData"}) + void testPartitionKFR(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1); + assertPartition(values, indices, new Partition(SP, QS, EC, SU)::partitionKFR); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionLSP(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1 || + (indices.length == 2 && Math.abs(indices[1] - indices[0]) < 10)); + // Uses a special sortselect size so ensure this is set + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setLinearSortSelectSize(3) + ::partitionLSP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionLSPMoveSample(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1 || + (indices.length == 2 && Math.abs(indices[1] - indices[0]) < 10)); + // Uses a special sortselect size so ensure this is set + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setLinearSortSelectSize(3) + .setControlFlags(Partition.FLAG_MOVE_SAMPLE) + ::partitionLSP); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionLinearBFPRTPER(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1 || + (indices.length == 2 && Math.abs(indices[1] - indices[0]) < 10)); + // Require the range >= 5: uses a special sortselect size + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setLinearSortSelectSize(indices.length == 1 ? 3 : 5) + .setLinearStrategy(LinearStrategy.BFPRT) + .setExpandStrategy(ExpandStrategy.PER) + ::partitionLinear); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionLinearBFPRTT1(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1 || + (indices.length == 2 && Math.abs(indices[1] - indices[0]) < 10)); + // Require the range >= 5: uses a special sortselect size + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setLinearSortSelectSize(indices.length == 1 ? 3 : 5) + .setLinearStrategy(LinearStrategy.BFPRT) + .setExpandStrategy(ExpandStrategy.T1) + ::partitionLinear); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionLinearBFPRTB1(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1 || + (indices.length == 2 && Math.abs(indices[1] - indices[0]) < 10)); + // Require the range >= 5: uses a special sortselect size + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setLinearSortSelectSize(indices.length == 1 ? 3 : 5) + .setLinearStrategy(LinearStrategy.BFPRT) + .setExpandStrategy(ExpandStrategy.B1) + ::partitionLinear); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionLinearBFPRTT2(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1 || + (indices.length == 2 && Math.abs(indices[1] - indices[0]) < 10)); + // Require the range >= 2*5: uses a special sortselect size + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setLinearSortSelectSize(indices.length == 1 ? 6 : 10) + .setLinearStrategy(LinearStrategy.BFPRT) + .setExpandStrategy(ExpandStrategy.T2) + ::partitionLinear); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionLinearBFPRTB2(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1 || + (indices.length == 2 && Math.abs(indices[1] - indices[0]) < 10)); + // Require the range >= 2*5: uses a special sortselect size + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setLinearSortSelectSize(indices.length == 1 ? 5 : 10) + .setLinearStrategy(LinearStrategy.BFPRT) + .setExpandStrategy(ExpandStrategy.B2) + ::partitionLinear); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionLinearRS(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1 || + (indices.length == 2 && Math.abs(indices[1] - indices[0]) < 10)); + // Require the range >= 9: uses a special sortselect size + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setLinearSortSelectSize(indices.length == 1 ? 5 : 9) + .setLinearStrategy(LinearStrategy.RS) + .setExpandStrategy(ExpandStrategy.T1) + ::partitionLinear); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionLinearBFPRTImprovedT2(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1 || + (indices.length == 2 && Math.abs(indices[1] - indices[0]) < 10)); + // Require the range >= 2*5: uses a special sortselect size + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setLinearSortSelectSize(indices.length == 1 ? 5 : 10) + .setLinearStrategy(LinearStrategy.BFPRT_IM) + .setExpandStrategy(ExpandStrategy.T2) + ::partitionLinear); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionLinearRSImproved(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1 || + (indices.length == 2 && Math.abs(indices[1] - indices[0]) < 10)); + // Require the range >= 2*9: uses a special sortselect size + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setLinearSortSelectSize(indices.length == 1 ? 9 : 18) + .setLinearStrategy(LinearStrategy.RS_IM) + .setExpandStrategy(ExpandStrategy.B2) + ::partitionLinear); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionLinearRSAdaptive(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1 || + (indices.length == 2 && Math.abs(indices[1] - indices[0]) < 10)); + // Require the range >= 9: uses a special sortselect size + assertPartition(values, indices, new Partition(SP, QS, EC, SU) + .setLinearSortSelectSize(indices.length == 1 ? 5 : 9) + .setLinearStrategy(LinearStrategy.RSA) + .setExpandStrategy(ExpandStrategy.T1) + ::partitionLinear); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionQA(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1 || + (indices.length == 2 && Math.abs(indices[1] - indices[0]) < 10)); + // Require the range >= 12: uses a special sortselect size + assertPartition(values, indices, new Partition(SP, QS, EC, Partition.SUBSAMPLING_SIZE) + .setLinearSortSelectSize(indices.length == 1 ? 6 : 12) + .setExpandStrategy(ExpandStrategy.T1) + ::partitionQA); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionQAAlwaysAdapt(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1 || + (indices.length == 2 && Math.abs(indices[1] - indices[0]) < 10)); + // Require the range >= 12: uses a special sortselect size + assertPartition(values, indices, new Partition(SP, QS, EC, Partition.SUBSAMPLING_SIZE) + .setLinearSortSelectSize(indices.length == 1 ? 6 : 12) + .setExpandStrategy(ExpandStrategy.T1) + .setAdaptMode(AdaptMode.ADAPT1) + ::partitionQA); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionQANoSampling(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1 || + (indices.length == 2 && Math.abs(indices[1] - indices[0]) < 10)); + // Require the range >= 12: uses a special sortselect size + assertPartition(values, indices, new Partition(SP, QS, EC, Partition.SUBSAMPLING_SIZE) + .setLinearSortSelectSize(indices.length == 1 ? 6 : 12) + .setExpandStrategy(ExpandStrategy.T1) + // No sampling but always use variable margins + .setAdaptMode(AdaptMode.ADAPT1B) + ::partitionQA); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionQAFarStepAndMiddle12(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1 || + (indices.length == 2 && Math.abs(indices[1] - indices[0]) < 10)); + // Require the range >= 12: uses a special sortselect size + assertPartition(values, indices, new Partition(SP, QS, EC, Partition.SUBSAMPLING_SIZE) + .setLinearSortSelectSize(indices.length == 1 ? 6 : 12) + .setExpandStrategy(ExpandStrategy.T1) + .setControlFlags(Partition.FLAG_QA_FAR_STEP | Partition.FLAG_QA_MIDDLE_12) + ::partitionQA); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionQAFarStepAdaptOriginal(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1 || + (indices.length == 2 && Math.abs(indices[1] - indices[0]) < 10)); + // Require the range >= 12: uses a special sortselect size + assertPartition(values, indices, new Partition(SP, QS, EC, Partition.SUBSAMPLING_SIZE) + .setLinearSortSelectSize(indices.length == 1 ? 6 : 12) + .setExpandStrategy(ExpandStrategy.T1) + .setControlFlags(Partition.FLAG_QA_FAR_STEP_ADAPT_ORIGINAL) + ::partitionQA); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionQASampleK(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1 || + (indices.length == 2 && Math.abs(indices[1] - indices[0]) < 10)); + // Require the range >= 12: uses a special sortselect size + assertPartition(values, indices, new Partition(SP, QS, EC, Partition.SUBSAMPLING_SIZE) + .setLinearSortSelectSize(indices.length == 1 ? 6 : 12) + .setExpandStrategy(ExpandStrategy.T1) + .setControlFlags(Partition.FLAG_QA_SAMPLE_K) + ::partitionQA); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionQASampleStep(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1 || + (indices.length == 2 && Math.abs(indices[1] - indices[0]) < 10)); + // Require the range >= 2*12: uses a special sortselect size + assertPartition(values, indices, new Partition(SP, QS, EC, 200) + .setLinearSortSelectSize(indices.length == 1 ? 12 : 24) + .setExpandStrategy(ExpandStrategy.T2) + ::partitionQA); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionQASampleStepRandom1(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1 || + (indices.length == 2 && Math.abs(indices[1] - indices[0]) < 10)); + // Require the range >= 2*12: uses a special sortselect size + assertPartition(values, indices, new Partition(SP, QS, EC, 200) + .setLinearSortSelectSize(indices.length == 1 ? 12 : 24) + .setExpandStrategy(ExpandStrategy.T2) + .setControlFlags(Partition.FLAG_RANDOM_SAMPLING) + ::partitionQA); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition"}) + void testPartitionQASampleStepRandom2(double[] values, int[] indices) { + Assumptions.assumeTrue(indices.length == 1 || + (indices.length == 2 && Math.abs(indices[1] - indices[0]) < 10)); + // Require the range >= 2*12: uses a special sortselect size + assertPartition(values, indices, new Partition(SP, QS, EC, 200) + .setLinearSortSelectSize(indices.length == 1 ? 12 : 24) + .setExpandStrategy(ExpandStrategy.T2) + .setControlFlags(Partition.FLAG_QA_RANDOM_SAMPLING) + ::partitionQA); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition", "testPartitionBigData"}) + void testPartitionQA2FRSampling(double[] values, int[] indices) { + assertPartitionQA2(values, indices, Partition.MODE_FR_SAMPLING); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition", "testPartitionBigData"}) + void testPartitionQA2Sampling(double[] values, int[] indices) { + assertPartitionQA2(values, indices, Partition.MODE_SAMPLING); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition", "testPartitionBigData"}) + void testPartitionQA2Adaption(double[] values, int[] indices) { + assertPartitionQA2(values, indices, Partition.MODE_ADAPTION); + } + + @ParameterizedTest + @MethodSource(value = {"testPartition", "testPartitionBigData"}) + void testPartitionQA2Strict(double[] values, int[] indices) { + assertPartitionQA2(values, indices, Partition.MODE_STRICT); + } + + private static void assertPartitionQA2(double[] values, int[] indices, int mode) { + Assumptions.assumeTrue(indices.length == 1 || + (indices.length == 2 && Math.abs(indices[1] - indices[0]) < 10)); + Partition.configureQaAdaptive(mode, 1); + try { + assertPartition(values, indices, Partition::partitionQA2); + } finally { + Partition.configureQaAdaptive(Partition.MODE_FR_SAMPLING, 1); + } + } + + static void assertPartitionPaired(double[] values, int[] indices, DoublePartitionFunction2 function) { + // Create a paired version of the indices. + // We apply the partition function to this and test the result as if values + // had been partitioned using indices. + final BitSet bs = new BitSet(); + for (final int i : indices) { + bs.set(i); + } + final int[] unique = bs.stream().toArray(); + // compress pairs + int n = 1; + for (int i = 1; i < unique.length; i++) { + final int k = unique[i]; + if (k - 1 == unique[n - 1]) { + // Mark as pair with sign bit + unique[n - 1] |= Integer.MIN_VALUE; + continue; + } + unique[n++] = k; + } + final int[] k = Arrays.copyOf(unique, n); + TestUtils.shuffle(RandomSource.XO_RO_SHI_RO_128_PP.create(0xdeadbeef), k); + assertPartition(values, indices, (a, ignoredIndices, ignoredN) -> function.partition(a, k)); + } + + static void assertPartition(double[] values, int[] indices, DoublePartitionFunction function) { + final double[] data = values.clone(); + final double[] sorted = values.clone(); + Arrays.sort(sorted); + // Indices may be destructively modified + function.partition(data, indices.clone(), indices.length); + if (indices.length == 0) { + return; + } + for (final int k : indices) { + Assertions.assertEquals(sorted[k], data[k], () -> "k[" + k + "]"); + } + // Check partial ordering + Arrays.sort(indices); + int i = 0; + for (final int k : indices) { + final double value = sorted[k]; + while (i < k) { + final int j = i; + Assertions.assertTrue(Double.compare(data[i], value) <= 0, + () -> j + " < " + k + " : " + data[j] + " < " + value); + i++; + } + } + final int k = indices[indices.length - 1]; + final double value = sorted[k]; + while (i < data.length) { + final int j = i; + Assertions.assertTrue(Double.compare(data[i], value) >= 0, + () -> k + " < " + j); + i++; + } + Arrays.sort(data); + Assertions.assertArrayEquals(sorted, data, "Data destroyed"); + } + + static Stream testPartition() { + final Stream.Builder builder = Stream.builder(); + UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(123); + // Sizes above and below the threshold for partitioning. + // The largest size should trigger single-pivot sub-sampling for pivot selection. + for (final int size : new int[] {5, 47, SU + 10}) { + final int halfSize = size >>> 1; + final int from = -halfSize; + final int to = -halfSize + size; + final double[] values = IntStream.range(from, to).asDoubleStream().toArray(); + final double[] zeros = values.clone(); + final int quarterSize = size >>> 2; + Arrays.fill(zeros, quarterSize, halfSize, -0.0); + Arrays.fill(zeros, halfSize, halfSize + quarterSize, 0.0); + for (final int k : new int[] {1, 2, 3, size}) { + for (int i = 0; i < 15; i++) { + // Note: Duplicate indices do not matter + final int[] indices = rng.ints(k, 0, size).toArray(); + builder.add(Arguments.of( + TestUtils.shuffle(rng, values.clone()), + indices)); + builder.add(Arguments.of( + TestUtils.shuffle(rng, zeros.clone()), + indices)); + } + } + // Test sequential processing by creating potential ranges + // after an initial low point. This should be high enough + // so any range analysis that joins indices will leave the initial + // index as a single point. + final int limit = 50; + if (size > limit) { + for (int i = 0; i < 10; i++) { + final int[] indices = rng.ints(size - limit, limit, size).toArray(); + // This sets a low index + indices[rng.nextInt(indices.length)] = rng.nextInt(0, limit >>> 1); + builder.add(Arguments.of( + TestUtils.shuffle(rng, values.clone()), + indices)); + } + } + // min; max; min/max + builder.add(Arguments.of(values.clone(), new int[] {0})); + builder.add(Arguments.of(values.clone(), new int[] {size - 1})); + builder.add(Arguments.of(values.clone(), new int[] {0, size - 1})); + builder.add(Arguments.of(zeros.clone(), new int[] {0})); + builder.add(Arguments.of(zeros.clone(), new int[] {size - 1})); + builder.add(Arguments.of(zeros.clone(), new int[] {0, size - 1})); + } + final double nan = Double.NaN; + builder.add(Arguments.of(new double[] {}, new int[0])); + builder.add(Arguments.of(new double[] {nan}, new int[] {0})); + builder.add(Arguments.of(new double[] {-0.0, nan}, new int[] {1})); + builder.add(Arguments.of(new double[] {nan, nan, nan}, new int[] {2})); + builder.add(Arguments.of(new double[] {nan, 0.0, -0.0, nan}, new int[] {3})); + builder.add(Arguments.of(new double[] {nan, 0.0, -0.0, nan}, new int[] {1, 2})); + builder.add(Arguments.of(new double[] {nan, 0.0, 1, -0.0, nan}, new int[] {1, 3})); + builder.add(Arguments.of(new double[] {nan, 0.0, -0.0}, new int[] {0, 2})); + builder.add(Arguments.of(new double[] {nan, 1.23, 0.0, -4.56, -0.0, nan}, new int[] {0, 1, 3})); + // Dual-pivot with a large middle region (> 5 / 8) requires equal elements loop + final int n = 128; + final double[] x = IntStream.range(0, n).asDoubleStream().toArray(); + // Put equal elements in the central region: + // 2/16 6/16 10/16 14/16 + // | P2 | + final int sixteenth = n / 16; + final int i2 = 2 * sixteenth; + final int i6 = 6 * sixteenth; + final double p1 = x[i2]; + final double p2 = x[n - i2]; + // Lots of values equal to the pivots + Arrays.fill(x, i2, i6, p1); + Arrays.fill(x, n - i6, n - i2, p2); + // Equal value in between the pivots + Arrays.fill(x, i6, n - i6, (p1 + p2) / 2); + // Shuffle this and partition in the middle. + // Use a fix seed to ensure we hit coverage with only 5 loops. + rng = RandomSource.XO_SHI_RO_128_PP.create(-8111061151820577011L); + for (int i = 0; i < 5; i++) { + builder.add(Arguments.of(TestUtils.shuffle(rng, x.clone()), new int[] {50})); + } + // A single value smaller/greater than the pivot at the left/right/both ends + Arrays.fill(x, 1); + for (int i = 0; i <= 2; i++) { + for (int j = 0; j <= 2; j++) { + x[n - 1] = i; + x[0] = j; + builder.add(Arguments.of(x.clone(), new int[] {50})); + } + } + return builder.build(); + } + + static Stream testPartitionBigData() { + final Stream.Builder builder = Stream.builder(); + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(123); + // Sizes above the threshold (600) for recursive partitioning + for (final int size : new int[] {1000, 5000, 10000}) { + final double[] a = IntStream.range(0, size).asDoubleStream().toArray(); + // With repeat elements + final double[] b = rng.ints(size, 0, size >> 3).asDoubleStream().toArray(); + for (int i = 0; i < 15; i++) { + builder.add(Arguments.of( + TestUtils.shuffle(rng, a.clone()), + new int[] {rng.nextInt(size)})); + builder.add(Arguments.of(b.clone(), + new int[] {rng.nextInt(size)})); + } + } + // Hit Floyd-Rivest sub-sampling conditions. + // Close to edge but outside edge select size. + final int n = 7000; + final double[] x = IntStream.range(0, n).asDoubleStream().toArray(); + builder.add(Arguments.of(x.clone(), new int[] {20})); + builder.add(Arguments.of(x, new int[] {n - 1 - 20})); + return builder.build(); + } + + @ParameterizedTest + @MethodSource(value = {"testSort"}) + void testSortSBM(double[] values) { + assertSort(values, + new Partition(SP, 3)::sortSBM); + } + + @ParameterizedTest + @MethodSource(value = {"testSort"}) + void testHeapSortUsingHeapSelectRange(double[] values) { + assumeNonNaN(values); + Assumptions.assumeTrue(values.length > 0); + assertSort(values, x -> { + replaceNegativeZeros(x, 0, x.length - 1); + Partition.heapSelectRange(x, 0, x.length - 1, 0, x.length - 1); + restoreNegativeZeros(x, 0, x.length - 1); + }); + } + + @ParameterizedTest + @MethodSource(value = {"testSort"}) + void testHeapSort(double[] values) { + assumeNonNaN(values); + Assumptions.assumeTrue(values.length > 0); + assertSort(values, x -> { + replaceNegativeZeros(x, 0, x.length - 1); + Partition.heapSort(x, 0, x.length - 1); + restoreNegativeZeros(x, 0, x.length - 1); + }); + } + + @ParameterizedTest + @MethodSource(value = {"testSort"}) + void testSortISP(double[] values) { + assertSort(values, new Partition(SP, QS) + .setSPStrategy(SPStrategy.SP) + ::sortISP); + } + + @ParameterizedTest + @MethodSource(value = {"testSort"}) + void testSortIBM(double[] values) { + assertSort(values, new Partition(SP, QS) + .setSPStrategy(SPStrategy.BM) + ::sortISP); + } + + @ParameterizedTest + @MethodSource(value = {"testSort"}) + void testSortISBM(double[] values) { + assertSort(values, new Partition(SP, QS) + .setSPStrategy(SPStrategy.SBM) + ::sortISP); + } + + @ParameterizedTest + @MethodSource(value = {"testSort"}) + void testSortIKBM(double[] values) { + assertSort(values, new Partition(SP, QS) + .setSPStrategy(SPStrategy.KBM) + ::sortISP); + } + + @ParameterizedTest + @MethodSource(value = {"testSort"}) + void testSortIDNF1(double[] values) { + assertSort(values, new Partition(SP, QS) + .setSPStrategy(SPStrategy.DNF1) + ::sortISP); + } + + @ParameterizedTest + @MethodSource(value = {"testSort"}) + void testSortIDNF2(double[] values) { + assertSort(values, new Partition(SP, QS) + .setSPStrategy(SPStrategy.DNF2) + ::sortISP); + } + + @ParameterizedTest + @MethodSource(value = {"testSort"}) + void testSortIDNF3(double[] values) { + assertSort(values, new Partition(SP, QS) + .setSPStrategy(SPStrategy.DNF3) + ::sortISP); + } + + @ParameterizedTest + @MethodSource(value = {"testSort"}) + void testSortIDP(double[] values) { + assertSort(values, + new Partition(DP, QS2)::sortIDP); + } + + @ParameterizedTest + @MethodSource(value = {"testSort"}) + void testInsertionSort(double[] values) { + assumeNonNaN(values); + assertSort(values, x -> { + replaceNegativeZeros(x, 0, x.length - 1); + Sorting.sort(x, 0, x.length - 1, false); + restoreNegativeZeros(x, 0, x.length - 1); + }); + if (values.length < 2) { + return; + } + // Check internal sort + // Set pivot at lower end + values[0] = Arrays.stream(values).min().getAsDouble(); + // check internal sort + assertSort(values, x -> { + replaceNegativeZeros(x, 1, x.length - 1); + Sorting.sort(x, 1, x.length - 1, false); + restoreNegativeZeros(x, 1, x.length - 1); + }); + } + + @ParameterizedTest + @MethodSource(value = {"testSort"}) + void testInsertionSort5(double[] values) { + // Cannot handle NaN or -0.0 + // Negative zeros are swapped for a proxy + assumeNonNaN(values); + final double[] data = Arrays.copyOf(values, 5); + assertSort(data, x -> { + replaceNegativeZeros(x, 0, x.length - 1); + Sorting.sort5(x, 0, 1, 2, 3, 4); + restoreNegativeZeros(x, 0, x.length - 1); + }); + } + + @Test + void testSortZero() { + final double a = -0.0; + final double b = 0.0; + final double[][] values = new double[][] { + {a, a}, + {a, b}, + {b, a}, + {b, b}, + {a, a, a}, + {a, a, b}, + {a, b, a}, + {a, b, b}, + {b, a, a}, + {b, a, b}, + {b, b, a}, + {b, b, b}, + {a, a, a, a}, + {a, a, a, b}, + {a, a, b, a}, + {a, a, b, b}, + {a, b, a, a}, + {a, b, a, b}, + {a, b, b, a}, + {a, b, b, b}, + {b, a, a, a}, + {b, a, a, b}, + {b, a, b, a}, + {b, a, b, b}, + {b, b, a, a}, + {b, b, a, b}, + {b, b, b, a}, + {b, b, b, b}, + }; + for (final double[] v : values) { + assertSort(v, x -> Partition.sortZero(x, 0, x.length - 1)); + } + } + + private static void assertSort(double[] values, Consumer function) { + final double[] data = values.clone(); + final double[] sorted = values.clone(); + Arrays.sort(sorted); + function.accept(data); + Assertions.assertArrayEquals(sorted, data); + } + + static Stream testSort() { + final Stream.Builder builder = Stream.builder(); + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(123); + // Sizes above and below the threshold for partitioning + for (final int size : new int[] {5, 50}) { + double[] a = new double[size]; + Arrays.fill(a, 1.23); + builder.add(a.clone()); + for (int ii = 0; ii < size; ii++) { + a[ii] = ii; + } + builder.add(a.clone()); + for (int ii = 0; ii < size; ii++) { + a[ii] = size - ii; + } + builder.add(a.clone()); + for (int i = 0; i < 5; i++) { + a = rng.doubles(size).toArray(); + builder.add(a.clone()); + final int j = rng.nextInt(size); + final int k = rng.nextInt(size); + a[j] = Double.NaN; + a[k] = Double.NaN; + builder.add(a.clone()); + a[j] = -0.0; + a[k] = 0.0; + builder.add(a.clone()); + for (int z = 0; z < size; z++) { + a[z] = rng.nextBoolean() ? -0.0 : 0.0; + } + builder.add(a.clone()); + a[j] = -rng.nextDouble(); + a[k] = rng.nextDouble(); + builder.add(a.clone()); + } + } + final double nan = Double.NaN; + builder.add(new double[] {}); + builder.add(new double[] {nan}); + builder.add(new double[] {-0.0, nan}); + builder.add(new double[] {nan, nan, nan}); + builder.add(new double[] {nan, 0.0, -0.0, nan}); + builder.add(new double[] {nan, 0.0, -0.0}); + builder.add(new double[] {nan, 0.0, 1, -0.0}); + builder.add(new double[] {nan, 1.23, 0.0, -4.56, -0.0, nan}); + return builder.build(); + } + + /** + * Test key analysis. + * The key analysis code decides the partition strategy. Currently this + * supports recommendations for processing keys or ranges of keys in ascending + * order based on separation between points, and the point when data partitioning + * switches to a full sort. + * + * @param size Length of the data to partition. + * @param k Indices (non-zero length). + * @param n Count of indices (either {@code 1 <= n <= k.length} or -1). + * @param minSeparation Minimum separation between points. + * @param minSelectSize Minimum selection size for insertion sort rather than selection. + * @param expected Expected keys (up to the end of the indices or the marker {@link Integer#MIN_VALUE}) + * @param cacheRange {@code [L, R]} bounds of returned {@link PivotCache}, or null. + */ + @ParameterizedTest + @MethodSource + void testKeyAnalysis(int size, int[] k, int n, int minSeparation, int minSelectSize, + int[] expected, int[] cacheRange) { + // Set the number of keys + n = n < 0 ? k.length : n; + final PivotCache pivotCache = new Partition(SP, minSelectSize) + .keyAnalysis(size, k, n, minSeparation); + // Truncate to the marker + int m = 0; + while (m < n && k[m] != Integer.MIN_VALUE) { + m++; + } + if (m == 0) { + // Full sort recommendation + Assertions.assertArrayEquals(expected, new int[] {Integer.MIN_VALUE}); + } else { + final int[] actual = Arrays.copyOf(k, m); + Assertions.assertArrayEquals(expected, actual, + () -> Arrays.toString(actual)); + } + if (cacheRange == null) { + Assertions.assertNull(pivotCache); + } else { + Assertions.assertEquals(cacheRange[0], pivotCache.left()); + Assertions.assertEquals(cacheRange[1], pivotCache.right()); + } + } + + static Stream testKeyAnalysis() { + final Stream.Builder builder = Stream.builder(); + final int allK = -1; + final int[] noCache = null; + final int[] fullSort = new int[] {Integer.MIN_VALUE}; + builder.add(Arguments.of(100, new int[] {3}, allK, 2, 0, + new int[] {3}, noCache)); + builder.add(Arguments.of(100, new int[] {3, 4, 5}, allK, 1, 0, + new int[] {3, 5}, noCache)); + builder.add(Arguments.of(100, new int[] {3, 4, 5, 8}, allK, 2, 0, + new int[] {3, 5, ~8}, new int[] {8, 8})); + builder.add(Arguments.of(100, new int[] {3, 4, 5, 6, 7, 8}, allK, 1, 0, + new int[] {3, 8}, noCache)); + builder.add(Arguments.of(100, new int[] {3, 4, 7, 8}, allK, 1, 0, + new int[] {3, 4, 7, 8}, new int[] {7, 8})); + builder.add(Arguments.of(100, new int[] {3, 4, 7, 8, 99}, allK, 1, 0, + new int[] {3, 4, 7, 8, ~99}, new int[] {7, 99})); + // Full sort recommendation: cases not large enough + builder.add(Arguments.of(20, new int[] {3, 5, 8, 17}, allK, 3, 0, + new int[] {3, 8, ~17}, new int[] {17, 17})); + builder.add(Arguments.of(20, new int[] {3, 5, 8, 17}, allK, 3, 10, + new int[] {3, 8, ~17}, new int[] {17, 17})); + builder.add(Arguments.of(20, new int[] {3, 5, 8, 17}, allK, 9, 0, + new int[] {3, 17}, noCache)); + // Full sort based on a single range to the end (due to high min separation) + builder.add(Arguments.of(20, new int[] {3, 5, 8, 17}, allK, 10, 10, + fullSort, noCache)); + // Full sort based on min select size + builder.add(Arguments.of(20, new int[] {10, 11}, allK, 1, 20, + fullSort, noCache)); + // No min separation - process each index + builder.add(Arguments.of(100, new int[] {3, 4, 5}, allK, 0, 0, + new int[] {~3, ~4, ~5}, new int[] {4, 5})); + builder.add(Arguments.of(100, new int[] {3, 4, 7, 8}, allK, 0, 0, + new int[] {~3, ~4, ~7, ~8}, new int[] {4, 8})); + // Duplicate keys + builder.add(Arguments.of(100, new int[] {0, 1, 2, 2, 3, 3}, allK, 0, 0, + new int[] {~0, ~1, ~2, ~3}, new int[] {1, 3})); + builder.add(Arguments.of(100, new int[] {0, 1, 2, 2, 3, 3}, allK, 1, 0, + new int[] {0, 3}, noCache)); + builder.add(Arguments.of(100, new int[] {0, 1, 2, 2, 3, 3, 8, 8, 8}, allK, 2, 0, + new int[] {0, 3, ~8}, new int[] {8, 8})); + builder.add(Arguments.of(100, new int[] {9, 6, 7, 8, 2, 1, 1, 3}, allK, 2, 0, + new int[] {1, 3, 6, 9}, new int[] {6, 9})); + // TODO: more cases + + // Repeat the contents of the stream with any case not using the full length of the data. + // by padding with random indices (these should be ignored) + final Stream.Builder builder2 = Stream.builder(); + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(); + builder.build().forEach(arg -> { + builder2.add(arg); + // unpack + final Object[] o = arg.get(); + final int size = (int) o[0]; + final int[] k = (int[]) o[1]; + final int n = (int) o[2]; + if (n < 0) { + final Object[] o2 = o.clone(); + // Add extra + final int extra = rng.nextInt(3, 10); + final int len = k.length; + // Extra are zeros + final int[] k2 = Arrays.copyOf(k, len + extra); + o2[1] = k2.clone(); + o2[2] = len; + builder2.add(Arguments.of(o2)); + // Deliberately add indices not in the original + final Object[] o3 = o2.clone(); + final int max = Arrays.stream(k).max().getAsInt(); + for (int i = len; i < k2.length; i++) { + k2[i] = rng.nextInt(max, size); + } + o3[1] = k2; + builder2.add(Arguments.of(o3)); + } + }); + return builder2.build(); + } + + /** + * Assume the data are non-NaN, otherwise skip the test. + * + * @param a Data. + */ + private void assumeNonNaN(double[] a) { + for (int i = 0; i < a.length; i++) { + Assumptions.assumeFalse(Double.isNaN(a[i])); + } + } + + /** + * Replace negative zeros with a proxy. Uses -{@link Double#MIN_VALUE} as the proxy. + * + * @param a Data. + * @param from Lower bound (inclusive). + * @param to Upper bound (inclusive). + */ + private static void replaceNegativeZeros(double[] a, int from, int to) { + for (int i = from; i <= to; i++) { + if (Double.doubleToRawLongBits(a[i]) == Long.MIN_VALUE) { + a[i] = -Double.MIN_VALUE; + } + } + } + /** + * Restore proxy negative zeros. + * + * @param a Data. + * @param from Lower bound (inclusive). + * @param to Upper bound (inclusive). + */ + private static void restoreNegativeZeros(double[] a, int from, int to) { + for (int i = from; i <= to; i++) { + if (a[i] == -Double.MIN_VALUE) { + a[i] = -0.0; + } + } + } + + @ParameterizedTest + @MethodSource + void testSearch(int[] keys, int left, int right) { + // Clip to correct range + final int l = left < 0 ? 0 : left; + final int r = right < 0 ? keys.length - 1 : right; + for (int i = l; i <= r; i++) { + final int k = keys[i]; + // Unspecified index when key is present + Assertions.assertEquals(k, keys[Partition.searchLessOrEqual(keys, l, r, k)], "leq"); + Assertions.assertEquals(k, keys[Partition.searchGreaterOrEqual(keys, l, r, k)], "geq"); + } + // Search above/below keys + Assertions.assertEquals(l - 1, Partition.searchLessOrEqual(keys, l, r, keys[l] - 44), "leq below"); + Assertions.assertEquals(r, Partition.searchLessOrEqual(keys, l, r, keys[r] + 44), "leq above"); + Assertions.assertEquals(l, Partition.searchGreaterOrEqual(keys, l, r, keys[l] - 44), "geq below"); + Assertions.assertEquals(r + 1, Partition.searchGreaterOrEqual(keys, l, r, keys[r] + 44), "geq above"); + // Search between neighbour keys + for (int i = l + 1; i <= r; i++) { + // Bound: keys[i-1] < k < keys[i] + final int k1 = keys[i - 1]; + final int k2 = keys[i]; + for (int k = k1 + 1; k < k2; k++) { + Assertions.assertEquals(i - 1, Partition.searchLessOrEqual(keys, l, r, k), "leq between"); + Assertions.assertEquals(i, Partition.searchGreaterOrEqual(keys, l, r, k), "geq between"); + } + } + } + + static Stream testSearch() { + final Stream.Builder builder = Stream.builder(); + final int allIndices = -1; + builder.add(Arguments.of(new int[] {1}, allIndices, allIndices)); + builder.add(Arguments.of(new int[] {1, 2}, allIndices, allIndices)); + builder.add(Arguments.of(new int[] {1, 10}, allIndices, allIndices)); + builder.add(Arguments.of(new int[] {1, 2, 3}, allIndices, allIndices)); + builder.add(Arguments.of(new int[] {1, 4, 7}, allIndices, allIndices)); + builder.add(Arguments.of(new int[] {1, 4, 5, 7}, allIndices, allIndices)); + // Duplicates. These match binary search when found. + builder.add(Arguments.of(new int[] {1, 1, 1, 1, 1, 1}, allIndices, allIndices)); + builder.add(Arguments.of(new int[] {1, 1, 1, 1, 3, 3, 3, 3, 3, 5, 5, 5, 5}, allIndices, allIndices)); + // Part of the range + builder.add(Arguments.of(new int[] {1, 4, 5, 7, 13, 15}, 2, 4)); + builder.add(Arguments.of(new int[] {1, 4, 5, 7, 13, 15}, 0, 3)); + builder.add(Arguments.of(new int[] {1, 4, 5, 7, 13, 15}, 3, 5)); + return builder.build(); + } + + @ParameterizedTest + @MethodSource + void testPartitionDP(double[] a, int pivot1, int pivot2, int k0, int[] bounds) { + final int r = a.length - 1; + final int[] b = new int[3]; + Assertions.assertEquals(k0, Partition.partitionDP(a, 0, r, pivot1, pivot2, b)); + Assertions.assertArrayEquals(bounds, b); + } + + static Stream testPartitionDP() { + final Stream.Builder builder = Stream.builder(); + // Test less-than fast-forward bounds check - all values are < the pivots + //builder.add(Arguments.of(new double[] {3, 4, 10, 12, 5, 6}, 2, 3, 4, new int[] {5, 5, 5})); + builder.add(Arguments.of(new double[] {3, 4, 10, 12, 5, 6}, 2, 3, 4, new int[] {5, 4, 5})); + // Test greater-than fast-forward bounds check - all values are > the pivots + //builder.add(Arguments.of(new double[] {3, 4, 1, 2, 5, 6}, 2, 3, 0, new int[] {1, 1, 1})); + builder.add(Arguments.of(new double[] {3, 4, 1, 2, 5, 6}, 2, 3, 0, new int[] {1, 0, 1})); + return builder.build(); + } + + /** + * Test the RNG used for shuffling. This test creates a RNG seeded using + * {@code n} and {@code k}. Then an ascending array of size = 2^power is created + * and shuffled to create the given number of {@code samples}. Each sample + * is used to add counts to a histogram. Actual values {@code v} are mapped to buckets + * using {@code v >> shift}. The number of buckets is 2^(power - shift) and the + * width of the bucket is 2^shift. The histogram counts should be uniform. + * + * @param n Seed. + * @param k Seed. + * @param samples Number of samples. + * @param power Defines the length of the data. + * @param shift Defines the bucket size for histogram counts. + */ + @ParameterizedTest + @MethodSource + void testRNG(int n, int k, int samples, int power, int shift) { + final int size = 1 << power; + final int[] a = IntStream.range(0, size).toArray(); + // histogram of index block count for each position + final long[][] h = new long[size][size >>> shift]; + final IntUnaryOperator rng = Partition.createFastRNG(n, k); + for (int s = samples; --s >= 0;) { + // Shuffle the data + for (int i = a.length; i > 1;) { + final int j = rng.applyAsInt(i); + final int t = a[--i]; + a[i] = a[j]; + a[j] = t; + } + // Add to histogram + for (int i = 0; i < size; i++) { + h[i][a[i] >>> shift]++; + } + } + // Chi-square test the histogram is uniform + final double p = org.apache.commons.math3.stat.inference.TestUtils.chiSquareTest(h); + Assertions.assertFalse(p < 1e-3, () -> "Not uniform: " + p); + } + + static Stream testRNG() { + final Stream.Builder builder = Stream.builder(); + // Smallest sample size for n=600 ~ 37 ~ 2^32 + builder.add(Arguments.of(600, 52, 100, 5, 1)); + // Smallest sample size for n=6000 ~ 167 ~ 2^8 + builder.add(Arguments.of(6000, 789, 100, 8, 3)); + // Largest sample size for n=2^31 ~ 830192 ~ 2^20. + // This works but is slow + //builder.add(Arguments.of(830192, 678, 100, 20, 12)); + return builder.build(); + } + + /** + * This is not a test. It runs the introselect algorithm as a full sort on the specified + * data. A histogram of the level of recursion required to visit all regions is recorded + * to file. + */ + @ParameterizedTest + @MethodSource + @Disabled("Used for testing") + void testRecursion(Distribution dist, Modification mod, int length, int range, boolean dualPivot) { + final int maxDepth = 2048; + final int[] h = new int[maxDepth + 1]; + // Use the defaults. + // If the single pivot strategy is changed from MEDIAN_OF_3 to DYNAMIC + // this avoid excess recursion. + final Partition p = new Partition( + Partition.PIVOTING_STRATEGY, + //PivotingStrategy.MEDIAN_OF_3, // Use this to see excess recursion + Partition.DUAL_PIVOTING_STRATEGY, + Partition.MIN_QUICKSELECT_SIZE, + Partition.EDGESELECT_CONSTANT, + Partition.SUBSAMPLING_SIZE); + p.setRecursionConsumer(i -> h[maxDepth - i]++); + final AbstractDataSource source = new AbstractDataSource() { + @Override + protected int getLength() { + return length; + } + }; + source.setDistribution(dist); + source.setModification(mod); + source.setRange(range); + source.setup(); + + // Sort the data. This will record the recursion depth when a region is complete. + for (int i = 0; i < source.size(); i++) { + final double[] x = source.getDataSample(i); + if (dualPivot) { + p.introselect(Partition::partitionDP, x, 0, x.length - 1, + IndexIntervals.anyIndex(), 0, x.length - 1, maxDepth); + } else { + p.introselect(Partition::partitionSBM, x, 0, x.length - 1, + IndexIntervals.anyIndex(), 0, x.length - 1, maxDepth); + } + } + + // Bracket the histogram. Assume at least 1 non-zero value. + int hi = h.length; + do { + --hi; + } while (h[hi] == 0); + + // Summary statistics + long s = 0; + long ss = 0; + long n = 0; + for (int i = 0; i < h.length; i++) { + final int c = h[i]; + if (c != 0) { + n += c; + s += (long) i * c; + ss += (long) i * i * c; + } + } + final double mean = s / (double) n; + double variance = ss - ((double) s * s) / n; + if (variance > 0) { + variance = variance / (n - 1); + } else { + variance = Double.isFinite(variance) ? 0.0 : Double.NaN; + } + final String name = dualPivot ? "DP" : "SP"; + final String distName = dist == null ? "ALL" : dist.name(); + final String modName = mod == null ? "ALL" : mod.name(); + // Flag when the method used excessive recursion. + // Note that recursion only occurs down to a small length which is finished with a sort. + final double expected = Math.log((length + range * 0.5) / Partition.MIN_QUICKSELECT_SIZE) / + Math.log(dualPivot ? 3 : 2); + String excess = ""; + for (double m = mean; m > expected && m > mean - 10; m -= 1) { + excess += "*"; + } + TestUtils.printf("%s %10s %15s %d-%d : n=%11d mean=%10.6f std=%10.6f max=%4d : expected=%10.6f %s%n", + name, distName, modName, length, length + range, + n, mean, Math.sqrt(variance), hi, expected, excess); + + // Record the histogram + final String dir = System.getProperty("java.io.tmpdir"); + final Path path = Paths.get(dir, String.format("%s_%s_%s_%d-%d.txt", + name, distName, modName, length, length + range)); + try (BufferedWriter bw = Files.newBufferedWriter(path); + Formatter f = new Formatter(bw)) { + for (int i = 0; i <= hi; i++) { + f.format("%d %d%n", i, h[i]); + } + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + } + + static Stream testRecursion() { + TestUtils.printf("Save directory: %s%n", System.getProperty("java.io.tmpdir")); + + final Stream.Builder builder = Stream.builder(); + //final int length = 10000023; + final int length = 1023; + final int range = 2; + + // All + //builder.add(Arguments.of(null, null, length, range, true)); + //builder.add(Arguments.of(null, null, length, range, false)); + + // Individual distribution / modification + // Both single-pivot method (using DYNAMIC pivoting strategy) and dual-pivot + // method have a mean recursion just above the theoretical max recursion depth. + for (final Boolean dp : new Boolean[] {Boolean.TRUE, Boolean.FALSE}) { + for (final Distribution dist : Distribution.values()) { + for (final Modification mod : Modification.values()) { + builder.add(Arguments.of(dist, mod, length, range, dp)); + } + } + } + + return builder.build(); + } + + /** + * This is not a test. It outputs the Bentley and McIlroy test data, optionally + * with a single round of partitioning performed. This allows visualising the + * change to the data made by the partition algorithm. + */ + @ParameterizedTest + @MethodSource + @Disabled("Used for testing") + void testData(int length, int seed, int partition) { + final AbstractDataSource source = new AbstractDataSource() { + @Override + protected int getLength() { + return length; + } + }; + source.setRange(0); + source.setModification(Modification.COPY); + source.setSeed(seed); + source.setRngSeed(0xdeadbeef); + source.setup(); + + // Get the data + final double[][] data = IntStream.range(0, source.size()) + .mapToObj(source::getDataSample).toArray(double[][]::new); + + // Optional: Run a single round of partitioning on the data. + final LinkedList pivots = new LinkedList<>(); + final int left = 0; + final int right = length - 1; + final int[] bounds = new int[3]; + if (partition == 1) { + for (final double[] d : data) { + int p = Partition.PIVOTING_STRATEGY.pivotIndex(d, left, right, (left + right) >>> 1); + p = Partition.partitionSBM(d, left, right, p, bounds); + pivots.add(formatPivotRange(p, bounds[0])); + } + } else if (partition == 2) { + for (final double[] d : data) { + int p = Partition.DUAL_PIVOTING_STRATEGY.pivotIndex(d, left, right, bounds); + p = Partition.partitionDP(d, left, right, p, bounds[0], bounds); + pivots.add(formatPivotRange(p, bounds[0]) + ":" + formatPivotRange(bounds[1], bounds[2])); + } + } + + // Print header (distributions are in enum order) + TestUtils.printf("m %d%n", seed); + TestUtils.printf("i"); + for (final Distribution d : Distribution.values()) { + TestUtils.printf(" %s", d); + if (!pivots.isEmpty()) { + TestUtils.printf(pivots.pop()); + } + } + TestUtils.printf("%n"); + + // Sort the data. This will record the recursion depth when a region is complete. + for (int j = 0; j < length; j++) { + TestUtils.printf("%d", j); + for (int i = 0; i < data.length; i++) { + TestUtils.printf(" %s", data[i][j]); + } + TestUtils.printf("%n"); + } + } + + private static String formatPivotRange(int lo, int hi) { + return lo == hi ? Integer.toString(lo) : lo + "-" + hi; + } + + static Stream testData() { + final Stream.Builder builder = Stream.builder(); + builder.add(Arguments.of(128, 32, 0)); + return builder.build(); + } + + /** + * Prints the size of the Floyd-Rivest recursive subset samples. + * This method is for information purposes. It is intended to be pasted into JShell + * and called with various parameters. Example: + * + *
{@code
+     * jshell> printSubSamplingSize(0, 1000000, 5000)
+     * 5000 [0, 1000000] (k=0.005 * 1000001) -> [4843, 9843] (k=0.032 * 5001)
+     * 5000 [4843, 9843] (k=0.032 * 5001) -> [4977, 5124] (k=0.162 * 148)
+     *
+     * jshell> printSubSamplingSize(0, 1000000, 500000)
+     * 500000 [0, 1000000] (k=0.500 * 1000001) -> [497631, 502631] (k=0.474 * 5001)
+     * 500000 [497631, 502631] (k=0.474 * 5001) -> [499913, 500059] (k=0.599 * 147)
+     * }
+ * + * @param l Left bound (inclusive). + * @param r Right bound (inclusive). + * @param k Target index. + */ + static void printSubSamplingSize(int l, int r, int k) { + int n = r - l; + if (n > 600) { + // Floyd-Rivest: use SELECT recursively on a sample of size S to get an estimate + // for the (k-l+1)-th smallest element into a[k], biased slightly so that the + // (k-l+1)-th element is expected to lie in the smaller set after partitioning. + ++n; + final int i = k - l + 1; + final double z = Math.log(n); + // s ~ sub-sample size ~ 0.5 * n^2/3 + final double s = 0.5 * Math.exp(0.6666666666666666 * z); + final double sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * Integer.signum(i - (n >> 1)); + final int ll = Math.max(l, (int) (k - i * s / n + sd)); + final int rr = Math.min(r, (int) (k + (n - i) * s / n + sd)); + // CHECKSTYLE: stop regex + System.out.printf("%d [%d, %d] (k=%.3f * %d) -> [%d, %d] (k=%.3f * %d)%n", + k, l, r, (double) i / n, n, ll, rr, (double) (k - ll + 1) / (rr - ll + 1), rr - ll + 1); + // CHECKSTYLE: start regex + printSubSamplingSize(ll, rr, k); + } + } + + /** + * Prints the size of the Kiwiel Floyd-Rivest recursive subset samples. + * This method is for information purposes. It is intended to be pasted into JShell + * and called with various parameters. Example: + * + *
{@code
+     * jshell> printSubSamplingSize(0, 1000000, 5000)
+     * 5000 [0, 1000000] (k=0.005 * 1000001) -> [4843, 9843] (k=0.032 * 5001)
+     * 5000 [4843, 9843] (k=0.032 * 5001) -> [4977, 5124] (k=0.162 * 148)
+     *
+     * jshell> printSubSamplingSize(0, 1000000, 500000)
+     * 500000 [0, 1000000] (k=0.500 * 1000001) -> [497631, 502631] (k=0.474 * 5001)
+     * 500000 [497631, 502631] (k=0.474 * 5001) -> [499913, 500059] (k=0.599 * 147)
+     * }
+ * + * @param l Left bound (inclusive). + * @param r Right bound (inclusive). + * @param k Target index. + */ + static void printKSubSamplingSize(int l, int r, int k) { + int n = r - l; + if (n > 600) { + // Floyd-Rivest sub-sampling + ++n; + // Step 1: Choose sample size s <= n-1 and gap g > 0 + final double z = Math.log(n); + // sample size = alpha * n^(2/3) * ln(n)^1/3 (4.1) + // sample size = alpha * n^(2/3) (4.17; original Floyd-Rivest size) + final double s = 0.5 * Math.exp(0.6666666666666666 * z) * Math.cbrt(z); + //final double s = 0.5 * Math.exp(0.6666666666666666 * z); + // gap = sqrt(beta * s * ln(n)) + final double g = Math.sqrt(0.25 * s * z); + final int rs = (int) (l + s - 1); + // Step 3: pivot selection + final double isn = (k - l + 1) * s / n; + final int ku = (int) Math.max(Math.floor(l - 1 + isn - g), l); + final int kv = (int) Math.min(Math.ceil(l - 1 + isn + g), rs); + // CHECKSTYLE: stop regex + System.out.printf("%d [%d, %d] (k=%.3f * %d) -> [0, %d, %d, %d] (ku=%.3f; kv=%.3f)%n", + k, l, r, (double) (k - l + 1) / n, n, ku, kv, rs, (double) (ku + 1) / (rs + 1), (double) (kv + 1) / (rs + 1)); + // CHECKSTYLE: start regex + printKSubSamplingSize(0, rs, ku); + printKSubSamplingSize(0, rs, kv); + } + } + + /** + * This is not a test. It runs the introselect algorithm with data that may trigger excess + * recursion when using the Floyd-Rivest algorithm. Use of a random sample can avoid + * excess recursion when the local data is non-representative of the range to partition. + */ + @ParameterizedTest + @MethodSource + @Disabled("Used for testing") + void testFloydRivestRecursion(int n, int subSamplingSize, PivotingStrategy sp, int controlFlags, + PairedKeyStrategy pk, double recursionMultiple, int recursionConstant) { + final AbstractDataSource source = new AbstractDataSource() { + @Override + protected int getLength() { + return n; + } + }; + source.setRange(0); + source.setup(); + // Target the "median" + final int[] k = {source.getLength() >> 1}; + final Partition p = new Partition(sp, QS, EC, subSamplingSize) + .setPairedKeyStrategy(pk) + .setRecursionMultiple(recursionMultiple) + .setRecursionConstant(recursionConstant); + p.setControlFlags(controlFlags); + final ArrayList excess = new ArrayList<>(); + for (int i = 0; i < source.size(); i++) { + final int index = i; + p.setRecursionConsumer(v -> excess.add(String.format( + "%d: %s%n", index, source.getDataSampleInfo(index)))); + p.partitionISP(source.getDataSample(i), k, 1); + } + TestUtils.printf("n=%d, su=%d, %s, flags=%d : stopped=%d%n", + n, subSamplingSize, sp, controlFlags, excess.size()); + //excess.forEach(TestUtils::printf); + } + + static Stream testFloydRivestRecursion() { + final Stream.Builder builder = Stream.builder(); + + // The following test cases show that using FR is better than median of 3 at avoiding + // excess recursion; but not as good as median of 9. + // However FR is faster than median of 9 on many datasets (over two-fold on large data). + // To mitigate worst-case recursion when using FR we can use a random sub-sample + // allowing the speed of FR without the weakness of excess recursion on patterned data. + + // n=5000 : # samples = 402 + // These use the original FR size of 600. + + // Recursion stopper using max depth + //n=5000, su=2147483647, MEDIAN_OF_3, flags=0 : stopped=25 + //n=5000, su=600, MEDIAN_OF_3, flags=0 : stopped=8 + //n=5000, su=600, MEDIAN_OF_9, flags=0 : stopped=3 + //n=5000, su=600, MEDIAN_OF_3, flags=2 : stopped=2 + //n=5000, su=2147483647, MEDIAN_OF_9, flags=0 : stopped=0 + //n=5000, su=600, MEDIAN_OF_9, flags=2 : stopped=0 + + builder.add(Arguments.of(5000, Integer.MAX_VALUE, PivotingStrategy.MEDIAN_OF_3, 0, PairedKeyStrategy.PAIRED_KEYS, 2, 0)); + builder.add(Arguments.of(5000, 600, PivotingStrategy.MEDIAN_OF_3, 0, PairedKeyStrategy.PAIRED_KEYS, 2, 0)); + builder.add(Arguments.of(5000, 600, PivotingStrategy.MEDIAN_OF_9, 0, PairedKeyStrategy.PAIRED_KEYS, 2, 0)); + builder.add(Arguments.of(5000, 600, PivotingStrategy.MEDIAN_OF_3, Partition.FLAG_RANDOM_SAMPLING, PairedKeyStrategy.PAIRED_KEYS, 2, 0)); + builder.add(Arguments.of(5000, Integer.MAX_VALUE, PivotingStrategy.MEDIAN_OF_9, 0, PairedKeyStrategy.PAIRED_KEYS, 2, 0)); + builder.add(Arguments.of(5000, 600, PivotingStrategy.MEDIAN_OF_9, Partition.FLAG_RANDOM_SAMPLING, PairedKeyStrategy.PAIRED_KEYS, 2, 0)); + + // At the threshold for random sub-sampling (see below where the constant + // has been copied from an early version which used FR random sub-sampling) + // n=25000 : # samples = 462 + + // Threshold to use a random sub-sample for the Floyd-Rivest algorithm. Note: + // Random sampling is a redundant overhead on fully random data and will part + // destroy sorted data. On data that is structured with repeat patterns, the + // shuffle removes side-effects of patterns and stabilises performance where the + // standard Floyd-Rivest algorithm (with a non-random local sample) will recurse + // excessively and trigger a switch to the stopper function. The threshold has + // been chosen at a level where average performance over a variety of data + // distributions shows no performance loss. Individual distributions may be better + // or worse at different thresholds. On random data the impact is minimal; on + // sorted data the impact is approximately 10%. On data with patterns that trigger + // excess recursion this can increase performance by an order of magnitude. Note + // that the stopper will still be used to avoid worst-case quickselect performance + // if this threshold is not appropriate for the input data. + final int randomSubSamplingSize = 25000; + + //n=25000, su=2147483647, MEDIAN_OF_9, flags=0 : stopped=0 + //n=25000, su=1200, MEDIAN_OF_9, flags=0 : stopped=3 + //n=25000, su=1200, MEDIAN_OF_9, flags=2 : stopped=0 + builder.add(Arguments.of(randomSubSamplingSize, Integer.MAX_VALUE, + PivotingStrategy.MEDIAN_OF_9, 0, PairedKeyStrategy.PAIRED_KEYS, 2, 0)); + builder.add(Arguments.of(randomSubSamplingSize, Partition.SELECT_SUB_SAMPLING_SIZE, + PivotingStrategy.MEDIAN_OF_9, 0, PairedKeyStrategy.PAIRED_KEYS, 2, 0)); + builder.add(Arguments.of(randomSubSamplingSize, Partition.SELECT_SUB_SAMPLING_SIZE, + PivotingStrategy.MEDIAN_OF_9, Partition.FLAG_RANDOM_SAMPLING, PairedKeyStrategy.PAIRED_KEYS, 2, 0)); + + // Recursion stopper using halving after c iterations. + // Use c=5 for 98% confidence the length will half. + + //n=5000, su=2147483647, MEDIAN_OF_3, flags=0 : stopped=47 + //n=5000, su=600, MEDIAN_OF_3, flags=0 : stopped=69 + //n=5000, su=600, MEDIAN_OF_9, flags=0 : stopped=25 + //n=5000, su=600, MEDIAN_OF_3, flags=2 : stopped=22 + //n=5000, su=2147483647, MEDIAN_OF_9, flags=0 : stopped=0 + //n=5000, su=600, MEDIAN_OF_9, flags=2 : stopped=0 + + builder.add(Arguments.of(5000, Integer.MAX_VALUE, PivotingStrategy.MEDIAN_OF_3, 0, PairedKeyStrategy.PAIRED_KEYS_2, 5, 1)); + builder.add(Arguments.of(5000, 600, PivotingStrategy.MEDIAN_OF_3, 0, PairedKeyStrategy.PAIRED_KEYS_2, 5, 1)); + builder.add(Arguments.of(5000, 600, PivotingStrategy.MEDIAN_OF_9, 0, PairedKeyStrategy.PAIRED_KEYS_2, 5, 1)); + builder.add(Arguments.of(5000, 600, PivotingStrategy.MEDIAN_OF_3, Partition.FLAG_RANDOM_SAMPLING, PairedKeyStrategy.PAIRED_KEYS_2, 5, 1)); + builder.add(Arguments.of(5000, Integer.MAX_VALUE, PivotingStrategy.MEDIAN_OF_9, 0, PairedKeyStrategy.PAIRED_KEYS_2, 5, 1)); + builder.add(Arguments.of(5000, 600, PivotingStrategy.MEDIAN_OF_9, Partition.FLAG_RANDOM_SAMPLING, PairedKeyStrategy.PAIRED_KEYS_2, 5, 1)); + + // At the threshold for random sub-sampling + + //n=25000, su=2147483647, MEDIAN_OF_9, flags=0 : stopped=0 + //n=25000, su=1200, MEDIAN_OF_9, flags=0 : stopped=42 + //n=25000, su=1200, MEDIAN_OF_9, flags=2 : stopped=0 + + builder.add(Arguments.of(randomSubSamplingSize, Integer.MAX_VALUE, + PivotingStrategy.MEDIAN_OF_9, 0, PairedKeyStrategy.PAIRED_KEYS_2, 5, 1)); + builder.add(Arguments.of(randomSubSamplingSize, Partition.SELECT_SUB_SAMPLING_SIZE, + PivotingStrategy.MEDIAN_OF_9, 0, PairedKeyStrategy.PAIRED_KEYS_2, 5, 1)); + builder.add(Arguments.of(randomSubSamplingSize, Partition.SELECT_SUB_SAMPLING_SIZE, + PivotingStrategy.MEDIAN_OF_9, Partition.FLAG_RANDOM_SAMPLING, PairedKeyStrategy.PAIRED_KEYS_2, 5, 1)); + + // Recursion stopper using partition length sum as twice the length + + //n=5000, su=2147483647, MEDIAN_OF_3, flags=0 : stopped=66 + //n=5000, su=600, MEDIAN_OF_3, flags=0 : stopped=160 + //n=5000, su=600, MEDIAN_OF_9, flags=0 : stopped=41 + //n=5000, su=600, MEDIAN_OF_3, flags=2 : stopped=84 + //n=5000, su=2147483647, MEDIAN_OF_9, flags=0 : stopped=10 + //n=5000, su=600, MEDIAN_OF_9, flags=2 : stopped=7 + + builder.add(Arguments.of(5000, Integer.MAX_VALUE, PivotingStrategy.MEDIAN_OF_3, 0, PairedKeyStrategy.PAIRED_KEYS_LEN, 2, 0)); + builder.add(Arguments.of(5000, 600, PivotingStrategy.MEDIAN_OF_3, 0, PairedKeyStrategy.PAIRED_KEYS_LEN, 2, 0)); + builder.add(Arguments.of(5000, 600, PivotingStrategy.MEDIAN_OF_9, 0, PairedKeyStrategy.PAIRED_KEYS_LEN, 2, 0)); + builder.add(Arguments.of(5000, 600, PivotingStrategy.MEDIAN_OF_3, Partition.FLAG_RANDOM_SAMPLING, PairedKeyStrategy.PAIRED_KEYS_LEN, 2, 0)); + builder.add(Arguments.of(5000, Integer.MAX_VALUE, PivotingStrategy.MEDIAN_OF_9, 0, PairedKeyStrategy.PAIRED_KEYS_LEN, 2, 0)); + builder.add(Arguments.of(5000, 600, PivotingStrategy.MEDIAN_OF_9, Partition.FLAG_RANDOM_SAMPLING, PairedKeyStrategy.PAIRED_KEYS_LEN, 2, 0)); + + // At the threshold for random sub-sampling + + //n=25000, su=2147483647, MEDIAN_OF_9, flags=0 : stopped=11 + //n=25000, su=1200, MEDIAN_OF_9, flags=0 : stopped=70 + //n=25000, su=1200, MEDIAN_OF_9, flags=2 : stopped=14 + + builder.add(Arguments.of(randomSubSamplingSize, Integer.MAX_VALUE, + PivotingStrategy.MEDIAN_OF_9, 0, PairedKeyStrategy.PAIRED_KEYS_LEN, 2, 0)); + builder.add(Arguments.of(randomSubSamplingSize, Partition.SELECT_SUB_SAMPLING_SIZE, + PivotingStrategy.MEDIAN_OF_9, 0, PairedKeyStrategy.PAIRED_KEYS_LEN, 2, 0)); + builder.add(Arguments.of(randomSubSamplingSize, Partition.SELECT_SUB_SAMPLING_SIZE, + PivotingStrategy.MEDIAN_OF_9, Partition.FLAG_RANDOM_SAMPLING, PairedKeyStrategy.PAIRED_KEYS_LEN, 2, 0)); + + return builder.build(); + } + + @Test + @Disabled("Used for testing") + void testConfiguredPartition() { + final AbstractDataSource source = new AbstractDataSource() { + @Override + protected int getLength() { + //return 1 << 16; + return 1 << 20; + } + }; + source.setRange(0); + source.setRngSeed(0xdeadbeef); + + // Uncomment for Valois + source.setDistribution( + Distribution.RANDOM + // Distribution.SORTED, + // Distribution.ONEZERO, + // Distribution.M3KILLER, + // Distribution.ROTATED, + // Distribution.TWOFACED, + // Distribution.ORGANPIPE + ); + source.setModification(Modification.COPY); + source.setSeed(Integer.MAX_VALUE); + source.setup(); + // Target the "median" + //final int[] k = {source.getLength() >> 1}; + // Target the "edge" + final int[] k = {source.getLength() / 20}; + + // Stop based on recursion depth + //final Partition p = new Partition(PivotingStrategy.MEDIAN_OF_3, QS, EC, Integer.MAX_VALUE) + // .setPairedKeyStrategy(PairedKeyStrategy.PAIRED_KEYS) + // .setRecursionMultiple(2); + + // Stop based on steps to half the initial length + final int su = 1200; + final Partition p = new Partition(PivotingStrategy.MEDIAN_OF_5, QS, EC, su) + .setPairedKeyStrategy(PairedKeyStrategy.KEY_RANGE) + .setControlFlags(Partition.FLAG_RANDOM_SAMPLING | + Partition.FLAG_QA_FAR_STEP | Partition.FLAG_QA_SAMPLE_K) + // 92.9% confidence + .setRecursionMultiple(4) + .setRecursionConstant(1); + + for (int i = 0; i < source.size(); i++) { + final int index = i; + p.setRecursionConsumer(v -> TestUtils.printf("%d: %s (%d)%n", index, source.getDataSampleInfo(index), v)); + //p.setSPStrategy(SPStrategy.SP); + //p.partitionISP(source.getDataSample(i), k, 1); + p.partitionQA(source.getDataSample(i), k, 1); + } + } + + @Test + @Disabled("Used for testing the QA implementation against select") + void testQAAndSelect() { + final AbstractDataSource source = new AbstractDataSource() { + @Override + protected int getLength() { + return 15000; + } + }; + + source.setup(); + // Target the "median" + final int[] k = {source.getLength() >> 1}; + + // QA2 algorithm should be configured the same as SELECT + + for (int i = 0; i < source.size(); i++) { + final double[] a = source.getDataSample(i); + Partition.partitionQA2(a, k, 1); + final double[] b = source.getDataSample(i); + Selection.select(b, k); + if (!Arrays.equals(a, b)) { + TestUtils.printf("%d: %s%n", i, source.getDataSampleInfo(i)); + } + } + } +} diff --git a/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/PivotCacheTest.java b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/PivotCacheTest.java new file mode 100644 index 000000000..22034454d --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/PivotCacheTest.java @@ -0,0 +1,388 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.util.Arrays; +import java.util.BitSet; +import java.util.stream.Stream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Test for {@link PivotCache} implementations. + */ +class PivotCacheTest { + + @ParameterizedTest + @MethodSource(value = {"testSingleRange", "testSinglePoint"}) + void testSingleRangeUsingFilledIndexSetAsScanningPivotCache(int left, int right, int[][] indices) { + // Collate all indices + int[] allIndices = Arrays.stream(indices).flatMapToInt(Arrays::stream).toArray(); + // Append left and right + final int n = allIndices.length; + allIndices = Arrays.copyOfRange(allIndices, 0, n + 2); + allIndices[n] = left; + allIndices[n + 1] = right; + final IndexSet set = IndexSet.of(allIndices); + assertSingleRange(left, right, indices, set.asScanningPivotCache(left, right)); + } + + @ParameterizedTest + @MethodSource(value = {"testSingleRange", "testSinglePoint"}) + void testSingleRangeUsingScanningPivotCache(int left, int right, int[][] indices) { + assertSingleRange(left, right, indices, IndexSet.createScanningPivotCache(left, right)); + } + + @ParameterizedTest + @MethodSource(value = {"testSingleRange", "testSinglePoint"}) + void testSingleRangeUsingIndexSet(int left, int right, int[][] indices) { + assertSingleRange(left, right, indices, IndexSet.ofRange(left, right)); + } + + @ParameterizedTest + @MethodSource + void testSinglePoint(int left, int right, int[][] indices) { + Assumptions.assumeTrue(left == right); + assertSingleRange(left, right, indices, PivotCaches.ofIndex(left)); + } + + @ParameterizedTest + @MethodSource + void testSingleRange(int left, int right, int[][] indices) { + assertSingleRange(left, right, indices, PivotCaches.ofRange(left, right)); + } + + @ParameterizedTest + @MethodSource(value = {"testSingleRange", "testSinglePoint"}) + void testPairedIndex(int left, int right, int[][] indices) { + if (left == right) { + assertSingleRange(left, right, indices, PivotCaches.ofPairedIndex(left)); + } else if (left + 1 == right) { + assertSingleRange(left, right, indices, + PivotCaches.ofPairedIndex(left | Integer.MIN_VALUE)); + } else { + Assumptions.abort("Not a paired index"); + } + } + + /** + * Assert caching pivots around a single range. + * + *

This test progressively adds more indices to the cache. It then verifies the + * range is correctly bracketed and any internal pivots can be traversed as sorted + * (pivot) and unsorted (non-pivot) regions. + * + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param indices Batches of indices to add. + * @param cache Pivot cache. + */ + private static void assertSingleRange(int left, int right, int[][] indices, PivotCache cache) { + final BitSet ref = new BitSet(); + Assertions.assertEquals(left, cache.left()); + Assertions.assertEquals(right, cache.right()); + Assertions.assertEquals(ref.previousSetBit(left), cache.previousPivot(left)); + Assertions.assertEquals(ref.nextSetBit(left), cache.nextPivot(left)); + Assertions.assertEquals(Integer.MAX_VALUE, cache.nextPivotOrElse(right, Integer.MAX_VALUE)); + // This assumes the cache supports internal scanning + // With current implementations this is true. + for (final int[] batch : indices) { + for (final int i : batch) { + ref.set(i); + cache.add(i); + if (i >= left && i <= right && !cache.sparse()) { + Assertions.assertTrue(cache.contains(i)); + } + } + Assertions.assertEquals(left, cache.left()); + Assertions.assertEquals(right, cache.right()); + // Flanking pivots. Note: If actually used for partitioning these + // must be updated from the possible -1 result. + final int lower = ref.previousSetBit(left); + final int upper = ref.nextSetBit(right); + Assertions.assertEquals(lower, cache.previousPivot(left)); + Assertions.assertEquals(upper, cache.nextPivot(right)); + if (upper < 0) { + Assertions.assertEquals(Integer.MAX_VALUE, cache.nextPivotOrElse(right, Integer.MAX_VALUE)); + } + + // The partition algorithm must run so [left, right] is sorted + // lower---left--------------------------right----upper + + if (cache.sparse()) { + continue; + } + + // Not sparse: can check for indices + Assertions.assertEquals(ref.get(left), cache.contains(left)); + Assertions.assertEquals(ref.get(right), cache.contains(right)); + + if (!(cache instanceof ScanningPivotCache)) { + continue; + } + final ScanningPivotCache scanningCache = (ScanningPivotCache) cache; + + // Test internal scanning from the ends for additional pivots + // lower---left------p----------p2-------right----upper + // s--------e + + int p = ref.nextSetBit(left); + Assertions.assertEquals(p, cache.nextPivot(left)); + + if (p == upper) { + // No internal pivots: just run partitioning + continue; + } + + int p2 = ref.previousSetBit(right); + Assertions.assertEquals(p2, cache.previousPivot(right)); + + // Must partition: (lower, p) and (p2, upper) + // Then fully sort all unsorted parts in (p, p2) + + // left to right + // Check the traversal with a copy + BitSet copy = (BitSet) ref.clone(); + // partition [left, p) using bracket (lower, p) + copy.set(left, p); + + int s = ref.nextClearBit(p); + int e = ref.nextSetBit(s); + // e can be -1 so check it is within left + while (left < e && e < upper) { + // sort [s, e) + copy.set(s, e); + Assertions.assertEquals(s, scanningCache.nextNonPivot(p)); + Assertions.assertEquals(e, cache.nextPivot(s)); + p = s; + s = ref.nextClearBit(p); + e = ref.nextSetBit(s); + } + // partition (p, right] using bracket (p, upper) + copy.set(p + 1, right + 1); + final int unsorted = copy.nextClearBit(left); + Assertions.assertTrue(right < unsorted, () -> "Bad left-to-right traversal: " + unsorted); + + // right to left + // Check the traversal with a copy + copy = (BitSet) ref.clone(); + // partition (p2, right] using bracket (p2, upper) + copy.set(p2 + 1, right + 1); + + e = ref.previousClearBit(p2); + s = ref.previousSetBit(e); + while (lower < s) { + // sort (s, e] + copy.set(s + 1, e + 1); + Assertions.assertEquals(e, scanningCache.previousNonPivot(p2)); + Assertions.assertEquals(s, cache.previousPivot(e)); + p2 = s; + e = ref.previousClearBit(p2); + s = ref.previousSetBit(e); + } + + // partition [left, p2) using bracket (lower, p2) + copy.set(left, p2); + final int unsorted2 = copy.previousClearBit(right); + Assertions.assertTrue(unsorted2 < left, () -> "Bad right-to-left traversal: " + unsorted2); + } + } + + static Stream testSinglePoint() { + final Stream.Builder builder = Stream.builder(); + // Ranges with no internal pivots + builder.accept(Arguments.of(5, 5, new int[][] {{5}})); + builder.accept(Arguments.of(5, 5, new int[][] {{2, 44}, {4, 6}, {5}})); + + return builder.build(); + } + + static Stream testSingleRange() { + final Stream.Builder builder = Stream.builder(); + // Ranges with no internal pivots + builder.accept(Arguments.of(5, 5, new int[][] {{5}})); + builder.accept(Arguments.of(5, 5, new int[][] {{2, 44}, {4, 6}, {5}})); + + builder.accept(Arguments.of(5, 6, new int[][] {{5, 6}})); + builder.accept(Arguments.of(5, 6, new int[][] {{2, 44}, {4, 6}, {5}})); + builder.accept(Arguments.of(5, 6, new int[][] {{2, 44}, {5, 7}, {6}})); + + builder.accept(Arguments.of(5, 80, new int[][] {{5, 80}})); + builder.accept(Arguments.of(5, 80, new int[][] {{2, 100}, {3, 90}, {4, 80}})); + builder.accept(Arguments.of(5, 80, new int[][] {{2, 100}, {5, 90}})); + + // 1 internal pivot + builder.accept(Arguments.of(5, 80, new int[][] {{63}})); + builder.accept(Arguments.of(5, 80, new int[][] {{63, 64, 65, 66}})); + builder.accept(Arguments.of(5, 80, new int[][] {{2, 63, 101}})); + + // multiple internal pivots + builder.accept(Arguments.of(5, 80, new int[][] {{31, 63}})); + builder.accept(Arguments.of(5, 80, new int[][] {{31, 44, 63}})); + builder.accept(Arguments.of(5, 80, new int[][] {{31, 32, 33, 44, 63}})); + + return builder.build(); + } + + @ParameterizedTest + @MethodSource(value = {"testSetRange", "testSetRangeSinglePoint"}) + void testSetRangeUsingFilledIndexSetAsScanningPivotCache(int left, int right, int[][] indices) { + // Collate all indices within [left, right] + int[] allIndices = Arrays.stream(indices).flatMapToInt(Arrays::stream) + .filter(i -> i >= left && i <= right).toArray(); + // Append left and right to ensure the range is supported + final int n = allIndices.length; + allIndices = Arrays.copyOfRange(allIndices, 0, n + 2); + allIndices[n] = left; + allIndices[n + 1] = right; + final IndexSet set = IndexSet.of(allIndices); + assertSetRange(left, right, indices, set.asScanningPivotCache(left, right)); + } + + @ParameterizedTest + @MethodSource(value = {"testSetRange", "testSetRangeSinglePoint"}) + void testSetRangeUsingScanningPivotCache(int left, int right, int[][] indices) { + assertSetRange(left, right, indices, IndexSet.createScanningPivotCache(left, right)); + } + + @ParameterizedTest + @MethodSource(value = {"testSetRange", "testSetRangeSinglePoint"}) + void testSetRangeUsingIndexSet(int left, int right, int[][] indices) { + assertSetRange(left, right, indices, IndexSet.ofRange(left, right)); + } + + @ParameterizedTest + @MethodSource + void testSetRangeSinglePoint(int left, int right, int[][] indices) { + Assumptions.assumeTrue(left == right); + assertSetRange(left, right, indices, PivotCaches.ofIndex(left)); + } + + @ParameterizedTest + @MethodSource(value = {"testSetRange"}) + void testSetRangeUsingRange(int left, int right, int[][] indices) { + Assumptions.assumeTrue(left < right); + assertSetRange(left, right, indices, PivotCaches.ofRange(left, right)); + } + + @ParameterizedTest + @MethodSource(value = {"testSingleRange", "testSinglePoint"}) + void testSetRangeUsingPairedIndex(int left, int right, int[][] indices) { + if (left == right) { + assertSingleRange(left, right, indices, PivotCaches.ofPairedIndex(left)); + } else if (left + 1 == right) { + assertSingleRange(left, right, indices, + PivotCaches.ofPairedIndex(left | Integer.MIN_VALUE)); + } else { + Assumptions.abort("Not a paired index"); + } + } + + /** + * Assert caching pivots around a single range. + * + *

This test progressively adds more indices to the cache. It then verifies the + * range is correctly bracketed and any internal pivots can be traversed as sorted + * (pivot) and unsorted (non-pivot) regions. + * + * @param left Lower bound (inclusive). + * @param right Upper bound (inclusive). + * @param indices Batches of indices to add. + * @param cache Pivot cache. + */ + private static void assertSetRange(int left, int right, int[][] indices, PivotCache cache) { + final BitSet ref = new BitSet(); + Assertions.assertEquals(left, cache.left()); + Assertions.assertEquals(right, cache.right()); + Assertions.assertEquals(ref.previousSetBit(left), cache.previousPivot(left)); + Assertions.assertEquals(ref.nextSetBit(left), cache.nextPivot(left)); + Assertions.assertEquals(Integer.MAX_VALUE, cache.nextPivotOrElse(right, Integer.MAX_VALUE)); + // This assumes the cache supports internal scanning + // With current implementations this is true. + for (final int[] batch : indices) { + // BitSet uses an exclusive end + ref.set(batch[0], batch[1] + 1); + cache.add(batch[0], batch[1]); + Assertions.assertEquals(left, cache.left()); + Assertions.assertEquals(right, cache.right()); + // Flanking pivots. Note: If actually used for partitioning these + // must be updated from the possible -1 result. + final int lower = ref.previousSetBit(left); + final int upper = ref.nextSetBit(right); + Assertions.assertEquals(lower, cache.previousPivot(left), "left flanking pivot"); + Assertions.assertEquals(upper, cache.nextPivot(right), "right flanking pivot"); + if (upper < 0) { + Assertions.assertEquals(Integer.MAX_VALUE, cache.nextPivotOrElse(right, Integer.MAX_VALUE)); + } + if (cache.sparse()) { + continue; + } + // Simple test within the range + for (int i = left; i <= right; i++) { + Assertions.assertEquals(ref.get(i), cache.contains(i)); + } + } + } + + static Stream testSetRangeSinglePoint() { + final Stream.Builder builder = Stream.builder(); + + // Highest value for an index. Use sparingly as the test will create a BitSet + // large enough to hold this value. + final int max = Integer.MAX_VALUE - 1; + + builder.accept(Arguments.of(5, 5, new int[][] {{5, 5}})); + builder.accept(Arguments.of(5, 5, new int[][] {{2, 44}, {4, 6}, {5, 5}})); + builder.accept(Arguments.of(5, 5, new int[][] {{2, 44}, {5, 6}})); + builder.accept(Arguments.of(5, 5, new int[][] {{2, 44}, {4, 5}})); + builder.accept(Arguments.of(5, 5, new int[][] {{2, 44}, {6, 7}, {5, 6}})); + builder.accept(Arguments.of(5, 5, new int[][] {{2, 44}, {3, 4}, {4, 5}})); + builder.accept(Arguments.of(5, 5, new int[][] {{1, 2}, {7, 7}})); + builder.accept(Arguments.of(5, 5, new int[][] {{0, 0}, {max, max}})); + + return builder.build(); + } + + static Stream testSetRange() { + final Stream.Builder builder = Stream.builder(); + + // Highest value for an index. Use sparingly as the test will create a BitSet + // large enough to hold this value. + final int max = Integer.MAX_VALUE - 1; + + builder.accept(Arguments.of(5, 6, new int[][] {{5, 6}})); + builder.accept(Arguments.of(5, 6, new int[][] {{2, 3}, {44, 49}, {7, 8}, {5, 5}})); + builder.accept(Arguments.of(5, 6, new int[][] {{0, 1}, {max - 1, max}})); + + // Bits span 2 longs + builder.accept(Arguments.of(5, 80, new int[][] {{42, 42}, {1, 1}, {2, 2}, {90, 95}, {67, 68}})); + builder.accept(Arguments.of(5, 80, new int[][] {{0, 1}, {max - 1, max}})); + + // Bits span 3 longs + builder.accept(Arguments.of(5, 140, new int[][] {{42, 42}, {1, 1}, {2, 2}, {90, 95}, {187, 190}, {67, 68}})); + builder.accept(Arguments.of(5, 140, new int[][] {{0, 0}, {max, max}})); + + return builder.build(); + } + + // TODO: + // Test moveLeft() +} diff --git a/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/PivotingStrategyTest.java b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/PivotingStrategyTest.java new file mode 100644 index 000000000..03dbcf3fc --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/PivotingStrategyTest.java @@ -0,0 +1,207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.util.Arrays; +import java.util.stream.Stream; +import org.apache.commons.rng.UniformRandomProvider; +import org.apache.commons.rng.simple.RandomSource; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Test for {@link PivotingStrategy}. + */ +class PivotingStrategyTest { + @ParameterizedTest + @MethodSource(value = {"testPivot"}) + void testCentral(double[] a) { + assertPivot(a, 0, PivotingStrategy.CENTRAL); + } + + @ParameterizedTest + @MethodSource(value = {"testPivot"}) + void testMedianOf3(double[] a) { + assertPivot(a, 0, PivotingStrategy.MEDIAN_OF_3); + } + + @ParameterizedTest + @MethodSource(value = {"testPivot"}) + void testMedianOf9(double[] a) { + // Sometimes this is off by an index of 1 + assertPivot(a, 0, PivotingStrategy.MEDIAN_OF_9, -1, 1); + } + + @ParameterizedTest + @MethodSource(value = {"testPivot", "testMedianOf5"}) + void testMedianOf5(double[] a) { + assertPivot(a, 0, PivotingStrategy.MEDIAN_OF_5); + } + + @ParameterizedTest + @MethodSource(value = {"testPivot", "testMedianOf5"}) + void testMedianOf5B(double[] a) { + assertPivot(a, 0, PivotingStrategy.MEDIAN_OF_5B); + } + + @ParameterizedTest + @MethodSource(value = {"testPivot"}) + void testDynamic(double[] a) { + final int index = PivotingStrategy.DYNAMIC.pivotIndex(a, 0, a.length - 1, 0); + PivotingStrategy s; + if (PivotingStrategy.DYNAMIC.getSampledIndices(0, a.length - 1, 0).length == 3) { + s = PivotingStrategy.MEDIAN_OF_3; + } else { + s = PivotingStrategy.MEDIAN_OF_9; + } + Assertions.assertEquals(s.pivotIndex(a, 0, a.length - 1, 0), index); + } + + @ParameterizedTest + @MethodSource(value = {"testPivot"}) + void testTarget(double[] a) { + assertPivot(a, 0, PivotingStrategy.TARGET); + assertPivot(a, 13, PivotingStrategy.TARGET); + } + + private static void assertPivot(double[] a, int target, PivotingStrategy s, int... offset) { + final double[] copy = a.clone(); + final int[] k = s.getSampledIndices(0, a.length - 1, target); + // Extract data + final double[] x = new double[k.length]; + for (int i = 0; i < k.length; i++) { + x[i] = a[k[i]]; + } + final int p1 = s.pivotIndex(a, 0, a.length - 1, target); + // Extract data after + final double[] y = new double[k.length]; + for (int i = 0; i < k.length; i++) { + y[i] = a[k[i]]; + } + // Test the effect on the data + final int effect = s.samplingEffect(); + if (effect == PivotingStrategy.SORT) { + Arrays.sort(x); + Assertions.assertArrayEquals(x, y, "Data at indices not sorted"); + } else if (effect == PivotingStrategy.UNCHANGED) { + Assertions.assertArrayEquals(x, y, "Data at indices changed"); + // Sort the data to obtain the expected pivot + Arrays.sort(x); + } else if (effect == PivotingStrategy.PARTIAL_SORT) { + Arrays.sort(x); + Arrays.sort(y); + Assertions.assertArrayEquals(x, y, "Data destroyed"); + } + // Pivot should be the centre of the sorted sample + final int m = k.length >>> 1; + // Allowed to be offset + if (offset.length != 0) { + boolean ok = x[m] == a[p1]; + for (final int o : offset) { + if (ok) { + break; + } + ok = x[m + o] == a[p1]; + } + Assertions.assertTrue(ok, () -> "Unexpected pivot: " + p1); + } else { + Assertions.assertEquals(x[m], a[p1], () -> "Unexpected pivot: " + p1); + } + // Flip data, pivot value should be the same + for (int i = 0, j = k.length - 1; i < j; i++, j--) { + final double v = copy[k[i]]; + copy[k[i]] = copy[k[j]]; + copy[k[j]] = v; + } + final int p1a = s.pivotIndex(copy, 0, a.length - 1, target); + Assertions.assertEquals(a[p1], copy[p1a], "Pivot changed"); + } + + @Test + void testMedianOf5Indexing() { + assertIndexing(PivotingStrategy.MEDIAN_OF_5, 5, 0); + } + + @Test + void testMedianOf5BIndexing() { + assertIndexing(PivotingStrategy.MEDIAN_OF_5B, 5, 0); + } + + private static void assertIndexing(PivotingStrategy s, int safeLength, int target) { + final double[] a = new double[safeLength - 1]; + Assertions.assertThrows(ArrayIndexOutOfBoundsException.class, + () -> s.pivotIndex(a, 0, a.length - 1, target), + () -> "Length: " + (safeLength - 1)); + for (int i = safeLength; i < 50; i++) { + final int n = i; + final double[] b = new double[i]; + Assertions.assertDoesNotThrow(() -> s.pivotIndex(b, 0, b.length - 1, target), () -> "Length: " + n); + } + } + + static Stream testPivot() { + final Stream.Builder builder = Stream.builder(); + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(123); + // Big enough to use median of 9 + final double[] a = rng.doubles(50).toArray(); + for (int i = 0; i < 10; i++) { + TestUtils.shuffle(rng, a); + builder.add(a.clone()); + } + return builder.build(); + } + + static Stream testMedianOf5() { + final Stream.Builder builder = Stream.builder(); + final double[] a = new double[5]; + // Permutations is 5! = 120 + final int shift = 42; + for (int i = 0; i < 5; i++) { + a[0] = i + shift; + for (int j = 0; j < 5; j++) { + if (j == i) { + continue; + } + a[1] = j + shift; + for (int k = 0; k < 5; k++) { + if (k == j || k == i) { + continue; + } + a[2] = k + shift; + for (int l = 0; l < 5; l++) { + if (l == k || l == j || l == i) { + continue; + } + a[3] = l + shift; + for (int m = 0; m < 5; m++) { + if (m == l || m == k || m == j || m == i) { + continue; + } + a[3] = m + shift; + builder.add(Arguments.of(a.clone(), 2)); + } + } + } + } + } + return builder.build(); + } +} diff --git a/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/QuantilePerformanceTest.java b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/QuantilePerformanceTest.java new file mode 100644 index 000000000..3debf4167 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/QuantilePerformanceTest.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.util.EnumSet; +import org.apache.commons.numbers.examples.jmh.arrays.SelectionPerformance.AbstractDataSource; +import org.apache.commons.numbers.examples.jmh.arrays.SelectionPerformance.AbstractDataSource.Distribution; +import org.apache.commons.numbers.examples.jmh.arrays.SelectionPerformance.AbstractDataSource.Modification; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Executes tests for {@link SelectionPerformance}. + */ +class SelectionPerformanceTest { + @Test + void testGetDistribution() { + assertGetEnumFromParam(Distribution.class); + } + + @Test + void testGetModification() { + assertGetEnumFromParam(Modification.class); + } + + static > void assertGetEnumFromParam(Class cls) { + Assertions.assertEquals(EnumSet.allOf(cls), + AbstractDataSource.getEnumFromParam(cls, "all")); + Assertions.assertThrows(IllegalStateException.class, + () -> AbstractDataSource.getEnumFromParam(cls, "nothing")); + for (final E e1 : cls.getEnumConstants()) { + final String s = e1.name().toLowerCase(); + Assertions.assertEquals(EnumSet.of(e1), + AbstractDataSource.getEnumFromParam(cls, e1.name())); + Assertions.assertEquals(EnumSet.of(e1), + AbstractDataSource.getEnumFromParam(cls, s)); + for (final E e2 : cls.getEnumConstants()) { + Assertions.assertEquals(EnumSet.of(e1, e2), + AbstractDataSource.getEnumFromParam(cls, s + ":" + e2.name())); + Assertions.assertEquals(EnumSet.of(e1, e2), + AbstractDataSource.getEnumFromParam(cls, e2.name() + ":" + e1.name())); + } + } + } +} diff --git a/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/SearchableIntervalTest.java b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/SearchableIntervalTest.java new file mode 100644 index 000000000..497bc3eb5 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/SearchableIntervalTest.java @@ -0,0 +1,270 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.util.BitSet; +import java.util.function.BiFunction; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.apache.commons.rng.UniformRandomProvider; +import org.apache.commons.rng.simple.RandomSource; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Test for {@link SearchableInterval} implementations. + */ +class SearchableIntervalTest { + @Test + void testAnyIndex() { + final SearchableInterval interval = IndexIntervals.anyIndex(); + // Full range of valid indices. + // Note Integer.MAX_VALUE is not a valid array index. + Assertions.assertEquals(0, interval.left()); + Assertions.assertEquals(Integer.MAX_VALUE - 1, interval.right()); + final int[] index = {0}; + for (final int i : new int[] {0, 1, 2, 42, 678268, Integer.MAX_VALUE - 1}) { + Assertions.assertEquals(i, interval.previousIndex(i)); + Assertions.assertEquals(i, interval.nextIndex(i)); + Assertions.assertEquals(i - 1, interval.split(i, i, index)); + Assertions.assertEquals(i + 1, index[0]); + } + } + + @Test + void testAnyIndex2() { + final SearchableInterval2 interval = IndexIntervals.anyIndex2(); + // Full range of valid indices. + // Note Integer.MAX_VALUE is not a valid array index. + Assertions.assertEquals(0, interval.start()); + Assertions.assertEquals(Integer.MAX_VALUE - 1, interval.end()); + final int[] index = {0}; + for (final int i : new int[] {0, 1, 2, 42, 678268, Integer.MAX_VALUE - 1}) { + Assertions.assertEquals(i, interval.previous(i, i)); + Assertions.assertEquals(i, interval.next(i, i)); + Assertions.assertEquals(i - 1, interval.split(-1, Integer.MAX_VALUE, i, i, index)); + Assertions.assertEquals(i + 1, index[0]); + } + } + + @Test + void testScanningKeySearchableIntervalInvalidIndicesThrows() { + assertInvalidIndicesThrows(ScanningKeyInterval::of); + // Invalid indices: not in [0, Integer.MAX_VALUE) + Assertions.assertThrows(IllegalArgumentException.class, + () -> ScanningKeyInterval.of(new int[] {-1, 2, 3}, 3)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> ScanningKeyInterval.of(new int[] {1, 2, Integer.MAX_VALUE}, 3)); + } + + @Test + void testBinarySearchKeySearchableIntervalInvalidIndicesThrows() { + assertInvalidIndicesThrows(BinarySearchKeyInterval::of); + } + + private static void assertInvalidIndicesThrows(BiFunction constructor) { + // Size zero + Assertions.assertThrows(IllegalArgumentException.class, () -> constructor.apply(new int[0], 0)); + Assertions.assertThrows(IllegalArgumentException.class, () -> constructor.apply(new int[10], 0)); + // Not sorted + Assertions.assertThrows(IllegalArgumentException.class, + () -> constructor.apply(new int[] {3, 2, 1}, 3)); + // Not unique + Assertions.assertThrows(IllegalArgumentException.class, + () -> constructor.apply(new int[] {1, 2, 2, 3}, 4)); + } + + @ParameterizedTest + @MethodSource(value = {"testPreviousNextIndex"}) + void testPreviousNextScanningKeySearchableInterval(int[] indices) { + assertPreviousNextIndex(ScanningKeyInterval.of(indices, indices.length), indices); + } + + @ParameterizedTest + @MethodSource(value = {"testPreviousNextIndex"}) + void testPreviousNextBinarySearchKeySearchableInterval(int[] indices) { + assertPreviousNextIndex(BinarySearchKeyInterval.of(indices, indices.length), indices); + } + + @ParameterizedTest + @MethodSource(value = {"testPreviousNextIndex"}) + void testPreviousNextIndexSet(int[] indices) { + // Skip this due to excess memory consumption + Assumptions.assumeTrue(indices[indices.length - 1] < Integer.MAX_VALUE - 1); + assertPreviousNextIndex(IndexSet.of(indices, indices.length), indices); + } + + @ParameterizedTest + @MethodSource(value = {"testPreviousNextIndex"}) + void testPreviousNextSearchableInterval(int[] indices) { + assertPreviousNextIndex(IndexIntervals.createSearchableInterval(indices, indices.length), indices); + } + + @ParameterizedTest + @MethodSource(value = {"testPreviousNextIndex"}) + void testPreviousNextCompressedIndexSet(int[] indices) { + // Skip this due to excess memory consumption + Assumptions.assumeTrue(indices[indices.length - 1] < Integer.MAX_VALUE - 1); + final int[] index = {0}; + // The test is adjusted as the compressed index set does not store all indices. + // So we scan previous and next instead and check we do not miss the index. + for (final int c : new int[] {1, 2, 3}) { + final SearchableInterval interval = CompressedIndexSet.of(c, indices, indices.length); + final int nm1 = indices.length - 1; + Assertions.assertEquals(indices[0], interval.left()); + Assertions.assertEquals(indices[nm1], interval.right()); + // Number of steps between indices should be less twice the + // compression level minus 1. Max steps required @ compression 2: + // -------i---------n----- + // -------cccc---cccc---- Compressed indices cover 4 real indices + final int maxSteps = (1 << (c + 1)) - 1; + for (int i = 0; i < indices.length; i++) { + if (i > 0) { + final int prev = indices[i - 1]; + int steps = 1; + int j = interval.previousIndex(indices[i] - 1); + // Splitting is tested against previous and next + if (i < nm1) { + Assertions.assertEquals(j, interval.split(indices[i], indices[i], index)); + Assertions.assertEquals(interval.nextIndex(indices[i] + 1), index[0]); + if (i > 1) { + final int upper = index[0]; + Assertions.assertEquals(interval.previousIndex(indices[i - 1] - 1), + interval.split(indices[i - 1], indices[i], index)); + Assertions.assertEquals(upper, index[0]); + } + } + // Scan previous + while (j > prev) { + steps++; + j = interval.previousIndex(j - 1); + } + Assertions.assertEquals(prev, j); + Assertions.assertTrue(steps <= maxSteps); + } + Assertions.assertEquals(indices[i], interval.previousIndex(indices[i])); + Assertions.assertEquals(indices[i], interval.nextIndex(indices[i])); + if (i < nm1) { + final int next = indices[i + 1]; + int steps = 1; + int j = interval.nextIndex(indices[i] + 1); + while (j < next) { + steps++; + j = interval.nextIndex(j + 1); + } + Assertions.assertEquals(next, j); + Assertions.assertTrue(steps <= maxSteps); + } + } + } + } + + private static void assertPreviousNextIndex(SearchableInterval interval, int[] indices) { + final int nm1 = indices.length - 1; + Assertions.assertEquals(indices[0], interval.left()); + Assertions.assertEquals(indices[nm1], interval.right()); + final int[] index = {0}; + // Note: For performance scanning is not supported outside the range + for (int i = 0; i < indices.length; i++) { + if (i > 0) { + Assertions.assertEquals(indices[i - 1], interval.previousIndex(indices[i] - 1)); + // Split on an index: Cannot call when k == left or k == right + if (i < nm1) { + Assertions.assertEquals(indices[i - 1], interval.split(indices[i], indices[i], index)); + Assertions.assertEquals(indices[i + 1], index[0]); + if (indices[i - 1] < indices[i] - 1 && indices[i] < indices[i + 1] - 1) { + // Split between indices + final int middle1 = (indices[i - 1] + indices[i]) >>> 1; + final int middle2 = (indices[i] + indices[i + 1]) >>> 1; + Assertions.assertEquals(indices[i - 1], interval.split(middle1, middle2, index)); + Assertions.assertEquals(indices[i + 1], index[0]); + } + } + } + Assertions.assertEquals(indices[i], interval.previousIndex(indices[i])); + Assertions.assertEquals(indices[i], interval.nextIndex(indices[i])); + if (i < nm1) { + Assertions.assertEquals(indices[i + 1], interval.nextIndex(indices[i] + 1)); + } + } + } + + static Stream testPreviousNextIndex() { + final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create(); + final Stream.Builder builder = Stream.builder(); + builder.accept(new int[] {4}); + builder.accept(new int[] {4, 78}); + builder.accept(new int[] {4, 78, 999}); + builder.accept(new int[] {4, 78, 79, 999}); + builder.accept(new int[] {4, 5, 6, 7, 8}); + for (final int size : new int[] {25, 100, 400}) { + final BitSet set = new BitSet(size); + for (final int n : new int[] {2, size / 8, size / 4, size / 2}) { + set.clear(); + rng.ints(n, 0, size).forEach(set::set); + final int[] a = set.stream().toArray(); + builder.accept(a.clone()); + // Force use of index 0 and max index + a[0] = 0; + a[a.length - 1] = Integer.MAX_VALUE - 1; + builder.accept(a); + } + } + return builder.build(); + } + + @Test + void testSearchableIntervalCreate() { + // The above tests verify the SearchableInterval implementations all work. + // Hit all paths in the key analysis performed to create an interval. + + // Small number of keys; no analysis + Assertions.assertEquals(ScanningKeyInterval.class, + IndexIntervals.createSearchableInterval(new int[] {1}, 1).getClass()); + + // >10 keys for key analysis + + // Small number of keys saturating the range + Assertions.assertEquals(IndexSet.class, + IndexIntervals.createSearchableInterval(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, 11).getClass()); + // Keys over a huge range + Assertions.assertEquals(ScanningKeyInterval.class, + IndexIntervals.createSearchableInterval(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, Integer.MAX_VALUE - 1}, 11).getClass()); + + // Small number of keys over a moderate range + int[] k = IntStream.range(0, 30).map(i -> i * 64) .toArray(); + Assertions.assertEquals(IndexSet.class, + IndexIntervals.createSearchableInterval(k.clone(), k.length).getClass()); + // Same keys over a huge range + k[k.length - 1] = Integer.MAX_VALUE - 1; + Assertions.assertEquals(ScanningKeyInterval.class, + IndexIntervals.createSearchableInterval(k, k.length).getClass()); + + // Moderate number of keys over a moderate range + k = IntStream.range(0, 3000).map(i -> i * 64) .toArray(); + Assertions.assertEquals(IndexSet.class, + IndexIntervals.createSearchableInterval(k.clone(), k.length).getClass()); + // Same keys over a huge range - switch to binary search on the keys + k[k.length - 1] = Integer.MAX_VALUE - 1; + Assertions.assertEquals(BinarySearchKeyInterval.class, + IndexIntervals.createSearchableInterval(k, k.length).getClass()); + } +} diff --git a/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/SortingTest.java b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/SortingTest.java new file mode 100644 index 000000000..973ae2aa6 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/SortingTest.java @@ -0,0 +1,1096 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.util.Arrays; +import java.util.BitSet; +import java.util.function.Consumer; +import java.util.function.ToIntFunction; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.apache.commons.rng.UniformRandomProvider; +import org.apache.commons.rng.sampling.PermutationSampler; +import org.apache.commons.rng.simple.RandomSource; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Test for {@link Sorting}. + */ +class SortingTest { + + /** + * Interface to test sorting unique indices. + */ + interface IndexSort { + /** + * Sort the indices into unique ascending order. + * + * @param a Indices. + * @param n Number of indices. + * @return number of unique indices. + */ + int sort(int[] a, int n); + } + + // double[] + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort", "testDoubleInsertionSortInternal"}) + void testDoubleInsertionSortInternal(double[] values) { + assertDoubleSort(values, x -> Sorting.sort(x, 0, x.length - 1, false)); + if (values.length < 2) { + return; + } + // Check internal sort + // Set pivot at lower end + values[0] = Arrays.stream(values).min().getAsDouble(); + // check internal sort + assertDoubleSort(values, x -> Sorting.sort(x, 1, x.length - 1, true)); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort", "testDoubleInsertionSortInternal"}) + void testDoublePairedInsertionSortInternal(double[] values) { + if (values.length < 2) { + return; + } + // Set pivot at lower end + values[0] = Arrays.stream(values).min().getAsDouble(); + assertDoubleSort(values.clone(), x -> Sorting.sortPairedInternal1(x, 1, x.length - 1)); + assertDoubleSort(values.clone(), x -> Sorting.sortPairedInternal2(x, 1, x.length - 1)); + assertDoubleSort(values.clone(), x -> Sorting.sortPairedInternal3(x, 1, x.length - 1)); + assertDoubleSort(values.clone(), x -> Sorting.sortPairedInternal4(x, 1, x.length - 1)); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort"}) + void testDoubleInsertionSort(double[] values) { + assertDoubleSort(values, x -> Sorting.sort(x, 0, x.length - 1)); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort"}) + void testDoubleInsertionSortB(double[] values) { + assertDoubleSort(values, x -> Sorting.sortb(x, 0, x.length - 1)); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort", "testDoubleSort3"}) + void testDoubleSort3(double[] values) { + final double[] data = Arrays.copyOf(values, 3); + assertDoubleSort(data, x -> Sorting.sort3(x, 0, 1, 2)); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort", "testDoubleSort3"}) + void testDoubleSort3b(double[] values) { + final double[] data = Arrays.copyOf(values, 3); + assertDoubleSort(data, x -> Sorting.sort3b(x, 0, 1, 2)); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort", "testDoubleSort3"}) + void testDoubleSort3c(double[] values) { + final double[] data = Arrays.copyOf(values, 3); + assertDoubleSort(data, x -> Sorting.sort3c(x, 0, 1, 2)); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort", "testDoubleSort4"}) + void testDoubleSort4(double[] values) { + final double[] data = Arrays.copyOf(values, 4); + assertDoubleSort(data, x -> Sorting.sort4(x, 0, 1, 2, 3)); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort", "testDoubleSort5"}) + void testDoubleSort5(double[] values) { + final double[] data = Arrays.copyOf(values, 5); + assertDoubleSort(data, x -> Sorting.sort5(x, 0, 1, 2, 3, 4)); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort", "testDoubleSort5"}) + void testDoubleSort5b(double[] values) { + final double[] data = Arrays.copyOf(values, 5); + assertDoubleSort(data, x -> Sorting.sort5b(x, 0, 1, 2, 3, 4)); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort", "testDoubleSort5"}) + void testDoubleSort5c(double[] values) { + final double[] data = Arrays.copyOf(values, 5); + assertDoubleSort(data, x -> Sorting.sort5c(x, 0, 1, 2, 3, 4)); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort"}) + void testDoubleSort7(double[] values) { + final double[] data = Arrays.copyOf(values, 7); + assertDoubleSort(data, x -> Sorting.sort7(x, 0, 1, 2, 3, 4, 5, 6)); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort"}) + void testDoubleSort8(double[] values) { + final double[] data = Arrays.copyOf(values, 8); + assertDoubleSort(data, x -> Sorting.sort8(x, 0, 1, 2, 3, 4, 5, 6, 7)); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort"}) + void testDoubleSort11(double[] values) { + final double[] data = Arrays.copyOf(values, 11); + assertDoubleSort(data, x -> Sorting.sort11(x, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort3Internal"}) + void testDoubleSort3Internal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + assertDoubleSortInternal(values, x -> Sorting.sort3(x, a, b, c), indices); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort3Internal"}) + void testDoubleSort3bInternal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + assertDoubleSortInternal(values, x -> Sorting.sort3b(x, a, b, c), indices); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort3Internal"}) + void testDoubleSort3cInternal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + assertDoubleSortInternal(values, x -> Sorting.sort3c(x, a, b, c), indices); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort4Internal"}) + void testDoubleSort4Internal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + final int d = indices[3]; + assertDoubleSortInternal(values, x -> Sorting.sort4(x, a, b, c, d), indices); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort5Internal"}) + void testDoubleSort5Internal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + final int d = indices[3]; + final int e = indices[4]; + assertDoubleSortInternal(values, x -> Sorting.sort5(x, a, b, c, d, e), indices); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort5Internal"}) + void testDoubleSort5bInternal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + final int d = indices[3]; + final int e = indices[4]; + assertDoubleSortInternal(values, x -> Sorting.sort5b(x, a, b, c, d, e), indices); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort5Internal"}) + void testDoubleSort5cInternal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + final int d = indices[3]; + final int e = indices[4]; + assertDoubleSortInternal(values, x -> Sorting.sort5c(x, a, b, c, d, e), indices); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort7Internal"}) + void testDoubleSort7Internal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + final int d = indices[3]; + final int e = indices[4]; + final int f = indices[5]; + final int g = indices[6]; + assertDoubleSortInternal(values, x -> Sorting.sort7(x, a, b, c, d, e, f, g), indices); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort8Internal"}) + void testDoubleSort8Internal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + final int d = indices[3]; + final int e = indices[4]; + final int f = indices[5]; + final int g = indices[6]; + final int h = indices[7]; + assertDoubleSortInternal(values, x -> Sorting.sort8(x, a, b, c, d, e, f, g, h), indices); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort11Internal"}) + void testDoubleSort11Internal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + final int d = indices[3]; + final int e = indices[4]; + final int f = indices[5]; + final int g = indices[6]; + final int h = indices[7]; + final int i = indices[8]; + final int j = indices[9]; + final int k = indices[10]; + assertDoubleSortInternal(values, x -> Sorting.sort11(x, a, b, c, d, e, f, g, h, i, j, k), indices); + } + + /** + * Assert that the sort {@code function} computes the same result as + * {@link Arrays#sort(double[])}. Ignores signed zeros. + * + * @param values Data. + * @param function Sort function. + */ + private static void assertDoubleSort(double[] values, Consumer function) { + final double[] expected = values.clone(); + Arrays.sort(expected); + final double[] actual = values.clone(); + function.accept(actual); + assertDoubleSort(expected, actual); + } + + /** + * Assert that the {@code expected} and {@code actual} sort are the same. Ignores + * signed zeros. + * + * @param expected Expected sort. + * @param actual Actual sort. + */ + private static void assertDoubleSort(double[] expected, double[] actual) { + // Detect signed zeros + int c = 0; + for (int i = 0; i < expected.length; i++) { + if (Double.compare(-0.0, expected[i]) == 0) { + c++; + } + } + // Check + if (c != 0) { + // Replace signed zeros + final double[] e = replaceSignedZeros(expected.clone()); + final double[] a = replaceSignedZeros(actual.clone()); + Assertions.assertArrayEquals(e, a, "Sort with +0.0"); + // Sort the signed zeros correctly + Arrays.sort(actual); + // Check the same number of signed zeros are present + Assertions.assertArrayEquals(expected, actual, "Signed zeros destroyed"); + } else { + Assertions.assertArrayEquals(expected, actual, "Invalid sort"); + } + } + + /** + * Assert that the sort {@code function} computes the same result as + * {@link Arrays#sort(double[])} run on the provided {@code indices}. Ignores signed + * zeros. + * + * @param values Data. + * @param function Sort function. + * @param indices Indices. + */ + private static void assertDoubleSortInternal(double[] values, Consumer function, int... indices) { + Assertions.assertFalse(containsDuplicates(indices), () -> "Duplicate indices: " + Arrays.toString(indices)); + // Pick out the data to sort + final double[] expected = extractIndices(values, indices); + Arrays.sort(expected); + final double[] data = values.clone(); + function.accept(data); + // Pick out the data that was sorted + final double[] actual = extractIndices(data, indices); + assertDoubleSort(expected, actual); + // Check outside the sorted indices + OUTSIDE: for (int i = 0; i < values.length; i++) { + for (final int ignore : indices) { + if (i == ignore) { + continue OUTSIDE; + } + } + Assertions.assertEquals(values[i], data[i]); + } + } + + static Stream testDoubleSort() { + final Stream.Builder builder = Stream.builder(); + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(); + for (final int size : new int[] {10, 15}) { + double[] a = new double[size]; + Arrays.fill(a, 1.23); + builder.add(a.clone()); + for (int ii = 0; ii < size; ii++) { + a[ii] = ii; + } + builder.add(a.clone()); + for (int ii = 0; ii < size; ii++) { + a[ii] = size - ii; + } + builder.add(a.clone()); + for (int i = 0; i < 5; i++) { + a = rng.doubles(size).toArray(); + builder.add(a.clone()); + final int j = rng.nextInt(size); + // Pick a different index + final int k = (j + rng.nextInt(size - 1)) % size; + a[j] = -0.0; + a[k] = 0.0; + builder.add(a.clone()); + for (int z = 0; z < size; z++) { + a[z] = rng.nextBoolean() ? -0.0 : 0.0; + } + } + } + return builder.build(); + } + + static Stream testDoubleInsertionSortInternal() { + final Stream.Builder builder = Stream.builder(); + builder.add(new double[] {}); + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(); + // Use small sizes to test the pair + for (final int size : new int[] {1, 2, 3, 4, 5}) { + double[] a = new double[size]; + Arrays.fill(a, 1.23); + builder.add(a.clone()); + for (int ii = 0; ii < size; ii++) { + a[ii] = ii; + } + builder.add(a.clone()); + for (int ii = 0; ii < size; ii++) { + a[ii] = size - ii; + } + builder.add(a.clone()); + if (size == 1) { + continue; + } + for (int i = 0; i < 5; i++) { + a = rng.doubles(size).toArray(); + builder.add(a.clone()); + final int j = rng.nextInt(size); + // Pick a different index + final int k = (j + rng.nextInt(size - 1)) % size; + a[j] = -0.0; + a[k] = 0.0; + builder.add(a.clone()); + for (int z = 0; z < size; z++) { + a[z] = rng.nextBoolean() ? -0.0 : 0.0; + } + } + } + return builder.build(); + } + + static Stream testDoubleSort3() { + // Permutations is 3! = 6 + final double x = 3.35; + final double y = 12.3; + final double z = -9.99; + final double[][] a = { + {x, y, z}, + {x, z, y}, + {z, x, y}, + {y, x, z}, + {y, z, x}, + {z, y, x}, + }; + return Arrays.stream(a); + } + + static Stream testDoubleSort4() { + final Stream.Builder builder = Stream.builder(); + final double[] a = new double[4]; + // Permutations is 4! = 24 + final int shift = 42; + for (int i = 0; i < 4; i++) { + a[0] = i + shift; + for (int j = 0; j < 4; j++) { + if (j == i) { + continue; + } + a[1] = j + shift; + for (int k = 0; k < 4; k++) { + if (k == j || k == i) { + continue; + } + a[2] = k + shift; + for (int l = 0; l < 4; l++) { + if (l == k || l == j || l == i) { + continue; + } + a[3] = l + shift; + builder.add(a.clone()); + } + } + } + } + return builder.build(); + } + + static Stream testDoubleSort5() { + final Stream.Builder builder = Stream.builder(); + final double[] a = new double[5]; + // Permutations is 5! = 120 + final int shift = 42; + for (int i = 0; i < 5; i++) { + a[0] = i + shift; + for (int j = 0; j < 5; j++) { + if (j == i) { + continue; + } + a[1] = j + shift; + for (int k = 0; k < 5; k++) { + if (k == j || k == i) { + continue; + } + a[2] = k + shift; + for (int l = 0; l < 5; l++) { + if (l == k || l == j || l == i) { + continue; + } + a[3] = l + shift; + for (int m = 0; m < 5; m++) { + if (m == l || m == k || m == j || m == i) { + continue; + } + a[3] = m + shift; + builder.add(a.clone()); + } + } + } + } + } + return builder.build(); + } + + static Stream testDoubleSort3Internal() { + return testDoubleSortInternal(3); + } + + static Stream testDoubleSort4Internal() { + return testDoubleSortInternal(4); + } + + static Stream testDoubleSort5Internal() { + return testDoubleSortInternal(5); + } + + static Stream testDoubleSort7Internal() { + return testDoubleSortInternal(7); + } + + static Stream testDoubleSort8Internal() { + return testDoubleSortInternal(8); + } + + static Stream testDoubleSort11Internal() { + return testDoubleSortInternal(11); + } + + static Stream testDoubleSortInternal(int k) { + final Stream.Builder builder = Stream.builder(); + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(); + for (final int size : new int[] {k, 2 * k, 4 * k}) { + double[] a = rng.doubles(size).toArray(); + final PermutationSampler s = new PermutationSampler(rng, size, k); + for (int i = k * k; i-- >= 0;) { + a = rng.doubles(size).toArray(); + final int[] indices = s.sample(); + builder.add(Arguments.of(a.clone(), indices)); + a[indices[0]] = -0.0; + a[indices[1]] = 0.0; + builder.add(Arguments.of(a.clone(), indices)); + for (final int z : indices) { + a[z] = rng.nextBoolean() ? -0.0 : 0.0; + } + builder.add(Arguments.of(a.clone(), indices)); + } + } + return builder.build(); + } + + private static double[] extractIndices(double[] values, int[] indices) { + final double[] data = new double[indices.length]; + for (int i = 0; i < indices.length; i++) { + data[i] = values[indices[i]]; + } + return data; + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort4Internal"}) + void testLowerMedian4Internal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + final int d = indices[3]; + assertDoubleMedian(values, x -> { + Sorting.lowerMedian4(x, a, b, c, d); + return b; + }, true, false, indices); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort4Internal"}) + void testLowerMedian4bInternal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + final int d = indices[3]; + assertDoubleMedian(values, x -> { + Sorting.lowerMedian4b(x, a, b, c, d); + return b; + }, true, true, indices); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort4Internal"}) + void testLowerMedian4cInternal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + final int d = indices[3]; + assertDoubleMedian(values, x -> { + Sorting.lowerMedian4c(x, a, b, c, d); + return b; + }, true, true, indices); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort4Internal"}) + void testLowerMedian4dInternal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + final int d = indices[3]; + assertDoubleMedian(values, x -> { + Sorting.lowerMedian4d(x, a, b, c, d); + return b; + }, true, true, indices); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort4Internal"}) + void testLowerMedian4eInternal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + final int d = indices[3]; + assertDoubleMedian(values, x -> { + Sorting.lowerMedian4e(x, a, b, c, d); + return b; + }, true, true, indices); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort4Internal"}) + void testUpperMedian4Internal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + final int d = indices[3]; + assertDoubleMedian(values, x -> { + Sorting.upperMedian4(x, a, b, c, d); + return c; + }, false, true, indices); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort4Internal"}) + void testUpperMedian4cInternal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + final int d = indices[3]; + assertDoubleMedian(values, x -> { + Sorting.upperMedian4c(x, a, b, c, d); + return c; + }, false, true, indices); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort4Internal"}) + void testUpperMedian4dInternal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + final int d = indices[3]; + assertDoubleMedian(values, x -> { + Sorting.upperMedian4d(x, a, b, c, d); + return c; + }, false, true, indices); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort4"}) + void testLowerMedian4(double[] a) { + // This method computes in place + assertDoubleMedian(a, x -> { + Sorting.lowerMedian4(x, 0, 1, 2, 3); + return 1; + }, true, true, 0, 1, 2, 3); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort4"}) + void testLowerMedian4b(double[] a) { + // This method computes in place + assertDoubleMedian(a, x -> { + Sorting.lowerMedian4b(x, 0, 1, 2, 3); + return 1; + }, true, true, 0, 1, 2, 3); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort4"}) + void testLowerMedian4c(double[] a) { + // This method computes in place + assertDoubleMedian(a, x -> { + Sorting.lowerMedian4c(x, 0, 1, 2, 3); + return 1; + }, true, true, 0, 1, 2, 3); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort4"}) + void testLowerMedian4d(double[] a) { + // This method computes in place + assertDoubleMedian(a, x -> { + Sorting.lowerMedian4d(x, 0, 1, 2, 3); + return 1; + }, true, true, 0, 1, 2, 3); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort4"}) + void testLowerMedian4e(double[] a) { + // This method computes in place + assertDoubleMedian(a, x -> { + Sorting.lowerMedian4e(x, 0, 1, 2, 3); + return 1; + }, true, true, 0, 1, 2, 3); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort4"}) + void testUpperMedian4(double[] a) { + // This method computes in place + assertDoubleMedian(a, x -> { + Sorting.upperMedian4(x, 0, 1, 2, 3); + return 2; + }, false, true, 0, 1, 2, 3); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort4"}) + void testUpperMedian4c(double[] a) { + // This method computes in place + assertDoubleMedian(a, x -> { + Sorting.upperMedian4c(x, 0, 1, 2, 3); + return 2; + }, false, true, 0, 1, 2, 3); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort4"}) + void testUpperMedian4d(double[] a) { + // This method computes in place + assertDoubleMedian(a, x -> { + Sorting.upperMedian4d(x, 0, 1, 2, 3); + return 2; + }, false, true, 0, 1, 2, 3); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort5Internal"}) + void testMedian5Internal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + final int d = indices[3]; + final int e = indices[4]; + assertDoubleMedian5(values, x -> Sorting.median5(x, a, b, c, d, e), false, indices); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort5"}) + void testMedian5(double[] a) { + assertDoubleMedian5(a, x -> Sorting.median5(x, 0), false, 0, 1, 2, 3, 4); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort5"}) + void testMedian5b(double[] a) { + assertDoubleMedian5(a, x -> Sorting.median5b(x, 0), true, 0, 1, 2, 3, 4); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort5"}) + void testMedian5c(double[] a) { + assertDoubleMedian5(a, x -> Sorting.median5c(x, 0), true, 0, 1, 2, 3, 4); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort5"}) + void testMedian5d(double[] a) { + // This method computes in place + assertDoubleMedian5(a, x -> { + Sorting.median5d(x, 0, 1, 2, 3, 4); + return 2; + }, true, 0, 1, 2, 3, 4); + } + + /** + * Assert that the median {@code function} computes the same result as + * {@link Arrays#sort(double[])} run on the provided {@code indices}. Ignores signed + * zeros. + * + * @param values Data. + * @param function Sort function. + * @param lower For even lengths use the lower median; else the upper median. + * @param stable If true then no swaps should be made on the second pass. + * @param indices Indices. + */ + private static void assertDoubleMedian(double[] values, ToIntFunction function, + boolean lower, boolean stable, int... indices) { + Assertions.assertFalse(containsDuplicates(indices), () -> "Duplicate indices: " + Arrays.toString(indices)); + // Pick out the data to sort + final double[] expected = extractIndices(values, indices); + Arrays.sort(expected); + final double[] data = values.clone(); + final int m = function.applyAsInt(data); + // Only the magnitude matters so use a delta of 0 to allow -0.0 == 0.0 + Assertions.assertEquals(expected[(lower ? -1 : 0) + (expected.length >>> 1)], data[m], 0.0); + // Check outside the sorted indices + OUTSIDE: for (int i = 0; i < values.length; i++) { + for (final int ignore : indices) { + if (i == ignore) { + continue OUTSIDE; + } + } + Assertions.assertEquals(values[i], data[i]); + } + // This is not a strict requirement but check that no swaps occur on a second pass + if (stable) { + final double[] x = data.clone(); + final int m2 = function.applyAsInt(data); + Assertions.assertEquals(m, m2); + Assertions.assertArrayEquals(x, data); + } + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort3"}) + void testMin3(double[] a) { + assertDoubleMinMax(a, x -> Sorting.min3(x, 0, 1, 2), true, 0, 1, 2); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort3"}) + void testMax3(double[] a) { + assertDoubleMinMax(a, x -> Sorting.max3(x, 0, 1, 2), false, 0, 1, 2); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort3Internal"}) + void testMin3Internal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + assertDoubleMinMax(values, x -> Sorting.min3(x, a, b, c), true, indices); + } + + @ParameterizedTest + @MethodSource(value = {"testDoubleSort3Internal"}) + void testMax3Internal(double[] values, int[] indices) { + final int a = indices[0]; + final int b = indices[1]; + final int c = indices[2]; + assertDoubleMinMax(values, x -> Sorting.max3(x, a, b, c), false, indices); + } + + /** + * Assert that the median {@code function} computes the same result as + * {@link Arrays#sort(double[])} run on the provided {@code indices}. Ignores signed + * zeros. + * + * @param values Data. + * @param function Min/Max function. + * @param min Compute min; else max. + * @param indices Indices. + */ + private static void assertDoubleMinMax(double[] values, Consumer function, + boolean min, int... indices) { + Assertions.assertFalse(containsDuplicates(indices), () -> "Duplicate indices: " + Arrays.toString(indices)); + // Pick out the data to sort + final double[] expected = extractIndices(values, indices); + Arrays.sort(expected); + final double[] data = values.clone(); + function.accept(data); + final int m = min ? indices[0] : indices[indices.length - 1]; + // Only the magnitude matters so use a delta of 0 to allow -0.0 == 0.0 + Assertions.assertEquals(expected[min ? 0 : indices.length - 1], data[m], 0.0); + // Check outside the sorted indices + OUTSIDE: for (int i = 0; i < values.length; i++) { + for (final int ignore : indices) { + if (i == ignore) { + continue OUTSIDE; + } + } + Assertions.assertEquals(values[i], data[i]); + } + } + + /** + * Assert that the median {@code function} computes the same result as + * {@link Arrays#sort(double[])} run on the provided {@code indices}. Ignores signed + * zeros. + * + * @param values Data. + * @param function Sort function. + * @param stable If true then no swaps should be made on the second pass. + * @param indices Indices. + */ + private static void assertDoubleMedian5(double[] values, ToIntFunction function, + boolean stable, int... indices) { + assertDoubleMedian(values, function, false, stable, indices); + } + + // Sorting unique indices + + @ParameterizedTest + @MethodSource(value = {"testSortUnique"}) + void testSortUniqueArray(int[] values, int n) { + assertSortUnique(values.length, values, n < 0 ? values.length : n); + } + + @ParameterizedTest + @MethodSource(value = {"testSortUnique"}) + void testSortUniqueIndexSet(int[] values, int n) { + assertSortUnique(0, values, n < 0 ? values.length : n); + } + + private static void assertSortUnique(int threshold, int[] values, int n) { + final int[] x = values.clone(); + final int[] expected = Arrays.stream(values).limit(n) + .distinct().sorted().toArray(); + final IndexSet set = Sorting.sortUnique(threshold, x, n); + for (int i = 0; i < expected.length; i++) { + Assertions.assertEquals(expected[i], x[i]); + } + if (n > 0) { + final int end = expected.length - 1; + final int max = x[n - 1]; + if (expected.length < n) { + Assertions.assertEquals(expected[end], ~max, "twos-complement max value"); + } else { + Assertions.assertEquals(expected[end], max, "max value"); + } + } + for (int i = expected.length; i < n; i++) { + Assertions.assertTrue(x[i] < 0, "Duplicate not set to negative"); + } + + if (x.length <= threshold) { + Assertions.assertNull(set); + } else if (n > 1) { + // Check the IndexSet contains all the indices + final int[] a = new int[expected.length]; + final int[] c = {0}; + set.forEach(i -> a[c[0]++] = i); + Assertions.assertArrayEquals(expected, a); + } + } + + static Stream testSortUnique() { + final Stream.Builder builder = Stream.builder(); + // Use length -1 to use the array length + builder.add(Arguments.of(new int[0], -1)); + builder.add(Arguments.of(new int[3], -1)); + builder.add(Arguments.of(new int[3], -1)); + builder.add(Arguments.of(new int[] {1, 2, 3}, -1)); + builder.add(Arguments.of(new int[] {1, 1, 1}, -1)); + builder.add(Arguments.of(new int[] {42}, -1)); + builder.add(Arguments.of(new int[] {42, 5, 7}, -1)); + builder.add(Arguments.of(new int[] {42, 5, 7, 7, 4}, -1)); + // Truncated indices + builder.add(Arguments.of(new int[] {42, 5, 7, 7, 4}, 3)); + return builder.build(); + } + + @ParameterizedTest + @MethodSource(value = {"testSortIndices"}) + void testSortIndicesInsertionSort(int[] values, int n) { + assertSortIndices(Sorting::sortIndicesInsertionSort, values, n, 1); + } + + @ParameterizedTest + @MethodSource(value = {"testSortIndices"}) + void testSortIndicesBinarySearch(int[] values, int n) { + assertSortIndices(Sorting::sortIndicesBinarySearch, values, n, 2); + } + + @ParameterizedTest + @MethodSource(value = {"testSortIndices"}) + void testSortIndicesHeapSort(int[] values, int n) { + assertSortIndices(Sorting::sortIndicesHeapSort, values, n, 3); + } + + @ParameterizedTest + @MethodSource(value = {"testSortIndices"}) + void testSortIndicesSort(int[] values, int n) { + assertSortIndices(Sorting::sortIndicesSort, values, n, 1); + } + + @ParameterizedTest + @MethodSource(value = {"testSortIndices"}) + void testSortIndicesIndexSet(int[] values, int n) { + assertSortIndices(Sorting::sortIndicesIndexSet, values, n, 1); + } + + @ParameterizedTest + @MethodSource(value = {"testSortIndices"}) + void testSortIndicesHashIndexSet(int[] values, int n) { + assertSortIndices(Sorting::sortIndicesHashIndexSet, values, n, 1); + } + + @ParameterizedTest + @MethodSource(value = {"testSortIndices"}) + void testSortIndices(int[] values, int n) { + assertSortIndices(Sorting::sortIndices, values, n, 0); + } + + private static void assertSortIndices(IndexSort fun, int[] values, int n, int minSupportedLength) { + // Negative n is a signal to use the full length + n = n < 0 ? values.length : n; + Assumptions.assumeTrue(n >= minSupportedLength); + final int[] x = values.clone(); + final int[] expected = Arrays.stream(values).limit(n) + .distinct().sorted().toArray(); + final int unique = fun.sort(x, n); + Assertions.assertEquals(expected.length, unique, "Incorrect unique length"); + for (int i = 0; i < expected.length; i++) { + final int index = i; + Assertions.assertEquals(expected[i], x[i], () -> "Error @ " + index); + } + // Test values after unique should be in the entire original data + final BitSet set = new BitSet(); + Arrays.stream(values).limit(n).forEach(set::set); + for (int i = expected.length; i < n; i++) { + Assertions.assertTrue(set.get(x[i]), "Data up to n destroyed"); + } + // Data after n should be untouched + for (int i = n; i < values.length; i++) { + Assertions.assertEquals(values[i], x[i], "Data after n destroyed"); + } + } + + static Stream testSortIndices() { + // Create data that should exercise all strategies in the heuristics in + // Sorting::sortIndices used to choose a sorting method + final Stream.Builder builder = Stream.builder(); + // Use length -1 to use the array length + builder.add(Arguments.of(new int[0], -1)); + builder.add(Arguments.of(new int[3], -1)); + builder.add(Arguments.of(new int[3], -1)); + builder.add(Arguments.of(new int[] {42}, -1)); + builder.add(Arguments.of(new int[] {1, 2, 3}, -1)); + builder.add(Arguments.of(new int[] {3, 2, 1}, -1)); + builder.add(Arguments.of(new int[] {42, 5, 7}, -1)); + // Duplicates + builder.add(Arguments.of(new int[] {1, 1}, -1)); + builder.add(Arguments.of(new int[] {1, 1, 1}, -1)); + builder.add(Arguments.of(new int[] {42, 5, 2, 9, 2, 9, 7, 7, 4}, -1)); + // Truncated indices + builder.add(Arguments.of(new int[] {3, 2, 1}, 1)); + builder.add(Arguments.of(new int[] {3, 2, 1}, 2)); + builder.add(Arguments.of(new int[] {2, 2, 1}, 2)); + builder.add(Arguments.of(new int[] {42, 5, 7, 7, 4}, 3)); + builder.add(Arguments.of(new int[] {5, 4, 3, 2, 1}, 3)); + builder.add(Arguments.of(new int[] {1, 2, 3, 4, 5}, 3)); + builder.add(Arguments.of(new int[] {5, 3, 1, 2, 4}, 3)); + // Some random indices with duplicates + final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(); + for (final int size : new int[] {5, 10, 30}) { + final int maxIndex = size >>> 1; + for (int i = 0; i < 5; i++) { + builder.add(Arguments.of(rng.ints(size, 0, maxIndex).toArray(), -1)); + } + } + // A lot of duplicates + builder.add(Arguments.of(rng.ints(50, 0, 3).toArray(), -1)); + builder.add(Arguments.of(rng.ints(50, 0, 5).toArray(), -1)); + builder.add(Arguments.of(rng.ints(50, 0, 10).toArray(), -1)); + // Bug where the first index was ignored when using an IndexSet + builder.add(Arguments.of(IntStream.range(0, 50).map(x -> 50 - x).toArray(), -1)); + // Sparse + builder.add(Arguments.of(rng.ints(25, 0, 100000).toArray(), -1)); + // Ascending + builder.add(Arguments.of(IntStream.range(99, 134).toArray(), -1)); + builder.add(Arguments.of(IntStream.range(99, 134).map(x -> x * 2).toArray(), -1)); + builder.add(Arguments.of(IntStream.range(99, 134).map(x -> x * 3).toArray(), -1)); + return builder.build(); + } + + // Helper methods + + private static boolean containsDuplicates(int[] indices) { + for (int i = 0; i < indices.length; i++) { + for (int j = 0; j < i; j++) { + if (indices[i] == indices[j]) { + return true; + } + } + } + return false; + } + + private static double[] replaceSignedZeros(double[] values) { + for (int i = 0; i < values.length; i++) { + if (Double.compare(-0.0, values[i]) == 0) { + values[i] = 0; + } + } + return values; + } +} diff --git a/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/SplittingIntervalTest.java b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/SplittingIntervalTest.java new file mode 100644 index 000000000..fb9318945 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/SplittingIntervalTest.java @@ -0,0 +1,185 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.util.Arrays; +import java.util.function.BiFunction; +import java.util.stream.Stream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Test for {@link SplittingInterval} implementations. + */ +class SplittingIntervalTest { + + @Test + void testKeyIntervalInvalidIndicesThrows() { + // Size zero + Assertions.assertThrows(IllegalArgumentException.class, () -> KeyUpdatingInterval.of(new int[0], 0)); + Assertions.assertThrows(IllegalArgumentException.class, () -> KeyUpdatingInterval.of(new int[10], 0)); + // Not sorted + Assertions.assertThrows(IllegalArgumentException.class, + () -> KeyUpdatingInterval.of(new int[] {3, 2, 1}, 3)); + // Not unique + Assertions.assertThrows(IllegalArgumentException.class, + () -> KeyUpdatingInterval.of(new int[] {1, 2, 2, 3}, 4)); + // Invalid indices: not in [0, Integer.MAX_VALUE) + Assertions.assertThrows(IllegalArgumentException.class, + () -> KeyUpdatingInterval.of(new int[] {-1, 2, 3}, 3)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> KeyUpdatingInterval.of(new int[] {1, 2, Integer.MAX_VALUE}, 3)); + } + + @Test + void testBitIndexUpdatingIntervalInvalidIndicesThrows() { + // Size zero + Assertions.assertThrows(IllegalArgumentException.class, () -> BitIndexUpdatingInterval.of(new int[0], 0)); + Assertions.assertThrows(IllegalArgumentException.class, () -> BitIndexUpdatingInterval.of(new int[10], 0)); + // Invalid indices: not in [0, Integer.MAX_VALUE) + Assertions.assertThrows(IllegalArgumentException.class, + () -> BitIndexUpdatingInterval.of(new int[] {-1, 2, 3}, 3)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> BitIndexUpdatingInterval.of(new int[] {1, 2, Integer.MAX_VALUE}, 3)); + } + + @ParameterizedTest + @MethodSource(value = {"testIndices"}) + void testSplitKeyInterval(int[] indices) { + assertSplit(KeyUpdatingInterval::of, indices); + } + + @ParameterizedTest + @MethodSource(value = {"testIndices"}) + void testSplitBitIndexUpdatingInterval(int[] indices) { + // Skip this due to excess memory consumption + Assumptions.assumeTrue(indices[indices.length - 1] < Integer.MAX_VALUE - 1); + assertSplit(BitIndexUpdatingInterval::of, indices); + } + + /** + * Assert the {@link SplittingInterval#splitLeft(int, int)} method. + * These are tested by successive calls to split the interval around the mid-point. + * + * @param constructor Interval constructor. + * @param indices Indices. + */ + private static void assertSplit(BiFunction constructor, int[] indices) { + assertSplitMedian(constructor.apply(indices, indices.length), + indices, 0, indices.length - 1); + assertSplitMiddleIndices(constructor.apply(indices, indices.length), + indices, 0, indices.length - 1); + } + + /** + * Assert a split using the median value between the split median. + * + * @param interval Interval. + * @param indices Indices. + * @param i Low index into the indices (inclusive). + * @param j High index into the indices (inclusive). + */ + private static void assertSplitMedian(SplittingInterval interval, int[] indices, int i, int j) { + if (indices[i] + 1 >= indices[j]) { + // Cannot split - no value between the low and high points + // Split on low should return null left, right may not be empty + final SplittingInterval leftInterval = interval.split(indices[i], indices[i]); + Assertions.assertNull(leftInterval, "left should be empty"); + if (indices[i] == indices[j]) { + Assertions.assertTrue(interval.empty(), "right should be empty"); + } else { + Assertions.assertFalse(interval.empty(), "right should not be empty"); + Assertions.assertEquals(indices[i + 1], interval.left()); + Assertions.assertEquals(indices[j], interval.right()); + } + return; + } + // Find the expected split about the median + final int m = (indices[i] + indices[j]) >>> 1; + // Binary search finds the value or the insertion index of the value + int hi = Arrays.binarySearch(indices, i, j + 1, m + 1); + if (hi < 0) { + // Use the insertion index + hi = ~hi; + } + // Scan for the lower index + int lo = hi; + do { + --lo; + } while (indices[lo] >= m); + + final int left = interval.left(); + final int right = interval.right(); + + final SplittingInterval leftInterval = interval.split(m, m); + Assertions.assertEquals(left, leftInterval.left()); + Assertions.assertEquals(indices[lo], leftInterval.right()); + Assertions.assertEquals(indices[hi], interval.left()); + Assertions.assertEquals(right, interval.right()); + + // Recurse + assertSplitMedian(leftInterval, indices, i, lo); + assertSplitMedian(interval, indices, hi, j); + } + + /** + * Assert a split using the two middle indices. + * + * @param interval Interval. + * @param indices Indices. + * @param i Low index into the indices (inclusive). + * @param j High index into the indices (inclusive). + */ + private static void assertSplitMiddleIndices(SplittingInterval interval, int[] indices, int i, int j) { + if (i + 3 >= j) { + // Cannot split - not two indices between low and high index + // Split on high may return left, right should be empty + final SplittingInterval leftInterval = interval.split(indices[j], indices[j]); + Assertions.assertTrue(interval.empty(), "right should be empty"); + if (indices[i] == indices[j]) { + Assertions.assertNull(leftInterval, "left should be empty"); + } else { + Assertions.assertEquals(indices[i], leftInterval.left()); + Assertions.assertEquals(indices[j - 1], leftInterval.right()); + } + return; + } + // Middle two indices + final int m1 = (i + j) >>> 1; + final int m2 = m1 + 1; + + final int left = interval.left(); + final int right = interval.right(); + final SplittingInterval leftInterval = interval.split(indices[m1], indices[m2]); + Assertions.assertEquals(left, leftInterval.left()); + Assertions.assertEquals(indices[m1 - 1], leftInterval.right()); + Assertions.assertEquals(indices[m2 + 1], interval.left()); + Assertions.assertEquals(right, interval.right()); + + // Recurse + assertSplitMiddleIndices(leftInterval, indices, i, m1 - 1); + assertSplitMiddleIndices(interval, indices, m2 + 1, j); + } + + static Stream testIndices() { + return SearchableIntervalTest.testPreviousNextIndex(); + } +} diff --git a/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/TestUtils.java b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/TestUtils.java new file mode 100644 index 000000000..0d1a27012 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/TestUtils.java @@ -0,0 +1,217 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.util.function.Supplier; +import org.apache.commons.numbers.core.DD; +import org.apache.commons.rng.UniformRandomProvider; +import org.junit.jupiter.api.Assertions; + +/** + * Test utilities. + */ +final class TestUtils { + /** No instances. */ + private TestUtils() {} + + // DD equality checks adapted from o.a.c.numbers.core.TestUtils + + /** + * Assert the two numbers are equal within the provided relative error. + * + *

The provided error is relative to the exact result in expected: (e - a) / e. + * If expected is zero this division is undefined. In this case the actual must be zero + * (no absolute tolerance is supported). The reporting of the error uses the absolute + * error and the return value of the relative error is 0. Cases of complete cancellation + * should be avoided for benchmarking relative accuracy. + * + *

Note that the actual double-double result is not validated using the high and low + * parts individually. These are summed and compared to the expected. + * + *

Set {@code eps} to negative to report the relative error to the stdout and + * ignore failures. + * + *

The relative error is signed. The sign of the error + * is the same as that returned from Double.compare(actual, expected); it is + * computed using {@code actual - expected}. + * + * @param expected expected value + * @param actual actual value + * @param eps maximum relative error between the two values + * @param msg failure message + * @return relative error difference between the values (signed) + * @throws NumberFormatException if {@code actual} contains non-finite values + */ + static double assertEquals(BigDecimal expected, DD actual, double eps, String msg) { + return assertEquals(expected, actual, eps, () -> msg); + } + + /** + * Assert the two numbers are equal within the provided relative error. + * + *

The provided error is relative to the exact result in expected: (e - a) / e. + * If expected is zero this division is undefined. In this case the actual must be zero + * (no absolute tolerance is supported). The reporting of the error uses the absolute + * error and the return value of the relative error is 0. Cases of complete cancellation + * should be avoided for benchmarking relative accuracy. + * + *

Note that the actual double-double result is not validated using the high and low + * parts individually. These are summed and compared to the expected. + * + *

Set {@code eps} to negative to report the relative error to the stdout and + * ignore failures. + * + *

The relative error is signed. The sign of the error + * is the same as that returned from Double.compare(actual, expected); it is + * computed using {@code actual - expected}. + * + * @param expected expected value + * @param actual actual value + * @param eps maximum relative error between the two values + * @param msg failure message + * @return relative error difference between the values (signed) + * @throws NumberFormatException if {@code actual} contains non-finite values + */ + static double assertEquals(BigDecimal expected, DD actual, double eps, Supplier msg) { + // actual - expected + final BigDecimal delta = new BigDecimal(actual.hi()) + .add(new BigDecimal(actual.lo())) + .subtract(expected); + boolean equal; + if (expected.compareTo(BigDecimal.ZERO) == 0) { + // Edge case. Currently an absolute tolerance is not supported as summation + // to zero cases generated in testing all pass. + equal = actual.doubleValue() == 0; + + // DEBUG: + if (eps < 0) { + if (!equal) { + printf("%sexpected 0 != actual <%s + %s> (abs.error=%s)%n", + prefix(msg), actual.hi(), actual.lo(), delta.doubleValue()); + } + } else if (!equal) { + Assertions.fail(String.format("%sexpected 0 != actual <%s + %s> (abs.error=%s)", + prefix(msg), actual.hi(), actual.lo(), delta.doubleValue())); + } + + return 0; + } + + final double rel = delta.divide(expected, MathContext.DECIMAL128).doubleValue(); + // Allow input of a negative maximum ULPs + equal = Math.abs(rel) <= Math.abs(eps); + + // DEBUG: + if (eps < 0) { + if (!equal) { + printf("%sexpected <%s> != actual <%s + %s> (rel.error=%s (%.3f x tol))%n", + prefix(msg), expected.round(MathContext.DECIMAL128), actual.hi(), actual.lo(), + rel, Math.abs(rel) / eps); + } + } else if (!equal) { + Assertions.fail(String.format("%sexpected <%s> != actual <%s + %s> (rel.error=%s (%.3f x tol))", + prefix(msg), expected.round(MathContext.DECIMAL128), actual.hi(), actual.lo(), + rel, Math.abs(rel) / eps)); + } + + return rel; + } + + /** + * Print a formatted message to stdout. + * Provides a single point to disable checkstyle warnings on print statements and + * enable/disable all print debugging. + * + * @param format Format string. + * @param args Arguments. + */ + static void printf(String format, Object... args) { + // CHECKSTYLE: stop regex + System.out.printf(format, args); + // CHECKSTYLE: resume regex + } + + /** + * Get the prefix for the message. + * + * @param msg Message supplier + * @return the prefix + */ + static String prefix(Supplier msg) { + return msg == null ? "" : msg.get() + ": "; + } + + // Uses Fisher-Yates shuffle copied from o.a.c.rng.sampling.ArraySampler + // TODO: This can be removed when {@code commons-rng-sampling 1.6} is released. + + /** + * Shuffles the entries of the given array. + * + * @param rng Source of randomness. + * @param array Array whose entries will be shuffled (in-place). + * @return Shuffled input array. + */ + static double[] shuffle(UniformRandomProvider rng, double[] array) { + for (int i = array.length; i > 1; i--) { + swap(array, i - 1, rng.nextInt(i)); + } + return array; + } + + /** + * Shuffles the entries of the given array. + * + * @param rng Source of randomness. + * @param array Array whose entries will be shuffled (in-place). + * @return Shuffled input array. + */ + static int[] shuffle(UniformRandomProvider rng, int[] array) { + for (int i = array.length; i > 1; i--) { + swap(array, i - 1, rng.nextInt(i)); + } + return array; + } + + /** + * Swaps the two specified elements in the array. + * + * @param array Array. + * @param i First index. + * @param j Second index. + */ + private static void swap(double[] array, int i, int j) { + final double tmp = array[i]; + array[i] = array[j]; + array[j] = tmp; + } + + /** + * Swaps the two specified elements in the array. + * + * @param array Array. + * @param i First index. + * @param j Second index. + */ + private static void swap(int[] array, int i, int j) { + final int tmp = array[i]; + array[i] = array[j]; + array[j] = tmp; + } +} diff --git a/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/UpdatingIntervalTest.java b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/UpdatingIntervalTest.java new file mode 100644 index 000000000..3aa986597 --- /dev/null +++ b/commons-numbers-examples/examples-jmh/src/test/java/org/apache/commons/numbers/examples/jmh/arrays/UpdatingIntervalTest.java @@ -0,0 +1,389 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.numbers.examples.jmh.arrays; + +import java.util.Arrays; +import java.util.function.BiFunction; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * Test for {@link UpdatingInterval} implementations. + */ +class UpdatingIntervalTest { + @ParameterizedTest + @ValueSource(ints = {0, 1, 42, Integer.MAX_VALUE - 1}) + void testPointInterval(int k) { + final UpdatingInterval interval = IndexIntervals.interval(k); + Assertions.assertEquals(k, interval.left()); + Assertions.assertEquals(k, interval.right()); + Assertions.assertThrows(UnsupportedOperationException.class, () -> interval.updateLeft(k)); + Assertions.assertThrows(UnsupportedOperationException.class, () -> interval.updateRight(k)); + Assertions.assertThrows(UnsupportedOperationException.class, () -> interval.splitLeft(k, k)); + Assertions.assertThrows(UnsupportedOperationException.class, () -> interval.splitRight(k, k)); + } + + @ParameterizedTest + @CsvSource({ + "1, 1", + "1, 2", + "1, 3", + "10, 42", + }) + void testRangeInterval(int lo, int hi) { + UpdatingInterval interval = IndexIntervals.interval(lo, hi); + Assertions.assertEquals(lo, interval.left()); + Assertions.assertEquals(hi, interval.right()); + if (interval.left() < interval.right()) { + Assertions.assertEquals(lo + 1, interval.updateLeft(lo + 1)); + } + interval = IndexIntervals.interval(lo, hi); + if (interval.left() < interval.right()) { + Assertions.assertEquals(hi - 1, interval.updateRight(hi - 1)); + } + interval = IndexIntervals.interval(lo, hi); + if (interval.left() + 2 < interval.right()) { + final int left = interval.left(); + final int right = interval.right(); + final int m1 = (interval.left() + interval.right()) >>> 1; + final int m2 = m1 + 1; + final UpdatingInterval leftInterval = interval.splitLeft(m1, m2); + Assertions.assertEquals(left, leftInterval.left()); + Assertions.assertEquals(m1 - 1, leftInterval.right()); + Assertions.assertEquals(m2 + 1, interval.left()); + Assertions.assertEquals(right, interval.right()); + + interval = IndexIntervals.interval(lo, hi); + final UpdatingInterval rightInterval = interval.splitRight(m1, m2); + Assertions.assertEquals(left, interval.left()); + Assertions.assertEquals(m1 - 1, interval.right()); + Assertions.assertEquals(m2 + 1, rightInterval.left()); + Assertions.assertEquals(right, rightInterval.right()); + } + } + + @Test + void testKeyIntervalInvalidIndicesThrows() { + // Size zero + Assertions.assertThrows(IllegalArgumentException.class, () -> KeyUpdatingInterval.of(new int[0], 0)); + Assertions.assertThrows(IllegalArgumentException.class, () -> KeyUpdatingInterval.of(new int[10], 0)); + // Not sorted + Assertions.assertThrows(IllegalArgumentException.class, + () -> KeyUpdatingInterval.of(new int[] {3, 2, 1}, 3)); + // Not unique + Assertions.assertThrows(IllegalArgumentException.class, + () -> KeyUpdatingInterval.of(new int[] {1, 2, 2, 3}, 4)); + // Invalid indices: not in [0, Integer.MAX_VALUE) + Assertions.assertThrows(IllegalArgumentException.class, + () -> KeyUpdatingInterval.of(new int[] {-1, 2, 3}, 3)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> KeyUpdatingInterval.of(new int[] {1, 2, Integer.MAX_VALUE}, 3)); + } + + @Test + void testBitIndexUpdatingIntervalInvalidIndicesThrows() { + // Size zero + Assertions.assertThrows(IllegalArgumentException.class, () -> BitIndexUpdatingInterval.of(new int[0], 0)); + Assertions.assertThrows(IllegalArgumentException.class, () -> BitIndexUpdatingInterval.of(new int[10], 0)); + // Invalid indices: not in [0, Integer.MAX_VALUE) + Assertions.assertThrows(IllegalArgumentException.class, + () -> BitIndexUpdatingInterval.of(new int[] {-1, 2, 3}, 3)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> BitIndexUpdatingInterval.of(new int[] {1, 2, Integer.MAX_VALUE}, 3)); + } + + @ParameterizedTest + @MethodSource(value = {"testIndices"}) + void testUpdateKeyInterval(int[] indices) { + assertUpdate(KeyUpdatingInterval::of, indices); + } + + @ParameterizedTest + @MethodSource(value = {"testIndices"}) + void testUpdateIndexSetInterval(int[] indices) { + // Skip this due to excess memory consumption + Assumptions.assumeTrue(indices[indices.length - 1] < Integer.MAX_VALUE - 1); + assertUpdate((k, n) -> IndexSet.of(k, n).interval(), indices); + } + + @ParameterizedTest + @MethodSource(value = {"testIndices"}) + void testUpdateBitIndexUpdatingInterval(int[] indices) { + // Skip this due to excess memory consumption + Assumptions.assumeTrue(indices[indices.length - 1] < Integer.MAX_VALUE - 1); + assertUpdate(BitIndexUpdatingInterval::of, indices); + } + + @ParameterizedTest + @MethodSource(value = {"testIndices"}) + void testUpdateIndexInterval(int[] indices) { + assertUpdate(IndexIntervals::createUpdatingInterval, indices); + } + + @ParameterizedTest + @MethodSource(value = {"testIndices"}) + void testSplitKeyInterval(int[] indices) { + assertSplit(KeyUpdatingInterval::of, indices); + } + + @ParameterizedTest + @MethodSource(value = {"testIndices"}) + void testSplitIndexSetInterval(int[] indices) { + // Skip this due to excess memory consumption + Assumptions.assumeTrue(indices[indices.length - 1] < Integer.MAX_VALUE - 1); + assertSplit((k, n) -> IndexSet.of(k, n).interval(), indices); + } + + @ParameterizedTest + @MethodSource(value = {"testIndices"}) + void testSplitBitIndexUpdatingInterval(int[] indices) { + // Skip this due to excess memory consumption + Assumptions.assumeTrue(indices[indices.length - 1] < Integer.MAX_VALUE - 1); + assertSplit(BitIndexUpdatingInterval::of, indices); + } + + @ParameterizedTest + @MethodSource(value = {"testIndices"}) + void testSplitIndexInterval(int[] indices) { + assertSplit(IndexIntervals::createUpdatingInterval, indices); + } + + /** + * Assert the {@link UpdatingInterval#updateLeft(int)} and {@link UpdatingInterval#updateRight(int)} methods. + * These are tested by successive calls to reduce the interval by 1 index until it + * has only 1 index remaining. + * + * @param constructor Interval constructor. + * @param indices Indices. + */ + private static void assertUpdate(BiFunction constructor, + int[] indices) { + UpdatingInterval interval = constructor.apply(indices, indices.length); + final int nm1 = indices.length - 1; + Assertions.assertEquals(indices[0], interval.left()); + Assertions.assertEquals(indices[nm1], interval.right()); + + // Use updateLeft to reduce the interval to length 1 + for (int i = 1; i < indices.length; i++) { + // rounded down median between indices + final int k = (indices[i - 1] + indices[i]) >>> 1; + interval.updateLeft(k + 1); + Assertions.assertEquals(indices[i], interval.left()); + } + Assertions.assertEquals(interval.left(), interval.right()); + + // Use updateRight to reduce the interval to length 1 + interval = constructor.apply(indices, indices.length); + for (int i = indices.length; --i > 0;) { + // rounded up median between indices + final int k = 1 + ((indices[i - 1] + indices[i]) >>> 1); + interval.updateRight(k - 1); + Assertions.assertEquals(indices[i - 1], interval.right()); + } + Assertions.assertEquals(interval.left(), interval.right()); + } + + /** + * Assert the {@link UpdatingInterval#splitLeft(int, int)} method. + * These are tested by successive calls to split the interval around the mid-point. + * + * @param constructor Interval constructor. + * @param indices Indices. + */ + private static void assertSplit(BiFunction constructor, int[] indices) { + assertSplitMedian(constructor.apply(indices, indices.length), + indices, 0, indices.length - 1, true); + assertSplitMedian(constructor.apply(indices, indices.length), + indices, 0, indices.length - 1, false); + assertSplitMiddleIndices(constructor.apply(indices, indices.length), + indices, 0, indices.length - 1, true); + assertSplitMiddleIndices(constructor.apply(indices, indices.length), + indices, 0, indices.length - 1, false); + } + + /** + * Assert a split using the median value between the split median. + * + * @param interval Interval. + * @param indices Indices. + * @param i Low index into the indices (inclusive). + * @param j High index into the indices (inclusive). + * @param splitLeft Use split left, else split right + */ + private static void assertSplitMedian(UpdatingInterval interval, int[] indices, int i, int j, + boolean splitLeft) { + if (indices[i] + 1 >= indices[j]) { + // Cannot split - no value between the low and high points + return; + } + // Find the expected split about the median + final int m = (indices[i] + indices[j]) >>> 1; + // Binary search finds the value or the insertion index of the value + int hi = Arrays.binarySearch(indices, i, j + 1, m + 1); + if (hi < 0) { + // Use the insertion index + hi = ~hi; + } + // Scan for the lower index + int lo = hi; + do { + --lo; + } while (indices[lo] >= m); + + final int left = interval.left(); + final int right = interval.right(); + + UpdatingInterval leftInterval; + if (splitLeft) { + leftInterval = interval.splitLeft(m, m); + } else { + UpdatingInterval rightInterval = interval.splitRight(m, m); + leftInterval = interval; + interval = rightInterval; + } + Assertions.assertEquals(left, leftInterval.left()); + Assertions.assertEquals(indices[lo], leftInterval.right()); + Assertions.assertEquals(indices[hi], interval.left()); + Assertions.assertEquals(right, interval.right()); + + // Recurse + assertSplitMedian(leftInterval, indices, i, lo, splitLeft); + assertSplitMedian(interval, indices, hi, j, splitLeft); + } + + /** + * Assert a split using the two middle indices. + * + * @param interval Interval. + * @param indices Indices. + * @param i Low index into the indices (inclusive). + * @param j High index into the indices (inclusive). + * @param splitLeft Use split left, else split right + */ + private static void assertSplitMiddleIndices(UpdatingInterval interval, int[] indices, int i, int j, + boolean splitLeft) { + if (i + 3 >= j) { + // Cannot split - not two indices between low and high index + return; + } + // Middle two indices + final int m1 = (i + j) >>> 1; + final int m2 = m1 + 1; + + final int left = interval.left(); + final int right = interval.right(); + UpdatingInterval leftInterval; + if (splitLeft) { + leftInterval = interval.splitLeft(indices[m1], indices[m2]); + } else { + UpdatingInterval rightInterval = interval.splitRight(indices[m1], indices[m2]); + leftInterval = interval; + interval = rightInterval; + } + Assertions.assertEquals(left, leftInterval.left()); + Assertions.assertEquals(indices[m1 - 1], leftInterval.right()); + Assertions.assertEquals(indices[m2 + 1], interval.left()); + Assertions.assertEquals(right, interval.right()); + + // Recurse + assertSplitMiddleIndices(leftInterval, indices, i, m1 - 1, splitLeft); + assertSplitMiddleIndices(interval, indices, m2 + 1, j, splitLeft); + } + + static Stream testIndices() { + return SearchableIntervalTest.testPreviousNextIndex(); + } + + @Test + void testIndexIntervalCreate() { + // The above tests verify the UpdatingInterval implementations all work. + // Hit all paths in the analysis performed to create an interval. + + // 1 key + Assertions.assertEquals(IndexIntervals.PointInterval.class, + IndexIntervals.createUpdatingInterval(new int[] {1}, 1).getClass()); + + // 2 close keys + Assertions.assertEquals(IndexIntervals.RangeInterval.class, + IndexIntervals.createUpdatingInterval(new int[] {2, 1}, 2).getClass()); + Assertions.assertEquals(IndexIntervals.RangeInterval.class, + IndexIntervals.createUpdatingInterval(new int[] {1, 2}, 2).getClass()); + + // 2 unsorted keys + Assertions.assertEquals(KeyUpdatingInterval.class, + IndexIntervals.createUpdatingInterval(new int[] {200, 1}, 2).getClass()); + + // Sorted number of keys saturating the range + Assertions.assertEquals(KeyUpdatingInterval.class, + IndexIntervals.createUpdatingInterval(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, 11).getClass()); + // Small number of keys saturating the range + Assertions.assertEquals(KeyUpdatingInterval.class, + IndexIntervals.createUpdatingInterval(new int[] {11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1}, 11).getClass()); + // Keys over a huge range + Assertions.assertEquals(KeyUpdatingInterval.class, + IndexIntervals.createUpdatingInterval(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, Integer.MAX_VALUE - 1}, 11).getClass()); + + // Small number of sorted keys over a moderate range + int[] k = IntStream.range(0, 30).map(i -> i * 64) .toArray(); + Assertions.assertEquals(KeyUpdatingInterval.class, + IndexIntervals.createUpdatingInterval(k.clone(), k.length).getClass()); + // Same keys not sorted + reverse(k, 0, k.length); + Assertions.assertEquals(BitIndexUpdatingInterval.class, + IndexIntervals.createUpdatingInterval(k.clone(), k.length).getClass()); + // Same keys over a huge range + k[k.length - 1] = Integer.MAX_VALUE - 1; + Assertions.assertEquals(KeyUpdatingInterval.class, + IndexIntervals.createUpdatingInterval(k, k.length).getClass()); + + // Moderate number of sorted keys over a moderate range + k = IntStream.range(0, 3000).map(i -> i * 64) .toArray(); + Assertions.assertEquals(KeyUpdatingInterval.class, + IndexIntervals.createUpdatingInterval(k.clone(), k.length).getClass()); + // Same keys not sorted + reverse(k, 0, k.length); + Assertions.assertEquals(BitIndexUpdatingInterval.class, + IndexIntervals.createUpdatingInterval(k.clone(), k.length).getClass()); + // Same keys over a huge range - switch to binary search on the keys + k[k.length - 1] = Integer.MAX_VALUE - 1; + Assertions.assertEquals(KeyUpdatingInterval.class, + IndexIntervals.createUpdatingInterval(k, k.length).getClass()); + } + + /** + * Reverse (part of) the data. + * + * @param a Data. + * @param from Start index to reverse (inclusive). + * @param to End index to reverse (exclusive). + */ + private static void reverse(int[] a, int from, int to) { + for (int i = from - 1, j = to; ++i < --j;) { + final int v = a[i]; + a[i] = a[j]; + a[j] = v; + } + } +} diff --git a/src/main/resources/checkstyle/checkstyle-suppressions.xml b/src/main/resources/checkstyle/checkstyle-suppressions.xml index 8c22e0902..c8072ef84 100644 --- a/src/main/resources/checkstyle/checkstyle-suppressions.xml +++ b/src/main/resources/checkstyle/checkstyle-suppressions.xml @@ -43,6 +43,21 @@ + + + + + + + + + + + + + + + @@ -56,4 +71,8 @@ + + + + diff --git a/src/main/resources/pmd/pmd-ruleset.xml b/src/main/resources/pmd/pmd-ruleset.xml index 98138da18..61455dc1c 100644 --- a/src/main/resources/pmd/pmd-ruleset.xml +++ b/src/main/resources/pmd/pmd-ruleset.xml @@ -120,14 +120,27 @@ + or @SimpleName='DD' + or @SimpleName='QuickSelect']"/> + + + + + + + + or @SimpleName='BoostBeta' + or @SimpleName='Sorting' + or @SimpleName='Selection' + or @SimpleName='QuickSelect']"/> @@ -149,6 +162,37 @@ + + + + + + + + + + + + + + + + + + + @@ -172,7 +216,10 @@ value="./ancestor-or-self::ClassOrInterfaceDeclaration[@SimpleName='Complex' or @SimpleName='Fraction' or @SimpleName='BigFraction' - or @SimpleName='DD']"/> + or @SimpleName='DD' + or @SimpleName='BitIndexUpdatingInterval' + or @SimpleName='HashIndexSet' + or @SimpleName='PairDoubleInteger']"/> @@ -183,33 +230,22 @@ - - + + value="./ancestor-or-self::ClassOrInterfaceDeclaration[@SimpleName='Sorting' + or @SimpleName='HashIndexSet' + or @SimpleName='Selection' + or @SimpleName='IndexSupport' + or @SimpleName='QuickSelect']"/> - - + - - - - - + value="./ancestor-or-self::ClassOrInterfaceDeclaration[@SimpleName='Sorting' + or @SimpleName='QuickSelect']"/>