二分查找与搜索树高频问题

关卡名

逢试必考的二分查找

我会了✔️



内容

1.山脉数组的峰顶索引

✔️

2.旋转数字的最小数字

✔️

3.寻找缺失数字

✔️

4.优化求平方根

✔️

5.中序与搜索树原理

✔️

6.二叉搜索树中搜索特定值

✔️

7.验证二叉搜索树

✔️

基于二分查找思想,可以拓展出很多算法问题的,而且很多都是考察的热门, 这里我们整理了几道经典的问题。
二分在算法中的应用非常多,也是很多大厂钟爱的考察类型,感兴趣的同学可以继续研究一下:

  1. CC150面试题53:在排序数组中查找数字,要求:统一几个数字在排序数组中出现的次数,例如,输入排序数组{1,2,3,3,3,3,4,5}和数字3,由于3在数组中出现了4次,因此输出4。
  2. leetcode 34. 在排序数组中查找元素的第一个和最后一个位置
  3. LeetCode875.爱吃香蕉的珂珂
  4. LeetCode29.两数相除

在前面我们发现很多题使用前序、后序或者层次遍历都可以解决,但几乎没有中序遍历的。这是因为中序与前后序相比有不一样的特征,例如中序可以和搜索树结合在一起,但是前后序则不行。
在理解了二分搜索之后,我们会发现中序搜索与二分查找简直就是一个娘养的,实在太像了。这里我们就来研究一下中序搜索的问题。 

1.基于二分查找的拓展问题 

1.1 山脉数组的峰顶索引

LeetCode852.这个题的要求有点啰嗦,核心意思就是在数组中的某位位置i开始,从0到i是递增的,从i+1 到数组最后是递减的,让你找到这个最高点。
详细要求是:符合下列属性的数组 arr 称为山脉数组 :arr.length >= 3存在 i(0 < i < arr.length - 1)使得:

  • arr[0] < arr[1] < ... arr[i-1] < arr[i]
  • arr[i] > arr[i+1] > ... > arr[arr.length - 1]

给你由整数组成的山脉数组 arr ,返回任何满足 arr[0] < arr[1] < ... arr[i - 1] < arr[i] > arr[i + 1] > ... > arr[arr.length - 1] 的下标 i 。
这个题其实就是前面找最小值的相关过程而已,最简单的方式是对数组进行一次遍历。
当我们遍历到下标i时,如果有arr[i-1]<arr[i]>arr[i+1],那么i就是我们需要找出的下标。
其实还可以更简单一些,因为是从左开始找的,开始的时候必然是arr[i-1]<a[i],所以只要找到第一个arr[i]>arr[i+1]的位置即可。代码就是:

public int peakIndexInMountainArray(int[] arr) {int n = arr.length;int ans = -1;for (int i = 1; i < n - 1; ++i) {if (arr[i] > arr[i + 1]) {ans = i;break;}}return ans;
}

这个题能否使用二分来优化一下呢?当然可以。
对于二分的某一个位置 mid,mid 可能的位置有3种情况:

  • mid在上升阶段的时候,满足arr[mid]>a[mid-1] && arr[mid]<arr[mid+1]
  • mid在顶峰的时候,满足arr[i]>a[i-1] && arr[i]>arr[i+1]
  • mid在下降阶段,满足arr[mid]<a[mid-1] && arr[mid]>arr[mid+1]

因此我们根据 mid 当前所在的位置,调整二分的左右指针,就能找到顶峰。 

public int peakIndexInMountainArray(int[] arr) {if (arr.length== 3)return 1;int left = 1, right = arr.length - 2;while(left < right) {int mid =left+ ((right - left)>>1);if (arr[mid] > arr[mid - 1] && arr[mid] > arr[mid + 1])return mid;if (arr[mid] < arr[mid + 1] && arr[mid] > arr[mid - 1])left = mid + 1;if (arr[mid] > arr[mid + 1] && arr[mid] < arr[mid - 1])right = mid - 1;}return left;
}

