使用二分查找判斷某個數在某個區間中--如何判斷某個IP地址所屬的地區


一,問題描述

給定100萬個區間對,假設這些區間對是互不重疊的,如何判斷某個數屬於哪個區間?

首先需要對區間的特性進行分析:區間是不是有序的?有序是指:后一個區間的起始位置要大於前一個區間的終點位置。
如:[0,10],[15,30],[47,89],[90,100]…..就是有序的區間
[15,30],[0,10],[90,100],[47,89]……就是無序的區間

其次,區間是不是連續的?連續是指:后一個區間的起始位置 比 前一個區間的終點位置大1,連續的區間一定是有序的。
如:[0,10],[11,30],[31,89],[90,100]……

下面先來考慮連續區間的查找,即:假設有100萬個區間,給定一個數,判斷這個數位於100萬個區間中的哪一個,一個實際的應用實例就是:給定一個IP地址,如何判斷該IP地址所屬的地區?比如:[startIp1, endIp1]---》廣東深圳、[startIp2,endIp2]---》廣東廣州、[startIp3, endIp3]---》四川成都……要查找某個IP所在的地區,先要判斷出該IP在哪個區間內,再取出該區間對應的地區信息。


二, 一種實現方式
首先將“字符串類型的IP地址”轉換成長整型,這是為了方便比較大小。比如:“70.112.108.147” 轉換之后變成1181772947,轉換結果是唯一的。具體原理可參考:這篇文章

簡要轉換思路是:一個IP地址32bit,一共有四部分,每部分都是一個十進制的整數。首先將每部分轉換成二進制,然后再對每部分移位,最終將每部分的移位結果相加,得到一個長整型的整數。如下圖所示(圖片來源):

經過上面的IP到長整型的轉換后,就可以使用一個長整型數組long[]來保存所有的IP區間對了。給定一個待查找的字符串類型的IP地址,先將之轉換成長整型,然后再使用二分查找算法查找long[]即可。
由於我們不僅僅是找出某個IP在哪個區間段內,而是根據該IP所在的區間段 獲得 該區間段對應的地區信息。由於IP區間保存在long[]數組中,因此使用一個ArrayList保存地區信息,通過數組下標的方式 將long[] 與ArrayList 元素一 一 對應起來。

 

三, 算法的正確性證明
對於二分查找而言,循環while(low <= high)最后執行的一步是 low==high,假設待查找的數 位於某個區間內,那么最后一次while循環時,low 和 high 要么同時指向該區間的左邊界,要么同時指向該區間的右邊界。假設待查找的數為15,如下圖所示:

若low和high同時指向左邊界(比如13),mid = (low+high)/2 = low = high,根據前面假設,這個數位於區間內,那么 arr[mid] < 這個數,low指針更新為mid+1,從而 low > high,跳出循環。而high指針則剛好指向這個數所在區間的左邊界。

若low和high同時指向右邊界(比如17),mid = (low+high)/2 = low = high,根據前面假設,這個數位於區間內,那么 arr[mid] > 這個數,high 指針更新為 mid-1,從而low>high,跳出循環,此時high指針也剛好指向該區間的左邊界。因此,最終 high 指針的位置就是這個數所在區間的左邊界。

由於每個區間有兩個位置(起始位置和結束位置),每個區間對應一個地區信息,因此:Long[]數組的長度是ArrayList長度的兩倍。那么二分查找中返回的 high 指針的位置除以2,就是該區間對應的地址信息了(ArrayList.get(high / 2))

當然了,若待查找的數,剛好位於區間的邊界上(起始位置/結束位置),那就代表二分查找命中,直接 return mid 返回查找結果了。

特殊情況:若待查找的數比所有區間中的最小的數還小,由於long[]是有序的,那么最后一次while(low<=high)循環一定是 low 和 high 同時指向 long[]中索引為0的位置,然后high = mid -1 變成 -1(即high>0)
若待查找的數比所有區間中的最大的數還要大,由於long[]是有序的,那么最后一次while(low<=high)循環一定是 low 和 high 同時指向 long[]中索引為long[]數組的arr.length-1的位置,然后low = mid +1 變成arr.length(即low > arr.length-1)

 

