pengkingli 2019-07-01
这篇文章主要深入数据结构与算法在解决实际问题怎么运用和分析的,对于 IP 对属地查找本身有 API 接口,那这篇文章主要对原理内部查询过程实现做详细解析,体会怎么将数据结构和算法解决实际的问题。
今天主要模拟一下怎么在 20 万数据中定位一个 IP 地址的归属地,不知道大家有没有用过百度搜索过 IP 地址的归属地。当我们在百度输入 IP 地址时,就会出现这个 IP 地址的归属地。
或者有一些 IP 归属地的查询工具也可以迅速的查找到 IP 归属地。
IP 地址数据那么庞大,它是怎么在短短不到一秒时间查找出 IP 地址的归属地呢?随后我带着疑问模拟了在 20 万条数据中快速查找一个 IP 地址的归属地。
问题分析
我们知道每个 IP 由两部分组成的,分别是网络地址和主机地址。而且每个 IP 地址是随机动态分配的,所以说,每个地区的 IP 地址的前多少位代表哪个地区,后多少位代表地区中的局域网。每个所以划定了 IP 范围,每个代表不同的归属地。
[112.222.133.0, 112.222.133.255] 山东潍坊市 [112.222.135.0, 112.222.136.255] 山东烟台市 [112.222.156.34, 112.222.157.255] 山东青岛市 [112.222.48.0, 112.222.48.255] 北京朝阳区 [112.222.49.15, 112.222.51.251] 福建省福州 [112.222.56.0, 112.222.56.255] 广东省深圳市
我们逐渐的将问题转化为了数据分析问题,也就是说,我们怎么查找一个 IP 地址所属的范围从而得出 IP 归属地呢?我们可能会想到用快速增删改查的数据结构和算法,平衡树、散列表、跳表、基于数组的二分查找等。
IP 地址的区间是连续的,可能先考虑到用一下二分查找,但是二分查找是有前提条件的:
1、二分查找是基于顺序数组的,运用的数组在时间复杂度为 (1) 的时间内随机快速访问数据的特性。
2、二分查找它必须是有序数据,而且不能频繁的进行动态插入和删除数据,适合一次排序,多次查找的情况回到我们问题符合要求。
通过两个二分查找的条件继续进行问题的分析,那么问题又来了,二分查找是快速的查找一个数据是否存在一组数据中,而且效率极高,1000亿查找一个数据只需 36 次查找。但是我们的要解决的问题是在区间查找。
二分查找的扩展
别着急,二分查找还可能有重复的数据,怎么解决?所以二分查找会延伸到查找重复数据的第一个数据或最后一个数据,都可以通过二分查找的算法进行改进的。
如果我们想要查找的 IP 地址在某一区间内,我们能不能转化为查找最后一个小于等于某一个区间的起始值。举个简单例子:有一下区间[1,5]、[6,10]、[11,15]、[16、20],比如 IP 为 9 ,每个区间的起始值分别为 1、6、11、16,也就是说 9 在这组区间起始值中,最后一个小于等于 9 的值,也就是 6 ,然后我们拿 9 去区间[ 6,10] 去查找是否存在该 IP ,如果存在,我们就输出该区间对应的 IP 归属地。
解决方案
问题已经分析完成了,下一步开始将问题转换为数据结构与算法的形式来解决。如果你真认为问题分析完成只剩下写代码了,你会接连的遇到棘手的问题。为了能够让大家更能体会到实际问题的复杂性,我会采用分步式递进最终的解决方法。
问题一:当下手开始写代码时,你会发现 IP 地址并不是像上述我们用到的整数,那我们怎么办呢?
※ 解决:你会想能不能将 IP 转化为整数来计算,这里我用 js 来转化。
1 //将 IP 地址转化为整数 2 const ipInt = (ip) =>{ 3 //IP转成整型 4 var num = 0; 5 ip = ip.split("."); 6 num = Number(ip[0]) * 256 * 256 * 256 + Number(ip[1]) * 256 * 256 + Number(ip[2]) * 256 + Number(ip[3]); 7 num = num >>> 0; 8 return num; 9 }
问题二:IP 地址实际上是动态生成的,怎么来进行模拟那么多随机的 IP 地址呢?
※ 解决:最大的 IP 是 255.255.255.255 转化成整数为 4294967295。也就是 40 亿,那我们用随机函数在 40 亿的范围内随机生成 20 万个的 IP 地址。
1 let i = 0; 2 const arrIp = []; 3 //随机生成 200000 条 IP 数据 4 while(i < 10000){ 5 const number = Math.floor(Math.random()*10000000); 6 arrIp.push(number); 7 i++; 8 }
问题三:随机生成的 IP 地址是无序的,我们要进行排序,那么排序的方式有很多,冒泡、归并、快排、堆排序等,选择哪一种呢?
※ 解决:对于在 20 万的 IP 查询一个 IP 的归属地,我用 js 在浏览器中实现的,想到存储空间有限,所以排序空间复杂度不能太高,查询效率又不能太慢。快排的可以实现空间复杂度为 O(1) 排序,而且排序效率复杂度为 O(nlog2n)
1 //对 20 万条数据进行快速排序 2 // 参数一(arrIP):要排序的数组IP 参数二(start):指向起始指针 参数三(end):指向末尾指针 3 const quickSort = (arr,startIndex,endIndex) =>{ 4 //递归终止条件 5 if(startIndex < endIndex){ 6 //一般选择最后一个元素为区分点(下标索引) 7 let pivot = endIndex; 8 //获取一组数据区分后的大于 pivot 点最后元素的索引 9 let partitionIndex = partition(arr,pivot,startIndex,endIndex); 10 //进行递归 11 quickSort(arr,startIndex,partitionIndex-1); 12 quickSort(arr,partitionIndex+1,endIndex); 13 } 14 } 15 16 // 获取排好序的区分点 Index 17 const partition = (arr,pivot,startIndex,endIndex) =>{ 18 //获取到该区分点的值 19 let pivotVal = arr[pivot]; 20 //永远指向第一个大于 pivot 的值 21 let swapIndex = startIndex; 22 //进行筛选 23 // i 为遍历数据指针 24 for(let i = startIndex; i < endIndex; i++){ 25 if(arr[i] < pivotVal){ 26 swap(arr,i,swapIndex); 27 swapIndex++; 28 } 29 } 30 //将大于 pivot 的值和小于 pivot 的值中间点和 pivot 的值交换 31 swap(arr,swapIndex,pivot) 32 //返回区分点的索引 33 return swapIndex; 34 } 35 36 //交换 37 const swap = (arr,i,j) =>{ 38 let temp = arr[i]; 39 arr[i] = arr[j]; 40 arr[j] = temp; 41 }
问题四: 因为我们要做的是查询某 IP 在哪一区间,而不是查找该 IP 地址,所以要对二分查找代码进行改进,让其转化为小于等于某区间的起始位置。
1 //对 20 万数据匹配IP对属地(二分查找) 2 const findIpAddress = (arr,value) =>{ 3 //声明两个指针 4 let low = 0; 5 let high = arr.length - 1; 6 7 while(low <= high){ 8 //取中间值 9 let mid = Math.floor((low + (high - low))/2); 10 //判断中间值 11 if(arr[mid] <= value){ 12 //进一步判断是否是小于 IP 区间的终点值[改进] 13 if(mid == arr.length - 1 || arr[mid + 1] > value){ 14 return mid; 15 }else{ 16 low = mid + 1; 17 } 18 }else{ 19 high = mid - 1; 20 } 21 } 22 return false; 23 }
IP 区间归属地我们自己设置几个简单的区间模拟一下,但是实际中很多的 IP 地址归属地划分的很精细的,所以我们在这不多陈述。
代码我们都做好了,我在这用前端做了一的简单的交互页面,我们来模拟一下,你会发现,当我们划分区间后,数据并没有 20 万,因为我们只记录区间的起始值查找就可以了,20 万数据实际大约也就是十几万甚至小于这个值。
我们可以设想一下如果把全球的数据存储到浏览器中会发生什么,所以小鹿随机生成了 50 亿的数据,来进行排序二分查找,你猜发生了什么情况?
浏览器只在呼呼的转圈,并不显示什么,好吧,作为一个前端开发者,存储那么多的数据来进行操作内存溢出了。如果你是一名后台开发者,可以尝试着用后台语言实现一下,看看能不能数据量大时,能不能再进行查找了?
通过上边的测试,小鹿从中又得出两个二分查找的适用条件:
1、数据量不能太大,数组在内存中需要连续的内存空间,像 java 语言,在内存空间紧张的情况下,二分查找就不适用了。但是 js 中的数组并不是连续的,而是以哈希映射的方式存在的。
2、数据量不能太小,如果数据量太小,我们直接遍历就可以了,无序写复杂的二分查找来进行查询。
二分查找的三点重点:
1、循环退出条件
注意是 low <= height,而不是 low < heigh。如果是后者,会造成循环指向一个数据。
2、mid 的取值
因为如果 low 比和 height 大的话,两者之和可能会溢出。应写成 low+(high-low)/2 ,如果优化到极致的话,改进为位运算符 low+((high-low)>>1)。
3、low 和 high 的更新
如果不进行 +1 和 -1 ,就有可能会发生死循环。
总结
自从学习数据结构与算法以来,发现它确实能解决很多我们身边实际的问题,而不仅仅停留到刷各种各样的算法题上。我们刷算法题的主要目的呢,是提高逻辑思维能力分析能力。还有一种能力也是需要提高的就是一个实际问题怎么才能转化为数据结构和算法问题,再考虑用什么样的数据结构和算法去解决?怎么找到一个最优的解决方案?
它对我们的理解、分析、转化实际问题到数据结构与算法提出了一个更高的要求,从之前写了两篇用数据结构与算法解决实际问题总结来看,我个人觉得不仅仅需要分析问题的能力,还考验一个人对所有数据结构与算法的灵活运用、优化、以及思想有很大的挑战性,因为不局限于一个算法题,还要考虑到实际的很多考虑不到的因素。