문제3. 파일명 정렬

세 차례의 코딩 테스트와 두 차례의 면접이라는 기나긴 블라인드 공채를 무사히 통과해 카카오에 입사한 무지는 파일 저장소 서버 관리를 맡게 되었다.

저장소 서버에는 프로그램의 과거 버전을 모두 담고 있어, 이름 순으로 정렬된 파일 목록은 보기가 불편했다. 파일을 이름 순으로 정렬하면 나중에 만들어진 ver-10.zip이 ver-9.zip보다 먼저 표시되기 때문이다.

버전 번호 외에도 숫자가 포함된 파일 목록은 여러 면에서 관리하기 불편했다. 예컨대 파일 목록이 [“img12.png”, “img10.png”, “img2.png”, “img1.png”]일 경우, 일반적인 정렬은 [“img1.png”, “img10.png”, “img12.png”, “img2.png”] 순이 되지만, 숫자 순으로 정렬된 [“img1.png”, “img2.png”, “img10.png”, img12.png”] 순이 훨씬 자연스럽다.

무지는 단순한 문자 코드 순이 아닌, 파일명에 포함된 숫자를 반영한 정렬 기능을 저장소 관리 프로그램에 구현하기로 했다.

소스 파일 저장소에 저장된 파일명은 100 글자 이내로, 영문 대소문자, 숫자, 공백(“ “), 마침표(“.”), 빼기 부호(“-“)만으로 이루어져 있다. 파일명은 영문자로 시작하며, 숫자를 하나 이상 포함하고 있다.

파일명은 크게 HEAD, NUMBER, TAIL의 세 부분으로 구성된다.

  • HEAD는 숫자가 아닌 문자로 이루어져 있으며, 최소한 한 글자 이상이다.
  • NUMBER는 한 글자에서 최대 다섯 글자 사이의 연속된 숫자로 이루어져 있으며, 앞쪽에 0이 올 수 있다. 0부터 99999 사이의 숫자로, 00000이나 0101 등도 가능하다.
  • TAIL은 그 나머지 부분으로, 여기에는 숫자가 다시 나타날 수도 있으며, 아무 글자도 없을 수 있다.
파일명 HEAD NUMBER TAIL
foo9.txt foo 9 .txt
foo010bar020.zip foo 010 bar020.zip
F-15 F- 15 (빈 문자열)

파일명을 세 부분으로 나눈 후, 다음 기준에 따라 파일명을 정렬한다.

  • 파일명은 우선 HEAD 부분을 기준으로 사전 순으로 정렬한다. 이때, 문자열 비교 시 대소문자 구분을 하지 않는다. MUZImuzi, MuZi는 정렬 시에 같은 순서로 취급된다.
  • 파일명의 HEAD 부분이 대소문자 차이 외에는 같을 경우, NUMBER의 숫자 순으로 정렬한다. 9 < 10 < 0011 < 012 < 13 < 014 순으로 정렬된다. 숫자 앞의 0은 무시되며, 012와 12는 정렬 시에 같은 같은 값으로 처리된다.
  • 두 파일의 HEAD 부분과, NUMBER의 숫자도 같을 경우, 원래 입력에 주어진 순서를 유지한다. MUZI01.zipmuzi1.png가 입력으로 들어오면, 정렬 후에도 입력 시 주어진 두 파일의 순서가 바뀌어서는 안 된다.

무지를 도와 파일명 정렬 프로그램을 구현하라.

입력 형식

입력으로 배열 files가 주어진다.

  • files는 1000 개 이하의 파일명을 포함하는 문자열 배열이다.
  • 각 파일명은 100 글자 이하 길이로, 영문 대소문자, 숫자, 공백(“ “), 마침표(“.”), 빼기 부호(“-“)만으로 이루어져 있다. 파일명은 영문자로 시작하며, 숫자를 하나 이상 포함하고 있다.
  • 중복된 파일명은 없으나, 대소문자나 숫자 앞부분의 0 차이가 있는 경우는 함께 주어질 수 있다. (muzi1.txt, MUZI1.txt, muzi001.txt, muzi1.TXT는 함께 입력으로 주어질 수 있다.)

출력 형식

위 기준에 따라 정렬된 배열을 출력한다.

입출력 예제

