leetcode-34-在排序数组中查找元素的第一个和最后一个位置(四种方法)

该博客分析了如何在已排序数组中高效地找到目标值的第一个和最后一个位置。通过顺序遍历、双指针法、改进的二分查找以及单循环二分法,逐步提升算法效率,最终达到O(log n)的时间复杂度。文中提供了详细的解题思路和代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]。

进阶:

  • 你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?

示例1:

输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

示例2:

输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

示例3:

输入:nums = [], target = 0
输出:[-1,-1]

分析:

由于nums数组是已经排序过的,无需排序这个步骤。显而易见最容易的解决方式就是顺序遍历,找到最前和最后的两个位置。但是这样的方式非常不稳定,如果target在nums中靠前且相同值的数个数少将会非常快,反之则有可能非常慢,但是最慢不过O(n),可见慢的时候和快的时候差别十分明显。

#pragma region 暴力法,顺序遍历
/*
执行结果:通过
执行用时:28 ms, 在所有 C++ 提交中击败了16.11%的用户
内存消耗:13.8 MB, 在所有 C++ 提交中击败了13.11%的用户

最快的一次:
执行用时: 12 ms
内存消耗: 13.6 MB
*/
vector<int> searchRange(vector<int>& nums, int target) {
	if (nums.size() == 0)
		return vector<int>({ -1,-1 });
	int LIndex, RIndex;
	vector<int> retVec;
	bool Lfind = false, Rfind = false;
	for (int i = 0; i < nums.size(); ++i) {
		if (!Lfind) {
			if (nums[i] == target) {
				Lfind = true;
				LIndex = i;
			}
		}
		else {
			if (nums[i] != target) {
				Rfind = true;
				RIndex = i - 1;
				break;
			}
		}
	}
	if (Lfind) {
		retVec.push_back(LIndex);
		if (Rfind)
			retVec.push_back(RIndex);
		else
			retVec.push_back(nums.size() - 1);
	}else {
		retVec.push_back(-1);
		retVec.push_back(-1);
	}
	return retVec;
}
#pragma endregion

有没有办法让这个遍历的过程快一点呢?我们可以从头部尾部一起向中间逼近,这样循环次数减少了一半,而且对于target偏向nums某一边这种情况的处理也更加理想。不过还是很慢,甚至失去了第一种方法在极端情况下的超快速度。

#pragma region 暴力法,两边遍历
/*
执行结果:通过
执行用时:28 ms, 在所有 C++ 提交中击败了16.11%的用户
内存消耗:13.7 MB, 在所有 C++ 提交中击败了17.00%的用户
*/
vector<int> searchRange(vector<int>& nums, int target) {
	if (nums.size() == 0)
		return vector<int>({ -1,-1 });
	int LIndex, RIndex;
	vector<int> retVec;
	bool Lfind = false, Rfind = false;
	int i = 0,j = nums.size()-1;
	while(i<=j) {
//		cout << "before(i = " << i << ",j = " << j << ")" << endl;
		if (!Lfind) {
			if (nums[i] == target) {
				Lfind = true;
				LIndex = i;
			}else
				++i;
		}
		if (!Rfind) {
			if (nums[j] == target) {
				Rfind = true;
				RIndex = j;
			}else
				--j;
		}
//		cout << "after(i = " << i << ",j = " << j << ")" << endl;
		if (Lfind && Rfind) 
			break;
	}
	if (Lfind) {
		retVec.push_back(LIndex);
		retVec.push_back(RIndex);
	}
	else {
		retVec.push_back(-1);
		retVec.push_back(-1);
	}
	return retVec;
}
#pragma endregion

以上两种方法对于target在nums中部的情况束手无策,因此联想到可以利用二分法快速找到某个等于target的点,并以它为中心向两侧扩散。

#pragma region 二分查值并从中心扩散
/*
执行结果:通过
执行用时:24 ms, 在所有 C++ 提交中击败了42.54%的用户
内存消耗:13.8 MB, 在所有 C++ 提交中击败了6.65%的用户
*/
vector<int> searchRange(vector<int>& nums, int target) {
	//二分法找值并从中间扩散法
	if (nums.size() == 0)
		return vector<int>({-1,-1});
	vector<int> retVec;
	bool isFind = false;
	int MaxIndex = nums.size() - 1;
	int LIndex = 0;
	int RIndex = nums.size() - 1;
	int Index = (LIndex+RIndex) / 2;//开始位置的下标
	while(LIndex <= RIndex) {//查找等于target的点
		if (target == nums[Index]) {
			isFind = true;
			break;
		}
		else if (target < nums[Index]) {
			RIndex = Index - 1;
			Index = (LIndex+RIndex) / 2;
		}
		else {
			LIndex = Index + 1;
			Index = (LIndex + RIndex) / 2;
		}
	}
	if (!isFind) {
		retVec.push_back(-1);
		retVec.push_back(-1);
	}
	else {
//		cout << "Find Index is --(" << Index << ")" << endl;
		bool lend = false, rend = false;
		for (int i = Index,j = Index;; i--, j++) {
			if (i >= 0 && nums[i] == target) {
				LIndex = i;
			}
			else { lend = true; }
			if (j <= MaxIndex && nums[j] == target) {
				RIndex = j;
			}
			else { rend = true; }
			if (lend && rend) {
				break;
			}
		}
		retVec.push_back(LIndex);
		retVec.push_back(RIndex);
	}
	return retVec;
}
#pragma endregion

