기록의 공유

잊지않기 위한 기록의 공유

Backend/Problem Solving

코데 대비4 - 정규표현식(문자열검증), 우선순위큐,n진법

backend dev 2025. 12. 29. 11:04

문자열검사

방법

1. Character 클래스 사용

2. Stream API 사용

3. 정규표현식 사용

 

 

문자열에 숫자가 있는지 검사

Character 클래스 사용

public static boolean hasDigit(String str) {
    for (char c : str.toCharArray()) {
        if (Character.isDigit(c)) {
            return true;
        }
    }
    return false;
}

Stream API를 사용

public static boolean hasDigit(String str) {
    return str.chars().anyMatch(Character::isDigit);
}


정규표현식 사용

public static boolean hasDigit(String str) {
    return str.matches(".*\\d.*");
}

 

문자열에 소문자가 있는지 검사

Character 클래스 사용

public static boolean hasLowerCase(String str) {
    for (char c : str.toCharArray()) {
        if (Character.isLowerCase(c)) {
            return true;
        }
    }
    return false;
}

Stream API를 사용

public static boolean hasLowerCase(String str) {
    return str.chars().anyMatch(Character::isLowerCase);
}

정규 표현식 사용

public static boolean hasLowerCase(String str) {
    return str.matches(".*[a-z].*");
}

 

문자열에 대문자가 있는지 검사

Character 클래스 사용

public static boolean hasUpperCase(String str) {
    for (char c : str.toCharArray()) {
        if (Character.isUpperCase(c)) {
            return true;
        }
    }
    return false;
}

Stream API를 사용

public static boolean hasUpperCase(String str) {
    return str.chars().anyMatch(Character::isUpperCase);
}

정규표현식 사용

public static boolean hasUpperCase(String str) {
    return str.matches(".*[A-Z].*");
}

 


정규표현식

정규표현식(Regular Expression, Regex)은 문자열 패턴을 표현하는 형식 언어입니다.

 

 

주요 용도

  • 문자열 검색 및 매칭
  • 문자열 치환
  • 입력값 유효성 검증 (이메일, 전화번호 등)
  • 로그 파싱
  • 데이터 추출

 

2. 기본 문법

 

리터럴 매칭

"hello".matches("hello")      // true - 정확히 일치
"hello".matches("Hello")      // false - 대소문자 구분
"hello world".matches("hello") // false - 전체 문자열 매칭 필요

 

와일드카드 (.)

  • 모든 문자 하나와 매칭 (개행 문자 제외)
"cat".matches("c.t")    // true
"cot".matches("c.t")    // true
"c9t".matches("c.t")    // true
"ct".matches("c.t")     // false - 문자가 하나 필요
"cart".matches("c.t")   // false - 전체 매칭 안됨

 

 

3. 메타 문자 상세

메타 문자는 특별한 의미를 가진 문자들입니다.

 

전체 메타 문자 목록

^ $ . * + ? { } [ ] \ | ( )

 

^  (문자열 시작)

"hello".matches("^h.*")     // true
"world".matches("^h.*")     // false

 

$ (문자열 끝)

"hello".matches(".*o$")     // true
"hello".matches(".*e$")     // false

 

.  (임의의 문자 하나)

"a1b".matches("a.b")        // true
"a b".matches("a.b")        // true
"ab".matches("a.b")         // false

 

* (0회 이상 반복)

"".matches("a*")            // true - 0개도 가능
"a".matches("a*")           // true
"aaaa".matches("a*")        // true
"b".matches("a*")           // false

 

+ (1회 이상 반복)

"".matches("a+")            // false - 최소 1개 필요
"a".matches("a+")           // true
"aaaa".matches("a+")        // true

 

? (0회 또는 1회 (있거나 없거나))

"color".matches("colou?r")  // true
"colour".matches("colou?r") // true
"colouur".matches("colou?r") // false

 

| (OR 연산자)

"cat".matches("cat|dog")    // true
"dog".matches("cat|dog")    // true
"bird".matches("cat|dog")   // false

 

() (그룹화)

"gray".matches("gr(a|e)y")  // true
"grey".matches("gr(a|e)y")  // true

 

4. 문자 클래스

기본 문자 클래스 [...]

  • 대괄호 안의 문자 중 하나와 매칭
"a".matches("[abc]")        // true
"b".matches("[abc]")        // true
"d".matches("[abc]")        // false

 

범위 지정 [a-z]

"m".matches("[a-z]")        // true - 소문자
"M".matches("[a-z]")        // false
"5".matches("[0-9]")        // true - 숫자
"a".matches("[a-zA-Z]")     // true - 영문자

 

