详细解析Redis的BitMap、HyperLogLog、GEO和Stream等高级数据类型的实现原理、操作命令及应用场景,包括签到统计、用户在线状态、去重计数、地理位置服务和消息队列等实际应用
LeetCode 295 - 数据流的中位数(Find Median from Data Stream)
使用两个堆(最大堆与最小堆)巧妙维护数据流的中位数,实现了 O(log n) 的插入和 O(1) 的查询效率,并通过数据平衡策略确保中位数快速获取。
❌ LeetCode 215 - 数组中的第K个最大元素 (超时分析与优化)
对 LeetCode 215 题 “数组中的第K个最大元素” 的 Quick Select 解法进行超时分析,并提供优化后的正确题解,强调了算法关键点和可读性。
LeetCode 347 - 前 K 个高频元素 (Top K Frequent Elements)
本文详细介绍了 LeetCode 347 题“前 K 个高频元素”的解题思路,并重点讲解了如何在 Go 语言中使用 heap(优先队列)来解决此问题。
❌ LeetCode 84 - 柱状图中最大的矩形
深入剖析 LeetCode 84 柱状图中最大的矩形问题,通过暴力解法、分治法和单调栈三种不同方法解决,并详细分析单调栈解法中的常见错误
LeetCode 739 - 每日温度 (Daily Temperatures)
详细解析 LeetCode 739 题「每日温度」的解题思路,使用单调栈巧妙解决,并探讨代码优化,让你的代码更简洁。
LeetCode 394 - 字符串解码 (String Decode)
详细解析LeetCode第394题字符串解码,通过栈辅助实现,并提供优化思路和代码。
LeetCode 155 - 最小栈 (Min Stack)
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
MySQL 是怎么加锁的?—— 深入解析MySQL锁机制
深入剖析MySQL的锁机制原理,包括行锁、表锁、间隙锁、记录锁和next-key锁等不同类型及其实现原理,通过命令分析实例帮助理解MySQL如何加行级锁。
LeetCode 4.寻找两个正序数组的中位数 - 深入理解二分思想
题目描述 给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数。 要求算法的时间复杂度为 O(log (m+n))。 示例: 1234567输入:nums1 = [1,3], nums2 = [2]输出:2.00000解释:合并数组 = [1,2,3],中位数 2输入:nums1 = [1,2], nums2 = [3,4]输出:2.50000解释:合并数组 = [1,2,3,4],中位数 (2 + 3) / 2 = 2.5 解题思路 这道题乍看之下,似乎可以通过合并两个数组后找出中位数来解决,但这样做的时间复杂度是 O(m+n),而题目要求 O(log(m+n))。一看到对数级的时间复杂度,我们应该想到二分查找。 核心思想 本题的核心思想是将两个数组进行虚拟切分,使得: 左半部分的元素个数等于右半部分(或者多一个,当总数为奇数时) 左半部分的所有元素都小于或等于右半部分的所有元素 找到这样的切分后,中位数就可以轻松计算出来了。 假设有两个数组: 12nums1: [a1, a2, a3, a4, a5]nums2: [b1, b2, b3, b4, b5, b6, b7] 我们的目标是在这两个数组中找出一个切分位置,将所有元素分为数量相等(或左侧多一个)的两部分: 12左半部分:[a1, a2, a3 | b1, b2, b3, b4]右半部分:[a4, a5 | b5, b6, b7] 当左半部分的最大值小于或等于右半部分的最小值时,我们就找到了正确的切分位置。中位数可以根据总元素个数的奇偶性计算: 元素总数为奇数:中位数 = 左半部分的最大值 元素总数为偶数:中位数 = (左半部分的最大值 + 右半部分的最小值) / 2 实现的关键点解析 1. 切分点的含义 重要的是,mid表示的是元素间的切分位置,而不是具体的元素。例如: 对于数组 [1, 3, 5, 7, 9]: mid=0 表示在1之前切分:[] | [1, 3, 5, 7, 9] mid=3 表示在7之前切分:[1, 3, 5] | [7, 9] mid=5 表示在所有元素之后切分:[1, 3, 5, 7, 9] | [] 这就是为什么right初始化为len1而不是len1-1,因为我们需要考虑将所有元素都放在左半部分的情况。 2. 为什么使用 left <= right 而不是 left < right? 在这个二分查找中,使用 left <= right 是必要的。当搜索区间收缩到只有一个元素时(即 left == right),这个元素可能就是正确的切分点。如果使用 left < right,循环会提前退出,可能会错过正确解。 特别是当nums1只有一个元素时,如果使用 left < right,算法可能无法正确处理这种情况。 3. 为什么只检查 mid1 < len1 和 mid1 > 0,不检查 mid2 的边界? 这是一个很好的问题!算法中我们检查了: 12345if mid1 < len1 && nums1[mid1] < nums2[mid2-1] { // ...} else if mid1 > 0 && nums1[mid1-1] > nums2[mid2] { // ...} 不需要检查mid2的边界,原因如下: mid2的计算保证了某些边界条件:mid2 = halfLen - mid1,其中halfLen = (len1+len2+1)/2 mid2的范围是有保证的: 当mid1增大时,mid2减小,反之亦然 我们知道 0 <= mid1 <= len1,因此 (halfLen-len1) <= mid2 <= halfLen 算法保证了mid2的合理性: 我们总是对较短的数组进行二分(通过交换nums1和nums2确保len1 <= len2) 这确保了halfLen-len1 >= 0,即mid2 >= 0 同样,由于halfLen <= (len1+len2+1)/2 <= (len2+len2+1)/2,可以推导出mid2 <= len2 因此,对mid1进行边界检查已经足够,不需要显式检查mid2的边界。这是算法设计的巧妙之处。 4. 为什么halfLength计算要加1? 在代码中,我们看到halfLength的计算使用了: 1halfLength := (len1+len2+1)>>1 // 左半部分应该包含的元素个数 这里的+1非常关键,它确保了在奇数长度的情况下,左半部分会多包含一个元素。让我们分析不同情况: 情况1:总长度为偶数 假设len1 + len2 = 8 使用(8) >> 1 = 4,左边和右边各4个元素 使用(8 + 1) >> 1 = 4,结果相同,左右各4个元素 情况2:总长度为奇数 假设len1 + len2 = 7 使用(7) >> 1 = 3,左边3个元素,右边4个元素 使用(7 + 1) >> 1 = 4,左边4个元素,右边3个元素 在处理中位数时,我们的代码逻辑是: 1234567// 如果两个数组的总长度为奇数,中位数就是左半部分的最大值if (len1+len2)%2 == 1 { return float64(maxLeft)}// 总长度为偶数时,中位数是左半部分最大值和右半部分最小值的平均值return float64(maxLeft + minRight) / 2.0 这里的算法设计假设: 奇数长度时,中位数是左半部分的最大值 偶数长度时,需要计算左半部分最大值和右半部分最小值的平均值 为了使这个逻辑正确工作,在奇数长度情况下,我们必须确保中位数在左半部分。例如,对于总长度7的数组,中位数是第4个元素,所以左半部分必须包含4个元素。 这就是为什么要使用(len1+len2+1)>>1而不是(len1+len2)>>1的原因:加上这个+1确保了在奇数长度情况下,左半部分会多包含一个元素,从而包含了中位数,使得算法的判断逻辑能够正确工作。 完整代码实现 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108/** * 寻找两个正序数组的中位数 * @param nums1 第一个有序数组 * @param nums2 第二个有序数组 * @return 两个数组合并后的中位数 */func findMedianSortedArrays(nums1 []int, nums2 []int) float64 { // 获取两个数组的长度 len1, len2 := len(nums1), len(nums2) // 优化:始终对较短的数组进行二分查找,减少搜索空间 // 如果 nums1 比 nums2 长,则交换两个数组 if len1 > len2 { return findMedianSortedArrays(nums2, nums1) } // 二分查找的边界 left := 0 // 二分查找的左边界,表示nums1可能的最小切分点 right := len1 // 二分查找的右边界,表示nums1可能的最大切分点 halfLen := (len1+len2+1)/2 // 左半部分应该包含的元素个数(包括奇数情况) // 在nums1上进行二分查找,寻找合适的切分点 for left <= right { // 计算当前nums1的切分点(将前mid1个元素放入左半部分) mid1 := (left + right) >> 1 // 使用位运算提高效率,等价于 (left + right) / 2 // 根据mid1计算nums2的切分点,确保左半部分总共有halfLen个元素 mid2 := halfLen - mid1 // 第一种情况:nums1的切分点太小,左半部分需要更多元素 // 检查:如果mid1元素小于nums2的mid2-1元素,说明mid1需要右移 if mid1 < len1 && nums1[mid1] < nums2[mid2-1] { // mid1太小,需要增加,向右移动left指针 left = mid1 + 1 } // 第二种情况:nums1的切分点太大,左半部分需要减少元素 // 检查:如果mid1-1元素大于nums2的mid2元素,说明mid1需要左移 else if mid1 > 0 && nums1[mid1-1] > nums2[mid2] { // mid1太大,需要减少,向左移动right指针 right = mid1 - 1 } // 找到了合适的切分点:左半部分的最大值小于等于右半部分的最小值 else { // 计算左半部分的最大值 var maxLeft int // 处理边界情况:当mid1=0时,nums1的左半部分为空 if mid1 == 0 { maxLeft = nums2[mid2-1] // 左半部分最大值来自nums2 } // 处理边界情况:当mid2=0时,nums2的左半部分为空 else if mid2 == 0 { maxLeft = nums1[mid1-1] // 左半部分最大值来自nums1 } // 一般情况:取nums1和nums2左半部分的最大值 else { maxLeft = max(nums1[mid1-1], nums2[mid2-1]) } // 如果两个数组的总长度为奇数,直接返回左半部分的最大值 if (len1+len2) % 2 == 1 { return float64(maxLeft) } // 计算右半部分的最小值(偶数情况下需要) var minRight int // 处理边界情况:当mid1=len1时,nums1的右半部分为空 if mid1 == len1 { minRight = nums2[mid2] // 右半部分最小值来自nums2 } // 处理边界情况:当mid2=len2时,nums2的右半部分为空 else if mid2 == len2 { minRight = nums1[mid1] // 右半部分最小值来自nums1 } // 一般情况:取nums1和nums2右半部分的最小值 else { minRight = min(nums1[mid1], nums2[mid2]) } // 总长度为偶数时,中位数是左半部分最大值和右半部分最小值的平均值 return float64(maxLeft + minRight) / 2.0 } } // 理论上不会执行到这里,添加返回值以满足编译要求 return 0.0}/** * 返回两个整数中的较大值 */func max(a, b int) int { if a > b ...