力扣 4. 寻找两个正序数组的中位数

news/2024/10/22 7:57:10/

使用归并的方式合并两个有序数组,并找出中位数:

通过归并排序合并两个数组,然后直接根据合并后数组的长度计算中位数。

class Solution {public double findMedianSortedArraysMerge(int[] nums1, int[] nums2) {int[] merged = new int[nums1.length + nums2.length];int i = 0, j = 0, k = 0;// 归并排序的合并过程while (i < nums1.length && j < nums2.length) {if (nums1[i] < nums2[j]) {merged[k++] = nums1[i++];} else {merged[k++] = nums2[j++];}}// 如果有剩余元素,直接复制到 merged 数组while (i < nums1.length) {merged[k++] = nums1[i++];}while (j < nums2.length) {merged[k++] = nums2[j++];}// 计算中位数if (merged.length % 2 == 0) {return (merged[merged.length / 2] + merged[merged.length / 2 - 1]) / 2.0;} else {return merged[merged.length / 2];}}
}

不合并数组,直接找到中位数的位置:

通过两个指针在两个数组中移动,直到找到中位数的位置。

class Solution {public double findMedianSortedArraysDirect(int[] nums1, int[] nums2) {int totalLength = nums1.length + nums2.length;int count = 0, i = 0, j = 0;int a = 0, b = 0; // 用于存储中间位置的数while (count <= totalLength / 2) {a = b; // 存储上一轮的 b,当总长度是偶数时需要用到// 移动指针,找到中位数的位置if (i < nums1.length && (j >= nums2.length || nums1[i] < nums2[j])) {b = nums1[i];i++;} else {b = nums2[j];j++;}count++;}// 根据总长度是奇数还是偶数,计算中位数if (totalLength % 2 == 0) {return (a + b) / 2.0;} else {return b;}}
}

二分查找:

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;return getKthElement(nums1, nums2, midIndex + 1);} else {// 如果是偶数,找到中间两个数,计算平均值int midIndex1 = totalLength / 2 - 1, midIndex2 = totalLength / 2;return (getKthElement(nums1, nums2, midIndex1 + 1) + getKthElement(nums1, nums2, midIndex2 + 1)) / 2.0;}}// 主要逻辑方法,用于找到两个排序数组中的第 k 小的数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]);}// 正常情况,比较两个数组中的第 k/2 个元素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];// 比较并更新索引,减少 k 的值if (pivot1 <= pivot2) {k -= (newIndex1 - index1 + 1);index1 = newIndex1 + 1;} else {k -= (newIndex2 - index2 + 1);index2 = newIndex2 + 1;}}}
}

让我们一步步分析这段代码和其中的逻辑,以便更好地理解如何通过二分查找达到 O(log(m + n)) 的时间复杂度。

这个算法的关键在于每次迭代排除 (k/2) 个元素,而不是逐个排除,从而将时间复杂度降低到对数级别。

  1. 初始条件:我们有两个排序数组 nums1nums2,以及一个目标值 k,表示我们需要找到两个数组合并后的第 k 个元素。

  2. 二分查找的核心:在每一轮迭代中,我们比较 nums1[k/2 - 1]nums2[k/2 - 1]。根据二分查找的策略,我们可以排除较小值所在数组的前 k/2 个元素。

  3. 边界条件处理

    • 如果 nums1nums2 为空,直接从另一个数组中返回第 k 个元素。

    • 如果 k == 1,返回 nums1[0]nums2[0] 中的较小值。

    • 如果 nums1[k/2 - 1]nums2[k/2 - 1] 越界,我们应选择相应数组的最后一个元素进行比较。

  4. 递归或迭代:根据比较结果,我们排除一部分元素。例如,如果 nums1[k/2 - 1] < nums2[k/2 - 1],那么我们可以安全地排除 nums1 的前 k/2 个元素。接下来,我们更新 k 的值为 k - k/2,继续在剩下的元素中查找。

  5. 更新索引和 k 的值:根据排除的元素数量更新 index1index2,同时减少 k 的值,因为我们已经排除了一些元素,所需查找的位置也相应地减少。

通过这种方式,每次迭代我们至少排除 k/2 个元素,这就是算法能达到 O(log(m + n)) 时间复杂度的原因。每一轮迭代后,k 的值都会减半,最终在对数次迭代后达到递归基。这种方法避免了直接合并数组,从而大大提高了效率。

在这段代码中,findMedianSortedArrays 方法首先判断总长度是奇数还是偶数,并相应地调用 getKthElement 方法找到中位数或两个中位数的平均值。getKthElement 方法通过二分查找的方式逐渐缩小搜索范围,直到找到第 k 小的元素。通过每次排除掉 k/2 个元素,该方法有效地将时间复杂度降低到了O(log(m+n))。

FQA:

为什么维护为 k ?k 是干什么的?

理解为什么更新 (k) 为 (k - k/2) 是理解这个算法核心部分的关键。

假设我们有两个排序数组 (A) 和 (B),我们需要找到这两个数组合并后的第 (k) 小的元素。为了做到这一点,我们可以同时在两个数组中进行查找,比较两个数组中第 (k/2) 个元素(具体而言,是索引为 (k/2 - 1) 的元素)。

这里的关键点是,如果 (A[k/2 - 1]) 小于 (B[k/2 - 1]),那么 (A[k/2 - 1]) 以及它之前的所有元素(共 (k/2) 个元素)都不可能是第 (k) 小的元素。为什么呢?让我们来详细分析一下:

