開発者にとって、新しい資料のレビューと学習に数週間を費やすことが多いため、コーディング面接の準備は非常に困難な場合があります。
実際のところ、ほとんどの開発者はコーディング面接に向けて完全に準備ができているとは決して感じていません。開発者が LeetCode の問題の解決に何週間も費やしたにもかかわらず、まだ準備ができていないと感じることは珍しくありません。このアプローチには明らかに問題があります。
従来のコーディング面接の準備に関連するいくつかの問題を詳しく見てみましょう。
従来の面接準備で最もよくある落とし穴の 1 つは、 「研磨」の練習です。このアプローチには、明確な計画や戦略を持たずに、できるだけ多くのLeetCode の問題を解決することが含まれます。これは生産的な戦略のように見えるかもしれませんが、いくつかの欠点があります。
構造化されたアプローチなしで問題を解決すると、自分の弱点に気づかない可能性があります。研究には意図的な計画はありません。目標は単にできるだけ多くの問題を解決することです。その結果、多くのことを学んだように感じるかもしれませんが、知識には大きなギャップがある可能性があります。
さらに、これの問題は、本質的に、多数の問題に対する解決策を暗記することを中心に展開していることです。面接官がこれまでに遭遇したものから少しでも逸脱する質問を提示した場合、100 の異なるコーディング問題の解決策を覚えようとしても効果がありません。そのため、さまざまな問題に対処する準備ができていないように感じる可能性があります。
この戦略に関する私の最後の問題は、長期的にはより多くのストレスと頭痛を引き起こすということです。転職するたびに同じ数週間の詰め込み授業を受けなければならないとしたら、毎回苦労することになるでしょう。物事を再学習し、以前と同じ問題を解決するのに何週間も費やすことになります。
これらの課題を考慮すると、より良い方法が必要です。
では、面接の準備をコーディングするための、より効果的で持続可能なアプローチはあるのでしょうか?答えは、コーディング パターンを理解し、活用することにあります。
コーディング面接の準備をするとき、私は問題の根本的なパターンを把握することを優先します。これらのパターンには、 Two-Pointers 、 Sliding Window 、 Modified Binary Search 、 Topological Sortなどのテクニックが含まれており、幅広いコーディングの問題に取り組むための多用途で強力なフレームワークを提供します。重点は、解決策の暗記から実証済みの問題解決テクニックに移ります。
コーディング パターンに焦点を当てることで、面接の準備を大幅に合理化し、効率を高めることができます。
準備は難しくするのではなく、賢く行ってください。
この記事ではこんなことをまとめてみました
この記事では多くのことを取り上げていますが、より詳細なトレーニング、説明、コーディングの練習が必要な場合は、Techinterviews.io で包括的なコーディング面接準備コースをチェックしてください。データ構造、行動面接、さらには給与交渉などの重要なトピックも取り上げます。
それでは、これらのコーディング パターンについて詳しく見ていきましょう。
リンク リストの反転には、リンク リスト内のポインタの方向を変更して要素の順序を逆にすることが含まれます。これはデータ構造における基本的な操作であり、多くの場合、ノード参照の慎重な操作が必要になります。
これは、リンクされたリストを処理する場合に便利で、制約はその場で反転することです。
リンクされたリストを元に戻すプロセスは次のとおりです。
まずは 3 つの変数current 、 previous 、 nextを定義します。 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; }
2 つのポインター手法では、データ構造 (通常は配列またはリンク リスト) を横断する 2 つのポインターを維持する必要があり、ペアまたは部分配列が関係する問題によく使用されます。異なる位置にある要素間の比較が必要な問題を最適化します。
この手法の利点は、特に最初に 1 つのポインターだけを使用する配列や文字列などの線形データ構造を扱う場合に、その単純さと効率にあります。 2 つのポインターを使用することで、冗長な操作を回避し、実行時の効率を大幅に向上させ、潜在的にO(n^2)からO(n)に削減できます。
「Two Pointers」パターンにはいくつかのバリエーションが含まれており、それぞれが特定のシナリオに合わせて調整されています。これらのバリエーションには、同じ方向、反対方向、およびデータ構造内を異なる速度で移動する 2 つのポインタを含む、「高速と低速」として知られる独自の方法(「ウサギとカメ」テクニックとも呼ばれる) が含まれます。サイクル。
データ構造をトラバースするために複数のポインターを使用する場合、そのアプローチは「2 つのポインター」パターンに従うものとして分類できます。
主要な指標:
テンプレートコード:
「反対方向」バリエーションのテンプレート
パイソン
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; }
2 つのヒープは、2 つのヒープ (最大ヒープと最小ヒープ) を利用して、データセット内の中央値の検索など、特定の問題を最適化します。このパターンは、バランスの取れた構造を維持するのに特に役立ちます。
仕組みは次のとおりです。
主要な指標:
テンプレートコード:
パイソン
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);
単調 - (関数または量が) 決して減少しない、または決して増加しないように変化すること。
単調スタックは、要素のスタックを非増加または非減少の順序で維持し、配列内の最も近い小さい/大きい要素を見つけるためによく使用されます。これは、特定の問題を最適化するための強力なツールです。
順序は厳密で、スタックの先頭にあるものよりも小さい (または大きい) 要素に遭遇した場合は、追加しようとしている要素が最小 (または最大) になるまで単調スタックからポップする必要があります。彼ら。
主要な指標:
テンプレートコード:
パイソン
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 は各ノードが 1 回だけアクセスされることを保証し、ツリー全体が走査されると自然に終了します。対照的に、グラフ 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 と同様に、グラフ 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 }
Disjoint Set Union (DSU)としても知られるUnion-Findデータ構造は、素セットまたは接続されたコンポーネントに関連する問題を効率的に管理および解決するために使用されます。これは、セットをマージする (結合) および要素が属するセットを決定する (検索) ための操作を提供します。 Union-Find は、クラスカルの最小スパニング ツリーやグラフのサイクル検出などのアルゴリズムでよく使用されます。
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 ) から始めます。ソートされたノードを保持するキューを初期化します。
次数の計算:グラフ内の各ノードの次数 (入力エッジの数) を計算します。 in-degree が 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 を実装する方法は次のとおりです。
初期化:空のトライから開始します。これは通常、関連付けられた文字のないルート ノードで構成されます。
挿入:トライに単語を挿入するには、ルート ノードから開始して、一度に 1 文字ずつツリーを下にたどります。単語内の各文字について:
単語の補完:単語がトライ内に存在するかどうかを確認するには、挿入と同様の方法でトライを走査します。単語内の各文字が現在のノードの子ノードに対応していることを確認してください。トラバースが単語の終わりに到達し、最後の文字ノードに有効な終了マーカー (ブール フラグなど) がある場合、その単語はトライ内に存在します。
前方一致検索: Trie は前方一致検索に優れています。特定の接頭辞を持つすべての単語を検索するには、ルート ノードでトラバースを開始し、接頭辞の文字に従ってツリーを下に移動します。接頭辞の最後の文字に到達したら、そのノードから深さ優先検索 (DFS) を実行して、同じ接頭辞を共有するすべての単語を検索できます。
削除:トライから単語を削除するには、単語の検索を実行します。単語の終わりに到達したら、終了マーカー (存在する場合) を削除します。ノードに他に子がない場合は、単語を表すトライのブランチ全体を安全に削除できます。
トライは、特に大きな語彙の場合、メモリを大量に消費する可能性があります。メモリを最適化するには、圧縮 (各ノードの文字配列の代わりにマップを使用するなど) や枝刈り (子孫を持たないノードを削除する) などの手法を適用できます。
トライは、効率的な文字列マッチング、オートコンプリート候補、スペル チェッカー、および共通の接頭辞を持つ単語のインデックス作成に特に役立ちます。これらは、共有接頭辞を持つ単語や文字列をツリー状の構造に保存および検索するための、高速かつスペース効率の高い方法を提供します。
主要な指標:
テンプレートコード:
パイソン
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); } } }
動的プログラミングは、コンピューター科学と数学で広範囲にわたる複雑な問題を効率的に解決するために使用される強力な問題解決手法です。これは、問題をより単純なサブ問題に分割でき、これらのサブ問題の解決策を組み合わせて問題全体を解決できる場合に特に役立ちます。
その主な特徴とその仕組みは次のとおりです。
最適な下部構造:
重複するサブ問題:
動的プログラミングの仕組み:
動的プログラミングは、グラフ内の最短パスの検索、リソース割り当ての最適化、組み合わせのカウント、ナップザック問題の解決など、幅広い問題に適用されます。複雑な問題をより単純な部分に分解し、冗長な計算を回避することでソリューションを最適化する機能により、アルゴリズムの設計と最適化における基本的な手法となっています。
重要な概念:ボトムアップ アプローチ、トップダウン、メモ化、表作成
主要な指標:
テンプレートコード:
トップダウンのメモ化のテンプレートは、動的プログラミングを実装する多くの方法の 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をチェックしてください。次のコーディング面接に備えるための包括的なカリキュラムを提供しており、データ構造、アルゴリズム、行動面接、さらには給与交渉などのトピックもカバーしています。練習できる組み込みのコーディング ワークスペースもあります。
プロモーション コードTECH30 を30 ドル割引で利用して、コーディング面接の準備を今すぐ強化しましょう!
@Limarc による注目の画像「コードを書く開発者」
Okso.appで作成されたグラフィック