19 분 소요

입력받고 출력하기

1. Scanner VS BufferedReader 선택 기준

Scanner

입력이 간단하고 데이터가 적을 때 사용 (10,000개 이하)

  • 다양한 자료형을 바로 입력받을 수 있음
  • 속도가 느림
Scanner sc = new Scanner(System.in);

int n = sc.nextInt();
double d = sc.nextrDouble();
String word = sc.next();        // 공백 전까지 문자열
String line = sc.nextLine();    // 한 줄 전체

BufferedReader

대량의 데이터(100,000 개 이상) 또는 시간 제한이 엄격한 경우 사용

  • 버퍼를 사용하기 때문에 매우 빠른 속도를 보여줌
  • 문자열로만 입력받기 때문에 파싱이 필요
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());   // 한 줄 읽고 정수 변환

// StringTokenizer와 함께 사용 (split보다 빠름)
StringTokenizer st = new StringTokenizer(br.readLine(), " ");
int a = Integer.parseInt(st.nextToken());
int b = Integer.parseInt(st.nextToken());

// 여러 줄 입력
String[] lines = new String[n];
for(int i = 0; i < n; i++) {
	lines[i] = br.readLine();
}

2. 2차원 배열 입력 패턴

공백으로 구분된 숫자

// 3 4
// 1 2 3 4
// 5 6 7 8
// 9 0 1 2
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
int[][] arr = new int[n][m];
for(int i = 0; i < n; i++) {
    for(int j = 0; j < m; j++) {
        arr[i][j] = sc.nextInt();
    }
}

// BufferedReader & StringTokenizer 사용
for(int i = 0; i < n; i++) {
    st = new StringTokenizer(br.readLine());
    for(int j = 0; j < m; j++) {
        arr[i][j] = Integer.parseInt(st.nextToken());
    }
}

붙어있는 숫자

// 3 4
// 1011
// 1001
// 0111
for(int i = 0; i < n; i++) {
    String line = sc.next();
    for(int j = 0; j < m; j++) {
        arr[i][j] = line.charAt(j) - '0';  // 문자를 숫자로 변환
    }
}

// BufferedReader 사용
for(int i = 0; i < n; i++) {
    String line = br.readLine();
    for(int j = 0; j < m; j++) {
        arr[i][j] = line.charAt(j) - '0';
    }
}

문자 배열

// ###.
// .#.#
// #...
char[][] board = new char[n][m];
for(int i = 0; i < n; i++) {
    board[i] = sc.next().toCharArray();
}

// BufferedReader 사용
char[][] board = new char[n][m];
for(int i = 0; i < n; i++) {
    board[i] = br.readLine().toCharArray();
}

3. 출력 최적화

System.out.println

출력이 적을 때 사용 (1,000개 이하)

  • 매 출력마다 flush 발생으로 느림

StringBuilder

대량 출력시(10,000개 이상) 사용

  • System.out.println 보다 약 10배 빠름
StringBuilder sb = new StringBuilder();
for(int i = 0; i < n; i++) {
	sb.append(result[i]).append('\n');
}
System.out.print(sb);

BufferedWriter

매우 많은 출력이 필요한 경우 사용

  • StringBuilder 보다 약간 더 빠름
  • flush()close() 필수
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
for(int i = 0; i < n; i++) {
	bw.write(String.valueOf(result[i]));
	bw.newLine();n
}
bw.flush();
bw.close();

자료구조

1. 배열과 리스트

배열 (Array)

크기 고정, 인덱스로 빠른 접근 가능

  • 접근 : O(1), 삽입/삭제 : O(n)
// 1차원 배열 선언과 초기화
int[] arr = new int[10];                    // 크기 10, 0으로 초기화
int[] arr2 = {1, 2, 3, 4, 5};               // 직접 초기화
int[] arr3 = new int[]{1, 2, 3, 4, 5};      // new 키워드 사용

// 배열 채우기
Arrays.fill(arr, -1);                       // 모든 원소를 -1로
Arrays.fill(arr, 2, 5, 100);                // 인덱스 2~4를 100으로

