数组DAY01(12.04)

10 阅读7分钟

数组理论基础

文章链接:programmercarl.com/%E6%95%B0%E…

704. 二分查找

1. 资源

先把 704写熟练,要熟悉 根据 左闭右开,左闭右闭 两种区间规则 写出来的二分法

题目链接:leetcode.cn/problems/bi…

文章讲解:programmercarl.com/0704.%E4%BA…

视频讲解:www.bilibili.com/video/BV1fA…

2. 个人总结

2.1. 算法原理

二分查找的核心思想非常简单:
每次把搜索范围砍掉一半,直到找到答案。

想象你在一本字典里找一个单词,不可能从第一页一页一页翻。更聪明的做法是:

  1. 把字典从中间翻开
  2. 看一下这页的单词,判断是比你要找的“靠前”还是“靠后”
  3. 再决定继续在前半部分翻,还是后半部分翻
  4. 不断重复这个过程

这就是二分查找。

2.2. 应用前提

什么类型的题目适合二分法?

  1. 有序序列
  2. 无重复元素
2.3. 注意点
  1. 区间的两种定义方式

1)左闭右闭:左边界是有序序列起始下标,右边界是结束位置下标。右边界必须是数组的有效下标。 举例(left = 0,right = size -1)

2)左闭右开:左边界是有序序列起始下标,右边界下标+1.(容器长度)。右边界是一个无效的数组下标。 举例(left = 0,right = size)

  1. while 循环条件:

· 1)左闭右闭:

while(left <= right)

因为right是一个有效的下标,left = right是一个有效的情况,需要进入循环判断

2)左闭右开:

  while(left < right)

right是无效的下标,如果left都等于right了,说明都已经越过查找的区间了,没必要再进入循环了。

  1. right赋值:

当明确查找的元素在序列的左侧,那么需要把右边界左移动,移动到上次进行判断的位置

1)左闭右闭:

right = mid -1

因为mid 在上一次循环的时候已经判断过了,此时最右边需要判断的是 mid -1位置上面的元素

2)左闭右开:(易错点!!)

这部分我第一次写错了,想当然的和上面写的一样,但是实际上,此时right一直都被定义为一个无效的下标,即真正的有效右边界+1.所以,此时真正有效的右边界时mid -1,那么right就应该时 (mid-1)+1,因此:

right = mid
2.4. 代码:

左闭右闭

class Solution {
public:
int search(vector<int>& nums, int target) 
{
    int left = 0;
    int right = nums.size() - 1;

    while (left <= right)
        {
            int mid = left + (right - left) / 2;

            if (nums[mid] == target)
                return mid;

            if (nums[mid] < target)
                left = mid + 1;
            else
                right = mid - 1;
        }
    return -1;
}
};

左闭右开

class Solution {
public:
int search(vector<int>& nums, int target) 
{
    int left = 0;
    int right = nums.size(); // 注意:右边界是开区间

    while (left < right) // 左闭右开循环条件
        {
            int mid = left + (right - left) / 2;

            if (nums[mid] == target)
                return mid;

            if (nums[mid] < target)
                left = mid + 1; // target 在右区间
            else
                right = mid;    // target 在左区间(mid 也可能是答案,所以 right = mid)
        }
    return -1; // 没找到
}
};

27. 移除元素

1. 资源

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

题目链接:leetcode.cn/problems/re…

文章讲解:programmercarl.com/0027.%E7%A7…

视频讲解:www.bilibili.com/video/BV12A…

2. 个人总结

2.1. 算法原理

双指针算法的核心思想是:

用两个指针同时遍历或操作数组/字符串,让一个指针记录当前位置,另一个指针记录结果位置或边界,完成原地修改、查找或比较操作。

  1. 两个指针同时“走路”

一个指针负责遍历数组/字符串(快指针)

另一个指针负责记录结果位置、边界或窗口(慢指针/左指针/右指针)

  1. 避免重复遍历
  2. 原地处理数据

双指针经常用来原地修改数组。一个遍历,一个写入 → 完成删除、移动、合并等操作

2.2. 应用前提

什么类型的题目适合双指针?

  1. 题目要求原地修改数组/返回长度/索引?
  • 是 → 候选双指针
  • 否 → 可能用哈希表、排序或栈
  1. 条件是否能仅凭当前指针位置判断?
  • 能 → 双指针可用
  • 不能 → 双指针单用不够,需要额外辅助结构