부정 문자 클래스 [^...]

  • 대괄호 안의 문자를 제외한 모든 문자
"a".matches("[^0-9]")       // true - 숫자가 아님
"5".matches("[^0-9]")       // false

 

 

여러문자를 확인하고싶다면 메타문자를 이용한다.

"aa".matches("[a-z]")       // false
"aa".matches("[a-z]+")      // true
"aa".matches("[a-z]*")      // true

"aa".matches("(aa)")        // true

 

 

미리 정의된 문자 클래스

 

/w,/W에 _라는 문자가 포함되어있는걸 주의한다.

"5".matches("\\d")          // true
"a".matches("\\d")          // false
"_".matches("\\w")          // true
" ".matches("\\s")          // true

 

주의: Java 문자열에서는 \를 \\로 이스케이프해야 합니다!

 

 

5. 수량자

정확한 횟수 {n}

"aaa".matches("a{3}")       // true
"aa".matches("a{3}")        // false
"aaaa".matches("a{3}")      // false

 

범위 지정 {n,m}

"aa".matches("a{2,4}")      // true
"aaa".matches("a{2,4}")     // true
"aaaa".matches("a{2,4}")    // true
"a".matches("a{2,4}")       // false
"aaaaa".matches("a{2,4}")   // false

 

최소 횟수 {n,}

"aa".matches("a{2,}")       // true
"aaaa".matches("a{2,}")     // true
"a".matches("a{2,}")        // false

 

 

 

특수 문자 이스케이프

 

메타 문자를 일반 문자로 사용하기

모든 메타 문자를 문자 그대로 매칭하려면 \를 앞에 붙입니다.

 

// 점(.)을 문자 그대로
"3.14".matches("3\\.14")           // true
"3x14".matches("3\\.14")           // false

// 별표(*)를 문자 그대로
"a*b".matches("a\\*b")             // true
"aaab".matches("a\\*b")            // false

// 대괄호를 문자 그대로
"[a]".matches("\\[a\\]")           // true

// 백슬래시 자체를 매칭
"a\\b".matches("a\\\\b")           // true

// 달러 기호
"$100".matches("\\$\\d+")          // true

// 괄호
"(test)".matches("\\(test\\)")     // true

 

 

Java에서 이중 백슬래시가 필요한 이유

// Java 문자열에서:
"\\"    → 실제 문자: \  (백슬래시 1개)
"\\d"   → 실제 문자: \d (정규표현식 엔진으로 전달)
"\\\\d" → 실제 문자: \\d (백슬래시 + d 문자)

// 예제
"5".matches("\\d")        // \d (숫자 패턴)
"\\d".matches("\\\\d")    // \\d (백슬래시+d 문자 그대로)

 

 

문자 클래스 내부에서는 대부분 이스케이프 불필요

// 범위 vs 리터럴 하이픈 비교
"a-b".matches("[a-b]")        // false (범위: a, b 중 1글자만)
"a-b".matches("[a\\-b]")      // false (문자: a, -, b 중 1글자만)
"a-b".matches("[a\\-b]+")     // true ✅ (a, -, b가 1개 이상)

"-".matches("[a-b]")          // false (하이픈은 a~b 범위에 없음)
"-".matches("[a\\-b]")        // true ✅ (리터럴 하이픈)
"-".matches("[-]")            // true ✅ (첫 위치라 리터럴)

// ^ 문자의 위치별 의미
"^".matches("[^a]")           // true ✅ (부정: a가 아닌 것)
"^".matches("[\\^]")          // true ✅ (리터럴 ^ 문자)
"^".matches("[a^]")           // true ✅ (첫 위치 아니면 리터럴)

// 부정 문자 클래스 예제
"a".matches("[^a]")           // false (a 제외)
"b".matches("[^a]")           // true (a 아님)
"x".matches("[^abc]")         // true (a,b,c 모두 아님)
"b".matches("[^abc]")         // false (b는 제외됨)

 

 

 

Java에서 정규표현식 사용

방법 1: String.matches() -> 코테에서 사용

  • 전체 문자열이 패턴과 일치하는지 확인
 
String text = "hello123";
boolean result = text.matches("[a-z]+\\d+");  // true

 

방법 2: Pattern & Matcher (권장) -> 개발에서 사용

  • 더 유연하고 성능이 좋음
String text = "Email: test@example.com, Phone: 010-1234-5678";
Pattern pattern = Pattern.compile("\\w+@\\w+\\.\\w+");
Matcher matcher = pattern.matcher(text);

// 찾기
if (matcher.find()) {
    System.out.println("Found: " + matcher.group());  // test@example.com
}

