✏️ Coding Test

[자바 코테 준비] 2. 자주 나오는 알고리즘 유형, 코드 정리

date
Aug 26, 2023
slug
coding-test-practice-2
author
status
Public
category
✏️ Coding Test
tags
summary
코테에 자주 나오는 유형 대비!
type
Post
thumbnail
 

✔️ 입력 범위로 시간복잡도 파악하기

1초=1억=10^8
입력 범위
시간복잡도
알고리즘
10
O(N!)
크기가 N인 순열
20
O(2^N)
크기가 N인 집합의 부분집합
500
O(N^3)
3중for, 플루이드
1만(10,000)
O(N^2)
2중for
500만(5,000,000)
O(NlogN)
이분탐색, 퀵정렬, 힙정렬
약 1억(=10^8)
O(N)
1중for
2^10^8
O(logN)
이분탐색
  • O(N) : 약 1억 = 10^8
  • O(N^2) : 약 1만 = 10,000
  • O(N^3) : 약 500
  • O(2^N) : 약 20
  • O(N!) : 10
 
 

✔️ 코딩 테스트에 자주 나오는 유형

기본

  • 구현(시뮬레이션)
  • 완전탐색
  • DFS/BFS
  • 백트래킹
  • 그리디
  • DP
  • 정렬
  • Queue, Stack
  • HashMap, HashSet, HashTable
  • Priority Queue
  • 이분탐색
  • 최단 경로(BFS, 다익스트라)
  • 문자열
  • 투포인터
  • 슬라이딩 윈도우
  • 누적합, 부분합

심화

  • 최단 경로(벨만포드-음의 간선, 플로이드워샬-음의 간선,모든노드~모든노드)
  • 그래프 이론(union-find(서로소 집합), MST-프림/크루스칼, 위상정렬)
  • 세그먼트 트리
  • 트라이
 
 

✔️ 알고리즘 유형 별 특징 및 접근 방법

1. 구현(시뮬레이션)

  • 인풋의 범위가 크지 않은 경우가 많음
  • 뭔가 문제가 복잡하고 은근히 꼼꼼하고 더럽게 자세함
  • 예외, 엣지케이스 처리가 어렵기 때문에, 수도코드를 잡고 나서 빠르게 구현 후 테케 돌리면서 엣지케이스 찾고 조금씩 완성도 높이기

2. DFS / BFS

  • 애들이 자꾸 퍼지려(치즈문제처럼) 하거나, 혹은 자꾸 어디를 찾아가려 함
  • 한 단계씩 방법을 거치면서 최종적으로 내가 원하는 방법에 도달할 수 있는지에 대한 문제도 있음

3. DP

  • 그냥 구현하려고 했을 때, 똑같은 행위를 계속해서 해야 하는 로직이 보임
  • 뭔가 본인만의 규칙을 가지고 최소값이나 최댓값을 구하려고 함
  • N번째 값을 알면 다음 값을 알 수 있을 것 같은 느낌이 들 때..
  • N번째 값을 알기 위해선 몇 번째 값들을 알아야 할까?를 생각하고 재귀적으로 값을 구하도록 재귀호출
  • 마지막 행위를 지워보고, 나머지 값들이 어디서 왔을지 찾아보기!
  • 재귀로 풀면 top-down, 반복문으로 풀면 bottom-up
  • 타일채우기, RBG거리

4. 그리디

  • 매번 최선의 선택만 수행해도 문제를 해결할 수 있다면 그리디
  • 기준이 명시되어 있음(가장 많은 회의 배정, 가장 적은 화폐 사용 등)
  • 거스름돈 문제(화폐가 서로소가 아니어야 함→서로소라면 DP로 해결), 회의실 배정

5. 이분탐색

  • 일단 인풋이 크면 1순위로 생각해보기
  • 효율성이 중요할 것 같으면 1순위로 생각해보기
  • 정렬이 자동으로 되어있거나 필요하면 2순위 정도로 생각해보기
  • 최대, 최소값 문제로 나오는 경우도 있음
  • 큰 틀은 left, right를 내가 지정할 수 있는지 가능 여부 먼저 본 후, mid 값으로 무언가 판단을 할 수 있어야 함

