本文是学习算法的笔记,《数据结构与算法之美》,极客时间的课程
通过IP地址来查找IP归属地功能,不知道你用过没?没用过也没关系,打开百度,在搜索框里随便输入一个IP地址,就会看到它的归属地。
这个功能并不复杂,它是通过维护一个很大的IP地址库来实现。地址库中包括IP地址范围和归属地的对应关系。
当我们想要查询202.102.133.13这个IP地址的归属地时,我们就在地址库中搜索,发到IP地址落在[202.102.133.0, 202.102.133.255] 这个地址范围内,那我们就可以将这个IP地址范围对应的归属地“山东东营市”显示给用户了。
学完今天的内容,你就会发现这个问题其实很简单。
上一节我讲了二分查找的原理,并且介绍了最简单的一种二分查找的代码实现。今天我们来讲几种二分查找的变形问题。
需要特别说明的一点,为了简化讲解,今天的内容,我都以数据从小到大排列为前提,如果要处理的数据是从大到小,解决的思路也是一样的。
100个人写二分查找,就会有100种实现。网上很多关于变形的二分查找实现方法,有很多写的非常简洁,比如正面这人写法。但是,尽管简洁,理解起来却非常烧脑,也很容易写错。
public int bsearch( int[] a, int n, int value){
int low = 0;
int high = n - 1;
while(low <= high){
int mid = low +(( high - low) >> 1);
if( a[mid >= value){
high = mid -1;
}else{
low = mid + 1;
}
}
if( low < n && a[low] == value ){
return low;
}else{
return -1;
}
}
看完这个实现之后,你是不是觉得不好理解?我换了一种实现方法,你看看是不是更容易理解呢?
public int bsearch( int[]a, int n, int value){
int low = 0;
int high = n - 1;
while(low <= high){
int mid = low + ((high-low) >> 1);
if( a[mid] > value){
high = mid -1;
}else(a[mid] < value){
low = mid + 1;
}else{
if(mid == 0 || ( a[mid - 1] != value)){
return mid;
}else{
high = mid - 1;
}
}
return -1;
}
}
这段代码,是不是比较容易理解些,**很多人觉得变形的二分查找很难写,主要原因是太追求第一种那样完美、简洁的写法。**而对于我们做工程开发的人来说,代码易懂、没 bug ,其实更重要,所以我觉得第二种写法更好。
(在学习这节的时候,本人用java实现的,如有谬误请指正)
// 查找第一个等于给定值的元素
private int binarySearchFirst(int[] a, int value) {
int n = a.length;
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = (low + high) / 2;
if (a[mid] == value) {
if (a[low] == value) {
return low;
}
if(a[mid - 1] != value) {
return mid;
}else {
high = mid -1;
}
} else if (a[mid] < value) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return -1;
}
这个和变体一思路类似
public int bsearch( int[]a, int n, int value){
int low = 0;
int high = n - 1;
while(low <= high){
int mid = low + ((high-low) >> 1);
if( a[mid] > value){
high = mid -1;
}else(a[mid] < value){
low = mid + 1;
}else{
if(mid == 0 || ( a[mid + 1] != value)){
return mid;
}else{
low = mid + 1;
}
}
return -1;
}
}
(在学习这节的时候,本人用java实现的,如有谬误请指正)
// 查找最后一个等于给定值的元素
private int binarySearchLast(int[] a, int value) {
int n = a.length;
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = (low + high) / 2;
if (a[mid] == value) {
if (a[high] == value) {
return high;
}
if(a[mid + 1] != value) {
return mid;
}else {
low = mid + 1;
}
} else if (a[mid] < value) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return -1;
}
比如,数组中存储这样一个序列: 3,4,6,7,10。如果查找第一个等于5的元素,那就是6.
public int bsearch( int[]a, int n, int value){
int low = 0;
int high = n - 1;
while(low <= high){
int mid = low + ((high-low) >> 1);
if( a[mid] >= value){
if(mid == 0 || ( a[mid - 1] < value)){
return mid;
}else{
high = mid + 1;
}
}else{
low = mid + 1;
}
}
return -1;
}
(在学习这节的时候,本人用java实现的,如有谬误请指正)
// 第一个大于等于
private int greaterOrEqualFirst(int[]a, int value) {
int n = a.length;
int low = 0;
int high = n -1;
// 判断首尾元素与value的大小关系
if(value <= a[low]) {
return low;
}
if(value > a[high]) {
return -1;
}
// 进入循环之后,保证 a[low] < value <= a[high]
while(low + 1 != high) {
int mid = (low + high) / 2;
if(a[mid] >= value) {
high = mid ;
}else {
low = mid ;
}
}
return high;
}
这个思路和上一个类似
public int bsearch( int[]a, int n, int value){
int low = 0;
int high = n - 1;
while(low <= high){
int mid = low + ((high-low) >> 1);
if( a[mid] <= value){
if(mid == n -1 || ( a[mid + 1] > value)){
return mid;
}else{
low = mid + 1;
}
}else{
high = mid - 1;
}
}
return -1;
}
(在学习这节的时候,本人用java实现的,如有谬误请指正)
// 最后一个小于等于
private int lessOrEqualLast(int[]a, int value) {
int n = a.length;
int low = 0;
int high = n -1;
if(value < a[low]) {
return -1;
}
if(value >= a[high]) {
return high;
}
while(low + 1 != high) {
int mid = (low + high) / 2;
if(a[mid] <= value) {
low = mid;
}else {
high = mid;
}
}
return low;
}
好了,现在我们回头看开篇的问题:如何快速定位出一个IP地址的归属地?
现在这个问题很简单了,如果IP区间与归属地的对应关系不经常更新,我们可以先预处理这12万条数据,让其按照IP从小到大排序。如何排序呢?我们知道,IP地址可以转化为32位的整型数。所以,我们可以将起始地址,按照对应的事型值的大小关系,从小到大进行排序。
然后这个问题就可以转化为我刚讲的第四种变形问题“在有序数组中,查找最后一个小于等于某个给定值的元素”了。