입력: [“img12.png”, “img10.png”, “img02.png”, “img1.png”, “IMG01.GIF”, “img2.JPG”]
출력: [“img1.png”, “IMG01.GIF”, “img02.png”, “img2.JPG”, “img10.png”, “img12.png”]

입력: [“F-5 Freedom Fighter”, “B-50 Superfortress”, “A-10 Thunderbolt II”, “F-14 Tomcat”]
출력: [“A-10 Thunderbolt II”, “B-50 Superfortress”, “F-5 Freedom Fighter”, “F-14 Tomcat”]

문제 해설

코딩의 기초 문제라고 할 수 있는 정렬 문제입니다. 하지만 조건은 꽤나 복합적입니다. 파일명을 세 부분으로 나눠, 첫 부분은 대소문자 구분 없이Case Insensitive, 다음 부분은 숫자 값에 따라Numerical 정렬해야 합니다. 또한 정렬 기준에 따라 차이가 없다면 원래 입력에서 주어진 순서를 유지하는 안정 정렬Stable Sort을 사용해야 합니다. 정렬 문제를 풀 때 한 번씩은 다 해보셨죠? 그런데 이걸 어떻게 꿰어서 하나의 프로그램으로 만들어야 할지 어려워하시더군요.

여러 정렬 알고리즘을 배우셨을 텐데요. 이 중에 안정 정렬이 어떤 건지 알고 계신가요? 빠른 정렬 알고리즘으로 가장 유명한 퀵 정렬Quick Sort는 아쉽게도 안정 정렬이 아닙니다. 효율이 좋은 O(n log n) 복잡도의 정렬 알고리즘 중에는 병합 정렬Merge Sort 등 일부 알고리즘은 안정 정렬이고요. 효율이 떨어지는 O(n2) 복잡도의 알고리즘 중 버블 정렬Bubble Sort과 삽입 정렬Insertion Sort은 모두 안정 정렬입니다. 여러분이 아는 다른 알고리즘도 안정 정렬인지 아닌지 확인해보세요. 알고리즘의 구현 방법에 따라 같은 알고리즘이라도 안정 정렬이거나 아닐 수도 있습니다.

정렬 문제가 워낙 많이 쓰이므로 많은 프로그래밍 언어에서 정렬 알고리즘을 기본 함수로 제공하고 있습니다. 자신이 사용하는 프로그래밍 언어에서 안정 정렬 알고리즘을 제공해주는지 알아두시는 게 좋습니다. 코딩 테스트에서 사용된 프로그래밍 언어 중 C++과 Python에는 안정 정렬이 있고, Java와 JavaScript, Swift에는 안정 정렬이 없습니다. PHP 언어는 숫자 값을 고려해 정렬하는 natsort()를 기본 함수로 제공하기도 합니다. (아쉽게도 문제 3과 조건이 달라 그대로는 쓸 수 없지만요.)

기본 정렬 함수가 안정 정렬을 지원하지 않거나, 이 문제처럼 비교 조건이 까다로운 경우에는 decorate-sort-undecorate 패턴을 이용해서 쉽게 해결할 수도 있답니다.

이 문제의 정답률은 66.95%였습니다. 언어별로는 C++과 Python 사용자들이 힘들어했습니다. 안정 정렬을 지원해주는 언어인데 도움이 안 되었나 봅니다.




효율이 떨어지는 정렬을 사용했다. 나중에 병합 정렬을 사용해보기로 하고..


괜히 이상한 부분에서 많이 꼬였다. head 부분을 순서를 정렬하고 숫자를 정렬하니 자꾸 이상해서 뭔가 싶더만 같은 head끼리만 정렬이 되었어야 했다.


package kakao02;

import java.util.Arrays;
import java.util.Vector;

public class kakao02 {
	static int Answer;

	public static void main(String args[]) throws Exception {

		String[] input1 = new String[] { "img12.png", "img10.png", "img02.png", "img1.png", "IMG01.GIF", "img2.JPG" };
		String[] input2 = new String[] { "F-5 Freedom Fighter", "B-50 Superfortress", "A-10 Thunderbolt II",
				"F-14 Tomcat" };
		String[] answer1 = Solution(input1);
		String[] answer2 = Solution(input2);
		for (int i = 0; i < input1.length; ++i)
			System.out.println(answer1[i]);
		for (int i = 0; i < input2.length; ++i)
			System.out.println(answer2[i]);
	}

