LeetCode算法——算法入门Day01之二分查找

LeetCode算法

第 1 天
在这里插入图片描述

1. 二分查找

原题

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
示例 1:

输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

示例 2:

输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1

来源:力扣(LeetCode)
链接

分析1——正常思路

首先我们从题干分析,给定一个n个元素的有序(升序)整型数组,
提取重要信息,查询数组中是否有目标值,不用考虑其他因素。

也就是说我们需要在一个数组中找到目标数据target。

正常思路:
对数据进行循环,对比每一个元素与target目标值是否相等。

代码 1
/**
 * @Author: zcl
 * @Date: 2022-01-20 11:43
 */
public class FirstDay01 {

	public static int search(int[] nums, int target) {
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] == target) {
                return i;
            }
        }
        return -1;
    }

    public static void main(String[] args) {
      	int[] nums = {-1,0,3,5,9,12};
        int i = search(nums, 2);
        System.out.println(i);
    }
}
时间复杂度

这思路应该不用思考什么,就是循环查询,我们来看一下这个时间复杂度

for (int i = 0; i < nums.length; i++) {
   if (nums[i] == target) {
        return i;
    }
}

nums.length 就是我们输入的参数num的长度,我们假设有n个参数的数据,那么,对于for循环来说,从0开始到n结束,一共执行了n次, i++ 需要判断也就是n+1次,所以时间复杂度是O(n)。
我们来举个例子,比如我们的num数组是 [-1,0,3,5,9,12],那么nums.lenght = 6

i  =  0   0  <  6  执行内部逻辑 i++,此时 i = 1
i  =  1   1  <  6  执行内部逻辑 i++,此时 i = 2
i  =  2   2  <  6  执行内部逻辑 i++,此时 i = 3
i  =  3   3  <  6  执行内部逻辑 i++,此时 i = 4
i  =  4   4  <  6  执行内部逻辑 i++,此时 i = 5
i  =  5   5  <  6  执行内部逻辑 i++,此时 i = 6
i  =  6   6  <  6  失败

统计:当nums数组元素参数为n个时
i = 0 赋值执行一次
i < nums.length 执行 n + 1次
i++ 执行n次

综上:时间复杂度,注意如下规则:
1、复杂度为常数
	如1,55,888等等 表示为O(1)
2、复杂度包含n时,省略系数与常数项,只取n的最高阶项
​ 	如:3n+12 为 O(n) ; 2n^3+4n^2+n 为O(n^3)
3、复杂度为对数时:
	如log10(n)、log2(n) 等等 都表示为 O(logn)
4、省略低阶,只取高阶 (即取最大的)
	如:logn+nlogn 表示为O(nlogn)

由此,我们可以知道,当前的循环,时间复杂度就是O(n),不能说很差,只能说是一般吧。
我们看一下题目,是有序数组,并按照从小到大的顺序排序。
也就是说这种循环所有数据的情况,当你的目标值在有序的数组中,如果比较小,那么循环的次数会很少,当目标值比较大的时候,你可能循环了整个数组,也找不到你要找的目标值。

对于这种情况,我们优化一下,看有没有时间复杂度更低的方法可以解决。

分析2——二分查找

思路分析:为什么是二分查找

首先我们来看看二分查找的定义:
二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。
百度百科

所以呢,在我们选在各种排序各种查找的前提是,我们见过并且大概知道原理,并且知道它适用于什么前提,比如此题:
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

  • 数组中查找目标值,满足查找的前提
  • n个元素有序并且是升序的整型数组 ,满足折半查找的线性表且是顺序存储结构,而且是有序排列的前提

综上,我们采用二分查找的方法;那么问题来了,怎么找?
我们以例子
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1:
为例分析如下:

  1. 二分查找(折半查找)

准备:
折半左指针:left
折半右指针:right
折半目标折半指针:index

在这里插入图片描述

第一步:
left = 0
right = nums.length - 1 = 6 - 1 = 5
如上图,我看可以看到右下角标注的是当前数组的角标

第二步:
折半,顾名思义,将数组对半,取中间值
index = (left + right )/ 2 = ( 0 + 5 ) / 2 = 2.5 int类型算法,省去小数即为 2
在这里插入图片描述
index = 2
nums[index] = 3
因为数组是升序排列,所以index位置的nums值 如果大于target目标值,则target应该在left -> index范围内

如果小于target目标值,说明左侧的均是小于target目标值,所以 target应该在 index -> right范围内

当前 num[index] = 3 target = 2 num[index] > target 所以范围应该缩小到left -> index范围内

因为当前index值已经比较过了,所以没有必要将其放到范围内,所以index 完全可以 减1 将其赋值给right

在这里插入图片描述

第三步
上诉完成之后,继续下一次循环,左侧未变,不更改,右侧范围缩小,此时应该将index 置为 right
在这里插入图片描述

当前范围应该是left - > index -1
即为 0 到 1

重复上诉步骤,
index = (left + right) / 2 = (0 + 1) / 2 = 0
nums[index] = nums[0] = 1 < target(2)
判断上诉步骤:
因为数组是升序排列,所以index位置的nums值 如果大于target目标值,则target应该在left -> index范围内