// 2차원 배열
int[][] matrix = new int[3][4];             // 3행 4열
int[][] matrix2 = {{1,2,3}, {4,5,6}};       // 직접 초기화

// 2차원 배열 특정 값으로 초기화
for(int[] row : matrix) {
    Arrays.fill(row, -1);
}

// 가변 배열 (각 행의 크기가 다름)
int[][] jagged = new int[3][];
jagged[0] = new int[2];
jagged[1] = new int[3];
jagged[2] = new int[1];

ArrayList

동적 크기 조정이 가능하며, 내부적으로는 배열을 사용

// 선언과 초기화
List<Integer> list = new ArrayList<>();

// 기본 연산
list.add(10);                     // 끝에 추가 O(1)
list.add(0, 5);                   // 0번 인덱스에 삽입 O(n)
list.set(0, 3);                   // 0번 인덱스 값 변경 O(1)
list.get(0);                      // 0번 인덱스 값 가져오기 O(1)
list.remove(0);                   // 0번 인덱스 제거 O(1)
list.remove(Integer.valueOf(10)); // 값 10 제거
list.clear();                     // 모두 제거

// 상태 확인
list.size();                      // 크기
list.isEmpty();                   // 비어있는지 확인
list.contains(10);                // 포함 여부 확인 O(n)

// 초기값 설정
List<Integer> list2 = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));

// 크기 지정 및 초기화
List<Integer> list3 = new ArrayList<>(Collections.nCopies(10, 0)); // 10개의 0

// 2차원 ArrayList (그래프 인접 리스트에서 자주 사용)
List<List<Integer>> graph = new ArrayList<>();
for(int i = 0; i < n; i++) {
    graph.add(new ArrayList<>());
}

// 값 추가
graph.get(0).add(1);  // 0번 노드에 1번 노드 연결
graph.get(0).add(2);  // 0번 노드에 2번 노드 연결

2. Stack과 Queue

Stack (LIFO - 후입선출)

Stack<Integer> stack = new Stack<>();

// 주요 메서드
stack.push(1);                    // 삽입 O(1)
int top = stack.pop();            // 제거하며 반환 O(1)
int peek = stack.peek();          // 제거하지 않고 반환 O(1)
boolean empty = stack.isEmpty();
int size = stack.size();
int pos = stack.search(1);        // 1의 위치

// Deque를 Stack으로 사용 (Stack보다 더 빨라서 권장)
Deque<Integer> stack2 = new ArrayDeque<>();
stack2.push(1);           // addFirst()와 동일
int top2 = stack2.pop();  // removeFirst()와 동일

Queue (FIFO - 선입선출)

Queue<Integer> queue = new LinkedList<>();

// 주요 메서드
queue.offer(1);              // 삽입 (add보다 안전) O(1)
int front = queue.poll();    // 제거하며 반환 O(1)
int peek = queue.peek();     // 제거하지 않고 반환 O(1)
int size = queue.size();
boolean empty = queue.isEmpty();

Deque (Double-ended Queue)

양쪽 끝에서 삽입/삭제가 가능하며, ArrayDeque 구현체를 권장한다. (LinkedList 보다 빠름)

Deque<Integer> deque = new ArrayDeque<>();

// 앞쪽 조작
deque.addFirst(1);               // 맨 앞에 추가
deque.removeFirst();             // 맨 앞 제거
deque.peekFirst();               // 맨 앞 확인

// 뒤쪽 조작
deque.addLast(2);                // 맨 뒤에 추가
deque.removeLast();              // 맨 뒤 제거
deque.peekLast();                // 맨 뒤 확인

// poll은 null 반환, remove는 예외 발생
int first = deque.pollFirst();   // 앞에서 제거, 없으면 null
int last = deque.pollLast();     // 뒤에서 제거, 없으면 null

3. 우선순위 큐 (Priority Queue / Heap)

우선순위가 높은 원소부터 제거

  • 내부 구현 : 이진 힙 (완전 이진 트리)
  • 삽입 /삭제 : O(log n), 최솟값 확인 : O(1)
// 최소힙 (기본)
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.offer(3);
minHeap.offer(1);
minHeap.offer(2);
System.out.println(minHeap.poll());   // 1 출력

