Java實現(xiàn)樹形List與扁平List互轉的示例代碼
背景:在平時的開發(fā)中,我們時常會遇到下列場景
- 公司的組織架構的數據存儲與展示
- 文件夾層級的數據存儲與展示
- 評論系統(tǒng)中,父評論與諸多子評論的數據存儲與展示
- ......
對于這種有層級的結構化數據,就像是一棵樹一樣。在關系型數據庫中,通常將一個個的節(jié)點信息存儲到表中,通過一個字段(例如,pid),指向其父節(jié)點。而在數據展示的時候,我們又希望它是呈現(xiàn)層級的,例如:
id name pid 1 總公司 -1 2 上海分公司 1 3 浙江分公司 1 4 閔行區(qū)分公司 2 5 嘉興分公司 3 { "id": 1, "name": "總公司", "pid": -1, "branch": [ { "id": 2, "name": "上海分公司", "pid": 1, "branch": [ { "id": 4, "name": "閔行區(qū)分公司", "pid": 2, "branch": [] } ] }, { "id": 3, "name": "浙江分公司", "pid": 1, "branch": [ { "id": 5, "name": "嘉興分公司", "pid": 3, "branch": [] } ] } ] }
所以,本文的主要內容就是提供幾種方案,實現(xiàn)這兩類數據的轉換方式。
存儲樹的表結構
如何在一張數據庫表中表示一顆樹結構中的所有節(jié)點信息,這里有一個示例:
DROP TABLE IF EXISTS zj_city; CREATE TABLE zj_city ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(50) COMMENT '節(jié)點名稱', pid int NOT NULL COMMENT '父節(jié)點', create_time DATETIME DEFAULT now() COMMENT '創(chuàng)建時間: 年-月-日 時:分:秒', update_time DATETIME DEFAULT now() ON UPDATE now() COMMENT '更新時間', is_deleted BIT DEFAULT 0 COMMENT '是否刪除:0-false-未刪除;1-true-已刪除', PRIMARY KEY (id) )COMMENT '浙江城市'; INSERT INTO zj_city(name, pid) VALUES ('浙江省',0), ('金華市',1), ('嘉興市',1), ('杭州市',1), ('寧波市',1); INSERT INTO zj_city(name, pid) VALUES ('下城區(qū)',4), ('錢塘區(qū)',4), ('西湖區(qū)',4), ('上城區(qū)',4); INSERT INTO zj_city(name, pid) VALUES ('南湖區(qū)',3), ('秀洲區(qū)',3), ('桐鄉(xiāng)市',3), ('平湖市',3), ('海寧市',3); INSERT INTO zj_city(name, pid) VALUES ('梧桐街道',12), ('鳳鳴街道',12), ('龍翔街道',12), ('崇福鎮(zhèn)',12), ('烏鎮(zhèn)鎮(zhèn)',12), ('高橋鎮(zhèn)',12), ('河山鎮(zhèn)',12), ('濮院鎮(zhèn)',12); SELECT * from zj_city;
扁平List轉樹形List
應用場景
- 公司組織結構
- 省市級
- 評論系統(tǒng)中,父評論與諸多子評論
- 其他層級展示的數據
雙層for
主要思想:外層循環(huán)-找父節(jié)點;內層循環(huán)-找子節(jié)點;因為每個元素都會找一遍,所有最終得到完整的樹
import com.alibaba.fastjson.JSON; import com.ks.boot.entity.CityEntity; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.List; public class TreeListDemo { List<CityEntity> cityEntities; /** * id name pid * 1 浙江 0 * 2 杭州 1 * 3 嘉興 1 * 4 南湖 3 * 5 桐鄉(xiāng) 3 * 6 余杭 2 * 7 西湖 2 * 8 云南 0 * 9 昆明 8 * 10 昭通 8 */ @BeforeEach public void init() { cityEntities = JSON.parseArray("[{\"id\":1,\"name\":\"浙江\",\"pid\":0},\n" + "{\"id\":2,\"name\":\"杭州\",\"pid\":1},\n" + "{\"id\":3,\"name\":\"嘉興\",\"pid\":1},\n" + "{\"id\":4,\"name\":\"南湖\",\"pid\":3},\n" + "{\"id\":5,\"name\":\"桐鄉(xiāng)\",\"pid\":3},\n" + "{\"id\":6,\"name\":\"余杭\",\"pid\":2},\n" + "{\"id\":7,\"name\":\"西湖\",\"pid\":2},\n" + "{\"id\":8,\"name\":\"云南\",\"pid\":0},\n" + "{\"id\":9,\"name\":\"昆明\",\"pid\":8},\n" + "{\"id\":10,\"name\":\"昭通\",\"pid\":8}]", CityEntity.class); } @Test public void testList2Tree() { List<CityEntity> resultTree = list2Tree(this.cityEntities); System.out.println(JSON.toJSONString(resultTree)); } /** * 雙層for,List轉Tree * 主要思想:外層循環(huán)-找父節(jié)點;內層循環(huán)-找子節(jié)點;因為每個元素都會找一遍,所有最終得到完整的樹 * 時間復雜度:N^2;空間復雜度:N */ public List<CityEntity> list2Tree(List<CityEntity> cityEntities) { List<CityEntity> resultCities = new ArrayList<>(); for (CityEntity city : cityEntities) { if(0 == city.getPid()) { //根節(jié)點、頂級節(jié)點,直接放入最終返回結果的List resultCities.add(city); } for (CityEntity curCity : cityEntities) { //根據當前city找它的子節(jié)點 if(city.getId().equals(curCity.getPid())) { if(city.getSubCityList() == null) { //還沒有任何子節(jié)點,new一個空的放進去 city.setSubCityList(new ArrayList<>()); } city.getSubCityList().add(curCity); } } } return resultCities; } } public class CityEntity { private Long id; private String name; private Long pid; private List<CityEntity> subCityList; getter/setter }
遞歸
主要思想:獲取所有根節(jié)點、頂級節(jié)點,再從List中查找根節(jié)點的子節(jié)點;
import com.alibaba.fastjson.JSON; import com.ks.boot.entity.CityEntity; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.List; public class TreeListDemo { List<CityEntity> cityEntities; /** * id name pid * 1 浙江 0 * 2 杭州 1 * 3 嘉興 1 * 4 南湖 3 * 5 桐鄉(xiāng) 3 * 6 余杭 2 * 7 西湖 2 * 8 云南 0 * 9 昆明 8 * 10 昭通 8 */ @BeforeEach public void init() { cityEntities = JSON.parseArray("[{\"id\":1,\"name\":\"浙江\",\"pid\":0},\n" + "{\"id\":2,\"name\":\"杭州\",\"pid\":1},\n" + "{\"id\":3,\"name\":\"嘉興\",\"pid\":1},\n" + "{\"id\":4,\"name\":\"南湖\",\"pid\":3},\n" + "{\"id\":5,\"name\":\"桐鄉(xiāng)\",\"pid\":3},\n" + "{\"id\":6,\"name\":\"余杭\",\"pid\":2},\n" + "{\"id\":7,\"name\":\"西湖\",\"pid\":2},\n" + "{\"id\":8,\"name\":\"云南\",\"pid\":0},\n" + "{\"id\":9,\"name\":\"昆明\",\"pid\":8},\n" + "{\"id\":10,\"name\":\"昭通\",\"pid\":8}]", CityEntity.class); } /** * 遞歸,List轉Tree * 主要思想:獲取所有根節(jié)點、頂級節(jié)點,再從List中查找根節(jié)點的子節(jié)點; * 時間復雜度:N*(1+N)/2,O(N^2),因為遞歸方法中,最好是List的第一元素就是要找的子節(jié)點,最壞是 * List的最后一個元素是子節(jié)點 */ @Test public void testList2Tree() { List<CityEntity> resultCities = new ArrayList<>(); for (CityEntity city : cityEntities) { if(0 == city.getPid()) { //獲取所有根節(jié)點、頂級節(jié)點,再根據根節(jié)點進行遞歸 CityEntity topCity = findChild(cityEntities, city); //此時的topCity已經包含它的所有子節(jié)點 resultCities.add(topCity); } } System.out.println(JSON.toJSONString(resultCities)); } /** * * @param cityEntities 在那個里面找 * @param curCity 找誰的子節(jié)點 * @return curCity的子節(jié)點 */ public CityEntity findChild(List<CityEntity> cityEntities, CityEntity curCity) { for (CityEntity city : cityEntities) { if(curCity.getId().equals(city.getPid())) { if(null == curCity.getSubCityList()) { curCity.setSubCityList(new ArrayList<>()); } CityEntity subChild = findChild(cityEntities, city); //每次遞歸,都從頭開始查找有沒有city的子節(jié)點 curCity.getSubCityList().add(subChild); } } return curCity; } } public class CityEntity { private Long id; private String name; private Long pid; private List<CityEntity> subCityList; getter/setter }
轉換為Map
主要思想:
- 在雙層for的解法中,由于內層for也需要遍歷以便List,造成時間復雜度上身為平方級
- 如果內層for不需要遍歷完整的List,而是事先預處理到Map中,尋找時直接從Map中獲取,則時間復雜度降為LogN
import com.alibaba.fastjson2.JSON; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; public class TreeListDemo { List<CityEntity> cityEntities; /** * id name pid * 1 浙江 0 * 2 杭州 1 * 3 嘉興 1 * 4 南湖 3 * 5 桐鄉(xiāng) 3 * 6 余杭 2 * 7 西湖 2 * 8 云南 0 * 9 昆明 8 * 10 昭通 8 */ @BeforeEach public void init() { cityEntities = JSON.parseArray("[{\"id\":1,\"name\":\"浙江\",\"pid\":0},\n" + "{\"id\":2,\"name\":\"杭州\",\"pid\":1},\n" + "{\"id\":3,\"name\":\"嘉興\",\"pid\":1},\n" + "{\"id\":4,\"name\":\"南湖\",\"pid\":3},\n" + "{\"id\":5,\"name\":\"桐鄉(xiāng)\",\"pid\":3},\n" + "{\"id\":6,\"name\":\"余杭\",\"pid\":2},\n" + "{\"id\":7,\"name\":\"西湖\",\"pid\":2},\n" + "{\"id\":8,\"name\":\"云南\",\"pid\":0},\n" + "{\"id\":9,\"name\":\"昆明\",\"pid\":8},\n" + "{\"id\":10,\"name\":\"昭通\",\"pid\":8}]", CityEntity.class); } /** * 在雙層for的解法中,由于內層for也需要遍歷以便List,造成時間復雜度上身為平方級 * 如果內層for不需要遍歷完整的List,而是事先預處理到Map中,尋找時直接從Map中獲取,則時間復雜度降為LogN */ @Test public void list2tree() { //收集每個PID下的節(jié)點為Map Map<Long, List<CityEntity>> cityMapByPid = cityEntities.stream().collect(Collectors.groupingBy(CityEntity::getPid)); List<CityEntity> resultCityList = new ArrayList<>(); for (CityEntity city : cityEntities) { if(0 == city.getPid()) { //根節(jié)點、頂點 resultCityList.add(city); } List<CityEntity> citiesByPid = cityMapByPid.get(city.getId()); if(null != citiesByPid && citiesByPid.size() > 0) { //有子節(jié)點 if(null == city.getSubCityList()) { city.setSubCityList(new ArrayList<>()); } city.getSubCityList().addAll(citiesByPid); //塞入 } } System.out.println(JSON.toJSONString(resultCityList)); } /** * 簡化寫法:在收集到Map時,對于沒有子節(jié)點的節(jié)點,創(chuàng)建一個空的塞入到Map中 */ @Test public void list2tree4Simple() { List<CityEntity> resultCityList = new ArrayList<>(); //保存每個PID下的子節(jié)點 Map<Long, List<CityEntity>> cityMapByPid = new HashMap<>(); for (CityEntity city : cityEntities) { //收集每個PID下的子節(jié)點 //獲取當前PID對應的子節(jié)點List,如果沒有則創(chuàng)建一個空的List塞入 //這個設計得很巧妙 List<CityEntity> children = cityMapByPid.getOrDefault(city.getPid(), new ArrayList<>()); children.add(city); //插入當前元素 cityMapByPid.put(city.getPid(), children); } for (CityEntity city : cityEntities) { if(0 == city.getPid()) { //根節(jié)點、頂點 resultCityList.add(city); } city.setSubCityList(cityMapByPid.get(city.getId())); } System.out.println(JSON.toJSONString(resultCityList)); } }
棧
主要思想
收集根節(jié)點、頂級節(jié)點,存入resultList,并且壓棧
循環(huán)出棧,棧元素Cur
- 找Cur的所有子元素為child
- 如果child不為空,則再壓入棧中。這一步的目的是,再一次找child的子元素
import com.alibaba.fastjson2.JSON; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.*; import java.util.stream.Collectors; public class TreeListDemo { List<CityEntity> cityEntities; /** * id name pid * 1 浙江 0 * 2 杭州 1 * 3 嘉興 1 * 4 南湖 3 * 5 桐鄉(xiāng) 3 * 6 余杭 2 * 7 西湖 2 * 8 云南 0 * 9 昆明 8 * 10 昭通 8 */ @BeforeEach public void init() { cityEntities = JSON.parseArray("[{\"id\":1,\"name\":\"浙江\",\"pid\":0},\n" + "{\"id\":2,\"name\":\"杭州\",\"pid\":1},\n" + "{\"id\":3,\"name\":\"嘉興\",\"pid\":1},\n" + "{\"id\":4,\"name\":\"南湖\",\"pid\":3},\n" + "{\"id\":5,\"name\":\"桐鄉(xiāng)\",\"pid\":3},\n" + "{\"id\":6,\"name\":\"余杭\",\"pid\":2},\n" + "{\"id\":7,\"name\":\"西湖\",\"pid\":2},\n" + "{\"id\":8,\"name\":\"云南\",\"pid\":0},\n" + "{\"id\":9,\"name\":\"昆明\",\"pid\":8},\n" + "{\"id\":10,\"name\":\"昭通\",\"pid\":8}]", CityEntity.class); } /** * 主要思想: * 收集根節(jié)點、頂級節(jié)點,存入resultList,并且壓棧 * 循環(huán)出棧,棧元素Cur * 找Cur的所有子元素為child * 如果child不為空,則再壓入棧中。這一步的目的是,再一次找child的子元素 * 時間復雜度:N(過濾出所有跟節(jié)點) + 常數(出棧) * N(遍歷List找當前節(jié)點的子元素) */ @Test public void list2tree() { List<CityEntity> resultCityList = new ArrayList<>(); Stack<CityEntity> stack = new Stack<>(); resultCityList = cityEntities.stream().filter(ele -> 0 == ele.getPid()).collect(Collectors.toList()); stack.addAll(resultCityList); //根節(jié)點、頂點入棧 while(!stack.isEmpty()) { CityEntity curCity = stack.pop(); List<CityEntity> child = cityEntities.stream().filter(ele -> curCity.getId().equals(ele.getPid())).collect(Collectors.toList()); if(!child.isEmpty()) { //這一步處理的原因是:當沒有子元素,不顯示該個字段。流處理后沒有元素只會返回空List,不會返回null curCity.setSubCityList(child); } if(!child.isEmpty()) { stack.addAll(child); } } System.out.println(JSON.toJSONString(resultCityList)); } }
樹形List轉扁平List
遞歸
主要思想:遍歷樹節(jié)點,一個樹節(jié)點如果有子樹,則再次遞歸此子樹,直到沒有子樹為止
import com.alibaba.fastjson2.JSON; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.List; /** * id name pid * 1 浙江 0 * 2 杭州 1 * 3 嘉興 1 * 4 南湖 3 * 5 桐鄉(xiāng) 3 * 6 余杭 2 * 7 西湖 2 * 8 云南 0 * 9 昆明 8 * 10 昭通 8 */ public class ListTreeDemo { List<CityEntity> treeList; @BeforeEach public void init() { treeList = JSON.parseArray("[{\"id\":1,\"name\":\"浙江\",\"pid\":0,\"subCityList\":[" + "{\"id\":2,\"name\":\"杭州\",\"pid\":1,\"subCityList\":[{\"id\":6,\"name\":\"余杭\",\"pid\":2},{\"id\":7,\"name\":\"西湖\",\"pid\":2}]}," + "{\"id\":3,\"name\":\"嘉興\",\"pid\":1,\"subCityList\":[{\"id\":4,\"name\":\"南湖\",\"pid\":3},{\"id\":5,\"name\":\"桐鄉(xiāng)\",\"pid\":3}]}]}," + "{\"id\":8,\"name\":\"云南\",\"pid\":0,\"subCityList\":[{\"id\":9,\"name\":\"昆明\",\"pid\":8},{\"id\":10,\"name\":\"昭通\",\"pid\":8}]}]", CityEntity.class); } @Test public void tree2list() { List<CityEntity> resList = new ArrayList<>(); //這一層for的目的是:遍歷根節(jié)點 for (CityEntity city : treeList) { reversion(city,resList); } System.out.println(JSON.toJSONString(resList)); } public void reversion(CityEntity curNode, List<CityEntity> resList) { resList.add(beanCopy(curNode)); List<CityEntity> subCityList = curNode.getSubCityList(); if(subCityList != null && !subCityList.isEmpty()) { for (CityEntity city : subCityList) { //遞歸尋找子節(jié)點的子節(jié)點們 reversion(city, resList); } } //遞歸的出口就是subCityList為null或者empty } private CityEntity beanCopy(CityEntity source) { CityEntity res = new CityEntity(); res.setId(source.getId()); res.setName(source.getName()); res.setPid(source.getPid()); return res; } }
棧
主要思想
依次遍歷樹形List,當前節(jié)點為Cur
- 將Cur收集到某個存儲結果的List
- 如果Cur有子樹,壓入某個棧中
依次彈出棧元素,當前彈出的元素為StackSubTree
- 如果StackSubTree還有子樹,繼續(xù)壓棧
- 如果StackSubTree沒有子樹,則放入結果List
import com.alibaba.fastjson2.JSON; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.List; import java.util.Stack; /** * id name pid * 1 浙江 0 * 2 杭州 1 * 3 嘉興 1 * 4 南湖 3 * 5 桐鄉(xiāng) 3 * 6 余杭 2 * 7 西湖 2 * 8 云南 0 * 9 昆明 8 * 10 昭通 8 */ public class ListTreeDemo { List<CityEntity> treeList; @BeforeEach public void init() { treeList = JSON.parseArray("[{\"id\":1,\"name\":\"浙江\",\"pid\":0,\"subCityList\":[" + "{\"id\":2,\"name\":\"杭州\",\"pid\":1,\"subCityList\":[{\"id\":6,\"name\":\"余杭\",\"pid\":2},{\"id\":7,\"name\":\"西湖\",\"pid\":2}]}," + "{\"id\":3,\"name\":\"嘉興\",\"pid\":1,\"subCityList\":[{\"id\":4,\"name\":\"南湖\",\"pid\":3},{\"id\":5,\"name\":\"桐鄉(xiāng)\",\"pid\":3}]}]}," + "{\"id\":8,\"name\":\"云南\",\"pid\":0,\"subCityList\":[{\"id\":9,\"name\":\"昆明\",\"pid\":8},{\"id\":10,\"name\":\"昭通\",\"pid\":8}]}]", CityEntity.class); } /** * 1. 依次遍歷樹形List,當前節(jié)點為Cur * a) 將Cur收集到某個存儲結果的List * b) 如果Cur有子樹,壓入某個棧中 * 2. 依次彈出棧元素,當前彈出的元素為StackSubTree * a) 如果StackSubTree還有子樹,繼續(xù)壓棧 * b) 如果StackSubTree沒有子樹,則放入結果List */ @Test public void tree2list() { List<CityEntity> resList = new ArrayList<>(); Stack<List<CityEntity>> stack = new Stack<>(); for (CityEntity curCity : treeList) { resList.add(beanCopy(curCity)); if (curCity.getSubCityList() != null && !curCity.getSubCityList().isEmpty()) { stack.push(curCity.getSubCityList()); } } while (!stack.isEmpty()) { List<CityEntity> subTree = stack.pop(); for (CityEntity city : subTree) { if (city.getSubCityList() != null && !city.getSubCityList().isEmpty()) { stack.push(city.getSubCityList()); } else { resList.add(beanCopy(city)); } } } System.out.println(JSON.toJSONString(resList)); } private CityEntity beanCopy(CityEntity source) { CityEntity res = new CityEntity(); res.setId(source.getId()); res.setName(source.getName()); res.setPid(source.getPid()); return res; } }
到此這篇關于Java實現(xiàn)樹形List與扁平List互轉的示例代碼的文章就介紹到這了,更多相關Java樹形List與扁平List互轉內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Quarkus中ConfigSourceInterceptor的加密配置實現(xiàn)
這篇文章主要為大家介紹Quarkus中ConfigSourceInterceptor加密配置的實現(xiàn)方式,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-02-02