	public static String[] Solution(String[] input) {
		String[] result = input;
		int size = input.length;
		String[] head = new String[size];

		int[] number = new int[size];

		for (int i = 0; i < size; ++i) {
			int j = 1;
			int last = 0;
			for (j = 1; j < size; ++j) {
				if (input[i].charAt(j) >= 48 && input[i].charAt(j) <= 57) {
					head[i] = (input[i].substring(0, j).toUpperCase());
					last = j;
					System.out.println(head[i]);
					// System.out.println(j);
					break;
				}
			}

			for (j = last + 1; j < input[i].length(); ++j) {
				// System.out.println("enter");
				if (input[i].charAt(j) < 48 || input[i].charAt(j) > 57) {
					number[i] = (Integer.parseInt(input[i].substring(last, j)));
					System.out.println(number[i]);
					break;
				}
			}
		}
		
		for (int i = 0; i < size - 1; ++i) {
			for (int j = i + 1; j < size; ++j) {
				if (head[i].compareTo(head[j]) > 0) {
					String temp;
					String tempFile;
					int tempInt;
					temp = head[i];
					head[i] = head[j];
					head[j] = temp;

					tempInt = number[i];
					number[i] = number[j];
					number[j] = tempInt;

					tempFile = result[i];
					result[i] = result[j];
					result[j] = tempFile;
				}
			}
		}

		for (int i = 0; i < size - 1; ++i) {
			for (int j = i + 1; j < size; ++j) {
				if (number[i] > number[j] && head[i].compareTo(head[j]) == 0) {
					String temp;
					String tempFile;
					int tempInt;
					temp = head[i];
					head[i] = head[j];
					head[j] = temp;

					tempInt = number[i];
					number[i] = number[j];
					number[j] = tempInt;

					tempFile = result[i];
					result[i] = result[j];
					result[j] = tempFile;
				}
			}
		}

		return input;
	}
}


문제2. 압축

신입사원 어피치는 카카오톡으로 전송되는 메시지를 압축하여 전송 효율을 높이는 업무를 맡게 되었다. 메시지를 압축하더라도 전달되는 정보가 바뀌어서는 안 되므로, 압축 전의 정보를 완벽하게 복원 가능한 무손실 압축 알고리즘을 구현하기로 했다.

어피치는 여러 압축 알고리즘 중에서 성능이 좋고 구현이 간단한 LZW(Lempel–Ziv–Welch) 압축을 구현하기로 했다. LZW 압축은 1983년 발표된 알고리즘으로, 이미지 파일 포맷인 GIF 등 다양한 응용에서 사용되었다.

LZW 압축은 다음 과정을 거친다.

  1. 길이가 1인 모든 단어를 포함하도록 사전을 초기화한다.
  2. 사전에서 현재 입력과 일치하는 가장 긴 문자열 w를 찾는다.
  3. w에 해당하는 사전의 색인 번호를 출력하고, 입력에서 w를 제거한다.
  4. 입력에서 처리되지 않은 다음 글자가 남아있다면(c), w+c에 해당하는 단어를 사전에 등록한다.
  5. 단계 2로 돌아간다.

압축 알고리즘이 영문 대문자만 처리한다고 할 때, 사전은 다음과 같이 초기화된다. 사전의 색인 번호는 정수값으로 주어지며, 1부터 시작한다고 하자.

색인 번호 1 2 3 24 25 26
단어 A B C X Y Z

예를 들어 입력으로 KAKAO가 들어온다고 하자.

  1. 현재 사전에는 KAKAO의 첫 글자 K는 등록되어 있으나, 두 번째 글자까지인 KA는 없으므로, 첫 글자 K에 해당하는 색인 번호 11을 출력하고, 다음 글자인 A를 포함한 KA를 사전에 27 번째로 등록한다.
  2. 두 번째 글자 A는 사전에 있으나, 세 번째 글자까지인 AK는 사전에 없으므로, A의 색인 번호 1을 출력하고, AK를 사전에 28 번째로 등록한다.
  3. 세 번째 글자에서 시작하는 KA가 사전에 있으므로, KA에 해당하는 색인 번호 27을 출력하고, 다음 글자 O를 포함한 KAO를 29 번째로 등록한다.
  4. 마지막으로 처리되지 않은 글자 O에 해당하는 색인 번호 15를 출력한다.
현재 입력(w) 다음 글자(c) 출력 사전 추가(w+c)
K A 11 27: KA
A K 1 28: AK
KA O 27 29: KAO
O   15  