1.2. 旋转数字的最小数字

我们说刷算法要按照专题来刷,这样才能看清很多题目的内在关系,二分查找也是如此,很多题目看似与二分无关,但是就是在考察二分查找,我们一起看一下。
LeetCode153 已知一个长度为 n 的数组,预先按照升序排列,经由1到n次旋转后,得到输入数组。例如原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:

  • 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
  • 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]

注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。

示例1:

输入:nums = [4,5,1,2,3]

输出:1

解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。

示例2:

输入:nums = [4,5,6,7,0,1,2]

输出:0

解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。

本部分都摘自LeetCode一个不包含重复元素的升序数组在经过旋转之后,可以得到下面可视化的折线图: 

其中横轴表示数组元素的下标,纵轴表示数组元素的值。图中标出了最小值的位置,是我们需要查找的目标。
我们考虑数组中的最后一个元素 x:在最小值右侧的元素(不包括最后一个元素本身),它们的值一定都严格小于 x;而在最小值左侧的元素,它们的值一定都严格大于 x。因此,我们可以根据这一条性质,通过二分查找的方法找出最小值。
在二分查找的每一步中,左边界为 low,右边界为 high,区间的中点为 pivot,最小值就在该区间内。我们将中轴元素 nums[pivot] 与右边界元素 nums[high] 进行比较,可能会有以下的三种情况:
第一种情况是nums[pivot]<nums[high]。如下图所示,这说明nums[pivot] 是最小值右侧的元素,因此我们可以忽略二分查找区间的右半部分。

第二种情况是 nums[pivot]>nums[high]。如下图所示,这说明nums[pivot] 是最小值左侧的元素,因此我们可以忽略二分查找区间的左半部分。

由于数组不包含重复元素,并且只要当前的区间长度不为 1,pivot 就不会与high 重合;而如果当前的区间长度为 1,这说明我们已经可以结束二分查找了。因此不会存在 nums[pivot]=nums[high] 的情况。
当二分查找结束时,我们就得到了最小值所在的位置。

public int findMin(int[] nums) {int low = 0;int high = nums.length - 1;while (low < high) {int pivot = low + ((high - low) >>1);if (nums[pivot] < nums[high]) {high = pivot;} else {low = pivot + 1;}}return nums[low];
}

这里你是否注意到high = pivot;而不是我们习惯的high = pivot-1呢?这是为了防止遗漏元素,例如[3,1,2],执行的时候nums[pivot]=1,小于nums[high]=2,此时如果high=pivot-1,则直接变成了0。所以对于这种边界情况,很难解释清楚,最好的策略就是多写几种场景测试一下看看。这也是二分查找比较烦的情况,一般来说解释比较困难,也不容易理解清楚,所以写几个典型的例子试一下,面试的时候大部分case能过就能通过。
我们可以再拓展一下,如果在上面的基础上存在重复元素会怎么样呢?感兴趣的同学可以研究一下LeetCode154这道题。

1.3 找缺失数字

public int missingNumber (int[] a) {int left = 0;int right = a.length-1;while(left < right){int mid = (left+right)/2;if(a[mid]==mid){left = mid+1;}else{right = mid;}}return left;
}

1.4 优化求平方根 

剑指offer题目
实现函数 int sqrt(int x).计算并返回x的平方根这个题的思路是用最快的方式找到n*n=x的n。
如果整数没有平方根,一般采用向下取整的方式得到结果。采用折半进行比较的实现过程是:

public int sqrt (int x) {int l=1,r=x;while(l <= r){int mid = l + ((r - l)>>1);if(x/mid > mid){l = mid + 1;} else if(x / mid < mid){r = mid - 1;} else  if(x/mid == mid){return mid;}}return r;
}

 这种优化思想要记住,凡是在有序区间查找的场景,都可以用二分查找来优化速度。 如果有序区间是变化的,那就每次都针对这个变化的区间进行二分查找。这种题目在LeetCode中特别多的。

2 中序与搜索树原理

