牛客编程题--必刷101之二分查找篇

这是牛客必刷题第二个知识点(二分查找),大家一起共勉!
在这里插入图片描述

17 二分查找-I

题目描述:请实现无重复数字的升序数组的二分查找
要求:给定一个 元素升序的、无重复数字的整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标(下标从 0 开始),否则返回 -1

输入:[-1,0,3,4,6,10,13,14],13
返回值:6
说明:13 出现在nums中并且下标为 6

输入:[],3
返回值:-1
说明:nums为空,返回-1

题目解法:常规的二分查找方法

 public int search (int[] nums, int target) {
        // 直接二分查找
        int left = 0;
        int right = nums.length-1;
        while(left <=right){
            int mid = (right + left)/2;
            //int mid = left + (right - left)/2;
            if(nums[mid] == target)
                return mid;
            else if(nums[mid]>target)
                right = mid-1;
            else if(nums[mid]<target)
                left = mid+1;
        }
        return -1;
        
        
    }

18 二维数组中的查找

题目描述
在一个二维数组array中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
[[1,2,8,9],[2,4,9,12],[4,7,10,13],[6,8,11,15]]
给定 target = 7,返回 true。
给定 target = 3,返回 false。

方法一:暴力解法
思路分析:直接遍历一遍数组,即可判断目标target是否存在。
复杂度分析
时间复杂度:O(n^2),因为最坏情况下,数组中的元素都需要遍历一次。
空间复杂度:O(1)

// 这种方法不推荐使用,时间复杂度较高
public bool Find(int target, vector<vector<int> > array) {
     // 判断数组是否为空
     if (array.size() ==0 || array[0].size() ==0) return false;
     for (const auto& vec : array) {
         for (const int val : vec) {
             if (val == target)
                 return true;
         }
     }
     return false;
 }

方法二:二分查找
假设arr数组,val,tar如下图所示:
如果我们把二分值定在右上角或者左下角,就可以进行二分。这里以右上角为例,左下角可自行分析:
在这里插入图片描述
具体步骤:
1)我么设初始值为右上角元素,arr[0][5] = val,目标tar = arr[3][1]
2)接下来进行二分操作:
3)如果val == target,直接返回
4)如果 tar > val, 说明target在更大的位置,val左边的元素显然都是 < val,间接 < tar,说明第 0 行都是无效的,所以val下移到arr[1][5]
5)如果 tar < val, 说明target在更小的位置,val下边的元素显然都是 > val,间接 > tar,说明第 5 列都是无效的,所以val左移到arr[0][4]
6)继续步骤2)

public boolean Find(int target, int [][] array) {
        //判断数组是否为空
        int m = array.length;
        if(m==0) return false;
        int n = array[0].length;
        if(n==0) return false;
        //设置右上角元素
        int r = 0, c = n-1;
        while(r < m && c >= 0){
            if(target == array[r][c]) return true;
            else if(target > array[r][c]) ++r;
            else --c;
        }
        return false;
    }

复杂度分析
时间复杂度:O(m+n) ,其中m为行数,n为列数,最坏情况下,需要遍历m+n次。
空间复杂度:O(1)

同理:也可以设置左下角val,然后target值大于val,那么直接列增加【c++】,如果小于val值,那么直接行减小【r–】

19 寻找峰值

题目描述:
给定一个长度为n的数组nums,请你找到峰值并返回其索引。数组可能包含多个峰值,在这种情况下,返回任何一个所在位置即可。
1.峰值元素是指其值严格大于左右相邻值的元素。严格大于即不能有等于
2.假设 nums[-1] = nums[n] = −∞
3.对于所有有效的 i 都有 nums[i] != nums[i + 1]

如输入[2,4,1,2,7,8,4]时,会形成两个山峰,一个是索引为1,峰值为4的山峰,另一个是索引为5,峰值为8的山峰,如下图所示:
输入:[2,4,1,2,7,8,4]
返回值:1
说明:4和8都是峰值元素,返回4的索引1或者8的索引5都可以

在这里插入图片描述

分析:
nums[mid] < nums[mid + 1]说明在“上坡”,则可以使left = mid +1(因为mid肯定不是峰值),向“峰”处压缩
nums[mid] > nums[mid + 1]说明在“下坡”,则应该使right =mid(mid可能是峰值),往“峰”处压缩

step 1:二分查找首先从数组首尾开始,每次取中间值,直到首尾相遇。
step 2:如果中间值的元素大于它右边的元素,说明往右是向下,我们不一定会遇到波峰,但是那就往左收缩区间。
step 3:如果中间值大于右边的元素,说明此时往右是向上,向上一定能有波峰,那我们往右收缩区间。
step 4:最后区间收尾相遇的点一定就是波峰。

