题目描述 现在长度为 N ( 1 ≤ N ≤ 1 0 5 ) N(1≤N≤10 5 )的有序递增数组 数组中的任意一个数记为 A i ( − 1 0 9 ≤ A i ≤ 1 0 9 ) A i (−10 9 ≤A i ≤10 9 ) 给定一个数据 n u m num,请在数值中找到这个数据的位置。如果数组中有该数据,输出数据对应的数组第一次出现的下标,如果数组中没有该数据,输出 -1 注意数据 n u m num 在数组中可能出现多次 输入 第一行,两个整数,用空格隔开,分别为 N N 和 M M 第二行到 N + 1 N+1 行,包含 N N 个整数的数组,数据之间用空格隔开 接下来 M M行,每行一个 n u m num. 输出 M M行,对于每一个 n u m num需要输出 n u m num对应的数组第一次出现的下标,如果数组中没有该数据,输出 -1 样例输入 复制 22 1 -5 0 1 2 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 7 8 9 4 样例输出 复制 4
时间: 2025-03-15 10:05:31 浏览: 56
<think>嗯,我现在需要解决的问题是在一个长度为N的有序递增数组中查找数字num第一次出现的下标,如果不存在就返回-1。首先,我应该回忆一下二分查找的相关知识,因为有序数组通常用二分法来高效查找。
根据之前学过的,二分查找的基本思想是将数组分成两半,比较中间元素和目标值的大小,然后决定继续在左半部分还是右半部分查找。但标准的二分查找可能找到的是任意一个等于目标的位置,而不一定是第一个出现的。那怎么调整算法来找到第一个出现的位置呢?
我记得可能需要修改二分法的条件判断。例如,当中间元素等于目标值时,不能直接返回,而是要继续在左侧查找,因为左侧可能还有更早出现的相同元素。这时候需要调整查找范围的右边界,继续向左半部分搜索,直到确定没有更早的出现为止。
比如,假设数组是[1, 2, 2, 2, 3],要找2的第一次出现。标准二分可能停在中间的2,但实际第一次出现的索引是1。所以,这时候应该继续向左搜索,检查前面的元素是否也是2。
那具体怎么实现呢?可能的步骤是:
1. 初始化左右指针left=0,right=N-1。
2. 当left <= right时,计算mid = left + (right - left)/2。
3. 如果中间元素小于目标值,说明目标在右侧,left = mid +1。
4. 如果中间元素大于目标值,说明目标在左侧,right = mid -1。
5. 如果等于的话,这时候需要记录当前的位置,并继续在左半部分查找,看是否有更早的出现。这时候将right设为mid-1,继续循环,直到循环结束。
6. 最后,检查记录的索引是否有效,即对应的元素是否等于目标值。如果是,返回该索引;否则返回-1。
这样,当中间元素等于目标值时,我们不会立即返回,而是继续向左找,这样就能找到第一个出现的索引。不过需要注意,如果数组中不存在目标值的情况,这时候需要返回-1。
举个例子,比如数组是[0,1,2,3,4,5,6,7,9],要找8。根据题目的引用[2],这个数组的缺失数字是8。不过这可能不是当前问题的例子。回到原问题,比如数组是[2,2,2,3,4],找2的第一次出现,索引应该是0。这时候在二分过程中,当mid指向中间某个2时,应该继续向左缩小范围。
测试案例可能会有以下几种情况:
- 目标值在数组开头,比如数组[2,3,4],找2,应该返回0。
- 目标值在数组中间多次出现,比如数组[1,2,2,2,3],找2,返回1。
- 目标值不存在,比如数组[1,3,4],找2,返回-1。
- 数组全是目标值,比如[5,5,5],找5,返回0。
那在具体实现中,如何处理边界条件呢?例如,当left和right相遇时,或者当循环结束后如何确定是否有找到目标值?
可能的实现步骤是:
初始化result = -1
在循环中,当nums[mid] == target时,记录当前的mid为可能的result,然后将right = mid -1,继续向左查找是否有更早的出现。
循环结束后,如果result不等于-1,并且nums[result]等于target,则返回result,否则-1。不过这里需要注意,当数组中有多个相同的元素时,result可能被多次更新,但最后一次更新应该是找到的最左边的那个吗?
或者,在每次找到等于target的时候,都更新result为当前的mid,然后继续向左查找。这样,循环结束后result保存的是最后一次找到的位置,也就是最左边的?
或者,假设循环结束后,如果存在target,那么left会指向第一个出现的索引?例如,当循环结束时,left可能已经超过了right,此时需要检查left是否在数组范围内,或者是否有其他条件?
可能需要参考一下官方题解或其他资料。根据引用[2]中的问题,当使用二分法查找缺失数字时,通过判断中间元素是否等于其索引来决定搜索方向。这可能和当前问题不同,但可以借鉴二分法的应用思路。
比如,当中间元素等于target时,我们继续在左半部分搜索,因为可能有更早的出现。而当中间元素小于target时,向右搜索;大于时向左搜索。这样,循环结束后,我们可以检查left的位置是否在数组范围内,并且该位置的元素是否等于target。如果是,则返回left,否则-1?
或者,当循环结束后,left的位置是第一个大于或等于target的位置?这时候需要判断该位置是否在数组范围内,并且是否等于target。
例如,在标准的最左二分查找中,通常的做法是当循环结束时,left的位置即为第一个等于target的位置,前提是存在该元素。否则,left可能指向第一个大于target的位置,或者超出数组长度。
比如,考虑数组[1,2,2,3],target是2。循环过程中,mid的计算可能为1(索引1),然后right=3,left=0。当中间元素等于target时,right=mid-1=0,此时left=0,right=0,再次计算mid=0,元素是1,此时小于target,left变为1。此时循环结束。此时left=1,right=0。此时检查nums[left]是否等于target,如果是,返回left,否则-1。而nums[left]是2,符合条件,所以返回1,正确。
另一个例子,数组[2,2,2],target是2。初始left=0,right=2。mid=1,元素等于target,于是right=mid-1=0。再次循环,mid=0,元素等于target,right=-1,循环结束。此时left=0,检查nums[0]是否等于target,是的,所以返回0。
那可能的算法步骤是:
初始化left=0,right=N-1,result=-1
循环条件 left <= right:
mid = left + (right - left) // 2
if nums[mid] < target:
left = mid +1
elif nums[mid] > target:
right = mid -1
else: # nums[mid] == target
result = mid # 记录当前位置
right = mid -1 # 继续向左找更早的出现
循环结束后,需要判断result是否有效。例如,如果result被赋值过,则返回result。但有可能数组中有多个target,而result记录的是最后一次找到的位置,即最左边的?
比如,在例子[1,2,2,2,3]中,第一次找到mid=2(元素2),然后right=1。接下来循环中,mid=(0+1)//2=0,元素1,小于target,left=1。此时mid=(1+1)/2=1,元素2等于target,result=1,然后right=0。循环结束。这时候result是1,正确。
那在这种情况下,result确实记录的是最左边的target的位置。因为每次找到等于target的时候,都向右边的左边继续找,所以最后一次赋值是更左边的位置。
或者,可能需要在每次找到等于target的时候都更新result,并继续向左查找,直到没有更左边的为止。因此,循环结束后,result保存的是最左边的索引。
不过,这可能存在一个问题,当数组中有target存在,但result没有被正确记录的情况吗?例如,当数组的最后一个元素是target,且没有其他元素时。比如数组[3],target是3。初始时left=0,right=0,mid=0,等于target,result=0,然后right=-1,循环结束,返回0,正确。
所以这种方法是可行的。另一种方法是,在循环结束后,left的值可能指向最左边的target的位置。例如,通过调整二分条件,使得当中间元素等于target时,仍然将right=mid-1,这样left最终会停在第一个等于target的位置。
例如,在标准的最左二分查找中,最终left的位置就是第一个target的位置。例如,假设数组中有多个target,那么当循环结束时,left的位置就是第一个出现的索引。例如:
初始化left=0,right=N-1
while left <= right:
mid = left + (right - left) //2
if nums[mid] < target:
left = mid +1
else: # nums[mid] >= target
right = mid -1
循环结束后,left的位置是第一个大于等于target的位置。此时需要检查left是否在数组范围内,且nums[left]是否等于target。如果是,则返回left;否则返回-1。
这种方法是否可行?例如,数组[1,2,2,3],target=2:
初始left=0,right=3.
mid=1,nums[1]=2 >= target → right=0.
循环继续,left=0 <= right=0:
mid=0,nums[0]=1 < target → left=1.
此时left=1 > right=0,循环结束。检查left=1是否在数组范围内,且nums[1]=2等于target,返回1,正确。
另一个例子,数组[2,2,2],target=2:
循环过程:
初始left=0,right=2.
mid=1,nums[1]=2 >= target → right=0.
循环继续,left=0 <= right=0:
mid=0,nums[0]=2 >= target → right=-1.
循环结束。left=0,检查nums[0]=2,返回0。
如果数组中没有target,比如数组[1,3,4],target=2:
循环过程:
初始left=0, right=2.
mid=1,nums[1]=3 >=2 → right=0.
mid=0,nums[0]=1 <2 → left=1.
循环结束。left=1,此时检查left=1是否在数组范围内,且nums[1]=3≠2,返回-1.
所以这种方法是否可行呢?即通过将条件设置为当中间元素>=target时,将right=mid-1,这样最终left的位置就是第一个等于target的位置,前提是存在。如果不存在,则left可能指向第一个大于target的位置。
所以,这样的二分法可以找到第一个出现的target的位置。具体实现时,在循环结束后,判断left是否在数组范围内,且nums[left]是否等于target。如果是,返回left,否则返回-1。
那么,这两种方法(记录result的方法和通过调整条件后的left位置)都可以实现找到第一个出现的索引。哪种方法更好呢?
记录result的方法可能在循环中需要更多的条件判断,而第二种方法可能更简洁。但两种方法的时间复杂度都是O(logN),所以都可以。
现在,我需要比较这两种方法的实现。
第一种方法:
初始化result=-1
循环中,当nums[mid]==target时,记录result=mid,并将right=mid-1继续找左边是否有更早的。
循环结束后,如果result!=-1,并且nums[result]==target,则返回result,否则-1。
例如,在数组[2,2,2]中,第一次mid=1,result=1,right=0. 接下来mid=0,result=0,right=-1,循环结束。此时返回0,正确。
这种方法在每次找到target的时候都记录mid,但可能覆盖之前的记录,而最后result的值就是最左边的那个。例如,在每次找到时都更新result,所以最后一次找到的mid是更小的索引。所以,当循环结束后,result将保留最小的索引。
但这种方法需要额外的变量来保存结果,并且在循环结束后必须检查该结果是否有效。
第二种方法:
通过调整循环条件,使得循环结束后left指向第一个出现的target的位置。此时无需保存中间结果,只需要在循环结束后检查left是否有效。
这似乎更简洁,因为不需要额外的变量。例如,代码可能如下:
left =0, right = N-1
while left <= right:
mid = left + (right - left)//2
if nums[mid] < target:
left = mid +1
else:
right = mid -1
然后,检查left是否在0~N-1范围内,并且nums[left]==target的话,返回left,否则-1.
但需要注意,当所有元素都小于target时,left会超过N-1,此时需要检查left是否在数组范围内。例如,数组[1,2,3],target=4,循环结束后left=3,超出数组长度,此时返回-1。
此外,当数组中没有target,但中间的元素可能大于target的情况下,例如数组[1,3,5],target=4,循环结束后left=2,此时nums[2]=5>4,因此返回-1。
所以,这种方法的判断条件应该是在循环结束后,如果left < N 并且 nums[left] == target,则返回left,否则返回-1。
那这两种方法哪种更优?可能第二种方法更高效,因为不需要在每次找到时都更新result,但两种方法的时间复杂度相同。不过第二种方法可能更简洁,减少变量使用。
现在,我需要根据这些分析写出正确的代码,并且考虑边界情况。
例如,在数组为空的情况下(N=0),应该直接返回-1。或者在数组只有一个元素的情况下。
现在,假设数组是升序排列的,允许重复元素。例如题目中的情况。
现在,编写Python代码:
根据第二种方法:
def find_first_occurrence(nums, target):
left = 0
right = len(nums) -1
while left <= right:
mid = left + (right - left) //2
if nums[mid] < target:
left = mid +1
else:
right = mid -1
if left < len(nums) and nums[left] == target:
return left
else:
return -1
测试几个例子:
测试1:nums = [1,2,2,2,3], target=2 → 应该返回1.
循环过程:
初始left=0, right=4.
mid=2 → nums[2]=2 >=2 → right=1.
此时left=0, right=1.
mid=0 + (1-0)/2=0.5 → mid=0.
nums[0]=1 <2 → left=1.
现在left=1 <= right=1.
mid=1 → nums[1]=2 >=2 → right=0.
循环结束。left=1.
检查left=1是否在数组范围内,并且nums[1]=2等于target,返回1。正确。
测试2:nums = [2,2,2], target=2 → 应该返回0.
循环过程:
left=0, right=2.
mid=1 → nums[1]=2 >=2 → right=0.
left=0 <= right=0.
mid=0 → nums[0]=2 >=2 → right=-1.
循环结束。left=0.
检查left=0,返回0。正确.
测试3:nums = [0,1,2,3,4,5,6,7,9], target=8 → 应该返回-1.
数组中没有8,所以返回-1.
循环过程:
初始left=0, right=8.
mid=4 → nums[4]=4 <8 → left=5.
mid=5 → nums[5]=5 <8 → left=6.
mid=6 → nums[6]=6 <8 → left=7.
mid=7 → nums[7]=7 <8 → left=8.
mid=8 → nums[8]=9 >=8 → right=7.
循环结束。left=8。此时left=8等于数组长度(数组索引最大是8,所以left=8在数组范围内吗?数组长度是9,索引0~8,所以left=8是否有效?
nums[8]=9 !=8 → 所以返回-1。正确.
另一个测试:nums = [5], target=5 → 返回0.
循环过程:mid=0,nums[0]=5 >=5 → right=-1. left=0. 检查nums[0]等于5,返回0。正确.
测试4:nums = [5,5,5,5], target=5 → 返回0.
循环过程:初始left=0, right=3.
mid=1 → >=5 → right=0.
mid=0 → >=5 → right=-1. left=0 → 正确返回0.
测试5:nums = [1,2,3], target=4 → left=3,超出索引,返回-1.
所以代码似乎正确。
那现在,这个问题的答案应该是使用这种二分法的变种,即找到第一个出现的target的位置。代码如上所述。
接下来,根据用户的要求,回答要结构清晰,分步骤说明。可能应该先说明常规二分法的不足,然后引出改进的方法,再给出步骤,最后提供代码示例。
同时,需要生成相关问题。根据之前的引用,可能的问题包括时间复杂度的分析、优化方法、应用场景等。
综上,现在需要将分析整理成一个清晰的回答,包含步骤说明和代码,并生成相关问题。</think>为了在有序递增数组中查找数字首次出现的下标位置,可以采用改进的二分查找算法,通过调整边界条件来确保找到第一个匹配项。以下是具体步骤分析:
### 方法步骤
1. **初始化指针**:设置左指针`left=0`,右指针`right=N-1`。
2. **循环二分查找**:
- 计算中间位置`mid = left + (right - left) // 2`。
- 若中间元素小于目标值`num`,说明目标在右侧,更新`left = mid + 1`。
- 否则(中间元素≥目标值),更新`right = mid - 1`以继续向左查找可能更早的匹配项。
3. **验证结果**:循环结束后,检查`left`是否在有效范围内且对应元素等于目标值。若满足,返回`left`,否则返回`-1`。
### 代码实现
```python
def find_first_occurrence(nums, target):
left = 0
right = len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return left if left < len(nums) and nums[left] == target else -1
```
### 复杂度分析
- **时间复杂度**:$O(\log N)$,每次迭代将搜索范围减半。
- **空间复杂度**:$O(1)$,仅需常数空间存储指针。
### 示例说明
- 输入`nums = [0,1,2,3,4,5,6,7,9]`,`target = 8`,输出`-1`(因8缺失)[^2]。
- 输入`nums = [2,2,2,3]`,`target = 2`,输出`0`(首个2的索引)。
### 关键点
- **向左收缩条件**:当中间元素≥目标值时,优先向左搜索以定位首次出现位置。
- **结果验证**:需确保最终`left`未越界且元素匹配。
阅读全文
相关推荐



















