[코테] 파일명 정렬 (정렬)

ekil·2026년 4월 23일

코딩테스트

목록 보기
14/15

파일명 정렬 (정렬)

2026.4.22, 2026.4.23 (작성일: 2026.4.23, 2026.4.24)

https://school.programmers.co.kr/learn/courses/30/lessons/17686

핵심 개념

  • 정규표현식
    • /\D/는 숫자가 아닌 것, /\d/는 숫자인 것 (/[0-9]/와 같음)
    • +를 붙이면 연속된 값을 찾음
    • match(정규식) 리턴 값은 [일치하는 문자열, index: 일치하는 첫번째 문자열 인덱스, input: 검사한 문자열 원문, groups: 그룹핑 결과]
    • 위 결과에서 일치하는 문자열을 사용하고 싶으면, [0]으로 접근 가능. match의 리턴 값을 백틱으로 감싼 ${} 안에 넣어도 뽑히는데, 길이가 1인 배열이라 우연히 동작하는 것임..!! (실행하다 [0]으로 접근했을 때 오류가 나서 저렇게 작성한 거였는데 다시 해보니 오류가 나지 않는다. 뭔가 다른 문법 오류가 있었나보다.)

내 풀이

function solution(files) {
    // 1. 파일명을 [HEAD, NUMBER, TAIL]로 분리한다.
    const slicedFiles = files.map(f => {
        // 정규표현식으로 연속된 숫자 값과 첫번째 인덱스를 구한다.
        const number = f.match(/\d+/);
        const NUMBER = `${number}`; // ✅ number[0]이 더 정확함
        const numberIndex = number.index;
        const tailIndex = numberIndex + NUMBER.length;
        
        // HEAD: 처음으로 숫자가 나오기 전까지
        const HEAD = f.substring(0, numberIndex);
        const TAIL = f.substring(tailIndex);
        
        // return [HEAD, NUMBER, TAIL];
        return { head: HEAD, number: NUMBER, tail: TAIL, fileName: f };
    });
    
    // 2. 기준에 맞게 정렬 시작
    const sorted = slicedFiles.sort((a, b) => {
        // 2-1. HEAD를 기준으로 사전 순 정렬 (대소문자 구분 없이)    
        if (a.head.toLowerCase() === b.head.toLowerCase()) {
            // 2-2. NUMBER를 숫자 순 정렬하되, 같은 값이면 순서를 유지 (0 리턴)
            return Number(a.number) === Number(b.number) ? 0 : Number(a.number) - Number(b.number);
        } else {
            return a.head.localeCompare(b.head);
        }
    });
    
    return sorted.map(s => s.fileName);
}

개선된 풀이

정규표현식을 바꾸지 않는 선에서 개선한 풀이

function solution(files) {
    // 1. 파일명을 { head, number, fileName } 구조로 분해한다.
    const destructured = files.map(f => {
        // 정규표현식으로 연속된 숫자 값과 첫번째 인덱스를 구한다.
        const number = f.match(/\d+/);
        
        // 정렬할 때 number는 숫자로 비교하므로 숫자로 변환해준다.
        const NUMBER = parseInt(`${number}`); 
        
        // head는 처음으로 숫자가 나오기 전까지 자른 문자열.
        // 정렬할 때 대소문자 구별 없으니 모두 소문자로 변환해준다.
        const numberIndex = number.index;
        const HEAD = f.substring(0, numberIndex).toLowerCase();
        
        return { head: HEAD, number: NUMBER, fileName: f };
    });
    
    // 2. 기준에 맞게 정렬 시작
    const sorted = destructured.sort((a, b) => {
        // head 사전 순 정렬
        if (a.head > b.head) return 1;
        if (a.head < b.head) return -1;
        
        // head가 같으면, number 숫자 순 정렬
        if (a.number > b.number) return 1;
        if (a.number < b.number) return -1;
        
        // head, number가 같으면 현재 순서 유지
        return 0;
    });
    
    return sorted.map(s => s.fileName);
}

정규표현식 고급 문법을 사용하도록 개선한 풀이

function solution(files) {
    // 1. 파일명을 { head, number, fileName } 구조로 분해한다.
    const destructured = files.map(f => {
        // 파일명 조건을 정규표현식으로 표현한다.
        const regex = /^([a-zA-Z-\. ]+)([0-9]+)(.*)$/;
        // const regex = /([a-zA-Z-\. ]+)([0-9]+)/;
        const [fileName, head, number] = f.match(regex);
        
        // head는 처음으로 숫자가 나오기 전까지 자른 문자열.
        // 정렬할 때 대소문자 구별 없으니 모두 소문자로 변환해준다.
        // 정렬할 때 number는 숫자로 비교하므로 숫자로 변환해준다.
        return { head: head.toLowerCase(), number: parseInt(number), fileName };
    });
    
    // 2. 기준에 맞게 정렬 시작
    const sorted = destructured.sort((a, b) => {
        // head 사전 순 정렬
        if (a.head > b.head) return 1;
        if (a.head < b.head) return -1;
        
        // head가 같으면, number 숫자 순 정렬
        if (a.number > b.number) return 1;
        if (a.number < b.number) return -1;
        
        // head, number가 같으면 현재 순서 유지
        return 0;
    });
    
    return sorted.map(s => s.fileName);
}

