废话不多说,喊一句号子鼓励自己:程序员永不失业,程序员走向架构!本篇Blog的主题是【数组的二分查找】,使用【数组】这个基本的数据结构来实现,这个高频题的站点是:CodeTop,筛选条件为:目标公司+最近一年+出现频率排序,由高到低的去牛客TOP101去找,只有两个地方都出现过才做这道题(CodeTop本身汇聚了LeetCode的来源),确保刷的题都是高频要面试考的题。
名曲目标题后,附上题目链接,后期可以依据解题思路反复快速练习,题目按照题干的基本数据结构分类,且每个分类的第一篇必定是对基础数据结构的介绍。
旋转排序数组的最小数字【MID】
先来找最小值
题干
直接粘题干和用例
解题思路
通过题目描述我们知道,旋转点后的排序子区间最大值也比旋转点之前的排序子区间最小值小,最小值就是旋转点,也就是数组的中间部分,所以我们使用碰撞双指针从两边向中心搜索,不断缩小搜索范围:
- step 1:双指针指向旋转后数组的首尾,作为区间端点。
- step 2:若是区间中点值大于区间右界值,说明中点在左侧递增区间,则最小的数字一定在中点右边。
- step 3:若是区间中点值等于区间右界值,则是不容易分辨最小数字在哪半个区间,比如[1,1,1,0,1],应该逐个缩减右界。
- step 4:若是区间中点值小于区间右界值,说明中点在右侧递增区间,则最小的数字一定在中点左边。
- step 5:通过调整区间最后即可锁定最小值所在。
代码实现
给出代码实现基本档案
基本数据结构:数组
辅助数据结构:无
算法:二分查找
技巧:双指针(碰撞双指针)
其中数据结构、算法和技巧分别来自:
- 10 个数据结构:数组、链表、栈、队列、散列表、二叉树、堆、跳表、图、Trie 树
- 10 个算法:递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法、回溯算法、动态规划、字符串匹配算法
- 技巧:双指针、滑动窗口、中心扩散
当然包括但不限于以上
import java.util.*; public class Solution { /** * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 * * * @param nums int整型一维数组 * @return int整型 */ public int minNumberInRotateArray (int[] nums) { // 1 入参判断,参数有效性校验 if (nums == null || nums.length == 0) { return -1; } // 2 定义左右碰撞指针,找中点位置 int left = 0; int right = nums.length - 1; while (left <= right) { int mid = (left + right) / 2; if (nums[mid] > nums[right]) { // mid在左侧递增区间,最小值在mid右侧[mid+1,right] left = mid + 1; } else if (nums[mid] < nums[right]) { // mid在右侧递增区间,最小值为mid或mid左侧[left,mid] right = mid; } else { // 无法判断,缩小右边界 right--; } } return nums[left]; } }
复杂度分析
时间复杂度:O(log n),二分法最坏情况对n取2的对数
空间复杂度:O(1),常数级变量,无额外辅助空间
搜索旋转排序数组【MID】
OK,接下来难度升级,不找最小值,找的是指定值
题干
直接粘题干和用例
解题思路
给出解题思路,最好有图对于有序数组,可以使用二分查找的方法查找元素。但是这道题中,数组本身不是有序的,进行旋转后只保证了数组的局部是有序的,这还能进行二分查找吗?答案是可以的。
可以发现的是,我们将数组从中间分开成左右两部分的时候,一定有一部分的数组是有序的。拿示例来看,我们从 6 这个位置分开以后数组变成了 [4, 5, 6] 和 [7, 0, 1, 2] 两个部分,其中左边 [4, 5, 6] 这个部分的数组是有序的,其他也是如此。
这启示我们可以在常规二分查找的时候查看当前 mid 为分割位置分割出来的两个部分 [l, mid] 和 [mid + 1, r] 哪个部分是有序的,并根据有序的那个部分确定我们该如何改变二分查找的上下界,因为我们能够根据有序的那部分判断出 target 在不在这个部分:
- 如果 [l, mid - 1] 是有序数组,且 target 的大小满足 [nums[l],nums[mid]),则我们应该将搜索范围缩小至 [l, mid - 1],否则在 [mid + 1, r] 中寻找。
- 如果 [mid, r] 是有序数组,且 target 的大小满足 (nums[mid+1],nums[r]],则我们应该将搜索范围缩小至 [mid + 1, r],否则在 [l, mid - 1] 中寻找。
- 定理一:只有在顺序区间内才可以通过区间两端的数值判断target是否在其中。
- 定理二:判断顺序区间还是乱序区间,只需要对比 left 和 right 是否是顺序对即可,left <= right,顺序区间,否则乱序区间。
- 定理三:每次二分都会至少存在一个顺序区间。
通过不断的用Mid二分,根据定理二,将整个数组划分成顺序区间和乱序区间,然后利用定理一判断target是否在顺序区间,如果在顺序区间,下次循环就直接取顺序区间,如果不在,那么下次循环就取乱序区间。最终target一定会在一个顺序区间,即使只有它一个元素。
代码实现
给出代码实现基本档案
基本数据结构:数组
辅助数据结构:无
算法:二分查找
技巧:双指针(碰撞双指针)
其中数据结构、算法和技巧分别来自:
- 10 个数据结构:数组、链表、栈、队列、散列表、二叉树、堆、跳表、图、Trie 树
- 10 个算法:递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法、回溯算法、动态规划、字符串匹配算法
- 技巧:双指针、滑动窗口、中心扩散
当然包括但不限于以上
class Solution { public int search(int[] nums, int target) { // 1 入参校验 if (nums == null || nums.length == 0) { return -1; } // 2 定义双指针 int left = 0; int right = nums.length - 1; while (left <= right) { int mid = (left + right) / 2; if (nums[mid] == target) { return mid; } // 因为只有在有序区间内target的判断才有意义,所以每次都在有序区间内判断,并且依据目标值是否在有序区间内,移动左右指针 if (nums[left] <= nums[mid]) { // 如果左边是顺序区间 if (target >= nums[left] && target < nums[mid]) { // 如果目标值在顺序区间,在顺序区间搜寻[left,mid-1] right = mid - 1; } else { // 如果目标值不在顺序区间,在非顺序区间搜寻[mid+1,right] left = mid + 1; } } else { // 如果右边是顺序区间 if (target > nums[mid] && target <= nums[right]) { // 如果目标值在顺序区间,在顺序区间搜寻[mid+1,right] left = mid + 1; } else { // 如果目标值不在顺序区间,在非顺序区间搜寻[left,mid-1] right = mid - 1; } } } return -1; } }
复杂度分析
时间复杂度:O(log n),二分法最坏情况对n取2的对数
空间复杂度:O(1),常数级变量,无额外辅助空间