开启力扣刷题之路 >数组> 二分法

Posted 王六六的IT日常

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了开启力扣刷题之路 >数组> 二分法相关的知识,希望对你有一定的参考价值。

【代码随想录】力扣刷题攻略  <<< 这是大佬的git链接,感兴趣的小伙伴一起追随大佬的脚步,少走弯路噢。---- 搬运工是我了。。。

                                                                                                          

根据大佬总结的刷题攻略开始力扣刷题!冲鸭!



前言

面对leetcode上近两千道题目,从何刷起?

【代码随想录】大佬说道:

按照如下类型来刷,数组-> 链表-> 哈希表->字符串->栈与队列->树->回溯->贪心->动态规划->图论->高级数据结构

再从简单刷起,做了几个类型题目之后,再慢慢做中等题目、困难题目。

一、数组理论基础

数组是非常基础的数据结构,在面试中,考察数组的题目一般在思维上都不难,主要是考察对代码的掌控能力

也就是说,想法很简单,但实现起来可能就不是那么回事了。首先要知道数组在内存中的存储方式,这样才能真正理解数组相关的面试题

数组是存放在连续内存空间上的相同类型数据的集合。

数组可以方便的通过下标索引的方式获取到下标下对应的数据。

举一个字符数组的例子,如图所示:

需要两点注意的是

  • 数组下标都是从0开始的。
  • 数组内存空间的地址是连续的

正是因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。

例如删除下标为3的元素,需要对下标为3的元素后面的所有元素都要做移动操作,如图所示:

数组的元素是不能删的,只能覆盖。

那么二维数组在内存的空间地址是连续的么?

不同编程语言的内存管理是不一样的,来做一个实验,Java代码如下:

Java是没有指针的,同时也不对程序员暴漏其元素的地址,寻址操作完全交给虚拟机。

public static void test_arr() {
    int[][] arr = {{1, 2, 3}, {3, 4, 5}, {6, 7, 8}, {9,9,9}};
    System.out.println(arr[0]);
    System.out.println(arr[1]);
    System.out.println(arr[2]);
    System.out.println(arr[3]);
}

输出的地址为:

