二分查找练习

寻找峰值

峰值元素是指其值严格大于左右相邻值的元素
给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞ 。

你必须实现时间复杂度为 O(log n) 的算法来解决此问题。

示例 1:
输入:nums = [1,2,3,1]
输出:2
解释:3 是峰值元素,你的函数应该返回其索引 2。

示例 2:
输入:nums = [1,2,1,3,5,6,4]
输出:1 或 5
解释:你的函数可以返回索引 1,其峰值元素为 2;
或者返回索引 5, 其峰值元素为 6。

提示:
1 <= nums.length <= 1000
-231 <= nums[i] <= 231 - 1
对于所有有效的 i 都有 nums[i] != nums[i + 1]

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/find-peak-element

方法一:
根据观察,如果存在峰值,那么这个峰值就会比他前后两个数字的值都要大,这时候只要拿当前这个元素的值和他前后两个元素的值进行比较,从而可以判断出这个元素是否为峰值。但是有两种情况需要考虑的是,就是连续升序和连续降序的情况,这时候,是没有办法进行比较前后两个元素的,此时只要保证了下标为0的元素比下标为1的元素的值要大,这时候就说明下标为0的元素是一个峰值,因为存在了降序的趋势,同样的,如果下标为nusm.length - 1的元素的值比他前一个元素的值要大,说明存在着升序的趋势,此时,nums.length - 1对应的值就是一个峰值,因为题目中已经明确说明了nums[-1] = nums[nums.length]= -∞,所以这两个端点只要满足了这两个条件,就必然是个峰值
否则,如果两个端点并没有满足条件,那么这时候说明下标为0的地方存在着升序的情况,而下标为nums.length - 1存在着降序,此时两者的中间必然存在峰值才会出现先升后降的趋势,因此,只要通过nums[i - 1] < nums[ i ] && nums[ i ] > nums[ i + 1]来找到峰值即可

class Solution {
    public int findPeakElement(int[] nums) {
         if(nums.length == 1)
             return 0;
         int i;
         for(i = 0; i < nums.length; ++i){
             if(i == 0 && nums[i] > nums[i + 1]){
    //如果是连续降序或者先降序后升序,那么始终第一个元素是一个峰值,所以可以直接返回下标0
                 return i;
             }else if(i == nums.length - 1 && nums[i] > nums[i - 1]){
    //如果连续升序,那么数组的最后一个元素才是峰值
                 return i;
             }else{
                 //数组并不是连续单调的,那么根据当前元素(不是端点)大于前后两个元素,从而得出当前元素是一个峰值,返回其下标
                 if(i > 0 && i < nums.length - 1 && nums[i - 1] < nums[i] && nums[i] > nums[i + 1])
                    return i;
             }
         }
         return -1;
    }
}

运行结果:
在这里插入图片描述

方法二:二分查找
我们知道,二分查找只应用于一个数组有序的地方,为什么这里也可以应用呢?因为我们可以发现部分数组是有序的,而我们就是要找这部分有序数组的最大值。因此可以应用二分查找。

  • 在low <= high的情况下进行二分查找
  • 在low等于high的情况,直接返回low,表示当前元素就是一个峰值
  • 否则获取中间值mid = low + (high - low) / 2,当然也可以是mid = (high + low) / 2
  • 将nums[mid]和nums[mid + 1]进行比较,如果nums[mid]更大,说明[mid,mid + 1]之间存在着降序,此时mid对应的值可能是一个峰值,当然也可能不是,因为可能在[low,mid]可能是升序,也可能是降序的,所以mid对应的值不一定是峰值,此时需要更新high为mid,否则,如果nums[mid]更小,说明[mid,mid + 1]存在升序的趋势,此时mid必然不是峰值,而mid + 1也不一定是峰值,因为[mid + 1,high]可能是升序,而mid + 1却不是最后一个元素,所以我们需要在[mid + 1,high]中寻找,所以更新low = mid + 1.

对应代码:

class Solution {

    /*
    利用二分查找,首先获得中间值nums[mid]
    这时候我们将其和mid + 1的值进行比较,如果mid的值更小,
    那么说明存在升序,所以这时候需要更新low = mid + 1
    否则,如果mid更大,说明存在降序这时候我们不可以
    直接考虑mid的值就是峰值,因为存在着连续降序的可能,所以
    不可以返回mid,而应该更新high=mid
    当low>high时候,直接返回low即可
    */
    public int findPeakElement(int[] nums) {
        int low = 0,high = nums.length - 1,mid;
        while(low <= high){
            if(low == high)//如果存在峰值,那么必然有low = high的情况
                return low;
            mid = low + (high - low) / 2;
            if(nums[mid] > nums[mid + 1]){
                //当前mid对应的值大于mid + 1对应的值,此时说明存在降序,此时不可以
                //返回mid,因为存在连续降序的情况,所以需要更新high为mid
                high = mid;
            }else{
                //当前的mid的值小于mid + 1的值,说明存在着升序,此时,需要更新low = mid + 1
                low = mid + 1;
            }
        }
        return -1;
    }


}

