查找算法
查找算法简介
1. 顺序查找
2. 二分查找
3. 斐波那契查找
4. 插值查找
5. 分块查找
查找算法简介
查找算法也叫搜索算法,常用于判断某个数是否在数列中,或者某个数在数列中的位置。影响查找时间长短的主要因素有算法、数据存储的方式及结构。
查找和排序算法一样,如果是以查找过程中被查找的表格或数据是否变动来分类,则可以分为静态查找(Static Search)和动态查找(Dynamic Search)。
- 静态查找是指数据在查找过程中,该查找数据不会有添加、删除或更新等操作,例如符号表查找就属于一种静态查找。
- 动态查找则是指所查找的数据,在查找过程中会经常性地添加、删除或更新。例如在网络上查找数据就是一种动态查找。
查找的几种常见方法:顺序查找、二分法查找、二叉树查找、哈希查找等。
1. 顺序查找
顺序查找是最简单、最直接的查找算法。顾名思义,顺序查找就是将数列从头到尾按照顺序查找一遍,是适用于小数据量的查找方法。
此方法优点是数据在查找前不需要进行任何的处理与排序;缺点是查找速度较慢。
时间复杂度
- 最优时间复杂度:O(1)
- 最坏时间复杂度:O(n)
1 # 顺序查找 2 def sequential_search(i_list, target): 3 for i in range(len(i_list)): 4 if i_list[i] == target: 5 return i 6 return -1 7 8 if __name__ == "__main__": 9 li = [1, 22, 44, 55, 66] 10 print(sequential_search(li, 0)) # -1 11 print(sequential_search(li, 1)) # 0 12 print(sequential_search(li, 66)) # 4 13 print(sequential_search(li, 67)) # -1
2. 二分查找法
二分查找又称折半查找,实质上是不断地将有序数据集进行对半分割,并检查每个分区的中间元素。
- 其优点是比较次数少,查找速度快,平均性能好;
- 其缺点是要求待查表为有序表,且插入删除困难。
因此,二分查找方法适用于不经常变动而查找频繁的有序列表。
算法步骤:
- 首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功。
- 否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。
- 重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。
时间复杂度
- 最优时间复杂度:O(1)
- 最坏时间复杂度:O(logn)
图解二分查找过程:
代码实现
返回值:如果查找成功返回目标的索引值;否则返回 -1。
方式一:递归实现
1 def binary_search(li, start_index, end_index, element): 2 if end_index >= start_index: 3 mid_index = (start_index + end_index) // 2 4 if element == li[mid_index]: 5 return mid_index 6 elif element > li[mid_index]: 7 return binary_search(li, mid_index+1, end_index, element) 8 else: 9 return binary_search(li, start_index, mid_index-1, element) 10 # 未找到该元素 11 return -1 12 13 14 if __name__ == "__main__": 15 li = list(range(100)) 16 print(binary_search(li, 0, len(li)-1, 0)) # 0 17 print(binary_search(li, 0, len(li)-1, 99)) # 99 18 print(binary_search(li, 0, len(li)-1, 100)) # -1
方式二:非递归实现
1 # 非递归实现 2 def binary_search(li, element): 3 start_index = 0 4 end_index = len(li) - 1 5 while end_index >= start_index: 6 mid_index = (start_index + end_index) // 2 7 if element == li[mid_index]: 8 return mid_index 9 elif element > li[mid_index]: 10 start_index = mid_index + 1 11 else: 12 end_index = mid_index - 1 13 return -1 14 15 16 if __name__ == "__main__": 17 li = list(range(100)) 18 print(binary_search(li, 0)) # 0 19 print(binary_search(li, 99)) # 99 20 print(binary_search(li, 100)) # -1
3. 斐波那契查找
斐波那契数列(Fibonacci)又称黄金分割数列,指的是这样一个数列:1, 1, 2, 3, 5, 8, 13, 21, ......。
在数学上,斐波那契被递归方法如下定义:F(1)=1, F(2)=1, F(n)=F(n-1)+F(n-2)(n>=2)。该数列越往后,相邻的两个数的比值越趋向于黄金比例值(0.618)。斐波那契查找就是在二分法查找的基础上根据斐波那契数列进行分割的。
斐波那契查找作为二分法查找的变种,与二分法的效率不相上下。根据数据在数列中的分布状态,二分法和斐波那契法很难说哪个更快一点(比如被查找数正好在数列靠前位置,斐波那契查找效率还不如二分法查找)。
原理
斐波那契查找与二分法查找几乎一样,只是在选取比较点(参照点)的位置有所区别。二分法选的是将数列中间的那个元素作为比较点(参照点),而斐波那契查找则选取了数列中间略为靠后的位置作为比较点,也就是黄金分割点。
既然叫做斐波那契查找,首先得列出一个斐波那契数列,如 [1, 2, 3, 5, 8, 13, 21] 这个斐波那契数列只是提供了被查找数列中参照点的位置,基本上就是下标。
以 i_list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 为例,共有 len(i_list)=10 个元素。选择一个比 len(i_list) 稍大的斐波那契数,也就是 13(因为斐波那契数列中没有10),比 13 小一号的斐波那契数是 8,那么首次选取的参照点就是 i_list[8]。如果被查找数比 i_list[8] 小,就看一下比 8 小一号的斐波那契数 5,第二次选取的参照点就是 i_list[5]。以此类推,就是把斐波那契数当做被查找数列的位置参数来使用。
代码实现
1 # 斐波那契数列 2 def fibonacci(n): 3 f_list = [1, 1] 4 while n > 1: 5 f_list.append(f_list[-1] + f_list[-2]) 6 n -= 1 7 return f_list[-1] 8 9 ## 斐波那契查找 10 def fibonacci_search(i_list, target): 11 12 i_len = len(i_list) 13 start_index = 0 14 end_index = i_len - 1 15 16 k = 1 17 # 斐波那契数列中的最后一个元素要比i_list长度稍微大一点 18 while fibonacci(k) < i_len : 19 k += 1 20 21 while start_index < end_index: 22 # 参照点的位置 23 mid_index = start_index + fibonacci(k-1) 24 if target == li[mid_index]: 25 return mid_index 26 elif target > li[mid_index]: 27 start_index = mid_index + 1 28 k -= 2 29 else: 30 end_index = mid_index - 1 31 k -= 1 32 # 当剩余相邻的两个元素比较时 33 if target == li[start_index]: 34 return start_index 35 elif target == li[end_index]: 36 return end_index 37 else: 38 return -1 39 40 if __name__ == "__main__": 41 li = [4, 12, 24, 45, 56, 63, 78] 42 print(fibonacci_search(li, 1)) # -1 43 print(fibonacci_search(li, 4)) # 0 44 print(fibonacci_search(li, 78)) # 6
4. 插值查找
在被查找数列中选取比较点(参照点)时,二分法采用的是中点,斐波那契法采用的是黄金分割点。二分查找法是最直观的,斐波那契查找法可能是最“美观”的,但都不是最科学的。
那么,如何选取最科学的那个比较点(参照点)呢?通常情况下被查找数列都不是等差的,也不知道数列的差值分布状态,没法确定到底选择哪个点是“最优解”。插值算法则给出了一个在大多数情况下的理论上最科学的比较点(参照点)。
插值查找法也是二分查找法的进阶算法,虽然当数列中的元素分布得比较广的情况下插值查找的效果并不太好。
原理
以英文词典为例,如果是要找以“a”开头的单词,一般都不会从中间位置开始翻(二分查找法),也不会从黄金分割点(页数*0.618)的位置开始找(斐波那契查找法),直觉上就应该从字典的前面开始查找。但数学没有直觉,它会告诉我们最佳取值点 mid_index = left_index + (target - i_list[left_index]) * (right_index-left_index) // (i_list[right_index]-i_list[left_index]),不管是找哪个字母开头的单词,还是在任意数列中寻找任意数,这个点在大多数情况下就是最优的比较点(至于为什么这个点是最优的比较点,请自行查找答案)。
对于等差数列,插值查找可能是最快的算法;而对于等比数列,效率远不如二分查找和斐波那契查找。
代码实现
1 def insert_search(i_list, target): 2 3 i_len = len(i_list) 4 left = 0 5 right = i_len - 1 6 7 while right - left > 1: 8 mid = left + (target - i_list[left]) * (right-left) // (i_list[right]-i_list[left]) 9 if mid == left: 10 # 当i_list[right]和i_list[left]相差太大时,有可能导致mid一直都等于left,从而陷入死循环 11 mid += 1 12 if target < i_list[mid]: 13 right = mid 14 elif target > i_list[mid]: 15 left = mid 16 else: 17 return mid 18 if target == i_list[left]: 19 return left 20 elif target == i_list[right]: 21 return right 22 else: 23 return -1
5. 分块查找
分块查找合以上几种查找方式有所不同:顺序查找的数列是可以无序的,二分法查找和斐波那契查找的数列的数列必须是有序的。分块查找介于两者之间,需要块有序,元素可以无序。
分块查找可以看成是顺序查找的进阶算法。虽然在分块时会耗费点时间,但是在后期的查找会快很多。一般情况下分块查找比顺序查找要快。
原理
分块查找首先按照一定的取值范围将数列分成数块(至少分成两块,否则就和顺序查找没区别了)。块内的元素是可以无序的,但块必须是有序的,意思是处于后面位置的块中的最小元素都要比前面位置块中的最大元素大。
首先将被查找数与块的分界数相比较(也可以与每个块中最大的元素比较),确定被查找数属于哪个块,然后再在对应块内逐个对比,也就是顺序查找了。
代码实现
1 # 存储每个块的最大值以及数列中对应元素的个数 2 index_list = [[25, 0], [50, 0], [75, 0], [100, 0]] 3 i_list = [1, 3, 66, 12, 99, 43, 54, 78, 33, 41, 98] 4 5 # 制造分块数列 6 def divide_block(): 7 global index_list, i_list 8 # 分块后的list 9 sort_list = [] 10 for key in index_list: 11 # 小于key[0]的元素单独成块 12 sub_list = [i for i in i_list if i < key[0]] 13 key[1] = len(sub_list) 14 sort_list += sub_list 15 # 过滤掉已经加入到sub_list的元素 16 i_list = list(set(i_list)-set(sub_list)) 17 i_list = sort_list 18 # 返回分块情况 19 return index_list 20 21 # 找到目标值的块并在块中进行顺序查找 22 def block_search(i_list, target, index_list): 23 print("i_list : %s" % i_list) 24 print("index_list : %s" % index_list) 25 # 起始索引 26 left = 0 27 # 终止索引 28 right = 0 29 # 首先找到目标值所在的块 30 for index_info in index_list: 31 right += index_info[1] 32 if target < index_info[0]: 33 break 34 else: 35 left += index_info[1] 36 # 在块中顺序查找 37 for i in range(left, right): 38 if target == i_list[i]: 39 return "%s is in the list, index is: %s" % (target, i) 40 return "%s is not in the list" % target 41 42 43 if __name__ == "__main__": 44 print(i_list) 45 divide_block() 46 print(block_search(i_list, 3, index_list)) 47 print(block_search(i_list, 99, index_list)) 48 print(block_search(i_list, 59, index_list))