코딩 인터뷰를 준비하는 것은 개발자가 새로운 자료를 검토하고 배우는 데 몇 주를 소비하는 경우가 많기 때문에 정말 어려운 일이 될 수 있습니다.
사실, 대부분의 개발자는 코딩 인터뷰에 대한 준비가 완전히 되어 있지 않다고 생각합니다. 개발자가 LeetCode에서 문제를 해결하는 데 몇 주를 소비하고도 여전히 준비가 되지 않았다고 느끼는 것은 드문 일이 아닙니다. 분명히 이 접근법에는 문제가 있습니다.
전통적인 코딩 인터뷰 준비와 관련된 몇 가지 문제를 자세히 살펴보겠습니다.
전통적인 인터뷰 준비에서 가장 흔한 함정 중 하나는 "연마" 관행입니다. 이 접근 방식에는 명확한 계획이나 전략 없이 가능한 한 많은 LeetCode 문제를 해결하는 것이 포함됩니다. 이는 생산적인 전략처럼 보일 수 있지만 몇 가지 단점이 있습니다.
체계적인 접근 방식 없이 문제를 해결하면 자신의 약점을 인식하지 못할 수도 있습니다. 연구에 대한 의도적인 계획은 없습니다. 목표는 단순히 가능한 한 많은 문제를 해결하는 것입니다. 결과적으로, 많은 것을 배웠다고 느낄 수도 있지만, 지식에 상당한 격차가 있을 수 있습니다.
더욱이 이것의 문제는 본질적으로 다양한 문제에 대한 해결책을 암기하는 것과 관련이 있다는 것입니다. 수백 가지 코딩 문제에 대한 해결책을 기억하려는 시도는 면접관이 이전에 접했던 것과 조금이라도 다른 질문을 제시할 때 효과적이지 않습니다. 이로 인해 다양한 문제를 처리할 준비가 되어 있지 않다는 느낌이 들 수 있습니다.
이 전략의 마지막 문제는 장기적으로 더 많은 스트레스와 두통을 유발한다는 것입니다. 직업을 바꾸고 싶을 때마다 몇 주간 똑같은 벼락치기 공부를 해야 한다면, 매번 어려움을 겪게 될 것입니다. 당신은 이전과 같은 문제를 다시 배우고 해결하는 데 몇 주를 보낼 것입니다.
이러한 과제를 감안할 때 더 나은 방법이 있어야 합니다.
그렇다면 코딩 인터뷰 준비에 더 효과적이고 지속 가능한 접근 방식이 있을까요? 그 답은 코딩 패턴을 이해하고 활용하는 데 있습니다.
저는 코딩 인터뷰를 준비할 때 문제의 기본 패턴을 파악하는 것을 우선시합니다. Two-Pointers , Sliding Window , Modified Binary Search , Topological Sort 등과 같은 기술을 포함하는 이러한 패턴은 광범위한 코딩 문제를 해결하기 위한 다양하고 강력한 프레임워크를 제공합니다. 해결책을 암기하는 것에서 검증된 문제 해결 기술로 강조점이 바뀌었습니다.
코딩 패턴에 집중하면 인터뷰 준비를 대폭 간소화하여 더욱 효율적으로 만들 수 있습니다.
기억하세요. 더 열심히 준비하는 것이 아니라 더 현명하게 준비하세요.
이번 글에서는 제가 정리한
이 기사에서 많은 내용을 다루고 있지만 더 심층적인 교육, 설명 및 코딩 연습을 원한다면 Techinterviews.io에서 종합 코딩 인터뷰 준비 과정을 확인하세요 . 우리는 또한 데이터 구조 , 행동 인터뷰 , 심지어 급여 협상 과 같은 중요한 주제도 다룹니다.
이제 이러한 코딩 패턴을 살펴보겠습니다.
연결 목록 반전에는 연결 목록의 포인터 방향을 변경하여 요소의 순서를 바꾸는 작업이 포함됩니다. 이는 데이터 구조의 기본 작업이며 종종 노드 참조를 주의 깊게 조작해야 합니다.
이는 연결된 목록을 처리할 때 유용하며 제약 조건은 이를 제자리에서 반전시키는 것입니다.
연결된 목록을 제자리에 되돌리는 프로세스는 다음과 같습니다.
세 가지 변수( current , Previous 및 next ) 를 정의하는 것부터 시작하세요. 연결된 리스트의 선두를 현재로 설정하고, 이전과 다음을 None으로 초기화합니다.
현재 변수가 None 이 아닌 동안 다음을 수행하십시오.
마지막으로, 역방향 목록의 헤드를 도달한 마지막 노드로 설정합니다. 이는 이전 변수에 저장됩니다.
주요 지표:
템플릿 코드:
파이썬
def reverse_linked_list(head): curr = head prev = None while curr: next_node = curr.next curr.next = prev prev = curr curr = next_node return prev
JS
function reverseLinkedList(head) { let curr = head; let prev = null; while (curr) { const nextNode = curr.next; curr.next = prev; prev = curr; curr = nextNode; } return prev; }
자바
public ListNode reverseLinkedList(ListNode head) { ListNode curr = head; ListNode prev = null; while (curr != null) { ListNode nextNode = curr.next; curr.next = prev; prev = curr; curr = nextNode; } return prev; }
수정된 이진 검색은 다양한 문제를 해결하기 위해 고전적인 이진 검색 알고리즘을 적용합니다. 변형에는 요소의 첫 번째/마지막 발생 찾기 또는 회전된 배열 검색이 포함됩니다. 중간점과 조건을 주의 깊게 처리해야 합니다.
정렬된 배열, 연결 목록 또는 행렬이 제공되면 수정된 이진 검색을 사용해 보세요.
정렬된 데이터 구조에 이 패턴을 적용하는 방법은 다음과 같습니다.
middle = start + (end - start) / 2
같이 중간을 계산하는 것이 더 안전합니다.
주요 지표:
템플릿 코드:
파이썬
def binary_search(arr: List[int], target: int) -> int: left, right = 0, len(arr) - 1 first_true_index = -1 # Perform binary search until left and right pointers meet while left <= right: mid = (left + right) // 2 if feasible(mid): # If the condition is true at mid index, update first_true_index first_true_index = mid right = mid - 1 else: # If the condition is false at mid index, narrow down the search space left = mid + 1 return first_true_index
JS
function binarySearch(arr, target) { let left = 0; let right = arr.length - 1; let firstTrueIndex = -1; // Perform binary search until left and right pointers meet while (left <= right) { const mid = Math.floor((left + right) / 2); if (feasible(mid)) { // If the condition is true at mid index, update firstTrueIndex firstTrueIndex = mid; right = mid - 1; } else { // If the condition is false at mid index, narrow down the search space left = mid + 1; } } return firstTrueIndex; }
자바
public int binarySearch(int[] arr, int target) { int left = 0; int right = arr.length - 1; int firstTrueIndex = -1; // Perform binary search until left and right pointers meet while (left <= right) { int mid = left + (right - left) / 2; if (feasible(mid)) { // If the condition is true at mid index, update firstTrueIndex firstTrueIndex = mid; right = mid - 1; } else { // If the condition is false at mid index, narrow down the search space left = mid + 1; } } return firstTrueIndex; }
두 포인터 기술에는 쌍이나 하위 배열과 관련된 문제에 자주 사용되는 데이터 구조(일반적으로 배열 또는 연결 목록)를 순회하는 두 개의 포인터를 유지하는 작업이 포함됩니다. 서로 다른 위치의 요소를 비교해야 하는 문제에 최적화됩니다.
이 기술의 장점은 단순성과 효율성에 있습니다. 특히 처음에 하나의 포인터만 사용할 수 있는 배열이나 문자열과 같은 선형 데이터 구조를 처리할 때 더욱 그렇습니다. 두 개의 포인터를 사용하면 중복 작업을 방지하고 런타임 효율성을 크게 향상시켜 잠재적으로 O(n^2) 에서 O(n) 으로 줄일 수 있습니다.
"두 포인터" 패턴은 각각 특정 시나리오에 맞게 조정된 여러 변형을 포함합니다. 이러한 변형에는 동일한 방향 , 반대 방향 및 "빠른 및 느린" 이라는 고유한 방법이 포함됩니다. 이 방법은 데이터 구조를 통해 서로 다른 속도로 이동하는 두 개의 포인터를 포함하는 "거북이와 토끼" 기술이라고도 하며 특히 감지에 유용합니다. 사이클.
여러 포인터를 사용하여 데이터 구조를 탐색하는 경우 접근 방식을 "두 포인터" 패턴을 따르는 것으로 분류할 수 있습니다.
주요 지표:
템플릿 코드:
"반대 방향" 변형을 위한 템플릿
파이썬
def two_pointers_opposite(arr): left = 0 right = len(arr) - 1 ans = 0 while left < right: # Perform logic using the left and right pointers if CONDITION: left += 1 else: right -= 1 return ans
JS
function twoPointersOpposite(arr) { let left = 0; let right = arr.length - 1; let ans = 0; while (left < right) { // Perform logic using the left and right pointers if (CONDITION) { left++; } else { right--; } } return ans; }
자바
public int twoPointersOpposite(int[] arr) { int left = 0; int right = arr.length - 1; int ans = 0; while (left < right) { // Perform logic using the left and right pointers if (CONDITION) { left++; } else { right--; } } return ans; }
슬라이딩 윈도우 기술에는 배열, 문자열 또는 연결된 목록과 같은 선형 데이터 구조에 대한 동적 윈도우를 유지하는 작업이 포함됩니다. 창의 크기는 구체적인 구현에 따라 달라질 수 있으며, 고정된 값으로 설정할 수도 있습니다. 이 창의 주요 목적은 특정 기준을 충족하는 데이터를 지속적으로 모니터링하고 캡처하여 하위 배열 또는 하위 문자열 문제를 효율적으로 해결하는 데 특히 유용하게 만드는 것입니다.
이 패턴은 창 내에서 개별 데이터를 쉽게 추적하기 위해 해시 맵을 활용하는 경우가 많습니다. 그러나 이 접근 방식은 O(n) 의 시공간 복잡성을 초래할 수 있다는 점에 유의하는 것이 중요합니다.
주요 지표:
템플릿 코드:
파이썬
def slidingWindow(nums): # Initialize necessary variables left = 0 window = [] # Initialize the window ans = 0 # Initialize the answer variable for right in range(len(nums)): window.append(nums[right]) # Expand the window to the right while invalid(window): # Condition to shrink the window from the left until it is valid again window.pop(0) # Remove the left element from the window left += 1 ans = max(ans, len(window)) # Update the answer, can vary on your implementation return ans
JS
function slidingWindow(nums) { let left = 0; const window = []; // Initialize the window let ans = 0; // Initialize the answer variable for (let right = 0; right < nums.length; right++) { window.push(nums[right]); // Expand the window to the right while (invalid(window)) { // Condition to shrink the window from the left until it is valid again window.shift(); // Remove the left element from the window left++; } ans = Math.max(ans, window.length); // Update the answer , can vary on your implementation } return ans; }
자바
public static int slidingWindow(int[] nums) { int left = 0; List<Integer> window = new ArrayList<>(); // Initialize the window int ans = 0; // Initialize the answer variable for (int right = 0; right < nums.length; right++) { window.add(nums[right]); // Expand the window to the right while (invalid(window)) { // Condition to shrink the window from the left until it is valid again window.remove(0); // Remove the left element from the window left++; } ans = Math.max(ans, window.size()); // Update the answer , can vary on your implementation } return ans; }
이 패턴에는 컬렉션에서 K개의 가장 크거나 가장 작은 요소를 찾는 작업이 포함되며, 종종 힙이나 우선 순위 큐와 같은 데이터 구조를 사용하여 구현됩니다. 값이나 빈도를 기준으로 요소의 하위 집합을 선택하는 데 유용합니다.
주어진 데이터 세트 내에서 가장 빈번하거나, 가장 작거나, 상위 'K' 요소를 찾으라는 요청을 받을 때마다 이 패턴 사용을 고려할 수 있습니다.
작동 방식은 간단합니다.
이 접근 방식의 장점은 효율성에 있습니다. 힙 자체가 자연스럽게 필요한 순서를 유지하므로 정렬 알고리즘에 의존할 필요가 없습니다.
주요 지표:
템플릿 코드:
파이썬
import heapq def top_k_elements(arr, k): heap = [] for num in arr: # Define your own criteria and logic to push elements onto the heap # For example, if you want to find the top k largest elements, push (-num, num) onto the heap heapq.heappush(heap, (-CRITERIA, num)) if len(heap) > k: heapq.heappop(heap) return [num for _, num in heap]
JS
function topKElements(arr, k) { const minHeap = []; for (const num of arr) { // Define your own criteria and logic to push elements onto the heap // For example, if you want to find the top k smallest elements, push num onto the heap minHeap.push(CRITERIA); if (minHeap.length > k) { minHeap.sort((a, b) => a - b).shift(); } } return minHeap.sort((a, b) => a - b); }
자바
import java.util.*; public List<Integer> topKElements(int[] arr, int k) { PriorityQueue<Integer> heap = new PriorityQueue<>(Comparator.comparingInt(a -> -CRITERIA)); for (int num : arr) { // Define your own criteria and logic to push elements onto the heap // For example, if you want to find the top k largest elements, push -num onto the heap heap.offer(-CRITERIA); if (heap.size() > k) { heap.poll(); } } List<Integer> topK = new ArrayList<>(); while (!heap.isEmpty()) { topK.add(-heap.poll()); } Collections.reverse(topK); return topK; }
두 개의 힙은 두 개의 힙(최대 힙과 최소 힙)을 활용하여 데이터 세트에서 중앙값 찾기와 같은 특정 문제를 최적화합니다. 이 패턴은 균형 잡힌 구조를 유지하는 데 특히 유용합니다.
작동 방식은 다음과 같습니다.
주요 지표:
템플릿 코드:
파이썬
import heapq class TwoHeaps: def __init__(self): self.min_heap = [] # Right heap to store larger half self.max_heap = [] # Left heap to store smaller half def add_num(self, num): if not self.max_heap or num <= -self.max_heap[0]: heapq.heappush(self.max_heap, -num) else: heapq.heappush(self.min_heap, num) # Balance the heaps if necessary if len(self.max_heap) > len(self.min_heap) + 1: heapq.heappush(self.min_heap, -heapq.heappop(self.max_heap)) elif len(self.min_heap) > len(self.max_heap): heapq.heappush(self.max_heap, -heapq.heappop(self.min_heap)) def find_median(self): if len(self.max_heap) == len(self.min_heap): return (-self.max_heap[0] + self.min_heap[0]) / 2.0 else: return -self.max_heap[0] # Usage: two_heaps = TwoHeaps() two_heaps.add_num(1) two_heaps.add_num(2) median = two_heaps.find_median() print("Median:", median)
JS
class TwoHeaps { constructor() { this.minHeap = []; // Right heap to store larger half this.maxHeap = []; // Left heap to store smaller half } addNumber(num) { if (this.maxHeap.length === 0 || num <= -this.maxHeap[0]) { this.maxHeap.push(-num); } else { this.minHeap.push(num); } // Balance the heaps if necessary if (this.maxHeap.length > this.minHeap.length + 1) { this.minHeap.push(-this.maxHeap.shift()); } else if (this.minHeap.length > this.maxHeap.length) { this.maxHeap.push(-this.minHeap.shift()); } } findMedian() { if (this.maxHeap.length === this.minHeap.length) { return (-this.maxHeap[0] + this.minHeap[0]) / 2; } else { return -this.maxHeap[0]; } } } // Usage: const twoHeaps = new TwoHeaps(); twoHeaps.addNumber(1); twoHeaps.addNumber(2); const median = twoHeaps.findMedian(); console.log("Median:", median);
자바
import java.util.*; class TwoHeaps { private PriorityQueue<Integer> minHeap; // Right heap to store larger half private PriorityQueue<Integer> maxHeap; // Left heap to store smaller half public TwoHeaps() { minHeap = new PriorityQueue<>(); maxHeap = new PriorityQueue<>(Collections.reverseOrder()); } public void addNumber(int num) { if (maxHeap.isEmpty() || num <= -maxHeap.peek()) { maxHeap.offer(-num); } else { minHeap.offer(num); } // Balance the heaps if necessary if (maxHeap.size() > minHeap.size() + 1) { minHeap.offer(-maxHeap.poll()); } else if (minHeap.size() > maxHeap.size()) { maxHeap.offer(-minHeap.poll()); } } public double findMedian() { if (maxHeap.size() == minHeap.size()) { return (-maxHeap.peek() + minHeap.peek()) / 2.0; } else { return -maxHeap.peek(); } } } // Usage: TwoHeaps twoHeaps = new TwoHeaps(); twoHeaps.addNumber(1); twoHeaps.addNumber(2); double median = twoHeaps.findMedian(); System.out.println("Median: " + median);
단조적(Monotonic) - (함수나 양의) 변화가 전혀 줄어들지 않거나 증가하지 않는 방식으로 변하는 것입니다.
단조 스택은 비증가 또는 비감소 순서로 요소 스택을 유지하며 배열에서 가장 가까운 작은/큰 요소를 찾는 데 자주 사용됩니다. 특정 문제를 최적화하기 위한 강력한 도구입니다.
순서는 엄격합니다. 스택의 맨 위에 있는 요소보다 작거나 큰 요소를 만날 때마다 추가하려는 요소가 가장 작은(또는 가장 큰) 요소가 될 때까지 단조 스택에서 팝해야 합니다. 그들을.
주요 지표:
템플릿 코드:
파이썬
def monotonic_stack(items): stack = [] for item in items: # Adjust the condition for comparisons to suit your needs while stack and stack[-1] <= item: stack.pop() # Do something with the popped item here stack.append(item)
JS
function monotonicStack(items) { const stack = []; for (const item of items) { // Adjust the condition for comparisons to suit your needs while (stack.length > 0 && stack[stack.length - 1] <= item) { stack.pop(); // Do something with the popped item here } stack.push(item); } return stack; }
자바
import java.util.*; public static int[] monotonicStack(int[] items) { Deque<Integer> stack = new ArrayDeque<>(); for (int item : items) { // Adjust the condition for comparisons to suit your needs while (!stack.isEmpty() && stack.peekLast() <= item) { stack.pollLast(); // Do something with the popped item here } stack.offerLast(item); } int[] result = new int[stack.size()]; int i = stack.size() - 1; while (!stack.isEmpty()) { result[i--] = stack.pollLast(); } return result; }
DFS 또는 깊이 우선 검색은 역 추적하기 전에 분기를 따라 최대한 깊이 탐색하는 순회 방법입니다. 이는 트리 및 그래프와 관련된 문제, 특히 순회 및 검색 작업에 널리 적용됩니다.
트리에서 DFS를 수행하려면 루트에서 시작해야 합니다. 그런 다음 아래 단계를 따르십시오.
그래프에서 DFS와의 차이점: 트리 DFS와 그래프 DFS의 주요 차이점은 주기가 있다는 것입니다. 트리에는 정의상 순환이 없으므로 트리 DFS는 각 노드가 정확히 한 번만 방문하도록 보장하고 전체 트리를 탐색할 때 자연스럽게 종료됩니다. 대조적으로, 그래프 DFS는 그래프 내의 순환 구조를 처리하기 위해 추가 조치를 통합해야 합니다. 한 주기에서 노드를 무기한 재방문하는 것을 방지하려면 그래프 DFS에는 방문한 노드를 표시 하고 역추적을 적절하게 처리하는 것과 같은 메커니즘이 필요합니다. 이러한 차이로 인해 사이클이 있을 때 무한 루프가 발생할 가능성이 있기 때문에 그래프 DFS가 트리 DFS보다 더 복잡해집니다.
주요 지표:
템플릿 코드:
파이썬
def dfs(root, target): if root is None: # Base case: Check if the current node is None return None if root.val == target: # Base case: Check if the current node value matches the target return root left = dfs(root.left, target) # Recursively search the left subtree if left is not None: # If the target is found in the left subtree, return the result return left return dfs(root.right, target) # Recursively search the right subtree
JS
function dfs(root, target) { if (root === null) { // Base case: Check if the current node is null return null; } if (root.val === target) { // Base case: Check if the current node value matches the target return root; } let left = dfs(root.left, target); // Recursively search the left subtree if (left !== null) { // If the target is found in the left subtree, return the result return left; } return dfs(root.right, target); // Recursively search the right subtree }
자바
public TreeNode dfs(TreeNode root, int target) { if (root == null) { // Base case: Check if the current node is null return null; } if (root.val == target) { // Base case: Check if the current node value matches the target return root; } TreeNode left = dfs(root.left, target); // Recursively search the left subtree if (left != null) { // If the target is found in the left subtree, return the result return left; } return dfs(root.right, target); // Recursively search the right subtree }
BFS는 다음 레벨로 이동하기 전에 현재 깊이의 모든 노드를 탐색하는 트리 및 그래프의 순회 기술입니다.
트리에서 BFS를 수행하려면 루트에서 시작해야 합니다. 그런 다음 아래 단계를 따르십시오.
방문할 노드를 추적하기 위해 빈 대기열 데이터 구조를 초기화합니다.
루트 노드를 대기열에 넣습니다 .
대기열이 빌 때까지 루프를 입력합니다.
대기열이 빌 때까지 3a-3c단계를 반복합니다 .
BFS 순회는 트리의 모든 노드를 왼쪽에서 오른쪽으로 수준별 방식으로 방문하면 완료됩니다.
본질적으로 트리의 BFS는 루트에서 시작하여 다음 레벨로 진행하기 전에 자식 노드로 이동하여 레벨별로 노드를 탐색합니다. 이렇게 하면 다음 레벨로 이동하기 전에 각 레벨의 노드를 방문할 수 있으므로 비가중 트리에서 최단 경로를 찾거나 레벨별로 트리 레벨을 탐색하는 등의 작업에 특히 유용합니다.
그래프에서 BFS와의 차이점: DFS와 유사하게 Graph BFS는 노드 간 주기 및 다중 경로의 존재에 적응합니다. 방문 노드 표시 및 특수 종료 조건과 같은 메커니즘을 사용하여 그래프 내의 복잡한 관계 네트워크를 효율적으로 탐색합니다.
주요 지표:
템플릿 코드:
파이썬
from collections import deque def bfs(root): # Create a queue and initialize it with the root node queue = deque([root]) # Perform breadth-first search until the queue is empty while len(queue) > 0: # Dequeue the front node from the queue node = queue.popleft() # Process the current node for child in node.children: if is_goal(child): # If the goal condition is satisfied, return the child node return FOUND(child) # Enqueue the child node to explore it in the next iterations queue.append(child) # Return NOT_FOUND if the goal is not reached return NOT_FOUND
JS
function bfs(root) { const queue = []; // Create a queue and initialize it with the root node queue.push(root); while (queue.length > 0) { // Perform breadth-first search until the queue is empty const node = queue.shift(); // Dequeue the front node from the queue for (const child of node.children) { // Process the current node if (isGoal(child)) { // If the goal condition is satisfied, return the child node return `FOUND ${child}`; } queue.push(child); // Enqueue the child node to explore it in the next iterations } } return 'NOT_FOUND'; // Return NOT_FOUND if the goal is not reached }
자바
import java.util.LinkedList; import java.util.Queue; public String bfs(Node root) { Queue<Node> queue = new LinkedList<>(); // Create a queue and initialize it with the root node queue.offer(root); while (!queue.isEmpty()) { // Perform breadth-first search until the queue is empty Node node = queue.poll(); // Dequeue the front node from the queue for (Node child : node.getChildren()) { // Process the current node if (isGoal(child)) { // If the goal condition is satisfied, return the child node return "FOUND(child)"; } queue.offer(child); // Enqueue the child node to explore it in the next iterations } } return "NOT_FOUND"; // Return NOT_FOUND if the goal is not reached }
DSU(Disjoint Set Union) 라고도 알려진 Union-Find 데이터 구조는 서로소 집합 또는 연결된 구성 요소와 관련된 문제를 효율적으로 관리하고 해결하는 데 사용됩니다. 집합을 병합하고(union) 요소가 속한 집합을 결정하는(찾기) 작업을 제공합니다. Union-Find는 일반적으로 Kruskal의 최소 스패닝 트리 및 그래프의 순환 감지와 같은 알고리즘에 사용됩니다.
Union Find는 다음과 같이 구현됩니다.
템플릿 코드:
파이썬
class UnionFind: def __init__(self): self.id = {} def find(self, x): y = self.id.get(x, x) if y != x: self.id[x] = y = self.find(y) return y def union(self, x, y): self.id[self.find(x)] = self.find(y)
JS
class UnionFind { constructor() { this.id = {}; } /** * Find the root parent of an element in the set. * Implements path compression for better efficiency. * @param x The element to find the root parent for. * @returns The root parent of the element. */ find(x) { let y = this.id[x] || x; if (y !== x) { this.id[x] = y = this.find(y); } return y; } /** * Union two elements into the same set. * @param x The first element. * @param y The second element. */ union(x, y) { this.id[this.find(x)] = this.find(y); } }
자바
import java.util.*; class UnionFind { private Map<String, String> id; public UnionFind() { id = new HashMap<>(); } /** * Find the root parent of an element in the set. * Implements path compression for better efficiency. * @param x The element to find the root parent for. * @return The root parent of the element. */ public String find(String x) { String y = id.getOrDefault(x, x); if (!y.equals(x)) { id.put(x, find(y)); } return y; } /** * Union two elements into the same set. * @param x The first element. * @param y The second element. */ public void union(String x, String y) { id.put(find(x), find(y)); } }
방향성 비순환 그래프(DAG)는 방향성 순환이 없는 방향성 그래프입니다.
토폴로지 정렬은 방향성 비순환 그래프( DAG )의 노드를 선형 순서로 배열하는 알고리즘으로, 각 노드는 후속 노드 앞에 표시됩니다. 이는 종속성 예약, 코드 컴파일, 다양한 애플리케이션의 작업 우선 순위 분석과 같은 작업에 매우 중요합니다.
다음은 토폴로지 정렬의 단계별 분석입니다.
초기화: 종속성이 있는 작업이나 노드를 나타내는 방향성 비순환 그래프( DAG )로 시작합니다. 정렬된 노드를 보유하기 위해 대기열을 초기화합니다.
차수 계산: 그래프의 각 노드에 대한 차수(들어오는 모서리 수)를 계산합니다. 진입차수가 0인 노드는 종속성이 없으며 위상 정렬의 시작점으로 적합합니다.
초기 대기열 채우기: 진입 차수가 0인 모든 노드를 대기열에 넣습니다. 이러한 노드를 먼저 처리할 수 있습니다.
토폴로지 정렬 루프: 큐가 비어 있지 않은 동안 다음 단계를 수행하십시오.
완료: 토폴로지 정렬 루프가 완료되면 큐는 비어 있고 정렬된 목록에는 유효한 토폴로지 순서로 모든 노드가 포함됩니다.
사이클 감지: 토폴로지 정렬 프로세스 중 어느 시점에서든 큐에 0차수가 남아 있는 노드가 없으면 그래프에 사이클이 있음을 나타내므로 토폴로지 정렬이 불가능해집니다.
토폴로지 정렬의 결과는 종속성을 존중하는 노드의 선형 순서이므로 작업을 예약하거나 다양한 애플리케이션에서 실행 순서를 분석하는 데 적합합니다.
주요 지표:
템플릿 코드:
파이썬
from collections import deque def find_indegree(graph): # Calculate the indegree of each node in the graph indegree = {node: 0 for node in graph} for node in graph: for neighbor in graph[node]: indegree[neighbor] += 1 return indegree def topological_sort(graph): result = [] queue = deque() indegree = find_indegree(graph) # Add nodes with 0 indegree to the queue for node in indegree: if indegree[node] == 0: queue.append(node) while queue: node = queue.popleft() result.append(node) # Update the indegree of neighboring nodes for neighbor in graph[node]: indegree[neighbor] -= 1 if indegree[neighbor] == 0: queue.append(neighbor) # If all nodes are visited, return the result if len(graph) == len(result): return result else: return None
JS
/** * Finds the indegree of each node in the graph. * @param {object} graph - The input graph. * @returns {object} - The indegree of each node. */ function findIndegree(graph) { const indegree = {}; for (const node in graph) { indegree[node] = 0; } for (const node in graph) { for (const neighbor of graph[node]) { indegree[neighbor]++; } } return indegree; } /** * Performs topological sorting on the given graph. * @param {object} graph - The input graph. * @returns {array|null} - The sorted nodes in topological order or null if a cycle is detected. */ function topologicalSort(graph) { const result = []; const queue = []; const indegree = findIndegree(graph); // Add nodes with no incoming edges to the queue for (const node in indegree) { if (indegree[node] === 0) { queue.push(node); } } while (queue.length > 0) { const node = queue.shift(); result.push(node); // Decrement the indegree of neighbors and enqueue if indegree becomes zero for (const neighbor of graph[node]) { indegree[neighbor]--; if (indegree[neighbor] === 0) { queue.push(neighbor); } } } // Check if all nodes have been visited (no cycles) if (Object.keys(graph).length === result.length) { return result; } else { return null; } }
자바
import java.util.*; /** * Finds the indegree of each node in the graph. * @param graph - The input graph. * @return The indegree of each node. */ public Map<String, Integer> findIndegree(Map<String, List<String>> graph) { Map<String, Integer> indegree = new HashMap<>(); for (String node : graph.keySet()) { indegree.put(node, 0); } for (String node : graph.keySet()) { for (String neighbor : graph.get(node)) { indegree.put(neighbor, indegree.getOrDefault(neighbor, 0) + 1); } } return indegree; } /** * Performs topological sorting on the given graph. * @param graph - The input graph. * @return The sorted nodes in topological order or null if a cycle is detected. */ public List<String> topologicalSort(Map<String, List<String>> graph) { List<String> result = new ArrayList<>(); Queue<String> queue = new LinkedList<>(); Map<String, Integer> indegree = findIndegree(graph); // Add nodes with no incoming edges to the queue for (String node : indegree.keySet()) { if (indegree.get(node) == 0) { queue.offer(node); } } while (!queue.isEmpty()) { String node = queue.poll(); result.add(node); // Decrement the indegree of neighbors and enqueue if indegree becomes zero for (String neighbor : graph.get(node)) { indegree.put(neighbor, indegree.get(neighbor) - 1); if (indegree.get(neighbor) == 0) { queue.offer(neighbor); } } } // Check if all nodes have been visited (no cycles) if (graph.size() == result.size()) { return result; } else { return null; } }
Trie 는 효율적인 문자열 일치 및 단어 저장에 사용되는 트리형 데이터 구조입니다. 공통 접두사가 있는 문자열을 저장하고 검색해야 하는 문제에 탁월합니다.
Trie를 구현하는 방법은 다음과 같습니다.
초기화: 일반적으로 연관된 문자가 없는 루트 노드로 구성된 빈 Trie로 시작합니다.
삽입: Trie에 단어를 삽입하려면 루트 노드에서 시작하여 한 번에 한 문자씩 트리 아래로 이동합니다. 단어의 각 문자에 대해 다음을 수행합니다.
단어 완성: Trie에 단어가 존재하는지 확인하려면 삽입과 유사한 방식으로 단어를 순회합니다. 단어의 각 문자가 현재 노드의 하위 노드에 해당하는지 확인하십시오. 순회가 단어 끝에 도달하고 마지막 문자 노드에 유효한 끝 표시(예: 부울 플래그)가 있는 경우 해당 단어는 Trie에 존재합니다.
접두어 검색: Trie는 접두어 검색에 탁월합니다. 주어진 접두사가 있는 모든 단어를 찾으려면 루트 노드에서 순회를 시작하고 접두사의 문자를 따라 트리 아래로 이동합니다. 접두사의 마지막 문자에 도달하면 해당 노드에서 깊이 우선 검색(DFS)을 수행하여 동일한 접두사를 공유하는 모든 단어를 찾을 수 있습니다.
삭제: Trie에서 단어를 삭제하려면 해당 단어를 검색하세요. 단어 끝에 도달하면 끝 표시를 제거합니다(있는 경우). 노드에 다른 하위 노드가 없으면 단어를 나타내는 Trie의 전체 분기를 안전하게 제거할 수 있습니다.
특히 대규모 어휘의 경우 시도는 메모리 집약적일 수 있습니다. 메모리를 최적화하기 위해 압축(예: 각 노드의 문자 배열 대신 맵 사용) 및 정리(후손이 없는 노드 제거)와 같은 기술을 적용할 수 있습니다.
시도는 효율적인 문자열 일치, 자동 완성 제안, 맞춤법 검사기 및 공통 접두사가 있는 단어 색인화에 특히 유용합니다. 트리형 구조에서 공유 접두사가 있는 단어나 문자열을 저장하고 검색하는 빠르고 공간 효율적인 방법을 제공합니다.
주요 지표:
템플릿 코드:
파이썬
class TrieNode: def __init__(self, value): self.value = value self.children = {} def insert(self, s, idx): # idx: index of the current character in s if idx != len(s): self.children.setdefault(s[idx], TrieNode(s[idx])) self.children.get(s[idx]).insert(s, idx + 1)
JS
class TrieNode { constructor(value) { this.value = value; this.children = {}; } insert(s, idx) { // idx: index of the current character in s if (idx !== s.length) { if (!this.children[s[idx]]) { this.children[s[idx]] = new TrieNode(s[idx]); } this.children[s[idx]].insert(s, idx + 1); } } }
자바
import java.util.*; class TrieNode { char value; Map<Character, TrieNode> children; TrieNode(char value) { this.value = value; this.children = new HashMap<>(); } void insert(String s, int idx) { // idx: index of the current character in s if (idx != s.length()) { char c = s.charAt(idx); if (!children.containsKey(c)) { children.put(c, new TrieNode(c)); } children.get(c).insert(s, idx + 1); } } }
동적 프로그래밍은 컴퓨터 과학 및 수학에서 광범위하고 복잡한 문제를 효율적으로 해결하는 데 사용되는 강력한 문제 해결 기술입니다. 문제를 더 간단한 하위 문제로 나눌 수 있고 이러한 하위 문제에 대한 솔루션을 결합하여 전체 문제를 해결할 수 있는 경우 특히 유용합니다.
주요 특징과 작동 방식은 다음과 같습니다.
최적의 하부 구조:
겹치는 하위 문제:
동적 프로그래밍 작동 방식:
동적 프로그래밍은 그래프에서 최단 경로 찾기, 자원 할당 최적화, 조합 계산, 배낭 문제 해결 등을 포함하여 광범위한 문제에 적용됩니다. 복잡한 문제를 더 간단한 부분으로 나누고 중복 계산을 피함으로써 솔루션을 최적화하는 능력은 알고리즘 설계 및 최적화의 기본 기술이 됩니다.
중요 개념: 상향식 접근 방식, 하향식, 메모, 표 작성
주요 지표:
템플릿 코드:
하향식 메모용 템플릿은 동적 프로그래밍을 구현하는 여러 방법 중 하나입니다.
파이썬
def top_down_memo(arr): def dp(state): # Base case(s): Define your own base case(s) for the problem if base_case: return 0 # Check if the state has already been memoized if state in memo: return memo[state] # Calculate the result using recurrence relation and memoize it result = recurrence_relation(state) memo[state] = result return result memo = {} # Memoization table to store calculated results return dp(initial_state)
JS
function topDownMemo(arr) { function dp(state) { // Base case(s): Define your own base case(s) for the problem if (baseCase) { return 0; } // Check if the state has already been memoized if (memo.has(state)) { return memo.get(state); } // Calculate the result using recurrence relation and memoize it const result = recurrenceRelation(state); memo.set(state, result); return result; } const memo = new Map(); // Memoization map to store calculated results return dp(initialState); }
자바
import java.util.HashMap; import java.util.Map; public int topDownMemo(int[] arr) { Map<StateType, Integer> memo = new HashMap<>(); // Memoization map to store calculated results return dp(initialState, memo); } private int dp(StateType state, Map<StateType, Integer> memo) { // Base case(s): Define your own base case(s) for the problem if (baseCase) { return 0; } // Check if the state has already been memoized if (memo.containsKey(state)) { return memo.get(state); } // Calculate the result using recurrence relation and memoize it int result = recurrenceRelation(state); memo.put(state, result); return result; }
역추적은 다양한 가능성을 시도하고 해결책이 나오지 않으면 취소하여 문제를 점진적으로 해결하는 일반적인 알고리즘 기술입니다. 철저한 검색이 필요할 때 사용됩니다.
역추적 작동 방식에 대한 자세한 내용은 다음과 같습니다.
역추적의 응용:
역추적은 다음을 포함한 다양한 문제 영역에서 사용됩니다.
주요 지표:
템플릿 코드:
파이썬
def backtrack(curr, OTHER_ARGUMENTS...): if BASE_CASE: # Modify the answer according to the problem's requirements return ans = 0 for ITERATE_OVER_INPUT: # Modify the current state according to the problem's requirements ans += backtrack(curr, OTHER_ARGUMENTS...) # Undo the modification of the current state (backtrack) return ans
JS
function backtrack(curr, ...OTHER_ARGUMENTS) { if (BASE_CASE) { // Modify the answer according to the problem's requirements return; } let ans = 0; for (let i = 0; i < ITERATE_OVER_INPUT.length; i++) { const item = ITERATE_OVER_INPUT[i]; // Modify the current state according to the problem's requirements ans += backtrack(curr, ...OTHER_ARGUMENTS); // Undo the modification of the current state (backtrack) } return ans; }
자바
public int backtrack(StateType curr, OtherArgumentType... OTHER_ARGUMENTS) { if (BASE_CASE) { // Modify the answer according to the problem's requirements return; } int ans = 0; for (int i = 0; i < ITERATE_OVER_INPUT.length; i++) { ItemType item = ITERATE_OVER_INPUT[i]; // Modify the current state according to the problem's requirements ans += backtrack(curr, OTHER_ARGUMENTS); // Undo the modification of the current state (backtrack) } return ans; }
간격 병합에는 겹치거나 인접한 간격을 통합 세트로 결합하는 작업이 포함되며, 시간 간격 또는 일정과 관련된 문제에 자주 사용됩니다. 간격 기반 작업을 단순화합니다.
다음은 병합 간격이 작동하는 방식을 자세히 살펴보겠습니다.
병합 간격의 적용:
병합 간격은 일반적으로 다음과 같은 경우에 사용됩니다.
이 기술은 겹치는 간격을 병합하여 간격 기반 작업을 단순화하고 다양한 애플리케이션에서 효율성을 향상시킵니다.
주요 지표:
템플릿 코드:
파이썬
def merge_intervals(intervals): # 1. Sort the intervals based on their start values. intervals.sort(key=lambda x: x[0]) # 2. Initialize an empty list to store merged intervals. merged_intervals = [] # 3. Iterate through the sorted intervals. for interval in intervals: # 4. If the merged_intervals list is empty or the current interval does not overlap with the last merged interval: if not merged_intervals or interval[0] > merged_intervals[-1][1]: merged_intervals.append(interval) else: # 5. If the current interval overlaps with the last merged interval, merge them. merged_intervals[-1][1] = max(merged_intervals[-1][1], interval[1]) # 6. Return the merged_intervals list. return merged_intervals
JS
function mergeIntervals(intervals) { // 1. Sort the intervals based on their start values. intervals.sort((a, b) => a[0] - b[0]); // 2. Initialize an empty array to store merged intervals. const mergedIntervals = []; // 3. Iterate through the sorted intervals. for (const interval of intervals) { // 4. If the mergedIntervals array is empty or the current interval does not overlap with the last merged interval: if (mergedIntervals.length === 0 || interval[0] > mergedIntervals[mergedIntervals.length - 1][1]) { mergedIntervals.push(interval); } else { // 5. If the current interval overlaps with the last merged interval, merge them. mergedIntervals[mergedIntervals.length - 1][1] = Math.max(mergedIntervals[mergedIntervals.length - 1][1], interval[1]); } } // 6. Return the mergedIntervals array. return mergedIntervals; }
자바
public class MergeIntervals { public int[][] merge(int[][] intervals) { // 1. Sort the intervals based on their start values. Arrays.sort(intervals, (a, b) -> a[0] - b[0]); // 2. Create a list to store merged intervals. List<int[]> mergedIntervals = new ArrayList<>(); // 3. Iterate through the sorted intervals. for (int[] interval : intervals) { // 4. If the mergedIntervals list is empty or the current interval does not overlap with the last merged interval: if (mergedIntervals.isEmpty() || interval[0] > mergedIntervals.get(mergedIntervals.size() - 1)[1]) { mergedIntervals.add(interval); } else { // 5. If the current interval overlaps with the last merged interval, merge them. mergedIntervals.get(mergedIntervals.size() - 1)[1] = Math.max(mergedIntervals.get(mergedIntervals.size() - 1)[1], interval[1]); } } // 6. Convert the list to an array and return it. return mergedIntervals.toArray(new int[mergedIntervals.size()][]); } }
이러한 패턴에 대해 자세히 알아보고 다음 코딩 인터뷰에서 이를 보다 효과적으로 구현할 수 있는 방법을 알아보려면 지금 techinterviews.io를 확인하세요! 우리는 데이터 구조 , 알고리즘 , 행동 인터뷰 , 심지어 급여 협상 과 같은 주제를 다루는 다음 코딩 인터뷰를 준비할 수 있도록 포괄적인 커리큘럼을 제공합니다. 연습할 수 있는 코딩 작업 공간도 내장되어 있습니다.
오늘 $30 할인된 프로모션 코드 TECH30을 사용하여 코딩 인터뷰 준비를 강화하세요!
@Limarc의 추천 이미지 “개발자 작성 코드”
Okso.app 으로 만든 그래픽