day1——二分查找,双指针

南京城天气阴,下雨,下午和晚上效率比较高,1.30睡的觉,加油。

日期:2025/3/12

                                                                                题目信息以及部分代码来源:代码随想录

算法题目概览

704. 二分查找

学习成果:掌握了左闭右闭的二分查找算法书写,把力扣704二分查找彻底掌握了,同时对于35题搜索插入位置也理解了,根据录哥给出的这个题解,从中学习了算法思考时的一些思想。对于34在排序数组中查找元素的第一个和最后一个位置这个题没怎么看

题目链接:704. 二分查找 - 力扣(LeetCode)

文章讲解:代码随想录

视频讲解:手把手带你撕出正确的二分法 | 二分查找法 | 二分搜索法 | LeetCode:704. 二分查找_哔哩哔哩_bilibili

27. 移除元素

题目建议: 暴力的解法,可以锻炼一下我们的代码实现能力,建议先把暴力写法写一遍。 双指针法 是本题的精髓,需要掌握

题目链接:27. 移除元素 - 力扣(LeetCode)

文章讲解:代码随想录

视频讲解:数组中移除元素并不容易! | LeetCode:27. 移除元素_哔哩哔哩_bilibili

977.有序数组的平方

题目建议: 本题关键在于理解双指针思想,这个题目双指针实现真的太巧妙了!之前没有系统学习过双指针的类型题目,感觉应该自己对双指针适用的题目总结一下

题目链接:977. 有序数组的平方 - 力扣(LeetCode)

文章讲解:代码随想录

视频讲解: 双指针法经典题目 | LeetCode:977.有序数组的平方_哔哩哔哩_bilibili

左闭右闭二分查找

题目介绍

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

  • n 将在 [1, 10000]之间。
  • nums 的每个元素都将在 [-9999, 9999]之间。

二分查找介绍

基本条件:数组为有序数组

针对于元素从小到大和元素从大到小的话,对应二分算法是不一样的,需要略作调整。

还有很多关于二分查找的简单变形,代码在这里基础上都有相应调整,由于本系列文章的性质,不多赘述。并且关于二分查找的库函数这里也不介绍。

二分手写代码难点:

1.搜索区间定义      2.循环搜索条件的边界     3.搜索区间变小后区间的更新

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

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

我认为左闭右闭即[left, right]比较好理解,故只介绍这一种 。

二分查找代码&细节解释

实现的是左闭右闭的代码思路

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
        while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
            int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
            if (nums[middle] > target) {
                right = middle - 1; // target 在左区间,所以[left, middle - 1]
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,所以[middle + 1, right]
            } else { // nums[middle] == target
                return middle; // 数组中找到目标值,直接返回下标
            }
        }
        // 未找到目标值
        return -1;
    }
};

1.变量初始化

int left = 0;: 初始化左指针 left,指向数组的第一个元素,索引为 0。

int right = nums.size() - 1;: 初始化右指针 right,指向数组的最后一个元素,索引为 nums.size() - 1。

关键点:区间定义

注释 // 定义target在左闭右闭的区间里,[left, right] 非常重要。它说明我们定义的搜索区间是 左闭右闭 的 [left, right]。这意味着 left 和 right 指向的元素都在我们当前考虑的搜索范围内

2.while (left <= right) 循环条件 

while (left <= right): 循环条件是 left <= right 而不是 left < right。

为什么是 <= 而不是 < ?

因为我们定义的区间是 左闭右闭 [left, right]。当 left == right 时,区间 [left, right] 仍然包含一个元素,即索引为 left(或 right)的元素,这个元素仍然需要被检查。只有当 left > right 时,区间才为空,搜索区间才无效,循环才应该结束。

3. 计算中间索引 middle

int middle = left + ((right - left) / 2);: 计算中间索引 middle。

为什么用 left + ((right - left) / 2) 而不是 (left + right) / 2 ?

虽然在数学上它们的值是相等的,但是 (left + right) / 2 在 left 和 right 都很大时,可能会导致 整数溢出。而 left + ((right - left) / 2) 可以有效地防止溢出。这种写法先计算 right - left 的差值,然后再除以 2,最后加上 left,可以保证结果不会溢出。

4.条件判断和区间更新

注意数组元素是从小到大的,条件判断是分成了><和==三种

if (nums[middle] > target): 如果中间元素 nums[middle] 大于目标值 target

right = middle - 1;: 说明目标值 target 只可能在中间元素的 左侧 区间。由于我们已经检查过 nums[middle] 不是目标值,所以新的搜索区间应该排除 middle 索引,将右边界 right 更新为 middle - 1。新的搜索区间变为 [left, middle - 1]。(这个过程可以在脑子里想一下可能比较清晰,记住我们的搜索区间是左闭右闭才能理解这里的-1

同理nums[middle] < target情况比较好想。

nums[middle] == target,说明找到了,可以返回了!

略作微调

上面介绍的是数组元素从小到大排列,如果数组元素改成从大到小的话,代码就要微调。1.变量初始化,2.while (left <= right) 循环条件 3. 计算中间索引 middle这三点都不用变。

只有关于4.条件判断和区间更新逻辑需要做出微调

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
        while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
            int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
            // 修改判断条件:
            // 在从大到小降序数组中,如果中间元素 nums[middle] 大于目标值 target,
            // 说明目标值 target 可能在 middle 的右侧(因为右侧的元素更小或相等)。
            if (nums[middle] > target) {
                left = middle + 1; // target 在右区间
            }
            // 修改判断条件:
            // 在从大到小降序数组中,如果中间元素 nums[middle] 小于目标值 target,
            // 说明目标值 target 可能在 middle 的左侧(因为左侧的元素更大或相等)。
            else if (nums[middle] < target) {
                right = middle - 1; // target 在左区间
            } else { // nums[middle] == target
                return middle; // 数组中找到目标值,直接返回下标
            }
        }
        // 未找到目标值
        return -1;
    }
};

