最近看到一道挺好玩的题目
写一个函数用来统计字符串中各字母出现的次数。(编程语言不限)
示例:
s1输入:X2Y3XZ,输出:X3Y3Z1;
s2输入:Z3X(XY)2,输出:X3Y2Z3;
s3输入:Z4(Y2(XZ2)3)2X2,输出:X8Y4Z16;
给的题目就上面这两行,再说一下这个题目
1.字符串中出现的字母只能是单字母
2.字符串中出现的括号符合乘法结合律
首先,当然是先去网上找程序啦,不要重复造轮子。网上找到的程序如下:
public static String countLetters(String s) {
if (s == null || s.length() == 0) {
return "";
}
Map<Character, Integer> map = new HashMap<>();
Stack<Integer> stack = new Stack<>();
int num = 1;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (Character.isDigit(c)) { // 数字,入栈
num = c - '0';
while (i + 1 < s.length() && Character.isDigit(s.charAt(i + 1))) { // 处理多位数字的情况
num = num * 10 + (s.charAt(++i) - '0');
}
stack.push(num);
} else if (c == ')') { // 右括号,出栈并计算
int multiply = stack.pop();
num *= multiply;
char prevChar = s.charAt(i - 1);
map.put(prevChar, map.getOrDefault(prevChar, 0) + num);
num = 1;
} else { // 字母或左括号,直接计数
if (!stack.isEmpty()) {
num *= stack.peek();
}
map.put(c, map.getOrDefault(c, 0) + num);
num = 1;
}
}
StringBuilder sb = new StringBuilder();
for (char c : map.keySet()) {
sb.append(c).append(map.get(c));
}
return sb.toString();
}
可惜的是,这个程序没法正常计算,不管了,那我们自己写。
可以看到上面的程序使用了栈和Map,栈用来处理字符串中的括号(乘法结合律),Map用来做统计。这个思路很不错。
算法是对数据结构的处理,因此将字符串的结构做些调整。
1.初始化字符串:有些字母后没有数字(默认为1),要给这些默字母后面补齐数字
2.处理括号:使用栈去解决乘法结合律,将字符串恢复成一个标准字符串,字符串中只有字符和数字,每个字符后都有一个表示数量的数字
3.使用Map遍历字符串,进行统计
以下程序是上述过程的实现:
public static void counts(String str) {
System.out.println("原始字符串:" + str);
// 初始化字符串
str = into(str);
// 处理字符串括号
str = kuohao(str);
System.out.println("处理之后的字符串:" + str);
HashMap<String, Integer> map = new HashMap<String, Integer>();
// 转为数组好处理,数组中偶数位是字母,奇数位是字母对应的数值
String[] strArray = str.split("");
for (int i = 0; i < str.length(); i += 2) {
// 如果已经存在健
if (map.containsKey(strArray[i])) {
// 获取键值,之后累加重新存入
Integer integer = map.get(strArray[i]);
map.put(strArray[i], integer + Integer.valueOf(strArray[i + 1]));
} else {
// 没有存在健,直接存入
map.put(strArray[i], Integer.valueOf(strArray[i + 1]));
}
}
// 遍历map输出
map.entrySet().stream().forEach(entry -> System.out.println("key: " + entry.getKey() + ", value: " + entry.getValue()));
}
// 初始化,将有些字母之后省略的数字给补上,也就是被省略的数字1
public static String into(String str) {
String newStr = new String();
// 初始化字符串
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
// 如果是数字直接跳
if (Character.isDigit(c) || c == '(' || c == ')') {
newStr = newStr + c;
continue;
}
// 如果是字母,判断字母之后是否有数字
if (i + 1 < str.length() && Character.isDigit(str.charAt(i + 1))) {
newStr = newStr + c;
} else {
newStr = newStr + c + "1";
}
}
System.out.println("初始化之后的字符串:" + newStr);
return newStr;
}
// 处理括号
public static String kuohao(String str) {
Stack<Character> stack = new Stack<>();
Stack<Character> stackTemp = new Stack<>();
String NewStr = "";
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
// 如果是左括号,之后括号中的内容入栈,遇到右括号停止
if (c == '(') {
// 标识变量退出变量
int sign = 0;
// 这里有括号嵌套问题,可以使用标识符来解决
for (int j = i; j < str.length(); j++) {
char c1 = str.charAt(j);
stack.push(str.charAt(j));
if (c1 == '(') {
sign++;
}
// 处理括号内容
if (c1 == ')') {
sign--;
// 将右括号弹出
stack.pop();
// 获得括号的乘数
int chengshu = str.charAt(j + 1) - '0';
Character pop;
// 出栈,并对该括号中的数字乘上乘数,之后再入栈
do {
pop = stack.pop();
// 如果出栈的是数字
if (Character.isDigit(pop)) {
Integer i1 = (pop - '0') * chengshu;
stackTemp.push(i1.toString().charAt(0));
} else if (Character.isLetter(pop)) {
stackTemp.push(pop);
}
} while (pop != '(');
// 将左括号弹出
// stack.pop();
// 重新入栈
while (!stackTemp.empty()) {
Character temp = stackTemp.pop();
stack.push(temp);
}
// 省略括号后面的乘数
j = j + 1;
// 退出条件
if (sign == 0) {
i = j;
break;
}
}
}
StringBuilder tempStr = new StringBuilder();
while (!stack.empty()) {
String temp = stack.pop().toString();
tempStr = tempStr.append(temp);
}
// 反转字符串并拼接
NewStr = NewStr + tempStr.reverse();
} else {
NewStr = NewStr + c;
}
}
return NewStr;
}
这个程序可以完美处理s1的字符串,s2也可以处理,但在s3时,会出现问题。
目前是将一个string字符串的每一位切割,判定是啥,字母则记录,字母之后的数字就是该字母的值。但这样带来一个问题是,当字母后的数值为一位以上时(如10),需要手动去字符串中截取这个完整的数值。目前来说还可以实现
说一下上面这个字符串中截取数值,通过for遍历字符串,转为char判断是否为数字,如果是,继续探测下一个位置,这样继续向后推进,直到遇到不是数值的,那么上面这么多就可以构成一个完整的数字
但这样做有两个问题,
第一是,一个多位数如果牵扯括号运算,将会多次进行这种 遍历,转换,检测,构成数字 这会使程序的可读性大幅下降,并且不太优雅。
第二个问题是,括号运算涉及栈,栈中只能存储char,无法存储多位的数值(只能存1-9),如果非要存储,有两种方法,
1.将数值分割,每一位作为一个char,存入栈,出栈时又要 遍历,判断,构成,因为是栈,还需要反转数值
2.将数值的值,所对应的ascll码值的字符存入栈,这个又带来的问题是,栈中的值,无法判断那个是真正的字符,那个是对应的数值,这里可以强制默认一个字符出来后,下一个字符就是该字符对应的值。但这样程序并不强健
方式1太过繁琐,尝试方式2
程序实现如下:
// 初始化,将有些字母之后省略的数字给补上,也就是被省略的数字1,
// 并将数字转化为对应的ascll码值对应的字符
public static String intoPro(String str) {
String newStr = new String();
String newStr1 = new String();
// 初始化字符串
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
// 如果是括号直接跳
if (Character.isDigit(c) || c == '(' || c == ')') {
newStr = newStr + c;
continue;
}
// 如果是字母,判断字母之后是否有数字
if (i + 1 < str.length() && Character.isDigit(str.charAt(i + 1))) {
newStr = newStr + c;
} else {
char temp = 1;
newStr = newStr + c + temp;
}
}
// 初始化之后,对字符中的数字处理位char
for (int i = 0; i < newStr.length(); i++) {
char c = newStr.charAt(i);
// 如果是数字,首先判断是几位数,之后转为ascll码值
if (Character.isDigit(c)) {
String tempInt = String.valueOf(c - '0');
int j = i + 1;
for (; j < newStr.length(); j++) {
char c1 = newStr.charAt(j);
if (Character.isDigit(c1)) {
tempInt += c1;
i = j;
} else {
break;
}
}
int i1 = Integer.parseInt(tempInt);
char charTemp = (char) i1;
newStr1 = newStr1 + charTemp;
} else {
newStr1 += c;
}
}
System.out.println("初始化之后的字符串(Pro):" + newStr1);
return newStr1;
}
// 处理括号
public static String kuohaoPro(String str) {
Stack<Character> stack = new Stack<>();
Stack<Character> stackTemp = new Stack<>();
String NewStr = "";
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
// 如果是左括号,之后括号中的内容入栈,遇到右括号停止
if (c == '(') {
// 标识变量退出变量
int sign = 0;
// 这里有括号嵌套问题,可以使用标识符来解决
for (int j = i; j < str.length(); j++) {
char c1 = str.charAt(j);
stack.push(str.charAt(j));
if (c1 == '(') sign++;
// 处理括号内容
if (c1 == ')') {
sign--;
// 将右括号弹出
stack.pop();
// 获得括号的乘数
int chengshu = str.charAt(j + 1);
Character pop;
do {
pop = stack.pop();
if (pop != '(') {
// 数字处理
int i1 = pop * chengshu;
stackTemp.push((char) i1);
// 数字对应的字母处理
pop = stack.pop();
stackTemp.push(pop);
}
} while (pop != '(');
// 重新入栈
while (!stackTemp.empty()) {
Character temp = stackTemp.pop();
stack.push(temp);
}
// 省略括号后面的乘数
j = j + 1;
// 退出条件
if (sign == 0) {
i = j;
break;
}
}
}
StringBuilder tempStr = new StringBuilder();
while (!stack.empty()) {
String temp = stack.pop().toString();
tempStr = tempStr.append(temp);
}
// 反转字符串并拼接
NewStr = NewStr + tempStr.reverse();
} else {
NewStr = NewStr + c;
}
}
return NewStr;
}
public static void countsPro(String str) {
System.out.println("原始字符串(Pro):" + str);
// 初始化字符串
str = intoPro(str);
// 处理字符串括号
str = kuohaoPro(str);
System.out.println("处理之后的字符串(Pro):" + str);
HashMap<String, Integer> map = new HashMap<String, Integer>();
// 转为数组好处理,数组中偶数位是字母,奇数位是字母对应的数值
String[] strArray = str.split("");
for (int i = 0; i < str.length(); i += 2) {
// 如果已经存在健
if (map.containsKey(strArray[i])) {
// 获取键值,之后累加重新存入
Integer integer = map.get(strArray[i]);
map.put(strArray[i], integer + (int) str.charAt(i + 1));
} else {
// 没有存在健,直接存入
map.put(strArray[i], (int) str.charAt(i + 1));
}
}
// 遍历map输出
map.entrySet().stream().forEach(entry -> System.out.println("key: " + entry.getKey() + ", value: " + entry.getValue()));
}
这个写起来还是十分折磨的,因为在处理括号部分要反复处理索引问题
上述程序可以完美解决多位数问题,当然,还是有问题的,在测试过程中,遇到两个问题
1.如果在处理字符串时,出现40,41这两个值,程序会出错,因为这两个值在Ascll中表示左右括号
2.单个字符的值,总计不能超过7W,可能是因为Ascll的限制
要如何继续改进呢,要怎么样解决多位数问题,
首先,从字符串中截取数值是必须做的,否则无法分离字母和数值。那么后续需要面对多位数的存储问题,如何很好的存储,方便之后的乘法,存取。我能想到的一种方法是,将字符串初始化之后,都存储到map中。但如果所有字母数字对都用Map存储,那么该如何做括号运算呢。因为多个括号嵌套需要记录多个左括号,Map中不能记录相同的键。
解决上述问题,可以尝试在初始化字符串之后,将字符串中的每个字符和对应的值存在map中,这样可以完全解决上述问题但这样又会带来问题,如何去处理字符串中的括号呢,目前的想法是,每个对应括号中的值,放在一个map中,也就是说,最里面的括号中的值,在一个map中,去做完乘法之后,再将这个map的值去复制到外面一层的map中。这个动作,要如何实现呢。
两种破局方式,
1.如果继续上面char模式,可以去记录括号数量,也就是去匹配左右括号,没有被匹配的括号就是char值被转换的,这样其实也有问题
如果恰好出现,数值转换时,同时出现左右括号,这种情况真是没法子了。并且会造成运算混乱
2.继续map模式,如果出现左括号,做一个String数组,将之后的所有东西进入数组(在数字进入数组时,进行多位数判断),之后遇到第一个右括号时,开始将数组索引向前,并且数组东西出来进入map中,直到数组索引遇到第一个左括号,停止。然后继续,重复上述过程,直到数组为空。
public class CountLetter {
// 初始化
public static String[] intoProMax(String str) {
String newStr = new String();
// 初始化字符串
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
// 如果是括号直接跳
if (Character.isDigit(c) || c == '(' || c == ')') {
newStr = newStr + c;
continue;
}
// 如果是字母,判断字母之后是否有数字
if (i + 1 < str.length() && Character.isDigit(str.charAt(i + 1))) {
newStr = newStr + c;
} else {
newStr = newStr + c + "1";
}
}
System.out.println("初始化之后的字符串(ProMax):" + newStr);
// 将初始化之后的字符串切割到一个数组中
String[] strArray = new String[newStr.length()];
// 数组索引,不用i是因为字符串索引在多位数判断时,会跳跃
int ArrayIndex = 0;
for (int i = 0; i < newStr.length(); i++) {
char c = newStr.charAt(i);
// 如果是字母,左右括号,直接进入数组
if (Character.isLetter(c) || c == '(' || c == ')') {
strArray[ArrayIndex] = String.valueOf(c);
ArrayIndex++;
continue;
}
// 如果是数字,截取完整数字之后进入数组
if (Character.isDigit(c)) {
String Num = String.valueOf(c);
// 如果已经时最后一个数字了,就不用判断是否是多位数
if (i == newStr.length() - 1) {
strArray[ArrayIndex] = Num;
break;
}
// 从当前数字向后遍历
for (int j = i + 1; j < newStr.length(); j++) {
char c1 = newStr.charAt(j);
// 如果是数字,添加到临时变量上,并改变外层索引值
if (Character.isDigit(c1)) {
Num += c1;
i = j;
} else {
// 否则将已有数值添加到数组,改变数组索引,结束
strArray[ArrayIndex] = Num;
ArrayIndex++;
break;
}
// 如果字符串已经到了结尾,要将已经截取好的字符串存入数组
if(j==newStr.length()-1){
strArray[ArrayIndex] = Num;
}
}
}
}
return strArray;
}
// 处理括号,并返回处理好的Map
public static void kuohaoProMax(String str) {
String[] strArray = intoProMax(str);
HashMap<Character, Integer> map = new HashMap<>();
Stack<String> stack = new Stack<>();
for (int i = 0; i < strArray.length; i += 2) {
String s = strArray[i];
// 退出条件
if (s == null) {
break;
}
char c = s.charAt(0);
// 如果是左括号,之后括号中的内容入栈,遇到右括号停止
if (c == '(') {
// 将左括号压入栈
stack.push(s);
HashMap<Character, Integer> mapStack = new HashMap<>();
for (int j = i + 1; j < strArray.length; j++) {
String s2 = strArray[j];
if (")".equals(s2)) {
String peek = "";
do {
String value = stack.pop();
String key = stack.pop();
if (!mapStack.containsKey(key)) {
mapStack.put(key.charAt(0), Integer.valueOf(value));
} else {
mapStack.put(key.charAt(0), mapStack.get(key) + Integer.valueOf(value));
}
peek = stack.peek();
} while (!"(".equals(peek));
// 将左括号弹出
stack.pop();
// 获取乘数
Integer NumValue = Integer.valueOf(strArray[j + 1]);
j = j + 1;
// 给每个map值承以乘数
mapStack.entrySet().stream().forEach(entry -> {
mapStack.put(entry.getKey(), entry.getValue() * NumValue);
});
if (stack.empty()) {
System.out.println("栈Map:");
mapStack.entrySet().stream().forEach(entry -> System.out.println("key: " + entry.getKey() + ", value: " + entry.getValue()));
// 改变i的值,因为j之前的都在栈中处理了,包括j+1的乘数也处理完毕,因此下一次的字符串遍历可以从j+1位置开始
// 但是设置的for自增规则是每次+2,因为每次会处理一个键值对,因此这里还要减去2
i = j + 1 - 2;
// 将栈Map与统计Map合并
mapStack.entrySet().stream().forEach((entry) -> {
Character key = entry.getKey();
Integer value = entry.getValue();
// 如果健已经存在,则值相加之后存入
if (map.containsKey(key)) {
map.put(key, map.get(key) + value);
} else {
map.put(key, value);
}
});
break;
}
} else {
stack.push(s2);
}
}
} else {
// 如果没有括号,就直接在Map中存储
Integer value = 1;
// 如果是数字,首先判断是几位数
value = Integer.valueOf(strArray[i + 1]);
// 判断是否已经存在键之后再存入
if (map.containsKey(c)) {
map.put(c, map.get(c) + value);
} else {
map.put(c, value);
}
}
}
System.out.println("统计Map:");
map.entrySet().
stream().
forEach(entry -> System.out.println("key: " + entry.getKey() + ", value: " + entry.getValue()));
}
}
上述程序就是我最后的结果了,可以完美解决括号问题,数字多位数问题,乘积上限问题。
其实还可以继续优化,就像开头说的,这个程序没法计数多个字符情况,比如:
输入字符:XYXUIXY
这个字符串中,X,XY,YX,XYX等字符出现的次数
这种是没法统计的,并且让我个人不满意的是,这个程序比较复杂,不优雅。如果可以简化,提高可读性会更好。
就这样。
后言
大地的另一面是梦中的世界,而我们则在那个世界的梦中
——《尺波》