Skip to content

Commit

Permalink
Binary search WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
dmlloyd committed Dec 3, 2024
1 parent 335fa2e commit e9d3050
Show file tree
Hide file tree
Showing 18 changed files with 734 additions and 0 deletions.
56 changes: 56 additions & 0 deletions binary-search/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.smallrye.common</groupId>
<artifactId>smallrye-common-parent</artifactId>
<version>2.9.0-SNAPSHOT</version>
</parent>

<artifactId>smallrye-common-binary-search</artifactId>

<name>SmallRye Common: Binary search</name>

<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>smallrye-common-function</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<profiles>
<profile>
<id>coverage</id>
<properties>
<argLine>@{jacocoArgLine}</argLine>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<executions>
<execution>
<id>report</id>
<phase>verify</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>


</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package io.smallrye.common.binarysearch;

/**
* A utility class which provides multiple variations on binary searching.
*
* <h2>Ranges</h2>
*
* The binary search algorithm operates over a range of values.
* The search will always return in logarithmic time with respect to the search interval.
* This implementation supports multiple kinds of value ranges:
* <ul>
* <li>{@code int} via the {@link #intRange()} method</li>
* <li>{@code long} via the {@link #longRange()} method</li>
* <li>Object ranges via the {@link #objRange()} method</li>
* </ul>
*
* Each of these methods returns an object with {@code find()} methods that perform variations on the binary search.
*
* <h3>Range predicates</h3>
*
* Ranges are searched by predicate.
* The predicate is evaluated for the lowest value within the range
* (that is, the value closest to the {@code from} argument) for which it returns {@code true}.
* If no value satisfies the predicate, the highest value (that is, the value given for the {@code to} argument) is returned.
* The value returned by each {@code find()} method is always within the range {@code [from, to]}.
* <p>
* In order to be well-defined, the predicate must be <em>continuous</em>, which is to say that
* given some value {@code n} which is the first value in the interval which satisfies the predicate,
* the predicate must return {@code false} for all {@code < n} and {@code true} for all {@code >= n}
* over the interval of {@code [from, to)}.
* If this constraint does not hold, then the results of the search will not be well-defined.
* The behavior of the predicate outside of this range does not affect the well-definedness of the search operation.
*
* <h3>Inverted ranges</h3>
*
* It is possible to search over an inverted range, i.e.
* a range for which {@code to} is numerically lower than {@code from}.
* While such searches are well-defined, it should be noted that the {@code from} bound
* remains inclusive while the {@code to} bound remains exclusive,
* which might be surprising in some circumstances.
*/
public final class BinarySearch {
private BinarySearch() {
}

/**
* {@return an object which can perform binary searches over an integer range (not <code>null</code>)}
* The returned object operates on a signed range by default.
*
* @see IntRange#unsigned()
*/
public static IntRange intRange() {
return IntRange.signed;
}

/**
* {@return an object which can perform binary searches over a long integer range (not <code>null</code>)}
* The returned object operates on a signed range by default.
*
* @see LongRange#unsigned()
*/
public static LongRange longRange() {
return LongRange.signed;
}

/**
* {@return an object which can perform binary searches over an object range (not <code>null</code>)}
*/
public static ObjRange objRange() {
return ObjRange.instance;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package io.smallrye.common.binarysearch;

import java.util.Comparator;
import java.util.function.BiPredicate;
import java.util.function.IntBinaryOperator;
import java.util.function.IntPredicate;
import java.util.function.Predicate;

import io.smallrye.common.function.ObjIntFunction;
import io.smallrye.common.function.ObjIntPredicate;

/**
* Binary searches over an integer range which use a customized midpoint function.
*
* @see IntRange#customMidpoint()
*/
public final class CustomMidpointIntRange {
private CustomMidpointIntRange() {
}

static final CustomMidpointIntRange instance = new CustomMidpointIntRange();

public int find(int from, int to, IntBinaryOperator midpoint, IntPredicate test) {
return find(test, from, to, midpoint, IntPredicate::test);
}

public <C> int find(C collection, int from, int to, IntBinaryOperator midpoint, ObjIntPredicate<C> test) {
int low = from;
int high = to;

int mid = midpoint.applyAsInt(low, high);
int newMid;
for (;;) {
if (test.test(collection, mid)) {
high = mid;
newMid = midpoint.applyAsInt(low, high);
if (mid == newMid) {
return low;
}
} else {
low = mid;
newMid = midpoint.applyAsInt(low, high);
if (mid == newMid) {
return high;
}
}
mid = newMid;
}
}

public <C, K> int find(C collection, int from, int to, IntBinaryOperator midpoint, ObjIntFunction<C, K> keyExtractor,
Predicate<K> test) {
return find(collection, test, from, to, midpoint, keyExtractor, Predicate::test);
}

public <C, K, V> int find(C collection, V searchVal, int from, int to, IntBinaryOperator midpoint,
ObjIntFunction<C, K> keyExtractor, BiPredicate<V, K> keyTester) {
return find(collection, from, to, midpoint, (c, i) -> keyTester.test(searchVal, keyExtractor.apply(c, i)));
}

public <C, K> int findFirst(C collection, K searchKey, int from, int to, IntBinaryOperator midpoint,
ObjIntFunction<C, K> keyExtractor, Comparator<? super K> cmp) {
return find(collection, searchKey, from, to, midpoint, keyExtractor, (v, k) -> cmp.compare(v, k) >= 0);
}

public <C, K extends Comparable<? super K>> int findFirst(C collection, K searchKey, int from, int to,
IntBinaryOperator midpoint, ObjIntFunction<C, K> keyExtractor) {
return findFirst(collection, searchKey, from, to, midpoint, keyExtractor, Comparator.naturalOrder());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.smallrye.common.binarysearch;

import java.util.Comparator;
import java.util.function.BiPredicate;
import java.util.function.LongBinaryOperator;
import java.util.function.LongPredicate;
import java.util.function.Predicate;

import io.smallrye.common.function.ObjLongFunction;
import io.smallrye.common.function.ObjLongPredicate;

/**
*
*/
public final class CustomMidpointLongRange {
private CustomMidpointLongRange() {
}

static final CustomMidpointLongRange instance = new CustomMidpointLongRange();

public long find(long from, long to, LongBinaryOperator midpoint, LongPredicate test) {
return find(test, from, to, midpoint, LongPredicate::test);
}

public <C> long find(C collection, long from, long to, LongBinaryOperator midpoint, ObjLongPredicate<C> test) {
long low = from;
long high = to;

long mid = midpoint.applyAsLong(low, high);
long newMid;
for (;;) {
if (test.test(collection, mid)) {
high = mid;
newMid = midpoint.applyAsLong(low, high);
if (mid == newMid) {
return low;
}
} else {
low = mid;
newMid = midpoint.applyAsLong(low, high);
if (mid == newMid) {
return high;
}
}
mid = newMid;
}
}

public <C, K> long find(C collection, long from, long to, LongBinaryOperator midpoint, ObjLongFunction<C, K> keyExtractor,
Predicate<K> test) {
return find(collection, test, from, to, midpoint, keyExtractor, Predicate::test);
}

public <C, K, V> long find(C collection, V searchVal, long from, long to, LongBinaryOperator midpoint,
ObjLongFunction<C, K> keyExtractor, BiPredicate<V, K> keyTester) {
return find(collection, from, to, midpoint, (c, i) -> keyTester.test(searchVal, keyExtractor.apply(c, i)));
}

public <C, K> long findFirst(C collection, K searchKey, long from, long to, LongBinaryOperator midpoint,
ObjLongFunction<C, K> keyExtractor, Comparator<? super K> cmp) {
return find(collection, searchKey, from, to, midpoint, keyExtractor, (v, k) -> cmp.compare(v, k) >= 0);
}

public <C, K extends Comparable<? super K>> long findFirst(C collection, K searchKey, long from, long to,
LongBinaryOperator midpoint, ObjLongFunction<C, K> keyExtractor) {
return findFirst(collection, searchKey, from, to, midpoint, keyExtractor, Comparator.naturalOrder());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package io.smallrye.common.binarysearch;

import java.util.Comparator;
import java.util.function.BiPredicate;
import java.util.function.IntBinaryOperator;
import java.util.function.IntPredicate;
import java.util.function.Predicate;

import io.smallrye.common.function.ObjIntFunction;
import io.smallrye.common.function.ObjIntPredicate;

/**
* Binary searches over an integer range.
*
* @see BinarySearch#intRange()
*/
public final class IntRange {
private final IntBinaryOperator midpoint;

private IntRange(IntBinaryOperator midpoint) {
this.midpoint = midpoint;
}

static final IntRange signed = new IntRange(IntRange::signedMidpoint);
static final IntRange unsigned = new IntRange(IntRange::unsignedMidpoint);

private static int signedMidpoint(int from, int to) {
return from + (to - from >> 1);
}

private static int unsignedMidpoint(int from, int to) {
return from + (to - from >>> 1);
}

/**
* Get the lowest index within the given range that satisfies the given test.
*
* @param from the low end of the range (inclusive)
* @param to the high end of the range (exclusive)
* @param test the test (must not be {@code null})
* @return the lowest index within the range which satisfies the test, or {@code to} if no values satisfy the range
*/
public int find(int from, int to, IntPredicate test) {
return find(test, from, to, IntPredicate::test);
}

public <C> int find(C collection, int from, int to, ObjIntPredicate<C> test) {
return customMidpoint().find(collection, from, to, midpoint, test);
}

public <C, K> int find(C collection, int from, int to, ObjIntFunction<C, K> keyExtractor, Predicate<K> test) {
return customMidpoint().find(collection, from, to, midpoint, keyExtractor, test);
}

public <C, K, V> int find(C collection, V searchVal, int from, int to, ObjIntFunction<C, K> keyExtractor,
BiPredicate<V, K> keyTester) {
return customMidpoint().find(collection, searchVal, from, to, midpoint, keyExtractor, keyTester);
}

public <C, K> int findFirst(C collection, K searchKey, int from, int to, ObjIntFunction<C, K> keyExtractor,
Comparator<? super K> cmp) {
return customMidpoint().findFirst(collection, searchKey, from, to, midpoint, keyExtractor, cmp);
}

public <C, K extends Comparable<? super K>> int findFirst(C collection, K searchKey, int from, int to,
ObjIntFunction<C, K> keyExtractor) {
return customMidpoint().findFirst(collection, searchKey, from, to, midpoint, keyExtractor);
}

/**
* {@return an object which can perform binary searches over a signed range (not <code>null</code>)}
*/
public IntRange signed() {
return signed;
}

/**
* {@return an object which can perform binary searches over an unsigned range (not <code>null</code>)}
*/
public IntRange unsigned() {
return unsigned;
}

/**
* {@return an object which performs binary searches over a range defined by a custom midpoint function (not
* <code>null</code>)}
*/
public CustomMidpointIntRange customMidpoint() {
return CustomMidpointIntRange.instance;
}
}
Loading

0 comments on commit e9d3050

Please sign in to comment.