6. 최단경로

  • BFS : 간선의 가중치가 없거나 모두 같을 때 사용
  • 다익스트라 : 특정 노드 → 모든 노드 // 필요: 우선순위큐, visited[][], distance[][], O(ElogV)
  • 벨만포드 : 특정 노드 → 모든 노드 // 음의 간선 가능, 매 반복마다 모든 간선 확인하므로 visited 필요X
  • 플로이드 워셜 : 모든 노드 → 모든 노드 // DIS_ab = min(DIS_ab, DIS_ak + DIS_kb), O(N^3)

7. 큐, 스택, 우선순위큐, 해시맵

  • 큐 : 선입선출
  • 스택 : 후입선출
  • 우선순위큐 : 들어오는 순서와 상관없이 가중치에 따라 선출, 자바는 우선순위큐 min힙
  • 해시맵,셋 : key-value 형태로 저장해두고 싶을 때

8. 브루트포스

  • 인풋이 매우 작음!!
  • 사실상 구현과 다름 없음
  • BFS, DFS, 백트래킹도 여기에 해당
  • 기본적으로 브루트포스 먼저 생각해보고, 가능하다면 최적화 or 다른 알고리즘으로 가기

9. 분할정복

  • 보드가 나오고 뭔가 프렉탈 같은 게 나오면 1순위로 생각해보기

10. 투포인터, 슬라이딩 윈도우

  • 시간복잡도 개선이 필요할 때 생각해보기
  • 정렬도 같이 서브로 묶어서 나올 수 있고, 뭔가 탐색은 해야하는데 인풋이 클 때..
  • 배열에서 인덱스 2개를 사용해야 할 때 투포인터+while문으로 한 번에 해결
  • 구간합
    • 두 수의 차이가 주어진 값보다 작으면 왼쪽 포인터++ 등의 로직

11. 유니온 파인드 Union-Find

  • 서로소 집합 찾기
  • 노드를 집합에 속하게 하는 Union 연산, 노드의 루트 노드를 찾는 Find 연산
  • 노드들을 루트 노드를 기준으로 하는 어떠한 집합으로 묶는다고 생각하기

12. 위상정렬

  • 그래프 노드 간 우선순위가 정해져 있을 경우(ex. 강의A를 듣기 위해 선수강 해야 하는 강의B..)
  • 그래프의 사이클 유무를 판별할 수 있음
  • 각 노드의 현재 indegree(차수)를 저장해야 함

13. 비트마스킹

  • 문제에서 0, 1로 표현할 수 있는 경우
  • 길이 32~64까지만 기억할 수 있음(int형 32자리, long형 64자리)
  • 방문처리 : bitmask |= 1<<n // n번째 자리에 1 넣기
  • 방문확인 : bitmask &= 1<<n // n번째 자리가 1인지 확인
 
 

✔️ 자주 쓰이는 알고리즘 코드

완전탐색

