-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Add ArrayUtils.binarySearch() with a key extractor #1270
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
kvr000
wants to merge
3
commits into
apache:master
Choose a base branch
from
kvr000:sorted-list-binary-search
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1432,6 +1432,221 @@ public static <T> T arraycopy(final T source, final int sourcePos, final T dest, | |
return dest; | ||
} | ||
|
||
/** | ||
* Searches element in array sorted by key. If there are multiple elements matching, it returns first occurrence. | ||
* If the array is not sorted, the result is undefined. | ||
* | ||
* @param array | ||
* array sorted by key field | ||
* @param key | ||
* key to search for | ||
* @param keyExtractor | ||
* function to extract key from element | ||
* @param comparator | ||
* comparator for keys | ||
* | ||
* @return | ||
* index of the first occurrence of search key, if it is contained in the array; otherwise, | ||
* (-first_greater - 1). The first_greater is the index of lowest greater element in the list - if all elements | ||
* are lower, the first_greater is defined as array.length. | ||
* | ||
* @param <T> | ||
* type of array element | ||
* @param <K> | ||
* type of key | ||
*/ | ||
public static <K, T> int binarySearchFirst( | ||
T[] array, | ||
K key, | ||
Function<T, K> keyExtractor, Comparator<? super K> comparator | ||
) { | ||
return binarySearchFirst0(array, 0, array.length, key, keyExtractor, comparator); | ||
} | ||
|
||
/** | ||
* Searches element in array sorted by key, within range fromIndex (inclusive) - toIndex (exclusive). If there are | ||
* multiple elements matching, it returns first occurrence. If the array is not sorted, the result is undefined. | ||
* | ||
* @param array | ||
* array sorted by key field | ||
* @param fromIndex | ||
* start index (inclusive) | ||
* @param toIndex | ||
* end index (exclusive) | ||
* @param key | ||
* key to search for | ||
* @param keyExtractor | ||
* function to extract key from element | ||
* @param comparator | ||
* comparator for keys | ||
* | ||
* @return | ||
* index of the first occurrence of search key, if it is contained in the array within specified range; | ||
* otherwise, (-first_greater - 1). The first_greater is the index of lowest greater element in the list - if | ||
* all elements are lower, the first_greater is defined as toIndex. | ||
* | ||
* @throws ArrayIndexOutOfBoundsException | ||
* when fromIndex or toIndex is out of array range | ||
* @throws IllegalArgumentException | ||
* when fromIndex is greater than toIndex | ||
* | ||
* @param <T> | ||
* type of array element | ||
* @param <K> | ||
* type of key | ||
*/ | ||
public static <T, K> int binarySearchFirst( | ||
T[] array, | ||
int fromIndex, int toIndex, | ||
K key, | ||
Function<T, K> keyExtractor, Comparator<? super K> comparator | ||
) { | ||
checkRange(array.length, fromIndex, toIndex); | ||
|
||
return binarySearchFirst0(array, fromIndex, toIndex, key, keyExtractor, comparator); | ||
} | ||
|
||
// common implementation for binarySearch methods, with same semantics: | ||
private static <T, K> int binarySearchFirst0( | ||
T[] array, | ||
int fromIndex, int toIndex, | ||
K key, | ||
Function<T, K> keyExtractor, Comparator<? super K> comparator | ||
) { | ||
int l = fromIndex; | ||
int h = toIndex - 1; | ||
|
||
while (l <= h) { | ||
final int m = (l + h) >>> 1; // unsigned shift to avoid overflow | ||
final K value = keyExtractor.apply(array[m]); | ||
final int c = comparator.compare(value, key); | ||
if (c < 0) { | ||
l = m + 1; | ||
} else if (c > 0) { | ||
h = m - 1; | ||
} else if (l < h) { | ||
// possibly multiple matching items remaining: | ||
h = m; | ||
} else { | ||
// single matching item remaining: | ||
return m; | ||
} | ||
} | ||
|
||
// not found, the l points to the lowest higher match: | ||
return -l - 1; | ||
} | ||
|
||
/** | ||
* Searches element in array sorted by key. If there are multiple elements matching, it returns last occurrence. | ||
* If the array is not sorted, the result is undefined. | ||
* | ||
* @param array | ||
* array sorted by key field | ||
* @param key | ||
* key to search for | ||
* @param keyExtractor | ||
* function to extract key from element | ||
* @param comparator | ||
* comparator for keys | ||
* | ||
* @return | ||
* index of the last occurrence of search key, if it is contained in the array; otherwise, | ||
* (-first_greater - 1). The first_greater is the index of lowest greater element in the list - if all elements | ||
* are lower, the first_greater is defined as array.length. | ||
* | ||
* @param <T> | ||
* type of array element | ||
* @param <K> | ||
* type of key | ||
*/ | ||
public static <K, T> int binarySearchLast( | ||
T[] array, | ||
K key, | ||
Function<T, K> keyExtractor, Comparator<? super K> comparator | ||
) { | ||
return binarySearchLast0(array, 0, array.length, key, keyExtractor, comparator); | ||
} | ||
|
||
/** | ||
* Searches element in array sorted by key, within range fromIndex (inclusive) - toIndex (exclusive). If there are | ||
* multiple elements matching, it returns last occurrence. If the array is not sorted, the result is undefined. | ||
* | ||
* @param array | ||
* array sorted by key field | ||
* @param fromIndex | ||
* start index (inclusive) | ||
* @param toIndex | ||
* end index (exclusive) | ||
* @param key | ||
* key to search for | ||
* @param keyExtractor | ||
* function to extract key from element | ||
* @param comparator | ||
* comparator for keys | ||
* | ||
* @return | ||
* index of the last occurrence of search key, if it is contained in the array within specified range; | ||
* otherwise, (-first_greater - 1). The first_greater is the index of lowest greater element in the list - if | ||
* all elements are lower, the first_greater is defined as toIndex. | ||
* | ||
* @throws ArrayIndexOutOfBoundsException | ||
* when fromIndex or toIndex is out of array range | ||
* @throws IllegalArgumentException | ||
* when fromIndex is greater than toIndex | ||
* | ||
* @param <T> | ||
* type of array element | ||
* @param <K> | ||
* type of key | ||
*/ | ||
public static <T, K> int binarySearchLast( | ||
T[] array, | ||
int fromIndex, int toIndex, | ||
K key, | ||
Function<T, K> keyExtractor, Comparator<? super K> comparator | ||
) { | ||
checkRange(array.length, fromIndex, toIndex); | ||
|
||
return binarySearchLast0(array, fromIndex, toIndex, key, keyExtractor, comparator); | ||
} | ||
|
||
// common implementation for binarySearch methods, with same semantics: | ||
private static <T, K> int binarySearchLast0( | ||
T[] array, | ||
int fromIndex, int toIndex, | ||
K key, | ||
Function<T, K> keyExtractor, Comparator<? super K> comparator | ||
) { | ||
int l = fromIndex; | ||
int h = toIndex - 1; | ||
|
||
while (l <= h) { | ||
final int m = (l + h) >>> 1; // unsigned shift to avoid overflow | ||
final K value = keyExtractor.apply(array[m]); | ||
final int c = comparator.compare(value, key); | ||
if (c < 0) { | ||
l = m + 1; | ||
} else if (c > 0) { | ||
h = m - 1; | ||
} else if (m + 1 < h) { | ||
// matching, more than two items remaining: | ||
l = m; | ||
} else if (m + 1 == h) { | ||
// two items remaining, next loops would result in unchanged l and h, we have to choose m or h: | ||
final K valueH = keyExtractor.apply(array[h]); | ||
final int cH = comparator.compare(valueH, key); | ||
return cH == 0 ? h : m; | ||
} else { | ||
// one item remaining, single match: | ||
return m; | ||
} | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if there are no higher matches? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Then |
||
// not found, the l points to the lowest higher match: | ||
return -l - 1; | ||
} | ||
|
||
/** | ||
* Clones an array or returns {@code null}. | ||
* <p> | ||
|
@@ -9464,4 +9679,18 @@ public static String[] toStringArray(final Object[] array, final String valueFor | |
public ArrayUtils() { | ||
// empty | ||
} | ||
|
||
static void checkRange(int length, int fromIndex, int toIndex) { | ||
if (fromIndex > toIndex) { | ||
throw new IllegalArgumentException( | ||
"fromIndex(" + fromIndex + ") > toIndex(" + toIndex + ")"); | ||
} | ||
if (fromIndex < 0) { | ||
throw new ArrayIndexOutOfBoundsException(fromIndex); | ||
} | ||
if (toIndex > length) { | ||
throw new ArrayIndexOutOfBoundsException(toIndex); | ||
} | ||
|
||
} | ||
} |
145 changes: 145 additions & 0 deletions
145
src/test/java/org/apache/commons/lang3/ArrayUtilsBinarySearchTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
/* | ||
* 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.lang3; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.junit.jupiter.api.Assertions.assertThrowsExactly; | ||
|
||
import java.util.stream.IntStream; | ||
|
||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.Timeout; | ||
|
||
/** | ||
* Unit tests {@link ArrayUtils} binarySearch functions. | ||
*/ | ||
public class ArrayUtilsBinarySearchTest extends AbstractLangTest { | ||
|
||
@Test | ||
public void binarySearchFirst_whenLowHigherThanEnd_throw() { | ||
final Data[] list = createList(0, 1); | ||
assertThrowsExactly(IllegalArgumentException.class, () -> | ||
ArrayUtils.binarySearchFirst(list, 1, 0, 0, Data::getValue, Integer::compare)); | ||
} | ||
|
||
@Test | ||
public void binarySearchFirst_whenLowNegative_throw() { | ||
final Data[] list = createList(0, 1); | ||
assertThrowsExactly(ArrayIndexOutOfBoundsException.class, () -> | ||
ArrayUtils.binarySearchFirst(list, -1, 0, 0, Data::getValue, Integer::compare)); | ||
} | ||
|
||
@Test | ||
public void binarySearchFirst_whenEndBeyondLength_throw() { | ||
final Data[] list = createList(0, 1); | ||
assertThrowsExactly(ArrayIndexOutOfBoundsException.class, () -> | ||
ArrayUtils.binarySearchFirst(list, 0, 3, 0, Data::getValue, Integer::compare)); | ||
} | ||
|
||
@Test | ||
public void binarySearchLast_whenLowHigherThanEnd_throw() { | ||
final Data[] list = createList(0, 1); | ||
assertThrowsExactly(IllegalArgumentException.class, () -> | ||
ArrayUtils.binarySearchLast(list, 1, 0, 0, Data::getValue, Integer::compare)); | ||
} | ||
|
||
@Test | ||
public void binarySearchFirst_whenEmpty_returnM1() { | ||
final Data[] list = createList(); | ||
final int found = ArrayUtils.binarySearchFirst(list, 0, Data::getValue, Integer::compare); | ||
assertEquals(-1, found); | ||
} | ||
|
||
@Test | ||
public void binarySearchFirst_whenExists_returnIndex() { | ||
final Data[] list = createList(0, 1, 2, 4, 7, 9, 12, 15, 17, 19, 25); | ||
final int found = ArrayUtils.binarySearchFirst(list, 9, Data::getValue, Integer::compare); | ||
assertEquals(5, found); | ||
} | ||
|
||
@Test | ||
@Timeout(10) | ||
public void binarySearchFirst_whenMultiple_returnFirst() { | ||
final Data[] list = createList(3, 4, 6, 6, 6, 7, 7, 8, 8, 9, 9, 9); | ||
for (int i = 0; i < list.length; ++i) { | ||
if (i > 0 && list[i].value == list[i - 1].value) { | ||
continue; | ||
} | ||
final int found = ArrayUtils.binarySearchFirst(list, list[i].value, Data::getValue, Integer::compare); | ||
assertEquals(i, found); | ||
} | ||
} | ||
|
||
@Test | ||
@Timeout(10) | ||
public void binarySearchLast_whenMultiple_returnFirst() { | ||
final Data[] list = createList(3, 4, 6, 6, 6, 7, 7, 8, 8, 9, 9, 9); | ||
for (int i = 0; i < list.length; ++i) { | ||
if (i < list.length - 1 && list[i].value == list[i + 1].value) { | ||
continue; | ||
} | ||
final int found = ArrayUtils.binarySearchLast(list, list[i].value, Data::getValue, Integer::compare); | ||
assertEquals(i, found); | ||
} | ||
} | ||
|
||
@Test | ||
public void binarySearchFirst_whenNotExistsMiddle_returnMinusInsertion() { | ||
final Data[] list = createList(0, 1, 2, 4, 7, 9, 12, 15, 17, 19, 25); | ||
final int found = ArrayUtils.binarySearchFirst(list, 8, Data::getValue, Integer::compare); | ||
assertEquals(-6, found); | ||
} | ||
|
||
@Test | ||
public void binarySearchFirst_whenNotExistsBeginning_returnMinus1() { | ||
final Data[] list = createList(0, 1, 2, 4, 7, 9, 12, 15, 17, 19, 25); | ||
final int found = ArrayUtils.binarySearchFirst(list, -3, Data::getValue, Integer::compare); | ||
assertEquals(-1, found); | ||
} | ||
|
||
@Test | ||
public void binarySearchFirst_whenNotExistsEnd_returnMinusLength() { | ||
final Data[] list = createList(0, 1, 2, 4, 7, 9, 12, 15, 17, 19, 25); | ||
final int found = ArrayUtils.binarySearchFirst(list, 29, Data::getValue, Integer::compare); | ||
assertEquals(-(list.length + 1), found); | ||
} | ||
|
||
@Test | ||
@Timeout(10) | ||
public void binarySearchFirst_whenUnsorted_dontInfiniteLoop() { | ||
final Data[] list = createList(7, 1, 4, 9, 11, 8); | ||
final int found = ArrayUtils.binarySearchFirst(list, 10, Data::getValue, Integer::compare); | ||
} | ||
|
||
private Data[] createList(int... values) { | ||
return IntStream.of(values).mapToObj(Data::new) | ||
.toArray(Data[]::new); | ||
} | ||
|
||
static class Data { | ||
|
||
private final int value; | ||
|
||
Data(int value) { | ||
this.value = value; | ||
} | ||
|
||
public int getValue() { | ||
return value; | ||
} | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.