

🔥个人主页: 中草药
🔥专栏:【算法工作坊】算法实战揭秘
一.山脉数组的峰顶索引
题目链接: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) 时间复杂度要高效得多。
代码
java
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;
}
举例
java
测试用例 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 是数组的长度,因为每次迭代都将搜索范围减半,这使得算法非常高效。
代码
java
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;
}
举例
java
测试用例 [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 是数组的长度,因为它在每一步都有效地将搜索空间减半。这使得算法在处理大数据量时非常高效。
代码
java
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];
}
举例
java
测试用例 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 是数组的长度,因为算法使用了二分查找,每次迭代都将搜索范围减半。
这种算法特别适用于数据量大、有序或部分有序的数组中查找缺失的元素,效率远高于线性查找。
代码
java
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;
}
举例
java
测试用例 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。
🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀
以上,就是本期的全部内容啦,若有错误疏忽希望各位大佬及时指出💐
制作不易,希望能对各位提供微小的帮助,可否留下你免费的赞呢🌸