// 모두 찾기
while (matcher.find()) {
    System.out.println(matcher.group());
}

 

많이쓰이는 패턴 예시

 

문자열 검증 패턴

public class ValidationPatterns {
    
    // 숫자 포함 여부
    public static boolean hasDigit(String str) {
        return str.matches(".*\\d.*");
        // "abc123" → true, "abc" → false
        // str.matches(".*[0-9].*") 와 동일하다.
        // 어떤 문자든 앞뒤에 있어도 되고 없어도 되는 상태에서 숫자가 최소 하나라도 있다면 true
    }
    
    // 영문자 포함 여부
    public static boolean hasLetter(String str) {
        return str.matches(".*[a-zA-Z].*");
        // "123abc" → true, "123" → false
        // .* → 앞에 아무 문자나 0개 이상
        // [a-zA-Z] → 영문자(대소문자) 1개
        // .* → 뒤에 아무 문자나 0개 이상
        // 문자열 어디든 영문자가 최소 하나라도 포함되면 true
    }
    
    // 특수문자 포함 여부
    public static boolean hasSpecialChar(String str) {
        return str.matches(".*[^a-zA-Z0-9].*");
        // "hello!" → true, "hello" → false
        // [^a-zA-Z0-9] → 영문자도 숫자도 아닌 문자 1개 (즉, 특수문자)
        // [^...] 는 "...이 아닌 것"을 의미
        // 문자열 어디든 특수문자(공백, 기호 등)가 최소 하나라도 포함되면 true
    }
    
    // 숫자로만 구성
    public static boolean onlyDigits(String str) {
        return str.matches("\\d+");
        // "12345" → true, "123a" → false
        // str.matches("[0-9]+") 와 동일하다.
        // \\d+ → 숫자가 1개 이상 연속으로
        // +가 없으면 \\d는 숫자 정확히 1개만 매칭 → "1"만 true, "123"은 false
        // 전체 문자열이 숫자로만 이루어져 있으면 true
    }
    
    // 영문자로만 구성
    public static boolean onlyLetters(String str) {
        return str.matches("[a-zA-Z]+");
        // "Hello" → true, "Hello123" → false
        // [a-zA-Z]+ → 영문자(대소문자)가 1개 이상 연속으로
        // +가 없으면 [a-zA-Z]는 영문자 정확히 1개만 매칭 → "a"만 true, "abc"는 false
        // 전체 문자열이 영문자로만 이루어져 있으면 true (숫자, 공백, 특수문자 포함 시 false)
    }
    
    // 영문자+숫자로만 구성
    public static boolean alphanumeric(String str) {
        return str.matches("[a-zA-Z0-9]+");
        // "abc123" → true, "abc_123" → false
        // str.matches("\\w+")는 다르다! (\\w는 밑줄(_)도 포함)
        // [a-zA-Z0-9]+ → 영문자 또는 숫자가 1개 이상 연속으로
        // 전체 문자열이 영문자와 숫자의 조합으로만 이루어져 있으면 true
        // 공백, 특수문자(_포함), 한글 등이 포함되면 false
    }
}

 

 

문자열 치환/제거 패턴

public class ReplacePatterns {
    
    // 모든 숫자 제거
    public static String removeDigits(String str) {
        return str.replaceAll("\\d", "");
        // "abc123def456" → "abcdef"
        // \\d → 숫자 1개를 찾아서 빈 문자열로 교체
        // replaceAll이므로 모든 숫자를 각각 찾아서 전부 제거
        // str.replaceAll("[0-9]", "") 와 동일
    }
    
    // 모든 공백 제거
    public static String removeSpaces(String str) {
        return str.replaceAll("\\s", "");
        // "hello world" → "helloworld"
        // \\s → 공백 문자 1개 (스페이스, 탭, 줄바꿈 등)
        // 연속된 공백이든 단일 공백이든 모두 각각 찾아서 제거
        // "a  b" (공백 2개) → 공백 2개를 각각 제거 → "ab"
    }
    
    // 여러 공백을 하나로
    public static String normalizeSpaces(String str) {
        return str.replaceAll("\\s+", " ");
        // "hello    world" → "hello world"
        // \\s+ → 연속된 공백 1개 이상을 하나의 덩어리로 매칭
        // +가 없으면: 공백 하나씩 각각 처리 → 의미 없음 (그냥 공백을 공백으로 교체)
        // +가 있으면: 연속된 공백 덩어리를 찾아서 → 공백 1개로 교체
        // "a    b" (공백 4개) → 공백 4개 덩어리를 찾아서 공백 1개로 → "a b"
    }
    
