{(leetcode 题号:169. 多数元素)+(189. 轮转数组)}时间复杂度与空间复杂度分析:

news2024/9/24 5:25:38

目录

一. 基本概念

1.时间复杂度

2.空间复杂度

二.实例分析

实例(1):旋转数组

方法1:暴力旋转法(时间复杂度加空间复杂度分析)

方法2 :三步整体逆序法 (时间复杂度加空间复杂度分析)

实例(2):斐波那契递归的时间复杂度和空间复杂度分析

           实例(3):169. 多数元素

方法1:随机法(时间复杂度加空间复杂度分析)

方法2:分治递归

方法3:投票算法(个人更喜欢称为擂台算法)


一. 基本概念

1.时间复杂度

(1)基本概念:

算法的时间复杂度的表达式是一个以问题规模N为自变量的函数。(问题规模一般与函数形参接收的值有关,可以是某个形参的值,字符串的长度,递归的阶数等等)

(2)计算方式

  • 求算法的时间复杂度第一步:先求出问题规模为N情形下算法中基本语句的执行次数f(N).(基本语句可以是循环语句,分支语句等等)
  • 对于递归函数我们还需计算出问题规模为N情形下,函数被调用的次数g(N);
  • 求算法的时间复杂度第二步:将表达式f(N)*g(N)化为大O的渐进表示法(对于非递归函数g(N)为1)

大O的渐进表示法:大O符号(Big O notation):是用于描述函数渐进行为的数学符号。

推导大O阶方法:

  1. 用常数1取代运行时间中的所有加法常数
  2. 在修改后的运行次数函数中,只保留最高阶项
  3. 如果最高阶项存在且不是1,则去除与这个项目相乘的常数,得到的结果就是大O阶。

另外有些算法的时间复杂度存在最好、平均和最坏情况
我们只关注算法的最坏运行情况
 

2.空间复杂度

(1)基本概念:

  • 空间复杂度也是以问题规模N为自变量的函数,是对一个算法在运行过程中临时占用存储空间大小的量度。

(2)计算方式

  • 计算空间复杂度的第一步:

计算函数在执行时函数栈帧上同一时刻存在的变量的最大个数f(N),对于递归算法,还要需要计算递归过程中函数同一时刻开辟的函数栈帧的最大个数g(N)

  • 计算空间复杂度的第二步:

将g(N)*f(N)化为大O的渐进表示法(非递归函数g(N)=1);

表达式g(N)*f(N)化为大O的渐进表示法的规则与时间复杂度相应的规则完全一样。

二.实例分析

实例(1):旋转数组

189. 轮转数组 - 力扣(Leetcode)

  •  问题描述:

给你一个数组,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

示例 :

输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]

示例 :

输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释: 
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]

题解接口定义(C++):

class Solution 
{
public:
    void rotate(vector<int>& nums, int k) 
    {

    }
};

方法1:暴力旋转法(时间复杂度加空间复杂度分析)

暴力旋转法的动图解析:

(暴力法无法通过leetcode的最后一个测试用例)

思路就是逐个字符地完成旋转:

class Solution 
{
public:

    void rotate(vector<int>& nums, int k) 
    {
        int sz = nums.size();
        int n = k%sz;
        while(n--)
        {
            int i=sz-1;
            int tem = nums[sz-1];
            for(i=sz-1;i>0;i--)
            {
                nums[i]=nums[i-1];
            }
            nums[0]=tem;
        }
    }
};
  • 算法的时间复杂度分析:

由于每完成一个数字的旋转就要挪动N个数字因此该算法for循环语句执行的次数为k*N

(k为要旋转的数字个数,N为数组的元素个数)(k取模后k<=N-1)

本例中k和N都是影响程序运行时间的变量:大O阶表达式中可以存在多个变量

算法的时间复杂度: O(N*k)

  • 算法的空间复杂度分析:

算法中创建的临时变量个数为常数个;

算法的空间复杂度为: O(1)

暴力法无法通过leetcode的最后一个测试用例:

方法2 :三步整体逆序法 (时间复杂度加空间复杂度分析)

三步整体逆序法的思路:

class Solution 
{
public:
    void  Reverse (int start,int end,vector<int>& nums)
    {
        while(start < end)
        {
            int tem = nums[start];
            nums[start]= nums[end];
            nums[end]=tem;
            start++;
            end--;
        }
    }

    void rotate(vector<int>& nums, int k) 
    {
        int sz = nums.size();
        k%=sz;
        Reverse(0,sz-1,nums);
        Reverse(0,k-1,nums);
        Reverse(k,sz-1,nums);
    }
};
  • 算法的时间复杂度分析:(N为数组元素个数,k为要逆序元素个数)
  1. 第一步整体逆序,在逆序函数Reverse中循环语句总共要执行N/2次
  2. 第二步Reverse(0,k-1,nums),函数中循环语句总共执行k/2次
  3. 第三步Reverse(k,sz-1,nums),函数中循环语句总共执行(N-k)/2次

因此算法的时间复杂度为O(N);

  • 算法空间复杂度分析:(N为数组元素个数,k为要逆序元素个数)

roate函数中创建的变量个数为常数个,Reverse函数中创建的变量也为常数个

因此算法的空间复杂度为O(1);

  • 可见这是一个非常高效的算法。

实例(2):斐波那契递归的时间复杂度和空间复杂度分析

int Fib(int n)
{
	if (n > 2)
	{
		return Fib(n - 1) + Fib(n - 2);
	}
	return (2 == n)? 2 : 1;
}
  • 时间复杂度分析:

以n=5为例分析递归树:

然而求上述递推公式的通项式太过麻烦(要用到待定系数构造法),我们选择采用一种更简单的分析法:先将二叉树补全。

可以看到各层之间的函数调用次数成等比数列。 

利用等比数列求和公式,可以求得Fib(N)的斐波那契递归函数调用总次数为:

 

当N足够大时,补上节点个数X相比于指数项可以忽略不计,因此根据大O阶的渐进表示法:

斐波那契递归的时间复杂度为:O(2^N) 

  • 空间复杂度分析:

4号Fib(2)压栈后会立马出栈:

因此分析可知:图中的四块栈帧空间会被各次函数调用重复利用,因此斐波那契递归函数Fib(N)同一时刻开辟的最大函数栈帧的个数为N个;

斐波那契递归的空间复杂度为: O(N);

实例(3):169. 多数元素

169. 多数元素 - 力扣(Leetcode)

  • 问题描述:

给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数大于n/2的元素。你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

输入:nums = [3,2,3]
输出:3

示例 2:

输入:nums = [2,2,1,1,1,2,2]
输出:2 

题解接口定义(C++):

class Solution
{
public:
    int majorityElement(vector<int>& nums)
    {

    }
};

 方法1:随机法(时间复杂度加空间复杂度分析)

由于我们要找的数组元素在数组中的存在个数占比超过百分之五十,因此我们可以考虑利用随机数生成器产生数组下标范围内的随机数随机定位一个数组元素判断其是否符合题目条件。

class Solution 
{
public:
    int majorityElement(vector<int>& nums) 
    {
        srand((unsigned int)time(nullptr));  用时间戳设定随机数种子
        int sz = nums.size();
        while(1)
        {
            int ans = (rand()%sz);           产生数组下标范围的随机整数
            int i=0;
            int count =0;
            for(i=0;i<sz;i++)                判断随机定位的元素是否符合题目条件
            {
                if(nums[i]==nums[ans])
                {
                    count++;
                }
            }
            if(count>sz/2)
            {
                return nums[ans];
            }
        }
    }
};

该解法的时间复杂度分析十分有意思。

  • 算法时间复杂度分析:
  1. 我们假设找到符合条件的数组元素总共要随机寻找X次
  2. 要求算法的时间复杂度,我们则要设法求出X的数学期望
  3. X是一个取值为整数1到正无穷的离散型随机变量
  4. 计算每个X的取值对应的概率,并得到X的分布列(为了方便计算分析我们假设多数元素恰好占数组元素个数的一半):

该分布列的数学期望为:

通过数列的错位相减法可以求出数列 i * (1/2)^i 的前X项和,再取X趋向正无穷的极限可以得到E(X)=2;

