二分查找与二叉树中序遍历——面试算法

目录

二分查找与分治

循环方式

递归方式

元素中有重复的二分查找

基于二分查找的拓展问题

山脉数组的顶峰索引——局部有序

旋转数字中的最小数字

找缺失数字

优化平方根

中序与搜索树

二叉搜索树中搜索特定值

验证二叉搜索树

有序数组转化为二叉搜索树

寻找两个有序数组中的中位数


凡是在排好序的地方查找的都就可以考虑用二分来优化查找效率。

不一定全局都排好才行,只要某个部分是排好的,就可以针对该部分进行二分查找,这是很多题目优化查找的重要途径。

为了更有通用性,插值查找使用的公式是:

mid=low+(key- a[low])/(a[high]-a[low])*(high-low)

二分查找与分治

二分查找就是将中间结果与目标进行比较,一次去掉一半,因此二分查找可以说是最简单、最典型的分治了。

循环方式

public static int binarySearch(int[] array, int low, int high, int target) {while (low <= high) {int mid = low + ((high - low) >> 1);if (array[mid] == target) {return mid;} else if (array[mid] > target) {// 由于array[mid]不是目标值,因此再次递归搜索时,可以将其排除high = mid - 1;} else {// 由于array[mid]不是目标值,因此再次递归搜索时,可以将其排除low = mid + 1;}}return -1;
}

递归方式

public  int binarySearch1(int[] array, int low, int high, int target) {//递归终止条件if(low <= high){int mid = low + ((high - low) >> 1);if(array[mid] == target){return mid  ;  // 返回目标值的位置,从1开始}else if(array[mid] > target){// 由于array[mid]不是目标值,因此再次递归搜索时,可以将其排除return binarySearch(array, low, mid-1, target);}else{// 由于array[mid]不是目标值,因此再次递归搜索时,可以将其排除return binarySearch(array, mid+1, high, target);}}return -1;   //表示没有搜索到
}

元素中有重复的二分查找

假如在上面的基础上,元素存在重复,如果重复则找左侧第一个。

这里的关键是找到目标结果之后不是返回而是继续向左侧移动。第一种,也是最简单的方式,找到相等位置向左使用线性查找,直到找到相应的位置。

public static int search(int[] nums, int target) {if (nums == null || nums.length == 0)return -1;int left = 0;int right = nums.length - 1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] < target) {left = mid + 1;} else if (nums[mid] > target) {right = mid - 1;} else {//找到之后,往左边找while (mid != 0 && nums[mid] == target)mid--;if (mid == 0 && nums[mid] == target) {return mid;}    return mid + 1;}}return -1;
}

基于二分查找的拓展问题

山脉数组的顶峰索引——局部有序

在数组中的某位位置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;
}

旋转数字中的最小数字

已知一个长度为 n 的数组,预先按照升序排列,经由1到n次旋转后,得到输入数组。给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素

示例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 次得到输入数组。

我们考虑数组中的最后一个元素 x:在最小值右侧的元素(不包括最后一个元素本身),它们的值一定都严格小于 x;而在最小值左侧的元素,它们的值一定都严格大于 x。因此,我们可以根据这一条性质,通过二分查找的方法找出最小值。

在二分查找的每一步中,左边界为 low,右边界为 high,区间的中点为 pivot,最小值就在该区间内。我们将中轴元素 nums[pivot] 与右边界元素 nums[high] 进行比较,可能会有以下的三种情况:

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

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];
}

找缺失数字

一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。

对于有序的也可以用二分查找,这里的关键点是在缺失的数字之前,必然有nums[i]==i,在缺失的数字之后,必然有nums[i]!=i。

因此,只需要二分找出第一个nums[i]!=i,此时下标i就是答案。若数组元素中没有找到此下标,那么缺失的就是n。

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-1;}}return left;
}

优化平方根

实现函数 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;
}

中序与搜索树

如果一棵二叉树是搜索树,则按照中序遍历其序列正好是一个递增序列。

  • 若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值;

  • 若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值;

  • 它的左、右子树也分别为二叉排序树。

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

二叉搜索树中搜索特定值

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

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);
}public TreeNode searchBST(TreeNode root, int val) {while (root != null && val != root.val)root = val < root.val ? root.left : root.right;return root;
}

验证二叉搜索树

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

  • 节点的左子树只包含 小于 当前节点的数。

  • 节点的右子树只包含 大于 当前节点的数。

  • 所有左子树和右子树自身必须也是二叉搜索树。

结合二叉搜索树的性质,中序遍历构成的序列一定是升序的。在中序遍历的时候实时检查当前节点的值是否大于前一个中序遍历到的节点的值即可。

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);
}