四,區間不連續的情況

區間不連續,只有序時,同樣可使用二分查找,可能出現的情況與(三)中分析的一樣,只是這里還有一種情況:待查找的數 不在 任何一個區間內,而是在兩個相鄰的區間之間。比如查找26,但它不在任何一個區間內。如下圖所示:

 

這種情況,跳出while循環的條件還是 low > high,但是此時 low 指向一個區間,而high指向另一個區間,可以根據 low 和 high 指向不同的區間來判斷26不在任何一個區間中。

若 low / 2 == high / 2 則 low 和 high 指向相同的區間,若 low /2 != high/2 則,low 和 high指向不同的區間。

如下圖所示:

而在(三)中,while循環結束后,low 和 high 還是指向同一個區間(具體而言,就是high 總是指向區間A的起始位置,而low指向區間A的終點位置)。

 

-------------------------------------------------------

重新更新:2019.5.29

看了下JDK的源碼:java.util.Arrays#binarySearch0(T[], int, int, T, java.util.Comparator<? super T>)

其實JDK里面Arrays類已經實現了二分查找,如果查找命中,則返回數組下標;若未命中,則返回一個負數,(負數+1)再乘以(-1) 就是待插入的下標(有序)。

因此,直接使用Arrays類的binarySearch方法就能完美實現 區間 查找。

示例如下:

給定有序區間:[2,5]  [8,9]  [9,16]  [19,25]

因為區間對放入數組,因此,數組的長度肯定是個偶數。因此,當數組中有重復的元素時,二分查找重復元素時,若查找命中,返回的下標是 "數組下標小的那個"。比如查找元素9,查找命中,返回的index=3。

將之放入數組,得到:[2,5,8,9,9,16,19,25]

假設查找2:

二分查找命中,返回元素2的數組下標 index=0,0是偶數,說明:元素2在區間(index,index+1)區間上,即區間[2,5]

假設查找9:

二分查找命中,返回元素9的數組下標index=3,3是奇數,說明:元素9在區間(index-1,index)上,即區間[8,9],當然了,對於這種特殊的情形,視具體的需求處理。

假設查找10:

二分查找不命中,返回 index=-6,(-6+1)*-1=5,說明元素10可插入在數組下標為5的位置處。由於5是個奇數,因此,元素10在區間(4,5)上,即區間[9,16]

假設查找18:

二分查找不命中,返回index=-7,(-7+1)*(-1)=6,說明元素18可插入在數組下標為6的位置處。由於6是個偶數,因此,元素18不在任何一個區間。

。。。。

總之,結合數組長度永遠是偶數(區間對),再結合二分查找返回的“數組下標”是否為奇偶,是否命中,是可以實現:給定一個數,快速地判斷這個數是否落在某個區間?若落在了某個區間,則具體是哪個區間上的。

 

另外一種形式的范圍查詢:

elasticsearch中,也有RangeQuery,它是基於kd樹實現的,能夠快速地針對大量的數據進行范圍查找。

 

------------------------------------------------------

 

五, 代碼實現

假設所有的IP信息存儲在文件ipJson.conf文件中,大約有100萬條,其中一條數據的格式如下:(自己構造的一條示例數據而已):該IP區間是[3085210000,3085219875],對應的地區是:”中國/四川/成都“

{"begin_int_ip":"3085210000","end_int_ip":"3085219875","country":"中國","province":"四川","city":"成都"}

 