// 최대힙
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());

// 커스텀 정렬 - Comparable 인터페이스 구현
class Node implements Comparable<Node> {
	int x, y, cost;
		
	Node(int x, int y, int cost) {
        this.x = x;
        this.y = y;
        this.cost = cost;
    }
		
    @Override
    public int compareTo(Node o) {
        /*
        *  음수 : this가 우선순위 높음
        *  양수 : o 가 우선순위 높음
        *  0 : 같음
        */
        if(this.cost == o.cost) {
                return this.x - o.x;   // cost가 같다면 x를 기준으로 오름차순
        }
        return this.cost - o.cost; // cost 기준으로 오름차순
    }
}

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

// 커스텀 정렬 - Comparator 사용
PriorityQueue<int[]> pq2 = new PriorityQueue<>(new Comparator<int[]>() {
    @Override
    public int compare(int[] a, int[] b) {
            return a[0] - b[0];
    }
});

// 주요 메서드
pq.offer(1);             // 추가 O(log n)
pq.poll();               // 최솟값 제거 및 반환 O(log n)
pq.peek();               // 최솟값 확인 O(1)
pq.size();              
pq.isEmpty();
pq.clear();              // 모두 제거 O(n)
pq.contains(2);          // 포함 여부 확인 O(n)

4. Map과 Set

HashMap

Key-Value 쌍, 순서를 보장하지 않으며 null 허용

Map<String, Integer> map = new HashMap<>();

// 기본 연산
map.put("apple", 1);                       // 추가/수정 O(1)
map.get("apple");                          // 값 가져오기, 없으면 null O(1)
map.getOrDefault("orange", 0);             // 값 가져오기, 없으면 기본값 O(1)
map.remove("apple");                       // 제거 O(1)
map.size();
map.clear();
map.isEmpty();

// 유용한 메서드
map.putIfAbsent("orange", 2);              // 키가 없을 때만 추가
map.compute("apple", (k, v) -> v + 1);     // 값 계산

// 확인
map.containsKey("apple");                  // 키 존재 확인 O(1)
map.containsValue(1);                      // 값 존재 확인 O(n)

// 순회
for(Map.Entity<String, Integer> entry : map.entrySet()) {
    String key = entry.getKey();
    int value = entry.getValue();
}

// Key만 순회
for(String key : map.keySet()) { ... }

// Value만 순회
for(int value : map.values()) { ... }

TreeMap

Red-Black Tree, Key 기준으로 정렬하며, null 키는 불가능하다.

모든 연산 O(log n)

Map<String, Integer> treeMap = new TreeMap<>();

// Key를 기준으로 자동 정렬
treeMap.put("banana", 2);
treeMap.put("apple", 1);
treeMap.put("cherry", 3);

// 순회시 : apple, banana, cherry 순서

Map<Integer. String> treeMap = new TreeMap<>();
treeMap.firstKey();      // 가장 작은 키
treeMap.lastKey();       // 가장 큰 키
treeMap.higherKey(3);    // 3보다 큰 첫 키
treeMap.lowerKey(3)      // 3보다 작은 마지막 키

HashSet

중복을 허용하지 않으며, 순서도 보장하지 않는다.

  • 평균 O(1), 최악의 경우 O(n)
Set<Integer> set = new HashSet<>();

// 기본 연산
set.add(1);                 // 추가 O(1)
set.add(1);                 // 무시됨
set.contains(1);            // 포함 여부 확인 O(1)        
set.remove(1);              // 제거 O(1)
set.size();
set.clear();
set.isEmpty();

// Set 연산
Set<Integer> set1 = new HashSet<>(Arrays.asList(1, 2, 3));
Set<Integer> set2 = new HashSet<>(Arrays.asList(2, 3, 4));

// 합집합
set1.addAll(set2);           // {1, 2, 3, 4}

// 교집합
set1.retainAll(set2);        // {2, 3}

// 차집합
set1.removeAll(set2);        // {1}

TreeSet

Red-Black Tree, 정렬된 Set

  • 모든 연산 O(log n)
Set<Integer> treeSet = new TreeSet<>();