public int findPeakElement (int[] nums) {
        int left = 0;
        int right = nums.length - 1;
        //二分法 
        while(left < right){
            int mid = (left + right) / 2;
            //右边是往下,不一定有坡峰
            if(nums[mid] > nums[mid + 1])
                right = mid;
            //右边是往上,一定能找到波峰
            else
                left = mid + 1;
        }
        //其中一个波峰
        return right;}

20 数组中的逆序对

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P mod 1000000007

示例1
输入:[1,2,3,4,5,6,7,0]
返回值:7
示例2
输入:[1,2,3]
返回值:0

方法一:暴力解法
public int InversePairs(int [] array) {
int res = 0;
int len = array.length;
for(int i = 0;i < len-1;i++){
for(int j=i;j<len;j++){
if(array[i]>array[j]){
res +=1;
res =res % 1000000007;
}
}
}
return res;

}

但是如果数据量级比较大,就会超时。

方法二:归并方法

先分:分呢,就是将数组分为两个子数组,两个子数组分为四个子数组,依次向下分,直到数组不能再分为止!

后并:并呢,就是从最小的数组按照顺序合并,从小到大或从大到小,依次向上合并,最后得到合并完的顺序数组!

介绍完归并排序,我们来说说归并统计法,我们要在哪个步骤去进行统计呢?

