Skip to content

搜尋、二分搜尋、跳躍二分搜

前提

distance(it_a , it_b);
計算兩個迭代器之間的距離

distance(v.begin(),find(v.begin(),v.end(),5))

會回傳找到的位置

搜尋

使用於未排序之資料
函數的find為O(n)
(若在已經排序(set、map)的find為O(log n)

函數搜尋
#include<bits/stdc++.h>
using namespace std;
#define nn "\n";
int main(){
    vector<int>v;
    v.push_back(2);
    v.push_back(3);
    v.push_back(1);
    v.push_back(5);
    v.push_back(6);
    for(int i:v){
        cout<<i<<" ";
    }cout<<nn;
    cout<<"5的位置: "<< distance(v.begin(),find(v.begin(),v.end(),5));
}
2 3 1 5 6    
5的位置: 3   

二分搜尋(自製)

只可用於已排序之資料
例如:set、map、multiset、multiset、已排序陣列

二分搜尋有兩種情況:

1.要找的值在陣列中
2.要找的值不在陣列中

以下分別說明

要找的值在陣列中

如果要找的值在集合中,就會回傳index,否則回傳-1

#include<bits/stdc++.h>
using namespace std;
int by(vector<int>v,int L,int R,double x){
    while(L<=R){
        int mid=(L+R)/2;
        if(v[mid]==x){
            return mid;
        }
        else if(x<v[mid]){
            R=mid-1;
        }
        else{
            L=mid+1;
        }
    }
    return -1;
}
int main(){
    vector<int>v={11,12,13,14,15,16,17,18,19};
    int n=v.size();
    cout<<by(v,0,n,15);
}

要找的值不在陣列中

如果要找的值不在陣列中,我們或許會想要找到「所有大於x的值」,或是「所有小於x的值」。

透過以下程式碼,可以看到,如果沒有在陣列中找到對應的數值(x):

  • L為:「小於x且最接近x的數」的index+1
  • R為:「大於x且最接近x的數」的index-1
#include<bits/stdc++.h>
using namespace std;

void by(vector<int>v,int L,int R,double x){
    while(L<=R){
        int mid=(L+R)/2;
        if(v[mid]==x){
            cout<<mid;
            return;
        }
        else if(x<v[mid]){
            R=mid-1;
        }
        else{
            L=mid+1;
        }
    }
    cout<<L<<" "<<R<<"\n";
    return ;
}

int main(){
    vector<int>v={11,12,13,14,15,16,17,18,19};
    int n=v.size();
    by(v,0,n,10);   //0 -1
    by(v,0,n,16.5); //5 4
    by(v,0,n,20);   //10 9
}

這樣我們如果要找到所有小於等於x的數,可以執行以下程式。

#include<bits/stdc++.h>
using namespace std;

int by(vector<int>v,int L,int R,double x){
    while(L<=R){
        int mid=(L+R)/2;
        if(v[mid]==x){
            return mid+1;      //注意這邊要加一!!!
        }
        else if(x<v[mid]){
            R=mid-1;
        }
        else{
            L=mid+1;
        }
    }
    return L;
}

int main(){
    vector<int>v={11,12,13,14,15,16,17,18,19};
    int n=v.size();
    cout<<by(v,0,n,14.5)<<" ";   
    cout<<by(v,0,n,15);   
}

內建二分搜

種類有:
1. find
直接回傳位置
2. lower_bound
回傳大於等於n的「最小值」
3. upper_bound
回傳大於n的「最小值」

網站資料:
https://blog.csdn.net/tjpuacm/article/details/26389441

find

  1. 若資料結構為陣列,直接使用lower_bound,然後因為lower_bound包含=,再排除大於的即可。
  2. set、map:
    #include<bits/stdc++.h>
    using namespace std;
    #define nn "\n";
    int main(){
        set<int>s;
        s.insert(1);
        s.insert(5);
        s.insert(6);
        s.insert(7);
        s.insert(4);
    
        if(s.find(5)!=s.end()){
            cout<<"found";
        }
        else{
            cout<<"Not found";
        }
    
        cout<<nn;
    
        if(s.find(2)!=s.end()){
            cout<<"found";
        }
        else{
            cout<<"Not found";
        }
    }
    

lower_bound、upper_bound:O(log n)

lower的意義是對於給定的已經排好序的a,
key最早能插入到那個位置 0 1 | 2 2 3 所以2最早插入到2號位置。

upper的意義是對於給定的已經排好序的a,
key最晚能插入到那個位置 0 1 2 2 | 3 所以2最晚插入到4號位置。

陣列
#include<bits/stdc++.h>
using namespace std;
#define nn "\n";
int main(){
    vector <int> v={1,2,5,4,7,8,9};
    sort(v.begin(),v.end());
    cout<<*lower_bound(v.begin(),v.end(),7);
    cout<<nn;
    cout<<*upper_bound(v.begin(),v.end(),7);
}
7
8

sort、map
#include<bits/stdc++.h>
using namespace std;
#define nn "\n";
int main(){
    set<int>s;
    s.insert(1);
    s.insert(5);
    s.insert(6);
    s.insert(7);
    s.insert(4);
    cout<<*s.lower_bound(4);
    cout<<nn;
    cout<<*s.upper_bound(4);
}
4
5

自定義lower_bound、upper_bound

當陣列中是一個pair或是tuple時要告訴程式該依照哪一個元素來找

#include <bits/stdc++.h>
using namespace std;

bool cmp(pair<int, int> a, pair<int, int> b) {
    return a.second < b.second;   //lower_bound和upper_bound都只能用"<"
}

int main() {
    vector<pair<int, int>> v;
    v.push_back({1, 2});
    v.push_back({2, 4});
    v.push_back({3, 6});
    v.push_back({4, 8});

    pair<int, int> pa = {0, 6};//只比較後面,所以前面是0沒差,然後要用pair或是要自己定義operator<

    cout << (*lower_bound(v.begin(), v.end(), pa, cmp)).second << "\n";
    cout << (*upper_bound(v.begin(), v.end(), pa, cmp)).second;
}

P-2-6. Two-Number problem

題目:https://judge.tcirc.tw/ShowProblem?problemid=d015

假設 A 為 m 個相異整數的集合,B 為 n 個相異整數的集合,而 K 是一個整數。請計算有多少對(a, b)的組合滿足 a 屬於 A, b 屬於 B 且 a+b = K。
Time limit: 1 秒
輸入格式:輸入可能有多行,第一行有三個整數 m, n 與 K,第二行有 m 個整數是 A
中的元素,第三行有 n 個整數 B 中的元素一筆測資。同一行相鄰數字間以空白間隔。
兩集合元素個數均不超過 10 萬,整數的絕對值不超過 10 億。
輸出格式:輸出組合個數。
範例輸入:
3 4 2
1 6 -3
5 1 -1 -3
範例輸出:
2

#include <bits/stdc++.h>
using namespace std;
int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    int m,n,k;
    cin>>m>>n>>k;
    
    //a放陣列
    int v[m];
    for(int i=0;i<m;i++){
        cin>>v[i];
    }
    
    //b放set(要排序)
    set<int>s;
    for(int i=0;i<n;i++){
        int x;
        cin>>x;
        s.insert(x);
    }
    
    
    //由a搜b中是否有k-a
    int ans=0;
    for(int i=0;i<m;i++){
        if(s.find(k-v[i])!=s.end()){
            ans++;
        }
    }
    cout<<ans;
}

