【Hello Algorithm】复杂度 二分法

news2024/10/2 20:33:40

作者:@小萌新
专栏:@算法
作者简介:大二学生 希望能和大家一起进步
本篇博客简介:介绍算法的复杂度 对数器和二分法

复杂度 对数器 二分法

  • 复杂度
    • 常数时间操作
    • 非常数时间操作
    • 时间复杂度
    • 空间复杂度
  • 二分法
    • 有序数组中找一个值
    • 寻找有序数组中大于等于某个数的位置
    • 无序数组中寻找一个局部最小值

复杂度

常数时间操作

我们把固定时间的操作称为常数时间操作

比如说我们定义两个变量a b

int a;
int b;

在这两个变量进行加减乘除操作的时候它们进行的是固定时间的操作 我们就将它们称为常数时间操作

因为在计算机眼里a和b都是由32位的01组成的(在32位的机器上)

不管a和b是什么数字相加 在计算机眼里都是进行32位数字的相加 所以我们说这个操作是常数时间的

常见的常数时间操作有

  • 常见的算术运算
  • 常见的位运算
  • 赋值 比较 自增 自减操作等
  • 数组寻址

非常数时间操作

我们使用单链表进行查找的时候就是一个非常数时间操作

单链表在内存中的逻辑结构大概是这样子的

在这里插入图片描述

每个单独的数据通过指针连接起来

我们假设这个单链表中有一千万个数据 那么我们查找第20万个数据和第800万个数据的时间肯定是相差很大的 这就叫做非常数时间操作

时间复杂度

我们常说的时间复杂度都是使用大O的渐进表示法来表示的

我们使用选择排序的例子来讲解时间复杂度的概念 如果对于选择排序有疑问的同学可以参考我的这篇博客

选择排序

简单介绍下选择排序

给我们一个N个无序数字组成的数组

我们首先遍历0~N-1位置 找出一个最小的数字和0位置的数字交换位置

之后我们再次遍历1~N-1位置 找出一个最小的数字和1位置的数字交换位置

以此类推 我们就能得到一个有序的数组

我们上面的算法就叫做选择排序

我们来分析下上面操作的时间复杂度

第一次遍历了N个元素 找出最小数字比较了N-1次 和0位置交换了一次

第二次遍历了N-1个元素 找出最小数字比较了N-2次 和0位置交换了一次

以此类推

我们实际的操作次数为 2(N + N-1 + N-2 + N-3 + … + 1) 不难发现前面是一个等差数列

我们根据等差数列公式可以写成以下格式

a N 2 + b N + c aN^2 + bN + c aN2+bN+c

其中a b c都是常数 我们舍弃掉这些常数 再只保留最高阶项

我们就能得到该算法使用大O的渐进表示法的时间复杂度为N的平方

使用大O的渐进表示法的意义

最高阶项对于一个算法的影响是最大的 举个实际的例子

假设A算法的时间复杂度为N的三次方 系数为1

B算法的时间复杂度为N的平方 系数为10

当N为100时候 A算法大概要执行一百万次操作而B算法只需要执行十万次操作就可以了

这还是在N的数字很小的情况下 在我们实际的业务逻辑中 数字N可能是用万来做单位的

空间复杂度

在除了用户提供给我们的变量空间之外额外开辟的空间叫做空间复杂度 我们一般也是使用大O的渐进表示法来表示

用户的输入空间以及用户要求的输出空间都不算是我们额外开辟的空间

比如说上面的选择排序其C++完整代码如下

void selectionsort(int* arr, int size)
{
	// assert
	assert(arr);

	// 还是一样 i控制多少次 j控制每一次的选择排序
	int i = 0;
	int j = 0;
	int maxi = 0;
	int tmp = 0;

	for ( i = 0; i < size-1; i++)
	{
		for (j = 0; j < size - 1 - i; j++)
		{
			if (arr[j]>arr[maxi])
			{
				maxi = j;
			}
		}

		tmp = arr[size - 1 - i];
		arr[size - 1 - i] = arr[maxi];
		arr[maxi] = tmp;
	}
}

除了用户提供给我们的数组之外 我们还额外开辟了四个变量 这是常数个变量 所以说这个算法的空间复杂度是O(1)

