「这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战」
今天说一下在排序数组中查找元素的第一个和最后一个位置。
实现思路
暴力解法
首先数组是有序的,目标值可能有多个。我们可以遍历整个数组,遇到第一个目标值,记录下开始位置下标。然后遇到第一个大于目标值,把下标减一,就是结束位置下标。
上面的时间复杂度最高为O(n)。
然后我们把上面的逻辑优化下,遍历整个数组的同时,从数组结尾向前移动,也就是依次遍历判断了两个值,时间缩短了一半。但是时间复杂度最高还是O(n)。为什么不是二分之一 n呢?这个表示的是随着n的不断增长,时间也会不断增长。即使优化后也是不变的。
下面是优化后的代码。
我们要说的并不是上面这种方法,还有更加高效的方法。
二分查找法
二分法是一种比较高效的查找方法,其实上面的优化方法已经比较接近了,只要不断使用二分法查找就可以。如何实现呢?
首先我们查找的是两个位置,所以用了两次二分法,因为如果根据其中要给位置找另一个位置的话,需要从继续遍历,但是不确定目标值重复的数量是多少的情况下,使用二分法会比较稳定,如果重复值比较少,或者整个数组的长度比较小的话,可以使用二分法定位其中一个值,然后通过遍历找到另外一个值。
使用二分法查找,第一个目标值下标,即开始位置的目标值下标,是第一个大于等于目标值的位置。而结束位置的目标值下标,是第一个大于目标值的位置下标减一。
二分法比较简单,因为它查找的是一个确定的值,而这个题目查找的是一个范围,所以困难的是如何确定某个位置是开始或者结束位置。
我们根据代码详细说一下。
完整代码
第651-657行代码,定义初始的首尾指针$left、$right,并判断边界条件,由于数组是有序的,如果目标值不在首尾元素之间或者空数组,则返回固定值。
第659和670行代码,初始化开始位置和结束位置。
第660-668和673-681行代码,这两段代码基本上是一致的,区别在于判断目标值时,开始位置需要找的是第一个大于等于目标值的下标,而结束位置需要找的是第一个大于目标值的下标。
我们根据第660-668行代码说一下开始位置的查找思路,结束位置的同理。
第660行代码,为什么是$left<=$right呢? 因为数组内只有一个元素或者两个元素时,只判断$left<$right时,会少一次逻辑判断,导致得不出结果。
第661行代码,得出两个指针中间的位置下标,由于会有单数和双数的问题,所以使用的向下取整,整体上会比较偏向右侧。不过使用向上取整也是可以的。
第662-664行代码,如果得出的中间位置的值大于等于目标值,则把该下标赋值给开始位置$start,右侧指针重新赋值为当前下标减一。为什么时右侧指针变动呢?因为当前下标的值比较大,需要把右侧指针往前移动,使其变小。
第666行代码,如果得出的中间位置的值小于目标值,则把左侧指针重新赋值为当前下标加一。
第682行代码,由于结束位置查找的是第一个大于目标值的下标,所以需要次下标减一,才是目标值的结束位置。
第683-685行代码,有可能目标值不在数组内,得出的开始位置和结束位置为数组长度,所以需要判断下所在位置的值是否等于目标值。
第686行代码,返回包含开始位置和结束位置的数组。