一,问题描述

给定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],对应的地区是:”中国/四川/成都“

  1. {"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. }

连续区间的二分查找算法如下:

  1. private int binarySearch(long[] arr, long searchNumber) {
  2. if(arr == null || arr.length == 0)
  3. throw new IllegalArgumentException("初始化失败...");
  4. return binarySearch(arr, 0, arr.length-1, searchNumber);
  5. }
  6. private int binarySearch(long[] arr, int low, int high, long searchNumber) {
  7. int mid;
  8. System.out.println("arr len:" + arr.length);
  9. while(low <= high)
  10. {
  11. mid = (low + high) / 2;
  12. if(arr[mid] > searchNumber)
  13. high = mid - 1;
  14. else if(arr[mid] < searchNumber)
  15. low = mid + 1;
  16. else
  17. return mid;//待查找的数刚好在区间边界上
  18. }
  19.  
  20. System.out.println("low=" + low + ", high=" + high);
  21.  
  22. //low > high
  23. if(low > arr.length-1 || high < 0)//待查找的数比最大的数还要大,或者比最小的数还要小
  24. return -1;//not found
  25.  
  26. return high;
  27. }

Address类的代码如下:

  1. public class Address {
  2. private String country;
  3. private String province;
  4. private String city;
  5.  
  6. public Address() {
  7. // TODO Auto-generated constructor stub
  8. }
  9.  
  10. public Address(String country, String province, String city) {
  11. this.country = country;
  12. this.province = province;
  13. this.city = city;
  14. }
  15.  
  16. public String getCountry() {
  17. return country;
  18. }
  19. public void setCountry(String country) {
  20. this.country = country;
  21. }
  22. public String getProvince() {
  23. return province;
  24. }
  25. public void setProvince(String province) {
  26. this.province = province;
  27. }
  28. public String getCity() {
  29. return city;
  30. }
  31. public void setCity(String city) {
  32. this.city = city;
  33. }
  34.  
  35. @Override
  36. public String toString() {
  37. return "country: " + country + ", province: " + province + ", city: " + city;
  38. }
  39. }

与Address类一样,IpInfo类也是个JAVA Bean,只是比Address类多了两个属性而已,这两个属性是:begin_int_ip 和 end_int_ip

整个完整代码实现如下:(自己测试了下,由于使用fastjson 将数据都加载到内存了,因此查找IP还是非常快的 ^~^)

  1. import java.io.File;
  2. import java.io.FileNotFoundException;
  3. import java.io.FileReader;
  4. import java.util.ArrayList;
  5. import java.util.List;
  6.  
  7. import com.alibaba.fastjson.JSONReader;
  8.  
  9. public class FindIp {
  10.  
  11. private static final String FILE_PATH = "F:\\ipJson.conf";
  12. private static final int RECORD_NUM = 1065589;//ipJson.conf中一共约有100万条IP地址段数据
  13. private long[] ipSegments;
  14. private static List<Address> ipRegions;
  15.  
  16. public FindIp() {
  17. ipSegments = new long[RECORD_NUM * 2];// 每个区间 有起始位置和终点位置 [startPos, endPos]
  18. ipRegions = new ArrayList<Address>(RECORD_NUM);
  19. }
  20.  
  21. public static void main(String[] args) {
  22. FindIp fip = new FindIp();
  23.  
  24. fip.parse();//将json格式的数据解析出来,然后放到 查找数组中.
  25. String[] ips = { "122.246.89.69", "183.228.145.144", "36.99.63.196", "124.114.242.174", "183.10.202.232" };
  26.  
  27. long startTime = System.currentTimeMillis();
  28. for (String ip : ips) {
  29. Address address = fip.find(ip);
  30. System.out.println("ip: " + ip + ", address:" + address);
  31. }
  32. long endTime = System.currentTimeMillis();
  33. System.out.println("find five ip address use time:" + (endTime - startTime) + "ms");
  34.  
  35. }
  36.  
  37. private int parse() {
  38. JSONReader jsonReader = null;
  39. int index = 0;
  40. try {
  41. jsonReader = new JSONReader(new FileReader(new File(FILE_PATH)));
  42. } catch (FileNotFoundException e) {
  43. }
  44. int recordNum = 0;
  45. jsonReader.startArray();// ---> [
  46.  
  47. while (jsonReader.hasNext()) {
  48. IpInfo ipInfo = jsonReader.readObject(IpInfo.class);// 根据 java bean 来解析
  49. ipSegments[index++] = ipInfo.getBegin_int_ip();
  50. ipSegments[index++] = ipInfo.getEnd_int_ip();
  51. ipRegions.add(new Address(ipInfo.getCountry(), ipInfo.getProvince(), ipInfo.getCity()));
  52. recordNum++;
  53. }
  54. jsonReader.endArray();// ---> ]
  55. jsonReader.close();
  56. return recordNum;
  57. }
  58.  
  59. public Address find(String ip) {
  60. long startTime = System.currentTimeMillis();
  61.  
  62. long ipConvert = toSmallLongFromIpAddress(ip);//
  63.  
  64. System.out.println("ip:" + ip + ", convertInt:" + ipConvert);
  65.  
  66. int index = binarySearch(ipSegments, ipConvert);
  67. if (index == -1)
  68. return new Address();// 未找到,返回一个没有任何信息的地址(avoid null pointer exception)
  69.  
  70. Address addressResult = ipRegions.get(index / 2);
  71. long endTime = System.currentTimeMillis();
  72. System.out.println("find: " + ip + " use time: " + (endTime - startTime));
  73. return addressResult;
  74. }
  75.  
  76. private int binarySearch(long[] arr, long searchNumber) {
  77. if (arr == null || arr.length == 0)
  78. throw new IllegalArgumentException("初始化失败...");
  79. return binarySearch(arr, 0, arr.length - 1, searchNumber);
  80. }
  81.  
  82. private int binarySearch(long[] arr, int low, int high, long searchNumber) {
  83. int mid;
  84. System.out.println("arr len:" + arr.length);
  85. while (low <= high) {
  86. mid = (low + high) / 2;
  87. if (arr[mid] > searchNumber)
  88. high = mid - 1;
  89. else if (arr[mid] < searchNumber)
  90. low = mid + 1;
  91. else
  92. return mid;// 待查找的数刚好在区间边界上
  93. }
  94.  
  95. System.out.println("low=" + low + ", high=" + high);
  96.  
  97. // low > high
  98. if (low > arr.length - 1 || high < 0)// 待查找的数比最大的数还要大,或者比最小的数还要小
  99. return -1;// not found
  100. return high;
  101. }
  102.  
  103. private static long toSmallLongFromIpAddress(String strIp) {
  104. long[] ip = new long[4];
  105. String[] ipSegments = strIp.split("\\.");
  106. for (int i = 0; i < 4; i++) {
  107. ip[i] = Long.parseLong(ipSegments[i]);
  108. }
  109. return (ip[0] << 24) + (ip[1] << 16) + (ip[2] << 8) + ip[3];
  110. }
  111. }

时间复杂度分析:假设共有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

使用二分查找判断某个数在某个区间中--如何判断某个IP地址所属的地区的更多相关文章

  1. vuex中filter的使用 && 快速判断一个数是否在一个数组中

    vue中filter的使用 computed: mapState({ items: state => state.items.filter(function (value, index, arr ...

  2. (Array) 一个 N*N 的矩阵,每一行从左到右有序,每一列从上到下有序,都是递增,写个程序,判断一个数是否在矩阵中。

    int search(int d[N][N], int key) { int i1, i2, j1, j2; i1 = j1 = 0; i2 = j2 = N-1; while(i1 < i2 ...

  3. Python3基础 if elif 示例 判断一个数在哪个区间内

             Python : 3.7.0          OS : Ubuntu 18.04.1 LTS         IDE : PyCharm 2018.2.4       Conda ...

  4. js 判断一个数是否在数组中

    ,,,,,,,); ; ; i < arr.length; i++) { ){ console.log(i); flag=; break; } } ){ console.log("66 ...

  5. 《Algorithms Unlocked》读书笔记2——二分查找和排序算法

    <Algorithms Unlocked>是 <算法导论>的合著者之一 Thomas H. Cormen 写的一本算法基础,算是啃CLRS前的开胃菜和辅助教材.如果CLRS的厚 ...

  6. 二分查找(lower_bound和upper_bound)

    转载自:https://www.cnblogs.com/luoxn28/p/5767571.html 1 二分查找 二分查找是一个基础的算法,也是面试中常考的一个知识点.二分查找就是将查找的键和子数组 ...

  7. C++ STL中的Binary search(二分查找)

    这篇博客转自爱国师哥,这里给出连接https://www.cnblogs.com/aiguona/p/7281856.html 一.解释 以前遇到二分的题目都是手动实现二分,不得不说错误比较多,关于返 ...

  8. STL模板整理 Binary search(二分查找)

    前言: 之前做题二分都是手动二分造轮子,用起来总是差强人意,后来看到STL才发现前辈们早就把轮子造好了,不得不说比自己手动实现好多了. 常用操作 1.头文件 #include <algorith ...

  9. C#二分查找法 破洞百出版本

    二分查找法在数据繁多的数据中查找是一种快速的方法,每次查找最多需要的次数 为2的n次方小于总个数. 当然是有前提的,就是需要把数据先排好序,这里指的都是数值型的数据. 基本思想就是把需要找的值与排序好 ...

随机推荐

  1. Logger.error方法之打印错误异常的详细堆栈信息

    一.问题场景 使用Logger.error方法时只能打印出异常类型,无法打印出详细的堆栈信息,使得定位问题变得困难和不方便. 二.先放出结论 Logger类下有多个不同的error方法,根据传入参数的 ...

  2. luogu3292 幸运数字 (点分治+线性基)

    首先第一眼是一个倍增套线性基,但是$O(Qlog^2Vlog^N)=10^{10}$的复杂度... 即使是st表也只是变成了$O(Nlog^2Vlog^N)$啊 考虑点分治,相对于倍增显著减少了线性基 ...

  3. linux环境sed命令实例学习

    命令简介: sed(Stream Editor)被称作流编辑器.linux命令环境的“三剑客”(grep,sed,awk)之一,功能强大,可以根据命令来处理数据流中的数据,命令可以在命令行中,也可以出 ...

  4. 杨辉三角 II

    题目描述 给定一个非负索引 k,其中 k ≤ 33,返回杨辉三角的第 k 行. 在杨辉三角中,每个数是它左上方和右上方的数的和. 示例: 输入: 3 输出: [1,3,3,1] 贴出代码 class ...

  5. SpringBoot构建大数据开发框架

    http://blog.51cto.com/yixianwei/2047886 为什么使用SpringBoot 1.web工程分层设计,表现层.业务逻辑层.持久层,按照技术职能分为这几个内聚的部分,从 ...

  6. Neovim中提示Error: Required vim compiled with +python

    Neovim在编辑python文件时出现错误提示,如下图 原因 出现该错误的原因说明未安装Python2/3的支持 解决方法 使用包管理器安装Neovim的Python支持python-neovim ...

  7. 洛谷P3723 礼物

    以前看到过,但是搞不倒.知道了算法之后就好搞了. 题意:给定两个长为n的序列,你可以把某个序列全部加上某个数c,变成循环同构序列. 求你操作后的min∑(ai - bi)² 解: 设加上的数为c,那么 ...

  8. 洛谷P3975 弦论

    题意:求一个串的字典序第k小的子串/本质不同第k小的子串. 解:一开始我的想法是在后缀树上找,但是不知道后缀树上的边对应的是哪些字符... 然而可以不用fail树转移,用转移边转移即可. 先建一个后缀 ...

  9. 【洛谷P2568】GCD

    题目大意:给定整数 \(N\),求\(1\le x,y\le N\) 且 \(gcd(x,y)\) 为素数的数对 \((x,y)\) 有多少对. 题解: \[ \sum_{p \in \text { ...

  10. Runtime.getRuntime().exec(...),当参数中有空格时!

    原以为不会有什么问题,但在测试时发现,问题大了. 如果想调用f:\mp3\i love you.mp3时, 我原以为正确的写法是: //在文件名前后加个双引号来解决文件名中有空格的情况 String ...