使用fastjson將數據解析出來,關於fastJson解析數據,可參考:FastJson使用示例,並初始化long[]數組和 ArrayList數組:代碼如下:

 1     private int parse() {
 2         JSONReader jsonReader = null;
 3         int index = 0;
 4         try {
 5             jsonReader = new JSONReader(new FileReader(new File(FILE_PATH)));
 6         } catch (FileNotFoundException e) {
 7         }
 8         int recordNum = 0;
 9         jsonReader.startArray();// ---> [
10 
11         while (jsonReader.hasNext()) {
12             IpInfo ipInfo = jsonReader.readObject(IpInfo.class);// 根據 java bean 來解析
13             ipSegments[index++] = ipInfo.getBegin_int_ip();
14             ipSegments[index++] = ipInfo.getEnd_int_ip();
15             ipRegions.add(new Address(ipInfo.getCountry(), ipInfo.getProvince(), ipInfo.getCity()));
16             recordNum++;
17         }
18         jsonReader.endArray();// ---> ]
19         jsonReader.close();
20         return recordNum;
21     }

 

將 字符串類型的IP地址轉換成長整型的方法如下:

1     private static long toSmallLongFromIpAddress(String strIp) {
2         long[] ip = new long[4];
3         String[] ipSegments = strIp.split("\\.");
4         for(int i = 0; i < 4; i++) {
5             ip[i] = Long.parseLong(ipSegments[i]);
6         }
7         return (ip[0] << 24) + (ip[1] << 16) + (ip[2] << 8) + ip[3];
8     }

 

連續區間的二分查找算法如下:

    private int binarySearch(long[] arr, long searchNumber) {
        if(arr == null || arr.length == 0)
            throw new IllegalArgumentException("初始化失敗...");
        return binarySearch(arr, 0, arr.length-1, searchNumber);
    }
    private int binarySearch(long[] arr, int low, int high, long searchNumber) {
        int mid;
        System.out.println("arr len:" + arr.length);
        while(low <= high)
        {
            mid = (low + high) / 2;
            if(arr[mid] > searchNumber)
                high = mid - 1;
            else if(arr[mid] < searchNumber)
                low = mid + 1;
            else
                return mid;//待查找的數剛好在區間邊界上
        }
        
        System.out.println("low=" + low + ", high=" + high);
        
        //low > high
        if(low > arr.length-1 || high < 0)//待查找的數比最大的數還要大,或者比最小的數還要小
            return -1;//not found
        
        return high;
    }

 

Address類的代碼如下:

public class Address {
    private String country;
    private String province;
    private String city;
    
    public Address() {
        // TODO Auto-generated constructor stub
    }
    
    
    public Address(String country, String province, String city) {
        this.country = country;
        this.province = province;
        this.city = city;
    }
    
    
    public String getCountry() {
        return country;
    }
    public void setCountry(String country) {
        this.country = country;
    }
    public String getProvince() {
        return province;
    }
    public void setProvince(String province) {
        this.province = province;
    }
    public String getCity() {
        return city;
    }
    public void setCity(String city) {
        this.city = city;
    }
    
    @Override
    public String toString() {
        return "country: " + country + ", province: " + province + ", city: " + city;
    }
}
View Code

 

與Address類一樣,IpInfo類也是個JAVA Bean,只是比Address類多了兩個屬性而已,這兩個屬性是:begin_int_ip 和 end_int_ip

整個完整代碼實現如下:(自己測試了下,由於使用fastjson 將數據都加載到內存了,因此查找IP還是非常快的 ^~^)

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.List;

import com.alibaba.fastjson.JSONReader;

public class FindIp {

    private static final String FILE_PATH = "F:\\ipJson.conf";
    private static final int RECORD_NUM = 1065589;//ipJson.conf中一共約有100萬條IP地址段數據
    private long[] ipSegments;
    private static List<Address> ipRegions;

    public FindIp() {
        ipSegments = new long[RECORD_NUM * 2];// 每個區間 有起始位置和終點位置 [startPos, endPos]
        ipRegions = new ArrayList<Address>(RECORD_NUM);
    }