순열
visited 필요, 방문 처리 → 순열호출 → 방문 취소
static void perm(int cnt, int[] out, int[] arr, boolean[] visited, int length) { if(cnt == length) { // 순열 생성 완료 // 완료 시 해야 할 일 return; } // 순열 생성 for (int i = 0; i < arr.length; i++) { if(!visited[i]) { // 방문한 적 없을 때만 visited[i] = true; out[cnt] = arr[i]; perm(cnt+1, out, arr, visited, length); visited[i] = false; } } } // n개 중 r개를 나열하는 순열 perm(int depth, int r){ 종료조건(depth==r) for(i=0~n) { if(방문 안 했음) 방문처리 넣기(depth) 다음칸 호출 안넣기 현재칸 호출 } }
중복순열
visited 필요 없음
static void perm(int cnt, int[] out, int[] arr, int length) { if(cnt == length) { // 순열 생성 완료 // 완료 시 해야 할 일 return; } // 중복순열 생성 for (int i = 0; i < arr.length; i++) { out[cnt] = arr[i]; perm(cnt+1, out, arr, length); } } }
조합
visited 필요, 방문 처리 → 조합호출 → 방문 취소
순열과 다른 점: 현재 원소보다 뒤에 있는 원소에 대해서만 탐색을 진행(탐색 시작 index 필요)
visited == 뽑은 원소
static void comb(int idx, int cnt, int[] arr, boolean[] visited, int length) { if(cnt == length) { // 순열 생성 완료 // 완료 시 해야 할 일 return; } // 조합 생성 for (int i = idx; i < arr.length; i++) { if(!visited[i]) { // 방문한 적 없을 때만 visited[i] = true; comb(idx+1, cnt+1, arr, visited, length); visited[i] = false; } } } // n개 중 r개 뽑는 조합 comb(){ 종료조건(depth==r) for(뽑은개수~n) { 넣기(depth)& 방문처리 다음칸 호출 방문처리 취소 }
중복조합
visited 필요, 방문 처리 → 순열호출 → 방문 취소
static void comb(int idx, int cnt, int[] arr, boolean[] visited, int length) { if(cnt == length) { // 순열 생성 완료 // 완료 시 해야 할 일 return; } // 중복조합 생성 for (int i = idx; i < arr.length; i++) { visited[i] = true; comb(idx+1, cnt+1, arr, visited, length); visited[i] = false; } }
BFS
DFS
백트래킹

그래프

트리의 지름
1. DFS를 통해 임의의 정점(x)으로부터 가장 먼 정점(y)을 구한다. 2. DFS를 통해 구해진 (y)정점으로부터 가장 먼 정점(z)를 구한다. 3. (y) 정점과 (z) 정점을 잇는 경로가 트리의 지름이 된다.
다익스트라
1. 출발 노드를 설정 2. 출발 노드를 기준으로 각 노드의 최소 비용을 저장 3. 방문하지 않은 노드 중에서 가장 비용이 적은 노드를 선택 4. 해당 노드를 거쳐서 특정한 노드로 가는 경우를 고려하여 최소 비용을 갱신 5. 3~4번을 반복하여 최소 비용 배열 갱신
벨만포드
1. 출발 노드를 설정 2. 최단 거리 테이블을 초기화 3. 다음의 과정을 (V-1)번 반복 3-1. 모든 간선 E개를 하나씩 확인 3-2. 각 간선을 거쳐 다른 노드로 가는 비용을 계산하여 최단 거리 테이블 갱신 3-3. 만약 음수 간선 순환이 발생하는지 체크하고 싶다면 3번 과정을 마지막에 한 번 더 수행(V번째)하여 최단 거리 테이블이 갱신된다면 음수 간선 순환이 존재하는 것
플로이드 워셜
1. 노드를 설정 2. 모든 구간마다 해당 노드를 거쳐가는 경우의 최소 비용 갱신 3. 모든 노드에 대해 2번 반복
프림
1. 임의의 정점 V을 비어있는 트리 T에 포함시킨다 2. T에 있는 노드와 T에 없는 노드 사이의 간선 중 가중치가 최소인 간선을 찾는다 2-1. 이때 사이클을 막기 위해, 연결된 정점이 이미 트리에 있다면 그 다음 순서의 간선을 찾는다 3. 찾은 간선이 연결하는 두 개의 노드 중 T에 없던 노드와 그 간선을 T에 포함시킨다 4. 모든 노드가 T에 포함될 때까지 = (n-1)개의 간선을 가질 때까지 2~3 반복
크루스칼
1. 그래프의 간선들을 가중치의 **오름차순으로 정렬(비용 낮은 순)** 2. 정렬된 간선 리스트에서 순서대로 사이클을 형성하지 않는 간선을 선택 2-1. 즉, 현재 선택된 간선들과 사이클을 형성하지 않으면서 비용이 가장 낮은 가중치의 간선을 선택 3. 해당 간선을 MST의 집합에 추가 4. 선택된 간선의 개수가 (n-1)개가 될 때까지 2~3 반복
위상정렬
1. 진입차수가 0인 정점을 큐에 삽입 2. 큐에서 정점을 꺼내 해당 정점과 연결된 모든 간선을 제거 2-1. 큐에서 정점을 꺼낼 때 따로 기록해둘것(이 순서가 정렬의 결과) 2-2. 해당 간선의 끝에 있는 정점들의 차수 1씩 차감 3. 간선 제거 후 진입차수가 0이 된 정점을 큐에 삽입 4. 큐가 빌 때까지 2~3번 과정을 반복 4-1. 이때, 모든 원소를 방문하기 전에 큐가 비게 된다면 **사이클이 존재한다는 뜻** 5. 모든 원소를 방문했다면 **큐에서 꺼낸 순서가 위상 정렬의 결과**
Union-Find, Disjoint Set

DP

LIS(최장증가부분수열)
LCS(최대공통부분수열)

문자열

KMP

수학, 기하

페르마 소정리
에라토스테네스의 체(소수 판별)
유클리드 호제법(최대공약수)
Convex Hull(볼록 껍질)
다각형 내부의 점 판정

자료구조

세그먼트 트리
import java.util.Arrays; public class SegmentTreeImpl { // Segment Tree 클래스 static class SegmentTree { int[] indexTree; // index tree int[] data; // 실제 저장되어있는 데이터 int leafCount, height; // leafCount: 실제 데이터 저장 노드 수(최하단, N), height: 트리 높이(=ceil(logN)) // 트리 height에 의해 인덱스 트리의 전체 노드의 수가 결정된다. // totalN = 2^(h+1)-1, leafNode 시작점: 2^h SegmentTree() { } SegmentTree(int[] arr){ // arr 에 해당하는 index tree 초기 생성 this.data = arr; // data 저장 this.leafCount = arr.length; // 실제 data 길이 this.height = (int)Math.ceil(Math.log(this.leafCount)/Math.log(2)); // 트리 높이 this.indexTree = new int[(int)Math.pow(2, height+1)+1]; // index tree 생성 // +1 하는 이유: 0 인덱스 삽입하여 계산을 용이하게 하기 위하여(실제로는 1번 인덱스부터 사용할 것임) makeIndexTree(); } @Override public String toString() { return "SegmentTree{" + "nodes=" + Arrays.toString(indexTree) + '}'; } // index tree 초기 생성 O(N) void makeIndexTree(){ makeSubTree(1, 1, leafCount); // 루트 노드(1)부터 시작, Top-down } private void makeSubTree(int treeIdx, int left, int right) { // left, right: 현재 노드가 커버하는 인덱스 범위 if(left==right) { // 리프노드 도착, 값 넣기 this.indexTree[treeIdx] = this.data[left-1]; return; } // 리프노드 도착하지 않은 경우 왼쪽, 오른쪽으로 내려가기(중위순회) int mid = (left+right)/2; makeSubTree(treeIdx*2, left, mid); // 왼쪽 서브 트리 makeSubTree(treeIdx*2+1, mid+1, right); // 오른쪽 서브 트리 // 위의 코드를 통해 왼쪽, 오른쪽 서브 트리가 모두 만들어졌다 // 왼쪽, 오른쪽 서브 트리의 부모 노드 각각 더하기 this.indexTree[treeIdx] = this.indexTree[treeIdx*2] + this.indexTree[treeIdx*2+1]; } // index tree 중간 인덱스 값 갱신 O(logN) public void update(int updateIdx, int updateValue) { int diff = updateValue-data[updateIdx-1]; updateSubTree(1, 1, leafCount, updateIdx, diff); data[updateIdx-1] = updateValue; } private void updateSubTree(int treeIdx, int left, int right, int updateIdx, int diff) { if(left <= updateIdx && updateIdx <= right) { this.indexTree[treeIdx] += diff; if(left==right) return; } int mid = (left+right)/2; if(updateIdx <= mid) { updateSubTree(treeIdx*2, left, mid, updateIdx, diff); } else { updateSubTree(treeIdx*2+1, mid+1, right, updateIdx, diff); } } // 구간합 구하기 O(logN) public int getPartialSum(int left, int right) { return getSubPartialSum(1, 1, leafCount, left, right); } private int getSubPartialSum(int treeIdx, int left, int right, int tleft, int tright) { if(left > tright || right < tleft) { return 0; } if(tleft <= left && right <= tright) { // 쏙 들어가면 더이상 내려가지 말고 리턴 return this.indexTree[treeIdx]; } int mid = (left+right)/2; return getSubPartialSum(treeIdx*2, left, mid,tleft,tright)+getSubPartialSum(treeIdx*2+1, mid+1, right,tleft,tright); } } }
트라이
class Trie { TrieNode root; public Trie() { this.root = new TrieNode(); } public void insert(String word) { // 1. root에서 시작 TrieNode node = root; // 2. 단어의 모든 알파벳에 대하여 for (int i = 0; i < word.length(); i++) { char c = word.charAt(i); // 트라이 현재 노드의 자식 중 만약 해당 문자가 있다면 그대로 넘어가고, 없다면 생성 if (node.children[c - 'a'] == null) { node.children[c - 'a'] = new TrieNode(); } node = node.children[c - 'a']; } // 3. 단어의 끝임을 표시 node.isEndOfWord = true; } public boolean search(String word) { // 1. root에서 시작 TrieNode node = root; // 2. 단어의 모든 알파벳에 대하여 for (int i = 0; i < word.length(); i++) { char c = word.charAt(i); // 트라이 현재 노드의 자식 중 만약 해당 문자가 있다면 그대로 넘어가고, 없다면 false if (node.children[c - 'a'] == null) { return false; } node = node.children[c - 'a']; } // 3. 마지막 노드가 EndOfWord인지 확인하여 true / false 리턴 return node.isEndOfWord; } // 특정 단어 지우기 public void delete(String word) { delete(this.root, word, 0); // 최초로 delete 던지는 부분 } private void delete(TrieNode thisNode, String word, int index) { char c = word.charAt(index); TrieNode childNode = thisNode.children[c - 'a']; // 아예 없는 단어인 경우(자식을 찾아 내려갈 수 없는 경우) 에러 출력 if (childNode == null) throw new Error("There is no [" + word + "] in this Trie."); index++; // 마지막 노드(삭제를 시작하는 첫 노드)에 도달했을 때 if (index == word.length()) { // 삭제조건 2번 항목 확인 // EndOfWord가 아닌 경우(트라이가 가지고 있는 단어가 아닌 경우) 에러 출력 if (!childNode.isEndOfWord) throw new Error("There is no [" + word + "] in this Trie."); // 삭제조건 1번 항목 확인 // 자식 노드가 없으면(이 단어를 포함하는 더 긴 단어가 없으면) 노드 삭제 if (!childNode.hasChild()) { thisNode.children[c] = null; } else { // 자식 노드가 있다면 이 노드가 단어의 마지막이었다는 것만 false로 변경 childNode.isEndOfWord = false; } } // 아직 마지막 노드가 아니라면 else { // 콜백 함수 delete(childNode, word, index); // 삭제조건 1,3번 항목 // 삭제 중, 자식 노드가 없고 현재 노드로 끝나는 다른 단어가 없는 경우 이 노드 삭제 if (!childNode.isEndOfWord && !childNode.hasChild()) thisNode.children[c] = null; } } } class TrieNode { TrieNode children[]; boolean isEndOfWord; public TrieNode(){ this.children = new TrieNode[26]; // 알파벳 개수 this.isEndOfWord = false; } public boolean containsKey(char ch) { if (children[ch - 'A'] != null) return true; // 해당 알파벳을 자식 노드로 가지고 있다면 true return false; } public TrieNode get(char ch) { return children[ch - 'A']; // 해당 알파벳 자식 노드 return, 만일 없다면 null } public void setChildren(char ch, TrieNode node) { children[ch - 'A'] = node; } public void setEndOfWord() { this.isEndOfWord = true; } public boolean hasChild() { for (int i = 0; i < 26; i++) { if(children[i] != null) { return true; } } return false; } }

기타

투포인터(부분합)
슬라이딩 윈도우
비트마스킹
int visited; // 방문 여부 확인 : AND if(visited & 1<<i == 0) // 인덱스 i를 방문한 적이 있는지 확인 // 방문 여부 기록 : OR visited |= 1<<i // 인덱스 i를 방문했다!!
 
 

 
참고