    // 영문자가 아닌 모든 것 제거
    public static String keepOnlyLetters(String str) {
        return str.replaceAll("[^a-zA-Z]", "");
        // "Hello, World! 123" → "HelloWorld"
        // [^a-zA-Z] → 영문자가 아닌 모든 문자 1개 (숫자, 공백, 특수문자 등)
        // [^...] → "...이 아닌 것"을 의미
        // 영문자를 제외한 나머지를 모두 찾아서 제거 → 영문자만 남음
    }
    
    // 숫자가 아닌 모든 것 제거
    public static String keepOnlyDigits(String str) {
        return str.replaceAll("[^0-9]", "");
        // "abc-123-def-456" → "123456"
        // [^0-9] → 숫자가 아닌 모든 문자 1개 (영문자, 공백, 특수문자 등)
        // 숫자를 제외한 나머지를 모두 제거 → 숫자만 남음
    }
    
    // 숫자가 아닌 모든 것 제거 (간단 버전)
    public static String keepOnlyDigits2(String str) {
        return str.replaceAll("\\D", "");
        // "abc-123-def-456" → "123456"
        // \\D → 숫자가 아닌 문자 (Digit가 아닌 것)
        // [^0-9]와 완전히 동일한 의미, 더 짧고 간결함
    }
    
    // 특수문자 제거 (영문자, 숫자, 공백만 남김)
    public static String removeSpecialChars(String str) {
        return str.replaceAll("[^a-zA-Z0-9\\s]", "");
        // "Hello@World#123!" → "HelloWorld123"
        // [^a-zA-Z0-9\\s] → 영문자도, 숫자도, 공백도 아닌 문자 (즉, 특수문자)
        // \\s를 넣어서 공백은 유지됨
        // "Hello@World #123!" → "HelloWorld 123" (공백 포함 시)
        // 특수문자만 골라서 제거 → 영문자, 숫자, 공백만 남음
    }
}

 

 

문자열 분리 패턴

public class SplitPatterns {
    
    // 공백으로 분리 (여러 공백도 처리)
    public static String[] splitBySpaces(String str) {
        return str.split("\\s+");
        // "hello    world  test" → ["hello", "world", "test"]
        // \\s+ → 연속된 공백 1개 이상을 구분자로 사용
        // +가 없으면: 공백 1개씩만 구분 → "a  b"는 ["a", "", "b"] (빈 문자열 생김)
        // +가 있으면: 연속된 공백을 하나의 구분자로 인식 → 빈 문자열 안 생김
        // 공백이 1개든 10개든 상관없이 단어만 깔끔하게 분리됨
    }
    
    // 쉼표나 공백으로 분리
    public static String[] splitByCommaOrSpace(String str) {
        return str.split("[,\\s]+");
        // "a,b, c  d" → ["a", "b", "c", "d"]
        // [,\\s] → 쉼표 또는 공백 중 하나
        // [,\\s]+ → 쉼표와 공백이 섞여서 1개 이상 연속된 것을 구분자로 사용
        // "a, , b" (쉼표+공백 혼합) → 전체를 하나의 구분자로 인식
        // 쉼표 앞뒤 공백이나 연속 쉼표 상관없이 깔끔하게 분리
    }
    
    // 숫자로 분리
    public static String[] splitByDigits(String str) {
        return str.split("\\d+");
        // "abc123def456ghi" → ["abc", "def", "ghi"]
        // \\d+ → 연속된 숫자 1개 이상을 구분자로 사용
        // 숫자 부분을 전부 제거하고 문자 부분만 분리해서 배열로 반환
        // "abc1def" → ["abc", "def"]
        // "abc123def" → ["abc", "def"] (숫자 개수 상관없음)
        // +가 없으면: 숫자 1개씩만 구분 → "a12b"는 ["a", "", "b"]
    }
    
    // 영문자로 분리
    public static String[] splitByLetters(String str) {
        return str.split("[a-zA-Z]+");
        // "123abc456def789" → ["123", "456", "789"]
        // [a-zA-Z]+ → 연속된 영문자 1개 이상을 구분자로 사용
        // 영문자 부분을 전부 제거하고 숫자 부분만 분리해서 배열로 반환
        // 대소문자 구분 없이 모든 영문자를 구분자로 인식
        // "123a456ABC789" → ["123", "456", "789"]
    }
    
    // 특수문자로 분리
    public static String[] splitBySpecialChars(String str) {
        return str.split("[^a-zA-Z0-9]+");
        // "hello@world#test" → ["hello", "world", "test"]
        // [^a-zA-Z0-9] → 영문자도 숫자도 아닌 문자 (즉, 특수문자와 공백)
        // [^a-zA-Z0-9]+ → 연속된 특수문자/공백 1개 이상을 구분자로 사용
        // 특수문자를 전부 제거하고 영문자+숫자 덩어리만 분리
        // "hello@@world" → ["hello", "world"] (특수문자 개수 상관없음)
        // "abc_123-def" → ["abc", "123", "def"] (밑줄, 하이픈도 특수문자로 처리)
    }
}

 

