用 JSON 表现树的结构兼谈队列、堆栈的练习(二)

  1. 云栖社区>
  2. 博客>
  3. 正文

用 JSON 表现树的结构兼谈队列、堆栈的练习(二)

sp42 2017-08-03 23:34:00 浏览1153
展开阅读全文

树的查找

查找,又叫作搜索 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;
}

完整代码在:http://git.oschina.net/sp42/ajaxjs/blob/master/ajaxjs-base/src/com/ajaxjs/util/collection/JsonStruTraveler.java

单测:http://git.oschina.net/sp42/ajaxjs/blob/master/ajaxjs-base/src/test/com/ajaxjs/util/collection/TestStruTravler.java

首先拆分输入的字符串,变为队列( 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 是重复字符串多少次的函数,比较简单,这里就不贴出了。

网友评论

登录后评论
0/500
评论
sp42
+ 关注