给定一个数组 要求统计数组里面每个数字出现的次数

我们看这个题目第一时间想到的肯定是使用map来创建一个数字-次数的映射

它的最差情况就是数组中的每个数字都不一样 此时我们要开辟的map映射的数量就是n个

所以说该题目的这种解法的空间复杂度都是O(N)

如何比较两个算法的优劣

一般来说我们比较两个算法的优劣都是先比较时间复杂度

假设有AB两个算法 A算法的时间复杂度是N的平方 B算法的时间复杂度是N

此时我们就可以说B算法比A算法更加优秀

但是如果说A B算法的时间复杂度都是N 此时我们就要开始比较空间复杂度 比较方式同上

在空间复杂度都相同的情况下我们就不比较常数项了

因为在单次的运算中 每种运算所需要的时间就是不同的

比如说位运算就是跑的比加运算要快 加运算就是跑的比除运算要快

所以说我们到这个阶段一般会使用大样本的数据直接来测时间

面试 竞赛中的算法最优解是什么意思?

一般来说我们在设计一个算法最优解的时候首先要考虑的就是这个算法的时间复杂度

在时间复杂度最低的基础上尽可能的去降低它的空间复杂度

需要注意的是 我们比较算法时一般不比较常数项的大小 具体原因可以参考上面的内容

如果说两个算法的时空复杂度都一样 我们可以认为这两个算法一样优秀

常见的时空复杂度及排名

O(1) > O(logN) > O(N) > O(N*logN) > O(N^2)

二分法

有序数组中找一个值

我们直接来看题目

要求在一个有序的数组中找一个数的下标 要你在这个数组中找到该值 并且返回它的下标 如果找不到就返回-1

如果说我们使用遍历的方式遍历一整个数组的话那么此时我们的时间复杂度就是O(N)了

但是这样子就浪费了有序这一个条件 这个时候就可以考虑使用我们的二分法

我们假设该有序数组如下 我们要找的元素是4

在这里插入图片描述

我们找到该数组最左边的下标和最右边的下标 将它们相加之后除以2 在该题目中我们找到的下标就是4

在这里插入图片描述

之后我们比较五下标对应的元素和我们要找的值之间对应的关系 之后二分法的精髓就来了

因为该数组为有序数组所以说:

如果中间下标的值大于我们要找的值 是不是从中间位置开始整个数组的左半边我们都可以舍弃了
如果中间下标的值小于我们要找的值 是不是从中间位置开始整个数组的右半边我们都可以舍弃了

也就是说我们通过一次操作就排除了一半的选项 这就是二分法的精髓 也是二分法时间复杂度O(logN)的原因

我们下面可以看看代码如何表示

#include <vector>    
#include <iostream>    
using namespace std;    
    
    
int binsearch(vector<int> v , int num)    
{    
  int left = 0;    
  int right = v.size() - 1;    
    
  while(left <= right)    
  {    
    int mid = (left + right) / 2 ;    
    if (v[mid] == num)    
    {    
      return mid;    
    }    
    else if (v[mid] > num)    
    {    
      right = mid - 1;    
    }                                                                                                                                                                             
    else    
    {    
      left = mid + 1;    
    }    
  }    
    
  return -1;    
}    
    
int main()    
{    
  vector<int> v1 = {0 , 1 , 4, 6, 7, 8, 9, 10 , 11 ,12};    
  int ret = binsearch(v1 , 4);    
  cout << ret << endl;    
  return 0;    
}  

代码很简单 但是这里也有一点小细节需要注意

我们的 left + right 在数组很大的情况下有可能会产生溢出的情况

所以说我们可以采用这种写法

		// mid = (left + right) /2  防止溢出 
		mid = left + (right - left) / 2;

该代码运行结果如下

在这里插入图片描述

下标确实是2 符合我们的预期

寻找有序数组中大于等于某个数的位置

给你一个有序数组 要求你找到从某个值开始 后面的所有值都大于等于我们的key值

该题目也是一个典型的二分法的应用

我们假设有序数组如下 要求我们从某个下标开始所有值都大于等于三
在这里插入图片描述

还是和上面的思路一样 我们找出该数组的左右下标之后找出中间位置

在这里插入图片描述

