191 lines
6.0 KiB
Kotlin
191 lines
6.0 KiB
Kotlin
/*
|
|
* Copyright 2023 dorkbox, llc
|
|
*
|
|
* Licensed 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.
|
|
*/
|
|
|
|
/*
|
|
* The MIT License
|
|
*
|
|
* Copyright 2013 Tim Boudreau.
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*/
|
|
package dorkbox.collections
|
|
|
|
import kotlin.math.abs
|
|
|
|
/**
|
|
* General-purpose binary search algorithm; you pass in an array or list
|
|
* wrapped in an instance of `Indexed`, and an
|
|
* `Evaluator` which converts the contents of the list into
|
|
* numbers used by the binary search algorithm. Note that the data
|
|
* (as returned by the Indexed array/list) ***must be in order from
|
|
* low to high***. The indices need not be contiguous (presumably
|
|
* they are not or you wouldn't be using this class), but they must be
|
|
* sorted. If assertions are enabled, this is enforced; if not, very
|
|
* bad things (endless loops, etc.) can happen as a consequence of passing
|
|
* unsorted data in.
|
|
*
|
|
*
|
|
* This class is not thread-safe and the size and contents of the `Indexed`
|
|
* should not change while a search is being performed.
|
|
*
|
|
* @author Tim Boudreau
|
|
*/
|
|
class BinarySearch<T>(private val eval: Evaluator<T>, private val indexed: Indexed<T>) {
|
|
companion object {
|
|
const val version = Collections.version
|
|
}
|
|
|
|
/**
|
|
* Create a new binary search.
|
|
*
|
|
* @param eval The thing which converts elements into numbers
|
|
* @param indexed A collection, list or array
|
|
*/
|
|
init {
|
|
assert(checkSorted())
|
|
}
|
|
|
|
constructor(eval: Evaluator<T>, l: List<T>) : this(eval, ListWrap<T>(l))
|
|
|
|
private fun checkSorted(): Boolean {
|
|
var `val` = Long.MIN_VALUE
|
|
val sz = indexed.size()
|
|
for (i in 0 until sz) {
|
|
val t = indexed[i]
|
|
val nue = eval.getValue(t)
|
|
if (`val` != Long.MIN_VALUE) {
|
|
if (nue < `val`) { throw StateException("Collection is not sorted at " + i + " - " + indexed) }
|
|
}
|
|
`val` = nue
|
|
}
|
|
return true
|
|
}
|
|
|
|
fun search(value: Long, bias: Bias): Long {
|
|
return search(0, indexed.size() - 1, value, bias)
|
|
}
|
|
|
|
fun match(prototype: T, bias: Bias): T? {
|
|
val value = eval.getValue(prototype)
|
|
val index = search(value, bias)
|
|
return if (index == -1L) null else indexed[index]
|
|
}
|
|
|
|
fun searchFor(value: Long, bias: Bias): T? {
|
|
val index = search(value, bias)
|
|
return if (index == -1L) null else indexed[index]
|
|
}
|
|
|
|
private fun search(start: Long, end: Long, value: Long, bias: Bias): Long {
|
|
val range = end - start
|
|
if (range == 0L) {
|
|
return start
|
|
}
|
|
if (range == 1L) {
|
|
val ahead = indexed[end]
|
|
val behind = indexed[start]
|
|
val v1 = eval.getValue(behind)
|
|
val v2 = eval.getValue(ahead)
|
|
return when (bias) {
|
|
Bias.BACKWARD -> start
|
|
Bias.FORWARD -> end
|
|
Bias.NEAREST -> if (v1 == value) {
|
|
start
|
|
}
|
|
else if (v2 == value) {
|
|
end
|
|
}
|
|
else {
|
|
if (abs((v1 - value).toDouble()) < abs((v2 - value).toDouble())) {
|
|
start
|
|
}
|
|
else {
|
|
end
|
|
}
|
|
}
|
|
|
|
Bias.NONE -> if (v1 == value) {
|
|
start
|
|
}
|
|
else if (v2 == value) {
|
|
end
|
|
}
|
|
else {
|
|
-1
|
|
}
|
|
}
|
|
|
|
}
|
|
val mid = start + range / 2
|
|
val vm = eval.getValue(indexed[mid])
|
|
|
|
return if (value >= vm) {
|
|
search(mid, end, value, bias)
|
|
}
|
|
else {
|
|
search(start, mid, value, bias)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts an object into a numeric value that is used to
|
|
* perform binary search
|
|
* @param <T>
|
|
</T> */
|
|
interface Evaluator<T> {
|
|
fun getValue(obj: T): Long
|
|
}
|
|
|
|
/**
|
|
* Abstraction for list-like things which have a length and indices
|
|
* @param <T>
|
|
</T> */
|
|
interface Indexed<T> {
|
|
operator fun get(index: Long): T
|
|
fun size(): Long
|
|
}
|
|
|
|
private class ListWrap<T> internal constructor(private val l: List<T>) : Indexed<T> {
|
|
override fun get(index: Long): T {
|
|
return l[index.toInt()]
|
|
}
|
|
|
|
override fun size(): Long {
|
|
return l.size.toLong()
|
|
}
|
|
|
|
override fun toString(): String {
|
|
return super.toString() + '{' + l + '}'
|
|
}
|
|
}
|
|
}
|