运行结果:
在这里插入图片描述

寻找旋转排序数组中的最小值

已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素

示例 1:
输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。

示例 2:
输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。

示例 3:
输入:nums = [11,13,15,17]
输出:11
解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。

提示:
n == nums.length
1 <= n <= 5000
-5000 <= nums[i] <= 5000
nums 中的所有整数 互不相同
nums 原来是一个升序排序的数组,并进行了 1 至 n 次旋转

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array

方法一:对整个数组进行排序,然后直接返回第一个元素就是最小元素
方法二:
我们来观察一下例子,通过例子,我们可以知道,所谓的旋转,就是右移1步。旋转k次,就是需要右移k步。这时候,尽管原来的升序数组不再是升序,但是将其拆成两个子数组的时候,那么这两个子数组都是升序的,并且第二个子数组的最大值必然小于等于第一个子数组的最小值。所以只要知道nums[ i ] > nums[ i + 1],那么说明nums[ i + 1]就是第二个子数组的起点,并且是整个数组的最小元素
对应代码:

class Solution {
    public int findMin(int[] nums) {
       int i;
       for(i = 0; i < nums.length - 1; ++i){
           if(nums[i] > nums[i + 1])
              return nums[i + 1];
       }
       return nums[0];
    }
}

运行结果:
在这里插入图片描述
方法三:直接利用二分查找
这个思路和方法二思路类似的,同样是在明白两个子数组都是升序的前提下进行右移的,并且第二个子数组的最大值必然小于等于第一个子数组的最小值,因为没有旋转是数组是升序的

  • 首先我们获取数组的中间值mid
  • 将nums[mid]和nusm[high]元素进行比较,如果nums[mid]更大,说明在[mid + 1,high]范围中存在不是单调的,可能存在第一个子数组的最大值,然后更新变成第二个子数组进行升序。,所以这时候我们需要更新low = mid + 1;
  • 否则,如果nums[mid] < nums[high],那么这时候mid和high同处在同一个子数组中,否则如果在不同的子数组,那么nums[high]必然是在第二个子数组,而此时nums[mid]在第一个子数组,此时第一个子数组的值必然大于等于第二个子数组的值,所以不满足,因此nums[mid] < nums[high]时,两者是处于同一个子数组中,所以得出[mid + 1,high]是只能单调递增的。这时候需要更新high = mid,之所以不是mid - 1是因为mid本身可能就是整个数组的最小值。
  • 否则,nums[mid] == nums[high],尽管因为元素互不相同,不存在nums[mid]等于nums[high]的情况,而如果数组元素时重复的,那么这时候并不能说明中间枢纽就在[mid + 1,high -1]中,而
    [1,1,1,1,1,1,1,1,1,13,1,1,1,1,1,1,1,1,1,1,1,1]就是最好说明的例子了。所以在数组允许元素重复时,low不可以更新,而将high更新为high–.
  • 否则,如果low 等于 high,说明只有一个元素,此时的mid = low = high,对应的元素当然是相同的,此时这个元素就是整个数组的最小值。而又因为数组中的元素是互不相同的,所以此时的low就是原来数组第一个元素的下标

对应代码:

class Solution {
    /*
    通过观察题目,可以知道,所谓的旋转,就是右移k步,因此将数组拆成了至多2个升序的数组
    所以,需要将中间值和最后一个元素进行比较, 如果中间值的元素nums[mid]大于最后一个元素,说明中间
    枢纽位于[mid + 1,nums.length - 1],否则,如果中间值的元素小于最后一个元素,说明[mid + 
    1,nums.length - 1]都是升序的,所以需要更新high为mid,之所以不是mid - 1,是因为mid可能就是中间
    枢纽对应的下标,因为元素互不相同,所以不存在nums[mid]等于nums[high]的情况,而如果数组元素时重
    复的,那么这时候并不能说明中间枢纽就在[mid + 1,high -1]中,而
    [1,1,1,1,1,1,1,1,1,13,1,1,1,1,1,1,1,1,1,1,1,1]就是最好说明的例子了。所以在数组允许元素重复
    时,low不可以更新,而将high更新为high--.
    */
    public int findMin(int[] nums) {
        int mid,low = 0,high = nums.length - 1;
        while(low <= high){
            if(low == high)//如果low等于high,这时候这个元素就是原来数组的最小值,并且因为元素互不相同,所以这个low下标就是原来数组第一个元素的下标
               return nums[low];
            mid = low + (high - low) / 2;
            if(nums[mid] > nums[high])
               low = mid + 1;
            else if(nums[mid] < nums[high])
               high = mid;
            else{
                --high;
            }
               
        }
        return nums[low];
    }

}