如果该下标对应的值大于等于我们key值 那么我们就记录下该下标 之后排除掉右边所有的值
如果该下标对应的值小于我们的key值 那么我们就排除掉左边所有的值

和第一个题目不同的是 我们在做该题时一次二分并不能找到我们的答案 必须要二分到最后一个值才能确定我们的答案是多少 (因为可能会有重复的值)

这也是我们记录下标的意义 每次找到符合要求的下标我们就更新它

代码如下

#include <iostream>    
using namespace std;    
#include <vector>    
    
void binsearch(vector<int> v , int num)    
{    
  int left = 0;    
  int right = v.size() -1 ;    
  int index = 0;    
  while(left <= right)    
  {    
    int mid = left + ((right - left) / 2);    
    if (v[mid] >= num)    
    {    
      index = mid;    
      right = mid -1;    
    }    
    else    
    {    
      left = mid + 1;    
    }    
  }    
    
  cout << "find the index is:" << index << endl;    
}    
    
    
int main()    
{    
  vector<int> v1 = {1 , 2 , 3, 3, 3, 4, 5, 5, 6, 7, 8, 9, 9, 9};    
  binsearch(v1 , 3);                                                                                                                                                              
  return 0;    
} 

运行结果如下

在这里插入图片描述

我们发现确实符合我们的预期

总结:

从这个题目中我们应该能够加深对于二分法的理解 它的特点在于能够一次性排除一半的值

通过这个特点我们能够很快速的在有序数组中找到某一个特定的位置

无序数组中寻找一个局部最小值

给你一个无序数组 (数组元素大于3)该数组中每个元素都不相等 要求你找到一个局部最小值

我们这里首先给出一个局部最小值的概念

如果这个值比它前一个位置和后一个位置的值都小 那么我们就认为这个值是一个局部最小值

如果是起始位置和终止位置我们就只需要比较它和后一个(前一个)位置就可以

明白局部最小值的概念之后我们肯定会先考虑两个特殊位置 最前面和最后面的位置

如果说这其中有一个位置是局部最小值我们直接返回就好了

如果说它们都不是一个局部最小值 那么该数组的大小排序应该是一个这样子的状态

在这里插入图片描述

中间是一个什么状态我们不知道 但是根据起始和终止位置我们能够推断的是 中间肯定会存在一个局部最小值

因为只有存在一个局部最小值该曲线到最后才会是一个上升的状态 否则就会是一直下降了

那么我们就可以通过二分法来找到这其中一个局部最小值

我们通过二分法找到中间位置之后该位置和相邻两个位置的关系只可能是这三种

在这里插入图片描述

如果是第一种情况 那么我们就可以直接返回该下标的位置 因为这就是一个局部最小值

如果是情况二 我们可以画出这样子的图来理解

在这里插入图片描述

通过这张图我们就可以推断出右边肯定会存在一个局部最小值 可以继续二分

同理如果是情况三

在这里插入图片描述
我们可以推断出左边肯定会存在一个局部最小值 可以继续二分

#include <iostream>
using namespace std;
#include <vector>

void binsearch(vector<int>& v1)
{
  int left = 0;
  int right = v1.size() -1;
  // left 
  if (v1[1] > v1[0])
  {
    cout << "find the min num:" << v1[left] << "index:" << left << endl;
    return;
  }
  // right 
  if (v1[right -1 ] > v1[right])
  {
    cout << "find the min num:" << v1[right] << "index:" << right << endl;
    return;    
  }    
    
  while(left <= right)    
  {
    int mid = (left + right) / 2;                                                                                                                                                                     
    if (v1[mid] < v1[mid - 1])    
    {    
      if (v1[mid] < v1[mid + 1])    
      {    
        cout << "find the min num:" << v1[mid] << "  index:  " << mid << endl;    
        return;    
      }    
      else    
      {    
        left = mid + 1;    
      }    
    }    
    else    
    {    
      right = mid - 1;    
    }    
  }    
} 

int main()
{
  vector<int> v1 = {3 , 1, 5, 6, 7, 5, 7, 8, 1, 9};
  binsearch(v1);
  return 0;
}

运行结果如下

在这里插入图片描述

我们可以发现1 确实是一个局部最小值

同样的 从这个题目中 我们可以更加深入的理解二分法

