【剑指offer】学习计划day4

news2024/11/27 18:32:36

目录

 一. 前言 

 二.数组中重复的数字

         a.题目

         b.题解分析

        c.AC代码 

 三.在排序数组中查找数字 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. 暴力排序

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

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


        法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代码 

//法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代码 

//法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代码 

//法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;
}


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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/778036.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

学习系统编程No.32【线程互斥实战】

引言&#xff1a; 北京时间&#xff1a;2023/7/19/15:22&#xff0c;昨天更新完博客&#xff0c;和舍友下了一会棋&#xff0c;快乐就是这么简单&#xff0c;哈哈哈&#xff01;总体来说&#xff0c;摆烂程度得到一定的改善&#xff0c;想要达到以前的水准&#xff0c;需要一定…

分布式 - 消息队列Kafka:Kafka分区常见问题总结

文章目录 01. Kafka 的分区是什么&#xff1f;02. Kafka 为什么需要分区&#xff1f;03. Kafka 分区有什么作用&#xff1f;03. Kafka 为什么使用分区的概念而不是直接使用多个主题呢&#xff1f;04. Kafka 分区的数量有什么限制&#xff1f;05. Kafka 分区的副本有什么作用&am…

[内网渗透]XXE-vulnhub

文章目录 [内网渗透]XXE-vulnhub环境安装信息收集解题步骤总结 [内网渗透]XXE-vulnhub 环境安装 首先在 vulnhub 下载文件&#xff1a; 将其解压为ovf格式&#xff1a; 使用vmware打开&#xff0c;新建一个虚拟机&#xff0c;然后开机&#xff0c;环境就搭好了 信息收集 由于…

【仿写spring】一、通过反射读取带有@RequestMapping与@Controller注解的类并模拟请求路径调用方法

目录 简介思路实践一、自定义注解RequestMapping&#xff0c;Controller二、路径转全限定名方法三、扫描文件夹四、通过反射来寻找有RequestMapping以及Controller的类五、获取对象实例六、通过invoke调用方法 文件结构以及测试结果1、文件结构2、TestController3、测试结果 简…

uniapp 条件编译失败,跑不起来

因为这行代码整个uniapp都跑不起来&#xff0c;谁懂救命。再说uniapp的异常提示也太反人类了<!-- <image :src"require(/ baseListItem.url)" /> -->

Linux进程控制(三)---进程替换+简易shell的实现

目录 execl() execv() execlp() execvp() 如何利用execl执行自己写的C/C可执行程序&#xff1f; 如何利用makefile同时编译两个文件 execle() execvpe() 简单shell的编写 什么是进程替换&#xff1f; 我们之前fork之后&#xff0c;是父子进程各自执行代码的一部分&am…

【人工智能】大模型平台新贵——文心千帆

个人主页&#xff1a;【&#x1f60a;个人主页】 &#x1f31e;热爱编程&#xff0c;热爱生活&#x1f31e; 文章目录 前言大模型平台文心千帆发布会推理能力模型微调 作用 前言 在不久的之前我们曾讨论过在ChatGPT爆火的大环境下&#xff0c;百度推出的“中国版ChatGPT”—文…

深度神经网络基础——深度学习神经网络基础 Tensorflow在深度学习的应用

目录 一、二、Tesnsflow入门 & 环境配置 & 认识Tensorflow三、线程与队列与IO操作神经网络基础知识1.简单神经网络2.卷积神经网络卷积层新的激活函数-Relu池化层(Pooling)计算 案例&#xff1a;Mnist手写数字图片识别卷积网络案例 一、二、Tesnsflow入门 & 环境配置…

IDEA无法链接代理The driver has not received any packets from the server.

问题如下: 1、本地部署Proxifier,且设置全局代理,截图如下 代理工具 2、通过Navicat 工具连接该远程数据库,连接正常,截图如下 3、通过IDEA或者Eclipse连接(通过代理),抛连接失败(数据库地址绝对无误),如果把数据库地址改成本地的mysql地址,可以正常连接(不用通过代理)…

mac使用mvn下载node-sass 会Binary download failed, trying source