주요 포인트:

  1. replaceAll: 패턴과 매칭되는 부분을 교체 (제거하려면 빈 문자열로)
  2. split: 패턴과 매칭되는 부분을 구분자로 사용해서 문자열 분리
  3. +의 중요성: 연속된 것을 하나로 처리하여 빈 문자열이나 중복 제거 방지
  4. [^...]: 부정 문자 클래스로 "~이 아닌 것" 표현 가능

 

복습문제
https://school.programmers.co.kr/learn/courses/30/lessons/72410


 

우선순위큐 ( PriorityQueue )

 

1. 기본 개념

  • Min Heap: 기본 설정, 작은 값이 먼저 나옴
  • 내부 구조: 완전 이진 트리 기반 힙
  • 용도: K번째 값, 최대/최소 추적, 다익스트라, Top K 문제

 

시간복잡도

 

 

Min Heap (기본)

PriorityQueue pq = new PriorityQueue<Integer>();

 

Max Heap

PriorityQueue pq = new PriorityQueue<Integer>(Comparator.reverseOrder());

// 또는
PriorityQueue pq = new PriorityQueue<Integer>((o1, o2) -> o2 - o1);

 

커스텀 정렬

// 배열 첫 번째 요소 기준
PriorityQueue pq = new PriorityQueue<int[]>((o1, o2) -> o1[0] - o2[0]);

// 복합 조건
PriorityQueue pq = new PriorityQueue<int[]>((o1, o2) -> {
    if (o1[0] != o2[0]) return o1[0] - o2[0];  // 첫 번째 오름차순
    return o2[1] - o1[1];                     // 두 번째 내림차순
});

// 절댓값 기준 (같으면 실제 값)
PriorityQueue pq = new PriorityQueue<Integer>((o1, o2) -> {
    int abs = Integer.compare(Math.abs(o1), Math.abs(o2));
    return abs != 0 ? abs : Integer.compare(o1, o2);
});

// ⚠️ Overflow 방지: (a, b) -> a - b 대신 Integer.compare(a, b) 사용!

 

 

주요 메서드

PriorityQueue<> pq = new PriorityQueue<Integer>();

// 삽입
pq.offer(5);        // 권장 (실패 시 false)
pq.add(5);          // 실패 시 예외

// 조회 (제거 X)
pq.peek();          // 비어있으면 null

// 삭제
pq.poll();          // 비어있으면 null
pq.remove();        // 비어있으면 예외

// 기타
pq.size();
pq.isEmpty();
pq.clear();

 

코딩테스트 핵심 패턴

 

패턴 1: K번째 최대/최소값 , Top K

 

언제 사용?

  • "K번째로 큰/작은 값을 찾으세요"
  • "상위 K개를 찾으세요"
  • 배열/스트림에서 K번째 요소 찾기

핵심 아이디어: 힙 크기를 K로 제한해서 메모리 절약 + O(n log k) 시간에 해결

 

K번째 최솟값

public int findKthSmallest(int[] nums, int k) {
    // Max Heap 사용 - 큰 값이 루트에 위치
    PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
    
    for (int num : nums) {
        maxHeap.offer(num);  // 일단 모든 값 추가
        
        // 힙 크기가 k를 초과하면
        if (maxHeap.size() > k) {
            maxHeap.poll();  // 가장 큰 값(루트) 제거
        }
        // 결과: 힙에는 가장 작은 k개만 남음
    }
    
    // 힙의 루트 = 작은 k개 중 가장 큰 값 = k번째 작은 값
    return maxHeap.peek();
}

/*
예시: [3, 2, 1, 5, 6, 4], k=2 (2번째 작은 값 찾기)

과정:
1. 3 추가 → [3]
2. 2 추가 → [3, 2], size=2
3. 1 추가 → [3, 2, 1], size>k → 3 제거 → [2, 1]
4. 5 추가 → [5, 2, 1], size>k → 5 제거 → [2, 1]
5. 6 추가 → [6, 2, 1], size>k → 6 제거 → [2, 1]
6. 4 추가 → [4, 2, 1], size>k → 4 제거 → [2, 1]

최종: [2, 1] → 루트 2가 정답 (2번째 작은 값)

왜 Max Heap?
- 작은 값들을 유지하면서, 그 중 가장 큰 값(루트)을 쉽게 제거하기 위해
- K개를 유지하면, 루트가 정확히 K번째 작은 값
*/