treeSet.add(3);
treeSet.add(1);
treeSet.add(2);

treeSet.first();    // 가장 작은 원소, 1
treeSet.last();     // 가장 큰 원소, 3
treeSet.higher(2);  // 2보다 큰 첫 원소, 3
treeSet.lower(2);   // 2보다 작은 마지막 원소, 1
treeSet.ceiling(2); // 2 이상인 첫 원소, 2
treeSet.floor(2);   // 2 이하인 마지막 원소, 2

문자열 처리

1. 문자열 기본 조작

String 은 불변(immutable) 객체이므로 문자열 열결 시 새로운 객체를 생성한다.

StringBuilder 사용 권장

String str = "Hello World";

// 길이와 접근
int len = str.length();
char ch = str.charAt(0);  // 'H'

// 부분 문자열
String sub1 = str.subString(6);        // "World" (6번부터 끝까지)
String sub2 = str.subString(0, 5);     // "Hello" (0번부터 4번까지)

// 검색
int idx = str.indexOf("World");             // 6 (첫 위치)
int notFound = str.indexOf("xyz");          // -1 (없으면 -1)
int lastIdx = str.lastIndexOf('l');         // 9 (첫 'l' 위치)

// 확인
boolean contains = str.contains("Hello");
boolean starts = str.startsWith("Hello");
boolean ends = str.endsWith("World");
boolean empty = str.isEmpty();

// 변환
String upper = str.toUpperCase();        // "HELLO WORLD"
String lower = str.toLowerCase();        // "hello world"
String trimmed = "    hello    ".trim(); // "hello" (앞뒤 공백 제거)

// 치환
String replaced = str.replace("World", "Java");    // "Hello Java"
String replacedAll = str.replaceAll("[0-9]", "");  // 정규 표현식 활용

2. 문자열 분리와 결합

// split
String data = "apple,banana,orange";
String[] fruits = data.split(",");           // 콤마로 분리
String[] words = "hello world".split(" ");   // 공백으로 분리
String[] chars = "abc".split("");            // 각 문자로 분리 ["a", "b", "c"]

// 정규식 split
String[] numbers = "1-2-3--4".split("-+");   // 연속된 - 처리 (정규표현식) ["1", "2", "3", "4"]
String[] onlyChar = "a1b2c3".split("\\d");   // ["a", "b", "c"]

// join - 문자열 결합 (Java 8+)
String joined = String.join("-", "a", "b", "c");            // "a-b-c"
String joined2 = String.join("", Arrays.asList("a", "b"));  // "ab"
String joined3 = String.join(", ", fruits);                 // "apple, banana, orange"

// StringBuilder (문자열 연결 최적화)
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(" ");
sb.append("World");

sb.insert(0, "Say ");     // 맨 앞에 삽입 "Say Hello World"
sb.delete(0, 4);          // 0~3 인덱스 삭제 "Hello World"
sb.reverse();             // 뒤집기
String result = sb.toString();

// StringBuilder 체이닝
String result2 = new StringBuilder()
    .append("Hello")
    .append(" ")
    .reverse()
    .toString();

3. 문자와 숫자 변환

// 문자 <-> 숫자
char c = '5';
int num = c - '0';             // 5 (문자를 숫자로)
char c2 = (char)(num + '0');   // '5' (숫자를 문자로)

// 문자 <-> 아스키코드
char ch = 'A';
int ascii = (int)ch;         // 65
char fromAscii = (char)65;   // 'A'

// 문자열 -> 숫자
String str = "123";
int intVal = Integer.parseInt(str);              // 123
long longVal = Long.parseLong(str);              // 123L
double doubleVal = Double.parseDouble("3.14");

// 숫자 -> 문자열
int num = 123;
String str = String.valueOf(num);             // "123" (권장)
String str2 = Integer.toString(num);
String str3 = "" + num;

// 진법 변환
String binary = Integer.toBinaryString(10);    // "1010"
String octal = Integer.toOctalString(10);      // "12"
String hex = Integer.toHexString(10);          // "a"

int fromBinary = Integer.parseInt("1010", 2);  // 10
int fromHex = Integer.parseInt("FF", 16);      // 255