m1 上使用nvm 以下node的版本可以直接下载&#xff08;Binary download&#xff0c;而不是 trying source&#xff09;而不用切换mac cpu架构 zhiwenwenzhiwenwendeMBP cockpit % nvm install 14.15.5 Downloading and installing node v14.15.5... Downloading https://node…

二十五章:TransUNet:Transformer为医学图像分割提供强大的编码器

0.摘要 医学图像分割是发展医疗系统的重要先决条件&#xff0c;特别是对于疾病诊断和治疗计划。在各种医学图像分割任务中&#xff0c;U型架构&#xff0c;也称为U-Net&#xff0c;已成为事实上的标准&#xff0c;并取得了巨大的成功。然而&#xff0c;由于卷积操作的固有局部性…

Zabbix监控linux主机(agent端)

目录 一、Linux-clinet操作&#xff08;agent&#xff09; 二、源码安装zabbix 三、Zabbix添加linux主机 为agent.zabbix.com添加模板 等待一会 查看效果如下 一、Linux-clinet操作&#xff08;agent&#xff09; [rootlocalhost ~]# ifconfig ens33[rootlocalhost ~]# vim…

免费的游戏图标素材库分享

游戏图标设计在游戏UI中占有非常重要的地位。例如&#xff0c;当我们看到一个游戏的启动图标时&#xff0c;很容易区分它是哪个游戏。设计游戏图标不仅是一个图形&#xff0c;也是一个标志。 本文将通过各种游戏图标设计素材分享游戏图标的类别和设计游戏图标的思考。 1. 游戏…

程序员基础知识—IP地址

文章目录 一、什么是IP地址二、IP地址的分类三、子网掩码 一、什么是IP地址 IP地址就像我们需要打电话时的电话号码一样&#xff0c;它用来标识网络中的一台主机&#xff0c;每台主机至少有一个IP地址&#xff0c;而且这个IP地址是全网唯一的。IP地址由网路号和主机号两部分组…

vue 3.0 如何加载图片

.logo { background: url(~/assets/images/logo.svg) no-repeat center center/contain; width: 117px; height: 24px; margin: 0 20px; } <a class"logo" href"#"></a> 比较实用的书写方式

小程序制作教程

步骤一&#xff1a;规划和设计 在开始制作微信小程序之前&#xff0c;首先需要规划和设计您的小程序。确定您想要提供的服务或功能&#xff0c;并考虑用户体验和界面设计。绘制草图和构思完整的页面布局&#xff0c;这将使您更好地理解小程序结构和功能。 步骤二&#xff1a;…

Arrays.asList

文章目录 摘要详解我们再去看看 java.util.ArrayList 为什么可变的呢&#xff1f;Arrays.asList()和 Collections.singletonList()额外&#xff1a;Collections.singletonList() 摘要 先总结要点&#xff0c;接下来详细讲解 返回由指定数组支持的长度不可变的列表&#xff0c…

题目3 文件包含(保姆级教程)

url&#xff1a;http://192.168.154.253:83 #打开http://XXX:81/&#xff0c;XXX为靶机的ip地址 审题 1、打开题目看到有一个提示&#xff0c;此题目需要通过利用存在的文件包含漏洞&#xff0c;尝试获取webshell&#xff0c;最后从根目录下key.php文件中获得flag 2、开始答题…

老年公寓人员定位管理系统:提升安全与关怀的智能解决方案

老年公寓作为提供安全居住环境和关怀服务的重要场所&#xff0c;面临着人员管理和安全控制的挑战。为了解决这些问题&#xff0c;老年公寓人员定位管理系统应运而生。基于为提供全面的安全管理和个性化关怀服务&#xff0c;华安联大便通过老年公寓人员定位管理系统的技术原理、…

在react中配置less

第一步&#xff1a;暴露出webpack配置文件 终端命令&#xff1a;npm run eject (此命令一旦运行不可逆) 第二步&#xff1a;安装less以及less-loader npm install less less-loader --save-dev 第三步&#xff1a;修改webpack的配置文件 运行完以上命令后&#xff0c;项目…