(无限级、非递归)树形分类
记得之前有一次去面试,被问了无限级怎么做。我想很简单,就说了最基本的结构:id、name、parentid。
又被问就这样吗?显然不被满意。后面自然就没通过面试。
遇上技术型面试官,如果问的技术问题不被满意,大抵就没有下文了。遇上那些一副老子技术天下第一,狗眼看人低的面试官,那就自认倒霉吧。
遇上追问你如何规划人生,比如五年规划什么的。笔者现在只堪堪在温饱线上挣扎,只想提高点技术实力,多拿点工资。如果笔者有大能耐,就完全可以和面试官侃侃而谈雄心壮志,也可以完全不甩面试官,甚至可以发出一个鄙夷的眼光。奈何笔者没什么能耐,不得不认真回答面试官,却又因为回答的空泛无力,还要被面试官指手画脚一番。结果自然又是没有下文。
回去之后,在百度上翻一下非递归版的无限级。就是多了几个排序字段,在CRUD操作时,使每条保存在数据库的记录,按树形排序。避免了在获取数据时要通过递归组成树形结果的性能消耗。
毕竟树形分类在网站系统中,查询是最频繁的操作,反而在增删改操作极少。所以在保存到数据库时,为了按树形排序,增加了增删改的复杂度,却可以换来查询上性能的提升。
此方案的发明者无疑是充满智慧的。
不过我想,我只是刚好在实际工作中没有用到它而已。
没用过的技术或方案多了去。下次再遇上这种拿几个技术问题唧唧歪歪的面试官,哥决定了不甩他。
本方案是在mysql+ci php mvc下设计的,所以命名习惯随俗。
下面是表的设计结构
字段 | 说明 |
id | 分类编号 |
parent_id | 分类的父亲节点 |
name | 分类名称 |
parent_path | 父节点的路径,用于找到一个节点的子节点和子子节点或所有子节点。也可以找到一个节点的所有父节点,比如在删除节点时同时删除其下所有子节点 |
order_path | 所有节点按树形排序,可以一个sql语句提取树形排序的分类,而不需要递归 |
level | 第几级节点,可以和css配合,美化和层次化显示效果 |
order_id | 辅助order_path完成同一级别下的排序 |
添加数据
有两种情况:1.添加根结点 2.添加子节点
1.添加根结点
需要注意,parent_path=‘0,’ ,
level=’0’ ,
order_id,'select max(order_id) as mOrder_id from info_class where parent_id=0’ ,mOrder_id存在,则order_id=mOrder_id+1,否则order_id=1 ,
order_path等于order_id前面补零,凑足四位。每一个级别4位的编码,可以存放0001~9999个分类,完全足够使用。
2.添加子节点
需要注意,parent_path=父节点的parent_path+父节点的id+‘,' ,
level,通过把parent_path转换为数组,得到数组长度来计算级别 ,
order_id,'select max(order_id) as mOrder_id from info_class where parent_id=父节点’ ,mOrder_id存在,则order_id=mOrder_id+1,否则order_id=1
order_path=父节点的order_path+order_id补零4位
代码如下:
function insert() { $data['name']=$this->input->post('info_class'); $data['style']=$this->input->post('info_style'); //if 'style' array is not empty, set it as string separate with ','. if($data['style'][0]!='') $data['style']=implode(',',$data['style']); $data['parent_id']=$this->input->post('info_parent'); if($data['parent_id']=='0'){ //if add a root node $data['parent_path']='0,'; $this->level='0'; $this->db->select_max('order_id'); $this->db->where('parent_id',0); $query=$this->db->get('mini_info_class'); $data['order_id']=$query->row()->order_id+1; $data['order_path']=substr(strval($data['order_id']+1000),1,3); }else{ //if add a child node $query=$this->info_class_model->GetClass(array('id'=>$data['parent_id'])); $data['order_path']=$query->order_path; $data['parent_path']=$query->parent_path.$data['parent_id'].','; $data['level']=count(explode(',',$data['parent_path']))-2; $this->db->select_max('order_id'); $this->db->where('parent_id',$data['parent_id']); $query=$this->db->get('mini_info_class'); $data['order_id']=$query->row()->order_id+1; $data['order_path']=$data['order_path'].substr(strval($data['order_id']+1000),1,3); } $continue_add=$this->input->post('continue_add'); if($this->info_class_model->AddClass($data)){ if($continue_add=='1'){ $this->session->set_userdata(array('pre_url'=>'mini_admin/info_class_add')); } redirect('index.php/submit_success','refresh'); } }
修改数据
有三种情况,1.根结点、或升级为根结点 2.不更改父节点 3.更改父节点
1.根结点、或升级为根结点
需要注意,parent_id=0 ,
parent_path=’0,’ ,
level=0 ,
如果是升级为根节点:
order_id,'select max(order_id) as mOrder_id from info_class where parent_id=0’ ,mOrder_id存在,则order_id=mOrder_id+1,否则order_id=1 ,
order_path等于order_id前面补零,凑足四位。每一个级别4位的编码,可以存放0001~9999个分类,完全足够使用。
2.不更改父节点
需要注意,parent_path=父节点的parent_path+父节点的id+‘,' ,
level,通过把parent_path转换为数组,得到数组长度来计算级别 ,
3.更改父节点
需要注意,parent_path=父节点的parent_path+父节点的id+‘,' ,
level,通过把parent_path转换为数组,得到数组长度来计算级别 ,
///
old_order_path=该节点的原order_path,如果该节点有子节点,修改成功后,将子节点的order_path中old_order_path部分替换为新生成的order_path
order_id,'select max(order_id) as mOrder_id from info_class where parent_id=新父节点’ ,mOrder_id存在,则order_id=mOrder_id+1,否则order_id=1
temp_order_path等于order_id前面补零,凑足四位。
order_path=新父节点的order_path+temp_order_path
代码如下:
function update($id) { $data['id']=$id; $data['name']=$this->input->post('info_class'); $style=$this->input->post('info_style'); if($style[0]!='') $data['style']=implode(',',$style); $data['parent_id']=$this->input->post('info_parent'); $order_path=''; $myRecord=$this->info_class_model->GetClass(array('id'=>$id)); $newParentRecord=$this->info_class_model->GetClass(array('id'=>$data['parent_id'])); if($data['parent_id']=='0'){ $data['parent_id']='0'; $data['parent_path']='0,'; $data['level']='0'; //if it's a children node before if($myRecord->parent_id!='0') { $this->db->select_max('order_id'); $this->db->where('parent_id',$data['parent_id']); $query=$this->db->get('mini_info_class'); $data['order_id']=$query->row()->order_id+1; $order_path=$order_path.substr(strval($data['order_id']+1000),1,3); $data['order_path']=$order_path; } }else{ $data['parent_path']=$newParentRecord->parent_path.$data['parent_id'].','; $data['level']=count(explode(',',$data['parent_path']))-2; //get this node's parent_id if($myRecord->parent_id!=$data['parent_id']){ //if you change it's parent node, must update it's 'order_path' in synchronization. $old_order_path=$myRecord->order_path; $new_order_path=$newParentRecord->order_path; $order_path=$new_order_path; $this->db->select_max('order_id'); $this->db->where('parent_id',$data['parent_id']); $query=$this->db->get('mini_info_class'); $data['order_id']=$query->row()->order_id+1; $order_path=$order_path.substr(strval($data['order_id']+1000),1,3); $data['order_path']=$order_path; $new_order_path=$order_path; } } if($this->info_class_model->UpdateClass($data)){ if(isset($new_order_path)){ //if you change it's parent node, must update children's 'order_path' in synchronization. $data1=array('order_path'=>"replace('order_path','".$old_order_path."','".$new_order_path."')"); $this->db->where('parent_id',$id); $this->db->update('mini_info_class',$data1); } redirect('index.php/submit_success','refresh'); } }
上移下移、或插入
只能在同一级别内,进行上移下移、或插入
1.上移
假设待上移节点为currentNode
根据currentNode获取上一个节点,再根据上一个节点,获取上上一个节点就是待插入位置的节点,假设为brotherNode,然后就可以把currentNode上移到brotherNode之下了。
接下来,
获取currentNode的父节点的order_path,假设为father_order_path
设定currentNode的order_id=brotherNode的order_id+1
设定currentNode的order_path=father_order_path+currentNode的order_id补零四位
设定currentNode同一级别的其他节点,满足order_id大于brotherNode的order_id(不包括currentNode)条件的节点,order_id+1,order_path=father_order_path+order_id补零四位。
2.下移、插入
下移、插入都是在某一个节点下插入,区别是下移是在下一个节点下插入,而插入是选择在某一节点下插入
获取待下移节点为currentNode
获取待插入位置的节点为brotherNode
接下来,
获取currentNode父节点的order_path,假设为father_order_path
设定currentNode的order_id=brotherNode的order_id+1
设定currentNode的order_path=father_order_path+currentNode的order_id补零四位
设定currentNode同一级别的其他节点,满足order_id大于brotherNode的order_id(不包括currentNode)条件的节点,order_id+1,order_path=father_order_path+order_id补零四位。
上移和 下移、插入算法基本一致,只有获取待插入位置节点稍微有点不同。
查询数据
select * from mini_info_class order by order_path
id | parent_id | parent_path | name | order_path | order_id | level |
20 | 0 | 0, | 天庭 | 002 | 2 | 0 |
27 | 20 | 0,20, | 日游神 | 002001 | 1 | 1 |
6 | 0 | 0, | 冥府 | 004 | 4 | 0 |
26 | 6 | 0,6, | 鬼差 | 004002 | 2 | 1 |
11 | 0 | 0, | 妖域 | 005 | 5 | 0 |
7 | 0 | 0, | 魔界 | 006 | 6 | 0 |
12 | 0 | 0, | 人间道 | 007 | 7 | 0 |
29 | 0 | 0, | 修罗道 | 008 | 8 | 0 |
28 | 29 | 0,29, | 阿修罗 | 008001 | 1 | 1 |