1. 程式人生 > >無限極分類原理與實現(轉)

無限極分類原理與實現(轉)

轉換 完成 外灘 獲得 意思 容易 set 導航 另一個

  前言

  無限極分類是我很久前學到知識,今天在做一個項目時,發現對其概念有點模糊,所以今天就來說說無限極分類。

  首先來說說什麽是無限極分類。按照我的理解,就是對數據完成多次分類,如同一棵樹一樣,從根開始,到主幹、枝幹、葉子……

  完成無限極分類,主要運用了兩種方法,一是遞歸方式,二是叠代方式。而主要運用無限極分類的地方有地址解析,面包屑導航等等。下面就來具體介紹兩種方法的原理及實現方法。

  家譜樹與子孫樹

  家譜樹是無限極分類的表現形式之一,另一個是子孫樹。一開始學習無限極分類時,我時常弄混這兩棵樹,現在看來自然是明白很多。從漢語的意思也能夠看出其中的區別。

  家譜,現在很多地方都流行起修家譜,那怎麽修家譜,按照我理解,就是給自己找一個祖宗,一代代找上去,形成了一個體系,這樣編篡而成的叫家譜。家譜樹就與之類似,從某個節點開始向上尋找其父節點,再找父節點的父節點,直到找不到為止。按照這種尋找,形成的一個類似樹狀的結構,就叫做家譜樹。

  而子孫樹與其相反,子孫樹類似於生物書中的遺傳圖,從某個節點開始尋找它的子節點,再找子節點的子節點,直到尋找完畢。這樣形成的樹狀結構就叫做子孫樹。

  從上面對家譜樹與子孫樹的描述,將其轉換為代碼時,我的第一印象就是利用遞歸方式,家譜樹,找父節點的父節點,子孫樹,找子節點的子節點。完全符合遞歸思想。所以首先我們來說說利用遞歸方式完成家譜樹與子孫樹。

  遞歸方式

  家譜樹的實現

  為更清楚的講解,我先將即將分類的數據貼在下面,是關於地址的數據:

$address = array(
    array(‘id‘=>1  , ‘address‘=>‘安徽‘ , ‘parent_id‘ => 0),
    array(‘id‘=>2  , ‘address‘=>‘江蘇‘ , ‘parent_id‘ => 0),
    array(‘id‘=>3  , ‘address‘=>‘合肥‘ , ‘parent_id‘ => 1),
    array(‘id‘=>4  , ‘address‘=>‘廬陽區‘ , ‘parent_id‘ => 3),
    array(‘id‘=>5  , ‘address‘=>‘大楊鎮‘ , ‘parent_id‘ => 4),
    array(‘id‘=>6  , ‘address‘=>‘南京‘ , ‘parent_id‘ => 2),
    array(‘id‘=>7  , ‘address‘=>‘玄武區‘ , ‘parent_id‘ => 6),
    array(‘id‘=>8  , ‘address‘=>‘梅園新村街道‘, ‘parent_id‘ => 7),
    array(‘id‘=>9  , ‘address‘=>‘上海‘ , ‘parent_id‘ => 0),
    array(‘id‘=>10 , ‘address‘=>‘黃浦區‘ , ‘parent_id‘ => 9),
    array(‘id‘=>11 , ‘address‘=>‘外灘‘ , ‘parent_id‘ => 10)
    array(‘id‘=>12 , ‘address‘=>‘安慶‘ , ‘parent_id‘ => 1)
    );

按照上文的介紹,對上面數據進行家譜樹無限極分類,假設我們想要尋找大楊鎮的家譜樹,先找到與之相關的信息。

‘id‘=>5  , ‘address‘=>‘大楊鎮‘ , ‘parent_id‘ => 4

可以看出它的父節點的id,即parent_id == 4,那麽id==4的節點就是其父節點,由此找到廬陽區:

‘id‘=>4  , ‘address‘=>‘廬陽區‘ , ‘parent_id‘ => 3

與上面類似,尋找id=3的節點,依次向上尋找,找到大楊鎮的家譜

大楊鎮 -> 廬陽區 -> 合肥 -> 安徽

那麽怎麽用代碼來完成它呢?其實很簡單,只需要判斷尋找的父id是否與節點的id相等,即parent_id ?= id

,相等就是要尋找的父節點,並把該節點的parent_id作為尋找的id,遞歸進行尋找。如下面的流程圖:

技術分享
遞歸方法求家譜樹

下面就開始編寫代碼:

/**
 * 獲取家譜樹
 * @param   array        $data   待分類的數據
 * @param   int/string   $pid    要找的祖先節點
 */