4. 문자 판별과 처리

char ch = 'A';

// 판별
boolean isLetter = Character.isLetter(ch);          // true (알파벳 여부 확인)
boolean isDigit = Character.isDigit('5');           // true (숫자 여부 확인)
boolean isUpperCase = Character.isUpperCase(ch);    // true (대문자 여부 확인)
boolean isWhitespace = Character.isWhitespace(' '); // true

// 대소문자 변환
char lower = Character.toLowerCase(ch);   // 'a'
char upper = Character.toUpperCase('a');  // 'A'

// char[] <-> String
String str = "hello";
char[] chars = str.toCharArray();             // ['h', 'e', 'l', 'l', 'o']
String fromChars = new String(chars);         // "hello"
String fromChars2 = String.valueOf(chars);    // "hello"

5. 정규표현식

String text = "abc123DEF456";

// 치환
text.replaceAll("[0-9]", "");         // "abcDEF" (숫자 제거)
text.replaceAll("[^0-9]", "");        // "123456" (숫자만 남김)
text.replaceAll("[a-z]", "");         // "123DEF456" (소문자 제거)
text.replaceAll("[^a-zA-Z]", "");     // "abcDEF" (알파벳만 남김)
text.replaceAll("\\s", "");           // 공백 제거
text.replaceAll("[^a-z0-9-_.]", "");  // 특수문자 제거

// 연속된 문자 처리
"aabbcc".replaceAll("(.)\\1+", "$1");  // "abc" (연속 문자 제거)
"a...b..c.".replaceAll("\\.+", ".");   // "a.b.c." (연속 마침표 제거)

// 앞뒤 특정 문자 제거
"..hello..".replaceAll("^\\.|\\.$", "");   // ".hello." (앞뒤 마침표 하나씩)
"..hello..".replaceAll("^\\.+|\\.+$", ""); // "hello" (앞뒤 모든 마침표)

// 패턴 확인
boolean isNumber = "123".matches("\\d+");                // true
boolean isAlpha = "abc".matches("[a-zA-Z]+");            // true
boolean isEmail = "test@test.com".matches(".*@.*\\..*"); // true

배열 다루기

1. 배열 복사

1차원 배열 깊은 복사

int[] original = {1, 2, 3, 4, 5};

// clone()
int[] copy1 = original.clone();

// Arrays.copyOf() - 크기 조정 가능
int[] copy2 = Arrays.copyOf(original, original.length);    // 같은 크기
int[] copy3 = Arrays.copyOf(original, 10);                 // 크기 늘리기, 남은 공간은 0으로 채움
int[] copy4 = Arrays.copyOf(original, 3);                  // 크기 줄이기, [1, 2, 3]

// Arrays.copyOfRange() - 범위 지정
int[] copy4 = Arrays.copyOfRange(original, 1, 4);          // [2, 3, 4]

// System.arraycopy(원본, 원본시작, 대상, 대상시작, 개수) - 가장 빠름
int[] copy5 = new int[original.length];
System.arraycopy(original, 0, copy5, 0, original.length);

2차원 배열 깊은 복사

int[][] matrix = {{1, 2}, {3, 4}, {5, 6}};

// 각 행을 clone 하는 방법
int[][] deepCopy = new int[matrix.length][];
for(int i = 0; i < matrix.length; i++) {
		deepCopy[i] = matrix[i].clone();
}

// 2중 for문 사용
int[][] deepCopy2 = new int[matrix.legnth][matrix[0].length];
for(int i = 0; i < matrix.legnth; i++) {
    for(int j = 0; j < matrix[0].length; j++){
        deepCopy2[i][j] = matrix[i][j];
    }
}

2. 컬렉션 변환

List ↔ Array

// List -> Array
List<Integer> list = Arrays.asList(1, 2, 3);

Integer[] arr = list.toArray(new Integer[0]);

// Array -> List
Integer[] array = {1, 2, 3, 4, 5};

List<Integer> list2 = Arrays.asList(array);                  // 고정 크기 (수정 불가)
List<Integer> list3 = new ArrayList<>(Arrays.asList(array)); // 수정 가능

int[] ↔ List

