LeetCode 1287. 有序数组中出现次数超过25%的元素
题目描述

题意分析
数组按非递减顺序排列,且恰好有一种值出现次数严格超过总长度的四分之一,要求找出这个值。
有序性保证相同值连续出现,因此高频值对应一段足够长的连续区间。可以先用几个固定位置找到候选,再通过二分计算候选的真实频次。
解法:四分位采样 + 二分边界
核心思路
[!blue]
令
q = floor(n / 4),严格超过四分之一等价于至少出现q + 1次。选取下标n / 4、n / 2、3 * n / 4处的三个值,整数除法均向下取整。这三个采样位置把未采样的部分分成至多四段,每段长度都不超过
q。若高频值的连续段避开全部采样位置,就只能落在某个这样的空隙中,不可能达到q + 1个位置。因此答案必然在三个候选之中。采样只能保证答案不会遗漏,不能保证每个候选都达标。对候选
num,二分第一个不小于它的位置left,再二分第一个大于它的位置right,所有等于它的元素恰好占据[left, right),频次就是right - left。两次二分都用
[0, n)作为初始查找范围,右边界可以最终返回n。区别只在保留左半边的条件:找下界用arr[mid] >= num,找上界用arr[mid] > num。用频次严格大于n / 4验证,不需要浮点运算。
解题步骤
- 根据数组长度取出三个四分位位置的值。
- 对每个候选分别调用
lowerBound和upperBound,得到完整出现区间。- 若边界差严格大于
n / 4,返回该候选;题目保证答案存在,因此必能找到。短数组中的采样下标可能重合,不影响正确性;候选始终只有三个,重复验证仍是常数次二分。单元素数组也能直接通过频次判断。
代码实现
class Solution {
public int findSpecialInteger(int[] arr) {
int n = arr.length;
// 高频值的连续段必覆盖至少一个四分位采样点。
int[] candidates = {
arr[n / 4],
arr[n / 2],
arr[3 * n / 4]
};
for (int num : candidates) {
int left = lowerBound(arr, num);
int right = upperBound(arr, num);
// 首个大于的位置减首个不小于的位置,才是完整频次。
if (right - left > n / 4) {
return num;
}
}
return arr[0];
}
private int lowerBound(int[] arr, int target) {
int left = 0;
int right = arr.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (arr[mid] >= target) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
private int upperBound(int[] arr, int target) {
int left = 0;
int right = arr.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (arr[mid] > target) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
}
func findSpecialInteger(arr []int) int {
n := len(arr)
// 高频值的连续段必覆盖至少一个四分位采样点。
candidates := []int{
arr[n/4],
arr[n/2],
arr[3*n/4],
}
for _, num := range candidates {
left := lowerBoundInt(arr, num)
right := upperBoundInt(arr, num)
// 首个大于的位置减首个不小于的位置,才是完整频次。
if right-left > n/4 {
return num
}
}
return arr[0]
}
func lowerBoundInt(arr []int, target int) int {
left, right := 0, len(arr)
for left < right {
mid := left + (right-left)/2
if arr[mid] >= target {
right = mid
} else {
left = mid + 1
}
}
return left
}
func upperBoundInt(arr []int, target int) int {
left, right := 0, len(arr)
for left < right {
mid := left + (right-left)/2
if arr[mid] > target {
right = mid
} else {
left = mid + 1
}
}
return left
}
复杂度分析
- 时间复杂度:$O(\log(n+1))$,固定三个候选各两次二分。
- 空间复杂度:$O(1)$。
关键点总结
[!green]
- 采样负责缩小候选,不能跳过频次验证。
- 上边界是最后一次出现的下一位,可能等于数组长度。
易错点总结
[!yellow]
- 只取中点可能错过靠边的高频值。
- 验证改成大于等于,会接受刚好四分之一。
- 把三个采样下标先统一整除再乘,会改变覆盖空隙的保证。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 229. 多数元素 II | 中等 | 都先利用频次阈值证明候选个数上界,再验证真实频次;本题从有序数组分位点取候选,该题用扩展投票维护至多两个候选。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!