이 과정을 거쳐 다섯 글자의 문장 KAKAO가 4개의 색인 번호 [11, 1, 27, 15]로 압축된다.

입력으로 TOBEORNOTTOBEORTOBEORNOT가 들어오면 다음과 같이 압축이 진행된다.

현재 입력(w) 다음 글자(c) 출력 사전 추가(w+c)
T O 20 27: TO
O B 15 28: OB
B E 2 29: BE
E O 5 30: EO
O R 15 31: OR
R N 18 32: RN
N O 14 33: NO
O T 15 34: OT
T T 20 35: TT
TO B 27 36: TOB
BE O 29 37: BEO
OR T 31 38: ORT
TOB E 36 39: TOBE
EO R 30 40: EOR
RN O 32 41: RNO
OT   34  

입력 형식

입력으로 영문 대문자로만 이뤄진 문자열 msg가 주어진다. msg의 길이는 1 글자 이상, 1000 글자 이하이다.

출력 형식

주어진 문자열을 압축한 후의 사전 색인 번호를 배열로 출력하라.

입출력 예제

msg answer
KAKAO [11, 1, 27, 15]
TOBEORNOTTOBEORTOBEORNOT [20, 15, 2, 5, 15, 18, 14, 15, 20, 27, 29, 31, 36, 30, 32, 34]
ABABABABABABABAB [1, 2, 27, 29, 28, 31, 30]

문제 해설

GIF 파일 등에서 실제로 쓰이는 LZW 알고리즘을 설명해주고, 구현하는 문제입니다. 실제로 쓰이는 알고리즘을 구현해보는 것이 어떠셨나요? 압축이라는 말만으로 얼핏 어려워 보이지만, 설명에 나온 의사코드Pseudocode를 그대로 따라서 구현만 하면 되는 문제로, 기초적인 문자열과 배열을 다룰 수 있다면 풀 수 있는 문제입니다.

이 문제의 정답률은 95.80%입니다. 가장 많은 지원자가 잘 풀어주셨습니다. 언어별로는 Java 언어 사용자들이 조금 어려워했습니다.





예전에 신입 공채 문제를 그냥 구경하던 도중에 문제 해설만 보고 GIF 변환 알고리즘 관련 문제가 나온다고? 하면서 겁먹었던 기억이 난다.


문제는 별로 어려울게 없는데 초기 26개 단어 사전을 일일히 쳐야 하나 했다가 Character.toString을 이용해서 작성했다.


쓰지 않던 것이라 노가다를 해야 하는 부분에서 잠깐 막혔다.


그 외에는 for문에서 익숙하게 보이던 ;;++i 부분이 빠져있다. 밑에서 jump를 더하기 때문인데 그냥 안에 i += jump로 써도 된다.



package kakao02;

import java.util.Vector;

public class kakao02 {
	static int Answer;

	public static void main(String args[]) throws Exception {

		Vector<Integer> answer1 = Solution("KAKAO");
		Vector<Integer> answer2 = Solution("TOBEORNOTTOBEORTOBEORNOT");
		Vector<Integer> answer3 = Solution("ABABABABABABABAB");
		System.out.println(answer1);
		System.out.println(answer2);
		System.out.println(answer3);
	}

	public static Vector<Integer> Solution(String msg) {
		Vector<String> dic = new Vector<String>();
		Vector<Integer> result = new Vector<Integer>();
		for(int i = 0; i<26; ++i)
		{
			dic.add(Character.toString((char)(65+i)));
		}
		for(int i = 0; i<msg.length();)
		{
			int n = -1;
			int tempN = -1;
			int jump = 1;
			for(int j = i+1; j<=msg.length();++j)
			{
				tempN = dic.indexOf(msg.substring(i,j));
				if(tempN != -1)
				{
					n = tempN;
					tempN = -1;
					jump = j-i;
				}
				else
				{
					dic.add(msg.substring(i, j));
					break;
				}
			}
			result.add(n+1);
			i+=jump;
		}
		return result;
	}
}


문제1. N진수 게임