有序数组转化为二叉搜索树

给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。

理论上如果要构造二叉搜索树,可以以升序序列中的任一个元素作为根节点,以该元素左边的升序序列构建左子树,以该元素右边的升序序列构建右子树,这样得到的树就是一棵二叉搜索树。 本题要求高度平衡,因此我们需要选择升序序列的中间元素作为根节点,这本质上就是二分查找的过程。

class Solution {public TreeNode sortedArrayToBST(int[] nums) {return helper(nums, 0, nums.length - 1);}public TreeNode helper(int[] nums, int left, int right) {if (left > right) {return null;}// 总是选择中间位置左边的数字作为根节点int mid = (left + right) / 2;TreeNode root = new TreeNode(nums[mid]);root.left = helper(nums, left, mid - 1);root.right = helper(nums, mid + 1, right);return root;}
}

寻找两个有序数组中的中位数

给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的中位数 。

首先,中位数到底是什么?

  • 如果合并后的数组长度是奇数,中位数就是中间那个数(比如总长度7,中位数是第4个数)。

  • 如果是偶数,中位数是中间两个数的平均值(比如总长度8,中位数是第4和第5个数的平均)。

所以问题可以转化为:如何找到两个有序数组中第k小的数,其中k可能是(m+n)/2或(m+n)/2+1。

二分思想延伸思路:每次排除不可能的部分 假设我们要找第k小的数,可以比较两个数组中第k/2位置的元素:

  • 比如k=5,我们比较nums1的第2个元素(k/2=2,索引从0开始是1)和nums2的第2个元素。

  • 如果nums1的这个元素更小,说明nums1的前k/2个元素都不可能是第k小的数,可以排除掉这部分,问题规模就缩小了一半!

举个栗子🌰: nums1 = [1,3,5], nums2 = [2,4,6], 找第4小的数(k=4)。

  1. 比较nums1的第2个元素(k/2=2,即nums1[1]=3)和nums2的第2个元素(nums2[1]=4)。

  2. 3 < 4 → 排除nums1的前2个元素(1和3),现在nums1剩下[5]。

  3. 问题变成从[5]和[2,4,6]中找第4-2=2小的数。

  4. 现在k=2,比较剩下的数组的第1个元素(k/2=1):

    1. nums1[0]=5 vs nums2[0]=2 → 2更小,排除nums2的前1个元素(2)。

  5. 现在问题变成从[5]和[4,6]中找第2-1=1小的数,即最小的数:4。

  6. 所以第4小的数是4。

边界情况怎么办?

  • 如果某个数组长度不够k/2: 比如nums1只剩2个元素,但k/2=3,这时候只能取nums1剩下的所有元素,排除掉这部分后k减去实际排除的数量。

  • 当k=1时: 直接比较两个数组当前剩下的第一个元素,取较小的那个。

  • 一个数组被完全排除: 直接在另一个数组中找第k小的数。

为什么能保证时间复杂度是O(log(m+n))? 每次排除掉k/2个元素,相当于每次将问题规模减半,类似二分查找,所以时间复杂度是对数级别的。

再举个栗子🌰(处理边界): nums1 = [1,2], nums2 = [3,4],找第3小的数(总长度4,中位数是第2和3的平均)。

  1. 找第3小的数:k=3。

  2. 比较nums1的第1个元素(k/2=1,nums1[0]=1)和nums2的第1个元素(nums2[0]=3)。

  3. 1 < 3 → 排除nums1的前1个元素(1),k=3-1=2。

  4. 现在nums1剩下[2],nums2是[3,4]。

  5. 找第2小的数:比较nums1[0]=2和nums2[0]=3 → 2更小,排除nums1的1个元素,k=2-1=1。

  6. 现在k=1,取剩下数组的第一个元素:nums2[0]=3。

  7. 所以第3小的是3,中位数是(2+3)/2=2.5。

总结步骤:

  1. 始终保持nums1是较短的数组(减少边界处理)。

  2. 递归或循环比较两个数组的k/2位置:

    1. 排除较小元素的前半部分。

    2. 更新k值(减去排除的数量)。

