本博文从一道华为上机题开始,题目如下:
将一个字符中所有出现的数字前后加上符号“*”,其他字符保持不变
public static String MarkNum(String pInStr){
return null;
}
输入描述:
输入一个字符串
输出描述:
字符中所有出现的数字前后加上符号“*”,其他字符保持不变
输入例子:
Jkdi234klowe90a3
输出例子:
Jkdi*234*klowe*90*a*3*
拿到这个题目,我的第一反应是想使用正则表达式,然而可是but凭着自己以前看过的可怜巴巴的一点知识折腾了半天,竟然还不能拿到结果,知识掌握的不够深刻。所以,这篇博文的主要目的是扫盲(不涉及正则表达式的效率问题),首先回顾regular expression的一些基础知识,然后详细介绍并举例java.util.regex包下Pattern类、Matcher类用法,以及String类中与RegEx相关的方法和用法,最后给出上述题目以及2017年阿里实习生招聘笔试题中第一个编程题的正则表达式代码。
RegEx
.:匹配换行符以外的任意字符。
?:其前面的内容0次或1次出现。
*:其前面的内容0次或多次出现。
+:其前面的内容1次或多次出现。
^:匹配开头位置。
$:匹配结尾位置。
|:或,(a|b)匹配a或b。
[]:匹配[]中的任意一个字符。
[^]:匹配除[^]中的任意一个字符。
[a-z]:匹配a-z中的任意一个字符。
{n}:其前面的内容重复出现n次。
{n,m}:其前面的内容重复出现次数取值[n-m]闭区间。
\b:匹配单词(a-zA-z0-9_组成)边界,即匹配这样的一个位置,一侧是单词的字符,一侧是非单词字符、或者单词的开始结束位置,个人觉得单词的开始结束位置指的匹配位于首位的单词或者末位的单词的情况。
\B:匹配非单词边界。
\d:匹配一位数字。
\D:匹配一位非数字。
\s:匹配任意空白符,包括空格(使用最多)、回车、换行、制表、换页。
\S:匹配非空白符。
\w:匹配单词字符[a-zA-z0-9_]。
\W:匹配非字符。
解释:
- ^和$匹配位置,如果RegEx为:^\d{4}$,则会匹配“6666”,而不匹配"QiYuane6666",RegEx为:\d{4},则都会匹配。
- RegEx:\bcan\b,匹配"I can use scanner"串中的单词can,而不匹配scanner 。注意这个RegEx和欲使用"\scan\s"匹配单词的不同,"\scan\s"只匹配前后有空白符的字符串。
- 欲匹配所有RegEx中的特殊字符使用反斜杠"\"进行转义。或者使用"[]",如"[?]"。亦或是内容放在"\Q""\E"之间将元字符视为非元字符,如"\Q$%#*?\E"。
- 零宽断言:字符串中满足给定断言的位置,匹配内容宽度为0。比如匹配4后不是8的数值字符串,使用"\d*4(?!8)\d*"在任何情况下使用良好,包括4结尾的情况。使用"\bcan\b"匹配单词can,在任何情况下使用良好,包括单词位于串结尾、开头、前后有标点符号的情况等。因为零宽所以该在设计RegEx时不要忘记其他字符,比如匹配字符串"yo yo ",RegEx为"\b(?<yo>\w+)\b\s+\k<yo>\s+",不要忘记空格的匹配。
- 当"?"紧跟在任何一个重复限制符 "*、+、?、{n}、{n,}、{n,m}"后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配字符,默认贪婪模式则尽可能多的匹配字符。例如,对于字符串 "yo yo yo","y.*?o" 匹配 "yo",而 "y.*o" 匹配 "yo yo yo"。
Pattern类
private Pattern(String s, int i)
{
compiled = false;
pattern = s;
flags = i;
if((flags & 256) != 0)
flags |= 64;
capturingGroupCount = 1;
localCount = 0;
if(pattern.length() > 0)
{
compile();
} else
{
root = new Start(lastAccept);
matchRoot = lastAccept;
}
}
public static Pattern compile(String s)
{
return new Pattern(s, 0);
}
/**
* 指定flag编译正则表达式。
* CANON_EQ(128):使用和Unicode等价的规范
* CASE_INSENSITIVE(2):不区分大小写
* COMMENTS(4):允许空白和注释
* DOTALL(32):dotall模式,元字符"."可以匹配任何字符,包括行结束符
* LITERAL(16):输入序列中的元字符或转义序列不具有任何特殊意义
* MULTILINE(8):多行模式中元字符"^"和"$"匹配换行,或开头结尾位置。默认情况下,仅匹配输入序列的开头和结尾位置
* UNICODE_CASE(64):符合 Unicode Standard的不区分大小匹配
* UNICODE_CASE(1):换行符仅匹配Unix换行符LF即'\n',Windows中为'\r''\n'
*/
public static Pattern compile(String s, int i)
{
return new Pattern(s, i);
}
public String pattern()
{
return pattern;
}
public String toString()
{
return pattern;
}
/**
* 创建一个Matcher匹配器
*/
public Matcher matcher(CharSequence charsequence)
{
if(!compiled)
synchronized(this)
{
if(!compiled)
compile();
}
Matcher matcher1 = new Matcher(this, charsequence);
return matcher1;
}
public int flags()
{
return flags;
}
/**
* 编译正则表达式,并将其匹配输入序列
*/
public static boolean matches(String s, CharSequence charsequence)
{
Pattern pattern1 = compile(s);
Matcher matcher1 = pattern1.matcher(charsequence);
return matcher1.matches();
}
/**
* 使用给定RegEx拆分字符串,limit参数决定结果数组的length
*/
public String[] split(CharSequence charsequence, int i)
{
int j = 0;
boolean flag = i > 0;
ArrayList arraylist = new ArrayList();
Matcher matcher1 = matcher(charsequence);
do
{
if(!matcher1.find())
break;
if(!flag || arraylist.size() < i - 1)
{
String s = charsequence.subSequence(j, matcher1.start()).toString();
arraylist.add(s);
j = matcher1.end();
} else
if(arraylist.size() == i - 1)
{
String s1 = charsequence.subSequence(j, charsequence.length()).toString();
arraylist.add(s1);
j = matcher1.end();
}
} while(true);
if(j == 0)
return (new String[] {
charsequence.toString()
});
if(!flag || arraylist.size() < i)
arraylist.add(charsequence.subSequence(j, charsequence.length()).toString());
int k = arraylist.size();
if(i == 0)
for(; k > 0 && ((String)arraylist.get(k - 1)).equals(""); k--);
String as[] = new String[k];
return (String[])arraylist.subList(0, k).toArray(as);
}
public String[] split(CharSequence charsequence)
{
return split(charsequence, 0);
}
Matcher类
- Matcher appendReplacement(StringBuffer sb,String replacement):首先使用StringBuilder辅助将匹配模式的子序列替换为指定字符串,然后将匹配模式的子序列之前的字符子串添加到指定的StringBuffer中,最后将第一步得到的StringBuilder追加到StringBuffer中。
- StringBuffer appendTail(StringBuffer sb):将输入序列lastAppendPosition之后的字符追加到StringBuffe中。可以在一次或多次调用appendReplacement方法后调用它来复制剩余的输入序列。
- int end():返回最后匹配字符之后的偏移量。
- String group():返回之前所匹配模式的输入子序列。对于输入序列 charsequence,和当前匹配器matcher,matcher.group()、matcher.group(0) 和charsequence.substring(matcher.start(), matcher.end()) 结果相同,得到匹配的整个子串。
- String group(int group):返回之前所匹配模式的指定组的输入子序列。
- int groupCount():返回此匹配器模式中的组数。
- boolean lookingAt():输入序列是否以与指定模式匹配的子序列开始。
- boolean matches():整个输入序列与模式相匹配时返回true。
- boolean find():输入序列上一次匹配之后是否存在匹配模式的下一个子序列。
- String replaceAll(String replacement):使用 replacement 字符串替换所有匹配给定模式的子序列。
解释:
- RegEx中组是用"()"括起来的子表达式,下列代码返回true。"(?<yo>\w+)"将"\w+"组指名"yo",之后可以通过组名访问之前匹配到的组,也可以使用"\1"访问。
Pattern pattern = Pattern.compile("\\b(?<yo>\\w+)\\b\\s+\\k<yo>\\s+\\b(\\w+)\\b.+\\2"); Matcher matcher = pattern.matcher("yo yo check it check"); System.out.println(matcher.find());
- group(),groupCount()方法示例:
Pattern pattern = Pattern.compile("^\\b(?<yo>\\w+)\\b\\s+\\k<yo>\\s+\\b(\\w+)\\b.+\\2$"); Matcher matcher = pattern.matcher("yo yo check it check"); if (matcher.find()) { for (int i = 0; i <= matcher.groupCount(); i++) { System.out.println(matcher.group(i)); } } /*输出 * group(0):yo yo check it check * group(1):yo * group(2):check */
String类
- boolean matches(String regex):返回字符串与RegEx的匹配结果,jdk1.7源码实现: Pattern.matches(s, this)。
- String replace(CharSequence target, CharSequence replacement):使用 replacement 序列替换字符串所有匹配 target 序列的子字符串。
- String replaceAll(String regex, String replacement):使用 replacement 字符串替换所有匹配给定 RegEx 的子字符串。此方法与 Pattern.compile(regex).matcher(str).replaceAll(repl) 的结果完全相同。replacement中如果想包含"\" 和"$"字面值,应使用"\"做转义。
- String replaceFirst(String regex, String replacement):使用 replacement 字符串替换匹配给定 RegEx 的第一个子字符串。此方法与Pattern.compile(regex).matcher(str).replaceFirst(repl) 的结果完全相同。replacement中如果想包含"\" 和"$"字面值,应使用"\"做转义。
- String[] split(String regex, int limit):根据给定RegEx来拆分字符串,limit 参数决定结果数组的length。limit > 0,结果数组的length不大于limit。limit = 0,尽可能多的匹配RegEx,结尾的空字符不输出。limit < 0,尽可能多的匹配RegEx,结果数组的length等于最大匹配次数+1。如果字符串不匹配 RegEx,结果数组只有一个元素,即此字符串。此方法与 Pattern.compile(regex).split(str,n) 的结果完全相同。
- String[] split(String regex):等同于String[] split(String regex, 0)。
解释:
- split(String regex, int limit) ,字符串"boo:and:foo:"在limit取不同值时结果如下表所示:
RegEx | limit | 结果数组length | 结果数组 |
: | 2 | 2 | {"boo","and:foo:"} |
: | 5 | 4 | {"boo","and","foo",""} |
: | 0 | 3 | {"boo","and","foo"} |
: | -3000 | 4 | {"boo","and","foo",""} |
编程题
华为上机题
import java.util.Scanner;
/**
* Copyright (c) 2017, xyzxidian@163.com All Rights Reserved. <br/>
* Function: TODO ADD FUNCTION.
* @author QiYuane
* @version
* @since JDK 1.7
*
* 将一个字符中所有出现的数字前后加上符号“*”,其他字符保持不变
* public static String MarkNum(String pInStr){
* return null;
* }
*
* 输入描述:
* 输入一个字符串
* 输出描述:
* 字符中所有出现的数字前后加上符号“*”,其他字符保持不变
*
* 输入例子:
* Jkdi234klowe90a3
* 输出例子:
* Jkdi*234*klowe*90*a*3*
**/
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String str = scanner.nextLine();
str = str.replaceAll("\\d+", "\\*$0\\*");
System.out.println(str);
}
scanner.close();
}
}
阿里2017实习生招聘编程题
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Copyright (c) 2017, xyzxidian@163.com All Rights Reserved. <br/>
* Function: TODO ADD FUNCTION.
* @author QiYuane
* @version
* @since JDK 1.7
*
* 阿里的消息中间件负责淘宝天猫支付宝等各个系统的消息中转,削峰填谷及架构的解耦。在每年的双11中承载了数万亿的消息,消息中间件中有Pub/Sub
* 两个角色,Pub方发送消息到中间件,消息中间件再根据订阅关系投递给订阅方。例如用户成功购买了一个物品,交易平台(Pub)会发送一条交易完成
* (trade-done)的消息,购物车平台(Sub)订阅到这个消息后,会将用户的购物车物品删除掉。这里涉及一个问题,交易平台会发送各种类型的消息,
* 消息中间件是如何准确的将相应的消息投递给购物车平台的?所使用的就是消息中间件的过滤功能,过滤的方式有很多种,表达式判断,正则匹配等。
* 假设让你来实现一个过滤功能,来匹配订阅是否符合,给定条件如下:
* "?"匹配一个字符
* "*"匹配任意连串的字符
* 如上面的例子中,购物车平台订阅方式是pattern=*trade-done,那么
* filter(100-trade-done,pattern) = 1,
* filter(200-trade-done,pattern) = 1,
* filter(200-pad-done,pattern) = 0
*
* 如果pattern=200-?*-done:
* filter(100-trade-done,pattern) = 0,
* filter(200-trade-done,pattern) = 1,
* filter(200-pad-done,pattern) = 1
*
* 如果pattern=1*trade*done:
* filter(100-trade-done,pattern) = 1,
* filter(200-trade-done,pattern) = 0,
* filter(200-pad-done,pattern) = 0
*
* 输入:
* 第一行,需要过滤的字符串 第二行,匹配模型
* 输出:
* 匹配结果,匹配成功返回1,匹配失败返回0
* 输入范例:
* 100-trade-done
* 1*trade*done
* 输出范例:
* 1
**/
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String str = scanner.nextLine();
String regex = scanner.nextLine();
/* 第一种
* regex = regex.replace("?", ".");
* regex = regex.replace("*", ".*");
* if (str.matches(regex)) {
* System.out.println(1);
* } else {
* System.out.println(0);
* }
* 会匹配 100-trade-done-trade-done、1-trade,done、100-trade-done-done 等这类字符串
*/
/*
* 第二种:不会匹配 100-trade-done-trade-done、1-trade,done、100-trade-done-done 等这类字符串,[\w-]匹配数字、字母、下划线、横线。
* 没法测通过率
*/
regex = regex.replace("?", "[\\w-]");
regex = regex.replace("*", "[\\w-]*?");
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(str);
if (matcher.find()) {
if (str.length() == matcher.group().length()) {
System.out.println(1);
} else {
System.out.println(0);
}
} else {
System.out.println(0);
}
}
scanner.close();
}
}
测试用例不是100%通过的原因肯定是由于误匹配,但是题目给的不匹配示例又不能说明什么算是精确匹配,或者说合法的消息串规则是什么,关于这道题的误匹配和漏匹配你有什么高见?