归并统计法,关键点在于合并环节,在合并数组的时候,当发现右边的小于左边的时候,此时可以直接求出当前产生的逆序对的个数。
在这里插入图片描述

 public class Solution {
    int count = 0;
    public int InversePairs(int [] array) {
        // 长度小于2则无逆序对
        if(array.length < 2)
            return 0;
        // 进入归并
        mergeSort(array,0,array.length-1);
        return count;
    }
     
    public void mergeSort(int[] array,int left,int right){
        // 找分割点
        int mid = left+(right-left)/2;
        if(left < right){
            // 左子数组
            mergeSort(array,left,mid);
            // 右子数组
            mergeSort(array,mid+1,right);
            // 并
            merge(array,left,mid,right);
        }
    }
 
    public void merge(int[] array,int left,int mid,int right){
        // 创建临时数组,长度为此时两个子数组加起来的长度
        int[] arr =  new int[right-left+1];
        // 临时数组的下标起点
        int c = 0;
        // 保存在原数组的起点下标值
        int s = left;
        // 左子数组的起始指针
        int l = left;
        // 右子数组的起始指针
        int r = mid+1;
        while(l <= mid && r <= right ){
            // 当左子数组的当前元素小的时候,跳过,无逆序对
            if(array[l] <= array[r]){
                // 放入临时数组
                arr[c++] = array[l++];
            }else{ // 否则,此时存在逆序对
                // 放入临时数组
                arr[c++] = array[r++];
                // 逆序对的个数为    左子数组的终点- 当前左子数组的当前指针
                count += mid+1-l;
                count %= 1000000007;

        }
        // 左子数组还有元素时,全部放入临时数组
        while(l <= mid)
            arr[c++] = array[l++];
        // 右子数组还有元素时,全部放入临时数组
        while(r <= right)
            arr[c++] = array[r++];
        // 将临时数组中的元素放入到原数组的指定位置
        for(int num:arr){
            array[s++] = num;
        }
    }
 }

21 旋转数组的最小数字

题目描述:
有一个长度为 n 的非降序数组,比如[1,2,3,4,5],将它进行旋转,即把一个数组最开始的若干个元素搬到数组的末尾,变成一个旋转数组,比如变成了[3,4,5,1,2],或者[4,5,1,2,3]这样的。请问,给定这样一个旋转数组,求数组中的最小值。

输入:[3,4,5,1,2]
返回值:1
输入:[3,100,200,3]
返回值:3

方法一:暴力解法:直接遍历一遍数组,即可找到最小值。但是本题的附加条件就没有用上。肯定不是面试官所期望的答案

方法二:二分查找
优点:可将遍历法的线性级别 时间复杂度降低至 对数级别
分析:找最小值,array[m] > array[j] 那么范围就是【m+1,j】;array[m] < array[j],那么其中包含m的值,所以范围【i,m】
具体步骤
1、初始化: 声明 i, j 双指针分别指向 array 数组左右两端
2、循环二分: 设 m = (i + j) / 2 为每次二分的中点( “/” 代表向下取整除法,因此恒有 i≤m+1、当 array[m] > array[j] 时: m 一定在左排序数组中,即旋转点 x 一定在 [m + 1, j] 闭区间内,因此执行 i = m + 1
2、当 array[m] < array[j] 时:m 一定在右排序数组中,即旋转点 x 一定在[i, m]闭区间内,因此执行 j = m
3、当 array[m] = array[j] 时: 无法判断 m 在哪个排序数组中,即无法判断旋转点 x 在 [i, m] 还是 [m + 1, j] 区间中。解决方案: 执行 j = j - 1 缩小判断范围
3、返回值: 当 i = j 时跳出二分循环,并返回旋转点的值 array[i] 即可。

在这里插入图片描述

public int minNumberInRotateArray(int [] array) {
        // 特殊情况判断
        if (array.length== 0) {
            return 0;
        }
        // 左右指针i j
        int i = 0, j = array.length - 1;
        // 循环
        while (i < j) {
            // 找到数组的中点 m
            int m = (i + j) / 2;
            // m在左排序数组中,旋转点在 [m+1, j] 中
            if (array[m] > array[j]) i = m + 1;
            // m 在右排序数组中,旋转点在 [i, m]中
            else if (array[m] < array[j]) j = m;
            // 缩小范围继续判断
            else j--;
        }
        // 返回旋转点
        return array[i];
    }

22 比较版本号

题目描述:牛客项目发布项目版本时会有版本号,比如1.02.11,2.14.4等等
现在给你2个版本号version1和version2,请你比较他们的大小。

比较规则:
一. 比较版本号时,请按从左到右的顺序依次比较它们的修订号。比较修订号时,只需比较忽略任何前导零后的整数值。比如"0.1"和"0.01"的版本号是相等的
二. 如果版本号没有指定某个下标处的修订号,则该修订号视为0。例如,“1.1"的版本号小于"1.1.1”。因为"1.1"的版本号相当于"1.1.0",第3位修订号的下标为0,小于1
三. version1 > version2 返回1,如果 version1 < version2 返回-1,不然返回0.

示例1
输入:“1.1”,“2.1”
返回值:-1
说明:version1 中下标为 0 的修订号是 “1”,version2 中下标为 0 的修订号是 “2” 。1 < 2,所以 version1 < version2,返回-1

示例2
输入:“1.1”,“1.01”
返回值:0
说明:version2忽略前导0,为"1.1",和version相同,返回0
示例3
输入:“1.1”,“1.1.1”
返回值:-1
说明:“1.1"的版本号小于"1.1.1”。因为"1.1"的版本号相当于"1.1.0",第3位修订号的下标为0,小于1,所以version1 < version2,返回-1
示例4
输入:“2.0.1”,“2”
返回值:1
说明:version1的下标2>version2的下标2,返回1
示例5
输入:“0.226”,“0.36”
返回值:1
说明:226>36,version1的下标2>version2的下标2,返回1

解题分析:双指针,使用v1和v2这两个值来分别计算版本号每个被’.'分割的块的版本号的大小,如果不相等,则进行比较

具体步骤:
在这里插入图片描述
1、初始化双指针v1,v2分别为0
2、分别对两个版本字符串进行遍历
1、遍历第一个版本字符串(索引 i),以 ‘.’ 为分割点(循环结束条件),计算每一块的版本号大小,记为 V1;
2、遍历第二个版本字符串(索引 j),以 ‘.’ 为分割点(循环结束条件),计算每一块的版本号大小,记为 V2;
3、对比V1, V2
1、:继续遍历两个版本字符串 ,
2、:直接返回 1
3、:直接返回 -1

public class Solution {
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 比较版本号
     * @param version1 string字符串 
     * @param version2 string字符串 
     * @return int整型
     */
    public int sgn(int a ,int b){
        if(a>b) return 1;
        else if(a<b) return -1;
        else return 0;
    }
    public int compare (String version1, String version2) {
        // 双指针,设置指针,从开头处开始
        int i=0, j=0;
        int l1 = version1.length(),l2 = version2.length();
        // 设置默认块
        int v1 = 0,v2 = 0;
        
        // 两个字符串均未跑完
        while (i < l1 || j < l2) {
            // 计算串1的当前块,如果字符串已经遍历完则什么也不做,用默认值0代替块中数据
            // 遇到点就跳出循环
            while (i < l1 && version1.charAt(i) != '.')
                v1 = v1*10 + (version1.charAt(i++) - '0');
 
            // 串2同理
            while (j < l2 && version2.charAt(j) != '.')
                v2 = v2*10 + (version2.charAt(j++)- '0');
 
            // 如果两个块中的数不一样,直接返回
            if (v1 != v2) return sgn(v1, v2);
            v1 = v2 = 0; // 恢复默认值
            i++;
            j++;    // 此时i和j要么出去了,要么遇到了点,跳过,出去了也无所谓
        }
        return 0;
        
        
    }
}

时间复杂度。其中 和 指的是输入字符串的长度。遍历版本字符串时间

空间复杂度:使用双指针及其他常数级空间变量

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

研行笔录

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值