这样一来相比前两种方法稳定快捷了许多。但还不够,进阶中要求时间复杂度O(logn),看到logn不由得联想到二分法,我们可以分别利用二分法找到等于target的值的左右边界LIndex和RIndex,不过在判断二分条件时与通常的大于等于小于有所不同,对于LIndex的查找过程,我们可以利用类似下面的判断条件。

if (nums[LIndex] < target) {//目标值在右侧
}else if (nums[LIndex] == target && (LIndex == 0 || nums[LIndex-1] < target)) {//找到目标值
}else{}

因为等于target的值中序号最小的那一个元素的前面要么没有元素(序号为0),要么它的前一个元素小于target。

代码实现如下:

#pragma region 双二分(分开循环)
/*
执行结果:通过
执行用时:12 ms, 在所有 C++ 提交中击败了98.53%的用户
内存消耗:13.7 MB, 在所有 C++ 提交中击败了17.84%的用户

执行结果:通过
执行用时:16 ms, 在所有 C++ 提交中击败了92.37%的用户
内存消耗:13.6 MB, 在所有 C++ 提交中击败了50.85%的用户
*/
vector<int> searchRange(vector<int>& nums, int target) {//双二分(分开循环)
	if (nums.size() == 0)
		return vector<int>({ -1,-1 });
	vector<int> retVec;
	int LLowIndex = 0, RLowIndex = 0;
	int LHighIndex = nums.size() - 1, RHighIndex = nums.size() - 1;
	int LIndex = (LLowIndex + LHighIndex) / 2;
	int RIndex = (RLowIndex + RHighIndex) / 2;
	bool Lfind = false, Rfind = false;
	while (LLowIndex <= LHighIndex) {
		if (nums[LIndex] < target) {//目标值在右侧
			LLowIndex = LIndex + 1;
			LIndex = (LLowIndex + LHighIndex) / 2;
		}
		else if (nums[LIndex] == target && (LIndex == 0 || nums[LIndex-1] < target)) {//找到目标值
			Lfind = true;
			break;
		}
		else {//目标值在左侧
			LHighIndex = LIndex - 1;
			LIndex = (LLowIndex + LHighIndex) / 2;
		}
	}
	while (RLowIndex <= RHighIndex) {
		if (nums[RIndex] > target) {//目标值在左侧
			RHighIndex = RIndex - 1;
			RIndex = (RLowIndex + RHighIndex) / 2;
		}
		else if (nums[RIndex] == target && (RIndex == nums.size()-1 || nums[RIndex + 1] > target)) {//找到目标值
			Rfind = true;
			break;
		}
		else {//目标值在右侧
			RLowIndex = RIndex + 1;
			RIndex = (RLowIndex + RHighIndex) / 2;
		}
	}
	if (Lfind) {
		retVec.push_back(LIndex);
		retVec.push_back(RIndex);
	}
	else {
		retVec.push_back(-1);
		retVec.push_back(-1);
	}
	return retVec;
}
#pragma endregion

可以看到这个时候速度已经很快了,而且比较稳定,对于各种姿势的nums都能很好地驾驭。那么,可不可以更快呢?🙀,上面我们用了两个while循环来分别寻找LIndex和RIndex,下面用一个循环来试试。

#pragma region 双二分(合并循环)
/*
执行结果:通过
执行用时:4 ms, 在所有 C++ 提交中击败了99.99%的用户
内存消耗:13.8 MB, 在所有 C++ 提交中击败了10.24%的用户

最慢的一次
执行用时: 32 ms
内存消耗: 13.8 MB
*/
vector<int> searchRange(vector<int>& nums, int target) {//双二分(合并循环)
	if (nums.size() == 0)
		return vector<int>({ -1,-1 });
	vector<int> retVec;
	int LLowIndex = 0, RLowIndex = 0;
	int LHighIndex = nums.size() - 1, RHighIndex = nums.size() - 1;
	int LIndex = (LLowIndex + LHighIndex) / 2;
	int RIndex = (RLowIndex + RHighIndex) / 2;
	bool Lfind = false, Rfind = false;
	while (LLowIndex <= LHighIndex || RLowIndex <= RHighIndex) {
		if (Lfind && Rfind)
			break;
		if (!Lfind) {
			if (nums[LIndex] < target) {//目标值在右侧
				LLowIndex = LIndex + 1;
				LIndex = (LLowIndex + LHighIndex) / 2;
			}
			else if (nums[LIndex] == target && (LIndex == 0 || nums[LIndex - 1] < target)) {//找到目标值
				Lfind = true;
			}
			else {//目标值在左侧
				LHighIndex = LIndex - 1;
				LIndex = (LLowIndex + LHighIndex) / 2;
			}
		}
		if (!Rfind) {
			if (nums[RIndex] > target) {//目标值在左侧
				RHighIndex = RIndex - 1;
				RIndex = (RLowIndex + RHighIndex) / 2;
			}
			else if (nums[RIndex] == target && (RIndex == nums.size() - 1 || nums[RIndex + 1] > target)) {//找到目标值
				Rfind = true;
			}
			else {//目标值在右侧
				RLowIndex = RIndex + 1;
				RIndex = (RLowIndex + RHighIndex) / 2;
			}
		}
	}
	if (Lfind) {
		retVec.push_back(LIndex);
		retVec.push_back(RIndex);
	}else {
		retVec.push_back(-1);
		retVec.push_back(-1);
	}
	return retVec;
}
#pragma endregion

芜湖🛫!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值