统计字母数字字符的出现次数并以图形化方式打印显示

7

我有一个字符串,想要计算所有字母和数字的出现次数,并创建一个图表以便可以直观地查看出现情况。

例如:

String sentence = "ABC ABC ABC 123"

A (3) * * *
B (3) * * *
C (3) * * *
D
.
.

我的思路:

  1. 计算字符串中所有数字和字母的数量
  2. 打印所有星号,次数为该数字(不幸的是,在Java中我无法将一个String与int相乘)

我认为有两种计算字符数量的方法。我可以使用charAt()方法或toCharArray()并循环遍历字符串或数组并计算字母。

例如:

aCounter = 0;
bCounter = 0;
char ch = sentence.charAt(i);

for (i = 0; i < sentence.length(); ++i) {
    if (ch == 'a') {
        aCounter++;
    }
    if (ch == 'b') {
        bCounter++;
    }
}

然而,我对这种方法有多个问题:

  • 我需要创建大量的计数器变量 - 从 aCounterzCounter 再到 0counter9counter
  • 我还需要再创建一个循环来打印星号!

我这里不是要求得出正确答案,只是想寻求一些好的方向,因为我现在陷入困境了。


5
使用哈希表,以字符作为键。然后,在伪代码中简单地执行 map[character]++ 即可。 - Marc B
哦,你想要每个字符出现的次数。咦,我知道这看起来太简单了。不是说它不容易。Marc B 有正确的想法。 - crush
你可以使用 Map<Char, Integer> 或者数组,因为你知道字母和数字的总数。 - crush
大小写是否重要?还是应该将大写字母A和小写字母a视为相同的字符? - crush
变量charCount = 0 变量c = null; for (var i = 0; i < newPwd.value.length; i++) { c = newPwd.value.substring(i, i + 1) 如果(isNaN(parseInt(c)) == true) { charCount++ } } - hima
我建议使用int[]数组而不是产生HashMap的开销。看看我的示例与HashMap示例的性能表现会很有趣。 - crush
12个回答

8

不需要为此创建HashTable/HashMap/HashSet

您提前知道要跟踪哪些字符,因此可以使用数组。

我想统计所有字母和数字的出现次数

将要跟踪的字符制作成字符串,然后初始化一个数组。

String sentence = "ABC ABC ABC 123";

//Make a map of all the characters you want to track.
String indexes = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";

//Initialize an array to the size of the possible matches.
int[] count = new int[indexes.length()];

//Loop through the sentence looking for matches.
for (int i = 0; i < sentence.length(); i++) {
    //This will get the index in the array, if it's a character we are tracking
    int index = indexes.indexOf(sentence.charAt(i));

    //If it's not a character we are tracking, indexOf returns -1, so skip those.
    if (index < 0)
        continue;

    count[index]++;
}

然后你可以使用以下代码将它们全部打印出来:
for (int i = 0; i < count.length; i++) {
    if (count[i] < 1)
        continue;

    System.out.println(String.format("%s (%d) %s",
            indexes.charAt(i),
            count[i],
            //This little bit of magic creates a string of nul bytes, then replaces it with asterisks.
            new String(new char[count[i]]).replace('\0', '*')));
}

如果您对new String(new char[count[i]]).replace('\0', '*'))这个部分不太熟悉,那么您可以使用StringBuilder在尝试输出之前构建星号String。您可以看到@mike的示例,了解如何实现。

输出结果

1 (1) *
2 (1) *
3 (1) *
A (3) ***
B (3) ***
C (3) ***

考虑因素

在决定如何解决这个问题时,需要考虑以下几点:

  • 你是否总是知道需要提前跟踪哪些字符,或者是否有时想要跟踪任何字符?如果是后者,则数组不适用于你;您需要使用高级数据结构,如TreeMap或HashMap。
  • 您是否总是计算特定char的出现次数,而不是String的出现次数?如果您必须修改此以计算String的出现次数,则使用String indexes映射技巧也无法为您工作。
  • 您是否正在课程中学习特定的数据结构?通常,此类问题分配给学生是为了理解如何应用特定概念。正如@kyle建议的那样,您应该尝试使用您在课堂上学习或已经学习过的数据结构。有时使用您尚未学习的结构可能会让您陷入麻烦,或者至少会得到较低的成绩。

我添加了另一种方法...相当冗长。如果我有时间,我会创建第三种方法,尽可能少的代码行数。 - mike
虽然不需要使用 HashMap,但它是适合此任务的数据结构(实际上,MultiSet 才是,但 Java 标准库中没有)。你的方法对于大多数输入都有效,但如果我恰好住在一个有特殊字符如 "ěščřžýáíé" 的国家怎么办?如果我忘记了一些字符 ("ůúňťď") 怎么办?此外,在每次迭代中调用 indexOf() 可以工作,但我很好奇它在处理非常长的输入字符串时的性能。总而言之,虽然这个方法可行,但我不认为这是现代 Java 方法的正确选择。这就像 1990 年代再次来临。 - Petr Janeček
@Slanec indexOf 是在跟踪字符的映射上调用的,而不是在输入字符串本身上调用的。此外,请阅读我的 注意事项 部分,您会发现您提出的其他观点已经在我的答案中了。 - crush
@crush 我感到很不好意思。我必须可耻地承认我没有阅读那一部分,它确实讲述了整个故事。是的,我知道indexOf()的用法,但我仍然会担心。这仍然是线性探测,虽然在小规模下并无大碍,但如果indexes数组更长且输入很多,则可能会引起注意。不过,对于教育目的来说,这是一个好方法,所以没问题。 - Petr Janeček
我在这里完成了:https://dev59.com/uXbZa4cB1Zd3GeqPJsQH#18787187。由于char具有固定的16位大小,因此对于一个`char`,有2^16 = 65536个可能的赋值。 - mike
显示剩余5条评论

2

不必循环一次来计算数量,再循环第二次来打印星号,你可以采用另一种方法:

Map<Character,String> results = new HashMap<Character, String>();

然后,每次迭代时,您都会检查您的映射是否包含该字符的数据,如果没有,则进行初始化。伪代码如下:

If the map contains data for the key
    Obtain the data for the character
    append a new asterisk
Else
    Create a String with an asterisk
    Append an asterisk
    Put the String with the character as key

如果您需要将星号的数量作为数字获取,您可以始终获得该字符串的大小(假设您没有输入任何空格)。

更新

作为增强功能,考虑到我与@crush分享的评论,两个调整可以改善逻辑:

  • StringBuilder 而非 String避免不必要的文本创建。
  • TreeMap 而非 HashMap它会给映射提供正确的顺序,允许按排序后的顺序打印其内容。

如果有足够的空间(和知识)来证明它们的用途,那么由OP添加这些额外的东西。


不要忘记在String中更新出现次数。记住,格式是A(3)***,因此您还必须更新计数。最后,在构建完所有字符串后,仍然必须迭代映射以显示其中的每个String - crush
@crush 计数是存储字符串的大小。确实,您需要遍历地图,但您拥有所有信息。最后一次迭代显示结果。 - Fritz
哦,我忽略了你只是在存储一个星号字符串。因此,最终你会得到类似这样的内容:String.format("%s (%d) %s", entry.getKey(), entry.getValue().length(), entry.getValue()); - crush
例如,如果问题的提出者知道 String#Format,那就用它。否则,简单的拼接也可以 :) - Fritz
我认为你的回答没有得到应有的关注。例如,如果不是因为String.replace()(在技术上内部循环),我的答案将需要三个循环。这种方法只需要两个循环。不过,我更喜欢使用TreeMap。然而,回到我的数组示例,一个String数组可以实现相同的结果,但连接会很麻烦,因为它会在每次循环时创建一个新的不可变String - crush
@crush 我也因为你巧妙地使用索引而点赞了你的回答。确实,TreeMap是一个不错的选择,可以按正确的顺序打印值,因为Character带有compareTo实现。我建议保留String方法,因为我不确定OP是否知道StringBuilder。起初,我使用构建器而不是原始的Strings,但为了保持一定程度的简单性而采用了更简单的方法,但这两个观点都是一个很好的更新。 - Fritz

2

以下是一些提示,帮助您入门:

  1. 不要为每个计数器使用单独的变量。使用数组(或一些集合类型...如果您已经学过的话...)。

  2. 您可以使用字符作为数组索引。

  3. 在开始打印任何内容之前,累加所有计数。


@mike 有时候,代码比文字说明更好。=)但这是对我下面实现的一个不错的总结。它很抽象!+1 - crush

2

这里介绍一种面向对象的方法,使用StringReader和Map。我使用了TreeMap来使输出排序。

public class StringHistogram
{
  public static void main(String[] args) throws IOException
  {
    Scanner sc = new Scanner(System.in);
    System.out.print("Please insert string: ");
    String s = sc.nextLine();
    sc.close();
    System.out.println(s);

    StringReader r = new StringReader(s);

    Map<Character, Integer> histogram = new TreeMap<Character, Integer>();
    int c;
    while ((c = r.read()) != -1) {
      Integer count = histogram.get((char) c);
      if (count == null)
        count = 0;
      histogram.put((char) c, count + 1);
    }
    r.close();
    for (Entry<Character, Integer> entry : histogram.entrySet())
      System.out.println(entry.getKey() + " (" + entry.getValue()
          + ") " + createAsterisk(entry.getValue()));
  }

  private static String createAsterisk(int number) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < number; i++)
      sb.append("*");
    return sb.toString();
  }
}

1
创建一个哈希表并遍历字符串,每次将当前字符添加到哈希表中。
     String str = "abc abc abc 123";
     Hashtable numbers = new Hashtable();
     int size = str.length();
     for(int i = 0 ; i< size ; i++)
     {
         char curr = str.charAt(i);
         if(numbers.contains(curr) == false)
         {
             numbers.put(curr, 1);
         }
         else
         {
             numbers.put(curr, ((int)numbers.get(curr)) + 1);
         }
     }

     Enumeration names = numbers.keys();
     char c;

     while(names.hasMoreElements()) {
        c = (char) names.nextElement();
        System.out.println(c + ": " +
        numbers.get(c));
     }

这是一种清晰的方法。不要忘记在你的答案中解决输出(星号问题)的打印! - crush
@crush 加入了那个。顺便说一下,不错的名字。 - No Idea For Name
6
哈希表(Hashtable)和枚举器(Enumeration)——这是哪一年? :)(现代代码应该使用HashMap和Iterator) - Joni

1
使用数组来存储计数器。您可以直接使用char作为数组索引,因此不需要复杂的逻辑。
要打印给定数量的星号,使用for循环是最简单的方法。

最简单的方法就是像 @crush 一样使用 new String(new char[count[i]]).replace('\0', '*')) - mike
“最简单”的方法是有争议的 - 创建一个由NUL字符填充的数组,从该数组创建一个字符串,并创建另一个字符串,其中每个NUL字符都被星号替换,这是不切实际和低效的。不过这是一个巧妙的技巧。 - Joni
@Joni 更高效的方法是使用 StringBuilder,但这样至少会增加2-3行代码,具体取决于你如何编写它。在 for 循环中进行字符串拼接将表现得更糟糕。我不明白你关于它是“非平凡”的评论。对我来说,这似乎相当平凡。 - crush
@crush 我在我发布的答案中使用了 StringBuilder。...但我总是喜欢一行代码:D ...我认为他所说的非平凡意味着它有点像黑客,而不是人们首先想到的方法。 - mike
如果你想打印一定数量的星号,首先想到的是一个循环,逐个打印它们。构建一个字符串则是在抽象层次上更高的一步... - Joni
好的。我没有将非平凡与那个等同起来,但现在我明白你的意思了。StringBuilder可能是打印n个星号的最佳解决方案,就像@mike在他的示例中所做的那样。 - crush

1
由于您是新手,还没有解决方案(这是每个人开始的地方),正确的答案是使用您在课堂上学习的数据结构。
如果您正在学习映射:
  • TreeMap按键的自然顺序排序(适合打印)
  • HashMap没有非常可预测的排序
如果您正在学习数组,则此线程中已经有很好的例子,例如crush的响应。

很好的观点。这绝不能被忽视。由于这是一项作业任务,将所学课程应用到问题中非常重要。 - crush

0

这是我使用StringBuffer实现算法的方式

public class StringManipulation {


public static void main(String[] args) {
    int occurrences = 0;
    int count = 0;
    int firstLoc = 0;
    int lastLoc = 0;
    boolean countedMulti = false;

    StringBuffer sb = new StringBuffer();

    String a = new String("ABC ABC ABC 123");
    String lastStrChar = null;

    char tempChar = 'z';

    while (count <= a.length()-1) 
    {
        for (int scanner = 48; scanner <= 90; scanner++) 
        {

            if (a.charAt(count) == scanner) {
                tempChar = (char)scanner;

                for (int i = 0; i <= a.length() - 1; i++) 
                {

                    if (tempChar == a.charAt(i)) {

                        if (count == 0) 
                        {
                            occurrences += 1;

                            sb.append(tempChar);
                        }

                        if (count > 0) {
                            if (a.charAt(count) != a.charAt(count - 1)) 
                            {
                                occurrences += 1;
                            }
                        }
                    }

                    if (count == i + 1) 
                    {
                        sb.append(tempChar);
                        occurrences = 0;
                    }

                    if ((sb.length() - 1) >= 0) 
                    {
                        lastStrChar = sb.substring(sb.length() - 1);

                        firstLoc = sb.indexOf(sb.substring(sb.length() - 1));
                        lastLoc = sb.length() - 1;

                        if (count>0 && sb.lastIndexOf(lastStrChar,firstLoc) != sb.lastIndexOf(lastStrChar, lastLoc)) 
                        {
                            countedMulti = true; //if the index is different for the same character, a duplicate char is found
                        } 
                        else 
                        {                           
                            countedMulti = false;               
                        }
                    }
                }

                if (!countedMulti) 
                {
                    System.out.print(lastStrChar + " appeared " + occurrences + " times\n");    
                }
            }
        }

        count++;
    }
  }
}

输出:

A appeared 3 times
B appeared 3 times
C appeared 3 times
1 appeared 1 times
2 appeared 1 times
3 appeared 1 times

0
将其分为两个方法 - 一个是根据字符和字符串创建一个名为“row”的字符串,另一个是调用第一个方法来处理36个字母数字字符中的每一个。
public static String alphNum = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";

    public static String count(char c, String str) {
        String stringToReturn = Character.toString(c);
        for(char ch : str.toCharArray()) {
            if (ch == c) {
                stringToReturn += " *";
            }
        }
        return stringToReturn;
    }

    public static void countAndPrintAlphNum(String str) {
        String stringToTest = str.toUpperCase();
        Set<String> rows = new HashSet<String>();
        char[] alphNumArray = alphNum.toCharArray();
        for(char c : alphNumArray) {
            rows.add(count(c, stringToTest));
        }
        for(String row : rows) {
            System.out.println(row);
        }

    }

    public static void main(String[] args) {
        countAndPrintAlphNum("Hi There 123!");
    }

注意:如果您想确保行按字母数字顺序打印(数字优先),请使用TreeSet而不是HashSet来存储行。

@crush 这是正确的,没错。当我说“确保”时,我是指以防编程时出现问题(假设alphNum字符串不按顺序排列)。当然,另一方面,也有可能希望数字放在最后,这样我们可以使用HashSet。 - James Dunn

0
这里是一个极简主义的非面向对象编程解答,只需要3行代码就可以实现。它能够工作,因为字符可以被解释为整数。

我有点担心没有关闭扫描器。但是由于Javadoc说System.in已经打开并准备好提供输入数据。我假设系统也会处理资源的关闭。
public class MinimalisticHistogram
{
  public static void main(String[] args)
  {
    int[] occurrences = new int[(int) Math.pow(2, 16)]; // 256 KB
    for (char c : new Scanner(System.in).nextLine().toCharArray()) occurrences[c]++;
    for (int i = 0; i < occurrences.length; i++) if (occurrences[i] != 0) System.out.println(String.format("%c %4s %s", i, "(" + occurrences[i] + ")", new String(new char[occurrences[i]]).replace('\0', '*')));
  }
}

你通过将一堆语句串联到最后一行来作弊 ;) - crush

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接