[I@7852e922
[I@4e25154f
[I@70dea4e
[I@5c647e05

这里的数值也是16进制,这不是真正的地址,而是经过处理过后的数值了,我们也可以看出,二维数组的每一行头结点的地址是没有规则的,更谈不上连续。

所以Java的二维数组可能是如下排列的方式:

大佬在git里给了C++相关方面的解读,我是学的java所以没写C++的解题思路和代码,学C++的小伙伴可以从下面的链接点进去了解相关的知识。

二、数组 ~ 二分法

题目链接:https://leetcode-cn.com/problems/binary-search/ (大佬把题目链接也甩过来了,好贴心~)

给定一个 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

提示:

  • 你可以假设 nums 中的所有元素是不重复的。
  • n 将在 [1, 10000]之间。
  • nums 的每个元素都将在 [-9999, 9999]之间。

1. 思路

二分查找是一种基于比较目标值(target)和数组中间元素(mid)的教科书式算法。

  • 如果target等于mid,则找到目标值。
  • 如果target较小,继续在左侧搜索。
  • 如果target较大,则继续在右侧搜索。

这道题目的前提是数组为有序数组,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件,当看到题目描述满足如上条件的时候,可要想一想是不是可以用二分法。

二分查找涉及的很多的边界条件,逻辑比较简单,但就是写不好。例如到底是 while(left < right) 还是 while(left <= right),到底是right = middle呢,还是要right = middle - 1呢?

大家写二分法经常写乱,主要是因为对区间的定义没有想清楚,区间的定义就是不变量。要在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。

分析二分查找的一个技巧是:不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节

写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。

下面根据这两种区间的定义分别讲解两种不同的二分写法。

1.1 二分法第一种写法

我们定义 target 是在一个在左闭右闭的区间里,也就是[left, right] (这个很重要非常重要)

区间的定义这就决定了二分法的代码应该如何写,因为定义target在[left, right]区间,所以有如下两点:

  • while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
  • if (nums[mid] > target) right 要赋值为 mid- 1,因为当前这个nums[mid]一定不是target,那么接下来要查找的左区间结束下标位置就是 mid - 1

算法:

1.初始化指针 left = 0, right = nums.length - 1。

2.当 left <= right:

比较中间元素 nums[mid] 和目标值 target 。

  • 如果 target = nums[mid],返回 mid。
  • 如果 target < nums[mid],则在左侧继续搜索 right = mid- 1。[ left , mid - 1 ]
  • 如果 target > nums[mid],则在右侧继续搜索 left = mid+ 1。[ mid + 1 , right ]

例如在数组:1,2,3,4,7,9,10中查找元素2,如图所示:                                

代码如下:

计算 mid 时需要防止溢出,代码中 int mid = left + (right - left) / 2 和 int mid = (left + right) / 2 的结果相同,有效防止了 left 和 right 太大直接相加导致溢出。

//版本一
class Solution {   //左闭右闭区间
    public int search(int[] nums, int target) {
        // 避免当 target 小于nums[0] || nums[nums.length - 1]时多次循环运算
        if (target < nums[0] || target > nums[nums.length - 1]) {
            return -1;
        }
        int left = 0, right = nums.length - 1;  // 定义target在左闭右闭的区间里,[left, right]

        while (left <= right) {   // 当left==right,区间[left, right]依然有效,所以用 <=

            int mid = left + (right - left) /2 ; // 防止溢出 等同于(left + right)/2
            if (nums[mid] == target)
                return mid; // 数组中找到目标值,直接返回下标
            else if (nums[mid] < target)
                left = mid + 1; // target 在右区间,搜索区间为[middle + 1, right]
            else if (nums[mid] > target)
                right = mid - 1; // target 在左区间,搜索区间为[left, middle - 1]
        }
        return -1;   // 未找到目标值
    }
}

>>>>>>>>>> 为什么 while 循环的条件中是 <=,而不是 <?

答:因为初始化 right 的赋值是 nums.length - 1,即最后一个元素的索引,而不是 nums.length。

这二者可能出现在不同功能的二分查找中,区别是:前者相当于两端都闭区间 [left, right],后者相当于左闭右开区间 [left, right),因为索引大小为 nums.length 是越界的。

该算法中使用的是[left, right] 两端都闭的区间。这个区间其实就是每次进行搜索的区间。

while(left <= right) 的终止条件是 left == right + 1,写成区间的形式就是 [right + 1, right],或者带个具体的数字进去 [3, 2],可见这时候区间为空,因为没有数字既大于等于 3 又小于等于 2 的吧。所以这时候 while 循环终止是正确的,直接返回 -1 即可。

while 循环什么时候应该终止?搜索区间为空的时候应该终止,意味着你没得找了,等于没找到。

作者:labuladong
链接:https://leetcode-cn.com/problems/binary-search/solution/er-fen-cha-zhao-xiang-jie-by-labuladong/
来源:力扣(LeetCode)
 

大佬在git里给了C++相关方面的解读,我是学的java所以没写C++的解题思路和代码,学C++的小伙伴可以从下面的链接点进去了解相关的知识。

1.2 二分法第二种写法

如果说 target 是定义在一个在左闭右开的区间里,也就是 [left, right) ,那么二分法的边界处理方式则截然不同。

有如下两点:

  • while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的
  • if (nums[mid] > target), right 更新为 mid,因为当前nums[mid]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为mid,即:下一个查询区间不会去比较nums[mid]

在数组:1,2,3,4,7,9,10中查找元素2,如图所示:(注意和方法一的区别)---- 向下取整

代码如下: 

计算 mid 时需要防止溢出,代码中 int mid = left + (right - left) / 2 和 int mid = (left + right) / 2 的结果相同,有效防止了 left 和 right 太大直接相加导致溢出。

//版本二
class Solution {      //左闭右开
    public int search(int[] nums, int target) {
        int left = 0, right = nums.length; // 定义target在左闭右开的区间里,即:[left, right)
        while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
            int mid = left + (right - left) /2;
            if (nums[mid] == target) // 数组中找到目标值,直接返回下标
                return mid;
            else if (nums[mid] < target) // target 在右区间,搜索区间为[middle + 1, right)
                left = mid + 1;
            else if (nums[mid] > target) // target 在左区间,搜索区间为[left, middle)
                right = mid;
        }
        return -1; // 未找到目标值
    }
}

while(left < right) 的终止条件是 left == right,写成区间的形式就是 [left, right]

或者带个具体的数字进去 [2, 2],这时候区间非空,还有一个数 2,但此时 while 循环终止了。也就是说这区间 [2, 2] 被漏掉了,索引 2 没有被搜索,如果这时候直接返回 -1 就是错误的。

作者:labuladong
链接:https://leetcode-cn.com/problems/binary-search/solution/er-fen-cha-zhao-xiang-jie-by-labuladong/
来源:力扣(LeetCode)

大佬在git里给了C++相关方面的解读,我是学的java所以没写C++的解题思路和代码,学C++的小伙伴可以从下面的链接点进去了解相关的知识。

2. 总结

二分法是非常重要的基础算法,为什么很多同学对于二分法都是一看就会,一写就废

其实主要就是对区间的定义没有理解清楚,在循环中没有始终坚持根据查找区间的定义来做边界处理。

区间的定义就是不变量,那么在循环中坚持根据查找区间的定义来做边界处理,就是循环不变量规则。

根据两种常见的区间定义,给出了两种二分法的写法,每一个边界为什么这么处理,都根据区间的定义做了详细介绍。

3. 相关题目推荐

  • 35.搜索插入位置
  • 34.在排序数组中查找元素的第一个和最后一个位置
  • 69.x 的平方根
  • 367.有效的完全平方数

总结

第一种,最基本的二分查找算法

因为初始化 right = nums.length - 1
所以决定了「搜索区间」是 [left, right]  左闭右闭
所以决定了 while (left <= right)
同时也决定了 left = mid+1 和 right = mid-1

因为我们只需找到一个 target 的索引即可
所以当 nums[mid] == target 时可以立即返回

第二种,寻找左侧边界的二分查找

因为初始化 right = nums.length
所以决定了「搜索区间」是 [left, right)   左闭右开
所以决定了 while (left < right)
同时也决定了 left = mid + 1 和 right = mid

因为我们需找到 target 的最左侧索引
所以当 nums[mid] == target 时不要立即返回
而要收紧右侧边界以锁定左侧边界

第三种,寻找右侧边界的二分查找

因为初始化 right = nums.length
所以决定了「搜索区间」是 [left, right)   左闭右开
所以决定了 while (left < right)
同时也决定了 left = mid + 1 和 right = mid

因为我们需找到 target 的最右侧索引
所以当 nums[mid] == target 时不要立即返回
而要收紧左侧边界以锁定右侧边界

又因为收紧左侧边界时必须 left = mid + 1
所以最后无论返回 left 还是 right,必须减一

作者:labuladong
链接:https://leetcode-cn.com/problems/binary-search/solution/er-fen-cha-zhao-xiang-jie-by-labuladong/
来源:力扣(LeetCode)

三种情况代码如下:

int binary_search(int[] nums, int target) {
    int left = 0, right = nums.length - 1; 
    while(left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1; 
        } else if(nums[mid] == target) {
            // 直接返回
            return mid;
        }
    }
    // 直接返回
    return -1;
}
//左侧边界
int left_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 别返回,锁定左侧边界
            right = mid - 1; //注意
        }
    }
    // 最后要检查 left 越界的情况
    if (left >= nums.length || nums[left] != target)
        return -1;
    return left;
}