// 시간: O(n log k), 공간: O(k)

 

K번째 최댓값

public int findKthLargest(int[] nums, int k) {
    // Min Heap 사용 - 작은 값이 루트에 위치
    PriorityQueue<Integer> minHeap = new PriorityQueue<>();
    
    for (int num : nums) {
        minHeap.offer(num);  // 일단 모든 값 추가
        
        // 힙 크기가 k를 초과하면
        if (minHeap.size() > k) {
            minHeap.poll();  // 가장 작은 값(루트) 제거
        }
        // 결과: 힙에는 가장 큰 k개만 남음
    }
    
    // 힙의 루트 = 큰 k개 중 가장 작은 값 = k번째 큰 값
    return minHeap.peek();
}

/*
예시: [3, 2, 1, 5, 6, 4], k=2 (2번째 큰 값 찾기)

과정:
1. 3 추가 → [3]
2. 2 추가 → [2, 3], size=2
3. 1 추가 → [1, 2, 3], size>k → 1 제거 → [2, 3]
4. 5 추가 → [2, 3, 5], size>k → 2 제거 → [3, 5]
5. 6 추가 → [3, 5, 6], size>k → 3 제거 → [5, 6]
6. 4 추가 → [4, 5, 6], size>k → 4 제거 → [5, 6]

최종: [5, 6] → 루트 5가 정답 (2번째 큰 값)

왜 Min Heap?
- 큰 값들을 유지하면서, 그 중 가장 작은 값(루트)을 쉽게 제거하기 위해
- K개를 유지하면, 루트가 정확히 K번째 큰 값
*/

// 시간: O(n log k), 공간: O(k)

 

암기 팁:

  • K번째 최솟값 → Max Heap
    (k개 빼고 큰 수들은 poll()로 인해 다 잘려나가서 결국 가장 작은 k개가 남고 그걸 poll()하니까 k번째 최솟값을 구할 수 있다.)
    K번째 최솟값을 구하기위해 K개만 남겨두고 큰값들은 다 잘라야하니까 Max Heap
  • K번째 최댓값 → Min Heap
    ( k개 빼고 작은 수들은 poll()로 인해 다 잘려나가서 결국 가장 큰 k개가 남고 그걸 poll()하니까 k번째 최댓값을 구할 수 있다. )
    K번째 최댓값을 구하기위해 K개만 남겨두고 작은값들은 다 잘라야하니까 Min Heap

 

응용패턴 : Top K 빈도수

 

"가장 많이 등장한 K개의 요소"
"빈도수 상위 K개 찾기"
빈도수 기준 Top K 문제

public int[] topKFrequent(int[] nums, int k) {
    // 1단계: 빈도수 계산
    // 시간: O(n)
    Map<Integer, Integer> count = new HashMap<>();
    for (int num : nums) {
        count.put(num, count.getOrDefault(num, 0) + 1);
    }
    
    // 2단계: Min Heap 생성 
    // [숫자, 빈도수]를 빈도수 기준으로 정렬
    // 빈도수 낮은 것이 루트에 위치 (Min Heap)
    PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]);
    //                                                        ↑
    //                                             a[1] - b[1]이면 빈도수 오름차순
    //                                             b[1] - a[1]이면 내림차순 (Max Heap)
    
    // 3단계: 각 (숫자, 빈도수) 쌍을 처리
    // 시간: O(n log k) - n개 요소, 힙 크기는 최대 k
    for (Map.Entry<Integer, Integer> entry : count.entrySet()) {
        int num = entry.getKey();
        int freq = entry.getValue();
        
        pq.offer(new int[]{num, freq});
        
        // 힙 크기가 k를 초과하면
        if (pq.size() > k) {
            pq.poll();  // 빈도수가 가장 낮은 것 제거!
        }
    }
    // 결과: 힙에는 빈도수 상위 K개만 남음
    
    // 4단계: 결과 추출
    // 시간: O(k log k)
    int[] result = new int[k];
    for (int i = 0; i < k; i++) {
        result[i] = pq.poll()[0];  // 숫자만 추출
    }
    return result;
}

 

 

 

 

패턴 2: 스트림에서 중간값 찾기

 

언제 사용?

  • 실시간으로 데이터가 들어올 때 중간값 추적
  • "지금까지 들어온 수들의 중간값은?"

