【剑指offer】学习计划day4

目录

[一. 前言](#一. 前言)

二.数组中重复的数字

a.题目

b.题解分析

c.AC代码

[三.在排序数组中查找数字 I](#三.在排序数组中查找数字 I)

a.题目

b.题解分析

c.AC代码

四.0~n-1中缺失的数字

a.题目

b.题解分析

c.AC代码


一. 前言

本系列是针对Leetcode中剑指offer学习计划的记录与思路讲解。详情查看以下链接:

剑指offer-学习计划https://leetcode.cn/study-plan/lcof/?progress=x56gvoct

本期是本系列的day4,今天的主题是----》查找算法(简单)

题目编号:JZ03,JZ53-I,JZ53-II

二.数组中重复的数字

a.题目

b.题解分析

法1. 暴力排序

我们可以直接对数组从小到大进行排序。然后遍历这个有序数组,如果出现前后两个元素重复的情况,则返回这个数即可。

但是我们知道排序的时间成本太高了,最快也是量级的,我们有没有办法将时间复杂度降到呢?


法2. 哈希表

这种和查找重复元素有关的题目,我们可以使用哈希表,哈希表查找的时间复杂度为O(1)。我们可以在直接遍历数组的同时在哈希表中查找是否存在此元素,如果存在,则该元素就是一个重复元素;如果不存在,则将其插入哈希表。

时间复杂度: 遍历数组O(N),哈希表插入和查找都为O(1),合计O(N)。

空间复杂度: 使用了哈希表作为辅助空间,空间复杂度为O(N)。


法3. 原地交换

方法二其实就是我们经常采用的以空间换时间的策略。那有没有什么方法既可以保证时间效率,又可以节省空间呢?答案是有的,我们可以在原数组上动刀子

这种方法其实也是利用了哈希映射的思想,我们观察题目发现数组的下标范围为0~n-1,每个元素也在0~n-1之间,因此我们可以将原数组看做一个哈希表来节省空间。具体的方式如下:

  1. 遍历数组arr,i为下标,如果arr[i] == i,即当前元素的值和其对应的下标相同,说明当前元素已经在正确的索引位置,无需交换,i++向后遍历。
  2. arr[i] != i时,我们需要将其交换到正确的索引位置。此时如果索引位置处的值等于arr[i],则说明此索引对应多个值,arr[i]就是一组重复数字,返回;否则就将arr[i]交换到索引位置arr[i]处。
  3. 如果数组遍历完依旧没有返回,则没有重复数字,返回-1。

时间复杂度: 遍历一遍数组,时间复杂度为O(N)。

**空间复杂度:**没有使用额外的辅助空间,空间复杂度为O(N)。


c.AC代码

cpp 复制代码
//法1:暴力排序,不推荐,代码略

//法2:哈希表,时间O(N),空间O(N)
class Solution {
public:
    int findRepeatNumber(vector<int>& nums)
    {
        unordered_map<int, bool> hash; //定义一个哈希表,表中的值表示对应元素是否存在
        for (int i = 0; i < nums.size(); i++) //遍历数组
        {
            if (hash[nums[i]] == true) //当前元素已经存在,直接返回
            {
                return nums[i];
            }
            hash[nums[i]] = true; //不存在则插入
        }
        return -1; //找不到重复元素,返回-1
    }
};


//法3:原地交换,时间O(N),空间O(1)
class Solution {
public:
    int findRepeatNumber(vector<int>& nums) 
    {
        int i=0;
        while(i < nums.size()) //遍历数组
        {
            if(nums[i] == i) //已经在索引位置上,无需交换,前进
            {
                i++;
            }
            else if(nums[ nums[i] ] == nums[i]) //不在索引位置,且索引位置已经有正确值了
            {
                return nums[i];
            }
            else //不在索引位置,索引位置不是正确值,交换
            {
                swap(nums[i],nums[ nums[i] ]);
            }
        }
        return -1; //找不到重复元素,返回-1
    }
};

三.在排序数组中查找数字 I

a.题目

b.题解分析

法1. 遍历计数(极度不推荐)

这种最朴素的解法就不用多说了,遍历数组用变量统计次数,一次循环O(N)搞定!但是如果你这么做的话面试官可能就要开始和你闲聊了,问问你们家乡的风土人情

题目阐明了nums是个排序数组自然有它的道理

法2. 两次二分(推荐)

是个排序数组又让我们查找,自然而然我们就会想到二分查找。既然让我们求出现的次数,那我们找到左边界,再找到右边界,两个下标相减不就好了,easy。

具体方法如下:

  1. 初始化:初始化左边界为0,右边界为numsSize-1。即闭区间。
  2. 找左边界下标:先求出中间下标mid,如果nums[mid] >= target,则缩小右边界right到mid-1,如果nums[mid] < target,则缩小左边界left到mid+1。循环直到left > right时,left就是我们要找的左边界。
  3. **判断有效性(可选):**判断一下找到的左边界是否有效,无效则说明没有此数字,直接返回。
  4. **找右边界下标:**求出中间下标mid,如果nums[mid] <= target,则缩小左边界left到mid+1,如果nums[mid] > target,则缩小右边界right到mid-1。循环直到left > right时,right就是我们要找的右边界。
  5. **返回区间个数:**由于我们的边界是闭区间,所以区间内的元素个数为right-left+1,即为最终答案。

具体动图如下:

时间复杂度: 二分法每次搜索区间缩小一半,时间复杂度O(logn)。

空间复杂度: 只使用到了常数级的变量,空间复杂度为O(1)。


法3. 一次二分(不太推荐)

上面是分别用2次二分查找左右边界,其实我们也可以只用1次二分同时查找左右边界噢,方法如下:

  1. 初始化:初始化左边界为0,右边界为numsSize-1。即闭区间。
  2. 边界缩小: 求出中间下标mid,如果nums[mid] > target,说明右边界在mid左边,因此缩小右边界right到mid-1;如果nums[mid] < target,说明左边界在mid右边,因此缩小左边界left到mid+1;而如果nums[mid] == target,我们显然不能将左右边界缩小到mid,这会导致跨过边界,只能一步步进行移动,即如果左边界不为目标值则left++,右边界不为目标值则right--;循环直到左右边界的值都为目标值,此时的left和right即为所找的边界。
  3. **返回区间个数:**由于我们的边界是闭区间,所以区间内的元素个数为right-left+1,即为最终答案。

具体动图如下:

但笔者不太推荐这种方法,原因是这种方法在一些情况下时间复杂度会退化到O(N)。例如:

我们发现每次mid的值都为3,nums[mid]恒等于Targer ,left和right只能移动1步,最后相当于把数组遍历了一遍,时间复杂度退化为O(N)。

时间复杂度: 采用二分法,时间复杂度O(logn);但在一些特殊情况如目标值正好在数组正中间时会退化为O(N)。

空间复杂度: 只使用到了常数级的变量,空间复杂度为O(1)。

c.AC代码

cpp 复制代码
//法1:循环计数,时间复杂度O(N),极度不推荐,代码略

//法2:两次二分法,时间复杂度O(logN),空间复杂度O(1)
int search(int* nums, int numsSize, int target)
{
    int left = 0;
    int right = numsSize - 1;
    //找左边界
    while (left <= right)
    {
        int mid = left + (right - left) / 2; //求中间值
        if (nums[mid] >= target)
        {
            right = mid - 1;
        }
        else if (nums[mid] < target)
        {
            left = mid + 1;
        }
    }
    int left_ans = left; //将左边界的下标保存起来
    //检查左边界合法性,可选
    if (left_ans >= numsSize || left_ans < 0 || nums[left_ans] != target)
    {
        return 0;
    }
    //重置用于找右边界
    left = 0;
    right = numsSize - 1;
    while (left <= right)
    {
        int mid = left + (right - left) / 2;
        if (nums[mid] > target)
        {
            right = mid - 1;
        }
        else if (nums[mid] <= target)
        {
            left = mid + 1;
        }
    }
    int right_ans = right; //保存右边界
    return right_ans - left_ans + 1; //闭区间,记得+1
}


//法3,一次二分找两边界,时间复杂度O(logN),但遇到特殊情况时间复杂度会退化到O(N),故不太推荐.空间复杂度O(1)
int search(int* nums, int numsSize, int target)
{
    int left = 0;
    int right = numsSize - 1;
    while (left <= right)
    {
        int mid = left + (right - left) / 2; //求中间值
        if (nums[mid] > target) //缩小右边界
        {
            right = mid - 1;
        }
        else if (nums[mid] < target) //缩小左边界
        {
            left = mid + 1;
        }
        else //等于目标值
        {
            if (nums[left] == target && nums[right] == target) //已经缩小到正确位置,break
                break;
            if (nums[left] != target) //一步步缩小
                left++;
            if (nums[right] != target)
                right--;

        }
    }
    return right - left + 1; //闭区间,记得+1
}

四.0~n-1中缺失的数字

a.题目

b.题解分析

法1. 直接遍历

由于数组是递增数组,假如没有缺失数字,则每个位置的数字应该会下标构成一一映射的关系,即nums[0]=0,nums[1]=1......。因此我们只要遍历一次数组,当出现nums[i] != i时,则说明下标i位置对应的数缺失,返回缺失的数i。如果数组遍历完还没找到,则说明0-numsSize-1的数都存在,消失的数就为numsSize。

时间复杂度: 遍历数组,时间复杂度O(N)。

空间复杂度: 只使用到了常数级的变量,空间复杂度为O(1)。


法2. 位运算

根据异或运算x⊕x=0 和 x⊕0=x的运算规则,我们可以先将0~n-1的每个数进行异或,然后再将结果和数组nums中的每个元素进行异或,最后剩下的一定就是消失的数字。

时间复杂度: 遍历数组进行异或运算,时间复杂度O(N)。

空间复杂度: 只使用到了常数级的变量,空间复杂度为O(1)。


法3. 数学

除了用异或,我们也可以先将0~n-1的每个数进行相加,这里可以使用等差数列前n项和公式。然后再将结果和数组中每个元素进行相减抵消,剩下的就是消失的数字。

时间复杂度: 遍历数组进行相减,时间复杂度O(N)。

空间复杂度: 只使用到了常数级的变量,空间复杂度为O(1)。


法4. 二分查找

既然是递增数组,又让我们查找,那可不可以使用二分的方式呢?答案是可以的。我们只要找到第一次出现nums[i] != i的位置即可,二分的具体思路如下:

  1. 初始化:初始化左边界为0,右边界为numsSize-1。即闭区间。
  2. **锁定位置:**先求出中间下标mid,如果nums[mid] == mid,则说明左半部分(包括mid)全部对应,消失的数字在右半部分,缩小左边界left到mid+1;如果nums[mid] != mid,则说明消失的数字在左半部分(包括mid),我们缩小右边界right到mid-1(当然这可能错过,但不影响)。循环直到left > right时我们就结束循环,此时的left即为消失的数字。
  3. 关于错过: 可能有的人会有疑问,如果mid恰好是消失的数字,那right缩小到mid-1不就错过mid了吗?确实是错过了,但又没有真正错过。我们发现错过之后的nums[mid] 始终等于 mid,即left会一直往右走,right就不会动了,当left > right时left恰巧就是我们错过的位置,这也是为什么要返回left的原因。

具体动图如下:

时间复杂度: 采用二分法,时间复杂度O(logn)。

空间复杂度: 只使用到了常数级的变量,空间复杂度为O(1)。

c.AC代码

cpp 复制代码
//法1,排序数组直接遍历查找 时间O(N),空间O(1)
int missingNumber(int* nums, int numsSize)
{
    //数组下标0-numsSize-1 , 数据范围0 - numsSize
    int k = 0;
    for (k = 0; k < numsSize; k++)
    {
        if (nums[k] != k)//不为k说明k为消失的数字
        {
            return k;
        }
    }
    //0-numsSize-1都存在,则k==numsSize即为消失的数字
    return k;
}

//法2,异或 时间O(N),空间O(1)
int missingNumber(int* nums, int numsSize)
{
    int val = 0;
    //对0-numsSize的数字进行异或
    for (int i = 0; i <= numsSize; i++)
    {
        val ^= i;
    }
    //再对数组每个元素进行异或
    for (int i = 0; i < numsSize; i++)
    {
        val ^= nums[i];
    }
    return val;
}

//法3,0-(n-1)全部相加然后与数组相减 时间O(N),空间O(1)
int missingNumber(int* nums, int numsSize)
{
    int sum = 0;
    //对0-numsSize的数字进行相加
    for (int i = 0; i <= numsSize; i++)
    {
        sum += i;
    }
    //再减去数组的每个元素
    for (int i = 0; i < numsSize; i++)
    {
        sum -= nums[i];
    }
    return sum;
}

//法4,采用二分法 时间O(logN),空间O(1)
int missingNumber(int* nums, int numsSize)
{
    int left = 0;
    int right = numsSize - 1;
    while (left <= right)
    {
        int mid = left + (right - left) / 2;
        if (nums[mid] == mid) //left右移
        {
            left = mid + 1;
        }
        else //nums[mid] != mid ,right左移
        {
            right = mid - 1;
        }
    }
    return left;
}

以上,就是本期的全部内容啦 🌸

制作不易,能否点个赞再走呢 🙏

相关推荐
海尔辛几秒前
学习黑客Kerberos深入浅出:安全王国的门票系统
学习·安全·kerberos·window
霸王蟹8 分钟前
React 19中如何向Vue那样自定义状态和方法暴露给父组件。
前端·javascript·学习·react.js·typescript
Moonnnn.37 分钟前
【数字电路】第七章 脉冲波形的产生与整形电路
笔记·学习
猴子请来的逗比4892 小时前
tomcat查看状态页及调优信息
服务器·学习·tomcat·firefox
贺函不是涵3 小时前
【沉浸式求职学习day43】【Java面试题精选3】
java·开发语言·学习
maray3 小时前
ETL 学习
数据仓库·学习·etl
海尔辛3 小时前
学习黑客Active Directory入门
学习·ad·window
superior tigre3 小时前
C++学习:六个月从基础到就业——C++20:协程(Coroutines)
c++·学习·c++20
冷崖3 小时前
网络编程-select(二)
网络·学习
superior tigre3 小时前
C++学习:六个月从基础到就业——C++20:概念(Concepts)
c++·学习·c++20