function Ancestry($data , $pid) {
    static $ancestry = array();

    foreach($data as $key => $value) {
        if($value[‘id‘] == $pid) {
            $ancestry[] = $value;

            Ancestry($data , $value[‘parent_id‘]);
        }
    }
    return $ancestry;
}

根據流程圖,代碼編寫完成。註意上面存儲結點的數組,即$ancestry,要添加靜態化關鍵字static,否則每次遞歸都會將該數組初始化。當然也可以使用array_merge將每次返回的數組與上一次的進行合並。

尋找家譜的關鍵就是條件判斷,尋找的parent_id等於某個節點的id值,顯然該節點就是要尋找的父節點。

代碼編寫完成,來看看是否符合我們的預期,來尋找大楊鎮的家譜:

Ancestry($address , 4);

結果:

Array
(
    [0] => Array
        (
            [id] => 4
            [address] => 廬陽區
            [parent_id] => 3
        )
    [1] => Array
        (
            [id] => 3
            [address] => 合肥
            [parent_id] => 1
        )
    [2] => Array
        (
            [id] => 1
            [address] => 安徽
            [parent_id] => 0
        )
)

可以看出結果與我們預期相符。那麽家譜樹的遞歸方法就完成了,下面來講子孫樹的實現。

子孫樹的實現

依然使用上面的數據,子孫樹是從父節點開始,向下尋找其子孫節點,而形成的一個樹狀圖形。

假設尋找id=0的子孫節點,那麽就要註意所有parent_id=0的節點,這些節點都是id=0的子節點。然後,把parent_id=0節點的id作為查詢id繼續向下查詢,直到查不到任何子節點為止。如下:

技術分享
子孫樹

流程圖如下:

技術分享
子孫樹流程圖

其流程與家譜樹類似,不同點,也是關鍵點就是條件語句的執行。家譜樹判斷的是當前節點的id是否與上一個節點的parent_id相等;子孫樹判斷的是當前節點的parent_id與上一個節點的id相等,按照這種條件判斷子孫樹能夠有多個子孫節點,而家譜樹只能存在一個祖先。代碼如下:

/**
 * 獲取子孫樹
 * @param   array        $data   待分類的數據
 * @param   int/string   $id     要找的子節點id
 * @param   int          $lev    節點等級
 */
 function getSubTree($data , $id = 0 , $lev = 0) {
     static $son = array();

     foreach($data as $key => $value) {
         if($value[‘parent_id‘] == $id) {
             $value[‘lev‘] = $lev;
             $son[] = $value;
             getSubTree($data , $value[‘id‘] , $lev+1);
         }
     }

     return $son;
 }

在函數中我添加了一個變量lev,為的是給存入的節點標註等級,方便看出子孫樹的結構。下面來測試結果:

getSubTree($data , 0 , 0);

因篇幅有限,將結果進行部分處理:

foreach($tree as $k => $v) {
    echo str_repeat(‘--‘ , $v[‘lev‘]) . $v[‘address‘] . ‘<br/>‘;
}

結果:

安徽
--合肥
----廬陽區
------大楊鎮
--安慶
江蘇
--南京
----玄武區
------梅園新村街道
上海
--黃浦區
----外灘

遞歸方式的家譜樹與子孫樹比較容易理解,只要對遞歸思想比較了解,一步步寫下來不是很難。比起遞歸方式,叠代方式可能更加讓人難以理解。下面就來介紹叠代方式的家譜樹與子孫樹編寫。

叠代方式

家譜樹

完成跌代方式的家譜樹之前,首先說一下尋找祖先節點的終止條件。雖然叫無限極分類,它不是絕對的無限,只是理論的無限。

如同我國上下五千年歷史,任一個大的姓氏,向上找其祖先,不是找到炎帝就是找到黃帝,在往前就沒有歷史記載了。所以在家譜樹的尋找中也有終止條件,就是在分類數據中再也找不到它的父節點時,表現在實例數據上,就是不存在parent_id < 0的節點。

這也是完成叠代的關鍵,以其作為叠代條件,對數據進行循環判斷,並把每次找到的節點的parent_id再次作為叠代條件,直到不滿足叠代條件。流程圖如下:

技術分享
家譜樹叠代流程

理清流程,現在開始完成代碼編寫:

function Ancestry($data , $pid) {
    $ancestry = array();

    while($pid > 0) {
        foreach($data as $v) {
            if($v[‘id‘] == $pid) {
                $ancestry[] = $v;

                $pid = $v[‘parent_id‘];
            }
        }
    }

    return $ancestry;
}

