無限極二叉樹,資料結構,經典演算法
利用多叉樹實現Ext JS中的無限級樹形選單(一種構建多級有序樹形結構JSON的方法)
一、問題研究的背景和意義
目前在Web應用程式開發領域,Ext JS框架已經逐漸被廣泛使用,它是富客戶端開發中出類拔萃的框架之一。在Ext的UI控制元件中,樹形控制元件無疑是最為常用的控制元件之一,它用來實現樹形結構的選單。TreeNode用來實現靜態的樹形選單,AsyncTreeNode用來實現動態的非同步載入樹形選單,後者最為常用,它通過接收伺服器端返回來的JSON格式的資料,動態生成樹形選單節點。動態生成樹有兩種思路:一種是一次性生成全部樹節點,另一種是逐級載入樹節點(利用AJAX,每次點選節點時查詢下一級節點)。對於大資料量的選單節點來說,逐級載入是比較合適的選擇,但是對於小資料量的選單來說,一次性生成全部節點應該是最為合理的方案。在實際應用開發中,一般不會遇到特別大資料量的場景,所以一次性生成全部選單節點是我們重點研究的技術點,本文就是介紹基於Ext
JS的應用系統中如何將資料庫中的無限級層次資料一次性在介面中生成全部選單節點(例如在介面中以樹形方式一次性展示出銀行所有分支機構的資訊),同時對每一個層次的選單節點按照某一屬性和規則排序,展示出有序的選單樹。
解決Ext JS無限級樹形選單的問題,可以拓展出更多的應用場景,例如樹形結構表格TreeGrid,一次性生成樹形表格,對樹形表格進行完整分頁,對錶格列進行全排序;或者可以利用本文的思路擴展出其他的更復雜的應用場景。
先看兩個圖例,有個直觀上的認識:
圖一,銀行分支機構樹形結構選單
圖二,樹形結構表格
二、詳細設計方案
讓我們先看一段程式碼片段:
檔案一,branchTree.html (Ext樹形控制元件頁面)
Js程式碼- Ext.onReady(
- function(){
- var tree = new Ext.tree.TreePanel({
- height: 300,
- width: 400,
- animate:true,
- enableDD:true,
- containerScroll: true,
- rootVisible: false,
- frame: true,
- // getBranch.do請求伺服器返回多級樹形結構的JSON字串
- loader: new Ext.tree.TreeLoader({dataUrl:'getBranch.do'}),
- root : new Ext.tree.AsyncTreeNode({id:'0',text:'根結點'
- });
- tree.expandAll();
- }
- );
檔案二,branchTreeJSON.jsp (接收getBranch.do請求,返回無限級JSON字串)
Java程式碼- <%
- // 讀取銀行分支機構的層次資料
- List result = DataAccess.getBankInfoList();
- // 將層次資料轉換為多叉樹物件(本文下面會詳細介紹該資料結構的實現方法)
- Node root = ExtTreeHelper.createExtTree(result);
- %>
- [
- <%=root.toString()%> <!-- 以JSON的形式返回響應資料,Ext.tree.TreeLoader會根據此資料生成樹形選單 -->
- ]
以上兩個程式檔案是一次性生成無限級樹形選單所必須的,其中最為關鍵的部分就是如何生成一個無限級的JSON字串,返回給客戶端的Ext樹形控制元件。對於銀行分支機構來說,需要返回類似如下的JSON串:
Js程式碼- {
- id: '100000',
- text: '廊坊銀行總行',
- children: [
- {
- id: '110000',
- text: '廊坊分行',
- children: [
- {
- id: '113000',
- text: '廊坊銀行開發區支行',
- leaf: true
- },
- {
- id: '112000',
- text: '廊坊銀行解放道支行',
- children: [
- {
- id: '112200',
- text: '廊坊銀行三大街支行',
- leaf: true
- },
- {
- id: '112100',
- text: '廊坊銀行廣陽道支行',
- leaf: true
- }
- ]
- },
- {
- id: '111000',
- text: '廊坊銀行金光道支行',
- leaf: true
- }
- ]
- }
- ]
- }
同時還可能需要對樹中每一個層次的節點按照某一屬性(比如分支機構編號)進行排序,以展示出有序的樹形選單。
現在可以把問題概括為:
1、 把資料庫中的層次資料轉換成JSON格式的字串
2、 對樹中每一個層次的節點按照某一屬性(比如分支機構編號)進行排序
下面介紹解決問題的思路:
在資料結構這門課中,我們都學過樹,無限級樹形選單就可以抽象成一種多叉樹結構,即每個節點下包含多個子節點的樹形結構,首先就需要把資料庫中的層次資料轉換成多叉樹結構的物件樹,也就是構造出一棵多叉樹。
有了資料結構,還要實現相應的演算法,我們需要實現兩種演算法:
1、兄弟節點橫向排序演算法,對隸屬於同一個父節點下面的所有直接子節點按照某一節點屬性和規則進行排序,保持兄弟節點橫向有序;
2、先序遍歷演算法,遞迴打印出無限級JSON字串。
概括起來分為三步:
1、 構造無序的多叉樹結構
2、 實現兄弟節點橫向排序方法
3、 實現先序遍歷方法,打印出JSON字串
如圖所示:
三、原始碼實現(Java語言版)
實現這樣一顆樹,需要設計三個類:樹類(MultipleTree.java)、節點類(Node.java)、孩子列表類(Children.java);為了方便演示,還需要構造一些假的層次資料,因此還需要建一個構造假資料的類(VirtualDataGenerator.java),以下程式碼拷貝出來之後可直接執行測試:
- package test;
- import java.util.ArrayList;
- import java.util.Comparator;
- import java.util.HashMap;
- import java.util.Iterator;
- import java.util.List;
- import java.util.Map;
- import java.util.Set;
- import java.util.Collections;
- /**
- * 多叉樹類
- */
- public class MultipleTree {
- public static void main(String[] args) {
- // 讀取層次資料結果集列表
- List dataList = VirtualDataGenerator.getVirtualResult();
- // 節點列表(散列表,用於臨時儲存節點物件)
- HashMap nodeList = new HashMap();
- // 根節點
- Node root = null;
- // 根據結果集構造節點列表(存入散列表)
- for (Iterator it = dataList.iterator(); it.hasNext();) {
- Map dataRecord = (Map) it.next();
- Node node = new Node();
- node.id = (String) dataRecord.get("id");
- node.text = (String) dataRecord.get("text");
- node.parentId = (String) dataRecord.get("parentId");
- nodeList.put(node.id, node);
- }
- // 構造無序的多叉樹
- Set entrySet = nodeList.entrySet();
- for (Iterator it = entrySet.iterator(); it.hasNext();) {
- Node node = (Node) ((Map.Entry) it.next()).getValue();
- if (node.parentId == null || node.parentId.equals("")) {
- root = node;
- } else {
- ((Node) nodeList.get(node.parentId)).addChild(node);
- }
- }
- // 輸出無序的樹形選單的JSON字串
- System.out.println(root.toString());
- // 對多叉樹進行橫向排序
- root.sortChildren();
- // 輸出有序的樹形選單的JSON字串
- System.out.println(root.toString());
- // 程式輸出結果如下(無序的樹形選單)(格式化後的結果):
- // {
- // id : '100000',
- // text : '廊坊銀行總行',
- // children : [
- // {
- // id : '110000',
- // text : '廊坊分行',
- // children : [
- // {
- // id : '113000',
- // text : '廊坊銀行開發區支行',
- // leaf : true
- // },
- // {
- // id : '111000',
- // text : '廊坊銀行金光道支行',
- // leaf : true
- // },
- // {
- // id : '112000',
- // text : '廊坊銀行解放道支行',
- // children : [
- // {
- // id : '112200',
- // text : '廊坊銀行三大街支行',
- // leaf : true
- // },
- // {
- // id : '112100',
- // text : '廊坊銀行廣陽道支行',
- // leaf : true
- // }
- // ]
- // }
- // ]
- // }
- // ]
- // }
- // 程式輸出結果如下(有序的樹形選單)(格式化後的結果):
- // {
- // id : '100000',
- // text : '廊坊銀行總行',
- // children : [
- // {
- // id : '110000',
- // text : '廊坊分行',
- // children : [
- // {
- // id : '111000',
- // text : '廊坊銀行金光道支行',
- // leaf : true
- // },
- // {
- // id : '112000',
- // text : '廊坊銀行解放道支行',
- // children : [
- // {
- // id : '112100',
- // text : '廊坊銀行廣陽道支行',
- // leaf : true
- // },
- // {
- // id : '112200',
- // text : '廊坊銀行三大街支行',
- // leaf : true
- // }
- // ]
- // },
- // {
- // id : '113000',
- // text : '廊坊銀行開發區支行',
- // leaf : true
- // }
- // ]
- // }
- // ]
- // }
- }
- }
- /**
- * 節點類
- */
- class Node {
- /**
- * 節點編號
- */
- public String id;
- /**
- * 節點內容
- */
- public String text;
- /**
- * 父節點編號
- */
- public String parentId;
- /**
- * 孩子節點列表
- */
- private Children children = new Children();
- // 先序遍歷,拼接JSON字串
- public String toString() {
- String result = "{"
- + "id : '" + id + "'"
- + ", text : '" + text + "'";
- if (children != null && children.getSize() != 0) {
- result += ", children : " + children.toString();
- } else {
- result += ", leaf : true";
- }
- return result + "}";
- }
- // 兄弟節點橫向排序
- public void sortChildren() {
- if (children != null && children.getSize() != 0) {
- children.sortChildren();
- }
- }
- // 新增孩子節點
- public void addChild(Node node) {
- this.children.addChild(node);
- }
- }
- /**
- * 孩子列表類
- */
- class Children {
- private List list = new ArrayList();
- public int getSize() {
- return list.size();
- }
- public void addChild(Node node) {
- list.add(node);
- }
- // 拼接孩子節點的JSON字串
- public String toString() {
- String result = "[";
- for (Iterator it = list.iterator(); it.hasNext();) {
- result += ((Node) it.next()).toString();
- result += ",";
- }
- result = result.substring(0, result.length() - 1);
- result += "]";
- return result;
- }
- // 孩子節點排序
- public void sortChildren() {
- // 對本層節點進行排序
- // 可根據不同的排序屬性,傳入不同的比較器,這裡傳入ID比較器
- Collections.sort(list, new NodeIDComparator());
- // 對每個節點的下一層節點進行排序
- for (Iterator it = list.iterator(); it.hasNext();) {
- ((Node) it.next()).sortChildren();
- }
- }
- }
- /**
- * 節點比較器
- */
- class NodeIDComparator implements Comparator {
- // 按照節點編號比較
- public int compare(Object o1, Object o2) {
- int j1 = Integer.parseInt(((Node)o1).id);
- int j2 = Integer.parseInt(((Node)o2).id);
- return (j1 < j2 ? -1 : (j1 == j2 ? 0 : 1));
- }
- }
- /**
- * 構造虛擬的層次資料
- */
- class VirtualDataGenerator {
- // 構造無序的結果集列表,實際應用中,該資料應該從資料庫中查詢獲得;
- public static List getVirtualResult() {
- List dataList = new ArrayList();
- HashMap dataRecord1 = new HashMap();
- dataRecord1.put("id", "112000");
- dataRecord1.put("text", "廊坊銀行解放道支行");
- dataRecord1.put("parentId", "110000");
- HashMap dataRecord2 = new HashMap();
- dataRecord2.put("id", "112200");
- dataRecord2.put("text", "廊坊銀行三大街支行");
- dataRecord2.put("parentId", "112000");
- HashMap dataRecord3 = new HashMap();
- dataRecord3.put("id", "112100");
- dataRecord3.put("text", "廊坊銀行廣陽道支行");
- dataRecord3.put("parentId", "112000");
- HashMap dataRecord4 = new HashMap();
- dataRecord4.put("id", "113000");
- dataRecord4.put("text", "廊坊銀行開發區支行");
- dataRecord4.put("parentId", "110000");
- HashMap dataRecord5 = new HashMap();
- dataRecord5.put("id", "100000");
- dataRecord5.put("text", "廊坊銀行總行");
- dataRecord5.put("parentId", "");
- HashMap dataRecord6 = new HashMap();
- dataRecord6.put("id", "110000");
- dataRecord6.put("text", "廊坊分行");
- dataRecord6.put("parentId", "100000");
- HashMap dataRecord7 = new HashMap();
- dataRecord7.put("id", "111000");
- dataRecord7.put("text", "廊坊銀行金光道支行");
- dataRecord7.put("parentId", "110000");
- dataList.add(dataRecord1);
- dataList.add(dataRecord2);
- dataList.add(dataRecord3);
- dataList.add(dataRecord4);
- dataList.add(dataRecord5);
- dataList.add(dataRecord6);
- dataList.add(dataRecord7);
- return dataList;
- }
- }
好了,通過上面的程式碼,就可以實現多叉樹的兄弟節點橫向排序和先序遍歷了,實現了將層次資料轉換為有序無限級JSON字串的目的。
在實際的專案中,可以把上面的有效程式碼融入其中,或者在此基礎上進行一些擴充套件:
1、 實現對指定層次的排序(例如只排序第一層的節點,或者只排序某一父節點下的所有子節點)
2、 遍歷輸出樹形結構時可以加入判斷條件過濾掉某些節點
3、 實現節點的刪除功能
4、 在節點類中增加一個父節點的引用,就可以計算出某一節點所處的級別
5、 在不支援層次查詢的資料庫應用系統中使用該演算法實現相同的效果
四、思考與總結
這篇文章的重點是如何構造有序的無限級的樹形結構JSON字串,一次性生成樹形選單,而不是利用AJAX的方式,反覆向伺服器端傳送請求,一級接一級的載入樹節點。
既然可以構造無限級的JSON字串,那麼也可以根據這個思路構造無限級的XML字串,或者構造具有層次結構的UL – LI組合(用UL - LI來展示樹形結構),或者構造具有層次結構的TABLE(用TABLE來展示樹形結構)。如下所示:
(1)XML層次結構
Xml程式碼- <menuGroup id="100000" name="廊坊銀行總行">
- <menuGroup id="110000" name="廊坊分行">
- <menu id="113000" name="廊坊銀行開發區支行">
- </menu>
- <menu id="111000" name="廊坊銀行金光道支行">
- </menu>
- <menuGroup id="112000" name="廊坊銀行解放道支行">
- <menu id="112200" name="廊坊銀行三大街支行">
- </menu>
- <menu id="112100" name="廊坊銀行廣陽道支行">
- </menu>
- </menuGroup>
- </menuGroup>
- </menuGroup>
(2)UL - LI 層次結構
Html程式碼- <ul>
- <li>廊坊銀行總行</li>
- <ul>
- <li>廊坊分行</li>
- <ul>
- <li>廊坊銀行開發區支行</li>
- <li>廊坊銀行解放道支行</li>
- <ul>
- <li>廊坊銀行三大街支行</li>
- <li>廊坊銀行廣陽道支行</li>
- </ul>
- <li>廊坊銀行金光道支行</li>
- </ul>
- </ul>
- </ul>
(3)TABLE層次結構
Html程式碼- <table>
- <tr><td>廊坊銀行總行</td></tr>
- <tr><td> 廊坊分行</td></tr>
- <tr><td> 廊坊銀行開發區支行</td></tr>
- <tr><td> 廊坊銀行解放道支行</td></tr>
- <tr><td> 廊坊銀行三大街支行</td></tr>
- <tr><td> 廊坊銀行廣陽道支行</td></tr>
- <tr><td> 廊坊銀行金光道支行</td></tr>
- </table>
另外對TreeGrid樹形表格也有一定的價值:
1、 一次性構造樹形表格,實現資料分級展示
2、 通過更換比較器,實現對不同表格列的全排序(全排序指的是對所有頁的資料進行排序,而不是隻對當前頁的資料排序)
3、 實現對樹形表格的完整分頁(每次分頁時,只取固定數目的第一層節點,之後呼叫toString方法,展示出完 整條數的分級資料)
五、參考書籍
1、Mark Allen Weiss,資料結構與演算法分析(Java語言描述)
2、Bruce Eckel,Thinking In Java Third Edition
3、David Flanagan,JavaScript: The Definitive Guide, 5th Edition
4、OCA Oracle Database 11g SQL Fundamentals I Exam Guide
六、聯絡方式