🔥个人主页: 中草药
🔥专栏:【算法工作坊】算法实战揭秘
一.山脉数组的峰顶索引
题目链接:852.山脉数组的峰顶
算法原理
这段代码实现了一个查找山峰数组中峰值索引的算法。山峰数组是一个先递增后递减的数组,即存在一个索引 i
使得对于所有的 j < i
,有 arr[j] < arr[j + 1]
,且对于所有的 k > i
,有 arr[k] > arr[k - 1]
。这个索引 i
就是峰值的索引。
算法使用了二分查找(Binary Search)的方法来寻找峰值索引。其核心思想是在数组中寻找拐点(即峰值),在该点左侧的值小于右侧的值,在右侧则相反。由于数组是先升后降的,这个拐点就是我们所要找的峰值。
具体分析如下:
-
初始化两个指针
left
和right
,分别指向数组的第二个元素和倒数第二个元素。这是因为数组的第一个和最后一个元素不可能是峰值。 -
在
while
循环中,计算中间位置mid
。这里使用(left + (right - left + 1)) / 2
而不是常见的(left + right) / 2
来避免可能的整数溢出,并确保mid
总是指向left
和right
之间的元素,包括边界上的元素。 -
如果
arr[mid]
大于arr[mid - 1]
,说明mid
可能是峰值或者峰值在mid
的右边,因此将left
更新为mid
。 -
否则,如果
arr[mid]
小于或等于arr[mid - 1]
,说明峰值在mid
的左边,因此将right
更新为mid - 1
。 -
当
left
和right
相遇时,循环结束,此时left
指向的位置就是峰值的索引。
这种算法的时间复杂度是 O(log n),其中 n 是数组的长度,因为每次迭代都将搜索范围减半。这比线性搜索的 O(n) 时间复杂度要高效得多。
代码
public int peakIndexInMountainArray(int[] arr) {
int left=1,right=arr.length-2;
while(left<right){
int mid=left+(right-left+1)/2;
if(arr[mid]>arr[mid-1]){
left=mid;
}else{
right=mid-1;
}
}
return left;
}
举例
测试用例 arr = [0,10,5,2]
首先,初始化 left = 1
和 right = arr.length - 2 = 2
。
接下来,我们进入 while
循环:
-
第一次循环:
left = 1
,right = 2
- 计算
mid = left + (right - left + 1) / 2 = 1 + (2 - 1 + 1) / 2 = 2
- 检查
arr[mid]
是否大于arr[mid - 1]
,也就是检查arr[2]
是否大于arr[1]
。由于arr[2] = 5
并不大于arr[1] = 10
,条件不满足。 - 所以,我们将
right
更新为mid - 1
,即right = 1
。
-
这时,
left
和right
都指向同一个位置1
,循环条件left < right
不再满足,循环结束。
最后,返回 left
的值,即 1
。这意味着数组中的峰值位于索引 1
上,这与给定数组 [0, 10, 5, 2]
的实际情况相吻合,因为最大值 10
确实位于索引 1
。
所以,这段代码正确地找到了山峰数组的峰值索引。
二.寻找峰值
题目链接:162.寻找峰值
算法原理
同样使用了二分查找(Binary Search)算法来找到所谓的“峰值元素”。峰值元素定义为一个元素,它严格大于它的邻居。注意,数组可以是未排序的,并且数组的两端被认为是邻居元素的“虚拟”较小值,这样数组的起始元素和末尾元素也可以成为峰值元素。
算法的步骤如下:
-
初始化两个指针
left
和right
,分别指向数组的起始位置0
和终止位置nums.length - 1
。 -
进入
while
循环,只要left
小于right
,表示搜索区间内还有多个元素需要考虑。 -
在循环内部,计算中间位置
mid
。这里使用(left + (right - left) / 2)
来避免整数溢出,并确保mid
总是落在left
和right
之间。 -
比较
nums[mid]
和nums[mid + 1]
的大小。如果nums[mid]
小于nums[mid + 1]
,那么峰值一定不在mid
及其左侧,因为从mid
到mid + 1
数组是上升的。这时,将left
更新为mid + 1
。 -
否则,如果
nums[mid]
大于或等于nums[mid + 1]
,那么峰值可能在mid
或者其左侧。这时,将right
更新为mid
。 -
当
left
和right
相遇时,循环结束,此时left
指向的位置就是峰值元素的索引。这是因为当left == right
时,它们共同指向的元素必定是峰值,因为在之前的迭代中,我们总是排除了较小的邻居元素所在的那一边。
这段代码的时间复杂度同样是 O(log n),其中 n 是数组的长度,因为每次迭代都将搜索范围减半,这使得算法非常高效。
代码
public int findPeakElement(int[] nums) {
int left=0,right=nums.length-1;
while(left<right){
int mid=left+(right-left)/2;
if(nums[mid]<nums[mid+1]){
left=mid+1;
}else{
right=mid;
}
}
return left;
}
举例
测试用例 [1,2,1,3,5,6,4]
我们开始分析:
-
初始化
left = 0
和right = nums.length - 1 = 6
。 -
第一次循环:
mid = left + (right - left) / 2 = 0 + (6 - 0) / 2 = 3
- 检查
nums[mid]
和nums[mid + 1]
,即nums[3]
和nums[4]
,比较3
和5
。 - 因为
nums[3]
小于nums[4]
,所以更新left
为mid + 1
,即left = 4
。
-
第二次循环:
- 此时
left = 4
和right = 6
。 mid = left + (right - left) / 2 = 4 + (6 - 4) / 2 = 5
- 检查
nums[mid]
和nums[mid + 1]
,即nums[5]
和nums[6]
,比较6
和4
。 - 因为
nums[5]
不小于nums[6]
,所以更新right
为mid
,即right = 5
。
- 此时
-
第三次循环:
- 现在
left = 4
和right = 5
。 mid = left + (right - left) / 2 = 4 + (5 - 4) / 2 = 4
- 检查
nums[mid]
和nums[mid + 1]
,即nums[4]
和nums[5]
,比较5
和6
。 - 因为
nums[4]
小于nums[5]
,所以更新left
为mid + 1
,即left = 5
。
- 现在
-
第四次循环:
- 此时
left = 5
和right = 5
。 - 因为
left
等于right
,while
循环的条件不再满足,循环结束。
- 此时
最终,函数返回 left
的值,即 5
。这表明数组 [1, 2, 1, 3, 5, 6, 4]
中的一个峰值元素位于索引 5
,其值为 6
。值得注意的是,根据题目的定义,可能有多个峰值元素,而算法保证返回的是其中一个。在这个例子中,索引 1
(nums[1] = 2
) 和索引 5
(nums[5] = 6
) 都是合法的峰值元素。
三.寻找旋转排序数组的最小值
题目链接:153.寻找旋转排序数组的最小值
算法原理
这段代码实现了一个算法,用于在一个旋转排序数组中找到最小元素。旋转排序数组指的是原本有序的数组经过若干次旋转得到的结果。例如,数组 [1, 2, 3, 4, 5]
经过旋转可能变成 [3, 4, 5, 1, 2]
。
算法的原理基于二分查找(Binary Search),但是针对旋转排序数组进行了调整。关键在于利用旋转特性来缩小搜索范围。旋转数组的最小元素位于旋转点之后,旋转点之前的子数组是递增的,旋转点之后的子数组也是递增的,但整个数组的顺序被打乱。
算法步骤如下:
-
初始化
left
和right
分别指向数组的起始和末尾位置。 -
获取数组最后一个元素
x
作为基准值。这是因为在旋转数组中,最后一个元素通常是未旋转前数组的最后一个元素,或者是旋转后新数组的最大值。 -
进入
while
循环,只要left < right
,就说明搜索空间大于1个元素。 -
计算中间位置
mid
,使用(left + (right - left) / 2)
来避免整数溢出问题。 -
比较
nums[mid]
和x
的大小:- 如果
nums[mid] > x
,说明mid
位于旋转点的左侧递增子数组中,最小值只能在mid
右侧的子数组中,因此更新left
为mid + 1
。 - 否则,
nums[mid] <= x
,说明mid
位于旋转点的右侧递增子数组中,或者正好位于旋转点上,最小值可能在mid
或者左侧子数组中,因此更新right
为mid
。
- 如果
-
当
left
和right
相遇时,循环结束,此时left
指向的位置就是最小元素的索引,返回nums[left]
即可得到最小值。
此算法的时间复杂度为 O(log n),其中 n 是数组的长度,因为它在每一步都有效地将搜索空间减半。这使得算法在处理大数据量时非常高效。
代码
public int findMin(int[] nums) {
int left=0,right=nums.length-1;
int x=nums[right];
while(left<right){
int mid=left+(right-left)/2;
if(nums[mid]>x){
left=mid+1;
}else{
right=mid;
}
}
return nums[left];
}
举例
测试用例 nums = [4,5,6,7,0,1,2]
我们开始逐步分析:
- 初始化
left = 0
和right = nums.length - 1 = 6
。 - 设置
x = nums[right] = nums[6] = 2
。
第一次循环:
mid = left + (right - left) / 2 = 0 + (6 - 0) / 2 = 3
- 检查
nums[mid]
和x
,即nums[3]
和2
,比较7
和2
。 - 因为
nums[mid]
大于x
,更新left
为mid + 1
,即left = 4
。
第二次循环:
- 此时
left = 4
和right = 6
。 mid = left + (right - left) / 2 = 4 + (6 - 4) / 2 = 5
- 检查
nums[mid]
和x
,即nums[5]
和2
,比较1
和2
。 - 因为
nums[mid]
不大于x
,更新right
为mid
,即right = 5
。
第三次循环:
- 此时
left = 4
和right = 5
。 mid = left + (right - left) / 2 = 4 + (5 - 4) / 2 = 4
- 检查
nums[mid]
和x
,即nums[4]
和2
,比较0
和2
。 - 因为
nums[mid]
不大于x
,更新right
为mid
,即right = 4
。
第四次循环:
- 现在
left = 4
和right = 4
。 while
循环的条件left < right
不再满足,循环结束。
最终,函数返回 nums[left]
的值,即 nums[4]
,结果为 0
。
这表明数组 [4, 5, 6, 7, 0, 1, 2]
中的最小元素为 0
,位于索引 4
。此算法成功找到了旋转排序数组中的最小元素。
四.LCR 173.点名
题目链接:LCR 173.点名
算法原理
-
初始化两个指针
left
和right
,分别指向数组的起始位置0
和终止位置records.length - 1
。 -
使用
while
循环,只要left < right
,意味着数组中还可能存在不匹配的情况。 -
计算中间位置
mid
,使用(left + (right - left) / 2)
来避免整数溢出。 -
检查
mid
位置的元素是否等于mid
:- 如果
records[mid]
等于mid
,这意味着mid
位置的值与索引匹配,因此缺失的元素可能在mid
的右侧。更新left
为mid + 1
。 - 否则,如果
records[mid]
不等于mid
,这可能是由于缺失的元素在mid
的位置应该出现,但实际没有出现。因此,更新right
为mid
,继续在左侧查找。
- 如果
-
当
left
和right
相遇时,循环结束。此时,left
指向的位置要么是缺失元素应该出现的位置,要么紧随其后。 -
最后,检查
left
位置的元素是否等于left
:- 如果
left
位置的元素等于left
,这意味着left
位置的元素没有缺失,因此缺失的元素应该是left + 1
。 - 否则,
left
位置的元素小于left
,这意味着left
位置的元素是缺失的,因此缺失的元素就是left
。
- 如果
时间复杂度为 O(log n),其中 n 是数组的长度,因为算法使用了二分查找,每次迭代都将搜索范围减半。
这种算法特别适用于数据量大、有序或部分有序的数组中查找缺失的元素,效率远高于线性查找。
代码
public int takeAttendance(int[] records) {
int left=0,right=records.length-1;
while(left<right){
int mid=left+(right-left)/2;
if(mid==records[mid]){
left=mid+1;
}else{
right=mid;
}
}
//判断特殊情况,如[0,1,2,3,4,5]此时缺少的值应该是6
if(left==records[left]){
return left+1;
}
return left;
}
举例
测试用例 records = [0, 1, 2, 3, 4, 5, 6, 8]
我们开始逐步分析:
- 初始化
left = 0
和right = records.length - 1 = 7
。
第一次循环:
mid = left + (right - left) / 2 = 0 + (7 - 0) / 2 = 3
- 检查
records[mid]
和mid
,即records[3]
和3
,比较3
和3
。 - 因为
records[mid]
等于mid
,更新left
为mid + 1
,即left = 4
。
第二次循环:
- 此时
left = 4
和right = 7
。 mid = left + (right - left) / 2 = 4 + (7 - 4) / 2 = 5
- 检查
records[mid]
和mid
,即records[5]
和5
,比较5
和5
。 - 因为
records[mid]
等于mid
,更新left
为mid + 1
,即left = 6
。
第三次循环:
- 此时
left = 6
和right = 7
。 mid = left + (right - left) / 2 = 6 + (7 - 6) / 2 = 6
- 检查
records[mid]
和mid
,即records[6]
和6
,比较6
和6
。 - 因为
records[mid]
等于mid
,更新left
为mid + 1
,即left = 7
。
第四次循环:
- 现在
left = 7
和right = 7
。 while
循环的条件left < right
不再满足,循环结束。
退出循环后:
- 检查
left
位置的元素是否等于left
,即records[7]
和7
,比较8
和7
。 - 因为
records[left]
不等于left
,直接返回left
的值,即7
。
然而,根据代码逻辑,如果 left
位置的元素等于 left
,我们应该返回 left + 1
;否则,返回 left
。在本例中,left
已经等于数组的长度,且 records[left]
实际上超出了正常的序列,因此正确结果应为 left
的值,即 7
,这表明缺失的元素是 7
。
因此,这段代码正确地找到了测试用例 [0, 1, 2, 3, 4, 5, 6, 8]
中缺失的元素,即 7
。
🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀
以上,就是本期的全部内容啦,若有错误疏忽希望各位大佬及时指出💐
制作不易,希望能对各位提供微小的帮助,可否留下你免费的赞呢🌸