运行结果:
在这里插入图片描述

寻找旋转排序数组中的最小元素II

已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,4]
若旋转 7 次,则可以得到 [0,1,4,4,5,6,7]
注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。
给你一个可能存在 重复 元素值的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。

示例 1:
输入:nums = [1,3,5]
输出:1

示例 2:
输入:nums = [2,2,2,0,1]
输出:0

提示:
n == nums.length
1 <= n <= 5000
-5000 <= nums[i] <= 5000
nums 原来是一个升序排序的数组,并进行了 1 至 n 次旋转

进阶:
这道题是 寻找旋转排序数组中的最小值 的延伸题目。
允许重复会影响算法的时间复杂度吗?会如何影响,为什么?

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array-ii
和上面的解题思路时一样的,对应代码也一样:

class Solution {
    /*
    通过观察题目,可以知道,所谓的旋转,就是右移k步,因此将数组拆成了至多2个升序的数组
    所以,需要将中间值和最后一个元素进行比较, 如果中间值的元素nums[mid]大于最后一个元素,说明中间
    枢纽位于[mid + 1,nums.length - 1],否则,如果中间值的元素小于最后一个元素,说明[mid + 
    1,nums.length - 1]都是升序的,所以需要更新high为mid,之所以不是mid - 1,是因为mid可能就是中间
    枢纽对应的下标,否则,如果中间值等于最后一个元素,说明在两者之间存在着中间枢纽.
    */
    public int findMin(int[] nums) {
        int mid,low = 0,high = nums.length - 1;
        while(low <= high){
            if(low == high)//如果low等于high,这时候这个元素就是原来数组的最小值,并且因为元素互不相同,所以这个low下标就是原来数组第一个元素的下标
               return nums[low];
            mid = low + (high - low) / 2;
            if(nums[mid] > nums[high])
               low = mid + 1;
            else if(nums[mid] < nums[high])
               high = mid;
            else{
                --high;
            }      
        }
        return nums[low];
    }

}

运行结果:
在这里插入图片描述

允许重复会影响算法的时间复杂度吗?会如何影响,为什么?

我觉得会的,因为上面一题中不允许元素重复,那么这时候就会执行high–这一步,而是每次要么更新low = mid + 1,要么更新high = mid,从而需要查找的范围大大缩小,而在这里允许元素重复,所以会执行high–,此时就会使得查找的范围缩减的幅度没有那么大,从而增加了程序运行的时间。

搜索旋转排序数组

**整数数组 nums 按升序排列,数组中的值 互不相同 **。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。

示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4

示例 2:
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1

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

提示:
1 <= nums.length <= 5000
-10^4 <= nums[i] <= 10^4
nums 中的每个值都 独一无二
题目数据保证 nums 在预先未知的某个下标上进行了旋转
-10^4 <= target <= 10^4
进阶:你可以设计一个时间复杂度为 O(log n) 的解决方案吗?

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/search-in-rotated-sorted-array

方法一:将数组分成两个子数组,分别对子数组进行二分查找
通过观察题目我们可以知道,一个升序数组旋转,就是每次将这个数组右移1步,一共会移动k次,所以这时候就会将一个数组至多分成两个升序的子数组。这时候我们只要对这两个子数组进行二分查找target即可。但是我们将怎样划分成为两个子数组呢?因为两个子数组都是升序的,所以只要我们找到第一个nums[ i ]大于nums[ i + 1]的时候,就可以知道nums[ i + 1]是第二个子数组的起点,也是整个数组的最小值,而 i 是第一个子数组的终点,此时两个子数组分别为[0,i],[i + 1,nums.length - 1]

当然我们可以向上面一样,因为当前数组的元素互不相等,所以可利用二分查找来找到第二个子数组的起点,同时整个数组的最小值时,这个值对应的下标就是中间枢纽的下标

对应代码:

class Solution {
    public int search(int[] nums, int target) {
        int pivotIndex = getPivotIndex(nums);//获取中间枢纽的下标,此时两个子数组分别为[0,pivotIndex - 1],[pivotIndex,nums.length - 1]
        int k = binarySearch(nums,0,pivotIndex - 1,target);
        return k == -1 ? binarySearch(nums,pivotIndex,nums.length - 1,target) : k;
    }
    public int binarySearch(int[] nums,int low,int high,int target){
        int mid;
        while(low <= high){
            mid = low + (high - low) / 2;
            if(nums[mid] == target)
              return mid;
            else if(nums[mid] < target)
              low = mid + 1;
            else
              high = mid - 1;
        }
        return -1;
    }
    public int getPivotIndex(int[] nums){
        /*
        利用nums[i] > nums[i + 1]找到中间枢纽(整个数组的最小值)为i + 1
        int i;
        for(i = 0; i < nums.length - 1; ++i){
            if(nums[i] > nums[i + 1])
               return i + 1;//如果当前元素大于下一个元素,说明下一个元素就是另一个子数组的起点,并且是整个数组的最小值
        }
        return i;//这里返回的是i,是因为考虑到了只有一个元素的情况下,那么下标为0就是中间枢纽的下标
        */
        int mid,low = 0,high = nums.length - 1;
        while(low <= high){
            if(low == high)//如果low等于high,说明只有一个元素,此时这个元素就是中间枢纽
               return low;
            mid = low + (high - low) / 2;
            if(nums[mid] > nums[high]) //如果mid的值大于high的值,说明[mid + 1,high]中存在趋势的变化,所以需要更新low = mid + 1
               low = mid + 1;
            else if(nums[mid] < nums[high]) //否则,如果小于,说明同处一个子数组,此时更新high为mid
               high = mid;
            else{
                --high;
            }
        }
        return 0;
    }
}

运行结果:
在这里插入图片描述

搜索旋转排序数组II

已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。
给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false 。

示例 1:
输入:nums = [2,5,6,0,0,1,2], target = 0
输出:true

示例 2:
输入:nums = [2,5,6,0,0,1,2], target = 3
输出:false

提示:
1 <= nums.length <= 5000
-104 <= nums[i] <= 104
题目数据保证 nums 在预先未知的某个下标上进行了旋转
-104 <= target <= 104

进阶:
这是 搜索旋转排序数组 的延伸题目,本题中的 nums 可能包含重复元素。
这会影响到程序的时间复杂度吗?会有怎样的影响,为什么?

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii

题目中说到原来的数组是非降序的,那么可能是升序的,也可能是不单调,即先升后降,或者先降后升,此时我们尝试提交一个先降后升的数组,后者是先升后降再升再降的数组,此时就会发现提示“The array is not rotated",表示这个数组不是旋转数组,这说明了原来的数组只能是升序的,进行旋转之后,划分的两个子数组都是升序的

所以在基于上面的例子,本题同样可以运用 划分两个升序的子数组 + 对每个子数组进行二分查找 的方法判断原来的数组是否存在target.

因为数组含有重复数字,所以本题不可以像上一题那样,利用二分查找找到中间枢纽的下标

对应代码:

class Solution {
    public boolean search(int[] nums, int target) {
            /*
            由于旋转,会将数组分成了两个升序的子数组,所以
            局部有序,对于这个子数组进行二分查找,所以只要
            找到中间枢纽对应的下标,就可以分成了两个有序的
            子数组了,此时就运用二分查找
            */
            int pivotIndex = getPivotIndex(nums); //获取中间枢纽的下标,此时[0,pivotIndex - 1],[pivotIndex,nums.length - 1]就是两个子数组

            return binarySearch(nums,0,pivotIndex - 1,target) || binarySearch(nums,pivotIndex,nums.length - 1,target);
    }
    //二分查找
    public boolean binarySearch(int[] nums,int low,int high,int target){
        int mid;
        while(low <= high){
             mid = low + (high - low) / 2;
             if(nums[mid] == target)
                return true;
             else if(nums[mid] > target)
                high = mid - 1;
             else
                low = mid + 1;
        }
        return false;
    }
    public int getPivotIndex(int[] nums){

        //利用nums[i] > nums[i + 1]找到中间枢纽(整个数组的最小值)为i + 1
        int i;
        for(i = 0; i < nums.length - 1; ++i){
            if(nums[i] > nums[i + 1])
               return i + 1;//如果当前元素大于下一个元素,说明下一个元素就是另一个子数组的起点,并且是整个数组的最小值
        }
        return i;//这里返回的是i,是因为考虑到了只有一个元素的情况下,那么下标为0就是中间枢纽的下标
    }
}

运行结果:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值