Se préparer aux entretiens de codage peut être un véritable défi, les développeurs passant souvent plusieurs semaines à réviser et à apprendre de nouveaux éléments.
La vérité est que la plupart des développeurs ne se sentent jamais pleinement préparés pour leurs entretiens de codage. Il n'est pas rare que les développeurs passent des semaines à résoudre des problèmes sur LeetCode et ne se sentent toujours pas préparés. De toute évidence, cette approche pose des problèmes.
Examinons de plus près certains problèmes associés à la préparation traditionnelle aux entretiens de codage.
L’un des pièges les plus courants dans la préparation traditionnelle aux entretiens est la pratique du « broyage ». Cette approche implique de résoudre autant de problèmes LeetCode que possible sans plan ou stratégie clair. Même si cette stratégie peut sembler productive, elle présente plusieurs inconvénients.
Lorsque vous résolvez des problèmes sans approche structurée, vous risquez de ne pas reconnaître vos faiblesses. Il n'y a pas de plan délibéré pour votre étude ; le but est simplement de résoudre autant de problèmes que possible. En conséquence, vous pouvez avoir l’impression d’avoir beaucoup appris, mais il peut y avoir des lacunes importantes dans vos connaissances.
De plus, le problème est qu’il s’agit essentiellement de mémoriser des solutions à une multitude de problèmes. Tenter de mémoriser les solutions à une centaine de problèmes de codage différents s'avère inefficace lorsque les enquêteurs présentent des questions qui s'écartent même légèrement de ce que vous avez rencontré auparavant. Cela peut vous donner le sentiment de ne pas être préparé à gérer les variations du problème.
Mon dernier problème avec cette stratégie est qu’à long terme, elle crée davantage de stress et de maux de tête. Si vous devez subir la même séance de bachotage de plusieurs semaines à chaque fois que vous souhaitez changer d'emploi, vous aurez du mal à chaque fois. Vous passerez des semaines à réapprendre des choses et à résoudre les mêmes problèmes qu'avant.
Compte tenu de ces défis, il doit y avoir une meilleure solution.
Alors, existe-t-il une approche plus efficace et plus durable pour coder la préparation aux entretiens ? La réponse réside dans la compréhension et l’utilisation des modèles de codage.
Lorsque je me prépare aux entretiens de codage, je donne la priorité à la compréhension des schémas sous-jacents des problèmes. Ces modèles, qui incluent des techniques telles que Two-Pointers , Sliding Window , Modified Binary Search , Topological Sort et bien d'autres, fournissent un cadre polyvalent et puissant pour résoudre un large éventail de problèmes de codage. L'accent passe de la mémorisation des solutions aux techniques éprouvées de résolution de problèmes.
En vous concentrant sur les modèles de codage, vous pouvez rationaliser considérablement la préparation de votre entretien, la rendant ainsi plus efficace.
N'oubliez pas : préparez-vous plus intelligemment, pas plus dur.
Dans cet article, j'ai compilé les
Bien que j'aborde beaucoup de choses dans cet article, si vous souhaitez une formation plus approfondie, des explications et des pratiques de codage, vous pouvez consulter notre cours complet de préparation aux entretiens de codage sur Techinterviews.io. Nous abordons également des sujets cruciaux tels que les structures de données , les entretiens comportementaux et même la négociation salariale .
Passons maintenant à ces modèles de codage.
L'inversion de liste chaînée consiste à changer la direction des pointeurs dans une liste chaînée pour inverser l'ordre des éléments. Il s'agit d'une opération fondamentale dans les structures de données et elle nécessite souvent une manipulation minutieuse des références de nœuds.
Ceci est utile lorsqu'il s'agit d'une liste chaînée et la contrainte est de l'inverser sur place.
Le processus pour inverser une liste chaînée en place est le suivant :
Commencez par définir trois variables : current , previous et next . Définissez current comme tête de la liste chaînée et initialisez previous et next sur None.
Bien que la variable actuelle ne soit pas None , procédez comme suit :
Enfin, définissez la tête de la liste inversée sur le dernier nœud atteint, qui est stocké dans la variable précédente .
Indicateurs clef:
Code du modèle :
Python
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; }
Java
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; }
La recherche binaire modifiée adapte l'algorithme de recherche binaire classique pour résoudre divers problèmes. Les variantes incluent la recherche de la première/dernière occurrence d'un élément ou la recherche dans des tableaux pivotés. Cela nécessite une gestion minutieuse des points médians et des conditions.
Si jamais vous recevez un tableau trié, une liste chaînée ou une matrice, envisagez d'utiliser une recherche binaire modifiée .
Voici un aperçu de la façon d'appliquer ce modèle à une structure de données triée :
middle = start + (end - start) / 2
.
Indicateurs clef:
Code du modèle :
Python
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; }
Java
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; }
La technique des deux pointeurs consiste à maintenir deux pointeurs qui traversent une structure de données, généralement des tableaux ou des listes chaînées, souvent utilisées pour des problèmes impliquant des paires ou des sous-tableaux. Il optimise les problèmes qui nécessitent une comparaison entre des éléments situés à différentes positions.
L'avantage de cette technique réside dans sa simplicité et son efficacité, en particulier lorsqu'il s'agit de structures de données linéaires telles que des tableaux ou des chaînes pour lesquelles vous pourriez initialement utiliser un seul pointeur. En utilisant deux pointeurs, vous pouvez éviter les opérations redondantes et améliorer considérablement l'efficacité de l'exécution, la réduisant potentiellement de O(n^2) à O(n) .
Le modèle « Two Pointers » englobe plusieurs variantes, chacune adaptée à des scénarios spécifiques. Ces variations incluent la même direction , la direction opposée et une méthode unique connue sous le nom de « rapide et lente », souvent appelée technique « tortue et lièvre » , qui implique deux pointeurs se déplaçant à des vitesses différentes à travers une structure de données, particulièrement utile pour détecter cycles.
Si vous utilisez plusieurs pointeurs pour parcourir une structure de données, vous pouvez classer votre approche comme suivant le modèle « Deux pointeurs » .
Indicateurs clef:
Code du modèle :
Modèle pour variation « direction opposée »
Python
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; }
Java
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; }
La technique de la fenêtre coulissante consiste à maintenir une fenêtre dynamique sur une structure de données linéaire, telle que des tableaux, des chaînes ou des listes chaînées. La taille de la fenêtre peut varier en fonction de l'implémentation spécifique et elle peut également être définie comme une valeur fixe. L'objectif principal de cette fenêtre est de surveiller et de capturer en permanence les données qui satisfont à des critères spécifiques, ce qui la rend particulièrement utile pour résoudre efficacement les problèmes de sous-réseaux ou de sous-chaînes.
Ce modèle utilise souvent une carte de hachage pour faciliter le suivi des données individuelles dans la fenêtre. Cependant, il est important de noter que cette approche peut entraîner une complexité spatio-temporelle de O(n) .
Indicateurs clef:
Code du modèle :
Python
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; }
Java
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; }
Ce modèle implique de rechercher les K éléments les plus grands ou les plus petits d'une collection, souvent implémentés à l'aide de structures de données telles que des tas ou des files d'attente prioritaires. C'est utile pour sélectionner un sous-ensemble d'éléments en fonction de leur valeur ou de leur fréquence.
Chaque fois qu'on nous demande de trouver les éléments « K » les plus fréquents, les plus petits ou les plus importants dans un ensemble de données donné, nous pouvons envisager d'utiliser ce modèle.
La façon dont cela fonctionne est simple :
La beauté de cette approche réside dans son efficacité ; vous n'avez pas besoin de recourir à des algorithmes de tri, car le tas lui-même maintient naturellement l'ordre requis pour vous.
Indicateurs clef:
Code du modèle :
Python
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); }
Java
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; }
Two Heaps utilise deux tas (un tas maximum et un tas min) pour optimiser certains problèmes, comme trouver des valeurs médianes dans un ensemble de données. Ce modèle est particulièrement utile pour maintenir une structure équilibrée.
Voici comment cela fonctionne:
Indicateurs clef:
Code du modèle :
Python
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);
Java
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);
Monotonique - (d'une fonction ou d'une quantité) variant de telle manière qu'elle ne diminue jamais ou n'augmente jamais.
La pile monotonique maintient une pile d'éléments dans un ordre non croissant ou non décroissant, souvent utilisée pour trouver les éléments plus petits/plus proches les plus proches dans un tableau. C'est un outil puissant pour optimiser certains problèmes.
L'ordre est strict, chaque fois que nous rencontrons un élément qui est plus petit (ou plus grand) que ce qui se trouve en haut de la pile, nous devons alors sortir de la pile monotone jusqu'à ce que l'élément que nous cherchons à ajouter soit le plus petit (ou le plus grand) des eux.
Indicateurs clef:
Code du modèle :
Python
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; }
Java
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 , ou Depth-First Search , est une méthode de traversée dans laquelle vous explorez aussi profondément que possible le long d'une branche avant de revenir en arrière ; il est largement appliqué aux problèmes impliquant des arbres et des graphiques, en particulier pour les opérations de parcours et de recherche.
Pour exécuter DFS sur une arborescence, vous devez commencer à la racine. Suivez ensuite les étapes ci-dessous :
Différence avec DFS sur un graphique : La principale différence entre DFS arborescent et DFS graphique réside dans la présence de cycles. Dans un arbre, il n'y a pas de cycles par définition, donc l'arbre DFS garantit que chaque nœud est visité exactement une fois, et il se termine naturellement lorsque l'arbre entier est parcouru. En revanche, le graphique DFS doit intégrer des mesures supplémentaires pour gérer les structures cycliques au sein d'un graphique. Pour éviter de revisiter indéfiniment les nœuds d'un cycle, le graphe DFS nécessite des mécanismes tels que le marquage des nœuds visités et la gestion appropriée du retour en arrière . Cette distinction rend le DFS graphique plus complexe que le DFS arborescent en raison du potentiel de boucles infinies en présence de cycles.
Indicateurs clef:
Code du modèle :
Python
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 }
Java
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 est une technique de parcours pour les arbres et les graphiques qui explore tous les nœuds à la profondeur actuelle avant de passer au niveau suivant.
Pour exécuter BFS sur une arborescence, vous devez commencer à la racine. Suivez ensuite les étapes ci-dessous :
Initialisez une structure de données de file d'attente vide pour garder une trace des nœuds à visiter.
Mettez le nœud racine dans la file d'attente.
Entrez une boucle jusqu'à ce que la file d'attente soit vide :
Répétez les étapes 3a à 3c jusqu'à ce que la file d'attente soit vide.
Le parcours BFS est terminé lorsque tous les nœuds de l'arborescence ont été visités par niveau, de gauche à droite.
Essentiellement, BFS sur un arbre explore les nœuds niveau par niveau , en commençant par la racine et en passant aux enfants avant de passer au niveau suivant. Cela garantit que les nœuds de chaque niveau sont visités avant de passer au niveau suivant, ce qui le rend particulièrement utile pour des tâches telles que la recherche du chemin le plus court dans un arbre non pondéré ou l'exploration d'un arbre niveau par niveau.
Différence avec BFS sur un graphique : Semblable à DFS, Graph BFS s'adapte à la présence de cycles et de chemins multiples entre les nœuds. Il utilise des mécanismes tels que le marquage des nœuds visités et des conditions de terminaison spécialisées pour naviguer efficacement dans le réseau complexe de relations au sein des graphiques.
Indicateurs clef:
Code du modèle :
Python
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 }
Java
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 }
La structure de données Union-Find , également connue sous le nom de Disjoint Set Union (DSU) , est utilisée pour gérer et résoudre efficacement des problèmes impliquant des ensembles disjoints ou des composants connectés. Il fournit des opérations pour fusionner des ensembles (union) et déterminer l'ensemble auquel appartient un élément (trouver). Union-Find est couramment utilisé dans des algorithmes tels que l'arbre couvrant minimum de Kruskal et la détection de cycles dans les graphiques.
Union Find est implémenté comme suit :
Code du modèle :
Python
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); } }
Java
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)); } }
Un graphe acyclique orienté (DAG) est un graphe orienté sans cycles orientés.
Le tri topologique est un algorithme permettant d'organiser les nœuds d'un graphe acyclique orienté ( DAG ) dans un ordre linéaire, où chaque nœud apparaît avant ses successeurs. Il est crucial pour des tâches telles que la planification des dépendances, la compilation du code et l'analyse de la priorité des tâches dans diverses applications.
Voici une présentation étape par étape du tri topologique :
Initialisation : commencez par un graphe acyclique dirigé ( DAG ) qui représente des tâches ou des nœuds avec des dépendances. Initialisez une file d'attente pour contenir les nœuds triés.
Calcul en degré : calculez le degré en degré (le nombre d'arêtes entrantes) pour chaque nœud du graphique. Les nœuds avec un degré d'entrée de 0 n'ont aucune dépendance et peuvent être le point de départ du tri topologique.
Remplissage initial de la file d'attente : mettez en file d'attente tous les nœuds avec un degré d'entrée de 0 dans la file d'attente. Ces nœuds peuvent être traités en premier.
Boucle de tri topologique : tant que la file d'attente n'est pas vide, effectuez les étapes suivantes :
Achèvement : une fois la boucle de tri topologique terminée, la file d'attente sera vide et la liste triée contiendra tous les nœuds dans un ordre topologique valide.
Détection de cycle : si à un moment donné du processus de tri topologique, il n'y a plus de nœuds avec un degré entrant de 0 dans la file d'attente, cela indique la présence de cycles dans le graphique, rendant le tri topologique impossible.
Le résultat du tri topologique est un ordre linéaire des nœuds qui respecte leurs dépendances, ce qui le rend adapté à la planification de tâches ou à l'analyse de l'ordre d'exécution dans diverses applications.
Indicateurs clef:
Code du modèle :
Python
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; } }
Java
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; } }
Un Trie est une structure de données arborescente utilisée pour une correspondance efficace des chaînes et le stockage des mots. Il excelle dans les problèmes où vous devez stocker et rechercher des chaînes avec des préfixes communs.
Voici comment implémenter un Trie :
Initialisation : commencez par un Trie vide, qui consiste généralement en un nœud racine sans caractère associé.
Insertion : pour insérer un mot dans le Trie, commencez par le nœud racine et parcourez l'arborescence, un caractère à la fois. Pour chaque caractère du mot :
Achèvement de mots : pour vérifier si un mot existe dans le Trie, parcourez-le d'une manière similaire à l'insertion. Assurez-vous que chaque caractère du mot correspond à un nœud enfant du nœud actuel. Si le parcours atteint la fin du mot et qu'il existe un marqueur de fin valide (par exemple, un indicateur booléen) au dernier nœud de caractère, le mot existe dans le Trie.
Recherche de préfixe : Trie excelle dans la recherche de préfixe. Pour trouver tous les mots avec un préfixe donné, démarrez le parcours au nœud racine et descendez dans l'arborescence en suivant les caractères du préfixe. Une fois que vous avez atteint le dernier caractère du préfixe, vous pouvez effectuer une recherche en profondeur (DFS) à partir de ce nœud pour trouver tous les mots partageant le même préfixe.
Suppression : Pour supprimer un mot du Trie, effectuez une recherche du mot. Lorsque vous atteignez la fin du mot, supprimez le marqueur de fin (s'il existe). Si le nœud n'a pas d'autres enfants, vous pouvez supprimer en toute sécurité la branche entière du Trie, qui représente le mot.
Les essais peuvent être gourmands en mémoire, en particulier pour les vocabulaires volumineux. Pour optimiser la mémoire, des techniques telles que la compression (par exemple, utiliser une carte au lieu d'un tableau de caractères dans chaque nœud) et l'élagage (supprimer les nœuds sans descendants) peuvent être appliquées.
Les essais sont particulièrement utiles pour une correspondance efficace des chaînes, des suggestions de saisie semi-automatique, des correcteurs orthographiques et des mots d'indexation avec des préfixes communs. Ils offrent un moyen rapide et peu encombrant de stocker et de rechercher des mots ou des chaînes avec des préfixes partagés dans une structure arborescente.
Indicateurs clef:
Code du modèle :
Python
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); } } }
Java
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); } } }
La programmation dynamique est une technique puissante de résolution de problèmes utilisée en informatique et en mathématiques pour résoudre efficacement un large éventail de problèmes complexes. Cela est particulièrement utile lorsqu’un problème peut être décomposé en sous-problèmes plus simples et que les solutions à ces sous-problèmes peuvent être combinées pour résoudre le problème global.
Voici ses principales caractéristiques et son fonctionnement :
Sous-structure optimale :
Sous-problèmes qui se chevauchent :
Comment fonctionne la programmation dynamique :
La programmation dynamique est appliquée à un large éventail de problèmes, notamment la recherche des chemins les plus courts dans les graphiques, l'optimisation de l'allocation des ressources, le comptage des combinaisons, la résolution de problèmes de sac à dos et bien plus encore. Sa capacité à optimiser des solutions en décomposant des problèmes complexes en parties plus simples et en évitant les calculs redondants en fait une technique fondamentale dans la conception et l’optimisation d’algorithmes.
Concepts importants : approche ascendante, descendante, mémorisation, tabulation
Indicateurs clef:
Code du modèle :
Le modèle de mémorisation descendante est l’une des nombreuses façons de mettre en œuvre une programmation dynamique.
Python
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); }
Java
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; }
Le backtracking est une technique algorithmique générale permettant de résoudre des problèmes de manière incrémentale en essayant différentes possibilités et en les annulant si elles ne conduisent pas à une solution. Il est utilisé lorsqu'une recherche exhaustive est requise.
Voici un aperçu détaillé du fonctionnement du backtracking :
Applications du retour en arrière :
Le backtracking est utilisé dans divers domaines problématiques, notamment :
Indicateurs clef:
Code du modèle :
Python
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; }
Java
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; }
La fusion d'intervalles consiste à combiner des intervalles qui se chevauchent ou adjacents dans un ensemble consolidé, souvent utilisé dans les problèmes impliquant des intervalles de temps ou une planification. Il simplifie les opérations basées sur des intervalles.
Voici un aperçu plus détaillé du fonctionnement de la fusion des intervalles :
Applications des intervalles de fusion :
Les intervalles de fusion sont couramment utilisés dans :
En fusionnant des intervalles qui se chevauchent, cette technique simplifie les opérations basées sur des intervalles et améliore l'efficacité dans diverses applications.
Indicateurs clef:
Code du modèle :
Python
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; }
Java
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()][]); } }
Si vous souhaitez en savoir plus sur ces modèles et comment les mettre en œuvre plus efficacement lors de votre prochain entretien de codage, consultez techinterviews.io dès aujourd'hui ! Nous proposons un programme complet pour vous préparer à votre prochain entretien de codage, couvrant des sujets tels que les structures de données , les algorithmes , les entretiens comportementaux et même la négociation salariale . Nous disposons même d’un espace de travail de codage intégré pour que vous puissiez vous entraîner.
Boostez votre préparation à un entretien de codage dès aujourd'hui avec notre code promotionnel TECH30 pour 30 $ de réduction !
Image en vedette « un développeur écrivant du code » par @Limarc
Graphiques réalisés avec Okso.app