叠代條件$pid>0,當pid>0時說明還有祖先存在,可以繼續叠代,否則說明沒有祖先,叠代終止。$pid = $v[‘parent_id‘]是叠代繼續進行的關鍵,每次找到祖先節點,就將祖先節點的父id傳遞給pid,進行下一次叠代。

運行這個函數,結果與使用遞歸方式的結果一致。

子孫樹的實現

使用叠代方式完成子孫樹,更為復雜,需要運用的棧的思想。在進行叠代的過程中,將每次尋找的id入棧,找到一個節點,就將該節點從原數據中刪除,當尋找到葉子節點時,即不存在子孫節點時,就將該葉子節點對應的id從棧中彈出,再尋找棧頂id的子孫節點,直到棧清空為止,叠代結束。下面用一個例子來說明:

$address = array(
    array(‘id‘=>1  , ‘address‘=>‘安徽‘ , ‘parent_id‘ => 0),
    array(‘id‘=>2  , ‘address‘=>‘江蘇‘ , ‘parent_id‘ => 0),
    array(‘id‘=>3  , ‘address‘=>‘合肥‘ , ‘parent_id‘ => 1),
    array(‘id‘=>4  , ‘address‘=>‘廬陽區‘ , ‘parent_id‘ => 3),
    array(‘id‘=>5  , ‘address‘=>‘大楊鎮‘ , ‘parent_id‘ => 4),
    array(‘id‘=>6  , ‘address‘=>‘南京‘ , ‘parent_id‘ => 2),
    array(‘id‘=>7  , ‘address‘=>‘玄武區‘ , ‘parent_id‘ => 6),
    array(‘id‘=>8  , ‘address‘=>‘梅園新村街道‘, ‘parent_id‘ => 7),
    array(‘id‘=>9  , ‘address‘=>‘上海‘ , ‘parent_id‘ => 0),
    array(‘id‘=>10 , ‘address‘=>‘黃浦區‘ , ‘parent_id‘ => 9),
    array(‘id‘=>11 , ‘address‘=>‘外灘‘ , ‘parent_id‘ => 10)
    array(‘id‘=>12 , ‘address‘=>‘安慶‘ , ‘parent_id‘ => 1)
    );

尋找id=0的子孫節點,id=0入棧,尋找到該節點,為

array(‘id‘=>1  , ‘address‘=>‘安徽‘ , ‘parent_id‘ => 0)

此時棧為[0],並且將該節點從原數據中刪除,再將id=1入棧,尋找id=1的子孫節點,找到為:

array(‘id‘=>3  , ‘address‘=>‘合肥‘ , ‘parent_id‘ => 1),

此時棧[0][1],將該節點刪除,id=3入棧,尋找id=3的子孫節點,找到:

array(‘id‘=>4  , ‘address‘=>‘廬陽區‘ , ‘parent_id‘ => 3)

[0][1][3],將該節點刪除,id=4入棧,尋找id=4的子孫節點,找到:

array(‘id‘=>5  , ‘address‘=>‘大楊鎮‘         , ‘parent_id‘ => 4),

[0][1][3][4],將該節點刪除,id=5入棧,棧[0][1][3][4][5],並尋找id=5的子節點,遍歷後未找到,於是將id=5出棧,再次尋找id=4的子孫節點,依次進行。最後完成整個叠代。

期間,棧的情況如下:

[0]
[0][1]
[0][1][3]
[0][1][3][4]
[0][1][3][4][5]
[0][1][3][4]
[0][1][3]
[0][1]
[0][1][12]
[0][1]
[0]
……

代碼如下:

function getSubTree($data , $id = 0) {
    $task = array($id);                          # 棧 任務表
    $son = array();

    while(!empty($task)) {
        $flag = false;                           # 是否找到節點標誌
        foreach($data as $k => $v) {

            # 判斷是否是子孫節點的條件 與 遞歸方式一致
            if($v[‘parent_id‘] == $id) {
                $son[] = $v;                     # 節點存入數組
                array_push($task , $v[‘id‘]);    # 節點id入棧
                $id = $v[‘id‘];                  # 判斷條件切換
                unset($data[$k]);                # 刪除節點
                $flag = true;                    # 找到節點標誌
            }
        }

        # flag == false說明已經到了葉子節點 無子孫節點了
        if($flag == false) {
            array_pop($task);                    # 出棧
            $id = end($task);                    # 尋找棧頂id的子節點
        }
    }
    return $son;
}