    public static void main(String[] args) {
        FindIp fip = new FindIp();
        
        fip.parse();//將json格式的數據解析出來,然后放到 查找數組中.
        String[] ips = { "122.246.89.69", "183.228.145.144", "36.99.63.196", "124.114.242.174", "183.10.202.232" };

        long startTime = System.currentTimeMillis();
        for (String ip : ips) {
            Address address = fip.find(ip);
            System.out.println("ip: " + ip + ", address:" + address);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("find five ip address use time:" + (endTime - startTime) + "ms");

    }

    private int parse() {
        JSONReader jsonReader = null;
        int index = 0;
        try {
            jsonReader = new JSONReader(new FileReader(new File(FILE_PATH)));
        } catch (FileNotFoundException e) {
        }
        int recordNum = 0;
        jsonReader.startArray();// ---> [

        while (jsonReader.hasNext()) {
            IpInfo ipInfo = jsonReader.readObject(IpInfo.class);// 根據 java bean 來解析
            ipSegments[index++] = ipInfo.getBegin_int_ip();
            ipSegments[index++] = ipInfo.getEnd_int_ip();
            ipRegions.add(new Address(ipInfo.getCountry(), ipInfo.getProvince(), ipInfo.getCity()));
            recordNum++;
        }
        jsonReader.endArray();// ---> ]
        jsonReader.close();
        return recordNum;
    }

    public Address find(String ip) {
        long startTime = System.currentTimeMillis();

        long ipConvert = toSmallLongFromIpAddress(ip);//

        System.out.println("ip:" + ip + ", convertInt:" + ipConvert);

        int index = binarySearch(ipSegments, ipConvert);
        if (index == -1)
            return new Address();// 未找到,返回一個沒有任何信息的地址(avoid null pointer exception)

        Address addressResult = ipRegions.get(index / 2);
        long endTime = System.currentTimeMillis();
        System.out.println("find: " + ip + " use time: " + (endTime - startTime));
        return addressResult;
    }

    private int binarySearch(long[] arr, long searchNumber) {
        if (arr == null || arr.length == 0)
            throw new IllegalArgumentException("初始化失敗...");
        return binarySearch(arr, 0, arr.length - 1, searchNumber);
    }

    private int binarySearch(long[] arr, int low, int high, long searchNumber) {
        int mid;
        System.out.println("arr len:" + arr.length);
        while (low <= high) {
            mid = (low + high) / 2;
            if (arr[mid] > searchNumber)
                high = mid - 1;
            else if (arr[mid] < searchNumber)
                low = mid + 1;
            else
                return mid;// 待查找的數剛好在區間邊界上
        }

        System.out.println("low=" + low + ", high=" + high);

        // low > high
        if (low > arr.length - 1 || high < 0)// 待查找的數比最大的數還要大,或者比最小的數還要小
            return -1;// not found
        return high;
    }

    private static long toSmallLongFromIpAddress(String strIp) {
        long[] ip = new long[4];
        String[] ipSegments = strIp.split("\\.");
        for (int i = 0; i < 4; i++) {
            ip[i] = Long.parseLong(ipSegments[i]);
        }
        return (ip[0] << 24) + (ip[1] << 16) + (ip[2] << 8) + ip[3];
    }
}
View Code

 

時間復雜度分析:假設共有N條IP區間數據,根據IP找該IP對應的區間,使用的是二分查找,時間復雜度為O(logN)。找到之后,根據區間的在long[]數組中的 索引 來定位該區間對應的地區,時間復雜度為O(1),故總的時間復雜度為O(logN)

空間復雜度分析:N條 IP區間需要 2*N個數組元素保存(因為每個區間上起始位置和結束位置),IP區間對應的地址信息使用長度為N的 ArrayList保存,空間復雜度為O(2*N)+O(N)=O(N)

 

六, 參考資料:

Converting IP Addresses To And From Integer Values With ColdFusion

針對范圍對的高效查找算法設計(不准用數組)

 Comparing IP Addresses in SQL

原文:http://www.cnblogs.com/hapjin/p/7252898.html 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM