5
\$\begingroup\$

I am new to Java and learning about Strings in Java. So I wrote a program which counts the number of times a character repeats in a String and also identifies unique characters(chars not repeated more than once) in a String.

Is this an efficient way to do this?

public static String uniqueValues(String str){
char[] arr = str.toCharArray();
int[] result = NumberOfOccurences.numberOfOccurencesOfLetters(arr);
StringBuilder sb = new StringBuilder();
for(int i=0;i<result.length;i++){
    if(result[i]==1){
        sb.append((char)(i+97));
        sb.append(" ");
        }
    }
return sb.toString();
}

//in the other class i wrote this method
public static int[] numberOfOccurencesOfLetters(char[] arr){
int[] result = new int[26];
int num;
for(int i=0;i<arr.length;i++){
    num =(arr[i]-97);
    result[num]++;
    }
return result;
}
\$\endgroup\$
  • \$\begingroup\$ 97 is a magic number. I would suggest using 'a' instead because that clarifies your intent. \$\endgroup\$ – gyre Aug 15 '17 at 20:51
5
\$\begingroup\$

There are a couple of improvements that can be made, mostly readability improvements as I believe your solution is quite efficient.

  • Format your code with proper indentation and spacing

Indentation is important for readability, so is spacing; don't be afraid to throw in plenty of whitespace if it makes things easier to read. Notice how I indented on every bracket level and put in white space in between some lines and put spaces in for loops etc.

public static String uniqueValues(String str) {
    char[] arr = str.toCharArray();
    int[] result = NumberOfOccurences.numberOfOccurencesOfLetters(arr);
    StringBuilder sb = new StringBuilder();

    for(int i = 0; i < result.length; i++) {
        if(result[i]==1) {
            sb.append((char)(i+97));
            sb.append(" ");
        }
    }

    return sb.toString();
}

//in the other class i wrote this method
public static int[] numberOfOccurencesOfLetters(char[] arr) {
    int[] result = new int[26];
    int num;

    for(int i = 0; i < arr.length; i++) {
        num = (arr[i]-97);
        result[num]++;
    }

    return result;
}
  • Pass in a string to numberOfOccurencesOfLetters instead of a char[].

Assuming you'll be using this method mostly of strings, you should pass in a string and do the conversion inside numberOfOccurencesOfLetters rather than before calling it every time.

public static int[] numberOfOccurencesOfLetters(String str) {
        char[] arr = str.toCharArray();
        int[] result = new int[26];
        int num;

        for(int i = 0; i < arr.length; i++) {
            num = (arr[i]-97);
            result[num]++;
        }

        return result;
    }
  • Declare variables closest to where they're used and limit their scope as much as possible.

For example int num; can exist only within the for loop instead of the whole function scope as we only use it inside the for loop.

        for(int i = 0; i < arr.length; i++) {
            int num = (arr[i]-97);
            result[num]++;
        }
  • Return a HashMap instead of int[] for numberOfOccurencesOfLetters.

Google what is a map in Java. This sacrifices some speed, but you'll get much more readable code with a return type that's easier to manipulate. Also, you won't be limited to the 26 characters that you have now and you'll be able to have upper-case letters and special characters as well.

This is how your whole code would look like with this implementation. If it doesn't compile I'll fix it when I get home.

import java.util.HashMap;

public static String uniqueValues(String str) {
    HashMap<Character, Integer> result = NumberOfOccurences.numberOfOccurencesOfLetters(str);
    StringBuilder sb = new StringBuilder();

    for (Character key : result.keySet()) {
        sb.append(key);
        sb.append(" ");
    }

    return sb.toString();
}

public static Map<Character, Integer> numberOfOccurencesOfLetters(String str) {
        HashMap<Character, Integer> hmap = new HashMap<Character, Integer>();
        char[] arr = str.toCharArray();

        for(int i = 0; i < arr.length; i++) {
            Character key = arr[i];
            hmap.put(key, hmap.getOrDefault(key, 0) + 1);
        }

        return hmap;
 }
\$\endgroup\$
  • \$\begingroup\$ Thanks! I dont have knowledge about Maps as of now. Taking baby steps. But yeah, I'll keep proper indentation and naming convention in mind from now. \$\endgroup\$ – LoneStar Aug 15 '17 at 18:12
  • \$\begingroup\$ You might want to declare your map variables using the generic interface Map instead of the class HashMap. That way, your code is not tied to a specific implementation of Map should you ever decide to change it. \$\endgroup\$ – gyre Aug 15 '17 at 20:46
  • \$\begingroup\$ @Majiick You can get rid of the old map-get-if-null-create-and-put construct via using the Java 8 function getOrDefault. (hmap.put(key, hmap.getOrDefault(key, 0) + 1)) \$\endgroup\$ – mtj Aug 16 '17 at 5:52
  • \$\begingroup\$ @mtj Thanks for the suggestion, I put it in. \$\endgroup\$ – Majiick Aug 16 '17 at 8:21
1
\$\begingroup\$

The operations you want are already included in the Streams API.

// code updated with suggestion from Nevay
public static void main(String[] args) {
    String string = "feifjwo";
    Map<Character, Long> frequencies = string.chars()
            .mapToObj(i -> (char) i)
            .collect(Collectors.groupingBy(i -> i, Collectors.counting()));

    System.out.println(frequencies);
    System.out.println(frequencies.keySet().stream()
            .distinct()
            .collect(Collectors.toSet()));
}

EDIT adding some actual review:

@Majiick already gave good advice.

I would add that naming variables is important. For example, instead of result, occurrences would have been better.

It seems there is a bug with if (result[i] == 1) in uniqueValues. I think you meant if (result[i] >= 1) which would also print characters that appear more than once.

Otherwise I don't think there is any performance problem.

\$\endgroup\$
  • \$\begingroup\$ Thanks. The main aim of all this was to code most of it myself, so that I have a better understanding, as I'm new to Java. I don't have knowledge about maps and other data structures, so I'll have to look into that before I can use them. \$\endgroup\$ – LoneStar Aug 15 '17 at 17:39
  • 1
    \$\begingroup\$ OK, sure. I'll leave it here since it could be interest to others. It's not just Maps, but Streams, which is quite recent (Java 8). You probably want to wait before playing with those. \$\endgroup\$ – toto2 Aug 15 '17 at 17:40
  • \$\begingroup\$ Great! Anyway, how is my code performance wise? Any way I can improve it? \$\endgroup\$ – LoneStar Aug 15 '17 at 17:44
  • \$\begingroup\$ @LoneStar I added some actual review comments. \$\endgroup\$ – toto2 Aug 15 '17 at 17:56
  • \$\begingroup\$ Your stream version is unnecessary verbose, string.chars().mapToObj(i -> (char) i).collect(groupingBy(i -> i, counting())) is sufficient to create a Map<Character, Long>. \$\endgroup\$ – Nevay Aug 18 '17 at 20:47
0
\$\begingroup\$

As addition to the @Majiick comments.

There is few more things that could improve the readability of the code:

  • The name of the method #numberOfOccurencesOfLetters is too verbose and adds too many 'noice' and since it is used in the context of the NumberOfOccurences class, the prefix 'numberOfOccurrences' could be removed.

I would also propose you the following implementation of NumberOfOccurences class, which would make it more functional

class CharSequence {

    private final char[] sequence = sequence;

    public CharSequence(String sequence) {
        this(sequence.toCharArray());
    }

    public CharSequence(char[] sequence) {
        this.sequence = sequence;
    }

    public Set<Character> uniques() {
        Set<Character> uniques = new HashSet<Character>();

        Map<Character, Integer> occurences = this.occurences();
        for (Map.Entry<Character, Integer> entry : occurences.entrySet()) {
            if (entry.getValue() == 1) {
                uniques.add(entry.getKey());
            }
        }

        return uniques;
    }

    public Map<Character, Integer> occurences() {
        Map<Character, Integer> occurences = new HashMap<>();

        for (int i = 0 ; i < sequence.length ; i++) {
            Character key = sequence[i];

            occurences.put(key, occurences.getOrDefault(key, 0)++);
        }

        return occurences;
    }
}

I have implemented it with Map, as @Majiick suggest, I also took his aproach of implementation. In addition I have also used Map.Entry, which you could also take a look.

In the end your code would look like this one:

public static String uniqueValues(String str) {
    CharSequence sequence = new CharSequence(str);

    StringBuilder sb = new StringBuilder();
    for (Character character : sequence.uniques()) {
        sb.append(character).append(" ");
    }

    return sb.toString();
}

Since Java 8 you could also use the new join method of String class, with which you could get rid of StringBuilder usage. With its usage, the code will look like this:

public static String uniqueValues(String str) {
    CharSequence sequence = new CharSequence(str);

    return String.join(sequence.uniques(), " ");
}

Please note that by implementing it in this way:

  • It is implemented in a OOP way
  • You could hide the string conversion into the class, this also make the class more functional, since you could make instances of the class using many different types of objects
  • There is also more places of performance improvments e.g. you could cache the result that is returned from occurrences() method and add more functionality on it
  • The methods are less verbose and still easy to understand
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.