  1. (A[k/2 - 1]) 是数组 (A) 中的第 (k/2) 小的元素。

  2. (B[k/2 - 1]) 是数组 (B) 中的第 (k/2) 小的元素。

由于 (A[k/2 - 1]) 小于 (B[k/2 - 1]),我们可以确定,在 (B) 数组中至少有 (k/2) 个元素是大于 (A[k/2 - 1]) 的。因此,合并后的数组中,至少有 (k/2 + k/2 = k) 个元素是大于或等于 (A[k/2 - 1]) 的。

这意味着 (A[k/2 - 1]) 最好的情况是第 (k) 小的元素(如果 (A[k/2 - 1] = B[k/2 - 1])),但通常情况下,它的排名要低于第 (k) 小。因此,我们可以安全地排除 (A) 中的前 (k/2) 个元素,然后在剩下的元素中寻找第 (k - k/2) 小的元素。

通过这种方式,每一步我们都能排除掉一部分元素,从而缩小搜索范围,这是达到 (O(\log(m + n))) 时间复杂度的关键。


http://www.ppmy.cn/news/1372502.html

相关文章

做分析用什么工具

做分析用什么工具 导读 数据分析是数据辅助决策的最后一公里&#xff0c;是最终的数据可视化展示与探索分析的部分&#xff0c;选择使用最适合的数据展示方式&#xff0c;可以帮助分析人员大大提升分析效率。 问题&#xff1a; ● 纠结选择哪个工具 ● 纠结从哪里学起&#x…

职场的过早优化

过早优化&#xff0c;指的是还没弄清楚需求未来的变化的走向的时候&#xff0c;忽略了更重要的问题。 放在职业发展上&#xff1a;你在没有积累足够职场资源&#xff08;眼界、能力、人脉等等&#xff09;&#xff0c;也没有对职业发展形成清晰认知的时候&#xff0c;就过早地进…

nodejs 实现方法返回值常见方式

1、使用回调函数 回调函数是一种常见的方式来处理异步操作的结果。定义一个函数&#xff0c;并将回调函数作为参数传递给该函数。在异步操作完成后&#xff0c;调用回调函数并传递结果作为参数。 function asyncFunction(callback) {// 异步操作...// 完成后调用回调函数callb…

数仓开发-面试二

1.finebi使用 2.数据抽取中间件 flink,kettle flink和kettle区别 3.flink本身的优点和缺点 4.flink容错机制 5.DS 6.数据库 7.主要找orcle、clickhourse 8.mysql离线查作业执行计划&#xff0c;如&#xff0c;你写个sql500&#xff0c;这个时候你怎么定位问题&#xff0c;查看问…

【Unity】使用ScriptableObject存储数据

1.为什么要用ScriptableObject&#xff1f; 在游戏开发中&#xff0c;有大量的配置数据需要存储&#xff0c;这个时候就需要ScriptableObject来存储数据了。 很多人会说我可以用json、xml、txt&#xff0c;excel等等 但是你们有没有想过&#xff0c;假设你使用的是json&#x…

稀碎从零算法笔记Day9-LeetCode:最长公共前缀

题型&#xff1a;字符串 链接&#xff1a;14. 最长公共前缀 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1a;LeetCode 题目描述&#xff08;红字为笔者添加&#xff09; 编写一个函数来查找字符串数组中的最长公共前缀(前X个字母相同)。 如果不存在公共前缀&…

html实体字符,看完这篇彻底明白了

二.技术基础知识 基础知识一直都是重点考察的内容&#xff0c;包含有HTML&#xff08;5&#xff09;、CSS&#xff08;3&#xff09;、JavaScript到 戳这里领取完整开源项目&#xff1a;【一线大厂前端面试题解析核心总结学习笔记Web真实项目实战最新讲解视频】 Vue&#xff0…

【操作系统】中断、驱动程序与 signal 处理函数

中断是 cpu 与外设打交道的重要方式。计算机有多重多样的外设&#xff0c;例如&#xff1a;键盘、鼠标、硬盘、显示器等。除了 cpu 向这些外设传输数据外&#xff0c;这些设备也会向 cpu 传输数据。学习后发现&#xff0c;中断的理解与驱动程序的理解关系密切。并且中断与信号处…