  3. 处理边界(数组长度不足、k=1等)。

通过不断排除不可能的部分,最终就能高效找到第k小的数啦!虽然细节有点多,但多举例子就能理解啦~ (๑•̀ㅂ•́)و✧

class Solution {public double findMedianSortedArrays(int[] nums1, int[] nums2) {int length1 = nums1.length, length2 = nums2.length;int totalLength = length1 + length2;if (totalLength % 2 == 1) {int midIndex = totalLength / 2;double median = getKthElement(nums1, nums2, midIndex + 1);return median;} else {int midIndex1 = totalLength / 2 - 1, midIndex2 = totalLength / 2;double median = (getKthElement(nums1, nums2, midIndex1 + 1) + getKthElement(nums1, nums2, midIndex2 + 1)) / 2.0;return median;}}public int getKthElement(int[] nums1, int[] nums2, int k) {int length1 = nums1.length, length2 = nums2.length;int index1 = 0, index2 = 0;while (true) {// 边界情况if (index1 == length1) {return nums2[index2 + k - 1];}if (index2 == length2) {return nums1[index1 + k - 1];}if (k == 1) {return Math.min(nums1[index1], nums2[index2]);}// 正常情况int half = k / 2;int newIndex1 = Math.min(index1 + half, length1) - 1;int newIndex2 = Math.min(index2 + half, length2) - 1;int pivot1 = nums1[newIndex1], pivot2 = nums2[newIndex2];if (pivot1 <= pivot2) {k -= (newIndex1 - index1 + 1);index1 = newIndex1 + 1;} else {k -= (newIndex2 - index2 + 1);index2 = newIndex2 + 1;}}}
}

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

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

相关文章

字符串——面试考察高频算法题

目录 转换成小写字母 字符串转化为整数 反转相关的问题 反转字符串 k个一组反转 仅仅反转字母 反转字符串里的单词 验证回文串 判断是否互为字符重排 最长公共前缀 字符串压缩问题 转换成小写字母 给你一个字符串 s &#xff0c;将该字符串中的大写字母转换成相同的…

现代复古电影海报品牌徽标设计衬线英文字体安装包 Thick – Retro Vintage Cinematic Font

Thick 是一种大胆的复古字体&#xff0c;专为有影响力的标题和怀旧的视觉效果而设计。其厚实的字体、复古魅力和电影风格使其成为电影海报、产品标签、活动品牌和编辑设计的理想选择。无论您是在引导电影的黄金时代&#xff0c;还是在现代布局中注入复古活力&#xff0c;Thick …

[C++面试] new、delete相关面试点

一、入门 1、说说new与malloc的基本用途 int* p1 (int*)malloc(sizeof(int)); // C风格 int* p2 new int(10); // C风格&#xff0c;初始化为10 new 是 C 中的运算符&#xff0c;用于在堆上动态分配内存并调用对象的构造函数&#xff0c;会自动计算所需内存…

Unity URP管线与HDRP管线对比

1. 渲染架构与底层技术 URP 渲染路径&#xff1a; 前向渲染&#xff08;Forward&#xff09;&#xff1a;默认单Pass前向&#xff0c;支持少量实时光源&#xff08;通常4-8个逐物体&#xff09;。 延迟渲染&#xff08;Deferred&#xff09;&#xff1a;可选但功能简化&#…

JDK8卸载与安装教程(超详细)

JDK8卸载与安装教程&#xff08;超详细&#xff09; 最近学习一个项目&#xff0c;需要使用更高级的JDK&#xff0c;这里记录一下卸载旧版本与安装新版本JDK的过程。 JDK8卸载 以windows10操作系统为例&#xff0c;使用快捷键winR输入cmd&#xff0c;打开控制台窗口&#xf…

python爬虫:DrissionPage实战教程

如果本文章看不懂可以看看上一篇文章&#xff0c;加强自己的基础&#xff1a;爬虫自动化工具&#xff1a;DrissionPage-CSDN博客 案例解析&#xff1a; 前提&#xff1a;我们以ChromiumPage为主&#xff0c;写代码工具使用Pycharm&#xff08;python环境3.9-3.10&#xff09; …

07-01-自考数据结构(20331)- 排序-内部排序知识点

内部排序算法是数据结构核心内容,主要包括插入类(直接插入、希尔)、交换类(冒泡、快速)、选择类(简单选择、堆)、归并和基数五大类排序方法。 知识拓扑 知识点介绍 直接插入排序 定义:将每个待排序元素插入到已排序序列的适当位置 算法步骤: 从第二个元素开始遍历…

Go语言-初学者日记(八):构建、部署与 Docker 化

&#x1f9f1; 一、go build&#xff1a;最基础的构建方式 Go 的构建工具链是出了名的轻量、简洁&#xff0c;直接用 go build 就能把项目编译成二进制文件。 ✅ 构建当前项目 go build -o myapp-o myapp 指定输出文件名默认会构建当前目录下的 main.go 或 package main &a…

教程:如何使用 JSON 合并脚本

目录 1. 介绍 2. 使用方法 3. 注意事项 4. 示例 5.完整代码 1. 介绍 该脚本用于将多个 COCO 格式的 JSON 标注文件合并为一个 JSON 文件。COCO 格式常用于目标检测和图像分割任务&#xff0c;包含以下三个主要部分&#xff1a; "images"&#xff1a;图像信息&a…

Java学习总结-缓冲流性能分析

测试用例&#xff1a; 分别使用原始的字节流&#xff0c;以及字节缓冲流复制一个很大的视频。 测试步骤&#xff1a; 在这个分析性能需要一个记录时间的工具&#xff1a;这个是记录1970-1-1 00&#xff1a;00&#xff1a;00到现在的总毫秒值。 long start System.currentT…

流影---开源网络流量分析平台(五)(成果展示)

目录 前沿 攻击过程 前沿 前四章我们已经成功安装了流影的各个功能&#xff0c;那么接下来我们就看看这个开源工具的实力&#xff0c;本实验将进行多个攻击手段&#xff08;ip扫描&#xff0c;端口扫描&#xff0c;sql注入&#xff09;攻击靶机&#xff0c;来看看流影的态感效…

vs环境中编译osg以及osgQt

1、下载 OpenSceneGraph 获取源代码 您可以通过以下方式获取 OSG 源代码: 官网下载:https://github.com/openscenegraph/OpenSceneGraph/releases 使用 git 克隆: git clone https://github.com/openscenegraph/OpenSceneGraph.git 2、下载必要的第三方依赖库 依赖库 ht…

Unity:标签(tags)

为什么需要Tags&#xff1f; 在游戏开发中&#xff0c;游戏对象&#xff08;GameObject&#xff09;数量可能非常多&#xff0c;比如玩家、敌人、子弹等。开发者需要一种简单的方法来区分这些对象&#xff0c;并根据它们的类型执行不同的逻辑。 核心需求&#xff1a; 分类和管…

【C++11】lambda

lambda lambda表达式语法 lambda表达式本质是一个匿名函数对象&#xff0c;跟普通函数不同的是它可以定义在函数内部。lambda表达式语法使用层而言没有类型&#xff0c;所以一般是用auto或者模板参数定义的对象去接收lambda对象。 lambda表达式的格式&#xff1a;[capture-l…

fpga:分秒计时器

任务目标 分秒计数器核心功能&#xff1a;实现从00:00到59:59的循环计数&#xff0c;通过四个七段数码管显示分钟和秒。 复位功能&#xff1a;支持硬件复位&#xff0c;将计数器归零并显示00:00。 启动/暂停控制&#xff1a;通过按键控制计时的启动和暂停。 消抖处理&#…

《UNIX网络编程卷1:套接字联网API》第6章 IO复用:select和poll函数

《UNIX网络编程卷1&#xff1a;套接字联网API》第6章 I/O复用&#xff1a;select和poll函数 6.1 I/O复用的核心价值与适用场景 I/O复用是高并发网络编程的基石&#xff0c;允许单个进程/线程同时监控多个文件描述符&#xff08;套接字&#xff09;的状态变化&#xff0c;从而高…

SpringBoot+vue前后端分离整合sa-token(无cookie登录态 详细的登录流程)

SpringBootvue前后端分离整合sa-token&#xff08;无cookie登录态 & 详细的登录流程&#xff09; 1.介绍sa-token1.1 框架定位1.2 核心优势 2.如何整合sa-token3.如何进行无cookie模式登录3.1后端3.1.1 VO层3.1.2 Controller层3.1.3 Service层 3.2前端3.2.1 登录按钮自定义…

MYOJ_1171:(洛谷P1075)[NOIP 2012 普及组] 质因数分解(数学相关,质数与约数基础)

题目描述 已知正整数 n 是两个不同的质数的乘积&#xff0c;试求出两者中较大的那个质数。 1≤n≤210^9 输入 输入一个正整数 n。 输出 输出一个正整数 p&#xff0c;即较大的那个质数。 样例输入输出 输入&#xff1a;21 输出&#xff1a;7 思路: 为了节约时间与…

Python语言的测试用例设计

Python语言的测试用例设计 引言 随着软件开发的不断进步&#xff0c;测试在软件开发生命周期中的重要性日益凸显。测试用例设计是软件测试的核心&#xff0c;它为软件系统的验证和验证提供了实施的基础。在Python语言中&#xff0c;由于其简洁明了的语法和强大的内置库&#…

SpringKafka消息消费:@KafkaListener与消费组配置

文章目录 引言一、Spring Kafka消费者基础配置二、KafkaListener注解使用三、消费组配置与负载均衡四、手动提交偏移量五、错误处理与重试机制总结 引言 Apache Kafka作为高吞吐量的分布式消息系统&#xff0c;在大数据处理和微服务架构中扮演着关键角色。Spring Kafka为Java开…