핵심 아이디어:

  • 데이터를 절반으로 나눔
  • 작은 쪽 절반은 Max Heap에
  • 큰 쪽 절반은 Min Heap에
  • 두 힙의 루트를 보면 중간값 계산 가능
  • 작은 쪽 절반인 Max Heap의 갯수가 큰 쪽 절반인 Min Heap 갯수가 한개 더 많은경우 
    즉 , 전체 갯수가 홀수개인경우에는 Max Heap의 루트가 중간값이다.
    작은 쪽 절반인 Max Heap의 갯수가 큰 쪽 절반인 Min Heap 갯수와 같은경우 
    즉 , 전체 갯수가 짝수개인경우에는 두 루트의 평균값이 중간값이다.

    위의 로직처럼 동작하기 위해 
    그래서 Max Heap 갯수 >= Min Heap 갯수가 되게끔 유지하는 코드가 들어가있다.
class MedianFinder {
    // 작은 쪽 절반 - Max Heap (가장 큰 값이 루트)
    PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
    // 큰 쪽 절반 - Min Heap (가장 작은 값이 루트)
    PriorityQueue<Integer> minHeap = new PriorityQueue<>();
    
    /*
    불변 조건 (Invariant):
    1. maxHeap의 모든 값 ≤ minHeap의 모든 값
    2. maxHeap.size() == minHeap.size() 또는 maxHeap.size() == minHeap.size() + 1
    3. maxHeap의 루트 = 작은 쪽 최댓값, minHeap의 루트 = 큰 쪽 최솟값
    
    시각화:
    작은 쪽 [..... maxHeap.peek()] | [minHeap.peek() .....]  큰 쪽
                    ↑ 중간값 ↑
    */
    
    public void addNum(int num) {
        // 1단계: 새 숫자를 일단 maxHeap에 추가
        maxHeap.offer(num);
        
        // 2단계: maxHeap의 최댓값을 minHeap으로 이동
        // 목적: maxHeap의 모든 값이 minHeap의 모든 값보다 작게 유지
        minHeap.offer(maxHeap.poll());
        
        // 3단계: 크기 균형 맞추기
        // maxHeap이 항상 같거나 1개 많게 유지
        if (maxHeap.size() < minHeap.size()) {
            maxHeap.offer(minHeap.poll());
        }
    }
    
    public double findMedian() {
        // 홀수 개: maxHeap의 루트가 중간값
        if (maxHeap.size() > minHeap.size()) {
            return maxHeap.peek();
        }
        // 짝수 개: 두 루트의 평균이 중간값
        return (maxHeap.peek() + minHeap.peek()) / 2.0;
    }
}

