二分查找真的很快吗
2020-03-12 18:39
92 查看
很常见的一个题目:给定升序排序数组
a,查找
a中大于等于
x的第一个数的索引。
有多种方法来完成这个题目,大部分人第一反应都是二分查找。我喜欢从实践出发来想问题。我有几个问题:
- 二分查找真的很快吗?如果快,快多少?
- 实际中我们应该怎么写才能更好地完成这个题目?
本文就是来回答这两个问题的。我不会在本文中赘述二分查找的思想原理,不了解的同学可以先出门搜索了解以下。
来点硬的,直接来跑一下结果。你可以在 colab 中自己试验一下。
代码
我们这里测试了 4 种方法,如下:
def recursive(a, low, high, x): '''递归写法''' middle = int((low + high) / 2) if high >= low: if a[middle] == x: return middle elif a[middle] > x: return recursive(a, low, middle - 1, x) else: return recursive(a, middle + 1, high, x) if x < a[middle]: return middle if x > a[middle]: return middle + 1 def whileloop(a, x): '''while 循环写法''' low = 0 high = len(a) - 1 while low <= high: middle = int((low + high) / 2) if a[middle] < x: low = middle + 1 elif a[middle] > x: high = middle - 1 else: return middle if x < a[middle]: return middle if x > a[middle]: return middle + 1 def forloop(a, x): '''for 循环写法''' for idx, item in enumerate(a): if item >= x: return idx def builtin_bisect(a, x): '''使用内置的 bisect''' return bisect.bisect_left(a, x)
共进行 3 次测试,各次
a的长度为 10310^3103、10610^6106 和 10910^9109,
x为长度的 0.67,测试 CPU 环境为 Intel® Xeon® CPU @ 2.00GHz,Python 版本为 Python 3.6.8,就是 colab 的环境:
recursive_times = [] whileloop_times = [] forloop_times = [] builtin_times = [] lengths = [int(1e3), int(1e6), int(1e9)] for i in lengths: a = range(i) x = 0.67 * i print(i) start = time.time() _ = recursive(a, 0, i - 1, x) cost = time.time() - start recursive_times.append(cost) start = time.time() _ = whileloop(a, x) cost = time.time() - start whileloop_times.append(cost) start = time.time() _ = forloop(a, x) cost = time.time() - start forloop_times.append(cost) start = time.time() _ = builtin_bisect(a, x) cost = time.time() - start builtin_times.append(cost) fig = plt.figure(figsize=(15, 8)) plt.plot(range(3), [i * 1000 for i in recursive_times], '-o', label="recursive") plt.plot(range(3), [i * 1000 for i in whileloop_times], '-o', label="whileloop") plt.plot(range(3), [i * 1000 for i in forloop_times], '-o', label="forloop") plt.plot(range(3), [i * 1000 for i in builtin_times], '-o', label="builtin_bisect") plt.xticks([0, 1, 2], [3, 6, 9]) plt.xlabel("length/10^") plt.ylabel("COST/ms") plt.legend() plt.grid(True)
测试结果:
可以看到随着数组长度的扩大,for 循环方法消耗的时间也非常非常快的增长,而其他方法波动很小。具体而言,数组长度变为原来的 1000 倍,for 循环方法耗时也变为原来的 1000 倍左右,而其他方法的耗时变为原来的 1-3 倍左右。
暂且除去 for 循环,如果我们细看在 10310^3103、10610^6106 和 10910^9109 处的图,可以看到剩余三个方法之间的差距,递归耗时均最高:
实际上
bisect内部实现用的就是 while 循环的方法,代码很短,我直接贴过来(吐槽下官方代码竟然没有很好的格式化):
def bisect_left(a, x, lo=0, hi=None): """Return the index where to insert item x in list a, assuming a is sorted. The return value i is such that all e in a[:i] have e < x, and all e in a[i:] have e >= x. So if x already appears in the list, a.insert(x) will insert just before the leftmost x already there. Optional args lo (default 0) and hi (default len(a)) bound the slice of a to be searched. """ if lo < 0: raise ValueError('lo must be non-negative') if hi is None: hi = len(a) while lo < hi: mid = (lo+hi)//2 if a[mid] < x: lo = mid+1 else: hi = mid return lo
详细的测试时间数据如下:
recursive_times=[1.5735626220703125e-05, 2.5272369384765625e-05, 4.553794860839844e-05] whileloop_times=[6.67572021484375e-06, 1.1205673217773438e-05, 1.8358230590820312e-05] forloop_times=[4.982948303222656e-05, 0.058376312255859375, 58.462812423706055] builtin_times=[6.4373016357421875e-06, 1.9311904907226562e-05, 2.4557113647460938e-05]
到此时我们回答了第一个问题,二分查找真的很快! 在数组长度较小时,差距还不是那么的明显。但是随着数组长度扩大,差距简直指数级扩大,差距甚至几百万倍。
至于第二个问题,我认为的答案是尽可能使用内置库,这些都是经过了很多优化,速度和稳定性都有保证,写起来还简单粗暴,何乐而不为呢?
写完感觉一篇废话 ?
Reference
- cpython/bisect.py at 3.6 · python/cpython
- 8.6. bisect — Array bisection algorithm — Python 3.6.9 documentation
END
- 点赞 2
- 收藏
- 分享
- 文章举报
相关文章推荐
- 二分查找的递归和非递归
- divide and conquer 二分查找
- python之各种二分查找
- 二分查找的扩展
- 【LintCode-457】经典二分查找问题(Java实现)
- 问题 A: 二分查找
- 二分查找树按照key值划分
- js顺序查找与二分查找
- Java必知必会查找算法:顺序查找、二分查找
- 二分查找 c++实现
- 二分查找及其变化应用
- c语言 快速排序 和 二分 查找 模型
- C语言中二分查找(折半查找)
- POJ2785 4 Values whose Sum is 0(暴力二分查找)
- hdu2516-取石子游戏 (斐波那契博弈)【博弈 二分查找】
- 判断给定的一个数字x是否在指定的一个有序的数字序列中存在[二分查找方式]...
- 二分查找
- 数据结构 顺序查找和二分查找
- 二分查找错误事例分析
- 二分查找