在前面我们发现很多题使用前序、后序或者层次遍历都可以解决,但几乎没有中序遍历的。这是因为中序与前后序相比有不一样的特征,例如中序可以和搜索树结合在一起,但是前后序则不行。
二叉搜索树是一个很简单的概念,但是想说清楚却不太容易。简单来说就是如果一棵二叉树是搜索树,则按照中序遍历其序列正好是一个递增序列。比较规范的定义是:

  • 若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
  • 若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
  • 它的左、右子树也分别为二叉排序树。下面这两棵树一个中序序列是{3,6,9,10,14,16,19},一个是{3,6,9,10},因此都是搜索树:

搜索树的题目虽然也是用递归,但是与前后序有很大区别,主要是因为搜索树是有序的,就可以根据条件决定某些递归就不必执行了,这也称为“剪枝”。

2.1 二叉搜索树中搜索特定值

LeetCode 700.给定二叉搜索树(BST)的根节点和一个值。 你需要在BST中找到节点值等于给定值的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 NULL。例如:

target为2,给定二叉搜索树:

        4/ \2   7/ \1   3

 你应该返回如下子树:

      2     / \   1   3

本题看起来很复杂,但是实现非常简单,递归:

  • 如果根节点为空 root == null 或者根节点的值等于搜索值 val == root.val,返回根节点。
  • 如果 val < root.val,进入根节点的左子树查找 searchBST(root.left, val)。
  • 如果 val > root.val,进入根节点的右子树查找 searchBST(root.right, val)。 
public TreeNode searchBST(TreeNode root, int val) {if (root == null || val == root.val) return root;return val < root.val ? searchBST(root.left, val) : searchBST(root.right, val);
}

如果采用迭代方式,也不复杂:

  • 如果根节点不空 root != null 且根节点不是目的节点 val != root.val:
  • 如果 val < root.val,进入根节点的左子树查找 root = root.left。
  • 如果 val > root.val,进入根节点的右子树查找 root = root.right。 
public TreeNode searchBST(TreeNode root, int val) {while (root != null && val != root.val)root = val < root.val ? root.left : root.right;return root;
}

2.2 验证二叉搜索树 

LeetCode98.给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:

  • 节点的左子树只包含 小于 当前节点的数。
  • 节点的右子树只包含 大于 当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。

示例:

 示例1:

输入:root = [2,1,3]

输出:true

示例2:

输入:root = [5,1,4,null,null,3,6]

输出:false

解释:根节点的值是 5 ,但是右子节点的值是 4 。

根据题目给出的性质,我们可以进一步知道二叉搜索树「中序遍历」得到的值构成的序列一定是升序的,在中序遍历的时候实时检查当前节点的值是否大于前一个中序遍历到的节点的值即可。

递归法:

long pre = Long.MIN_VALUE;
public boolean isValidBST(TreeNode root) {if (root == null) {return true;}// 如果左子树下某个元素不满足要求,则退出if (!isValidBST(root.left)) {return false;}// 访问当前节点:如果当前节点小于等于中序遍历的前一个节点,说明不满足BST,返回 false;否则继续遍历。if (root.val <= pre) {return false;}pre = root.val;// 访问右子树return isValidBST(root.right);
}

