迭代数组时如何检测循环?

时间:2012-02-14 21:47:04

标签: php arrays

$arr = array(
  'a'          => 1,
  'b'          => 15,
  'c'          => 0,
);

$arr['c'] = &$arr;

print_r($arr); // <-- CYCLE

你知道我怎么能检测出我的数组值是否以某种方式指向现有元素,或导致无限循环?

4 个答案:

答案 0 :(得分:3)

使用记忆,Luke 。当您的迭代器作为另一个元素的元素遇到数组时,只需将它的引用/ id存储为类似于set或list(或其他合适的容器,例如数组)。所以你要记住你已处理过的数组,然后在下次见面时忽略它或停止循环。

<?php
function get_id(&$array) {
  return crc32(serialize($array)); 
}

# This will not work as expected. I leave it here just for future versions.
function get_id_alt(&$array) {
  $obj = (object) $array;
  return spl_object_hash($obj);
}

function iterate(array $input, &$reached=array(), $id_func='get_id') {
  // this is an unseen array, memorize it
  array_push($reached, $id_func($input));

  foreach($input as $key=>$item) {
    if (is_array($item)) {        // This is an array
      if (in_array($id_func($item), $reached)) {
        // We found an array that have been processed before. What will you do?
      } else {
        // Recurse!
        iterate($item, $reached);
      }
    } else {
      // It is not traversable value, do what you want here
    }
  }
}

PS:我使用spl_object_hash作为ID函数,你可以使用另一个,如果喜欢(但我不知道其他人可以识别相同的对象,就像这个一样)。 / p>

UPD:自PHP 5.3.10起,使用spl_object_hash无法给出正确的结果:它将任何数组视为同一个对象,无论其内容如何。但是使用类似%hash_fn%(serialize($array))的smth效果很好(谨防性能下降!)。

答案 1 :(得分:3)

如果print_r告诉您递归,为什么不使用它? :)

// Set up bad array
$arr = array(
  'a'          => 1,
  'b'          => 15,
  'c'          => 0,
);
$arr['c'] = &$arr;

// Check print_r
if(strpos(print_r($a,1),'*RECURSION*') !== false) {
  echo "Houston, we got a problem!\n";
}

编辑:正如@Vyktor所述,这并不适用于所有情况,并且可能会产生错误的正面,但serialize()也会提供递归指示。它为递归提供R。因此,如果R的输出中的任何字符串外都有serialize(),我们会检查:

<?php
// Set up bad array
$arr = array(
  'a'          => 1,
  'b'          => 15,
  'c'          => 0,
);
$arr['c'] = &$arr;

$str = serialize($arr);   // Serialize it
$len = strlen($str);      // Get the length

// Simple serialize "parser"
$state = 0;
$recursion_found = false;
for($i=0;$i<$len;$i++) {
  $byte = $str[$i];
  if($byte == "\"" && $state == 0) {
    $state = 1; // in string!
  } else if($byte == "\"" && $state == 1) {
    // Check if the " is escaped
    if($str[$i-1] != "\\") {
      $state = 0; // not in string
    }
  } else if($byte == "R" && $state == 0) { // any R that is not in a string
    $recursion_found = true;
  }
}

if($recursion_found) {
  echo "There is recursion!\n";
}

答案 2 :(得分:3)

(我刚发布这个作为@Cheery提到的Is there a way to detect circular arrays in pure PHP?问题的答案,但我可能会根据提问者的需要更新答案。)

下面的isRecursiveArray(array)方法检测循环/递归数组。它通过临时将包含已知对象引用的元素添加到数组末尾来跟踪已访问的数组。

function removeLastElementIfSame(array & $array, $reference) {
    if(end($array) === $reference) {
        unset($array[key($array)]);
    }
}

function isRecursiveArrayIteration(array & $array, $reference) {
    $last_element   = end($array);
    if($reference === $last_element) {
        return true;
    }
    $array[]    = $reference;

    foreach($array as &$element) {
        if(is_array($element)) {
            if(isRecursiveArrayIteration($element, $reference)) {
                removeLastElementIfSame($array, $reference);
                return true;
            }
        }
    }

    removeLastElementIfSame($array, $reference);

    return false;
}

function isRecursiveArray(array $array) {
    $some_reference = new stdclass();
    return isRecursiveArrayIteration($array, $some_reference);
}



$array      = array('a','b','c');
var_dump(isRecursiveArray($array));
print_r($array);



$array      = array('a','b','c');
$array[]    = $array;
var_dump(isRecursiveArray($array));
print_r($array);



$array      = array('a','b','c');
$array[]    = &$array;
var_dump(isRecursiveArray($array));
print_r($array);



$array      = array('a','b','c');
$array[]    = &$array;
$array      = array($array);
var_dump(isRecursiveArray($array));
print_r($array);

答案 3 :(得分:0)

数组和对象的衍生工作(两者):

function isRecursiveArrayObjectTest(&$object, $marker)
{
    if ( is_array($object) ) {
        // Move the array pointer to the end and test if we encounter the marker (if so, its recursive)
        $last = end($object);
        if ($marker === $last) {
            return true;
        }
        // Add a marker to the end of the array to test for recursion (removed upon exit)
        $object[] = $marker;
    } elseif ( is_object($object) ) {
        // Treat objects differently
        if ( $marker === $object->__testRecursion ) {
            return true;
        }
        // Add a marker using a member that is unlikely to be defined (removed upon exit)
        $object->__testRecursion = $marker;
    } else {
        return false;
    }

    $isRecur = false;

    // Loop over the elements of the array, recursively testing each one (if it is an array|object)
    foreach($object as &$element) {
        if(is_array($element) || (is_object($element) && $element !== $marker)) {
            if(isRecursiveArrayObjectTest($element, $marker)) {
                $isRecur = true;
                break;
            }
        }
    }

    // remove marker before we leave
    if (is_array($object) && end($object) === $marker) {
        unset($object[key($object)]);
    } elseif ( is_object($object) && isset($object->__testRecursion) ) {
        unset($object->__testRecursion);
    }

    return $isRecur;
}

function isRecursiveArrayObject($object)
{
    // Create a 'marker' to detect where we have visited to determine recursion
    $marker = new stdClass();
    return isRecursiveArrayObjectTest($object, $marker);
}