【初阶数据结构与算法】复杂度分析练习之轮转数组(多种方法)

在这里插入图片描述

文章目录

  • 复杂度练习之轮转数组
    • 方法1
    • 方法2
    • 方法3
  • 总结

复杂度练习之轮转数组

题目链接:https://leetcode.cn/problems/rotate-array/description/
   为什么我们把这道题作为复杂度的练习题呢?是因为我们以后做题都会涉及到复杂度的计算,我们要保证我们写的程序不会超出题目的时间和空间的要求
   这道题就可以给我们一些启发,比如这道题有的方法复杂度过高,超出了题目要求的时间或者是空间限制,我们就需要计算我们方法的时间和空间复杂度找出问题,然后及时的换另一种方法,不死磕一个方法

方法1

   我们先来看看题目的介绍以及其中的第一个示例:
在这里插入图片描述
   题目的大致意思就是对数组进行右轮转操作,每轮转一次就会把最右边的元素放到最左边去,把其它所有元素向后移动一位,然后将这样的操作进行k次,这样一解释我们的第一个思路就出来了
   就是将数组中最后一个元素存起来,然后把数组中除了最后一个元素外的所有元素往后移动一位,然后我们把最后一个元素再放到数组的开头,也就是下标为0的位置,如图:
在这里插入图片描述
   上图演示的就是轮转一次的过程,我们只需要将这个过程循环k次即可,知道思路过后我们就来实现这个代码,首先每次循环我们需要将最后一个元素保存,这里就定义一个整型变量end来存储
   关键就是我们要将除了最后一个元素的所有元素都向后移动一位,我们可以使用memmove函数,但是我们为了方便观察它的时间和空间复杂度,就手写一下
   由于从第一个元素直接往后移动会导致第二个元素被覆盖,所以我们采用的方法是从最后一个要移动的元素开始移动,然后移动倒数第二个需要移动的元素,如下图:
在这里插入图片描述
   为了实现上图的效果,我们使用一个for循环,将i定位到下标为数组元素大小-2的位置,在上面例子中,数组元素大小为7,减2后就是5,也就是我们要移动的最后一个元素
   在第一次循环中,i = 5,我们要将下标为5的元素放在下标为6的位置上,然后我们让i减1,在第二次循环中,i = 4,我们就要将下标为4的元素放在下标为5的位置上,然后i再减1,最后循环这样的操作,直到i不再是有效的下标,代码如下:

 for (int i = numsSize - 2; i >= 0; i--)
 {
     nums[i + 1] = nums[i];
 }

   循环结束之后就说明我们的数组需要移动的元素已经移动完毕了,最后一步就是将我们保存的end放入下标为0的位置,到这里我们的代码就写完了,如下:

void rotate(int* nums, int numsSize, int k)
{
    while (k--)
    {
        int end = nums[numsSize - 1];
        for (int i = numsSize - 2; i >= 0; i--)
        {
            nums[i + 1] = nums[i];
        }
        nums[0] = end;
    }
}

   我们来运行自测一下看看代码有没有问题:
在这里插入图片描述
   可以看到自测通过了,我们提交代码试试:
在这里插入图片描述
   可以看到居然失败了,平台提示我们代码的运行时间超出了限制,这是为什么呢?我们来计算一下我们这种方法的时间复杂度
   我们这种方法有两层循环,其中外层while循环的执行次数属于未知数,会运行k次,k可以非常大,内层的for循环的执行次数要看数组的元素个数,也属于未知数,会执行n次,所以最后总结一下,整个代码的时间复杂度是O( N^2 )的级别,这个时间复杂度是非常糟糕的,所以导致了我们的代码超出了时间限制
   在计算时间复杂度之后,我们顺便来计算一下这个代码的空间复杂度,可以看到我们创建的变量个数都是有限个,所以这个代码的空间复杂度为O(1)
   通过分析方法1的时间复杂度,我们发现它的时间复杂度达到了O( N^2 ),导致代码超出了时间限制,所以方法1并不可行,接着让我们来学习其它方法

