深入浅出PHP数组

数组的表达方式

构造数组的几种方法

可以用 array() 语言结构来新建一个数组。它接受任意数量用逗号分隔的 键(key) => 值(value)对。

自 5.4 起可以使用短数组定义语法,用 [] 替代 array()。

数组key和value的限制条件

key 可以是 integer 或者 string。value 可以是任意类型。

此外 key 会有如下的强制转换:

如果在数组定义中多个单元都使用了同一个键名,则只使用了最后一个,之前的都被覆盖了。

PHP Predefined Interfaces and Classes 预订义接口

Traversable(遍历)接口

检测一个类是否可以使用 foreach 进行遍历的接口。 无法被单独实现的基本抽象接口。相反它必须由 IteratorAggregate 或 Iterator 接口实现。 这个接口没有任何方法,它的作用仅仅是作为所有可遍历类的基本接口。 PHP 已经提供了一些用于日常任务的迭代器。 详细列表参见 SPL 迭代器

IteratorAggregate(聚合式迭代器)接口

创建外部迭代器的接口。

Iterator(迭代器)接口

可在内部迭代自己的外部迭代器或类的接口。

ArrayAccess(数组式访问)接口

提供像访问数组一样访问对象的能力的接口。

序列化接口

自定义序列化的接口。实现此接口的类将不再支持 __sleep() 和 __wakeup()。不论何时,只要有实例需要被序列化,serialize 方法都将被调用。它将不会调用 __destruct() 或有其他影响,除非程序化地调用此方法。当数据被反序列化时,类将被感知并且调用合适的 unserialize() 方法而不是调用 __construct()。如果需要执行标准的构造器,你应该在这个方法中进行处理。

Closure 类

用于代表 匿名函数 的类. 匿名函数(在 PHP 5.3 中被引入)会产生这个类型的对象。在过去,这个类被认为是一个实现细节,但现在可以依赖它做一些事情。自 PHP 5.4 起,这个类带有一些方法,允许在匿名函数创建后对其进行更多的控制。 除了此处列出的方法,还有一个 __invoke 方法。这是为了与其他实现了 __invoke()魔术方法 的对象保持一致性,但调用匿名函数的过程与它无关。

实现PHP数组式访问接口

class Test implements Iterator{
    private $item = array('a' => 4, 1, 2, 3);

    public function rewind() {
        reset($this->item);
    }

    public function current() {
        return current($this->item);
    }

    public function key() {
        return key($this->item);
    }

    public function next() {
        return next($this->item);
    }

    public function valid() {
        return($this->current() !== false);
    }
 }

 $t = new Test;
 foreach($t as $k => $v){
     var_dump($k, $v);
 }

PHP目前有哪些预订义数组,各是什么用途?

$GLOBALS

引用全局作用域中可用的全部变量

$_SERVER

$HTTP_SERVER_VARS [已弃用] — 服务器和执行环境信息
$_SERVER 是一个包含了诸如头信息(header)、路径(path)、以及脚本位置(script locations)等等信息的数组。这个数组中的项目由 Web 服务器创建。不能保证每个服务器都提供全部项目;服务器可能会忽略一些,或者提供一些没有在这里列举出来的项目。这也就意味着大量的此类变量都会在» CGI 1.1 规范中说明,所以应该仔细研究一下。

$_GET

$_GET – $HTTP_GET_VARS [已弃用] — HTTP GET 变量

$_POST

$_POST – $HTTP_POST_VARS [已弃用] — HTTP POST 变量

$_FILES

HTTP 文件上传变量

HTTP Cookies

$_SESSION

Session 变量

$_REQUEST

默认情况下包含了 $_GET,$_POST 和 $_COOKIE 的数组。

$_ENV

$_ENV – $HTTP_ENV_VARS [已弃用] — 环境变量 通过环境方式传递给当前脚本的变量的数组。

这些变量被从 PHP 解析器的运行环境导入到 PHP 的全局命名空间。很多是由支持 PHP 运行的 Shell 提供的,并且不同的系统很可能运行着不同种类的 Shell,所以不可能有一份确定的列表。请查看你的 Shell 文档来获取定义的环境变量列表。
其他环境变量包含了 CGI 变量,而不管 PHP 是以服务器模块还是 CGI 处理器的方式运行。
$HTTP_ENV_VARS 包含相同的信息,但它不是一个超全局变量。 (注意 $HTTP_ENV_VARS 和 $_ENV 是不同的变量,PHP 处理它们的方式不同)