解释一下,比如这个题目中 num[i] == value,可以让通过快指针拿到值,判断是否满足临界条件,但是有些是不行的,比如 找两个数和为目标值(无序数组) nums = [3, 1, 5, 7], target = 8。 一个快指针拿到值之后,没办法进行临界条件判断。

  1. 是否存在“两个维度”或“两个方向”的操作?
  • 一个遍历,一个记录位置 → 双指针适合
  • 左右同时移动 → 对撞指针/滑动窗口
2.3. 注意点

一定要理解题目中快慢指针的作用分别是什么。

本题目中,记住快指针用来遍历数组,找到应该加入新数组的元素,慢指针用来记录新数组下标。

将快指针找到的新数组元素赋值给慢指针记录的新数组下标位置即可。

2.4. 代码:

暴力解 size这个比较关键,它是有效数组的边界,超出size的部分是已经移动过的,后面不需要再遍历

class Solution {
public:
int removeElement(vector<int>& nums, int val) {
    int size = nums.size();
    for(int i = 0; i < size; ++i)
        {//找到元素所在位置
            if(nums[i] == val)
            {
                for(int j = i; j < size -1; ++j)
                    {//遍历后面的数组,把后面的数组向前移动1位
                        nums[j] = nums[j+1];
                    }
                i--;//重新判断i位置上的新元素
                size--;
            }
        }

    return size;
}
};

双指针

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        //快慢指针
        int nSlowIndex = 0; //快指针遍历数组,查找应该加入新数组的元素
        int nFastIndex = 0; //慢指针,在确认新数组下标后,后移,记录新数组下一个元素的位置
        for(nFastIndex;nFastIndex < nums.size();++nFastIndex)
        { 
            if(nums[nFastIndex] != val)
            {//不是目标值,应该保留在新数组
                nums[nSlowIndex] = nums[nFastIndex];    //新数组的元素写入新数组应该插入的下标位置
                nSlowIndex++;       //新数组应该插入的下一个位置
            }
        }
        
        return nSlowIndex;
    }
};

977.有序数组的平方

1. 资源

题目建议: 本题关键在于理解双指针思想

题目链接:leetcode.cn/problems/sq…

文章讲解:programmercarl.com/0977.%E6%9C…

视频讲解: www.bilibili.com/video/BV1QB…

2. 个人总结

2.1. 算法原理

显然,这个题目需要考虑负数。如果全是非负数的话,平方后顺序不变,完全没必要费心思。

而有负数,且非递减的数组有个特点:平方之后最大值要么出现在最左边,要么出现在最右边。

一个指针从左,一个指针从右,同时判断哪一边绝对值大,把平方最大值放到结果数组对应位置 ,两个指针对撞代表边界情况。

2.2. 应用前提

什么类型的题目适合双指针?

  1. 题目要求原地修改数组/返回长度/索引?
  • 是 → 候选双指针
  • 否 → 可能用哈希表、排序或栈
  1. 条件是否能仅凭当前指针位置判断?
  • 能 → 双指针可用
  • 不能 → 双指针单用不够,需要额外辅助结构

解释一下,比如这个题目中 num[i] == value,可以让通过快指针拿到值,判断是否满足临界条件,但是有些是不行的,比如 找两个数和为目标值(无序数组) nums = [3, 1, 5, 7], target = 8。 一个快指针拿到值之后,没办法进行临界条件判断。

  1. 是否存在“两个维度”或“两个方向”的操作?
  • 一个遍历,一个记录位置 → 双指针适合
  • 左右同时移动 → 对撞指针/滑动窗口

注意:上面只是提出了一些可以参考的特点,不是说必须满足上述三个条件!

2.3. 注意点

要想到最大值在最左、最右的这种规律。

双指针分别从最左、最右像中间移动。

注意while判断边界,因为左右两个指针指向的都是有效元素,此时不应该漏掉i==j的情况

2.4. 代码:

暴力解

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        for(auto &value : nums)
        {
            value *= value;
        }
        sort(nums.begin(),nums.end());

        return nums;
    }
};

双指针

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        vector<int> result(nums.size());
        int k = result.size() -1; //最大值在新数组应该放的位置
        int i = 0, j = nums.size()-1;
        while(i<=j)
        {
            //最大值在左边
            if((nums[i] * nums[i]) > (nums[j]*nums[j]))
            {
                //最大值赋给新数组,并更新下一次应该放置最大值的位置
                result[k] = nums[i]*nums[i];
                k--;
                i++;
            }
            else
            {//最大值在右边
                result[k] = nums[j] * nums[j];
                k--;
                j--;
            }
            //相等,先放左边和右边都行,这里面相等放在执行else
        }
    return result;
    }
};