所以总体来说,大概只需随机取两次数组元素就可以取到多数元素,再经过两次遍历数组元素验证结果的正确性,该算法for语句执行次数的数学期望为2N(N为数组的元素个数)

因此算法的时间复杂度为:O(N)

  • 算法的空间复杂度:

函数中创建的变量个数为常数个.

算法的空间复杂度为:O(1)

方法2:分治递归

如果数 a 是数组 nums 的众数,如果我们将数组nums进行二等分,那么 a 必定是至少一部分的众数,因此我们就可以使用分治法解决这个问题:将数组二等分成左右两部分,分别求出左半部分的众数 a1 以及右半部分的众数 a2,随后在 a1 和 a2 中选出正确的众数。

数组的左半边和右半边的众数又可以用相同的方法去求,由此便形成递归直到所有的子问题都是长度为 1 的数组

class Solution 
{
public:
    int GetMulti(int left,int right , vector<int>&nums)  //封装一个递归函数
    {                                                    //递归函数的返回值代表是区间的众数
        int mid = (left+right)/2;                        //找出二分点
        int rightMulti=nums[left];
        int leftMulti=nums[right-1];
        if(right > left+1)                               //如果区间中的元素超过一个则继续二 
                                                         //分区间
        {
            rightMulti = GetMulti(mid,right,nums);       //求右半区间的众数
            leftMulti = GetMulti(left,mid,nums);         //求左半区间的众数
        }
        


        if(rightMulti==leftMulti)//左右半区间众数相同直接返回
        {
            return rightMulti;
        }
       

        int i=0;
        int count =0;
        for(i=left;i<right;i++)  //验证右半区间的众数是否为整个区间的众数(拿左边的判断也可以)                                                
        {
            if(rightMulti==nums[i])
            {
                count++;
            }
        }


        if(count>(right-left)/2) //判断若是众数则将其返回
        {
            return rightMulti;
        }
        else
        {
            return leftMulti;
        }
    }


    int majorityElement(vector<int>& nums) 
    {
        int sz = nums.size();
        return GetMulti(0,sz, nums);
    }
};

图解递归树:

为了方便作图,我们将递归函数名简化为G,由于引用形参一直为nums不变,所以图中省去递归函数的引用形参:以八个元素的数组为例,递归函数的初始传参为G(0,8)

  • 分治法在本题中合理性的分析:
  • 算法时间复杂度分析:

可以看到递归树的每一层中,各函数检查众数的for循环执行的次数总和都为N,而递归的深度(递归树的层数为logN(二分对数底数为2))

因此算法的时间复杂度为 : O(N*logN);

  •  算法的空间复杂度分析:

从上面的递归树易知:该递归函数执行的过程中,同一时刻同时开辟的函数栈帧最大个数为logN(底数为2),每个栈帧中的变量个数为常数个。

因此算法的空间空间复杂度为: O(logN)

方法3:投票算法(个人更喜欢称为擂台算法)

算法的核心思路:

如果我们把众数记为+1,把其他数记为−1,将它们全部加起来,显然和大于 0

  • 设定一个计数器count=0,创建一个变量用于存储假设的众数winner
  • 第一个元素先赋值给假设的众数winner,count置为1,然后开始遍历数组。
  • 遍历过程中,遇到和winner相等的元素,count++,否则count--
  • 若count减为0,winner则替换为当前遍历到的数组元素,并且count置为1(表示当前元素为假设的众数)
  • 根据算法的核心思路,winner最后存储的就是数组的众数
class Solution 
{
public:
    int majorityElement(vector<int>& nums) 
    {
        int sz = nums.size();
        int winner =nums[0];   //先假定第一个元素为众数
        int count =1;          //count 置为1
        int i=0;
        for(i=1;i<sz;i++)        
        {
            if(nums[i]==winner)
            {
                count++;
            }
            else
            {
                count --;
            }

            if(0==count)       //count减为0后winner则替换
            {
                winner = nums[i];
                count=1;
            }
        }
        return winner;
    }
};

算法动画解析:

  • 算法的时间复杂度:O(n);
  • 算法的空间复杂度:  O(1) ;

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

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

相关文章

模拟实现C库函数(1)