方法2

   方法1没有通过检测的原因就是时间复杂度太高了,那么我们能不能将复杂度降到O(N)呢?说不定那时就可以通过
   在上面的方法1中我们每轮转一次就要将数组中的元素一个一个往后挪动,导致时间复杂度超出限制,于是在方法2中我们对它进行一个小小的优化,我们可以创建一个新的数组来一次性存放要移动的数据
   比如我们要轮转k次,那么就要移动k个元素,我们可以把原数组中后k个元素移动到新数组的前k个位置上,把前n-k个元素移动到新数组中的后n-k的位置上,它们看起来是两步,但是实际上我们可以通过一个很巧妙的方法将这两步一起实现了,如下:

for(int i = 0; i < numsSize; i++)
{
   newarr[(i+k)%numsSize] = nums[i];
}

是不是有点难懂,我们来结合画图来解释一下:
在这里插入图片描述
   当i = 0时,nums(i+k)%numsSize = 3,刚好是新数组中的后n-k个元素中的第一个元素,所以当i = 0时,就把原数组中前k个元素中的第一个元素,放入了新数组中的后n-k个元素中的第一个元素的位置,如图:
在这里插入图片描述
   所以循环往复n-k次都是慢慢把原数组中前n-k个元素,移动到新数组中后n-k个位置上,如图:
在这里插入图片描述
   当i = 4时,nums(i+k)%numsSize = 0,刚好就是把原数组后面的元素放到了新数组的最前面,循环往复k次过后,就成功地把原数组中后k个元素移动到了新数组中前k个位置上,如图:
在这里插入图片描述
   这样就成功实现了将原数组中的元素轮转k次后,存放在新数组中,但是由于判题的时候是根据原数组nums来判断的,所以我们还要写一个循环将新数组中的数据拷贝回原数组中,如下:

for(int i = 0; i<numsSize; i++)
{
      nums[i] = newarr[i];
}

   完整代码为:

void rotate(int* nums, int numsSize, int k)
{
   int newarr[numsSize];
   
   for(int i = 0; i<numsSize; i++)
   {
      newarr[(i+k)%numsSize] = nums[i];
   }
   for(int i = 0; i<numsSize; i++)
   {
      nums[i] = newarr[i];
   }
}

   随后我们运行一下代码发现没有问题,然后我们提交一下,如图:
在这里插入图片描述

   可以看到代码成功通过了,接着我们来分析一下这段代码的时间复杂度和空间复杂度,我们使用了两个for循环,但是是并列而不是嵌套的关系,所以时间复杂度为O(N),比上一个方法优化了很多
   接着我们来看空间复杂度,我们创建了一个和原数组相同大小的数组,所以空间复杂度就是O(N),那么我们有没有什么方法再优化一下,使得时间复杂度保持O(N),但是空间复杂度降到O(1)呢?这就是我们要介绍的方法3

方法3

   方法3也不墨迹了,我们直接给出方法,因为不太能想到,我们以前没见过没关系,只要以后会做就可以了,这个方法有三步,首先逆置前n-k个元素,然后逆置后k个元素,最后将整个数组整体逆置一次,如图:
在这里插入图片描述
   所以我们只需要写出一个实现逆置的函数,然后调用三次就可以了,关键在于如何逆置,其实很简单,就是将要逆置的元素中前面的元素和后面的元素交换位置,如图:
在这里插入图片描述
   接着我们就来写这个逆置方法,函数名就叫reserve,我们需要的参数就是数组nums,以及begin和end,begin和end就是我们要逆置的元素的开头和结尾的下标
   实现方法就是上图画出的样子,让begin和end位置的数据交换,然后begin往后走,end往前走,继续交换,直到begin>end,所以我们写一个循环,循环条件就是begin<end,那么begin会不会和end相等呢?
   如果我们要逆置的元素的个数为奇数,是有可能相等的,相等的时候它们指向同一个元素,自然不需要做交换,如下图:
在这里插入图片描述

   现在知道了原理,实现这个函数就非常简单了,如下:

void reserve(int* nums, int begin, int end)
{
    while( begin < end )
    {
        int tmp = nums[begin];
        nums[begin] = nums[end];
        nums[end] = tmp;
        begin++, end--;
    }
}

   实现完这个代码之后我们就可以使用了,先让前n-k个元素逆置,如下:

    reserve(nums, 0, numsSize-k-1);

   接着让后k个元素逆置,如下:

    reserve(nums, numsSize-k,numsSize-1);

   最后使得整个数组全部逆置,如下:

    reserve(nums, 0, numsSize-1);

   最后我们还要注意一点,就是我们的numsSize-k-1可能会没有意义,就是当这个数组被轮转的次数超过它本身的大小的时候,例如数组只有一个元素,但是要轮转2次,numsSize-k-1就是-2,没有意义了
   怎么解决呢?主要是我们要理解,如果轮转数组大小次,那么相当于没有轮转,还是以前的样子,所以我们可以使用下面的方法来避免数组轮转多个循环:

k = k % numsSize; 

   这样我们就可以保证数组不会多次循环轮转,比如元素大小是7个,轮转8次,那么前7次轮转会使得数组恢复原样,没有意义,最后轮转的1次才有意义,所以我们模上数组元素个数,使得轮转次数直接变成1,就让程序高效了
   同时也解决了numsSize-k-1没有意义的问题,如果数组只有一个元素,轮转两次,那么就让2模1,得到了0,numsSize-k-1=0,就可以使得上面我们的代码成立
接下来我们来看看完整题解:

void reserve(int* nums, int begin, int end)
{
    while( begin < end )
    {
        int tmp = nums[begin];
        nums[begin] = nums[end];
        nums[end] = tmp;
        begin++, end--;
    }
}

void rotate(int* nums, int numsSize, int k) 
{
    k %= numsSize; 
    reserve(nums, 0, numsSize-k-1);
    reserve(nums, numsSize-k,numsSize-1);
    reserve(nums, 0, numsSize-1);
}

   最后我们来提交一下代码:
在这里插入图片描述
   可以看到成功通过了,我们来计算一下这个代码的时间复杂度和空间复杂度,由于我们的循环是同级关系,所以时间复杂度为O(N),由于我们轮转是对原数组直接轮转的,所以没有开辟新的数组,空间复杂度为O(1)

总结

最后我们来总结一下我们上面写的三个方法

  1. 在方法1中,我们使用了两层循环嵌套,每轮转一次就要把n - 1个数据往后挪动一次,虽然空间复杂度为O(1),但是时间复杂度到达了O(N^2),不是很好,导致了最后超出了题目的时间限制
  2. 在方法2中,我们创建了一个和原数组相同大小的新数组,然后把轮转后的数据存放进这个新数组,关键就是要理解newarr[(i+k)%numsSize] = nums[i]这条语句,最后再将轮转完后的新数组的数据重新放回原数组,它的时间复杂度为O(N),空间复杂度为O(N),已经可以通过题目,但是它的空间复杂度还是稍微有点高
  3. 在最后的方法3中,我们实现了一个函数可以逆置数组中的元素,我们只需要调用这个函数三次就可以实现数组的轮转,第一次逆置前n-k个元素,第二次逆置后k个元素,最后将整个数组逆置就可以使得数组轮转k次,我们要注意的是要理解为什么要使用k%=numsSize这条语句
  4. 最后的思考环节,这里我们的轮转是向右轮转,如果是向左轮转该怎么做呢?向左轮转和向右轮转的关系是什么呢?欢迎在评论区讨论

   那么今天的分享就到此结束啦,我们虽然整篇文章只讲了一道题,但是我们更加深入地理解了时间复杂度和空间复杂度的重要性,这是我们以后写程序需要时刻注意的
   如果有什么问题欢迎私信我,bye~

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

哲学家就餐问题(Java实现信号量和PV操作)