跳躍二分搜

Danger | 注意!!!

裡面是while不是if

這是比較特別的二分搜尋,用來解決一個數列可以分兩半,找到前半邊的最後一項(例如:大於目標數值、小於目標數值)他的對象也是已排序的陣列,以二分搜找出小於或是小於等於目標數值的index

我們來看看它的運作過程:

他先把跳躍的距離(junp)設為總大小的一半
接著確認跳了之後的 v[junp] 是否小於目標
如果小於:代表可以跳(將index加上junp)
如果不會:那就把跳躍的距離除以二直到可以跳,或是jump=0,如果等於零
就代表已經找到了小於目標數值的最大值
跳了第一次之後就會遇到問題:有可能會超出陣列範圍,導致無法找到v[index],

所以判斷可以跳的條件為:
1. 跳了不會超出陣列範圍
2. 跳了之後能夠小於目標數值

全部可分為兩半:小於6、大於6
我們來看找尋小於物標數值(6)的最大值例子:

int v[10]={1,3,4,5,7,9,11,14,15,19};  //遞增陣列     
int L=0,R=10;  //起點、終點     
int x=6  //目標數值     
int index;  //位置     

alt text

#include<bits/stdc++.h>
using namespace std;
#define nn "\n"
int main(){
    int v[10]={1,3,4,5,7,9,11,14,15,19};
    int L=0,R=10;
    int x=6;
    int index=L;

    for (int jump = (R-L)/2; jump>0;jump>>=1) {
        while (index+jump<R && v[index+jump]<x){
            index += jump;
        }
    }
    cout<<index;
}
3

判斷「是否有找到」

#include <bits/stdc++.h>
using namespace std;
#define nn "\n"
#define int long long

int v[1000];   // index
int n;

int jumpfind(int x) { // find_the_last(<=x)
    int i = 0;
    for (int j = n / 2; j > 0; j >>= 1) {
        while ((i + j <n) && (v[i + j] <= x )) {
            i += j;
        }
    }
    // 檢查是否找到符合條件的元素
    if (v[i] <= x) {
        return i;
    } else {
        return  n; // 沒找到,返回n
    }
}

signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);

    istringstream cin("6 \
1 2 5 7 8 9");

    cin>>n;
    for (int i = 0; i < 6; i++) {
        cin >> v[i];
    }

    int x = 8;
    cout << "x: " << jumpfind(x) << nn;

    return 0;
}

前面提到可分為兩半,小於6、大於6
在第11行的while (index+jump<R && v[index+jump]<x){中,v[index+jump]<x就是用來分辨是否小於6要如何思考寫大於6還是小於6呢?
起點位置小於6,所以就是小於6

跳躍二分搜適合用於有單調性的題目喔

在貪心演算法有一題「P-4-9. 基地台 (APCS201703) 」可以練習看看