【六】【算法分析与设计】二分查找

69. x 的平方根 

给你一个非负整数 x ,计算并返回 x算术平方根

由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。

注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5

示例 1:

输入:x = 4 输出:2

示例 2:

输入:x = 8 输出:2 解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。

提示:

  • 0 <= x <= 2^31-1

二分查找

 
int mySqrt(long long a) {if (a == 0)return a;long l = 1, r = a, mid;while (l < r) {mid = l + (r - l) / 2;if (mid * mid < a && (mid + 1) * (mid + 1) > a || mid * mid == a) {return mid;} else if (mid * mid > a) {r = mid;} else {l = mid + 1;}}return r;
}

int mySqrt(long long a) {这行定义了函数 mySqrt,它接受一个 long long 类型的参数 a,表示要求平方根的数字。返回类型是 int,意味着返回的是 a 的平方根的整数部分。

if (a == 0) return a;这里检查 a 是否等于 0,如果是,则直接返回 0。这是因为 0 的平方根是 0,这个条件判断帮助我们提前处理边界情况。

long l = 1, r = a, mid;这行初始化了三个变量,l(左边界)被初始化为 1,r(右边界)被初始化为 amid 用于存储中间值。这是二分查找算法的初始设置,用于在 1 到 a 之间找到平方根的整数部分。注意我们寻找的区间是[l,r)左闭右开,一直维护这个区间并且不断地缩小区间。

while (l < r) {这个 while 循环将会继续执行,直到左边界 l 不再小于右边界 r。这个循环是二分查找的核心,用于缩小查找范围。

mid = l + (r - l) / 2;在每次循环中,我们计算 lr 之间的中点 mid。这种计算方式可以防止 (l + r) 直接相加时可能出现的整数溢出。

if (mid * mid < a && (mid + 1) * (mid + 1) > a || mid * mid == a) { return mid; }这个 if 语句检查 mid 的平方是否恰好等于 a,或者 mid 的平方小于 a(mid + 1) 的平方大于 a。如果满足这些条件之一,那么 mid 就是 a 的平方根的整数部分,函数返回 mid

else if (mid * mid > a) { r = mid; }如果 mid 的平方大于 a,说明平方根在 lmid 之间,所以我们把右边界更新为 mid

else { l = mid + 1; }如果 mid 的平方小于 a,说明平方根在 (mid + 1)r 之间,所以我们把左边界更新为 mid + 1

时间复杂度和空间复杂度分析

时间复杂度: O(log a)。因为这是一个二分查找算法,每次循环都将搜索范围减半,所以时间复杂度是对数级别的。

空间复杂度: O(1)。我们只使用了有限的几个变量,所以空间复杂度是常数级别的。

牛顿迭代法:

 
int mySqrt(int a) {long x = a;while (x * x > a) {x = (x + a / x) / 2;}return x;}

int mySqrt(int a) {这行定义了一个名为 mySqrt 的函数,它接受一个 int 类型的参数 a,表示要求平方根的数字。返回类型也是 int,表示返回的是 a 的平方根的整数部分。

long x = a;这里,我们首先将输入的 int 类型的 a 转换成 long 类型并赋值给变量 x。这样做主要是为了防止在后面的计算中可能出现的溢出问题。选择 long 类型是因为它的范围比 int 类型更大,可以处理更大的数。

while (x * x > a) {这个 while 循环的条件是 x * x 大于 a,意味着只要 x 的平方还大于 a,就继续迭代。牛顿迭代法的核心思想是逐渐逼近真实的根,所以这个条件确保了我们只在还没有找到合适的平方根时继续迭代。

x = (x + a / x) / 2;这行是牛顿迭代法的迭代公式推导得到的公式。

牛顿迭代法的基本原理

假设我们要找的方程是 $$f(x) = 0$$,并且我们已经有了一个近似解 $$x_n$$。牛顿迭代法的下一个近似解 $$x_{n+1}$$可以通过下面的公式计算得到:$$ x_{n+1} = x_n - \frac{f(x_n)}{f'(x_n)} $$

这里, $$f'(x_n)$$ 表示 $$f(x)$$在 $$x_n $$处的导数。直观上,这个公式通过当前估计点的函数值和斜率来找到函数图形与x轴的交点,这个交点就是新的估计根。

while 循环结束,说明我们找到了满足 x * x <= a 的最大的 x 值,这个 x 就是 a 的平方根的整数部分,函数返回这个值。

时间复杂度和空间复杂度分析

时间复杂度: O(log a)。虽然这看起来是一个简单的迭代,但牛顿迭代法的收敛速度非常快,通常被认为具有对数时间复杂度。

空间复杂度: O(1)。我们只使用了有限的几个变量进行计算,因此空间复杂度是常数级别的。

34. 在排序数组中查找元素的第一个和最后一个位置

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

 

输入:nums = [5,7,7,8,8,10], target = 8 输出:[3,4]

示例 2:

 

输入:nums = [5,7,7,8,8,10], target = 6 输出:[-1,-1]

示例 3:

输入:nums = [], target = 0 输出:[-1,-1]

提示:

  • 0 <= nums.length <= 10(5)

  • -10(9) <= nums[i] <= 10(9)

  • nums 是一个非递减数组

  • -10(9) <= target <= 10(9)

 
class Solution {
public:// 辅函数int lower_bound(vector<int>& nums, int target) { // 小于  大于等于int l = 0, r = nums.size() - 1, mid;while (l < r) {mid = (l + r) / 2;if (nums[mid] >= target) {r = mid;} else {l = mid + 1;}}return l;}// 辅函数int upper_bound(vector<int>& nums, int target) { // 小于等于  大于int l = 0, r = nums.size() - 1, mid;while (l < r) {mid = (l + r + 1) / 2;if (nums[mid] > target) {r = mid - 1;} else {l = mid;}}return l;}// 主函数vector<int> searchRange(vector<int>& nums, int target) {if (nums.empty())return vector<int>{-1, -1};int lower = lower_bound(nums, target);int upper = upper_bound(nums, target);if (nums[lower] != target) {return vector<int>{-1, -1};}return vector<int>{lower, upper};}
};

辅助函数lower_bound解析

int lower_bound(vector<int>& nums, int target) {这是lower_bound函数的定义,它接收一个整数数组nums和一个整数target作为参数,目标是找到第一个不小于target的元素的索引。

int l = 0, r = nums.size() - 1, mid;定义了三个整数变量lrmid,分别表示搜索范围的左边界、右边界和中点。初始化l为0,rnums.size() - 1,因为是闭区间搜索。

while (l < r) { mid = (l + r) / 2; if (nums[mid] >= target) { r = mid; } else { l = mid + 1; } }这段循环通过不断地调整lr的值来缩小搜索范围,最终目的是找到第一个不小于target的元素。这里采用的是二分查找算法,可以高效地定位元素。不断地维护区间[l,r]的意义,并且不断地缩小区间直至区间大小为1[l,r]的意义是大于等于target,前面都是小于target的数,后面都是大于等于target的数。如果r是等于midr不会改变,所以不能选择右侧为最后的判断项,所以mid的计算不需要加1

return l; }返回左边界l,这时l即为第一个不小于target的元素的索引。

辅助函数upper_bound解析

int upper_bound(vector<int>& nums, int target) {定义upper_bound函数,其目的是找到最后一个等于target的元素的索引。

int l = 0, r = nums.size() - 1, mid;lower_bound函数类似,定义搜索范围的左边界l、右边界r以及中点mid

while (l < r) { mid = (l + r + 1) / 2; if (nums[mid] > target) { r = mid - 1; } else { l = mid; } }这个循环是为了找到最后一个等于target的元素。与lower_bound不同的是,这里在计算mid时加了1,并且调整r的策略也略有不同,以确保可以正确处理边界情况。不断地维护区间[l,r]的意义,并且不断地缩小区间直至区间大小为1[l,r]的意义是小于等于target,前面都是小于等于target的数,后面都是大于target的数。如果l是等于midl不会改变,所以不能选择左侧为最后的判断项,所以mid的计算需要加1

return l; }返回左边界l,此时l指向第一个大于target的元素的索引。

主函数searchRange解析

vector<int> searchRange(vector<int>& nums, int target) {这是主函数的定义,它接收一个整数数组nums和一个整数target,返回一个包含两个整数的数组,代表target的起始和结束位置。

if (nums.empty()) return vector<int>{-1, -1};如果输入数组为空,则直接返回{-1, -1},表示找不到target

int lower = lower_bound(nums, target); int upper = upper_bound(nums, target);调用lower_boundupper_bound函数找到target的下界和上界。

if (nums[lower] != target) { return vector<int>{-1, -1}; }如果lower位置的元素不是target,说明target不在数组中,返回{-1, -1}

return vector<int>{lower, upper}; 返回一个临时对象。 }如果找到了target,返回其起始和结束位置的索引。

时间复杂度和空间复杂度分析

时间复杂度:这两个辅助函数以及主函数都使用了二分查找,所以时间复杂度为O(log n)。

空间复杂度:没有使用额外的空间(除了输入和返回的数组外),所以空间复杂度为O(1)。

81. 搜索旋转排序数组 II

已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。

在传递给函数之前,nums 在预先未知的某个下标 k0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4]

给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false

你必须尽可能减少整个操作步骤。

示例 1:

 

输入:nums = [2,5,6,0,0,1,2], target = 0 输出:true

示例 2:

 

输入:nums = [2,5,6,0,0,1,2], target = 3 输出:false

提示:

  • 1 <= nums.length <= 5000

  • -10(4) <= nums[i] <= 10(4)

  • 题目数据保证 nums 在预先未知的某个下标上进行了旋转

  • -10(4) <= target <= 10(4)

进阶:

  • 这是 搜索旋转排序数组 的延伸题目,本题中的 nums 可能包含重复元素。

  • 这会影响到程序的时间复杂度吗?会有怎样的影响,为什么?

 
bool search(vector<int>& nums, int target) {int start = 0, end = nums.size() - 1;while (start <= end) {int mid = (start + end) / 2;if (nums[mid] == target) {return true;}if (nums[start] == nums[mid]) {// 无法判断哪个区间是增序的++start;} else if (nums[mid] <= nums[end]) {// 右区间是增序的if (target >= nums[mid] && target <= nums[end]) {start = mid;} else {end = mid - 1;}} else {// 左区间是增序的if (target >= nums[start] && target <= nums[mid]) {end = mid;} else {start = mid + 1;}}}return false;
}

bool search(vector<int>& nums, int target) {定义了一个名为search的函数,它接受一个整数数组nums和一个整数target作为参数,并返回一个布尔值。这个布尔值表示target是否存在于nums中。

int start = 0, end = nums.size() - 1;定义了两个整数startend作为搜索的起始和结束位置,分别初始化为0和nums.size() - 1

while (start <= end) {这是一个循环,条件是start小于等于end。循环的目的是在nums中查找target,直到找到targetstart大于end

int mid = (start + end) / 2;计算中点mid的位置,用于分割数组并决定下一步搜索的方向。

if (nums[mid] == target) { return true; }如果在mid位置找到了target,则函数返回true

if (nums[start] == nums[mid]) { // 无法判断哪个区间是增序的 ++start; }如果start位置和mid位置的元素相等,无法判断哪个区间是有序的,所以将start向前移动一位,以缩小搜索范围。

else if (nums[mid] <= nums[end]) { // 右区间是增序的 if (target >= nums[mid] && target <= nums[end]) { start = mid; } else { end = mid - 1; } }如果mid位置的元素小于等于end位置的元素,说明右区间是有序的。然后判断target是否在这个有序的右区间内。如果是,调整startmid;如果不是,调整endmid - 1

else { // 左区间是增序的 if (target >= nums[start] && target <= nums[mid]) { end = mid; } else { start = mid + 1; } }如果上述条件都不满足,说明左区间是有序的。接着判断target是否在这个有序的左区间内。如果是,调整endmid;如果不是,调整startmid + 1

return false; }如果循环结束还没有找到target,则返回false,表示target不在nums中。

时间复杂度和空间复杂度分析

时间复杂度:O(log n)。虽然存在一些特殊情况(如nums[start] == nums[mid]时只能逐个检查),大多数情况下该算法能够以对数时间复杂度运行,因为它每次都将搜索范围减半。

空间复杂度:O(1)。该算法只使用了固定数量的额外空间(几个整型变量),因此空间复杂度是常数级别的。

结尾

最后,感谢您阅读我的文章,希望这些内容能够对您有所启发和帮助。如果您有任何问题或想要分享您的观点,请随时在评论区留言。

同时,不要忘记订阅我的博客以获取更多有趣的内容。在未来的文章中,我将继续探讨这个话题的不同方面,为您呈现更多深度和见解。

谢谢您的支持,期待与您在下一篇文章中再次相遇!

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

玩转 Spring 状态机:更优雅的实现订单状态流转

说起 Spring 状态机&#xff0c;大家很容易联想到这个状态机和设计模式中状态模式的区别是啥呢&#xff1f;没错&#xff0c;Spring 状态机就是状态模式的一种实现&#xff0c;在介绍 Spring 状态机之前&#xff0c;让我们来看看设计模式中的状态模式。 1. 状态模式 状态模式…

如何使用vue定义组件之——子组件调用父组件数据

1.定义父子模板template <div class"container"><my-father></my-father><my-father></my-father><my-father></my-father><!-- 此处无法调用子组件&#xff0c;子组件必须依赖于父组件进行展示 --><!-- <my-…

数据结构:图及相关算法讲解

图 1.图的基本概念2. 图的存储结构2.1邻接矩阵2.2邻接表2.3两种实现的比较 3.图的遍历3.1 图的广度优先遍历3.2 图的深度优先遍历 4.最小生成树4.1 Kruskal算法4.2 Prim算法4.3 两个算法比较 5.最短路径5.1两个抽象存储5.2单源最短路径--Dijkstra算法5.3单源最短路径--Bellman-…

【Git版本控制系统】:起步

目录 前言 版本控制 集中式与分布式的区别 Windows安装Git 核心 文件状态 工作区域 基本工作流程 配置用户信息 获取帮助 在线资源 前言 本篇文件的环境是Windows环境下实现。 在日常工作中git少不了&#xff0c;所以编写本篇文章介绍Git基础&#xff0c;专栏会不…

unicloud JQL数据库操作介绍

JQL数据库操作 JQL&#xff0c;全称 javascript query language&#xff0c;是一种js方式操作数据库的规范。 JQL大幅降低了js工程师操作数据库的难度&#xff0c;比SQL和传统MongoDB API更清晰、易掌握。JQL支持强大的DB Schema&#xff0c;内置数据规则和权限。DB Schema 支…

与结构数列顺序有关的两个方程组

已知一组4点的结构数列顺序为&#xff0c; 方程组41 (5*x1)/5.0r1 (2*x23*x3)/5.0r2 (4*x21*x8)/5.0r3 (3*x32*x5)/5.0r4 (5*x5)/5.0r5 (4*x31*x14)/5.0r6 (1*x13*x41*x16)/5.0r7 (2*x32*x71*x10)/5.0r8 (2*x53*x7)/5.0r9 (2*x23*x10)/5.0r10 (2*x42*x91*x11)/5.0r11…

Vue 中使用 v-for 渲染列表时绑定 key 的重要性

在 Vue.js 中&#xff0c;v-for 是一个常用的指令&#xff0c;用于渲染列表数据到页面上。然而&#xff0c;在使用 v-for 渲染列表时&#xff0c;绑定一个 key 是至关重要的实践之一。本文将详细介绍为什么在 Vue 中使用 v-for 渲染列表时绑定 key 是如此重要&#xff0c;并深入…

迅为iTOP-RK3588开发板Buildroot系统功能测试

第三章 Buildroot系统功能测试 烧写buildroot系统镜像&#xff0c;buildroot系统镜像在网盘资料“iTOP-3588开发板\01_【iTOP-RK3588开发板】基础资料\06_iTOP-RK3588开发板Linux镜像\01_Buildroot镜像”目录下&#xff0c;本小节测试buildroot系统。 3.1系统启动 Buildroot系…

Qt_vc++崩溃日志分析

环境 Clion &#xff1a;2019.3.6 Qt &#xff1a;5.9.6&#xff08;vc2015&#xff09; 编译工具&#xff1a;vs2015 update3 崩溃日志收集 自行百度&#xff0c;会查到很多&#xff0c;一下代码仅供参考&#xff08;来自https://blog.csdn.net/weixin_45571586/article/…

蓝桥杯-特殊日期

代码及思路详解 #include <iostream> using namespace std; int func(int n) {int sum0; while(n){sumn%10;n/10;//d得到每一位的数 }return sum; } int main() {int count0;int year,month,days[13]{0,31,28,31,30,31,30,31,31,30,31,30,31};for(year1900;year<999…

2024年腾讯云学生服务器优惠价格、续费和购买流程

腾讯云学生服务器优惠活动「云校园」轻量应用服务器2核2G学生价30元3个月、58元6个月、112元一年&#xff0c;轻量应用服务器4核8G配置112元3个月、352.8元6个月、646.8元一年&#xff0c;CVM云服务器2核4G3M公网带宽配置842.4元一年&#xff0c;腾讯云服务器网txyfwq.com分享2…

Windows系统下载安装Plex媒体服务结合内网穿透远程访问本地影音文件

文章目录 1.前言2. Plex网站搭建2.1 Plex下载和安装2.2 Plex网页测试2.3 cpolar的安装和注册 3. 本地网页发布3.1 Cpolar云端设置3.2 Cpolar本地设置 4. 公网访问测试5. 结语 1.前言 用手机或者平板电脑看视频&#xff0c;已经算是生活中稀松平常的场景了&#xff0c;特别是各…