"啊~所有经历给它赋予魔力。"很久没更新过C专栏的文章了&#xff0c;借复习(review)的机会&#xff0c;本节的内容针对我们一些常见、常用的C库函数的模拟实现。“当你行走了一段时间后&#xff0c;回头往往那不管是起初咿咿呀呀胡乱踩陷的小坑时&#xff0c;还是之后…

C++11并发指南三(stdmutex详解)

C11并发指南三&#xff08;std:mutex详解&#xff09; 文章目录C11并发指南三&#xff08;std:mutex详解&#xff09;<mutex> 头文件介绍Mutex 系列类(四种)Lock 类&#xff08;两种&#xff09;其他类型函数std::mutex 介绍std::mutex 的成员函数std::recursive_mutex 介…

miracl

文章目录Windows平台编译网址 https://miracl.com/https://github.com/miracl/MIRACL Windows平台编译 源码目录下新建文件夹ms32或ms64&#xff0c;把/lib/ms32doit.bat或ms64doit.bat分别拷进去。 把源码include和source目录所有文件拷贝进要编译的ms32或ms64&#xff0c…

32. 实战:PyQuery实现抓取TX图文新闻

目录 前言 &#xff08;链接在评论区&#xff09;&#xff08;链接在评论区&#xff09;&#xff08;链接在评论区&#xff09; 目的 &#xff08;链接在评论区&#xff09;&#xff08;链接在评论区&#xff09;&#xff08;链接在评论区&…

ATAC-seq分析:Motifs分析(11)

1. 切割位点 ATACseq 应该在较小的保护区&#xff08;如转录因子结合位点&#xff09;周围生成较短的片段&#xff08;我们的无核小体区域&#xff09;。 因此&#xff0c;我们可以在不同组织/细胞类型/样本中寻找围绕感兴趣基序的切割位点堆积。 为了从我们的 BAM 文件中生成切…

FecMall多语言商城宝塔安装搭建教程

FecMall多语言商城宝塔安装搭建教程 1.1、删除禁用函数 PHP管理→禁用函数&#xff0c;删除putenv、pcntl_signal函数 如果不删除会报错&#xff1a;[ErrorException] pcntl_signal() has been disabled for security reasons 1.2下载fecmall 进入如下目录中cd /www/wwwroot 下…

行为型模式-中介模式

1.概述 一般来说&#xff0c;同事类之间的关系是比较复杂的&#xff0c;多个同事类之间互相关联时&#xff0c;他们之间的关系会呈现为复杂的网状结构&#xff0c;这是一种过度耦合的架构&#xff0c;即不利于类的复用&#xff0c;也不稳定。例如在下左图中&#xff0c;有六个…

LeetCode 2325. 解密消息

给你字符串 key 和 message &#xff0c;分别表示一个加密密钥和一段加密消息。解密 message 的步骤如下&#xff1a; 使用 key 中 26 个英文小写字母第一次出现的顺序作为替换表中的字母 顺序 。 将替换表与普通英文字母表对齐&#xff0c;形成对照表。 按照对照表 替换 mess…

〖产品思维训练白宝书 - 核心竞争力篇⑤〗- 产品经理核心竞争力解读之如何培养创造力

大家好&#xff0c;我是 哈士奇 &#xff0c;一位工作了十年的"技术混子"&#xff0c; 致力于为开发者赋能的UP主, 目前正在运营着 TFS_CLUB社区。 &#x1f4ac; 人生格言&#xff1a;优于别人,并不高贵,真正的高贵应该是优于过去的自己。&#x1f4ac; &#x1f4e…

【从零开始】力扣刷题(1)

文章目录前言数组&#xff1a;数组的遍历485.最大连续的一个数495.提莫攻击414.第三大的数628.三个数的最大乘积数组&#xff1a;统计数组中的元素645.错误的集合697.数组的度484.找到所有数组中消失的数组442.数组中重复的数据41.缺失的第一个正数274.H指数前言 我根据这里的…

Scalable SoftGroup for 3D Instance Segmentation on Point Clouds

