【问题标题】:Find a matching or closest value in an array在数组中查找匹配或最接近的值
【发布时间】:2011-07-24 19:24:55
【问题描述】:

对于给定的目标值,如何搜索并找到数组中最接近的值?

假设我有这个示例数组:

array(0, 5, 10, 11, 12, 20)

例如,当我使用目标值 0 进行搜索时,函数应返回 0;当我用 3 搜索时,它会返回 5;当我用 14 搜索时,它会返回 12。

【问题讨论】:

    标签: php arrays search sorting


    【解决方案1】:

    这与Mario's answer 的方法相同,但我使用array_search() 和min() 而不是排序。性能是一样的,所以只是归结为偏好问题。

    function findClosest(array $values, $match)
    {
        $map = [];
        foreach ($values as $v) {
            $map[$v] = abs($match - $v);
        }
        return array_search(min($map), $map);
    }
    

    【讨论】:

      【解决方案2】:

      一种特殊的惰性方法是让 PHP 按与搜索数字的距离对数组进行排序:

      $num = 3;    
      $array = array(0, 5, 10, 11, 12, 20);
      $smallest = [];
      
      foreach ($array as $i) {
          $smallest[$i] = abs($i - $num);
      }
      asort($smallest);
      print key($smallest);
      

      【讨论】:

        【解决方案3】:

        我将提供一个较晚的答案,通过维护两个临时变量并实​​现提前返回来努力避免不必要的迭代和过多的函数调用。

        一个优雅的解决方案不应该要求大于n的时间复杂度——换句话说,大的O应该是O(n) 而小o应该是o(1)。大 O 只会通过预先对 haystack 进行排序,然后再次迭代 haystack 变得更糟。为了达到o(1),你需要在遇到相同匹配时提前返回——无需进一步搜索。

        我的 sn-p 将任意返回距离最短的第一个出现的值(以防多个值具有相同的距离)。 OP 未指定任何其他行为。

        与其他一些答案相比,一个微不足道的性能改进是 abs() 是循环内的唯一函数调用,每次迭代最多调用 1 次。以前的一些答案会重新计算当前值的距离以及每次迭代中当前最接近的匹配 - 这比必要的工作量更大。

        代码:(Demo)

        $haystack = [-6, 0, 5, 10, 11, 12, 20];
        
        $needles = [0, 3, 14, -3];
        
        function getNearest($needle, $haystack) {
            if (!$haystack) {
                throw new Exception('empty haystack');
            }
            $bestDistance = PHP_INT_MAX;
            foreach ($haystack as $value) {
                if ($value === $needle) {
                    return $needle;
                }
                $distance = abs($value - $needle);
                if ($distance < $bestDistance) {
                    $bestDistance = $distance;
                    $keep = $value;
                }
            }
            return $keep ?? $value; // coalesce to silence potential IDE complaint
        }
        
        foreach ($needles as $needle) { // each test case
            echo "$needle -> " . getNearest($needle, $haystack) . "\n";
        }
        

        输出:

        0 -> 0
        3 -> 5
        14 -> 12
        -3 -> -6
        

        【讨论】:

          【解决方案4】:
          function closestnumber($number, $candidates) {
              $last = null;
              foreach ($candidates as $cand) {
                  if ($cand < $number) {
                      $last = $cand;
                  } elseif ($cand == $number) {
                     return $number;
                  } elseif ($cand > $number) {
                     return $last;
                  }
              }
              return $last;
          }
          

          【讨论】:

          • 这是否需要对数组值进行排序?
          • 我认为是这样,并且该函数由于某种原因没有通过我的单元测试(无法断言 0 匹配预期的 5。):(
          【解决方案5】:

          二分查找最接近的值(数组必须排序):

          function findClosest($sortedArr, $val)
          {
              $low = 0;
              $high = count($sortedArr) - 1;
              while ($low <= $high) {
                  if ($high - $low <= 1) {
                      if (abs($sortedArr[$low] - $val) < abs($sortedArr[$high] - $val)) {
                          return $sortedArr[$low];
                      } else {
                          return $sortedArr[$high];
                      }
                  }
          
                  $mid = (int)(($high + $low) / 2);
                  if ($val < $sortedArr[$mid]) {
                      $high = $mid;
                  } else {
                      $low = $mid;
                  }
              }
          
              // Empty array
              return false;
          }
          

          【讨论】:

            【解决方案6】:

            我发现的基于Piyush Dholariya's answer的最佳方法:

            $array = [4, 9, 15, 6, 2];
            $goal = 7;
            
            $closest = array_reduce($array, function($carry, $item) use($goal) {
                return (abs($item - $goal) < abs($carry - $goal) ? $item : $carry);
            }, reset($array)); // Returns 6
            

            【讨论】:

              【解决方案7】:

              例如,考虑到输入数组按升序排列asort(),使用dichotomic search 进行搜索会快得多。

              这是我用来在按 DateTime 对象排序的Iterable 事件列表中插入新事件的一些代码的快速而肮脏的改编……

              因此,此代码将返回左侧最近的点(之前/更小)。

              如果您想找到数学上最近的点:考虑比较搜索值与返回值的距离以及返回值右侧(下一个)的点(如果存在)。

              function dichotomicSearch($search, $haystack, $position=false)
              {
                  // Set a cursor between two values
                  if($position === false)
                  {    $position=(object)  array(
                          'min' => 0,
                          'cur' => round(count($haystack)/2, 0, PHP_ROUND_HALF_ODD),
                          'max' => count($haystack)
                          );
                  }
              
                  // Return insertion point (to push using array_splice something at the right spot in a sorted array)
                  if(is_numeric($position)){return $position;}
              
                  // Return the index of the value when found
                  if($search == $haystack[$position->cur]){return $position->cur;}
              
                  // Searched value is smaller (go left)
                  if($search <= $haystack[$position->cur])
                  {
                      // Not found (closest value would be $position->min || $position->min+1)
                      if($position->cur == $position->min){return $position->min;}
              
                      // Resetting the interval from [min,max[ to [min,cur[
                      $position->max=$position->cur;
                      // Resetting cursor to the new middle of the interval
                      $position->cur=round($position->cur/2, 0, PHP_ROUND_HALF_DOWN);
                      return dichotomicSearch($search, $haystack, $position);
                  }
              
                  // Search value is greater (go right)
                      // Not found (closest value would be $position->max-1 || $position->max)
                      if($position->cur < $position->min or $position->cur >= $position->max){return $position->max;}
                      // Resetting the interval from [min,max[ to [cur,max[
                      $position->min = $position->cur;
                      // Resetting cursor to the new middle of the interval
                      $position->cur = $position->min + round(($position->max-$position->min)/2, 0, PHP_ROUND_HALF_UP);
                      if($position->cur >= $position->max){return $position->max;}
                      return dichotomicSearch($search, $haystack, $position);        
              }
              

              【讨论】:

                【解决方案8】:

                这是我为排序的大数组

                编写的高性能函数

                经过测试,对于一个包含 20000 个元素的数组,主循环只需要大约 20 次迭代。

                请注意数组必须排序(升序)!

                define('ARRAY_NEAREST_DEFAULT',    0);
                define('ARRAY_NEAREST_LOWER',      1);
                define('ARRAY_NEAREST_HIGHER',     2);
                
                /**
                 * Finds nearest value in numeric array. Can be used in loops.
                 * Array needs to be non-assocative and sorted.
                 * 
                 * @param array $array
                 * @param int $value
                 * @param int $method ARRAY_NEAREST_DEFAULT|ARRAY_NEAREST_LOWER|ARRAY_NEAREST_HIGHER
                 * @return int
                 */
                function array_numeric_sorted_nearest($array, $value, $method = ARRAY_NEAREST_DEFAULT) {    
                    $count = count($array);
                
                    if($count == 0) {
                        return null;
                    }    
                
                    $div_step               = 2;    
                    $index                  = ceil($count / $div_step);    
                    $best_index             = null;
                    $best_score             = null;
                    $direction              = null;    
                    $indexes_checked        = Array();
                
                    while(true) {        
                        if(isset($indexes_checked[$index])) {
                            break ;
                        }
                
                        $curr_key = $array[$index];
                        if($curr_key === null) {
                            break ;
                        }
                
                        $indexes_checked[$index] = true;
                
                        // perfect match, nothing else to do
                        if($curr_key == $value) {
                            return $curr_key;
                        }
                
                        $prev_key = $array[$index - 1];
                        $next_key = $array[$index + 1];
                
                        switch($method) {
                            default:
                            case ARRAY_NEAREST_DEFAULT:
                                $curr_score = abs($curr_key - $value);
                
                                $prev_score = $prev_key !== null ? abs($prev_key - $value) : null;
                                $next_score = $next_key !== null ? abs($next_key - $value) : null;
                
                                if($prev_score === null) {
                                    $direction = 1;                    
                                }else if ($next_score === null) {
                                    break 2;
                                }else{                    
                                    $direction = $next_score < $prev_score ? 1 : -1;                    
                                }
                                break;
                            case ARRAY_NEAREST_LOWER:
                                $curr_score = $curr_key - $value;
                                if($curr_score > 0) {
                                    $curr_score = null;
                                }else{
                                    $curr_score = abs($curr_score);
                                }
                
                                if($curr_score === null) {
                                    $direction = -1;
                                }else{
                                    $direction = 1;
                                }                
                                break;
                            case ARRAY_NEAREST_HIGHER:
                                $curr_score = $curr_key - $value;
                                if($curr_score < 0) {
                                    $curr_score = null;
                                }
                
                                if($curr_score === null) {
                                    $direction = 1;
                                }else{
                                    $direction = -1;
                                }  
                                break;
                        }
                
                        if(($curr_score !== null) && ($curr_score < $best_score) || ($best_score === null)) {
                            $best_index = $index;
                            $best_score = $curr_score;
                        }
                
                        $div_step *= 2;
                        $index += $direction * ceil($count / $div_step);
                    }
                
                    return $array[$best_index];
                }
                
                • ARRAY_NEAREST_DEFAULT 找到最近的元素
                • ARRAY_NEAREST_LOWER 找到最近的元素,即 LOWER
                • ARRAY_NEAREST_HIGHER 找到最近的更高元素

                用法:

                $test = Array(5,2,8,3,9,12,20,...,52100,52460,62000);
                
                // sort an array and use array_numeric_sorted_nearest
                // for multiple searches. 
                // for every iteration it start from half of chunk where
                // first chunk is whole array
                // function doesn't work with unosrted arrays, and it's much
                // faster than other solutions here for sorted arrays
                
                sort($test);
                $nearest = array_numeric_sorted_nearest($test, 8256);
                $nearest = array_numeric_sorted_nearest($test, 3433);
                $nearest = array_numeric_sorted_nearest($test, 1100);
                $nearest = array_numeric_sorted_nearest($test, 700);
                

                【讨论】:

                • @MjrKusanagi array_numeric_sorted_nearest 是主要功能,但使用前需要对数组进行排序
                • 这个函数有很多bug,在运行各种测试用例时,它会因索引上的一个问题而关闭。 (示例测试数据:$array = [1, 5, 10]; $value = 2 或 $array = [0.34, 0.5, 0.9, 1.9, 18.6]; $value = 0.89)
                • @calumbrodie 您的测试用例在这里似乎运行良好!
                • 请注意,此函数在 PHP7 上无法开箱即用,当缺少数组索引时会引发错误。您必须添加适当的 isset 检查
                • 我在this gist中使这段代码与PHP7兼容
                【解决方案9】:

                要在对象数组中搜索最接近的值,您可以使用来自Tim Cooper's answer 的改编代码。

                <?php
                // create array of ten objects with random values
                $images = array();
                for ($i = 0; $i < 10; $i++)
                    $images[ $i ] = (object)array(
                        'width' => rand(100, 1000)
                    );
                
                // print array
                print_r($images);
                
                // adapted function from Tim Copper's solution
                // https://stackoverflow.com/a/5464961/496176
                function closest($array, $member, $number) {
                    $arr = array();
                    foreach ($array as $key => $value)
                        $arr[$key] = $value->$member;
                    $closest = null;
                    foreach ($arr as $item)
                        if ($closest === null || abs($number - $closest) > abs($item - $number))
                            $closest = $item;
                    $key = array_search($closest, $arr);
                    return $array[$key];
                }
                
                // object needed
                $needed_object = closest($images, 'width', 320);
                
                // print result
                print_r($needed_object);
                ?>
                

                【讨论】:

                  【解决方案10】:

                  将您要搜索的数字作为第一个参数传递,将数字数组传递给第二个参数:

                  function getClosest($search, $arr) {
                     $closest = null;
                     foreach ($arr as $item) {
                        if ($closest === null || abs($search - $closest) > abs($item - $search)) {
                           $closest = $item;
                        }
                     }
                     return $closest;
                  }
                  

                  【讨论】:

                  • 这里要添加的一件事是,如果您想要低于搜索值的最接近值,则需要按升序对数组进行排序,否则降序以获得高于搜索值的值。
                  • 很抱歉加入这个旧答案,但如果使用负数,我似乎遇到了问题,有什么建议吗?
                  • 我认为应该是abs($search - $closest) &gt;= abs($item - $search) 将数字四舍五入到最接近的值。
                  • 有人知道 $closest === null 是干什么用的吗?
                  • @RobertSinclair $closest === null 用于第一次迭代,在最接近设置为任何值之前
                  【解决方案11】:

                  Tim's implementation 大部分时间都会删掉它。尽管如此,为了性能谨慎,您可以在迭代之前对列表进行排序,并在下一个差异大于上一个时中断搜索。

                  <?php
                  function getIndexOfClosestValue ($needle, $haystack) {
                      if (count($haystack) === 1) {
                          return $haystack[0];
                      }
                  
                      sort($haystack);
                  
                      $closest_value_index = 0;
                      $last_closest_value_index = null;
                  
                      foreach ($haystack as $i => $item) {
                          if (abs($needle - $haystack[$closest_value_index]) > abs($item - $needle)) {
                              $closest_value_index = $i;
                          }
                  
                          if ($closest_value_index === $last_closest_value_index) {
                              break;
                          }
                      }
                      return $closest_value_index;
                  }
                  
                  function getClosestValue ($needle, $haystack) {
                      return $haystack[getIndexOfClosestValue($needle, $haystack)];
                  }
                  
                  // Test
                  
                  $needles = [0, 2, 3, 4, 5, 11, 19, 20];
                  $haystack = [0, 5, 10, 11, 12, 20];
                  $expectation = [0, 0, 1, 1, 1, 3, 5, 5];
                  
                  foreach ($needles as $i => $needle) {
                      var_dump( getIndexOfClosestValue($needle, $haystack) === $expectation[$i] );
                  }
                  

                  【讨论】:

                    【解决方案12】:

                    您可以简单地使用array_search,它会返回一个键,如果在数组中找到许多您的搜索实例,它将返回找到的第一个。

                    Quote from PHP:

                    如果多次在 haystack 中找到 needle,则返回第一个匹配的键。要返回所有匹配值的键,请使用 array_keys() 和可选的 search_value 参数。

                    示例用法:

                    if(false !== ($index = array_search(12,array(0, 5, 10, 11, 12, 20))))
                    {
                        echo $index; //5
                    }
                    

                    更新:

                    function findNearest($number,$Array)
                    {
                        //First check if we have an exact number
                        if(false !== ($exact = array_search($number,$Array)))
                        {
                             return $Array[$exact];
                        }
                    
                        //Sort the array
                        sort($Array);
                    
                       //make sure our search is greater then the smallest value
                       if ($number < $Array[0] ) 
                       { 
                           return $Array[0];
                       }
                    
                        $closest = $Array[0]; //Set the closest to the lowest number to start
                    
                        foreach($Array as $value)
                        {
                            if(abs($number - $closest) > abs($value - $number))
                            {
                                $closest = $value;
                            }
                        }
                    
                        return $closest;
                    }
                    

                    【讨论】:

                    • 我看不出这有什么帮助,因为搜索 2 不会像想要的那样返回 0。
                    • 最初的答案是错误的,它只找到完全匹配。二是迭代次数过多。
                    【解决方案13】:
                    <?php
                    $arr = array(0, 5, 10, 11, 12, 20);
                    
                    function getNearest($arr,$var){
                        usort($arr, function($a,$b) use ($var){
                            return  abs($a - $var) - abs($b - $var);
                        });
                        return array_shift($arr);
                    }
                    ?>
                    

                    【讨论】:

                    • 可能有效,但它需要比 TimCooper 更多的处理。
                    • 确实有效,但事实上,我认为蒂姆的需要更少的周期。
                    猜你喜欢
                    • 1970-01-01
                    • 2019-11-02
                    • 2020-01-30
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2021-12-26
                    • 1970-01-01
                    • 2021-03-11
                    相关资源
                    最近更新 更多