不一定有序才可以二分 二分的精髓在于想办法一次性排除掉一半的数据

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

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

相关文章

树的存储和遍历

文章目录 6.5 树与森林6.5.1 树的存储结构1. 双亲表示法(顺序存储结构)2 孩子链表表示法3 孩子兄弟表示法(二叉树表示法) 6.5.2 森林与二叉树的转换1 树转换成二叉树2 二叉树转换成树3 森林转换成二叉树4 二叉树转换成森林 6.5.3 树和森林的遍历1. 树的遍历2. 森林的遍历 6.6 赫…

数据库篇:表设计、创建编辑以及导出导入数据

微信小程序云开发实战-答题积分赛小程序系列 数据库篇:表设计、添加编辑以及导出导入数据 原型: 最终实现界面截图:

Moqui REST API的两种实现方法

实现Restful API的方法 实现REST API有两种方法。第一种&#xff1a; The main tool for building a REST API based on internal services and entity operations is to define resource paths in a Service REST API XML file such as the moqui.rest.xml file in moqui-fr…

chatGPT国内可用镜像源地址

chatGPT国内可用镜像源地址 彷丶徨丶 关注 IP属地: 湖北 0.811 2023.03.15 16:02:16 字数 1,152 阅读 249,582 如果你正在尝试访问Chatgpt网站&#xff0c;但由于某些原因无法访问该网站&#xff0c;那么你可以尝试使用Chatgpt的国内镜像网站。以下是一些Chatgpt国内镜像网站的…

java基础知识——27.动态代理

这篇文章&#xff0c;我们来学一下java的动态代理 目录 1.动态代理的介绍 2.具体的代码实现 1.动态代理的介绍 动态代理&#xff1a;无侵入式的额外给代码增加功能 很不好理解&#xff0c;下面&#xff0c;我们通过两个例子来说明一下什么是动态代理&#xff1a; 例一&a…

shell编程 -- 基础

shell是一个命令行解释器&#xff0c;它接收应用程序/用户命令&#xff0c;然后调用操作系统内核。 linux笔记 链接&#xff1a;https://pan.baidu.com/s/16GZCPfUTRzUqIyGnYwPuUg?pwds5xt 提取码&#xff1a;s5xt 脚本执行 采用bash或者sh脚本的相对路径或绝对路径&#x…

TikTok跨境电商如何选品和营销?

鑫优尚电子商务&#xff1a;TikTok目前发展飞速&#xff0c;全球的MAU是5.6亿。现在作为全球炙手可热的短视频平台&#xff0c;全球流量相当庞大&#xff0c;覆盖75个语种、全球150个国家和地区。 对于从事跨境电商行业的人来说&#xff0c;又怎能错过一个流量这么好的平台呢&a…

ChatGPT注册详细步骤教程-ChatGPT申请教程

注册chatGPT账号的详细经验教程 注册ChatGPT账号是使用这一自然语言生成技术的关键步骤。下面是注册ChatGPT账号的详细经验教程&#xff1a; 访问OpenAI注册页面 在Web浏览器中打开OpenAI注册页面。 2.输入个人信息 在注册页面上&#xff0c;您需要提供以下个人信息&#…

树莓派 二维云台调零控制

目录 舵机的工作原理 案例程序 要求&#xff1a; 程序&#xff1a; 二维云台是通过IIC总线进行控制的&#xff0c;我们可以通过窗口命令输入&#xff1a;i2cdetect -y 1来检测IIC总线是否连接正常。 当有40显示的时候就说明IIC总线正常。 操控舵机我们需要一个PCA9685的模…

【移动端网页布局】流式布局案例 ④ ( Banner 栏制作 | 固定定位 | 标准流 | 百分比宽度设置 )

文章目录 一、Banner 栏样式及核心要点1、实现效果2、核心要点分析 二、完整代码示例1、HTML 标签结构2、CSS 样式3、展示效果 一、Banner 栏样式及核心要点 1、实现效果 在上一篇博客中 , 实现了 搜索栏 , 在本篇博客开始实现 搜索栏 下方的 Banner 栏 ; 2、核心要点分析 Bann…

OpenCV实战(21)——基于随机样本一致匹配图像

