起因:
日常开发中,我们会碰到构造树的需求,通过id,pid的关系去构建一个树结构,然后对树进行遍历等操作。其实现方式分为两种: 1. 递归, 2. 引用
而这两个方法的优缺点也很明显。
- 递归实现起来较容易,但是随着数着数据量的增大,其性能很低。
- 引用本身概念好理解,性能也很好,但是用好它还是存在着一定的门槛,不太好写。
写本文的起因是,这几天碰到非常好的一个解决方案,让我重新理解了引用。通过本文,总结下自己的学习成果.ok,那直接上代码了。
Practise
如果下面的代码,你看完就能理解了,说明你引用真是学到家了, 你也可以直接跳过本文哈~。
function buildTreeByReference($data, $id = 'id', $pid = 'pid', $child = "children")
{
$tmp = []; //以id为健,$value为值的容器,可以很巧妙的判断根节点元素
$tree = [];
//利用引用,对$data的数据进行操作
foreach ($data as $key => &$value) {
//
$tmp[$value['id']] = &$value;
if (!isset($tmp[$value['pid']])) {
$tree[] = &$tmp[$value['id']];
}else {
$temp = &$tmp[$value['pid']];
$temp[$child][] = &$value;
}
unset($temp, $value);
}
return $tree;
}
ok,先不说其他的,你先拿下面的数据测试下这个方法.
$data= [
["id" => 1, "pid" => 0 , "name" => 'Universe'],
["id" => 2, "pid" => 1 , "name" => 'Earth'],
["id" => 3, "pid" => 2 , "name" => 'China'],
["id" => 4, "pid" => 3 , "name" => 'Beijing'],
];
补充:这个方法需要注意一点,需要父节点在前,不适合无序数据,所以如果是无序的,先得排序.
如果没有意外,打印的结果,应该如下:
array(1) {
[0]=>
array(4) {
["id"]=>
int(1)
["pid"]=>
int(0)
["name"]=>
string(8) "Universe"
["children"]=>
array(1) {
[0]=>
array(4) {
["id"]=>
int(2)
["pid"]=>
int(1)
["name"]=>
string(5) "Earth"
["children"]=>
array(1) {
[0]=>
array(4) {
["id"]=>
int(3)
["pid"]=>
int(2)
["name"]=>
string(5) "China"
["children"]=>
array(1) {
[0]=>
array(3) {
["id"]=>
int(4)
["pid"]=>
int(3)
["name"]=>
string(7) "Beijing"
}
}
}
}
}
}
}
}
如果到此,你还想不明白,没关系,我们一一来分析下.
其实要彻底弄明白这个解决方案,需要理解二个部分。
- foreach赋值原理
- 引用的原理
foreach
$data = ["student", "teacher"];
foreach ($data as $index => $item) {
}
注意每次循环的时候, 是把$data[0]和$data[1] 的“值”复制一份 再赋给 $item
引用( 一定要自己动手试验下)
$a = 1;
$b = &$a;
$c = $b;
$c = 2;
猜猜看 $b = ?;
到此,如果你能理解上面foreach和引用,并且能理解这个解决方案的所有执行过程,那么恭喜你,你学的真好! 但如果还是有困难,没关系,咱们一步一步踏踏实实的来.
Analysis
ok,深吸一口气,跟着我的思路,咱们一步一步来.
- 首先咱们看下原函数
function buildTreeByReference($data, $id = 'id', $pid = 'pid', $child = "children")
{
$tmp = []; #以id为健,$value为值的容器,可以很巧妙的判断根节点元素
$tree = [];
#利用引用,对$data的数据进行操作
foreach ($data as $key => &$value) {
#&$value取到$data元素对应值的引用
$tmp[$value['id']] = &$value;
#以$value['id']为键,&$value引用为值push到$tmp中,
#这样可以巧妙的判断当前元素是否为根节点
if (!isset($tmp[$value['pid']])) {
#将根节点push到$tree中
$tree[] = &$tmp[$value['id']];
}else {
#若当前元素的父节点存在于$tmp中, 引用获取$tmp中对应父节点的值
$temp = &$tmp[$value['pid']];
#然后将当前元素push到其父节点的children中
$temp[$child][] = &$value;
}
#为了不引起变量污染, 引用用完后,需要unset掉
unset($temp, $value);
}
return $tree;
}
- 第一次循环
function buildTreeByReference($data, $id = 'id', $pid = 'pid', $child = "children")
{
# $tmp = [];
# $tree = [];
# foreach ($data as $key => &$value) {
//
$tmp[$value['id']] = &$value;
if (!isset($tmp[$value['pid']])) {
$tree[] = &$tmp[$value['id']];
}else {
# $temp = &$tmp[$value['pid']];
# $temp[$child][] = &$value;
# }
unset($temp, $value);
}
return $tree;
}
变量情况:
$data[0] = ["id" => 1, "pid" => 0 , "name" => 'Universe'];
$tmp[1] = &$data[0];
$tree[] = &$data[0]
- 第二次循环
function buildTreeByReference($data, $id = 'id', $pid = 'pid', $child = "children")
{
# $tmp = [];
# $tree = [];
# foreach ($data as $key => &$value) {
//
$tmp[$value['id']] = &$value;
# if (!isset($tmp[$value['pid']])) {
# $tree[] = &$tmp[$value['id']];
}else {
$temp = &$tmp[$value['pid']];
$temp[$child][] = &$value;
}
unset($temp, $value);
}
return $tree;
}
变量情况:
$data[1] = ["id" => 2, "pid" => 1 , "name" => 'Earth'];
$value=&$data[1];
$tmp[2] = &$data[1];
注意:
$temp即&$tmp[1],即和$data[0]指向相同的地址
所以$temp['children'][] = &$value ,操作的结果是:
$data[
[
"id" => 1,
"pid" => 0 ,
"name" => 'Universe'
"children"=>[
&$data[1], //注意:存储的是引用
]
]
...
]
4.第三次循环
function buildTreeByReference($data, $id = 'id', $pid = 'pid', $child = "children")
{
# $tmp = [];
# $tree = [];
# foreach ($data as $key => &$value) {
//
$tmp[$value['id']] = &$value;
# if (!isset($tmp[$value['pid']])) {
# $tree[] = &$tmp[$value['id']];
}else {
$temp = &$tmp[$value['pid']];
$temp[$child][] = &$value;
}
unset($temp, $value);
}
return $tree;
}
变量情况:
$data[2] = ["id" => 3, "pid" => 2 , "name" => 'China'];
$value = &$data[2];
$tmp[3] = &$data[2];
注意:
$temp即&$tmp[2],即和$data[1]指向相同的地址
所以$temp['children'][] = &$value ,操作的结果是:
这里注意一下:
这是第二次循环的时候,children中存储的$data[1]的引用
$data[
[
"id" => 1,
"pid" => 0 ,
"name" => 'Universe'
"children"=>[
&$data[1], //注意:存储的是引用
]
]
...
]
第三次循环的的时候,则是$data[1]['children'][] = &$value, 而$value指向的是$data[2]
,所以结果是:
$data[
[
"id" => 1,
"pid" => 0 ,
"name" => 'Universe'
"children"=>[
// &$data[1], //注意:存储的是引用
[
"id" => 2,
"pid" => 1 ,
"name" => 'Earth'
"children" => [
&data[2] //注意:存储的是引用
]
]
]
]
]
...
]
5.第四次循环
function buildTreeByReference($data, $id = 'id', $pid = 'pid', $child = "children")
{
# $tmp = [];
# $tree = [];
# foreach ($data as $key => &$value) {
//
$tmp[$value['id']] = &$value;
# if (!isset($tmp[$value['pid']])) {
# $tree[] = &$tmp[$value['id']];
}else {
$temp = &$tmp[$value['pid']];
$temp[$child][] = &$value;
}
unset($temp, $value);
}
return $tree;
}
变量情况:
$data[3] = ["id" => 4, "pid" => 3 , "name" => 'Beijing'];
$value = &$data[3];
$tmp[3] = &$data[3];
注意:
$temp即&$tmp[2],即和$data[1]指向相同的地址
所以$temp['children'][] = &$value ,操作的结果是:
这里注意一下:
这是第三次循环的时候,children中存储的$data[2]的引用
$data[
[
"id" => 1,
"pid" => 0 ,
"name" => 'Universe'
"children"=>[
// &$data[1], //注意:存储的是引用
[
"id" => 2,
"pid" => 1 ,
"name" => 'Earth'
"children" => [
&data[2] //注意:存储的是引用
]
]
]
]
]
...
]
第四次循环的的时候,则是$data[2]['children'][] = &$value, 而$value指向的是$data[3]
,所以结果是:
$data[
[
"id" => 1,
"pid" => 0 ,
"name" => 'Universe'
"children"=>[
// &$data[1], //注意:存储的是引用
[
"id" => 2,
"pid" => 1 ,
"name" => 'Earth'
"children" => [
// &data[2] //注意:存储的是引用
[
"id" => 3,
"pid" => 2 ,
"name" => 'China'
"children" =>[
&$data[3]; //注意:存储的是引用
]
]
]
]
]
]
]
...
]
ok,至此,整个执行过程走通了,你懂了吗?:)
对了,还另外一个方法,也是通过引用的,这个我就不分析,要是理解上面的方法,下面的相对来说简单些。
public static function buildTreeByReference1($data, $id = 'id', $pid = 'pid', $child = "children")
{
$tmp = [];
foreach ($data as $key => $value) {
$tmp[$value[$id]] = $value;
}
$tree = [];
foreach ($tmp as $key => $value) {
if (isset($tmp[$value['pid']])) {
$tmp[$value['pid']]['children'][] = &$tmp[$key];
}else{
$tree[] = &$tmp[$key];
}
}
return $tree;
}