這裏找到節點後必須把該節點從原數據中刪除,否則會造成每次都找到該節點,形成無限叠代的bug。在這裏利用數組函數array_push與array_pop模擬進棧與出棧操作。

利用叠代完成子孫樹比較復雜,且我沒有測試過這個與遞歸方式誰的效率高,不過利用叠代完成家譜樹明顯比起遞歸方法效率高。

應用

面包屑導航

說完了無限極分類的實現原理與方法,現在來說說在網站中對無限極分類的應用。最常用的就是面包屑導航了。

什麽是面包屑導航,這個稱呼來自於童話故事"漢賽爾和格萊特",具體什麽故事就不敘述了,有興趣的可以去谷歌一下。面包屑導航的作用就是告訴訪問者他們目前在網站中的位置以及如何返回。下圖就是一個典型的面包屑導航。

技術分享
面包屑導航

面包屑是一個典型家譜樹的應用,不要看它是從左到右,分類級數越來越低,就認為它是子孫樹應用,要知道子孫樹是可能存在多個分支,而面包屑導航要求的是一條主幹。

將上面家譜樹代碼做一定修改,就能夠完成面包屑導航。我們采用遞歸方式的家譜樹。代碼如下:

function Ancestry($data , $pid) {
    static $ancestry = array();

    foreach($data as $key => $value) {
        if($value[‘id‘] == $pid) {

            Ancestry($data , $value[‘parent_id‘]);

            $ancestry[] = $value;                
        }
    }
    return $ancestry;
}

如果先進行遞歸調用,在遞歸結束再將找到的節點存入數組中,就能夠使祖先節點排列在數組前列,子孫節點排列在數組後列,方便進行提取數據。

簡化演示步驟,不從數據庫中取出數據,改為模擬數據:

 $tmp = array(
    array(‘cate_id‘=1 , ‘name‘=>‘首頁‘ , ‘parent_id‘=>‘0‘),
    array(‘cate_id‘=2 , ‘name‘=>‘新聞中心‘ , ‘parent_id‘=>‘1‘),
    array(‘cate_id‘=3 , ‘name‘=>‘娛樂新聞‘ , ‘parent_id‘=>‘2‘),
    array(‘cate_id‘=4 , ‘name‘=>‘軍事要聞‘ , ‘parent_id‘=>‘2‘),
    array(‘cate_id‘=5 , ‘name‘=>‘體育新聞‘ , ‘parent_id‘=>‘2‘),
    array(‘cate_id‘=6 , ‘name‘=>‘博客‘ , ‘parent_id‘=>‘1‘),
    array(‘cate_id‘=7 , ‘name‘=>‘旅遊日誌‘ , ‘parent_id‘=>‘6‘),
    array(‘cate_id‘=8 , ‘name‘=>‘心情‘ , ‘parent_id‘=>‘6‘),
    array(‘cate_id‘=9 , ‘name‘=>‘小小說‘ , ‘parent_id‘=>‘6‘),
    array(‘cate_id‘=10 , ‘name‘=>‘明星‘ , ‘parent_id‘=>‘3‘),
    array(‘cate_id‘=11 , ‘name‘=>‘網紅‘ , ‘parent_id‘=>‘3‘)
    );

假設用戶點進明星導航,那麽在網站顯示的導航為:

$tree = Ancestry($tmp , 10);
foreach ($tree as $key => $value) {
    echo $value[‘name‘] . ‘>‘;
}
技術分享
面包屑導航

防止設置父類為子類

在網站建立中,可能會碰到用戶進行編輯時出現誤操作,將某個欄目的父節點設置成了該欄目的子節點,進行這樣的設置後會導致數據庫中的數據丟失,因此在進行數據更新之前應該註意這一點。

利用家譜樹,就能夠避免發生這種錯誤。在用戶提交表單時,我們將即將修改欄目的父節點的家譜樹取出,並對家譜樹進行遍歷,如果發現該家譜樹中發現了要修改的節點,就說明是錯誤操作。有點繞,舉個例子來說明:

修改欄目新聞中心的父節點為娛樂新聞,就把娛樂新聞的家譜樹取出來:

娛樂新聞 新聞中心 首頁

在該家譜樹中發現要修改的節點,新聞中心,那麽說明出現了錯誤。具體代碼如下:

$data = Ancestry($tmp , 3);
foreach ($data as $key => $value) {
    if($value[‘cate_id‘] == 3) {
        echo  ‘Error‘;
        break;
    }
}


作者:阿V薄荷加可樂
鏈接:http://www.jianshu.com/p/ff07b46666c7
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請註明出處。

無限極分類原理與實現(轉)