双指针移除元素

 题目介绍

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组

假设 nums 中共有n个元素,不等于 val 的元素数量为 k,那么等于val的元素自然就是n-k个,那么我们需要做的事情是将n个元素中等于val的值全部删去,使得修改后的数组前k个元素为原数组中的k个不等于val的值。

原地的含义:

原地”指的是在原始数组上直接进行操作,而不创建新的数组。这意味着你不能分配额外的内存空间来存储结果,而必须修改输入的数组本身。

具体来说,“原地”意味着:

  • 空间复杂度为 O(1):你只能使用常数级别的额外空间。也就是说,你不能创建一个新的数组来存储结果,而是要在原始数组上进行修改。
  • 直接修改输入数组:所有的操作都必须在输入的数组上进行,而不是创建一个新的数组。

题目思路:

这个题我看到第一眼的想法然后创建一个和原数组大小一样的数组,遍历一遍原数组,遍历过程中遇到不等于val的元素依次放置到新数组中。但是显然这样子时间复杂度和空间复杂度都是O(n),在这个题目中我们应该追求的是空间复杂度为O(1)。所以就不能创建新的数组了,只能对原数组进行原地操作。想了一会没有想到好的解决思路,就去看了卡哥的双指针解法。

双指针法讲解&代码

双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。

定义快慢指针

  • 快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
  • 慢指针:指向更新 新数组下标的位置

很多同学这道题目做的很懵,就是不理解 快慢指针究竟都是什么含义,所以一定要明确含义,后面的思路就更容易理解了。

双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组、链表、字符串等操作的面试题,都使用双指针法,下面我们也会介绍经典使用场景。

// 时间复杂度:O(n)
// 空间复杂度:O(1)
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int slowIndex = 0;
        for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
            if (val != nums[fastIndex]) {
                nums[slowIndex++] = nums[fastIndex];
            }
        }
        return slowIndex;
    }
};

这道题目的话不太理解的话可以进行一下空间的想象,当然最有效的方式就是画个图然后标记两个指针手动推导一下,真正理解了思路之后代码就会很好写!!!

快慢指针法的经典使用场景

  • 链表问题
    • 判断链表是否存在环:快指针每次移动两步,慢指针每次移动一步。如果链表存在环,快指针最终会追上慢指针。
    • 查找链表的中间节点:快指针每次移动两步,慢指针每次移动一步。当快指针到达链表末尾时,慢指针正好指向中间节点。
    • 查找链表中倒数第 k 个节点:快指针先移动 k 步,然后快慢指针同时移动。当快指针到达链表末尾时,慢指针指向倒数第 k 个节点。
  • 数组问题
    • 移除数组中满足特定条件的元素快指针遍历数组进行筛选工作,慢指针进行填充工作,快指针遇到不符合条件的元素就忽略,遇到符合条件的就让慢指针来将这个元素填充到数组中。比如可以筛除特定元素,或者所有奇数/偶数,或者某个数的倍数的所有元素。
  • 字符串问题
    • 判断回文字符串:通过使用快慢指针从字符串两端向中间移动,可以快速判断字符串是否为回文。

快慢指针的优点

  • 空间复杂度低:通常只需要常数级别的额外空间(O(1))。
  • 时间复杂度相对较低:在某些情况下,可以达到线性时间复杂度(O(n))。
  • 代码实现简单:相对于其他复杂的算法,快慢指针法的代码实现通常比较简洁。

双指针有序数组的平方

 题目介绍

看到这个题目,由于我们首先可以想到的解决方式就是遍历一遍将每个数组元素进行平方,然后再使用排序算法进行排序。

代码如下:

class Solution {
public:
    vector<int> sortedSquares(vector<int>& A) {
        for (int i = 0; i < A.size(); i++) {
            A[i] *= A[i];
        }
        sort(A.begin(), A.end()); // 快速排序
        return A;
    }
};

这个时间复杂度是 O(n + nlogn), 可以说是O(nlogn)的时间复杂度,空间复杂度为O(1)。如果这个数组是一个乱序数组的话,这样做的话是毫无问题的。

但本题中说到这是一个非递减顺序的数组,这个关键信息我们根本没有用到。下面我们来分析考虑一下双指针法!这里的双指针法时间复杂度为O(n),空间复杂度为O(n)。

双指针法讲解&代码

数组其实是有序的, 只不过负数平方之后可能成为最大数了。

那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间

此时可以考虑双指针法了,i指向起始位置,j指向终止位置。

定义一个新数组result,和A数组一样的大小,让k指向result数组终止位置。

如果A[i] * A[i] < A[j] * A[j] 那么result[k--] = A[j] * A[j]; 。

如果A[i] * A[i] >= A[j] * A[j] 那么result[k--] = A[i] * A[i]; 。

如动画所示:

时间复杂度为O(n),空间复杂度为O(n)

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        vector<int> square_num(nums.size(),0);
        int nums_num=nums.size()-1;
        int pointer_left=0,pointer_right=nums.size()-1;
        while(pointer_left<=pointer_right){// 注意这里要pointer_left <= pointer_right,因为最后要处理两个元素
            if(abs(nums[pointer_left])<=abs(nums[pointer_right])){//这里的判断也可以用平方
                square_num[nums_num--]=nums[pointer_right]*nums[pointer_right];
                pointer_right--;
            }else{
                square_num[nums_num--]=nums[pointer_left]*nums[pointer_left];
                pointer_left++;
            }
        }

    return square_num;
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值