如果小于target目标值,说明左侧的均是小于target目标值,所以 target应该在 index -> right范围内

此时满足小于目标值,所以应该在 index -> right 之间

同理,index 当前位置已经比较过了,所以应该index+ 1 同时置为left

在这里插入图片描述
此时还有当前位置还没有比较,所以我们的判断条件应该是 left <= right ,说明左指针往右移动,右指针往左移动,两者重合了,当前所在位置,没有判断,所以条件应该包括 等于

当前值:
index = (left + right) / 2 = (1 + 1) / 2 = 1
nums[index] = nums[1] = 0 < target(2)

如果小于target目标值,说明左侧的均是小于target目标值,所以 target应该在 index -> right范围内

我们来看一下临界情况下,index = 1 right = 1
正常我们应该是对index + 1 复制给left ,此时left = 2,

在这里插入图片描述
说明一旦left > right说明我们找不到我们要寻找的target ,可以结束循环;

分析完毕,来看下代码梳理

代码 2
package com.spring.zcl.study.springbootstudy.leecode.algorithmIntroduction;

/**
 * @Author: zcl
 * @Date: 2022-01-20 11:43
 */
public class FirstDay01 {

    public static int search(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        int index = -1;
        while (left <= right) {
            index = (left + right)/2;
            System.out.println("left = " + left + "; right = " + right + "; index = " + index);
            if (nums[index] == target) {
                return index;
            }
            if (nums[index] > target) {
                right = index - 1;
            } else {
                left = index + 1;
            }
        }
        return -1;
    }
    public static void main(String[] args) {
        int[] nums = {-1,0,3,5,9,12};
        int search = search(nums, 2);
        System.out.println(search);
    }
}

我们在循环中打印了一下指针的位置:
运行结果如下:

left = 0; right = 5; index = 2
left = 0; right = 1; index = 0
left = 1; right = 1; index = 1
-1

看运行结果和分析的是一样的

时间复杂度
 while (left <= right) {
        index = (left + right)/2;
        System.out.println("left = " + left + "; right = " + right + "; index = " + index);
        if (nums[index] == target) {
            return index;
        }
        if (nums[index] > target) {
            right = index - 1;
        } else {
            left = index + 1;
        }
    }

二分查找的循环,我们看一下时间复杂度是多少:
例子:
还是输入: nums = [-1,0,3,5,9,12], target = 2为例

left = 0  right = nums.length  - 1 =  6 - 1 = 5  0 < 5  index = 2   nums[2] = 3 > 2
left = 0  right = index - 1 = 2 - 1 = 1 0 < 1           index = 0 nums[0] = -1 < 2
left = index + 1 = 0 + 1 = 1 right = 1 1 == 1          index = 1 num[1] = 0 < 2
left = index + 1 = 1 + 1 = 2 right = 1  2 > 1 结束

当n = 6 时,循环3次,判断4次   logn的复杂度
怎么得到的呢?
我们接着扩大n值看一下规律:
当n = 8 ,  折半 8/2 = 4 4/2 = 2 2/2 = 1 结束 3次
当n = 16, 折半 16/2 = 8 8/2 = 4 4/2 = 2 2/2=1 结束 4次
当n是,    折半 n/2/2/2/2..../2 = 1 时结束 2^ (次数) = n
次数= logn
所以时间复杂度应该是O(logn)

到这里已经算是分析完成了,

优化1 —— 加法or减法

但是是否还有继续优化的方式呢,思考一下,当前算法是采用的算数运算

也就是 (left + right)/ 2

这个位置有什么可优化的呢,我也是接触到另一道类似的二分查找才发现,当right 取值接近int类型最大值的时候, 此时left = 0 , 没有什么问题,当我们计算 left + right 不会出现问题

但是当进入下次循环的时候,有可能出现超过int最大值的问题:

( left + right) / 2 = (0 + right)/2 = 1/2 right

此时的nums[1/2right] = m

m > target ,按照我们的逻辑,target的目标值范围应该在 index - > right之间 也就是1/2right ----> right - 1的范围

当我们再次进入循环
index = (right -1 + 1/2right) = (3/2right - 1)

right的值是接近int的最大值的,3/2right会超过int的最大值,所以这块我们需要做下优化,考虑到int类型的值问题,我们应该尽量采用 减法而不是加法来进行计算

( left + right) / 2 我们可以修改为 left + (right - left) / 2就可解决超过最大值的问题;

优化2 —— 算数运算or 位运算

left + (right - left) / 2

学过计算机基础理论的应该都清楚,计算机中是没有算数运算的,那么计算机是怎么计算的呢,计算机是通过将算数的转换成2进制,然后进行与运算,或运算,异或等等运算,而且计算机中实际上是没有除法运算的,只不过是转换成减法进行运算。这里就不赘述了。

所以我们可以将其优化成直接进行位运算

left + (right - left ) >> 1

相信你会有惊喜的发现

结束

收获:

  • 二分查找能够将时间复杂度O(n)的算法优化到O(logn)
  • 极限值的判断,加法选择还是减法选择,结果一样,但是过程可能会很不一样
  • 位运算优于算数运算
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值