树的查找
查找,又叫作搜索 search。查找跟遍历的概念不同,遍历是全部的节点都要走一遍,而查找,找到目标节点就立刻返回,不会继续遍历了。当然,如果什么都没查找到,就是一次完整的遍历过程了。
查找的依据是什么?假设我们对 K/V 结构,也就是 Map,对其增加 id 字段,用来作为查找的依据,那么用户输入如果符合 id 就返回节点,表示查找成功。
单层的查找很好做,在递归的函数里 if(id = map.get("id") ) 判断一下就可以了。JSON 是多层的,怎么做多层的呢?
还是那个 json 例子,已经有 id 字段存在了。我们希望输入查找条件如 "product/new/yuecai" 返回 {id:yuecai, name : 粤菜} 这个节点。
调用方式如下
JsonStruTraveler t = new JsonStruTraveler();
assertEquals("关于我们", t.findByPath("about", list).get("name"));
assertEquals("企业文化", t.findByPath("about/cluture", list).get("name"));
assertEquals("粤菜", t.findByPath("product/new/yuecai", list).get("name"));
findByPath 返回是 map 类型,就是这个节点。如果你有了解过 JSONPath,其实是有点相似的。
由于水平有限,当前只能做的从根节点一步一步查找,非常简单的原理。不支持从非第一级节点开始的查找。
查找的原理
原理还是非常简单,依然是递归的调用。不过在输入条件的处理上,使用了一点技巧,就是队列的运用。
/**
*
* 根据路径查找节点
* @param str
* Key 列表字符
* @param list
* 列表
* @return Map
*/
public Map<String, Object> findByPath(String str, List<Map<String, Object>> list) {
if (str.startsWith("/"))
str = str.substring(1, str.length());
if (str.endsWith("/"))
str = str.substring(0, str.length() - 1);
String[] arr = str.split("/");
Queue<String> q = new LinkedList<>(Arrays.asList(arr));
return findByPath(q, list);
}
/**
* 真正的查找函数
*
* @param stack
* 栈
* @param list
* 列表
* @return Map
*/
@SuppressWarnings("unchecked")
private Map<String, Object> findByPath(Queue<String> queue, List<Map<String, Object>> list) {
Map<String, Object> map = null;
while (!queue.isEmpty()) {
map = findMap(list, queue.poll());
if (map != null) {
if (queue.isEmpty()) {
break;// found it!
} else if (map.get(children) != null) {
map = findByPath(queue, (List<Map<String, Object>>) map.get(children));
} else {
map = null;
}
}
}
return map;
}
首先拆分输入的字符串,变为队列( Queue 窄化了 LinkedList)。当 findMap 函数找到有节点,此时已消耗了队列的一个元素(queue.poll())。如果队列已经空了,表示已经查找完毕,返回节点。否则再看看下级节点 children 是否有,这一步是递归的调用,私有函数 private Map<String, Object> findByPath 就是专门为递归用的函数。
如果不使用队列,也可以完成,但就比较累,要自己写多几行代码控制,就操作如下代码。
最开始的时候,我先想到的不是 队列,而是 栈。后来想想了,生成栈的过程比较繁琐,因为字符串 split 之后,是要反转数组才能变为栈的:
String[] arr = str.split("/");
Stack<String> stack = new Stack<>();
for (int i = arr.length - 1; i >= 0; i--)
stack.push(arr[i]);
而后来想了下既然要反转数组,那不与直接使用与栈相反的 队列 好了。这里用栈或是队列是有差别的但差别不大,我们只是避免手写更多的行数来控制数组,对精简代码有好处。
扁平化 Map
JSON 树变成 MapList 之后,仍然是多层结构,如访问其中某个值(注意不是“节点”),必须逐级别访问。每级访问时都要判断对象是否为空,这就是要避免的 Java 空指针问题。例如 ((Map<String, Object>)list.get(0)).get("level"),比较繁琐。于是,我们希望可以写一个表达式就可以直接访问某个节点。上述如 product/new/yuecai 路径的方式是一种思路;这里再介绍一种不同的方式,它把 / 变成 . 如 product.new.yuecai,而且内部实现完全不同,速度会快很多。这就是扁平化 Map。
之所以强调扁平化 Map 中的 Map ,其意思是只强调处理 K/V 的 Map,输入类型要求是 Map,对于遇到 List 类型的 Value 不会去区分那是值还是子节点,一律当作下一级的子节点集合处理。这与“product/new/yuecai” 指定 children 项的方式来处理子集合稍有区别的,后者要求输入的是 List。而且前者通常是返回某个值,后者返回的是节点(Map),返回的内容其意义不一样。
首先定义内部接口 TravelMapList_Iterator,有三个方法:
/**
* 遍历 MapList 的回调
* @author Frank Cheung frank@ajaxjs.com
*/
public static interface TravelMapList_Iterator {
/**
* 当得到了 Map 的 key 和 value 时调用
*
* @param key 键名称
* @param obj 键值
*/
public void handler(String key, Object obj);
/**
* 当得到一个新的 key 时候
* @param key 键名称
*/
public void newKey(String key);
/**
* 当退出一个当前 key 的时候
* @param key 键名称
*/
public void exitKey(String key);
}
接着定义递归函数遍历 MapList
/**
* 遍历 MapList,允许 TravelMapList_Iterator 控制
*
* @param map
* 输入 Map
* @param iterator
* 回调
*/
@SuppressWarnings("unchecked")
public static void travelMapList(Map<String, Object> map, TravelMapList_Iterator iterator) {
for (String key : map.keySet()) {
Object obj = map.get(key);
if (iterator != null)
iterator.handler(key, obj);
if (obj != null) {
if (obj instanceof Map) {
if (iterator != null)
iterator.newKey(key);
travelMapList((Map<String, Object>) obj, iterator);
if (iterator != null)
iterator.exitKey(key);
} else if (obj instanceof List) {
List<Object> list = (List<Object>) obj;
for (Object item : list) {
if (item != null && item instanceof Map)
travelMapList((Map<String, Object>) item, iterator);
}
}
}
}
}
TravelMapList_Iterator 到底有什么用?说到底是为了进栈和退栈用的。堆栈有后入先出的特性,在这里使用最适合不过了——用来记录所在层级。
/**
* 扁平化多层 map 为单层
* @param map
* @return
*/
public static Map<String, Object> flatMap(Map<String, Object> map) {
final Stack<String> stack = new Stack<>();
final Map<String, Object> _map = new HashMap<>();
travelMapList(map, new TravelMapList_Iterator() {
@Override
public void handler(String key, Object obj) {
if (obj == null || obj instanceof String || obj instanceof Number || obj instanceof Boolean) {
StringBuilder sb = new StringBuilder();
for (String s : stack) {
sb.append(s + ".");
}
_map.put(sb + key, obj);
// System.out.println(sb + key + ":" + obj);
}
}
@Override
public void newKey(String key) {
stack.add(key); // 进栈
}
@Override
public void exitKey(String key) {
stack.pop(); // 退栈
}
});
return _map;
}
外界调用 API 其实就是这个 flatMap 方法。
调用例子:
Map<String, Object> f_map = JsonStruTraveler.flatMap(map);
assertEquals(7, f_map.get("data.jobCatalog_Id"));
输入 JSON
{
"site" : {
"titlePrefix" : "大华•川式料理",
"keywords" : "大华•川式料理",
"description" : "大华•川式料理饮食有限公司于2015年成立,本公司目标致力打造中国新派川菜系列。炜爵爷川菜料理系列的精髓在于清、鲜、醇、浓、香、烫、酥、嫩,擅用麻辣。在服务出品环节上,团队以ISO9000为蓝本建立标准化餐饮体系,务求以崭新的姿态面向社会各界人仕,提供更优质的服务以及出品。炜爵爷宗旨:麻辣鲜香椒,美味有诀窍,靓油用一次,精品煮御赐。 ",
"footCopyright":"dsds"
},
"dfd":{
"dfd":'fdsf',
"id": 888,
"dfdff":{
"dd":'fd'
}
},
"clientFullName":"大华•川式料理",
"clientShortName":"大华",
"isDebug": true,
"data" : {
"newsCatalog_Id" : 6,
"jobCatalog_Id" :7
}
}
最后得到 map 结构是这样子的,根节点的话则没有 . (点)。
由于是 map 查找,所以查找的速度会很快。
格式化 JSON
如果对栈的技术不甚明了,可以尝试通过格式化 JSON 这个例子去理解。尽管这例子没有使用的栈,但是有相仿的地方,都是异曲同工的,大家可以细细品味。
/**
* 格式化 JSON,使其美观输出到控制或其他地方 请注意 对于json中原有\n \t 的情况未做过多考虑 得到格式化json数据 退格用\t
* 换行用\r
*
* @param json
* 原 JSON 字符串
* @return 格式化后美观的 JSON
*/
public static String format(String json) {
int level = 0;
StringBuilder str = new StringBuilder();
for (int i = 0; i < json.length(); i++) {
char c = json.charAt(i);
if (level > 0 && '\n' == str.charAt(str.length() - 1))
str.append(StringUtil.repeatStr("\t", "", level));
switch (c) {
case '{':
case '[':
str.append(c + "\n");
level++;
break;
case ',':
if (json.charAt(i + 1) == '"')
str.append(c + "\n"); // 后面必定是跟着 key 的双引号,但 其实 json 可以 key 不带双引号的
break;
case '}':
case ']':
str.append("\n");
level--;
str.append(StringUtil.repeatStr("\t", "", level));
str.append(c);
break;
default:
str.append(c);
break;
}
}
return str.toString();
}
其中,level 表示缩进数,level++ 可看着“进栈”的操作,反之,level-- 为“退栈”,这一退一进是不是与栈的 push/pop 很像?
StringUtil.repeatStr 是重复字符串多少次的函数,比较简单,这里就不贴出了。