/*
==================== 홀수 개 예시: 1, 2, 3 추가 ====================

addNum(1):
  1. maxHeap.offer(1) 
     → maxHeap = [1]
  
  2. minHeap.offer(maxHeap.poll())
     → maxHeap.poll() = 1
     → maxHeap = [], minHeap = [1]
  
  3. maxHeap.size(0) < minHeap.size(1) → 균형 맞추기
     → maxHeap.offer(minHeap.poll())
     → maxHeap = [1], minHeap = []
  
  상태: maxHeap=[1], minHeap=[]
  중간값 = maxHeap.peek() = 1 ✓

---

addNum(2):
  1. maxHeap.offer(2)
     → maxHeap = [2, 1] (Max Heap: 2가 루트)
  
  2. minHeap.offer(maxHeap.poll())
     → maxHeap.poll() = 2 (최댓값)
     → maxHeap = [1], minHeap = [2]
  
  3. maxHeap.size(1) == minHeap.size(1) → 균형 맞춤, skip
  
  상태: maxHeap=[1], minHeap=[2]
  시각화: [1] | [2]
  중간값 = (1 + 2) / 2.0 = 1.5 ✓

---

addNum(3):
  1. maxHeap.offer(3)
     → maxHeap = [3, 1] (Max Heap: 3이 루트)
  
  2. minHeap.offer(maxHeap.poll())
     → maxHeap.poll() = 3 (최댓값)
     → maxHeap = [1], minHeap = [2, 3] (Min Heap: 2가 루트)
  
  3. maxHeap.size(1) < minHeap.size(2) → 균형 맞추기
     → minHeap.poll() = 2 (최솟값)
     → maxHeap.offer(2)
     → maxHeap = [2, 1] (Max Heap: 2가 루트), minHeap = [3]
  
  상태: maxHeap=[2, 1], minHeap=[3]
  시각화: [1, 2] | [3]
           ↑ 2가 루트
  중간값 = maxHeap.peek() = 2 ✓
  
  실제 정렬: [1, 2, 3] → 중간값 2 맞음!

==================== 짝수 개 예시: 1, 2, 3, 4 추가 ====================

현재 상태: maxHeap=[2, 1], minHeap=[3]

addNum(4):
  1. maxHeap.offer(4)
     → maxHeap = [4, 2, 1] (Max Heap: 4가 루트)
  
  2. minHeap.offer(maxHeap.poll())
     → maxHeap.poll() = 4 (최댓값)
     → maxHeap = [2, 1], minHeap = [3, 4] (Min Heap: 3이 루트)
  
  3. maxHeap.size(2) == minHeap.size(2) → 균형 맞춤, skip
  
  상태: maxHeap=[2, 1], minHeap=[3, 4]
  시각화: [1, 2] | [3, 4]
           ↑ 2     ↑ 3
  중간값 = (2 + 3) / 2.0 = 2.5 ✓
  
  실제 정렬: [1, 2, 3, 4] → 중간값 (2+3)/2 = 2.5 맞음!

==================== 또 다른 짝수 예시: 5, 15, 1, 3 ====================

addNum(5):
  maxHeap=[5], minHeap=[]
  중간값 = 5

addNum(15):
  1. maxHeap.offer(15) → maxHeap=[15, 5]
  2. minHeap.offer(maxHeap.poll()) → maxHeap=[5], minHeap=[15]
  3. skip
  
  상태: maxHeap=[5], minHeap=[15]
  중간값 = (5 + 15) / 2.0 = 10.0 ✓
  
  실제 정렬: [5, 15] → 중간값 10 맞음!

addNum(1):
  1. maxHeap.offer(1) → maxHeap=[5, 1]
  2. minHeap.offer(maxHeap.poll()) → maxHeap=[1], minHeap=[5, 15]
  3. maxHeap.size(1) < minHeap.size(2)
     → maxHeap.offer(minHeap.poll())
     → maxHeap=[5, 1], minHeap=[15]
  
  상태: maxHeap=[5, 1], minHeap=[15]
  중간값 = 5 ✓
  
  실제 정렬: [1, 5, 15] → 중간값 5 맞음!

addNum(3):
  1. maxHeap.offer(3) → maxHeap=[5, 3, 1]
  2. minHeap.offer(maxHeap.poll()) → maxHeap=[3, 1], minHeap=[5, 15]
  3. skip
  
  상태: maxHeap=[3, 1], minHeap=[5, 15]
  중간값 = (3 + 5) / 2.0 = 4.0 ✓
  
  실제 정렬: [1, 3, 5, 15] → 중간값 (3+5)/2 = 4 맞음!

==================== 왜 이 방법이 동작하는가? ====================

핵심 원리:
1. 전체 데이터를 두 그룹으로 분할
   - 작은 쪽 절반 (maxHeap): 중간값보다 작거나 같은 값들
   - 큰 쪽 절반 (minHeap): 중간값보다 크거나 같은 값들

2. maxHeap은 Max Heap이므로:
   - 작은 쪽의 "최댓값"이 루트 = 작은 쪽 그룹의 대표값
   - 홀수 개일 때 이 값이 정확히 중간값

3. minHeap은 Min Heap이므로:
   - 큰 쪽의 "최솟값"이 루트 = 큰 쪽 그룹의 대표값
   - 짝수 개일 때 두 루트의 평균이 중간값

4. 항상 maxHeap.size() >= minHeap.size() 유지:
   - 홀수 개: maxHeap이 1개 더 많음 → maxHeap.peek()이 중간값
   - 짝수 개: 크기 같음 → 두 루트 평균이 중간값
*/

// 시간: addNum O(log n), findMedian O(1)
// 공간: O(n)

 


 

N진법

 

[Java] 10진수 <-> 2진수, 8진수, 16진수로 변환하기

10진수 -> 2진수, 8진수, 16진수로 변환하기 java.lang.Integer의 toBinaryString(), toOctalString(), toHexaString() 메소드를 이용하여 10진수를 2진수, 8진수, 16진수 문자열로 변환할 수 있습니다. 리턴 타입 클래스

hianna.tistory.com

위 블로그있는 방식이 더간단한 방법

 

아래방법은 반복문으로 구현하는방법 참고용

public class DecimalToBinary {
    public static String toBinary(int decimal) {
        if (decimal == 0) return "0";
        
        StringBuilder binary = new StringBuilder();
        int num = decimal;
        
        while (num > 0) {
            binary.append(num % 2);  // 나머지를 추가
            num = num / 2;            // 2로 나눈 몫
        }
        
        return binary.reverse().toString();  // 역순으로 뒤집기
    }
    
    public static void main(String[] args) {
        System.out.println(toBinary(10));   // 1010
        System.out.println(toBinary(25));   // 11001
        System.out.println(toBinary(255));  // 11111111
    }
}