핵심 차이

1번 개선안

  • 정렬 기준을 보면 TAIL부는 고려하지 않으니, 구하지 않는다.
  • 파일명을 원하는 구조로 가공할 때, 정렬할 때 사용하는 형태로 리턴한다. (HEAD는 소문자로 통일, NUMBER는 숫자로 변환)
  • 정렬할 때 localeCompare 메서드 대신 단순 비교 연산자를 이용한다. (코드가 더 명확하게 보임)

2번 개선안

  • 문제의 '파일명' 규칙을 정규표현식으로 옮기면: const regex = /^([a-zA-Z-\. ]+)([0-9]+)(.*)$/;
    • ^: 문자열 시작
    • $: 문자열 종료
    • 위 두개를 넣지 않으면 시작/종료가 아닌 지점에 대해서도 검색할테니 명시하는 게 정확할 듯
    • 영어 대소문자, 숫자 같은 표현은 범위를 - 와 함께 적어주면 되고, 여러 조건은 그냥 나열하면 되네. (a-zA-Z 같이)
    • 문제에서 파일명에 허용되는 다른 문자열로 -, ., (공백)을 말했으니 그것도 넣어준다.
    • ()는 그룹화 패턴
    • +는 연속된 문자열을 찾는 패턴
    • .은 아무 문자열이나!
    • *은 앞에 온 것이 0번 이상 반복되는 패턴
    • .*은 임의의 문자열이 0번 이상 반복, 즉 아무 문자열이나 상관 없음 (빈 문자열도 OK)
    • 대소문자나 특수문자로 구성된 연속된 문자열 덩어리, 숫자로 구성된 연속된 문자열 덩어리, 아무거로나 구성된 문자열 덩어리로 쪼개는 것이다!
  • 'img2.JPG'.match(regex) 결과는 아래와 같다.
[
  'img2.JPG',
  'img',
  '2',
  '.JPG',
  index: 0,
  input: 'img2.JPG',
  groups: undefined
]
  • 아웃풋에서 적절히 꺼내서 쓰면 된다. 구조 분해 할당 가능하다.

막혔던 포인트

// 작성했던 풀이 노트
1. 주어진 문자열을 [HEAD, NUMBER, TAIL]로 쪼갠다.
   HEAD와 NUMBER를 구별할 방법 -> 처음으로 숫자가 나오는 지점에서 자르기
2. HEAD를 기준으로 오름차순 정렬한다.
3. HEAD 기준, '순서가 같은 것들' = '모두 소문자로 변환 시 "같은 문자열"인 것들'에 대해서만 NUMBER를 비교한다.
   NUMBER를 정수로 변환하고, 그 값을 오름차순 정렬한다.
   만약 정수로 변환한 값이 서로 같다면 정렬하지 않는다!
  • 정렬보단 "문자열 쪼개기"에서 좀 막혔다. 처음으로 숫자가 나오는 지점을 어떻게 판별하지?

풀면서 찾은 개념

문자열을 쪼갤 방법을 많이 찾아봤다.

  • split: seperator 인자로 문자열이나 정규표현식을 받음, 다만 문자열을 수정함 => 얕은 복사본에 대해 숫자를 찾는 정규표현식 담아 split을 실행하고, 잘린 것의 맨뒤를 제거하면 HEAD...
  • substring은 문자열을 반환함 (원본 수정 안하는 메서드)
  • Regex의 exec()은 만족하는 첫요소를 반환함 => 이걸 이용하고 인덱스를 찾을 수야 있겠지.. 근데 더 좋은 방법이 있을 것 같다.
  • match는 어떤 형태의 값을 리턴하는지 콘솔 로깅 여러번 해보니, 작성한 정규식을 만족하는 값, index, input, groups 키와 값을 배열로 뱉고 있었다. 신기한 발견
  • 정규표현식 문법을 이해하는 데는 이 글이 큰 도움이 되었다.

sort의 인자로 넘기는 콜백에서 0을 리턴하면 순서를 변경하지 않는다.

다음에 비슷한 문제 만나면

  • 늘 숙제 같던 정규표현식을 그래도 이번에 좀 들여다봤다. 사실 이번 문제는 AI한테 힌트 요청 안하고 검색만 적극 활용해서 풀었다. 검색이랑 프로그래머스에 있는 다른 사람의 풀이 보면서 개선작업! 그래서 비슷하게 정규식을 이용하는 게 가장 효율적일 때 전보다 덜 막막하게 풀어볼 수 있을 듯!!
profile
좋아하는 일을 잘함으로써 먹고살고 싶은 프론트엔드 개발자입니다.

0개의 댓글