OpenCV实战&#xff08;21&#xff09;——基于随机样本一致匹配图像 0. 前言1. 基于随机样本一致匹配图像1.1 计算基本矩阵与匹配集1.2 随机样本一致算法 2. 算法优化2.1 优化基本矩阵2.2 优化匹配集 3. 完整代码小结系列链接 0. 前言 当两台摄像机拍摄同一场景时&#xff0c…

【Vue面试题】Vue2.x生命周期?

文章目录 1.有哪些生命周期&#xff08;系统自带&#xff09;?beforeCreate( 创建前 )created ( 创建后&#xff09;beforeMount (挂载前)mount (挂载后)beforeUpdate (更新前)updated (更新后)beforeDestroy&#xff08;销毁前&#xff09;destroy&#xff08;销毁后&#xf…

突发:深度学习之父Hinton为了警告AI的风险,不惜从谷歌离职!

‍数据智能产业创新服务媒体 ——聚焦数智 改变商业 今天&#xff0c;AI领域发生了一件标志性事件。那就是Hinton 为了能更自由的表达对AI失控的担忧&#xff0c;不惜从工作了10年的谷歌离职&#xff0c;可见他真的深切的感受到了危机。 不久前&#xff0c;纽约时报的一篇采访…

干货! ICLR:将语言模型绑定到符号语言中个人信息

点击蓝字 关注我们 AI TIME欢迎每一位AI爱好者的加入&#xff01; ╱ 作者简介╱ 承洲骏 上海交通大学硕士生&#xff0c;研究方向为代码生成&#xff0c;目前在香港大学余涛老师的实验室担任研究助理。 个人主页&#xff1a;http://blankcheng.github.io 谢天宝 香港大学一年级…

武忠祥老师每日一题||不定积分基础训练(六)

解法一&#xff1a; 求出 f ( x ) , 进而对 f ( x ) 进行积分。 求出f(x),进而对f(x)进行积分。 求出f(x),进而对f(x)进行积分。 令 ln ⁡ x t , 原式 f ( t ) ln ⁡ ( 1 e t ) e t 令\ln xt,原式f(t)\frac{\ln (1e^t)}{e^t} 令lnxt,原式f(t)etln(1et)​ 则 ∫ f ( x ) d…

分布式配置中心Apollo教程

分布式配置中心Apollo教程 简介 Apollo配置中心课程是传智燕青老师针对微服务开发设计的系列课程之一&#xff0c;本课程讲解了Apollo分布式系统配置中心的使用方法和工作原理&#xff0c;并从实战出发讲解生产环境下的配置中心的构建方案&#xff0c;从Apollo的应用、原理、项…

transformer and DETR

RNN 很难并行化处理 Transformer 1、Input向量x1-x4分别乘上矩阵W得到embedding向量a1-a4。 2、向量a1-a4分别乘上Wq、Wk、Wv得到不同的qi、ki、vi&#xff08;i{1,2,3,4}&#xff09;。 3、使用q1对每个k&#xff08;ki&#xff09;做attention得到a1,i&#xff08;i{1,2,3,4…

STL容器类

STL 1. STL初识 1.1 迭代器 1.1.1 原生指针也是迭代器 #define _CRT_SECURE_NO_WARNINGS #include<iostream> using namespace std; void test01() {int arr[5] { 1,2,3,4,5 };int* p arr;for (int i 0; i < 5; i) {cout << arr[i] << endl;cout &…

在c++项目中使用rapidjson(有具体的步骤,十分详细) windows10系统

具体的步骤&#xff1a; 先下载rapidjson的依赖包 方式1&#xff1a;直接使用git去下载 地址&#xff1a;git clone https://github.com/miloyip/rapidjson.git 方式2&#xff1a;下载我上传的依赖包 将依赖包引入到项目中 1 将解压后的文件放在你c项目中 2 将rapidjson文…

Python小姿势 - # Python中的模板语言

Python中的模板语言 Python是一门非常灵活的语言&#xff0c;其中一个体现就是它可以使用模板语言来生成静态文件。模板语言是一种特殊的语言&#xff0c;用来将静态文本和动态数据结合起来生成新的文本。 Python的模板语言最早出现在Web应用开发中&#xff0c;用来生成HTML页面…