$_EVN是什么,如何让$_EVN有效?


数组类型转换

其它数据类型转换为数组有什么规律?

对于任意 integer,float,string,boolean 和 resource 类型,如果将一个值转换为数组,将得到一个仅有一个元素的数组,其下标为 0,该元素即为此标量的值。换句话说,(array)$scalarValue 与 array($scalarValue) 完全一样。
如果一个 object 类型转换为 array,则结果为一个数组,其单元为该对象的属性。键名将为成员变量名,不过有几点例外:整数属性不可访问;私有变量前会加上类名作前缀;保护变量前会加上一个 ‘*’ 做前缀。这些前缀的前后都各有一个 NULL 字符。这会导致一些不可预知的行为
将 NULL 转换为 array 会得到一个空的数组。

有哪些方法可以实现数组的串行化?

serialize unserialize
json_encode json_decode
implode explode
CJSON


数组的遍历

遍历数组有多少种方法,如何选用这些方法?

array_walkarray_map 有什么不同?

array array_map ( callable $callback , array $arr1 [, array $… ] )

array_map() 返回一个数组,该数组包含了 arr1 中的所有单元经过 callback 作用过之后的单元。callback 接受的参数数目应该和传递给 array_map() 函数的数组数目一致。

bool array_walk ( array &$array , callable $funcname [, mixed $userdata = NULL ] )

array_walk() 不会受到 array 内部数组指针的影响。array_walk() 会遍历整个数组而不管指针的位置。将用户自定义函数 funcname 应用到 array 数组中的每个单元。

forforeach 谁更快呢?为什么?

有很多关于PHP数组的遍历效率的误解。我打算尽量揭开它的神秘面纱。

测试分为两个部分:

  1. 读:只遍历数组,不修改数组
  2. 写:遍历数组,并修改数组

测试版本:PHP 5.5.9-1ubuntu4.5 (cli) (built: Oct 29 2014 11:59:10)
测试数组:$array = range(1, 1000000);

测试读

$start = microtime(true);
foreach($array as $value){$value;};
printf('%.6f' . PHP_EOL, microtime(true) - $start);

$start = microtime(true);
$count = count($array);
for($i=0;$i<$count;$i++){$array[$i];}
printf('%.6f' . PHP_EOL, microtime(true) - $start);

常见错误:

  1. 比较两个空循环:foreach($array as $value); for($i=0;$i<$count;$i++); for-loop根本没有遍历数组,因此结果没有多少参考价值。
  2. for($i=0;$i<count($array);$i++) PHP每次循环都会重新计算count($array)的值,虽然时间复杂度为1,但仍会对结果产生影响。

结果分析:

foreach   for
0.040372   0.063877

结果分析: PHP数组内部指针(Bucket *pInternalPointer)指向当前数组元素(Bucket),当前数组元素保存着指向下一元素的指针(struct bucket *pListNext),foreach能用到这个结构快速遍历整个数组。 for-loop的开销在于哈希算法,以及哈希冲突时遍历整个Bucket(PHP中的哈希表是使用拉链法来解决冲突的,具体点讲就是使用链表来存储哈希到同一个槽位的数据, Zend为了保存数据之间的关系使用了双向列表来链接元素)

测试写

$start = microtime(true);
foreach($array as $key => $value) {
	$array[$key] += 1;
}
printf('%.6f' . PHP_EOL, microtime(true) - $start);

$start = microtime(true);
$count = count($array);
for($i=0;$i<$count;$i++) {
	$array[$i] += 1;
}
printf('%.6f' . PHP_EOL, microtime(true) - $start);

常见错误:

  1. 只测试读性能,忽略写性能
  2. 测试写的代码本身有个问题,待会再说

结果分析:

foreach   for
0.299544   0.077412

foreach循环性能下降很快,主要性能消耗在执行变量分离(PHP的COW机制(copy on write 写时复制),遍历时$value and $array[$key]共同指向一个Bucket, 所以性能损耗很小,但当其中一个值改变时,则需要执行变量分离)

*这也是测试代码的问题所在,foreach不仅执行了哈希查找,还进行了变量分离,有个办法可以避免这个问题,但很少人用:

foreach($array as &$value) {
    $value += 1;
}
unset($value);

PHP数组在底实现上是什么数据结构?

PHP中的哈希表实现在Zend/zend_hash.c中, PHP使用如下两个数据结构来实现哈希表,HashTable结构体用于保存整个哈希表需要的基本信息, 而Bucket结构体用于保存具体的数据内容

typedef struct _hashtable { 
    uint nTableSize;        // hash Bucket的大小,最小为8,以2x增长。
    uint nTableMask;        // nTableSize-1 , 索引取值的优化
    uint nNumOfElements;    // hash Bucket中当前存在的元素个数,count()函数会直接返回此值 
    ulong nNextFreeElement; // 下一个数字索引的位置
    Bucket *pInternalPointer;   // 当前遍历的指针(foreach比for快的原因之一)
    Bucket *pListHead;          // 存储数组头元素指针
    Bucket *pListTail;          // 存储数组尾元素指针
    Bucket **arBuckets;         // 存储hash数组
    dtor_func_t pDestructor;    // 在删除元素时执行的回调函数,用于资源的释放
    zend_bool persistent;       //指出了Bucket内存分配的方式。如果persisient为TRUE,则使用操作系统本身的内存分配函数为Bucket分配内存,否则使用PHP的内存分配函数。
    unsigned char nApplyCount; // 标记当前hash Bucket被递归访问的次数(防止多次递归)
    zend_bool bApplyProtection;// 标记当前hash桶允许不允许多次访问,不允许时,最多只能递归3次
#if ZEND_DEBUG
    int inconsistent;
#endif
} HashTable;

数据容器:槽位

typedef struct bucket {
    ulong h;            // 对char *key进行hash后的值,或者是用户指定的数字索引值
    uint nKeyLength;    // hash关键字的长度,如果数组索引为数字,此值为0
    void *pData;        // 指向value,一般是用户数据的副本,如果是指针数据,则指向pDataPtr
    void *pDataPtr;     //如果是指针数据,此值会指向真正的value,同时上面pData会指向此值
    struct bucket *pListNext;   // 整个hash表的下一元素
    struct bucket *pListLast;   // 整个哈希表该元素的上一个元素
    struct bucket *pNext;       // 存放在同一个hash Bucket内的下一个元素
    struct bucket *pLast;       // 同一个哈希bucket的上一个元素
    // 保存当前值所对于的key字符串,这个字段只能定义在最后,实现变长结构体
    char arKey[1];              
} Bucket;

如何用指针函数操作PHP数组?

reset($array);
while(list($key, $value) = each($array));

$value = reset($array);
do {
} while($value = next($array));

使用foreach时应该注意什么问题?

  1. foreach 依赖内部数组指针,在循环中修改其值将可能导致意外的行为。
  2. 当 foreach 开始执行时,数组内部的指针会自动指向第一个单元。这意味着不需要在 foreach 循环之前调用 reset()。
  3. 数组最后一个元素的 $value 引用在 foreach 循环之后仍会保留。建议使用 unset() 来将其销毁。
  4. foreach 不支持用“@”来抑制错误信息的能力。
  5. foreach 结束循环时, 数组内部指针会指向下一个单元。

思考,会输出什么结果?为什么?

1.

$array = array(1,2,3,4);
echo next($array);

foreach ($array as $value) {
  if (!next($array)) reset($array);
}

echo current($array);

2.

$array = array(1,2,3,4);
foreach($array as $key => $value) {
	$array[$key] = &$value;	
}

3.

$array = array(1,2,3,4);
foreach($array as $key => $value) {
	$value = &$array[$key];	
}

数组元素查找

如何判断数组中key是否存在?

  1. array_key_exists 检查给定的键名或索引是否存在于数组中。
  2. isset 检测变量是否设置,并且不是 NULL。

如何判断数组中值是否存在?

  1. in_array 检查数组中是否存在某个值
  2. array_search 在数组中搜索给定的值,如果成功则返回相应的键名
  3. array_keys 返回数组中所有的键名,如果指定了可选参数 search_value,则只返回该值的键名。否则 input 数组中的所有键名都会被返回。

如何依据数组的key返回值?

$array[$key]

如何依据数组的值返回key?

  1. array_flip
  2. array_search 在数组中搜索给定的值,如果成功则返回相应的键名
  3. array_keys 返回数组中所有的键名,如果指定了可选参数 search_value,则只返回该值的键名。否则 input 数组中的所有键名都会被返回。

