二分查找
时间复杂度O(logn)
这是一种极其高效的时间复杂度,有的时候甚至比时间复杂度是常量级 O(1) 的算法还要高效
比如2的32次方,找一个数,最多比较32次,而常数级可以是O(1000)
1 | public int bsearch(int[] a, int n, int value) { |
优化mid 的计算方式:
low+(high-low)/2防止(low+high)溢出
更进一步优化:
除以 2 操作转化成位运算,
low+((high-low)>>1),因为相比除法运算来说,计算机处理位运算要快得多
二分查找局限性:
1 | 1二分查找依赖的是顺序表结构,即数组。主要原因是二分查找算法需要按照下标随机访问元素,而链表随机访问的时间复杂度是 O(n) |
那么,如何在 1000 万个整数中快速查找某个整数?
1 | 内存限制是 100MB,每个数据大小是 8 字节,存储为数据后,内存占用80MB,先排序,再二分查找 |
二分查找的变形问题:
1 | 1查找第一个值等于给定值的元素(存在重复数据) |
可参考leetcode34题
1查找第一个值等于给定值的元素(存在重复数据)
2查找最后一个值等于给定值的元素,类似第一个问题
1 | public int bsearch(int[] a, int n, int value) { |
3查找第一个大于等于给定值的元素
4查找最后一个小于等于给定值的元素
1 | public int bsearch(int[] a, int n, int value) { |
那么,如何快速定位出一个 IP 地址的归属地?
可以将ip转为32位整型,再排序
1 | ip地址本身就是32位标识,只需要将按点拆成数组。将数组元素转换成int值,只有移动对应的位数即可:arr[0]左移24位,arr[1]左移16位,arr[2]左移8位,arr[3]左移0位 |
用数组,直接下标找归属地,为啥还用二分查找,绕道?
总结:
凡是用二分查找能解决的,绝大部分我们更倾向于用散列表或者二叉查找树。即便是二分查找在内存使用上更节省,但是毕竟内存如此紧缺的情况并不多,二分查找的场景中,“值等于给定值”不多,更多的是在大于小于等“近似值”问题,优势更明显
Redis跳表
这种链表加多级索引的结构,就是跳表(空间换时间),Redis 中的有序集合(Sorted Set)就是用跳表来实现的
跳表的查询的时间复杂度是O(logn),插入、删除操作的时间复杂度也是 O(logn)
两节点一个索引算,索引的结点总和是 n/2+n/4+n/8…+8+4+2=n-2,即n个节点的链表,需要建立近n个索引,空间复杂度为O(n)。若是三节点一个索引,则索引总数是n/2,节省了一半的空间。
1 | 等比数列求和 n/2, n/4, ... , 2 这个数列中一共有 log(n/2) 项 等比数列求和公式 S = a0(1-q^n)/(1-q), 其中 a0 表示首项,n 表示项数 这里的 a0=n/2, 项数=log(n/2), q=1/2 S = (n/2)*(1-2/n)/(1-1/2) = n-2 |
实际开发中,不必太在意索引占用的额外空间,因为原始链表中存储的可能是很大的对象,而索引存储的是关键值和指针,几乎可以忽略不计。
问题:
为什么 Redis 要用跳表来实现有序集合,而不是红黑树?
1 | 严格讲,Redis 中的有序集合除了跳表 还使用了散列表 |
当然,跳表不能完全取代红黑树,因为红黑树出现的早,已有很多编程语言中都有封装,例如Map类型,而跳表没有现成的实现,需要自己写
redis中删除了索引上的数据,是否需要重新建立索引?
1 | 答:而跳表是通过随机函数来维护索引的“平衡性”,比如,插入数据的时候,我们可以选择同时将这个数据插入到部分索引层中。红黑树、AVL树通过左右旋的方式保持左右子树的大小平衡 |
以上学于极客时间,做的笔记