//右侧边界
int right_bound(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            // 别返回,锁定右侧边界
            left = mid + 1; //注意
        }
    }
    // 最后要检查 right 越界的情况
    if (right < 0 || nums[right] != target)
        return -1;
    return right;
}

对于寻找左右边界的二分搜索,常见的手法是使用左闭右开的「搜索区间」,根据逻辑将「搜索区间」全都统一成了两端都闭。

复盘开始:

1、分析二分查找代码时,不要出现 else,全部展开成 else if 方便理解。

2、注意「搜索区间」和 while 的终止条件,如果存在漏掉的元素,记得在最后检查。

3、如需定义左闭右开的「搜索区间」搜索左右边界,只要在 nums[mid] == target 时做修改即可,搜索右侧时需要减一。

4、如果将「搜索区间」全都统一成两端都闭,好记,只要稍改 nums[mid] == target 条件处的代码和返回的逻辑即可,推荐拿小本本记下,作为二分搜索模板

作者:labuladong
链接:https://leetcode-cn.com/problems/binary-search/solution/er-fen-cha-zhao-xiang-jie-by-labuladong/
来源:力扣(LeetCode)

二分法写到这里~~~~~~~~~~~~~~

 

以上是关于开启力扣刷题之路 >数组> 二分法的主要内容,如果未能解决你的问题,请参考以下文章

力扣刷题详解(含代码动态展示)

力扣刷题每日打卡

力扣刷题

两个数组的交集(力扣刷题)

力扣刷题每日打卡

力扣刷题每日打卡