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:
为例分析如下:
- 二分查找(折半查找)
准备:
折半左指针: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)
- 极限值的判断,加法选择还是减法选择,结果一样,但是过程可能会很不一样
- 位运算优于算数运算