赞
踩
目 录
2.3.5 二分查找代码优化-对于不在数列中的元素寻找最接近的值
顺序查找是最常见也是最朴素的查找思想,即按顺序比较一个有序或者无序的数列的每个元素,直到找到关键字为止。
它非常简单,适用于一个有序或者无序的数列。时间复杂度:O(n)
# 顺序查找,找到就返回所在地址,找不到返回 -1
# 可以看到如果存在多个相同元素,那么找到的第一个即返回
- import random
- a=[0, 1, 3, 4, 4, 7, 9, 9,9,9,10,10,10,10, 11, 12,66,100]
- random.shuffle(a)
- print("shuffled list ==>",a)
-
- # 顺序查找,找到就返回所在地址,找不到返回 -1
- # 可以看到如果存在多个相同元素,那么找到的第一个即返回
- def SequentiaSearch(nums,obj):
- nums_len=len(nums)
- i=0
- while(i<nums_len):
- if(nums[i]==obj):
- return(i)
- i+=1
- return(-1)
- obj_list=list(set(a))
- obj_list.extend([-100,99,101])
- print("obj_list is ==>",obj_list)
-
- for ele in obj_list:
- print(ele,SequentiaSearch(a,ele))
运行结果
二分查找是非常常见的查找算法:
它适用于一个有序(正序和反序不限)的数列。时间复杂度:O(log2n)。在有序的数列里面,它的查找速度远大于顺序查找。
# 这个算法非常简单,只是在正序的列表中查找某个元素
# 如果查找到,则返回该元素的所在位置,
# 如果相同的元素有多个,那么返回的则是找到的第一个符合要求的元素的位置。
请注意:这里说寻找序列里面的第一个符合要求的元素的位置,不是指在数列里面第一个元素的位置。
这段是设计的简单的二分查找代码,面向的是正序的数列,如果是反序的数列,依样画葫芦颠倒一下比较顺序就好。
- # 这个算法非常简单,只是在正序的列表中查找某个元素
- # 如果查找到,则返回该元素的所在位置,
- # 如果相同的元素有多个,那么返回的则是找到的第一个符合要求的元素的位置
- def half_seek(nums,obj):
- left=0
- right=len(nums)-1
- #
- while(left<=right):
- # 中间的元素地址
- mid=(left+right)//2
-
- # 如果找到一个符合要求的,即可返回元素所在地址
- if(nums[mid]==obj):
- return(mid)
- elif(nums[mid]<obj):
- left=mid+1
- else:
- right=mid-1
- return(-1)
这段是测试代码
- import copy
- a=[0, 1, 3, 4, 4, 7, 9, 9,9,9,10,10,10,10, 11, 12,66,100]
- b=[-1,88,13]
- b.extend(list(set(a)))
- b.append(100)
- b.append(101)
- print(a)
- print("*"*90)
- print(b)
- print("*"*90)
- # f 是我用来设计和 half_seek 做比较的函数
- # f 是利用 Python3 内置方法 index 实现的顺序查找方法
- f=lambda x,y:x.index(y) if y in x else -1
- for tmp in b:
- print(tmp,half_seek(a,tmp),f(a,tmp))
下面是使用测试代码得到的运行结果
从运行结果可以看出,针对在数列或者不在数列中的元素,该函数都能返回正确的结果。但是针对元素9、10,简单二分查找算法返回的元素地址和顺序查找返回的地址是不一样的,这是因为数列中存在多个相同的元素。
那么在这样一种情况下,用户可能不介意是数列中第几个符合条件的元素被返回,也可能有一定的要求。最常见的是:
1)返回数列中第一个符合条件的元素,
2)返回数列中最后一个符合条件的元素。
如果一个正序的数列里面,存在多个相同的元素,那么返回数列中第一个符合条件的元素,也可以理解为查找左端元素。
代码实现如下:
- # 这个二分查找算法如果相同的元素有多个,
- # 那么返回的则是找到的第一个符合要求的位置(左侧)
- def half_seek_left(nums,obj):
- left=0
- right=len(nums)-1
- found=-1
- #
- while(left<=right):
- # 中间的元素地址
- mid=(left+right)//2
- # 如果找到一个符合要求的,继续向左去搜索
- if(nums[mid]==obj):
- found=mid #注意这里用了一个 found 来记录已经找到的地方
- print("mid is found in place==>",found)
- right=mid-1 # 这里不是直接返回而是继续向左查找
- elif(nums[mid]<obj):
- left=mid+1
- else:
- right=mid-1
- print("left is {0} and right is {1}!".format(left,right))
- return(found)
测试代码如下
- a=[0, 1, 3, 4, 4, 7, 9, 9,9,9,10,10,10,10, 11, 12,66,100]
- b=[-1,88,13]
- b.extend(list(set(a)))
- b.append(100)
- b.append(101)
- print(a)
- print("*"*90)
- print(b)
- print("*"*90)
- # f 是我用来设计和 half_seek 做比较的函数
- # f 是利用 Python3 内置方法 index 实现的顺序查找方法
- f=lambda x,y:x.index(y) if y in x else -1
- for tmp in b:
- print(tmp,half_seek_left(a,tmp),f(a,tmp))
运行结果如下:
根据上面的运行结果可以看到,所有在数列的中的元素都被找到,并且返回左侧端的元素,和对应的 f 函数返回值一样;对于不在数列中的元素则返回了 -1。测试代码中打印出了 mid 符合条件的元素依次被找到的情况,如果不使用 found 来记录符合条件的 mid 元素,那么在继续寻找时候可能会丢失符合条件的元素信息。
如果一个正序的数列里面,存在多个相同的元素,那么返回数列中最后个符合条件的元素,也可以理解为查找右端元素。
代码实现如下:
- # 这个二分查找算法如果相同的元素有多个,
- # 那么返回的则是找到的最后一个符合要求的位置(右侧)
- def half_seek_right(nums,obj):
- left=0
- right=len(nums)-1
- found=-1
- #
- while(left<=right):
- # 中间的元素地址
- mid=(left+right)//2
- # 如果找到一个符合要求的,继续向右去搜索
- if(nums[mid]==obj):
- found=mid #注意这里用了一个 found 来记录已经找到的地方
- print("mid is found in place==>",found)
- left=mid+1 # 这里不是直接返回而是继续向右查找
- elif(nums[mid]<obj):
- left=mid+1
- else:
- right=mid-1
- print("left is {0} and right is {1}!".format(left,right))
- return(found)
测试代码如下
- a=[0, 1, 3, 4, 4, 7, 9, 9,9,9,10,10,10,10, 11, 12,66,100]
- b=[-1,88,13]
- b.extend(list(set(a)))
- b.append(101)
- f=lambda x,y:x.index(y) if y in x else -1
- print(a)
- print("*"*90)
- print(b)
- print("*"*90)
- for tmp in b:
- print("*"*50)
- print(tmp,half_seek_right(a,tmp),f(a,tmp))
测试代码运行结果如下:
运行结果符合预期。
以简单的二分查找为例,我在代码上加上 print 语句,可以看到当查找的元素不在数列中时候,代码也傻乎乎的查询和比对了很多次。
当前代码如下:
- # 这个算法非常简单,只是在正序的列表中查找某个元素
- # 如果查找到,则返回该元素的所在位置,
- # 如果相同的元素有多个,那么返回的则是找到的第一个符合要求的位置
- def half_seek(nums,obj):
- left=0
- right=len(nums)-1
- #
- while(left<=right):
- # 中间的元素地址
- mid=(left+right)//2
- print("current mid is {0}".format(mid))
- # 如果找到一个符合要求的,即可返回元素所在地址
- if(nums[mid]==obj):
- return(mid)
- elif(nums[mid]<obj):
- left=mid+1
- else:
- right=mid-1
- return(-1)
- a=[0, 1, 3, 4, 4, 7, 9, 9,9,9,10,10,10,10, 11, 12,66,100]
- half_seek(a,101)
运行结果可以看出,对于有序数列,即使被查找的元素一看就是大于数列中的最大值或者小于数列中的最小值,算法还是把数列翻了个底朝天,然后才返回 -1。
对于这种情况,尤其是查找次数非常多,不在数列中的情况非常普遍时候,是很影响性能的。
所以建议这时候可以做个优化,在二分查找前就做个判断。代码实现如下:
- # 这个算法非常简单,只是在正序的列表中查找某个元素
- # 如果查找到,则返回该元素的所在位置,
- # 如果相同的元素有多个,那么返回的则是找到的第一个符合要求的位置
- def half_seek(nums,obj):
- left=0
- right=len(nums)-1
- if( obj < nums[0] or obj >nums[-1]):
- return(-1)
- #
- while(left<=right):
- # 中间的元素地址
- mid=(left+right)//2
- print("current mid is {0}".format(mid))
- # 如果找到一个符合要求的,即可返回元素所在地址
- if(nums[mid]==obj):
- return(mid)
- elif(nums[mid]<obj):
- left=mid+1
- else:
- right=mid-1
- return(-1)
- a=[0, 1, 3, 4, 4, 7, 9, 9,9,9,10,10,10,10, 11, 12,66,100]
- half_seek(a,101)
运行结果可以很清楚的看到,减少了 5 次 即
很多时候我们用二分查找如果找不到只返回 -1 这个地址是不够的,我们知道一个最接近这个元素的值是多少。
代码实现如下:
- # 这个算法非常简单,只是在正序的列表中查找某个元素
- # 如果查找到,则返回该元素的所在位置,
- # 如果相同的元素有多个,那么返回的则是找到的第一个符合要求的位置
- # 如果没有合适的元素,则比较left right 两个位置的元素到底谁更贴近目标
- def half_seek_nearest(nums,obj):
- left=0
- right=len(nums)-1
- # 考虑目标数小于列表最小数的情况
- if( obj < nums[0] ):
- return(left)
-
- # 考虑目标数大于列表最大数的情况
- if(obj >nums[-1]):
- return(right)
- #
- while(left<=right):
- # 中间的元素地址
- mid=(left+right)//2
- # 如果找到一个符合要求的,即可返回元素所在地址
- if(nums[mid]==obj):
- print("Searching {0},found address is {1} and nearest ele is {2}".format(obj,mid,nums[mid]))
- return(mid)
- elif(nums[mid]<obj):
- left=mid+1
- else:
- right=mid-1
-
- # 因为前面考虑了目标数过大或者过小情况,所以不需要考虑 left、right 越界的情况
- if(abs(nums[left]-obj)<=abs(nums[right]-obj)):
- found=left
- else:
- found=right
- print("Searching {0},found address is {1} and nearest ele is {2}".format(obj,found,nums[found]))
- return(found)
测试代码如下:
- a=[0, 1, 3, 4, 4, 7, 9, 9,9,9,10,10,10,10, 11, 12,66,70,82,100]
- b=[-1,13,8,13,14,15,20,21,88,99,]
- b.extend(list(set(a)))
- f=lambda x,y:x.index(y) if y in x else -1
- print(a)
- print("*"*90)
- print(b)
- print("*"*90)
-
- # f 是我用来设计和 half_seek 做比较的函数
- # f 是利用 Python3 内置方法 index 实现的顺序查找方法
- f=lambda x,y:x.index(y) if y in x else -1
- for tmp in b:
- print("-"*50)
- print(tmp,half_seek_nearest(a,tmp),f(a,tmp))
运行结果让人满意:
下面这段代码也可以实现相关功能,但是和上面代码性能的优缺点在于数据的特点。
如果数据列表很长,并且查找的数据有很多不在列表内,那么使用上面的算法,先判断是否在列表内,再二分查找;毕竟列表长度越长,每次查找的时间也越长。
如果查找的数据很少不在列表内,那么就先二分查找再避免每次查一个数据都要先比较最小值和最大值。
- def half_seek_nearest2(nums,obj):
- left=0
- right=len(nums)-1
- #
- while(left<=right):
- # 中间的元素地址
- mid=(left+right)//2
- # 如果找到一个符合要求的,即可返回元素所在地址
- if(nums[mid]==obj):
- #print("Searching {0},found address is {1} and nearest ele is {2}".format(obj,mid,nums[mid]))
- return(mid)
- elif(nums[mid]<obj):
- left=mid+1
- else:
- right=mid-1
-
- # 考虑obj 大于最大数
- if(left>len(nums)-1):
- found=len(nums)-1
- # 考虑obj 小于最小数
- elif(right<0):
- found=0
- # 考虑最后一次比较是向右挪动
- elif(left>mid):
- if(abs(nums[left]-obj)<abs(nums[mid]-obj)):
- found=left
- else:
- found=mid
- # 考虑最后一次比较是向左挪动
- else:
- if(abs(nums[right]-obj)<=abs(nums[mid]-obj)):
- found=right
- else:
- found=mid
-
- return(found)
很多时候我们用二分查找是为了排序,我们需要在数列中插入一个新元素。在这种情况下,没找到相同的元素很正常,但是我们要找到一个合适的位置插入新元素,这时候如果找不到只返回 -1 这个地址是不够的,我们需要返回能插入的地址。
这时候就需要将问题转换成找到一个元素小于等于搜索的元素且最接近搜索的元素,并且考虑到插入算法的稳定性和减少挪动元素的概率,问题就转换成找一个元素小于等于搜索的元素且最接近搜索的元素,如果有多个相同的元素符合要求,那么找最右侧的元素。
代码实现:
- # 这个算法非常简单,只是在正序的列表中插入某个元素
- # 如果查找到有相同的元素,然后插入在相同元素的最右端
- # 如果没有合适的元素,则插入在小于该元素的最大元素后面即可
- def half_seek_forInsert(nums,obj):
- left=0
- right=len(nums)-1
- found=-1
-
- if( obj < nums[0] ):
- found=left
- elif(obj >nums[-1]):
- found=len(nums)
- else:
- while(left<=right):
- mid=(left+right)//2 # 中间的元素地址
-
- #
- if(nums[mid]==obj):
- found=mid #先记录这个查找到的地方
- left=mid+1 #继续向右查找
- elif(nums[mid]<obj):
- left=mid+1
- else:
- right=mid-1
-
- # 如果没有找到,但是left和right肯定是在要插入的元素附近
- if(found==-1):
- if((nums[left] <obj) and (nums[right] <obj)):
- found=left if left>right else right
- elif((nums[left] >obj) and (nums[right] >obj)):
- found=left if left<=right else right
- else:
- found=left if left>=right else right
-
- #将数据往后移动,空出 found 处填入 obj
- nums.append("end")
- k=len(nums)-1
- while(k>found):
- nums[k]=nums[k-1]
- k-=1
- nums[k]=obj
- return(nums)
这是一段测试代码,主要是测试插入的元素已经在数列中有的这种情况。
- a=[0, 1, 3, 4, 4, 7, 9, 9,9,9,10,10,10,10, 11, 12,66,70,82,100]
- b=list(set(a))
-
- for tmp in b:
- print("-"*90)
- a=[0, 1, 3, 4, 4, 7, 9, 9,9,9,10,10,10,10, 11, 12,66,70,82,100]
- print(a,tmp)
- print(half_seek_forInsert(a,tmp))
运行情况:
这是另外一段测试代码,主要是测试插入的元素不在数列中有的这种情况。
- a=[0, 1, 3, 4, 4, 7, 9, 9,9,9,10,10,10,10, 11, 12,66,70,82,100]
- b=[-1,-2,2,5,6,8,13,13,14,15,16,20,21,67,68,69,88,99,101,102]
-
-
- for tmp in b:
- print("-"*90)
- a=[0, 1, 3, 4, 4, 7, 9, 9,9,9,10,10,10,10, 11, 12,66,70,82,100]
- print(a,tmp)
- print(half_seek_forInsert(a,tmp))
运行情况:
'''
要是大家觉得写得还行,麻烦点个赞或者收藏吧,想个博客涨涨人气,非常感谢!
更欢迎大家一起来讨论,共同学习进步!
'''
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。