“舅,这次要给我讲解啥子函数式?” YY 问道
“简单点,知道你java基础不错,那就来个 java实现统计单词频率问题” 我答道
题目
给定一段英文,找出每个单词使用的频率, 按照String字典顺序排序,并打印出所有单词及其频率的排序列表。如: “My name is …, I come from …, I am … years old!”
结果为:
am 1
come 1
from 1
i 2
is 1
my 1
name 1
old 1
years 1
【木丁糖 http://blog.youkuaiyun.com/shrimpcolo 未经允许严禁转载,请尊重作者劳动成果。[Q群联系我:631353571】
“舅, 你不是说你会Kotlin嘛, 秀一下呗?” YY 挑衅的说。
“哟,可以啊,激将! 没事,那就秀一段给你瞧下, 看好了” 说完,只听见键盘敲击声。不一会,代码展示如下:
Kotlin实现
fun main(args: Array<String>) {
val wordString = "My name is ..., I come from ..., I_m... he's ... 10 years old! my"
wordString
.toLowerCase() //*转小写
.replace(Regex("[^a-z0-9A-Z\\s]"), "") //*替换不是字母数字空格的字符为 ""
.split(" ") //*空格分词
.filter { it.matches(Regex("\\w+")) } //*过滤单词
.sortedBy { it } //排序
.groupBy { it } //分组
.toList() // 转换为列表
.sortedByDescending { it.second.size } // 按照词频高低排序
.forEach { (key, value) -> println("$key: ${value.size}") } //打印
}
结果如下: 最酷炫的是,在一个长长的链式中实现了我们所要的功能。统计词频,按照词频高低排序,词频相同的按照字母字典排序。
my: 2
10: 1
come: 1
from: 1
hes: 1
i: 1
im: 1
is: 1
name: 1
old: 1
years: 1
YY 脑袋凑到我跟前,盯着屏幕,不由的发出 “喔 **, Kotlin这么简洁,而且还是实现了 按照词频 & 字母顺序排序, 牛 x”
“那是,也不看下是谁写的, 你看到思路了没?” 转头我问YY
“大概看懂了”YY回复说
“说说看” 我再次给YY挖坑
“喏,不都是写了注释么? ” YY 指着注释跟我说
“晕~~, 思路是清晰的,也就是其中的排序稍微麻烦些,其他没有难点”我鄙视的说
“YY,那你说说,Java的思路?” 我挑衅的问到
“没问题,只不过我的慢慢来实现” YY 回复到
【木丁糖 http://blog.youkuaiyun.com/shrimpcolo 未经允许严禁转载,请尊重作者劳动成果。[Q群联系我:631353571】
思路
给定一段英文,首先需要识别其中的每个单词,再统计每个单词出现的频率,再对单词进行字母顺序排列,并打印其中出现的频率。这里给出一个可行的思路:
1. 函数传入(String), 将单词放到Map<String, Integer>
中, 使用正则表达式识别单词
2. 遍历所有找到单词, 将首次遇到单词 加入Map, 将重复遇到单词的出现次数加 1
3. 遍历打印 key, value
【木丁糖 http://blog.youkuaiyun.com/shrimpcolo 未经允许严禁转载,请尊重作者劳动成果。[Q群联系我:631353571】
Java版本
YY说出了他的思路:
给定一段英文,首先需要识别其中的每个单词,再统计每个单词出现的频率,再对单词进行字母顺序排列,并打印其中出现的频率。
1. 函数传入(String), 将单词放到Map<String, Integer>
中, 使用正则表达式识别单词
2. 遍历所有找到单词, 将首次遇到单词 加入Map, 将重复遇到单词的出现次数加 1
3. 遍历打印 key, value
private static Map<String, Integer> wordFreq(String wordsString) {
TreeMap<String, Integer> wordMap = new TreeMap<>();
Matcher matcher = Pattern.compile("\\w+").matcher(wordsString);
while (matcher.find()) {
String word = matcher.group().toLowerCase();
if (wordMap.get(word) == null) {
wordMap.put(word, 1);
}else {
wordMap.put(word, wordMap.get(word) + 1);
}
}
return wordMap;
}
“TreeMap 有自动排序功能, Matcher实现分词。 该方法将输入的String 中单词统计并放入到TreeMap中,其中的Key,是单词本身,value,单词的次数 ” YY 解释道
打印方式如下:
String wordsTemp = "My name is ..., I come from ..., I am ... years old!";
for (Entry<String, Integer> entry : wordFreq(wordsTemp).entrySet()) {
out.println(entry.getKey() + " " + entry.getValue());
}
舅点评:
“YY, Java传统实现中,你这个算是比较优雅的实现了,是Java高级使用。但是,这是命令方式的实现“
”怎么说?“ YY 疑惑的问
”没有发现? 我们需要深入到细节里面, 每个小细节都需要自己实现。第一步做什么,第二部做什么,为了效率,最好都在一个循环内实现。而且,你还偷懒了 把正则表达式匹配单词 写成【”\w+”】, 这个算是细节了,就不追究了“ 我故作恐吓的数落YY
”我都说了,我是个半吊子,没法跟大神比“ YY 辩解道
”莫嘴贫,你试着用java 8 实现下?“ 我回复道
”OK, 我就知道你会说java 8,我会!“ YY 故作镇定的答复
【木丁糖 http://blog.youkuaiyun.com/shrimpcolo 未经允许严禁转载,请尊重作者劳动成果。[Q群联系我:631353571】
Java 8实现
”我是参考: Java 8 中的 Streams API 详解 这篇文章熟悉 Java 8 Stream 常用API的 ” YY 首先解释下
“霍, 这么厉害,还找到这篇参考文章,可以嘛” 我鼓励着YY
YY 给出了他自己的java 8 思路。
函数式思路
YY没有刻意去记Stream 的各种API,只是慢慢的接触这些API,再慢慢使用就可以了。我更在乎YY的思路。针对题目来说,YY跳出之前的思维框框。以下列文字为例, 找出其中的单词,并统计出现的频率?
String wordsTemp = “My name is …, I come from …, I’m 10 … years old! my”;
函数式,有个特点,高度抽象,怎么理解? 不要深入实现的细节,这里的细节可以让Java 8 Stream帮你实现。
对于这个题目,思路可以抽象为:
1. 分词 -> 将一句话中的每个单词分开, 单词之间是以: ” “分开
2. 过滤 -> 过滤不是单词的符号
3. 排序 -> 按照字母字典排序
4. 单词分组统计
不一会儿,YY给出了他的java 8 版本一
【木丁糖 http://blog.youkuaiyun.com/shrimpcolo 未经允许严禁转载,请尊重作者劳动成果。[Q群联系我:631353571】
java 8 版本一
private static void textCount(@NotNull String text) {
Arrays.stream(text.toLowerCase() //小写
.split(" ")) //分词
.filter(word -> word.matches("\\w+")) //过滤
.sorted(Comparator.naturalOrder()) //字典排序
// .forEach(s -> out.println(s)); //调试打印
.collect(groupingBy(word -> word)) //分组归类
.forEach((key, value) -> out.println(key + ": " + value.size())); //打印 key, value
}
我指出了 YY 遇到的问题
注意 这个版本是有问题的, 不信看结果。结果少了 I’m, old! 为什么?
name: 1
i: 1
is: 1
from: 1
come: 1
my: 2
years: 1
10: 1
这是因为少考虑了替换,需要把某些字符 ’ !消灭掉,所以,在分词之前加入替换的正则表达式。
【木丁糖 http://blog.youkuaiyun.com/shrimpcolo 未经允许严禁转载,请尊重作者劳动成果。[Q群联系我:631353571】
版本二
private static void textCount(@NotNull String text) {
Arrays.stream(text.toLowerCase()
.replaceAll("[^a-z0-9A-Z\\s]", "") //*新增替换特殊字符*
.split(" "))
.filter(word -> word.matches("\\w+"))
.sorted(Comparator.naturalOrder())
// .forEach(s -> out.println(s));
.collect(groupingBy(word -> word))
.forEach((key, value) -> out.println(key + ": " + value.size()));
}
结果:
im: 1
old: 1
name: 1
i: 1
is: 1
from: 1
come: 1
my: 2
years: 1
10: 1
我还是看到YY的问题: 最后的 (key, value) (单词 单词频率) 没有排序,其实在 .sorted(Comparator.naturalOrder())
后,排序好了,但是 .collect(groupingBy(word -> word))
破坏了, groupingBy并不是字典顺序来分组,他是乱序的。而且还因为 collect
返回值不是Stream, 导致无法再次使用 【.】 去做进一步的函数调用,所以 这里没有办法使用一个函数调用链 来实现。只能把 Arrays.stream
的返回值保存到 一个Map中,然后再使用Map迭代方式去排序再打印。参考实现:
private static void textCount(@NotNull String text) {
Map<String, List<String>> map = Arrays.stream(text.toLowerCase()
.replaceAll("[^a-z0-9A-Z\\s]", "")
.split(" "))
.filter(word -> word.matches("\\w+"))
.sorted(Comparator.naturalOrder())
.collect(groupingBy(word -> word));
TreeMap<String, Integer> treeMap = new TreeMap<>();
for(Entry<String, List<String>> entry : map.entrySet()) {
treeMap.put(entry.getKey(), entry.getValue().size());
}
treeMap.forEach((key, value) -> out.println(key + " " + value));
}
“YY, 你完成的不错,虽然不能像Kotlin那样一个链式就搞定了,那是java 8 的问题,而不是你的问题,你已经上道了,后续多练习总结,超越我指日可待” 我故意夸夸YY
“不用给安慰,还是有差距的啊!”YY 认真的说
“看你颓废样,大神又不是说出来的,是真刀真枪干出来的” 我鼓励的说到
【木丁糖 http://blog.youkuaiyun.com/shrimpcolo 未经允许严禁转载,请尊重作者劳动成果。[Q群联系我:631353571】