튜브가 활동하는 코딩 동아리에서는 전통적으로 해오는 게임이 있다. 이 게임은 여러 사람이 둥글게 앉아서 숫자를 하나씩 차례대로 말하는 게임인데, 규칙은 다음과 같다.

  1. 숫자를 0부터 시작해서 차례대로 말한다. 첫 번째 사람은 0, 두 번째 사람은 1, … 열 번째 사람은 9를 말한다.
  2. 10 이상의 숫자부터는 한 자리씩 끊어서 말한다. 즉 열한 번째 사람은 10의 첫 자리인 1, 열두 번째 사람은 둘째 자리인 0을 말한다.

이렇게 게임을 진행할 경우,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 0, 1, 1, 1, 2, 1, 3, 1, 4, …
순으로 숫자를 말하면 된다.

한편 코딩 동아리 일원들은 컴퓨터를 다루는 사람답게 이진수로 이 게임을 진행하기도 하는데, 이 경우에는
0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, …
순으로 숫자를 말하면 된다.

이진수로 진행하는 게임에 익숙해져 질려가던 사람들은 좀 더 난이도를 높이기 위해 이진법에서 십육진법까지 모든 진법으로 게임을 진행해보기로 했다. 숫자 게임이 익숙하지 않은 튜브는 게임에 져서 벌칙을 받는 굴욕을 피하기 위해, 자신이 말해야 하는 숫자를 스마트폰에 미리 출력해주는 프로그램을 만들려고 한다. 튜브의 프로그램을 구현하라.

입력 형식

진법 n, 미리 구할 숫자의 갯수 t, 게임에 참가하는 인원 m, 튜브의 순서 p 가 주어진다.

  • 2 ≦ n ≦ 16
  • 0 < t ≦ 1000
  • 2 ≦ m ≦ 100
  • 1 ≦ pm

출력 형식

튜브가 말해야 하는 숫자 t개를 공백 없이 차례대로 나타낸 문자열. 단, 10~15는 각각 대문자 A~F로 출력한다.

입출력 예제

n t m p result
2 4 2 1 “0111”
16 16 2 1 “02468ACE11111111”
16 16 2 2 “13579BDF01234567”

문제 해설

반복문과 진법 변환을 할 수 있다면 어렵지 않게 풀 수 있는 문제입니다. 진법 변환은 프로그래밍 언어를 처음 배울 때 연습 문제로 많이 풀어봤을 문제일 텐데요. 오래간만에 풀어보려니 쉽지만은 않았던 듯 싶습니다.

참고로 이 문제는 챔퍼나운 수라는 수학 상수를 이용한 문제입니다.

이 문제의 정답률은 91.85%였습니다. 대부분 잘 풀어주셨으나, 언어별로는 C++ 사용자들이 약간 어려워했습니다.




진번 변환 문제를 어떻게 풀어야 되는지 순간 기억이 나지 않았다.  그 외에는 별 문제는 없었는데


미리 전체 String을 만들고 그 안에서 튜브가 원하는 글자를 뽑아주는데 전체 문장을 만들 때 m * t의 개수만큼의 숫자를 먼저 계산을 해버렸다.


좀 더 효율적인 방법이 있을텐데..  딱 필요한 개수만큼의 숫자를 먼저 계산하고 그 안에서 뽑을 수 있다면 더 좋을 것이다.



package kakao02;

public class kakao02 {
	static int Answer;

	public static void main(String args[]) throws Exception {

		String answer1 = Solution(2, 4, 2, 1);
		String answer2 = Solution(16, 16, 2, 1);
		String answer3 = Solution(16, 16, 2, 2);
		System.out.println(answer1);
		System.out.println(answer2);
		System.out.println(answer3);
	}

	public static String Solution(int n, int t, int m, int p) {

		String[] lastNum = new String[] { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E",
				"F" };
		String allText = "0";
		String result = "";
		if (n <= 10) {
			for (int i = 1; i < t * m; ++i) {
				int temp = i;
				String tempS = "";
				while (true) {
					tempS = Integer.toString(temp % n) + tempS;
					temp /= n;
					if (temp == 0)
						break;
				}
				allText += tempS;

			}
		} else {
			for (int i = 1; i < t * m; ++i) {
				int temp = i;
				String tempS = "";
				while (true) {
					String num;
					num = lastNum[temp % n];
					temp /= n;
					tempS = num + tempS;
					if (temp == 0)
						break;
				}
				allText += tempS;
			}
		}

		for (int i = 0; i < t; ++i) {
			result += allText.charAt((p - 1) + m * i);
		}
		return result;
	}
}

+ Recent posts