Abstract 本文考虑了一个称为SoftGroup的网络&#xff0c;用于准确和可扩展的3D实例分割。现有的最先进方法会产生硬语义预测&#xff0c;然后进行分组以获得实例分割结果。然而&#xff0c;源于硬决策的错误会传播到分组中&#xff0c;导致预测实例与ground truth的低重叠和大…

数据结构:排序的基本概念

排序(sorting)是按关键字的非递减或非递增顺序对一组记录重新进行整队(或排列)的操作。确切描述如下: 假设含有 n 个记录的序列为 {r1 ,r2 , … ,rn} (3-1) 它们的关键字相应为 {k1 ,k2 , … ,kn} 对式(3-1)的记录序列进行排序就是要确定序号 1,2,,n 的一种排列 p1,p2 , … …

MyBatis 表连接查询写法|三种对应关系

❤️作者主页&#xff1a;微凉秋意 ✅作者简介&#xff1a;后端领域优质创作者&#x1f3c6;&#xff0c;CSDN内容合伙人&#x1f3c6;&#xff0c;阿里云专家博主&#x1f3c6; ✨精品专栏&#xff1a;C面向对象 &#x1f525;系列专栏&#xff1a;JavaWeb 文章目录前言表连接…

JS 6万字超详细总结

文章目录1. JS简介2. JS的使用2.1 行内式2.2 内嵌式2.3 外部式3. JS基础语法3.1 注释3.2 变量3.3 输入和输出3.4 数据类型3.4.1 字符串类型3.4.2 数字类型3.4.3 布尔类型3.4.4 Undefined和Null3.4.5 获取变量的类型3.5 数据类型转换3.5.1 转换为字符串类型3.5.2 转换为数字类型…

Java多线程(二)——ReentrantLock源码解析(补充4——条件变量Condition)

ReentrantLock源码解析&#xff08;补充4——条件变量Condition&#xff09; 上一章 ReentrantLock源码解析 仅介绍了 ReentrantLock 的常用方法以及公平锁、非公平锁的实现。这里对上一章做一些补充。主要是&#xff1a; AQS 中阻塞的线程被唤醒后的执行流程可打断的锁 lock…

CDH数仓项目(二) —— 用户行为数仓和业务数仓搭建

0 说明 本文基于《CDH数仓项目(一) —— CDH安装部署搭建详细流程》开始搭建数仓 1 数仓搭建环境准备 1.1 Flume安装部署 1&#xff09;添加服务 2) 选择Flume 3&#xff09;选择依赖 4)选择部署节点 5) 安装完成 1.2 安装Sqoop 1&#xff09;添加服务 2&#xff09;选…

Unity2023 Alpha新功能简介

Unity2023特征&#xff1a;Graphic&#xff1a;添加了新的光线跟踪加速结构。添加实例签名&#xff0c;允许将网格实例添加到GPU光线跟踪的加速结构中。从栅格化管道中渲染网格。HDRP&#xff1a;为HDRP添加了光线追踪地形支持。Eidtor&#xff1a;添加了“聚焦窗口改变”回调到…

spring AOP 原理

一、spring注册 AnnotationAwareAspectJAutoProxyCreator 通过EnableAspectJAutoProxy可以看到先把AspectJAutoProxyRegistrar通过Import注册到spring。 AspectJAutoProxyRegistrar实现了ImportBeanDefinitionRegistrar接口&#xff0c;所以就有了将某个bean引入spring 的能力…

#C. 笨友圈

题目思路1直接模拟题意,相当于邻接矩阵,用bool类型的二维数组vis[i][j]来存储i和j是否为好友,交叉点为1代表是好友,为0代表不是;a[i]存储i这个人看到的信息数量。然后输入后如果符号代表要将u,v加个好友,就将vis[u][v] 1和vis[v][u] 1,如果是拉黑就将vis[u][v] 0和vis[v][u]…

ATAC-seq分析:差异分析(10)

在下部分中&#xff0c;我们将研究如何使用 R/Bioconductor 识别开放区域中的变化。 在这里&#xff0c;我们将采用类似于 Diffbind 中的方法&#xff0c;并在 ATACseq 分析中合理建立。 1. 识别非冗余峰 首先&#xff0c;我们将定义至少 2 个样本中存在的一组非冗余峰&#xf…