迭代法: 

   /*** 迭代实现** @param root* @return*/public static boolean isValidBST2(TreeNode root) {Deque<TreeNode> stack = new LinkedList<TreeNode>();double inorder = 0;while (!stack.isEmpty() || root != null) {while (root != null) {stack.push(root);root = root.left;}root = stack.pop();// 如果中序遍历得到的节点的值小于等于前一个 inorder,说明不是二叉搜索树if (root.val <= inorder) {return false;}inorder = root.val;root = root.right;}return true;}

 如果这个题理解了,可以继续研究LeetCode530.二叉搜索树的最小绝对差和LeetCode501.二叉搜索树中的众数两个题。

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

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

相关文章

conda 安装指定Version的指定Build

入下图&#xff0c;我想装cudnn的7.6.5的指定Build版本cuda10.0_0 应该使用如下命令&#xff1a; mamba install cudnn7.6.5cuda10.0_0 没有mamba用conda install也可以

04 # 第一个 TypeScript 程序

初始化项目以及安装依赖 新建 ts_in_action 文件夾 npm init -y安装好 typescript&#xff0c;就可以执行下面命令查看帮助信息 npm i typescript -g tsc -h创建配置文件&#xff0c;执行下面命令就会生成一个 tsconfig.json 文件 tsc --init使用 tsc 编译一个 js 文件 新…

daima8资源网整站数据打包完整代码(集成了ripro9.1主题,开箱即用)

基于ripro9.1完全明文无加密后门版本定制开发&#xff0c;无需独立服务器&#xff0c;虚拟主机也可以完美运营&#xff0c;只要主机支持php和mysql即可。整合了微信登录和几款第三方的主题文件&#xff0c;看起来更美观一些。站长本人就是程序员&#xff0c;所以本站的代码资源…

PyCharm关闭很慢的解决办法

使用PyCharm2023.2.5的时候碰到了一个问题&#xff0c;每次关闭项目的时候都很慢很慢&#xff0c;在网上查了&#xff0c;有可能是因为缓存的问题&#xff0c;于是试着清除缓存&#xff0c;发现还是没有用&#xff0c;关闭的时候还是很慢&#xff0c;后面看到一种解决办法&…

算法:笛卡尔平面坐标系上,若干连接点形成线,剔除距离小于阈值的点,Kotlin

算法&#xff1a;笛卡尔平面坐标系上&#xff0c;若干连接点形成线&#xff0c;剔除距离小于阈值的点&#xff0c;Kotlin const val THRESHOLD 0.6f //距离小于这个点将被剔除。data class Point(val x: Float, val y: Float)fun removeNearbyPoint(points: List<Point>…

指针概念及应用

指针的相关概念 1.指针是什么&#xff1f; 指针是内存中的一个最小单元的编号&#xff0c;其实就是指地址&#xff0c;对于我们平时口中所讲述的指针&#xff0c;通常指的是指针变量&#xff0c;指针变量是用来存放内存地址的变量。 2.地址与指针 一个32位机器在一个进程中…

多线程原理和常用方法以及Thread和Runnable的区别

文章目录 &#x1f366;多线程原理&#x1f367;随机性打印&#x1f368;多线程内存图解 &#x1f369;Thread类的常用方法&#x1f36a;获取线程名称 getName()&#x1f382;设置线程名称 setName() 或者 new Thread("线程名字")&#x1f370;使当前正在执行的线程以…

python 交互模式和命令行模式的问题

python 模式的冲突 unexpected character after line continuation character 理论上 ide里&#xff0c;输入 python 文件路径\文件.py 就可以执行 但是有时候却报错 unexpected character after line continuation character 出现上述错误的原因是没有退出解释器&#x…

JMeter从入门到精通

1、 jmeter的介绍 jmeter也是一款接口测试工具&#xff0c;由java语言开发的&#xff0c;主要进行性能测试。 2、jmeter安装 jmeter官网下载链接&#xff1a; https://jmeter.apache.org/download_jmeter.cgi &#xff0c;查看是否安装成功【jmeter -v】 下载 java jdk1.8&…

计算一个4+4+1的队形变换问题

2 2 1 1 2 2 2 2 1 1 2 2 3 3 A A 3 3 4 4 A 12 4 4 4 4 12 A 4 4 2 2 1 1 2 2 操场上有4个人以4a1的结构在6*6的平面上运动&#xff0c;行分布是0&#xff0c;0&#xff0c;0&#xff0c;1&#xff0c;1&#xff0c;2&#xff0c;列分布…

[Android] c++ 通过 JNI 调用 JAVA函数

如何使用&#xff1a; Calling Java from C with JNI - CodeProject c里的 JNI 类型 和 JAVA 类型的映射关系&#xff1a; JNI Types and Data Structures Primitive Types and Native Equivalents Java TypeNative TypeDescriptionbooleanjbooleanunsigned 8 bitsbytejbyt…

局域网协议:以太网(Ethernet)详解

文章目录 Ethernet的组成以太网和 Wi-Fi以太网应用场景以太网的发展历程以太网数据链路层CSMA/CD (载波侦听多路访问/冲突检测)推荐阅读 以太网&#xff08;Ethernet&#xff09;是一种局域网&#xff08;LAN&#xff09;技术&#xff0c;用于在局域网范围内传输数据。它是最常…

【深度学习】gan网络原理生成对抗网络

【深度学习】gan网络原理生成对抗网络 GAN的基本思想源自博弈论你的二人零和博弈&#xff0c;由一个生成器和一个判别器构成&#xff0c;通过对抗学习的方式训练&#xff0c;目的是估测数据样本的潜在分布并生成新的数据样本。 1.下载数据并对数据进行规范 transform tran…

《ChatGPT实操应用大全》探索无限可能

&#x1f5e3;️探索ChatGPT&#xff0c;开启无限可能&#x1f680; 文末有免费送书福利&#xff01;&#xff01;&#xff01; ChatGPT是人类有史以来最伟大的发明。他能写作、绘画、翻译、看病、做菜、编程、数据分析、制作视频、解高等数学题…&#xff0c;他会的技能…

《2023开发者生态系统现状》:ChatGPT 是最常用的 AI 工具,60%开发者使用代码生成工具辅助编程

在前有编程语言历经 80 年的迭代&#xff0c;后有 GitHub Copilot、ChatGPT 等 AI 辅助编程工具的层出不穷&#xff0c;开发者的开发方式发生了什么样的变化&#xff1f;行业中领头的 Java IDE IntelliJ IDEA、Kotlin 编程语言背后的软件工具开发公司 JetBrains 基于全球 26,34…

【Java 基础】09 封装 继承 多态

我们都知道 Java 是以面向对象而著称&#xff0c;最著名的当然就是面向对象的三大特性啦&#xff0c;接下来就逐一举例说明一下。 1. 封装 封装指的是将类的内部细节隐藏起来&#xff0c;只对外提供必要的访问方式。 例如&#xff1a; 我们使用的计算器做一个乘法运算&#x…

[原创]Delphi的SizeOf(), Length(), 动态数组, 静态数组的关系.

[简介] 常用网名: 猪头三 出生日期: 1981.XX.XXQQ: 643439947 个人网站: 80x86汇编小站 https://www.x86asm.org 编程生涯: 2001年~至今[共22年] 职业生涯: 20年 开发语言: C/C、80x86ASM、PHP、Perl、Objective-C、Object Pascal、C#、Python 开发工具: Visual Studio、Delphi…

SQL Sever 复习笔记【一】

SQL Sever 基础知识 一、查询数据第1节 基本 SQL Server 语句SELECT第2节 SELECT语句示例2.1 SELECT - 检索表示例的某些列2.2 SELECT - 检索表的所有列2.3 SELECT - 对结果集进行筛选2.4 SELECT - 对结果集进行排序2.5 SELECT - 对结果集进行分组2.5 SELECT - 对结果集进行筛选…

【前端】多线程 worker

VUE3 引用 npm install worker-loader 在vue.config.js文件的defineConfig里加上配置参数 chainWebpack: config > {config.module.rule(worker-loader).test(/\.worker\.js$/).use({loader: worker-loader,options: {inline: true}}).loader(worker-loader).end()}先在…

C语言错误处理之 “<errno.h>与<error.h>”

目录 前言 错误号处理方式 errno.h头文件 error.h头文件 参数解释&#xff1a; 关于的”__attribute__“解释&#xff1a; 关于“属性”的解释&#xff1a; 实例一&#xff1a; 实例二&#xff1a; error.h与errno.h的区别 补充内容&#xff1a; 前言 在开始学习…