🔥个人主页: 中草药
🔥专栏:【算法工作坊】算法实战揭秘
🏟️一. 二分查找
题目链接:704.二分查找
算法原理
二分查找算法的基本思想是通过将搜索区间分成两半,然后根据目标值与中间值的比较结果来缩小搜索范围,从而减少搜索次数。具体步骤如下:
- 初始化两个指针,
left
和right
,分别指向数组的起始位置和结束位置。 - 当
left <= right
时,执行以下操作:- 计算中间位置
mid = left + (right - left) / 2
。这里使用(right - left) / 2
而不是(left + right) / 2
是为了避免在left
和right
都非常大的情况下发生整数溢出。 - 检查中间位置的元素
nums[mid]
是否等于目标值target
。- 如果
nums[mid] < target
,说明目标值可能在右半部分,因此更新left
为mid + 1
。 - 如果
nums[mid] > target
,说明目标值可能在左半部分,因此更新right
为mid - 1
。 - 如果
nums[mid] == target
,则找到目标值,返回其索引mid
。
- 如果
- 计算中间位置
- 如果没有找到目标值,即循环结束后,返回
-1
表示目标值不在数组中。
代码分析
- 初始化:
left = 0
和right = nums.length - 1
,定义了搜索的初始范围。 - 循环条件:
while (left <= right)
确保搜索不会超出范围,并且当left
和right
相遇或交叉时停止。 - 中间点计算:
int mid = left + (right - left) / 2
,避免整数溢出。 - 比较与更新:根据
nums[mid]
与target
的关系,更新left
或right
,逐步缩小搜索范围。 - 返回结果:如果找到目标值,返回其索引;否则,返回
-1
表示未找到。
二分查找的时间复杂度为 O(log n),其中 n 是数组长度,是一种高效的查找算法。
代码
public int search(int[] nums, int target) {
int left=0,right=nums.length-1;
while(left<=right){
int mid=left+(right-left)/2;
if (nums[mid]<target){
left=mid+1;
}else if (nums[mid]>target){
right=mid-1;
}else {
return mid;
}
}
return -1;
}
举例
测试用例 nums=[-1,0,3,5,9,12] , target=9
当使用测试用例 nums = [-1, 0, 3, 5, 9, 12]
和 target = 9
分析这段二分查找代码时,我们可以逐步跟踪算法的执行过程:
-
初始化:
left = 0
,right = nums.length - 1 = 5
-
第一次循环:
- 计算
mid
:mid = left + (right - left) / 2 = 0 + (5 - 0) / 2 = 2
- 比较
nums[mid]
与target
:nums[mid] = nums[2] = 3
小于target = 9
- 更新
left
:left = mid + 1 = 3
- 计算
-
第二次循环:
- 更新后的
left = 3
,right = 5
- 计算
mid
:mid = left + (right - left) / 2 = 3 + (5 - 3) / 2 = 4
- 比较
nums[mid]
与target
:nums[mid] = nums[4] = 9
等于target = 9
- 找到目标值,返回
mid = 4
- 更新后的
因此,在这个测试用例中,算法最终会返回 4
,这是目标值 9
在数组中的索引位置。
这个过程展示了二分查找如何有效地将搜索范围不断减半,直到找到目标值或确定目标值不存在于数组中。在这个例子中,我们从整个数组开始,经过两次比较就找到了目标值,这体现了二分查找算法的高效性。
🏡二.在排序数组中寻找第一个和最后一个位置
算法原理
这段代码实现了在有序数组中查找一个给定目标值的起始位置和结束位置的算法,即找出目标值出现的第一个和最后一个位置。算法主要分为两个部分:查找左端点和查找右端点,这里使用了修改版的二分查找算法。
查找左端点
为了找到目标值的最左侧位置,我们需要确保在 nums[mid] >= target
的情况下,仍然有机会向左移动,因为可能存在 nums[mid] == target
但是 mid
不是最左侧的情况。因此,这里的循环条件是 left < right
,并且在 nums[mid] >= target
的情况下,将 right
设置为 mid
,而不是常见的 mid - 1
。这样可以确保即使 nums[mid]
等于目标值,我们也会继续在左边查找,直到找到第一个等于目标值的元素或者 left
和 right
相遇。
查找右端点
查找右端点的过程与查找左端点类似,但是有几个关键区别:
- 循环条件同样为
left < right
,但计算mid
时加上1
:mid = left + (right - left + 1) / 2
。这是因为我们要找的是目标值的最右侧位置,如果不加1
,可能会陷入无限循环,尤其是在数组中目标值重复的情况下。 - 在
nums[mid] <= target
的情况下,我们将left
设置为mid
,而不是常见的mid + 1
。这样可以确保即使nums[mid]
等于目标值,我们也会继续在右边查找,直到找到最后一个等于目标值的元素或者left
和right
相遇。
代码分析
-
初始化:
- 定义返回数组
ret
初始值为[-1, -1]
。 - 检查边界情况,如果数组为空,则直接返回
ret
。
- 定义返回数组
-
查找左端点:
- 使用二分查找,不断更新
left
和right
,直到找到目标值的第一个位置,或确认目标值不存在。 - 如果找到目标值,更新
ret[0]
为该位置。
- 使用二分查找,不断更新
-
查找右端点:
- 重新初始化
left
和right
,再次进行二分查找,但这次是为了找到目标值的最后一个位置。 - 更新
ret[1]
为该位置。
- 重新初始化
这种算法的时间复杂度为 O(log n),其中 n 是数组的长度,因为它对每个端点都进行了独立的二分查找。这种方法适用于处理需要同时找到目标值起止位置的问题,例如在统计目标值出现次数或确定目标值的范围等场景。
代码
public int[] searchRange(int[] nums, int target) {
int[] ret={-1,-1};
//判断边界情况
if(nums.length==0){
return ret;
}
//寻找左端点
int left=0,right=nums.length-1;
while(left<right){
int mid=left+(right-left)/2;
if(nums[mid]<target){
left=mid+1;
}else{
right=mid;
}
}
//判断是否有符合要求的值
if(nums[left]==target){
ret[0]=left;
}else{
return ret;
}
//寻找右端点
left=0;right=nums.length-1;
while(left<right){
int mid=left+(right-left+1)/2;//寻找右端点防止死循环
if(nums[mid]<=target){
left=mid;
}else{
right=mid-1;
}
}
ret[1]=left;
return ret;
}
举例
测试用例 nums=[5,7,7,8,8,10],targer=8
使用测试用例 nums = [5, 7, 7, 8, 8, 10]
和 target = 8
来分析上述代码,我们可以详细地跟踪代码的执行流程,以便理解它是如何找到目标值 8
的起始位置和结束位置的。
查找左端点
- 初始化:
left = 0
,right = nums.length - 1 = 5
- 第一次循环:
- 计算
mid
:mid = left + (right - left) / 2 = 0 + (5 - 0) / 2 = 2
- 比较
nums[mid]
与target
:nums[mid] = nums[2] = 7
小于target = 8
- 更新
left
:left = mid + 1 = 3
- 计算
- 第二次循环:
- 更新后的
left = 3
,right = 5
- 计算
mid
:mid = left + (right - left) / 2 = 3 + (5 - 3) / 2 = 4
- 比较
nums[mid]
与target
:nums[mid] = nums[4] = 8
等于target = 8
- 更新
right
:right = mid = 4
- 更新后的
- 第三次循环:
- 更新后的
left = 3
,right = 4
- 因为
left < right
不再成立,循环结束 - 检查
nums[left]
:nums[left] = nums[3] = 8
等于target = 8
- 更新
ret[0]
为left = 3
- 更新后的
查找右端点
- 初始化:
- 再次设置
left = 0
,right = nums.length - 1 = 5
- 再次设置
- 第一次循环:
- 计算
mid
:mid = left + (right - left + 1) / 2 = 0 + (5 - 0 + 1) / 2 = 3
- 比较
nums[mid]
与target
:nums[mid] = nums[3] = 8
小于等于target = 8
- 更新
left
:left = mid = 3
- 计算
- 第二次循环:
- 更新后的
left = 3
,right = 5
- 计算
mid
:mid = left + (right - left + 1) / 2 = 3 + (5 - 3 + 1) / 2 = 4
- 比较
nums[mid]
与target
:nums[mid] = nums[4] = 8
小于等于target = 8
- 更新
left
:left = mid = 4
- 更新后的
- 第三次循环:
- 更新后的
left = 4
,right = 5
- 计算
mid
:mid = left + (right - left + 1) / 2 = 4 + (5 - 4 + 1) / 2 = 5
- 比较
nums[mid]
与target
:nums[mid] = nums[5] = 10
大于target = 8
- 更新
right
:right = mid - 1 = 4
- 更新后的
- 第四次循环:
- 更新后的
left = 4
,right = 4
- 因为
left < right
不再成立,循环结束
- 更新后的
此时,left
的值即为右端点的索引值。
结果
最终,ret[0]
被设置为 3
,ret[1]
被设置为 4
,表示目标值 8
在数组中的起始位置为 3
,结束位置为 4
。因此,函数返回 [3, 4]
。
🏣三.x的平方根
题目链接:69.x的平方根
算法原理
二分查找通常用于在有序数组中查找特定元素,但在这里被创造性地用来查找一个数值。算法通过不断地将搜索空间减半来缩小目标值的范围,直到找到满足条件的解。
查找平方根的适应
对于查找平方根问题,搜索空间是所有可能的整数,从 0
到 x
(包括 x
)。算法初始化两个指针 left
和 right
,分别指向搜索区间的两端。每次迭代中,算法计算中间点 mid
,并检查 mid * mid
是否小于等于 x
。基于比较结果,算法会更新搜索区间的一端,从而缩小搜索范围。
特殊处理
- 初始条件:如果
x
小于1
,直接返回0
,因为0
和1
的平方根都是它们自身。 - 计算中间值:
mid = left + (right - left + 1) / 2
,这里加1
是为了防止在某些情况下陷入死循环,确保在left
和right
相邻时能够正确地选择较大的那个作为结果。 - 终止条件:循环直到
left
不再小于right
,这时left
指向的就是满足条件的最大整数。
代码分析
- 边界条件处理:如果输入
x
小于1
,立即返回0
。 - 初始化:设置
left = 0
和right = x
,定义搜索范围。 - 循环:只要
left
小于right
,就继续循环。- 计算
mid
:mid = left + (right - left + 1) / 2
,确保mid
始终偏向右侧,避免死循环。 - 检查
mid
的平方是否小于等于x
:- 如果是,说明
mid
可能是解的一部分,更新left = mid
。 - 如果不是,说明
mid
太大,更新right = mid - 1
。
- 如果是,说明
- 计算
- 返回结果:循环结束时,
left
指向的就是满足条件的最大整数,将其转换为int
类型并返回。
这种算法的时间复杂度为 O(log x),因为每次迭代都将搜索空间减半。它是一种高效且精确的方法来计算整数平方根。
代码
public int mySqrt(int x) {
//处理细节
if(x<1){
return 0;
}
long left=0,right=x;
while(left<right){
long mid=left+(right-left+1)/2;
if(mid*mid<=x){//由于如果数据很大,mid*mid很容易溢出 所以用long类型
left=mid;
}else{
right=mid-1;
}
}
return (int)left;//为了符合要求,最后再进行强转
}
举例
测试用例 x = 8
使用测试用例 x = 8
来分析上述代码,我们可以追踪二分查找算法在寻找整数平方根时的具体步骤。
初始化
left = 0
right = x = 8
第一次循环
- 计算
mid
:mid = left + (right - left + 1) / 2 = 0 + (8 - 0 + 1) / 2 = 4.5
,但由于mid
是长整型变量,实际取值为5
- 比较
mid * mid
与x
:mid * mid = 5 * 5 = 25
,大于x = 8
- 更新指针:
right = mid - 1 = 4
第二次循环
- 计算
mid
:mid = left + (right - left + 1) / 2 = 0 + (4 - 0 + 1) / 2 = 2.5
,实际取值为3
- 比较
mid * mid
与x
:mid * mid = 3 * 3 = 9
,大于x = 8
- 更新指针:
right = mid - 1 = 2
第三次循环
- 计算
mid
:mid = left + (right - left + 1) / 2 = 0 + (2 - 0 + 1) / 2 = 1.5
,实际取值为2
- 比较
mid * mid
与x
:mid * mid = 2 * 2 = 4
,小于等于x = 8
- 更新指针:
left = mid = 2
第四次循环
- 此时,
left = 2
和right = 2
,即left
和right
已经相等。 - 计算
mid
:mid = left + (right - left + 1) / 2 = 2 + (2 - 2 + 1) / 2 = 2.5
,实际取值为3
- 比较
mid * mid
与x
:mid * mid = 3 * 3 = 9
,大于x = 8
- 更新指针:
right = mid - 1 = 2
,但实际上right
的值不会改变,因为right
和left
已经相等。
结束循环
由于 left
和 right
相等,下一次循环条件 left < right
不再满足,循环结束。
返回结果
- 最终
left
的值为2
,即x = 8
的整数平方根最大值,满足y * y <= x
的条件。 - 函数返回
2
作为结果。
因此,对于输入 x = 8
,代码返回的整数平方根是 2
,这是正确的结果,因为 2 * 2 = 4
小于等于 8
,而 3 * 3 = 9
大于 8
。
🏪四.搜索插入位置
题目链接:35.搜索插入位置
算法原理
标准二分查找
基本思路与标准二分查找类似,初始化两个指针 left
和 right
,分别指向数组的起始位置和结束位置。在每一步中,算法计算中间位置 mid
,并根据 nums[mid]
与 target
的大小关系调整搜索范围,直到找到目标值或确定目标值应该插入的位置。
查找插入位置
对于查找插入位置问题,关键在于如何处理 nums[mid]
与 target
相等的情况。在标准二分查找中,我们通常会在找到目标值后立即返回。但在查找插入位置的场景下,即使 nums[mid]
等于 target
,我们也需要继续在左侧查找,以确定 target
的最左侧插入位置(即第一个等于 target
的位置,或第一个大于 target
的位置之前的那个位置)。
边界情况处理
- 目标值大于数组中所有元素:在进入循环前,代码首先检查如果
target
大于数组中的最大值 (nums[right]
),则直接返回数组长度作为插入位置,这是数组末尾的下一个位置。 - 循环终止条件:循环在
left
不小于right
时结束,此时left
指向的位置就是target
应该插入的位置。
代码分析
- 边界条件处理:如果
target
大于数组中的最大值,直接返回数组长度作为插入位置。 - 初始化:设置
left = 0
和right = nums.length - 1
,定义搜索范围。 - 循环:只要
left < right
,就继续循环。- 计算
mid
:mid = left + (right - left) / 2
,避免整数溢出。 - 检查
nums[mid]
与target
的大小关系:- 如果
nums[mid]
小于target
,说明target
可能位于mid
的右侧,更新left = mid + 1
。 - 如果
nums[mid]
不小于target
,说明target
可能位于mid
的左侧或等于mid
,更新right = mid
。
- 如果
- 计算
- 返回结果:循环结束时,
left
指向的就是target
应该插入的位置,直接返回left
。
这种算法的时间复杂度为 O(log n),其中 n 是数组的长度,因为它每次迭代都将搜索空间减半。这是一个非常高效的解决方案,尤其适用于大型数据集。
代码
public int searchInsert(int[] nums, int target) {
int left=0,right=nums.length-1;
if(target>nums[right]){
return nums.length;
}
while(left<right){
int mid=left+(right-left)/2;
if(nums[mid]<target){
left=mid+1;
}else{
right=mid;
}
}
return left;
}
举例
测试用例 nums = [1,3,5,6], target = 5
初始化
left = 0
right = nums.length - 1 = 3
第一次循环
- 计算
mid
:mid = left + (right - left) / 2 = 0 + (3 - 0) / 2 = 1.5
,向下取整为1
- 比较
nums[mid]
与target
:nums[mid] = nums[1] = 3
,小于target = 5
- 更新指针:
left = mid + 1 = 2
第二次循环
- 计算
mid
:mid = left + (right - left) / 2 = 2 + (3 - 2) / 2 = 2.5
,向下取整为2
- 比较
nums[mid]
与target
:nums[mid] = nums[2] = 5
,等于target = 5
- 更新指针:
right = mid = 2
第三次循环
在第二次循环中,由于 nums[mid]
等于 target
,算法将 right
设置为 mid
。但在下一次循环开始时,left
和 right
的值相同,即 left = right = 2
。这导致循环条件 left < right
不再满足,循环终止。
返回结果
- 循环结束后,
left
的值为2
,这是target = 5
应该插入的位置,因为在nums
数组中,5
正好位于第2
个位置(基于0
索引)。
总结
对于输入 nums = [1, 3, 5, 6]
和 target = 5
,代码正确地返回了 2
,这是 5
在数组中的位置,同时也是如果数组中不存在 5
时,5
应该被插入的位置以保持数组的升序状态。这个算法有效地利用了二分查找的特性,以对数时间复杂度 O(log n) 确定了目标值的正确位置。
🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀
以上,就是本期的全部内容啦,若有错误疏忽希望各位大佬及时指出💐
制作不易,希望能对各位提供微小的帮助,可否留下你免费的赞呢🌸