Integer[] intArr = {1, 2, 3};
List<Integer> intList = new ArrayList<>();
for(int num : intArr) {
    intList.add(num);
}

// List<Integer> -> int[]
List<Integer> intList2 = Arrays.asList(1, 2, 3);
int[] intArr2 = new int[intList2.size()];
for(int i = 0; i < intList2.size(); i++) {
    intArr2[i] = intList2.get(i);
}

Set ↔ List

// List -> Set (중복 제거)
List<Integer> listWithDup = Arrays.asList(1,2,2,3,3,3);
Set<Integer> set = new HashSet<>(listWithDup);          // {1, 2, 3}

// Set -> List
Set<Integer> set2 = new HashSet<>(Arrays.asList(1,2,3));
List<Integer> listFromSet = new ArrayList<>(set2);

정렬과 탐색

1. 정렬

1차원 배열 정렬

int[] arr = {3, 1, 4, 1, 5};

// 기본 정렬
Arrays.sort(arr);           // 전체 오름차순
Arrays.sort(arr, 1, 4);     // 부분 정렬 (1~3 인덱스)

// 내림차순 (Integer 배열만 가능)
Integer[] arrInteger = {3, 1, 4, 1, 5};
Arrays.sort(arrInteger, Collections.reverseOrder());

2차원 배열 정렬

int[][] points = {{3,5}, {1,2}, {4,1}, {1,3}};

// 첫 번째 원소 기준 오름차순, 같은 경우 두 번째 원소 기준
Arrays.sort(points, new Comparator<int[]>() {
    @Override
    public int compare(int[] a, int[] b) {
        if(a[0] == b[0]) return a[1] - b[1];    // 첫 번째 원소가 같은 경우 두 번째 기준 오름차순
        return a[0] - b[0];				
    }
});

// 결과 : [[1,2], [1,3], [3,5], [4,1]]

리스트 정렬

List<Integer> list = new ArrayList<>(Arrays.asList(3,1,4,1,5));

Collections.sort(list);                                // 오름차순
Collections.sort(list, Collections.reverseOrder());    // 내림차순

// 객체 리스트 정렬
class Person {
    String name;
    int age;
		
    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

List<Person> people = new ArrayList<>();

// 나이순, 같은 경우 이름순 정렬
Collections.sort(people, new Comparator<Person>() {
    @Override
    public int compare(Person a, Person b) {
        if(a.age == b.age) {
            return a.name.compareTo(b.name);  // 문자열 비교
        }
        return a.age - b.age;                 // 나이 기준 오름차순
        // return b.age - a.age;              // 내림차순
    }
});

문자열 정렬

String[] words = {"banana", "apple", "cherry"};
Arrays.sort(words);                              // 사전순 정렬

// 길이순 정렬
Arrays.sort(words, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        if(a.length() == b.length()) {
            return a.compareTo(b);              // 길이가 같은 경우 사전순 정렬
        }
        return a.length() - b.length();         // 길이순 정렬
    }
});

정렬된 배열/리스트에서 사용가능하며, O(log n)의 시간복잡도를 가짐

배열과 리스트에서 이진 탐색

// 배열에서 이진 탐색
int[] sorted = {1, 3, 5, 7, 9, 11};

int index = Arrays.binarySearch(sorted, 7);       // 3 반환
int notFound = Arrays.binarySearch(sorted, 6);    // -4 (음수 = 해당 값이 없다는 것을 의미)
// 음수 반환값 : -(삽입 위치 + 1), -4 -> 해당 값은 없지만 3번 인덱스에 삽입하면 됨.

// 리스트에서 이진 탐색
List<Integer> list = Arrays.asList(1, 3, 5, 7, 9);
int idx = Collections.binarySearch(list, 5);         // 2 반환

기본 이진 탐색

