PHP数组区间查找:3种方法快速定位数字范围
2025-05-06 10:19:52
在数组中快速定位数字所属的范围
哥们儿,你是不是也碰到过这种事儿:手头有个数组,里面存了一堆数字,像 [0, 32, 64, 96, 128, ...]
这样。这些数字呢,定义了一段段的区间。然后,你又有另外一个数字,比如 44
,想知道这个 44
究竟落在了哪个区间里?具体点说,就像题目里的,44
大于 32
但小于等于 64
,所以它属于 64
这个“上限”所代表的范围。
这事儿吧,听起来绕,其实不难。下面咱就掰扯掰扯。
问题来了:怎么知道我的数字在哪一段?
咱们先把场景摆出来,就像你问的那样:
$ar = array(0, 32, 64, 96, 128, 160, 192, 224);
$num = 44;
目标就是要找出 $num
(也就是 44
) 应该归到哪个“档位”。看一眼就能明白,44
它比 32
大,又没超过 64
(或者说小于等于64
),所以我们希望得到的结果是 64
。如果 $num
是 32
,那它应该也算在 32
这个档的末尾,即 (0, 32]
这个区间,上限是 32
。
为什么会碰到这个问题?
这种需求其实挺常见的。比如说:
- 你要根据用户的消费金额给他划分会员等级,消费 0-100 是普通会员,101-500 是银牌,501-1000 是金牌。这里的
[0, 100, 500, 1000]
就是你的范围数组。 - 处理数据分桶,把连续的数据点归到离散的桶里,方便统计分析。
- 游戏里根据分数、经验值等确定玩家的段位或解锁的成就。
基本上,只要你想把一个数值映射到一个预设的、有序的等级或者类别,就可能用到这个逻辑。
解决方案:庖丁解牛
针对这个问题,有几种路子可以走,各有各的妙处。
方法一:简单直接的循环遍历
这是最直观,也最容易想到的法子。既然数组里的数是排好序的(从小到大),咱们就从头到尾扫一遍。
原理和作用
我们挨个儿比较数组里的元素。对于数组 $ar
中的任意两个相邻元素 $ar[$i-1]
和 $ar[$i]
,如果我们的目标数字 $num
正好满足 $ar[$i-1] < $num <= $ar[$i]
,那 $ar[$i]
就是我们要找的那个范围的上限。
特别地,如果 $num
小于或等于数组的第一个元素 $ar[0]
,那么它就属于由 $ar[0]
定义的第一个范围。
代码示例 (PHP)
<?php
$ar = array(0, 32, 64, 96, 128, 160, 192, 224);
$num = 44;
function findRangeUpperBound_loop($arr, $number) {
if (empty($arr)) {
return null; // 空数组,啥也干不了
}
// 先处理特殊情况:如果数字小于等于数组的第一个元素
// (假设第一个元素就是第一个区间的上限)
if ($number <= $arr[0]) {
return $arr[0];
}
// 遍历数组,从第二个元素开始,因为它需要和前一个元素构成区间
for ($i = 1; $i < count($arr); $i++) {
// 我们要找的是 $arr[$i-1] < $number <= $arr[$i]
if ($number > $arr[$i-1] && $number <= $arr[$i]) {
return $arr[$i];
}
}
// 如果数字比数组里所有元素都大
// 这种情况下,题目没明确说怎么办。可以返回null,或者最后一个元素,或者抛异常
// 这里我们返回null,表示超出现有定义的最大范围
// 或者,如果业务逻辑是“只要比最后一个小元素的都归到最后一个区间”,则可以返回 $arr[count($arr)-1]
// 但按照题目例子44落在(32,64],返回64,则下面这样更合适:
// 若 $num > $arr[count($arr)-1],表示没有合适的区间上限
if ($number > $arr[count($arr)-1]) {
// 这里可以根据实际业务定义,比如返回数组最后一个元素作为“无限大”区间的象征,
// 或者严格按(L,R]则返回null。
// 若允许等于最后一个元素,那么前面的循环在 $number <= $arr[count($arr)-1] 时会处理。
// 如果是严格大于,那确实就是没找到。
return null; // 或者你可以定义成抛出错误,或者返回一个特殊值
}
return null; // 默认没找到 (理论上如果$num不大于最大值,且数组有序,前面总能找到)
}
$upperBound = findRangeUpperBound_loop($ar, $num);
if ($upperBound !== null) {
echo "数字 $num 属于上限为 $upperBound 的范围。\n"; // 输出: 数字 44 属于上限为 64 的范围。
} else {
echo "数字 $num 未能在数组定义的范围内找到合适的上限。\n";
}
// 测试其他几个数
$num1 = 32;
$upperBound1 = findRangeUpperBound_loop($ar, $num1); // 应该得到 32
echo "数字 $num1 属于上限为 $upperBound1 的范围。\n";
$num2 = 0;
$upperBound2 = findRangeUpperBound_loop($ar, $num2); // 应该得到 0
echo "数字 $num2 属于上限为 $upperBound2 的范围。\n";
$num3 = 250;
$upperBound3 = findRangeUpperBound_loop($ar, $num3); // 应该得到 null (或根据业务调整)
echo "数字 $num3 ";
if ($upperBound3 !== null) {
echo "属于上限为 $upperBound3 的范围。\n";
} else {
echo "未能在数组定义的范围内找到合适的上限。\n";
}
$num4 = -5;
$upperBound4 = findRangeUpperBound_loop($ar, $num4); // 应该得到 0
echo "数字 $num4 属于上限为 $upperBound4 的范围。\n";
?>
安全建议
这种方法本身没啥安全风险,就是个纯粹的数值比较。顶多注意下输入的 $arr
是不是个数组,$number
是不是个数字,别因为类型不对搞出些 PHP 的 Warning
或 Notice
。
进阶使用技巧
- 处理未排序数组 :如果你的
$ar
没准儿不是排好序的,那在用这个方法之前,你得先给它排个序:sort($ar, SORT_NUMERIC);
。不然结果肯定不对。 - 空数组或单一元素数组 :上面的代码里简单处理了空数组。如果数组只有一个元素,比如
$ar = [64];
,那么任何小于等于64
的数都应该归到64
这个范围。代码里的if ($number <= $arr[0])
就能处理好。循环for ($i = 1; ...)
则不会执行。 - 数字超出所有范围 :如果
$num
比数组里最大的数还大,比如$ar
最大是224
,$num
是300
。这种情况下,函数返回null
。你可以根据业务需求决定是返回null
、最后一个元素、还是抛出一个异常,告诉调用方“这数太大,没坑给它了!”。
方法二:更高效的二分查找法
如果你的数组 $ar
特别长,几千上万个元素,那挨个儿循环就有点慢了。这时候,二分查找(Binary Search)就能派上用场。
原理和作用
二分查找的前提是数组必须是有序的 。它的核心思想是不断地把查找范围缩小一半。
- 先看数组中间那个元素。
- 如果你的
$num
正好卡在这个中间元素定义的范围,那运气不错,找到了。 - 如果
$num
比中间元素小(或者应该在更左边的区间),那就在数组的左半边继续找。 - 如果
$num
比中间元素大(或者应该在更右边的区间),那就在数组的右半边继续找。 - 重复这个过程,直到找到目标,或者范围小到不能再分了。
我们要找的不是 $num
是否存在于数组中,而是它所属的那个 ($L, $R]
区间的 $R
。换句话说,我们要找到数组中第一个大于等于 $num
的元素。这个元素就是我们要的上限 $R
。
代码示例 (PHP)
<?php
$ar = array(0, 32, 64, 96, 128, 160, 192, 224);
$num = 44;
function findRangeUpperBound_binarySearch($arr, $number) {
if (empty($arr)) {
return null;
}
// 数组必须是有序的。如果不能保证,先排序。
// sort($arr, SORT_NUMERIC); // 如果需要的话
// 特殊情况:如果数字小于等于数组的第一个元素
if ($number <= $arr[0]) {
return $arr[0];
}
// 特殊情况:如果数字大于数组的最后一个元素
if ($number > $arr[count($arr) - 1]) {
return null; // 超出最大范围
}
$low = 0;
$high = count($arr) - 1;
$ans = null; // 用来存储可能的答案
// 目标是找到最小的 $arr[$mid] 使得 $arr[$mid] >= $number
// 并且,如果 $arr[$mid] 是这个答案,那么 $arr[$mid-1] < $number (除非 $mid=0)
// 实际上我们是寻找 $arr[i-1] < $number <= $arr[i] 中的 $arr[i]
while ($low <= $high) {
$mid = $low + floor(($high - $low) / 2);
if ($arr[$mid] >= $number) {
// $arr[$mid] 是一个潜在的上限
// 我们需要确认它是不是最小的那个符合条件的上限
// 同时,我们要确保它前一个元素(如果存在)是小于 $number 的
if ($mid == 0 || $arr[$mid-1] < $number) {
// 如果 $arr[$mid] >= $number
// 并且 ($mid是第一个元素 OR $arr[$mid-1] < $number)
// 这就意味着 $number 在 ($arr[$mid-1], $arr[$mid]] 这个区间里
// (注意 $number <= $arr[mid] 是隐含在 $arr[mid] >= $number 里的)
$ans = $arr[$mid];
break; // 直接找到了,可以跳出
} else {
// $arr[$mid] >= $number 但是 $arr[$mid-1] >= $number 也成立
// 说明真正的上限还在左边
$ans = $arr[$mid]; // 记录一个可能的答案
$high = $mid - 1; // 继续往左边找更小的上限
}
} else { // $arr[$mid] < $number
// 当前中间值太小了,目标在右半边
$low = $mid + 1;
}
}
// 上面的循环找到的是 $arr[i-1] < $number <= $arr[i] 中的 $arr[i]
// 但一种更简洁的二分查找变体是直接找到第一个 >= number 的元素
// 我们来改写一下以找到那个点:
$low = 0;
$high = count($arr) - 1;
$result_candidate_index = -1;
while ($low <= $high) {
$mid_idx = $low + floor(($high - $low) / 2);
if ($arr[$mid_idx] >= $number) {
// $arr[$mid_idx] 是一个候选的上限值
// 因为我们要找第一个 (最小的) 大于等于 $number 的值
// 所以记录下来,然后尝试在左边找有没有更小的
$result_candidate_index = $mid_idx;
$high = $mid_idx - 1;
} else { // $arr[$mid_idx] < $number
// 中间值太小了,目标上限肯定在右边
$low = $mid_idx + 1;
}
}
if ($result_candidate_index != -1) {
// 我们找到了第一个 $arr[index] >= $number
// 现在要确认 $number > $arr[index-1] (如果 index > 0)
// 或者 $number <= $arr[0] (如果 index == 0)
$found_upper_bound = $arr[$result_candidate_index];
if ($result_candidate_index == 0) { // 找到了,且是数组第一个元素
return $found_upper_bound; // e.g. num = -5, ar=[0,32..] -> returns 0
// e.g. num = 0, ar=[0,32..] -> returns 0
} else {
// 如果 $number <= $arr[$result_candidate_index - 1],这意味着 $number 也小于等于前一个区间的上限
// 这不符合我们 $L < $num <= $R 的定义。
// 举例: $ar = [0, 32, 64], $num = 30.
// Binary search for >=30 finds 32 (index 1). $arr[index-1] = $arr[0] = 0. 0 < 30. Correct.
// 举例: $ar = [0, 32, 64], $num = 32.
// Binary search for >=32 finds 32 (index 1). $arr[index-1] = $arr[0] = 0. 0 < 32. Correct.
// 所以,只要 $result_candidate_index 有效,且这个元素符合 >= $number,它就是上限。
// $number > $arr[$result_candidate_index - 1] 这个条件是必须的。
if ($number > $arr[$result_candidate_index - 1]) {
return $found_upper_bound;
} else {
// 这种情况比较tricky,比如 $arr = [0,32,32,64], $num = 32
// "第一个大于等于32的"可能是 index 1 (value 32)。 $arr[0]=0 < 32. OK, return $arr[1]=32.
// 如果数组是严格递增的, $arr[idx-1] < $arr[idx] ,那么 $arr[idx-1] < $number 肯定成立(除非num非常小,已被开头处理)
// 如果 $arr = [0, 10, 20, 20, 30] and $num = 20.
// BS might find $arr[2]=20. $arr[1]=10 < 20. Returns $arr[2]=20. This is one correct range.
// The problem implicitly assumes $ar does not have duplicates for defining distinct ranges.
// If $ar = [0, 32, 64], and $num = 32.
// Our search for first $x >= 32 gives $x=32 at index 1.
// $ar[1-1] = $ar[0] = 0. Is $num > $ar[0]? $32 > 0$, yes. So return $ar[1]=32.
// This is fine. The binary search correctly identified the smallest element that acts as an upper bound.
return $found_upper_bound; // Fallback from more complex logic to simple "first >= val" logic
}
}
}
return null; // Should be covered by initial checks if $number is too large
}
$upperBound = findRangeUpperBound_binarySearch($ar, $num);
if ($upperBound !== null) {
echo "数字 $num 属于上限为 $upperBound 的范围 (二分查找)。\n"; // 输出: 数字 44 属于上限为 64 的范围 (二分查找)。
} else {
echo "数字 $num 未能在数组定义的范围内找到合适的上限 (二分查找)。\n";
}
// 测试其他几个数
$num1 = 32;
$upperBound1 = findRangeUpperBound_binarySearch($ar, $num1);
echo "数字 $num1 属于上限为 $upperBound1 的范围 (二分查找)。\n"; // 32
$num2 = 0;
$upperBound2 = findRangeUpperBound_binarySearch($ar, $num2);
echo "数字 $num2 属于上限为 $upperBound2 的范围 (二分查找)。\n"; // 0
$num3 = 250;
$upperBound3 = findRangeUpperBound_binarySearch($ar, $num3);
echo "数字 $num3 ";
if ($upperBound3 !== null) {
echo "属于上限为 $upperBound3 的范围 (二分查找)。\n";
} else {
echo "未能在数组定义的范围内找到合适的上限 (二分查找)。\n"; // null
}
$num4 = -5;
$upperBound4 = findRangeUpperBound_binarySearch($ar, $num4);
echo "数字 $num4 属于上限为 $upperBound4 的范围 (二分查找)。\n"; // 0
?>
安全建议
跟循环遍历一样,主要还是数据类型校验。二分查找本身是安全的。
进阶使用技巧
- 确保数组有序 :再次强调,二分查找的命根子就是数组有序。如果拿到的数据不能保证这一点,用之前务必
sort()
。 - 处理重复值 :如果范围数组
$ar
里有重复值,比如[0, 32, 32, 64]
,那么32
这个值就可能出现在多个位置。上面的二分查找版本(第二个实现)设计为寻找第一个大于或等于$number
的元素,这对于定义(L, R]
区间的R
是合适的。例如,如果$num = 32
,它会找到第一个32
作为上限,其对应的下限是0
,所以区间是(0, 32]
。 - 理解边界 :二分查找的边界条件 (
$low <= $high
还是$low < $high
,以及$high = $mid - 1
还是$mid
) 写起来容易犯错。多测试几个边界值(比如$num
等于数组元素、小于第一个、大于最后一个)能帮你逮住虫子。我提供的第二个二分查找实现更标准地用于寻找 "lower_bound" (C++ STL 的概念),即第一个不小于目标值的元素。
方法三:巧用 PHP 内置函数组合 (针对 PHP)
PHP 自带了不少好用的数组函数,组合一下也能解决问题。
原理和作用
思路是:
- 筛选出数组
$ar
中所有大于等于$num
的元素。 - 从这些筛选出来的元素中,找到最小的那个。这个最小的元素就是我们要的上限
$R
。 - 需要额外判断一下,筛选出的这个最小元素
$R_found
,它在原数组中的前一个元素$L_candidate
是否真的小于$num
,这才构成$L_candidate < $num <= R_found
。
代码示例 (PHP)
<?php
$ar = array(0, 32, 64, 96, 128, 160, 192, 224);
$num = 44;
function findRangeUpperBound_phpBuiltin($arr, $number) {
if (empty($arr)) {
return null;
}
// 先处理 $number <= $arr[0] 的情况
if ($number <= $arr[0]) {
return $arr[0];
}
// 筛选出所有大于等于 $number 的元素
$greaterOrEqualElements = array_filter($arr, function($value) use ($number) {
return $value >= $number;
});
if (empty($greaterOrEqualElements)) {
// 没有元素大于等于 $number,说明 $number 比数组中所有数都大
return null; // 超出最大范围
}
// 从筛选结果中找到最小的那个,这个就是潜在的上限 $R
$potentialUpperBound = min($greaterOrEqualElements);
// 现在验证这个 $potentialUpperBound 是否真的构成 $L < $num <= $potentialUpperBound
// 我们需要找到 $potentialUpperBound 在原数组 $arr 中的位置,然后看它前一个元素
$upperBoundKey = array_search($potentialUpperBound, $arr);
// array_search 可能返回 false 如果元素不存在 (理论上这里一定存在)
// 或者返回 0 如果它是第一个元素 (已被开头的if处理)
if ($upperBoundKey !== false && $upperBoundKey > 0) {
$lowerBoundCandidate = $arr[$upperBoundKey - 1];
if ($number > $lowerBoundCandidate) {
return $potentialUpperBound;
} else {
// 这种情况例如: $ar = [0, 32, 64], $num = 32.
// $greaterOrEqualElements = [32, 64]. $potentialUpperBound = 32. $upperBoundKey = 1.
// $lowerBoundCandidate = $ar[0] = 0. $number (32) > $lowerBoundCandidate (0). return 32. Correct.
//
// 如果 $arr = [0, 30, 32, 64], $num = 31
// $greaterOrEqualElements = [32, 64]. $potentialUpperBound = 32. $upperBoundKey = 2.
// $lowerBoundCandidate = $ar[1] = 30. $number (31) > $lowerBoundCandidate (30). return 32. Correct.
// 如果 $num 正好等于某个数组元素 $arr[k],且 $arr[k-1] == $arr[k] (有重复)
// 比如 $arr = [0, 32, 32, 64], $num = 32
// $greaterOrEqualElements = [32, 32, 64]. $potentialUpperBound = 32.
// array_search for 32 might return key 1. $arr[0]=0. 32 > 0. Returns 32. (Correct: range (0,32])
// This logic generally holds.
return $potentialUpperBound;
}
} elseif ($upperBoundKey === 0) { // $potentialUpperBound 是数组第一个元素
return $potentialUpperBound; // 这已经被 $number <= $arr[0] cover了,但作为完整性。
}
return null; // 理论上不应该到这里如果之前的逻辑都对
}
$upperBound = findRangeUpperBound_phpBuiltin($ar, $num);
if ($upperBound !== null) {
echo "数字 $num 属于上限为 $upperBound 的范围 (内置函数)。\n"; // 输出: 数字 44 属于上限为 64 的范围 (内置函数)。
} else {
echo "数字 $num 未能在数组定义的范围内找到合适的上限 (内置函数)。\n";
}
// 测试其他几个数
$num1 = 32;
$upperBound1 = findRangeUpperBound_phpBuiltin($ar, $num1);
echo "数字 $num1 属于上限为 $upperBound1 的范围 (内置函数)。\n"; // 32
$num2 = 0;
$upperBound2 = findRangeUpperBound_phpBuiltin($ar, $num2);
echo "数字 $num2 属于上限为 $upperBound2 的范围 (内置函数)。\n"; // 0
$num3 = 250;
$upperBound3 = findRangeUpperBound_phpBuiltin($ar, $num3);
echo "数字 $num3 ";
if ($upperBound3 !== null) {
echo "属于上限为 $upperBound3 的范围 (内置函数)。\n";
} else {
echo "未能在数组定义的范围内找到合适的上限 (内置函数)。\n"; // null
}
$num4 = -5;
$upperBound4 = findRangeUpperBound_phpBuiltin($ar, $num4);
echo "数字 $num4 属于上限为 $upperBound4 的范围 (内置函数)。\n"; // 0
?>
安全建议
还是老样子,注意输入类型。array_filter
和 min
对非数字或非数组输入会报警。
进阶使用技巧
- 性能考量 :
array_filter
会遍历整个数组创建一个新数组,然后min
再遍历这个新数组。如果原数组非常大,这个性能可能不如二分查找。但对于中小型数组,代码写起来简洁明了。 - 数组键的保留 :
array_filter
默认会保留原数组的键。如果你后面需要用键做些文章,这是个优点。 - 替代方案
array_reduce
? :也可以用array_reduce
实现类似逻辑,但可能会更绕一点,可读性上不一定有优势。
选哪个方案?小结一下
到底用哪种方法,得看你的具体情况:
- 数组不大,代码想简单点? —— 用 循环遍历法 。好懂,不容易错。
- 数组巨大,追求极致性能? —— 用 二分查找法 。前提是数组有序,稍微复杂点,但快。
- 写 PHP,想用现成的轮子,数组大小适中? —— 用 PHP 内置函数组合 。代码相对简洁,但也得注意性能。
总的来说,没有万能钥匙,挑个顺手的、能解决你问题的就行。关键是理解每种方法背后的逻辑和它的适用场景。搞定了这些,下次再碰到类似问题,你就能胸有成竹地选出最合适的家伙什儿了。