哲学家就餐是经典的PV操作。 一个哲学家同时拿起左边的筷子和右边的筷子进行就餐&#xff0c;每一个哲学家都会等待右边的筷子&#xff0c;具备了死锁问题之一的循环等待。 基础的哲学家就餐问题代码 在Java中&#xff0c;Semaphore 是一个用于控制对某个资源的访问的同步工具…

mutable用法

mutable 关键字用于允许类的某个成员变量在 const 成员函数中被修改。通常&#xff0c;const 成员函数不能改变对象的任何成员变量&#xff0c;但将成员变量声明为 mutable 可以例外 class Hero { public:Hero():m_Hp(0), m_getHpCounter(0){}int getHp() const {m_getHpCounte…

C++ | Leetcode C++题解之第537题复数乘法

题目&#xff1a; 题解&#xff1a; class Solution { public:string complexNumberMultiply(string num1, string num2) {regex re("\\|i"); vector<string> complex1(sregex_token_iterator(num1.begin(), num1.end(), re, -1), std::sregex_token_iterator…

告别传统营销,HubSpot AI分析工具带你玩转新潮流

你们知道吗&#xff1f;现在的人工智能&#xff08;AI&#xff09;技术可是越来越厉害了&#xff0c;它简直就是我们营销人员的超级外挂&#xff01;有了AI分析工具&#xff0c;我们不仅能优化营销效果&#xff0c;还能大大提升工作效率。那么&#xff0c;具体是怎么一回事呢&a…

Docker打包自己项目推到Docker hub仓库(windows10)

一、启用Hyper-V和容器特性 1.应用和功能 2.点击程序和功能 3.启用或关闭Windows功能 4.开启Hyper-V 和 容器特性 记得重启生效&#xff01;&#xff01;&#xff01; 二、安装WSL2&#xff1a;写文章-CSDN创作中心https://mp.csdn.net/mp_blog/creation/editor/143057041 三…

如何删除react项目的默认图标,使在浏览器中不显示默认图标favicon.ico

要删除 React 项目的默认图标&#xff0c;使在浏览器中不显示默认图标favicon.ico&#xff0c;其实有两种方法&#xff1a; 方法一 方法要点&#xff1a;删除掉 public 目录下的 favicon.ico 文件&#xff0c;再用浏览器访问时&#xff0c;如果加载不到图标文件&#xff0c;就…

软件测试:测试用例详解

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 一、通用测试用例八要素   1、用例编号&#xff1b;    2、测试项目&#xff1b;   3、测试标题&#xff1b; 4、重要级别&#xff1b;    5、预置…

支付幂等性的实现中,通过“一锁、二判、三更新”

在这个支付幂等性的实现中&#xff0c;通过“一锁、二判、三更新”严格控制了支付链接生成接口的幂等性&#xff0c;确保同一业务单号在同一时间只会生成一个有效的支付链接&#xff0c;避免重复支付或其他意外操作。 Facade DistributeLock(keyExpression "#payCreate…

Charles抓包_Android

1.下载地址 2.破解方法 3.安卓调试办法 查看官方文档&#xff0c;Android N之后抓包要声明App可用User目录下的CA证书 3.1.在Proxy下进行以下设置&#xff08;路径Proxy->Proxy Settings&#xff09; 3.1.1.不抓包Windows&#xff0c;即不勾选此项&#xff0c;免得打输出不…

Chromium Mojo(IPC)进程通信演示 c++(3)

122版本自带的mojom通信例子channel-associated-interface 仅供学习参考&#xff1a; codelabs\mojo_examples\03-channel-associated-interface-freezing 其余定义参考上一篇文章&#xff1a; Chromium Mojo(IPC)进程通信演示 c&#xff08;2&#xff09;-CSDN博客​​​​…

鸢尾博客项目开源

1.博客介绍 鸢尾博客是一个基于Spring BootVue3 TypeScript ViteJavaFx的客户端和服务器端的博客系统。项目采用前端与后端分离&#xff0c;支持移动端自适应&#xff0c;配有完备的前台和后台管理功能。后端使用Sa-Token进行权限管理,支持动态菜单权限&#xff0c;服务健康…