public int binarySearch(int[] arr, int target, int left, int right) {
    while(left <= right) {
        int mid = left + (right - left) / 2;  // 오버플로우 방지
            
        if(arr[mid] == target) {
            return mid;
        } else if(arr[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return -1;  // 찾지 못한 경우
}

Lower Bound & Upper Bound

// Lower Bound (target 이상인 첫 위치)
public int lowerBound(int[] arr, int target) {
    int left = 0, right = arr.length;
		
    while(left < right) {
        int mid = left + (right - left) / 2;
        if(arr[mid] < target) {
            left = mid + 1;
        } else {
            right = mid;
        }
    }
    return left;
}
// [1, 2, 2, 2, 3, 4] 에서 2의 lowerBound = 1

// Upper Bound (target 초과인 첫 위치)
public int upperBound(int[] arr, int target) {
    int left = 0, right = arr.length;
		
    while(left < right) {
        int mid = left + (right - left) / 2;
        if(arr[mid] <= target) {
            left = mid + 1;
        } else {
            right = mid;
        }
    }
    return left;
}
// [1, 2, 2, 2, 3, 4] 에서 2의 upperBound = 4

// target의 개수 구하기
int count = upperBound(arr, target) - lowerBound(arr, target);

수학과 비트 연산

1. 기본 수학 연산

// 절댓값, 최대/최소
int abs = Math.abs(-5);           // 5
int max = Math.max(10, 20);       // 20
int min = Math.min(10, 20);       // 10

// 거듭제곱, 제곱근
double pow = Math.pow(2, 10);     // 1024.0 (2^10)
double sqrt = Math.sqrt(16);      // 4.0

// 올림, 내림, 반올림
double ceil = Math.ceil(4.2);     // 5.0
double floor = Math.floor(4.8);   // 4.0
long round = Math.round(4.6);     // 5

// 로그
double log = Math.log(Math.E);    // 1.0 (자연로그)
double log10 = Math.log10(100);   // 2.0 (상용로그)

2. 최대공약수와 최소공배수

최대공약수 (Greatest Common Divisor)

// 유클리드 호제법
public int gcd(int a, int b) {
    return b == 0 ? a : gcd(b, a % b);
}

// 반복문 버전
public int gcdIterative(int a, int b) {
    while (b != 0) {
        int temp = b;
        b = a % b;
        a = temp;
    }
    return a;
}

// 여러 수의 GCD
public int gcdMultiple(int[] arr) {
    int result = arr[0];
    for(int i = 1; i < arr.length; i++) {
        result = gcd(result, arr[i]);
    }
    return result;
}

최소공배수 (Least Common Multiple)

public int lcm(int a, int b) {
    return a / gcd(a, b) * b; // 오버플로우 방지 (a * b 대신)
}

// 여러 수의 LCM
public int lcmMultiple(int[] arr) {
    int result = arr[0];
    for(int i = 1; i < arr.length; i++) {
        result = lcm(result, arr[i]);
    }
    return result;
}

3. 소수 판별과 에라토스테네스의 체

단일 소수 판별 O(√n)

public boolean isPrime(int n) {
    if(n < 2) return false;
		
    for(int i = 2; i * i <= n; i++) {
        if(n % i == 0) return false;
    }
    return true;
}

에라토스테네스의 체

범위 내 모든 소수 구하기

  • 시간 복잡도 : O(n log log n)
  • 공간 복잡도 : O(n)
public boolean[] sieveOfEratosthenes(int n) {
    boolean[] isPrime = new boolean[n + 1];
    Arrays.fill(isPrime, true);
    isPrime[0] = isPrime[1] = false;
		
    for(int i = 2; i * i <= n; i++) {
        if(isPrime[i]) {
            // i의 배수 지우기 (i*i부터 시작)
            for(int j = i * i; j <= n; j += i) {
                isPrime[j] = false;
            }
        }
    }
    return isPrime;
}

소인수분해

public List<Integer> primeFactorization(int n) {
    List<Integer> factors = new ArrayList<>();
		
    for(int i = 2; i * i <= n; i++) {
        while(n % i == 0) {
            factors.add(i);
            n /= i;
        }
    }
    if(n > 1) factors.add(n);
		
    return factors;
}

4. 비트 연산

기본 비트 연산

int a = 5;         // 0101
int b = 3;         // 0011

int and = a & b;   // 0001 = 1 (AND)
int or = a | b;    // 0111 = 7 (OR)
int xor = a ^ b;   // 0110 = 6 (XOR)
int not = ~a;      // 1111 .... 1010 = -6 (NOT)

시프트 연산

int left = a << 1;            // 1010 = 10 (왼쪽 시프트, *2)
int right = a >> 1;           // 0010 = 2 (오른쪽 시프트, /2)
int unsignedRight = a >>> 1;  // 부호 없는 오른쪽 시프트

비트 마스킹

int num = 0b1010;                // 10

// n번째 비트 켜기 (0부터 시작)
int setBit = num | (1 << n);            // n=2인 경우 1110

// n번째 비트 끄기
int clearBit = num & ~(1 << n);         // n=3인 경우 0110

// n번째 비트 토글
int toggleBit = num ^ (1 << n);         // n=1인 경우 0100

// n번째 비트 확인
boolean isSet = (num & (1 << n)) != 0;

boolean isPowerOfTwo = n > 0 && (n & (n - 1)) == 0;  // 2의 거듭제곱
int clearRightmost = n & (n - 1);                    // 가장 오른쪽 1을 0으로
int isolateRightmost = n & -n;                       // 가장 오른쪽 1만 남기기

부분집합 생성

int[] elements = {1, 2, 3};
int n = elements.length;

for(int mask = 0; mask < (1 << n); mask++) {
    System.out.print("{ ");
    for(int i = 0; i < n; i++) {
        if((mask & (1 << i)) != 0) {
            System.out.print(elements[i] + " ");
        }
    }
    System.out.println("}");
}

// 출력 : { }, { 1 }, { 2 }, { 1 2 }, { 3 }, { 1 3 }, { 2 3 }, { 1 2 3 }

순열과 조합

1. 순열 (Permutation)

순서가 있는 선택, nPr = n! / (n-r)!

순열

public void permutation(int[] arr, boolean[] visited, int[] output, int depth, int n, int r) {
    if(depth == r) {
        // output 배열에 순열 하나가 완성
        for(int i = 0; i < r; i++) {
            System.out.println(output[i] + " ");
        }
        System.out.println();
        return;
    }
		
    for(int i = 0; i < n; i++) {
        if(!visited[i]) [
            visited[i] = true;
            output[depth] = arr[i];
            permutation(arr, visited, output, depth + 1, n, r);
            visited[i] = false;
        }
    }
}

// 사용
int[] arr = {1, 2, 3};
boolean[] visited = new boolean[3];
int[] output = new int[2];
permutation(arr, visited, output, 0, 3, 2);

중복 순열

public void permutationWithRep(int[] arr, int[] output, int depth, int n, int r) {
    if(depth == r) {
        for(int i = 0; i < r; i++) {
            System.out.println(output[i] + " ");
        }
        System.out.println();
        return;
    }
		
    for(int i = 0; i < n; i++) {
        output[depth] = arr[i];
        permutationWithRep(arr, output, depth + 1, n, r);
    }
}

2. 조합 (Combination)

순서가 없는 선택, nCr = n! / (r! * (n-r)!)

조합

public void combination(int[] arr, int[] output, int start, int depth, int n, int r) {
    if(depth == r) {
        for(int i = 0; i < r; i++) {
            System.out.println(output[i] + " ");
        }
        System.out.println();
        return;
    }
		
    for(int i = start; i < n; i++) {
        output[depth] = arr[i];
        combination(arr, output, i + 1, depth + 1, n, r);
    }
}

중복 조합

public void combinationWithRep(int[] arr, int[] output, int start, int depth, int n, int r) {
    if(depth == r) {
        for(int i = 0; i < r; i++) {
            System.out.println(output[i] + " ");
        }
        System.out.println();
        return;
    }
		
    for(int i = start; i < n; i++) {
        output[depth] = arr[i];
        combinationWithRep(arr, output, i, depth + 1, n, r); // i+1이 아닌 i
    }
}

조합 수 계산

public long combination(int n, int r) {
    if(r > n - r) r = n - r;   // nCr = nC(n-r) 최적화
		
    long result = 1;
    for(int i = 0; i < r; i++) {
        result *= (n - i);
        result /= (i + 1);
    }
    return result;
}

댓글남기기