제공된 영문 텍스트에서 각 알파벳이 출현하는 빈도수와 차지하는 비율을 구하는 문제입니다. 영문 텍스트는 파일에서 읽어들이도록 설정해야 하고, 요구하는 최종 출력 모습의 예시는 아래와 같습니다.
A = 455개, 5.66%
B = 109개, 1.36%
C = 172개, 2.14%
...
(알파벳, 빈도수)
를 맵핑하기주어진 파일 경로에서 텍스트 파일을 읽어들여 한 줄씩 StringBuilder
에 추가하고, 결과 값을 String 타입으로 반환하는 메소드를 사용합니다.
public static String getLoadText(String filePath) {
StringBuilder sb = new StringBuilder(); //StringBuilder 객체 생성
try { //아래 내용을 실행
Path path = Paths.get(filePath); //파일 경로를 이용해 Path 객체를 생성
List<String> lines = Files.readAllLines(path); //Path 객체를 이용해 파일 내용을 모두 읽어 라인별로 List 형태로 저장
for (int i = 0; i < lines.size(); i++) { //라인별로 읽어서 StringBuilder(sb)에 추가
if (i > 0) {
sb.append("\n"); //인덱스가 0보다 크면 (첫 줄 제외) 개행 문자 추가
}
sb.append(lines.get(i)); //StringBuilder에 추가
}
} catch (IOException e) { //예외가 발생하면 실행할 내용
e.printStackTrace();
}
return sb.toString();
}
}
위 메소드를 메인 메소드 안에 활용할 차례입니다. String filePath
에 파일이 위치한 자세한 경로를 적어 넣습니다. 더불어 빈도수와 비율을 구하기 위한 코드들을 추가합니다.
public static void main(String[] args) {
String filePath = "C:\\Java\\president_inaugural_address.txt";
String text = getLoadText(filePath).toLowerCase();
Map<Character, Integer> map = new HashMap<>();
for (int i = 0; i < text.length(); i++) {
map.put(text.charAt(i), map.getOrDefault(text.charAt(i), 0) + 1);
}
for (char c = 'a'; c <= 'z'; c++) {
int count = map.getOrDefault(c, 0);
double percentage = (double) count / text.length() * 100;
System.out.printf("%C = %7d개, %.2f%%\n", c, count, percentage);
}
}
문제의 요구사항을 반영하기 위해 아래와 같은 코드가 사용됐습니다.
toLowerCase()
: 대소문자의 구분없는 출현 빈도수와 비율을 구하기 위해 String 타입으로 반환된 텍스트를 모두 소문자로 불러옵니다.
String.charAt(index)
: String 타입으로 저장된 문자열 중에 지정한 index에 위치한 문자를 char 타입으로 변환합니다.
Map<Character, Integer>
: map 인터페이스를 통해 HashMap 클래스를 사용합니다. 알파벳과 빈도수를 한 쌍으로서 자료화할 수 있습니다.
map.put(key, value)
: String 타입의 key와 value를 추가합니다. 동일한 key 값을 추가하면 value의 값이 덮어쓰기가 됩니다.map.getOrDefault(key, DefaultValue)
: 해당 key에 맵핑된 value 값이 없는 경우 지정한 DefaultValue를 반환합니다.작성한 코드에서는 알파벳이 한번도 출현하지 않았을 때 null대신 DefaultValue인 0을 반환하도록 설정했습니다. 동일한 알파벳을 찾을 때 마다 1씩 숫자가 증가하도록 위 코드에서는 + 1
을 추가했습니다.
예를 들어, text 문자열의 첫번째 문자가 'a'라고 가정하면 처음에는 map에 'a'라는 키가 없으므로 map.getOrDefault(text.charAt(i), 0)이라는 코드는 값으로 0을 반환합니다. 따라서 map.put(text.charAt(i), map.getOrDefault(text.charAt(i), 0) + 1)
은 'a'라는 문자를 key 값으로, 0에 1을 더한 값인 1을 value 값으로 맵에 저장합니다 (map.put('a', 1)
). 만약 다음 문자도 'a'라면 문자가 key 값으로서 이미 맵에 존재하므로 현재 value 값인 1에 1을 더하여 2가 되고, 'a'라는 key에 대한 value 값으로 덮어쓰기가 이루어집니다. 결과적으로 알파벳 'a'는 해당 문자열 내에서 2번 등장했다는 정보를 가지게 됩니다. (map.put('a', 2)
)
printf("format", value)
: 특정한 형태로 값을 출력합니다. 출력하려는 값에 맞게 원하는 지시자를 넣어서 사용합니다.지시자 | 내용 |
---|---|
%c | char 타입의 소문자 형식으로 출력 |
%C | char 타입의 대문자 형식으로 출력 |
%d | 10진수 정수 형식으로 출력 (남은 자리는 공백으로 채움) |
%02d | 10진수 정수 형식으로 2자리로 출력 (남은 자리는 0으로 채움) |
%.2f | floating point (소수점)을 2자리 까지 출력 |
%% | 퍼센트 기호 출력 |
%7d
를 사용해 최대 7자리의 정수를 출력 후 남은 자리는 공백으로 채우고, 모든 값이 오른쪽 정렬이 되면서 문제에서 요구한 포맷과 비슷한 출력 형태가 나오도록 했습니다. 또한 \n
개행문자를 추가해 각 알파벳 자료에 줄바꿈을 해주었습니다.
문제를 처음 읽었을 때는 각 알파벳을 위한 배열을 만든 뒤 전체 텍스트에서 하나씩 찾아내는 방식으로 접근해야 하나 고민했습니다. HashMap
이 가진 특징 중, 데이터에 순서를 고려하지 않고 저장하며, key의 중복을 허용하지 않는다
는 점이 이 문제를 푸는데 큰 도움이 됐습니다. 덕분에 전체 텍스트를 인덱스 순으로 처음부터 끝까지 돌며 알파벳 순서를 신경쓰지 않고도 key 값을 편하게 수집할 수 있었고, 각 알파벳에 해당 value를 put 메소드를 이용해 쌍으로 저장할 수 있었습니다.
특히 getOrDefault
메소드를 알게된 것이 큰 수확이었는데요. 빈도수
를 구하는 데 핵심적인 역할을 해줬습니다. 지정된 key와 맵핑해둔 값이 없으면 내가 지정한 기본값을 반환하게 할 수 있다
는 점을 역이용해서 원하는 값을 구현할 수 있었습니다.
getOrDefault
: 찾는 key가 존재하면 해당 key와 맵핑된 value값을 반환하고, 없으면 지정한 기본값을 반환하고, 맵핑된 value값이 없더라도 NullPointerException을 방지코드의 기능 자체만 외울 것이 아니라 활용 방법을 탐색하는 자세가 중요하다는 걸 배울 수 있었던 기회였습니다.
참고
https://blog.jiniworld.me/68 (문자열 형식)
https://myeongju00.tistory.com/61 (StringBuilder)