【模型学习之路】手写+分析bert

手写分析bert 目录 前言 架构 embeddings Bertmodel 预训练任务 MLM NSP Bert 后话 netron可视化 code2flow可视化 fine tuning 前言 Attention is all you need! 读本文前&#xff0c;建议至少看懂【模型学习之路】手写分析Transformer-CSDN博客。 毕竟Bert是tr…

word及Excel常见功能使用

最近一直在整理需规文档及表格&#xff0c;Word及Excel需要熟练使用。 Word文档 清除复制过来的样式 当复制文字时&#xff0c;一般会带着字体样式&#xff0c;此时可选中该文字 并使用 ctrlshiftN 快捷键进行清除。 批注 插入->批注&#xff0c;选中文本 点击“批注”…

【C++篇】数据之林:解读二叉搜索树的优雅结构与运算哲学

文章目录 二叉搜索树详解&#xff1a;基础与基本操作前言第一章&#xff1a;二叉搜索树的概念1.1 二叉搜索树的定义1.1.1 为什么使用二叉搜索树&#xff1f; 第二章&#xff1a;二叉搜索树的性能分析2.1 最佳与最差情况2.1.1 最佳情况2.1.2 最差情况 2.2 平衡树的优势 第三章&a…

【Mac】安装 VMware Fusion Pro

VMware Fusion Pro 软件已经正式免费提供给个人用户使用&#xff01; 1、下载 【官网】 下拉找到 VMware Fusion Pro Download 登陆账号 如果没有账号&#xff0c;点击右上角 LOGIN &#xff0c;选择 REGISTER 注册信息除了邮箱外可随意填写 登陆时&#xff0c;Username为…

文心一言 VS 讯飞星火 VS chatgpt (383)-- 算法导论24.5 3题

三、对引理 24.10 的证明进行改善&#xff0c;使其可以处理最短路径权重为 ∞ ∞ ∞ 和 − ∞ -∞ −∞ 的情况。引理 24.10(三角不等式)的内容是&#xff1a;设 G ( V , E ) G(V,E) G(V,E) 为一个带权重的有向图&#xff0c;其权重函数由 w : E → R w:E→R w:E→R 给出&…

Linux 服务器使用指南:从入门到登录

&#x1f31f;快来参与讨论&#x1f4ac;&#xff0c;点赞&#x1f44d;、收藏⭐、分享&#x1f4e4;&#xff0c;共创活力社区。 &#x1f31f; &#x1f6a9;博主致力于用通俗易懂且不失专业性的文字&#xff0c;讲解计算机领域那些看似枯燥的知识点&#x1f6a9; 目录 一…

【Maven】——基础入门,插件安装、配置和简单使用,Maven如何设置国内源

阿华代码&#xff0c;不是逆风&#xff0c;就是我疯 你们的点赞收藏是我前进最大的动力&#xff01;&#xff01; 希望本文内容能够帮助到你&#xff01;&#xff01; 目录 引入&#xff1a; 一&#xff1a;Maven插件的安装 1&#xff1a;环境准备 2&#xff1a;创建项目 二…

数据库基础(2) . 安装MySQL

0.增加右键菜单选项 添加 管理员cmd 到鼠标右键 运行 reg文件 在注册表中添加信息 这样在右键菜单中就有以管理员身份打开命令行的选项了 1.获取安装程序 网址: https://dev.mysql.com/downloads/mysql/ 到官网下载MySQL8 的zip包, 然后解压 下载后的包为: mysql-8.0.16-…

cocos开发QA

目录 TS相关foreach循环中使用return循环延迟动态获取类属性 Cocos相关属性检查器添加Enum属性使用Enum报错 枚举“XXX”用于其声明前实现不规则点击区域使用cc.RevoluteJoint的enable激活组件无效本地存储以及相关问题JSON.stringify(map)返回{}数据加密客户端复制文本使用客户…