如何依据给定值在数组进行模糊查找?

$array = array(599 => 'PHP', 'JAVA');
$search = 'P';
$result = array_filter(array_map(function($value) use ($search) {
		return is_numeric(strpos($value, $search)) ? $value :  false;
}, $array));

二维或者多维数组数据处理有什么技巧?

    $grade = array("score" => array(70, 95, 70.0, 60, "70"),
                   "name" => array("Zhang San", "Li Si", "Wang Wu",
                                   "Zhao Liu", "Liu Qi"));
    array_multisort($grade["score"], SORT_NUMERIC, SORT_DESC,
                    // 将分数作为数值,由高到低排序
                    $grade["name"], SORT_STRING, SORT_ASC);
                    // 将名字作为字符串,由小到大排序
$data[] = array('volume' => 67, 'edition' => 2);
$data[] = array('volume' => 86, 'edition' => 1);
$data[] = array('volume' => 85, 'edition' => 6);
$data[] = array('volume' => 98, 'edition' => 2);
$data[] = array('volume' => 86, 'edition' => 6);
$data[] = array('volume' => 67, 'edition' => 7);

$volume  = array_column($data, 'volume');
$edition = array_column($data, 'edition');

array_multisort($volume, SORT_DESC, $edition, SORT_ASC, $data);

数组元素插入和移除

如何往PHP数组中任意位置加入元素?

PHP数组元素的过滤和移除有哪些方法可以实现?

array array_filter ( array $input [, callable $callback = “” ] )
依次将 input 数组中的每个值传递到 callback 函数。如果 callback 函数返回 TRUE,则 input 数组的当前值会被包含在返回的结果数组中。数组的键名保留不变。


数组的排序和对比

如何巧妙记忆PHP数组中的排序函数?

如何运用array_multisort函数?

如何对PHP多维数组和对象数组排序?


数组和数据结构

PHP数组能够实现哪些数据结构?

PHP数组如何进行集合相关的运算?

巧妙实现遍历子集

array_map(function($value) {}, $array);

PHP数组如何实现堆栈?

用堆栈解决10进制转换为2进制数

$n = 254;
$a = [];
while($n > 1) {
	$a[] = $n % 2;
	$n >>= 1;
}
echo implode('', array_reverse($a));

使用队列的方式解决目录遍历

递归调用

function recursiveDirectory($dir) {
	if (is_dir($dir) && $db = opendir($dir)) {
		while ($file = readdir($db)) {
			$path = $dir . '/' . $file;
			if (in_array($file, array('.', '..'))) {
				$files[] = $file;
				continue;
			} elseif (is_dir($path)) {
				RecursiveDirectory($path);
			} elseif (is_file($path)) {
			}
		}	
		closedir($db);
	}
}

使用队列

function queueDirectory($dir) {
	$queue = array('');
	while (($file = array_shift($queue)) !== null) {
		$path = join(DIRECTORY_SEPARATOR, array($dir, $file));
		if (in_array($file, array('.', '..'))) {
			$files[] = $file;
			continue;
		} elseif (is_dir($path) && $dh = opendir($path)) {
			while ($file = readdir($dh)) {
				$queue[] = $file;
			}
			closedir($dh);
		} elseif (is_file($path)) {
		}
	}
}

使用SPL

$objects = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path), RecursiveIteratorIterator::SELF_FIRST);
foreach($objects as $name => $object){
}

“约瑟夫环”问题

function josephus($n, $m) {
    $i = 1;
    while (count($n) > 1) {
        foreach ($n as $k => $v) {
            if ($i++ == $m) {
                unset($n[$k]);
                $i = 1;
            }
        }
    }

    return array_pop($n);
}
function josephus($total, $index) {
    $result = 0;
    for($i=2; $i<= $total; $i++) {
        $result = ($result + $index) % $i;
    }
    $num = $result + 1;
    
    return $num;
}

参考

深入理解PHP之数组(遍历顺序)
深入理解PHP原理之foreach
深入理解PHP内核
The PHP Benchmark
A Closer Look Into PHP Arrays: What You Don’t See
深入理解PHP原理之变量分离/引用(Variables Separation)\

Yan Peipan 18 December 2014