私はいくつかのコードをいじくり回していて、私が知らなかった何かに気づきました。通常のバイナリ検索では、複数回発生するキーのデータセット内のランダムなインデックスが返されます。以下のコードを変更して最初の出現を返すにはどうすればよいですか?これは人々がすることですか?
//ripped from the JDK
public static int binarySearchValue(InvertedContainer.InvertedIndex[] a, long key) {
return bSearchVal(a, 0, a.length, key);
}
private static int bSearchVal(InvertedContainer.InvertedIndex[] a, int fromIndex,
int toIndex, long key) {
int low = fromIndex;
int high = toIndex - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
long midVal = a[mid].val;
if (midVal < key)
low = mid + 1;
else if (midVal > key)
high = mid - 1;
else
return mid; // key found
}
return (low); // key not found. return insertion point
}
a一致する値が見つかったら、基本的に、一致しない一致するエントリが見つかるまでコレクションをたどる必要があります。
潜在的に探しているキーのすぐ下のキーのインデックスをフェッチして、2つの間でバイナリチョップを実行することで高速化できますが、おそらくもっと単純なバージョンを選択しますが、これは、非常に多くの等しいエントリがない限り、「十分に効率的」である可能性があります。
Jon Skeetsの投稿への追加:
potentialより高速な実装は実際には実装が難しくなく、2行のコードしか追加されません。これが私が行う方法です。
if (midVal < key)
low = mid + 1;
else if (midVal > key)
high = mid - 1;
else if (low != mid) //Equal but range is not fully scanned
high = mid; //Set upper bound to current number and rescan
else //Equal and full range is scanned
return mid;
マッチングの定義をより明確にするだけで、既存の検索アルゴリズムを適応させることができます。シーケンス1、3、で強調表示された5は、5、5,5,9は、その前の数(3)が5より小さいため、最初のものです。したがって、midがキーに等しい配列要素に到達した場合、a [mid-1]が一致した場合にのみ一致として扱います。キーよりも小さい場合、他の等しい配列要素は要素よりも大きいものとして扱われます。これで、アルゴリズムは次のようになります(挿入ポイントのネガを返すというJon Skeetの提案を含めた後):
public static int binarySearch(int[] a, int key) {
int low=0,high=a.length-1;
while (low<=high) {
int mid=(low+high) >>> 1;
int midVal=a[mid];
if (midVal < key)
low=mid+1;
else if (mid>0 && a[mid-1]>=key) //we already know midval>=key here
high=mid-1;
else if (midVal==key) //found the 1st key
return mid;
else
return ~mid; //found insertion point
}
return ~(a.length); //insertion point after everything
}
より多くの比較を使用しますが、おそらくキャッシュ効果のために、ベンチマークでStev314のバージョンよりも高速になりました。
二分探索の代わりに「下限」アルゴリズムを実装できます。このアルゴリズムが使用されます。 in C++/STL およびそのトランスクリプトJavaは簡単です。下限のアルゴリズムの複雑さも、バイナリ検索としてO(log n)です。これはより優れています。最初にバイナリ検索を使用し、最初に一致する要素を線形検索するよりも、これは最悪の場合の動作O(n)になります。
データがすべて不可欠な場合は、このハックが役立ちます。 float配列を使用して値を格納します。
float array[]; //contains all integral values
int searchValue;
int firstIndex = -(binarySearch(array, (float)searchValue - 0.5F) + 1);
基本的には、検索値とその前の整数の間にある値の挿入インデックスを見つけます。すべての値は整数であるため、検索値の最初の出現を検索します。
また、この実行はlog(n)時間です。
例:
import Java.util.Arrays;
public class BinarySearch {
// considering array elements are integers
float ar[] = new float[] { 1, 2, 3, 3, 4, 4, 5, 9, 9, 12, 12 };
public void returnFirstOccurrence(int key) {
int firstIndex = -(Arrays.binarySearch(ar, key - 0.5F) + 1);
if (ar[firstIndex] != key)
System.out.println("Key doesn't exist");
else
System.out.println("First index of key is " + firstIndex);
}
public static void main(String Args[]) throws Exception {
new BinarySearch().returnFirstOccurrence(9);
}
}
出力:7
p.s:私はこのトリックをいくつかのコーディングコンテストで使用しましたが、毎回うまく機能しました。
次のアルゴリズムは、検索キーと同じかそれ以上のキーを持つ最初のアイテムをバイナリ検索します...
while (upperbound > lowerbound)
{
testpos = lowerbound + ((upperbound-lowerbound) / 2);
if (item[testpos] >= goal)
{
// new best-so-far
upperbound = testpos;
}
else
{
lowerbound = testpos + 1;
}
}
これはJava用に書かれていませんが、私にはよくわかりません。そのため、微調整が必要になる場合があります。境界は半分開いており(下限は包括的で上限は排他的)、これは正確さのために重要であることに注意してください。
これは、他の同様の検索に適合させることができます-例:最後のキーを見つける<=検索値。
これは私の以前の質問と回答から少し変更されています ここ 。
1つのアプローチは、バイナリ検索全体を通して不変条件を保持することです。特定のケースでは、不変条件は次のようになります。
array[low] < key
key <= array[high]
次に、バイナリ検索を使用して、低と高の間のギャップを最小限に抑えることができます。 low + 1 == high
の場合、high
が答えになります。 C++のサンプルコード:
// check invariant on initial values.
if (array[low] >= key) return low;
if (array[high] < key) return high+1;
// low + 1 < high ensures high is at least low + 2, thus
// mid will always be different from low or high. It will
// stop when low + 1 == high.
while (low + 1 < high) {
int mid = low + (high - low) / 2;
if (array[mid] < key) {
low = mid; // invariant: array[low] < key
} else {
high = mid; // invariant: array[high] >= key
}
}
return high;
これとサンプルコードの主な違いは、low
とhigh
をmid+1
やmid-1
ではなく、mid
のみに更新することです。 array[mid]
の値を指定すると、境界を更新するときに不変条件が保持されることを保証できます。検索を開始する前に、初期値の不変条件も確認する必要があります。
この スレッド では、バイナリ検索の完全な例(再帰バージョン)と他の2つのバージョン(ベース元のインデックス)を使用すると、特定のキーの最初のインデックスと最後のインデックスを取得できます。
便宜上、関連するJUnitテストを追加しました。
これが解決策です。バイナリ検索を使用して、並べ替えられた配列で複数回出現するキーの下位インデックスを取得するために見つけました。
int lowerBound(int[] array,int fromIndex, int toIndex, int key)
{
int low = fromIndex-1, high = toIndex;
while (low+1 != high)
{
int mid = (low+high)>>>1;
if (array[mid]< key) low=mid;
else high=mid;
}
int p = high;
if ( p >= toIndex || array[p] != key )
p=-1;//no key found
return p;
}
バイナリ検索を使用して、上限で機能するようにこのコードを少し変更する必要があるため、コードの作業コピーを次に示します。
int upperBound(int[] array,int fromIndex, int toIndex, int key)
{
int low = fromIndex-1, high = toIndex;
while (low+1 != high)
{
int mid = (low+high)>>>1;
if (array[mid]> key) high=mid;
else low=mid;
}
int p = low;
if ( p >= toIndex || array[p] != key )
p=-1;//no key found
return p;
}
より簡単なアプローチは、最新のmid
インデックスを格納することだと思います。ここでxs[mid] == key
を結果変数に入れてから、バイナリ検索を実行し続けます。
これがSwiftコード:
func first<T: Comparable>(xs: [T], key: T) -> Int {
var lo = xs.startIndex
var hi = xs.endIndex - 1
var res = -1
while lo <= hi {
let mid = lo + (hi - lo) >> 1
if xs[mid] == key { hi = mid - 1; res = mid }
else if xs[mid] < key { lo = mid + 1}
else if xs[mid] > key { hi = mid - 1 }
}
return res
}
また、キーの最後のインデックスを見つける場合、これには非常に小さな変更(1行のみ)が必要です。
func last<T: Comparable>(xs: [T], key: T) -> Int {
var lo = xs.startIndex
var hi = xs.endIndex - 1
var res = -1
while lo <= hi {
let mid = lo + (hi - lo) >> 1
if xs[mid] == key { lo = mid + 1; res = mid }
else if xs[mid] < key { lo = mid + 1}
else if xs[mid] > key { hi = mid - 1 }
}
return res
}
これはトリックを行う必要があります
private static int bSearchVal(InvertedContainer.InvertedIndex[] a, int fromIndex,
int toIndex, long key) {
int low = fromIndex;
int high = toIndex - 1;
int result = low;
while (low <= high) {
int mid = (low + high) >>> 1;
long midVal = a[mid].val;
if (midVal < key)
low = mid + 1;
else if (midVal > key)
high = mid - 1;
else
{
result = mid;
high = mid -1;
}
}
return result;
}
要素の最後の出現について:
static int elementExists(int input[], int element){
int lo=0;
int high = input.length-1;
while(lo<high){
int mid = (lo + high )/2;
if(element >input[mid] ){
lo = mid+1;
}
else if(element < input[mid]){
high= mid-1;
}
else if (high != input.length-1) //Change for the Occurrence check
lo = mid;
else {
return mid;
}
}
return -1;
}
最初の発生の場合:
else if (lo != mid){
high = mid;
}
これがscalaのソリューションのバリエーションです。 whileループの代わりにパターンマッチングと再帰を使用して、最初のオカレンスを取得しました。
def binarySearch(arr:Array[Int],key:Int):Int = {
def binSearchHelper(lo:Int,hi:Int,mid:Int):Int = {
if(lo > hi) -1 else {
if(arr(mid) == key) mid else if(arr(mid) > key){
binSearchHelper(lo,mid-1,lo + (((mid-1) - lo)/2))
}else{
binSearchHelper(mid+1,hi,(mid+1) + ((hi - (mid+1))/2))
}
}
}
binSearchHelper(0,arr.size-1,(arr.size-1)/2)
}
def findFirstOccurrence(arr:Array[Int],key:Int):Int = {
val startIdx = binarySearch(arr,key)
startIdx match {
case 0 => 0
case -1 => -1
case _ if startIdx > 0 => {
if(arr(startIdx - 1) < key) startIdx else {
findFirstOccurrence(arr.slice(0,startIdx),key)
}
}
}
}
このjavascript再帰ソリューションを試してください。 O(log(N))という意味で最適です
function solve(A, e) {
function solve (A, start, end, e, bestUntilNow) {
if (start > end) {
if (A[start] === e)
return start
return bestUntilNow
} else {
const mid = start + Math.floor((end - start) / 2)
if (A[mid] === e) {
return solve(A, start, mid - 1, e, mid)
} else if (e < A[mid]) {
return solve(A, start, mid - 1, e, bestUntilNow)
} else {
return solve(A, mid + 1, end, e, bestUntilNow)
}
}
}
return solve(A, 0, A.length, e, -1)
}