Article original : Developer Interview Prep – How to Use a Collaborative Approach to Problem-Solving
Par Alberto Gonzalez Rosales
Peu importe votre expérience en tant que développeur, passer un entretien pour un nouvel emploi est toujours stressant. Cela a certainement été le cas pour moi.
Dans mon cas, je travaille professionnellement en tant que développeur logiciel depuis deux ans et demi, et j'ai déjà dû affronter cinq entretiens de développeur.
J'ai vu des gens se concentrer trop sur l'apprentissage de problèmes algorithmiques spécifiques, ou s'entraîner sur des plateformes en ligne spécialisées dans les défis d'entretien. Mais il y a un autre aspect des entretiens que je pense être tout aussi important – et auquel les gens prêtent moins attention.
Alors, si vous voulez savoir comment je pense que vous devriez orienter votre préparation pour les entretiens, prenez une tasse de café et installez-vous confortablement.
Cela va être un peu mouvementé (mais ne vous inquiétez pas – des exemples en Python sont inclus !).
Voici ce que nous allons couvrir :
- Les algorithmes et les structures de données sont-ils importants pour les entretiens ?
- Se concentrer sur une approche collaborative
- Essayez de faire un entretien simulée
– Le problème initial de l'exemple d'entretien
– Le deuxième problème de l'exemple d'entretien
– Le problème le plus difficile de l'exemple d'entretien - Mots de la fin
Commençons.
Les algorithmes et les structures de données sont-ils importants pour les entretiens ?
La réponse courte est Oui.
Connaître les structures de données et les algorithmes vous aidera souvent à obtenir un emploi. De plus, les avantages d'avoir une solide formation algorithmique incluent la capacité à prendre de meilleures décisions face à un problème difficile.
Dans la vie réelle, les problèmes algorithmiques n'apparaîtront pas comme les énoncés des exercices académiques que vous pourriez étudier. Pourtant, plus vous êtes entraîné, plus vous êtes susceptible de reconnaître une application d'un arbre binaire équilibré ou d'un algorithme de plus court chemin déguisé.
Avec cela en tête, je recommanderais de ne pas s'entraîner aux structures de données et aux algorithmes uniquement pour les entretiens que vous souhaitez réussir. La conception des algorithmes et l'utilisation des bonnes structures de données sont des sujets suffisamment beaux pour être étudiés simplement pour le plaisir de savoir.
Le champ d'un entretien est très limité. Au lieu de cela, concentrez-vous sur la partie résolution de problèmes. Essayez de comprendre comment utiliser un algorithme (ou ses variations) pour des tâches similaires. Étudiez toutes les variantes et apprenez à les reconnaître dans différentes situations. Cela vous aidera à aborder de nouvelles tâches avec un esprit ouvert.
De plus, ne mémorisez pas les solutions. Cela ne vous mènera nulle part. Il est très facile pour un développeur expérimenté de poser les bonnes questions qui vous mettront en difficulté si vous ne vous entraînez que pour des sujets très spécifiques.
Prenez un problème et essayez de le résoudre en utilisant différentes méthodes. Essayez de résoudre des problèmes plus difficiles à chaque fois. Sortez de votre zone de confort.
Cela portera ses fruits.
Se concentrer sur une approche collaborative
La plupart des gens s'entraînent pour leurs entretiens sur des sites comme Leetcode. Il n'y a pas de problème avec cela. Ces sites sont bons pour entraîner vos compétences en résolution de problèmes.
Je participe à des concours de programmation compétitive depuis au moins huit ans, et j'ai toujours bénéficié de ces ressources en ligne.
Mais il y a une chose difficile à comprendre pour la plupart des ingénieurs logiciels qui veulent bien performer lors d'un entretien. Ils ne s'entraînent pas pour l'aspect le plus important du développement logiciel : la collaboration.
Habituellement, lorsque vous êtes confronté à un problème sur une plateforme en ligne, il vient avec certaines contraintes sur les valeurs maximales pour certaines entrées. Il peut également avoir des limites de temps et de mémoire qui vous obligent à ajuster vos solutions pour être plus ou moins efficaces. Mais ce n'est pas ainsi que cela se passera dans la vie réelle.
Il n'est pas évident de mapper ces contraintes à des scénarios de la vie réelle. Elles se présentent généralement sous la forme de demandes très spécifiques d'un client, ou de caractéristiques très spécifiques de l'équipe.
Dans un projet réel, l'équipe collaborera pour déterminer quelles seront ces contraintes. Vous devez analyser vos cas d'utilisation, le temps dont vous disposez pour la tâche, qui est l'utilisateur final, combien de personnes travailleront dessus, et ainsi de suite...
Après une série de discussions, vous parviendrez à un consensus et commencerez enfin à implémenter une solution qui répond à vos besoins. Et cette solution n'a même pas besoin d'être la plus efficace dans certains cas, mais la plus rapide à implémenter, par exemple.
C'est aussi ce que les intervieweurs veulent voir chez les candidats pendant les entretiens.
Vous ne sautez pas directement à l'implémentation de la meilleure solution que vous connaissez pour le problème auquel vous êtes confronté. Au lieu de cela, vous devriez utiliser vos intervieweurs comme une ressource précieuse, les traiter comme s'ils étaient vos coéquipiers (après tout, vous voulez qu'ils le deviennent). Posez des questions sur la manière dont l'équipe préfère que la solution du problème soit implémentée.
Cela mènera à une discussion très fructueuse où vous pourrez montrer vos capacités de codage et vos compétences collaboratives. Vous pourriez commencer par proposer des solutions simples et guider vos intervieweurs à travers le processus d'amélioration de la solution en utilisant les meilleurs atouts que vous avez en main.
En guise de note finale sur l'entraînement en ligne, je recommanderais de faire des entraînements individuels et en équipe. Utilisez des sites tels que Codeforces ou AtCoder, qui ont un grand ensemble de problèmes intéressants (et difficiles), et essayez de rivaliser avec vous-même chaque jour.
Ne vous concentrez pas sur votre classement. Je l'ai fait avant, et cela ne fait que vous retenir.
Essayez de faire un entretien simulée
Si vous êtes arrivé à ce point dans l'article, je vous proposerais un petit exercice. Faisons un entretien simulée où je jouerai le rôle de l'intervieweur. Je vous dirai comment je pense que le candidat (vous) devrait répondre à chaque question et effectuer chaque tâche.
Bien sûr, nous nous concentrerons uniquement sur la partie défi de programmation. D'autres aspects des entretiens, comme parler des expériences précédentes, sont également importants, mais nous essaierons de couvrir cela dans un autre article.
Alors, si vous vous sentez prêt, c'est parti !
Le problème initial de l'exemple d'entretien
Commençons par la tâche que vous allez résoudre. Gardez à l'esprit que si vous et moi nous trouvons un jour dans cette situation, je n'utiliserai pas cet exemple, mais bien sûr, j'utiliserai la même méthodologie 😉.
L'énoncé du problème est le suivant :
"Étant donné un nombre entier
X, déterminez s'il est un nombre premier."
Vous pourriez être tenté d'opter pour la meilleure solution que vous connaissez pour résoudre ce problème. Je pense que cette approche, comme je l'ai expliqué précédemment, n'est pas toujours correcte.
Au lieu de cela, ce que je voudrais savoir, ce sont les différentes approches pour résoudre ce problème. De plus, j'aimerais que vous posiez des questions sur les exigences pour cette tâche. Quelque chose comme :
- Devons-nous viser la performance ou résoudre le problème plus rapidement ?
- Devons-nous faire une solution facile à comprendre pour les autres développeurs ?
Habituellement, certains aspects à considérer lors de la création de la première solution pour un problème sont :
- Facile vs Difficile : Devons-nous faire une solution facile à implémenter même si elle n'est pas parfaite, ou devons-nous opter pour une solution plus robuste qui sera difficile à implémenter ?
- Naïf vs Efficace : Devons-nous livrer une solution naïve fonctionnelle d'abord, puis une solution plus efficace, ou devons-nous aller directement à l'efficace ?
Évaluez ce que votre équipe veut optimiser. Parvenez à un consensus et implémentez la solution convenue.
Dans mon cas, je serais ravi si vous proposiez la solution la plus facile, la plus rapide à implémenter et correcte à laquelle vous pouvez penser, puis me guidiez à travers le processus d'amélioration de cette solution.
Un exemple de très bonne solution initiale à ce problème est quelque chose comme ce qui suit :
# naive.py
def is_prime(x: int) -> bool:
if x in [0, 1]:
return False
for i in range(2, x):
if x % i == 0:
return False
return True
Comme vous pouvez le voir, cette fonction est correcte. Puisqu'un nombre premier est un entier divisible uniquement par le nombre 1 et lui-même, il est logique d'itérer de 2 à x - 1 à la recherche d'un diviseur de x. Si nous en trouvons un, nous pouvons immédiatement retourner False. Sinon, nous retournons True. En tant que cas particuliers, les nombres 0 et 1 ne sont pas premiers par définition.
C'est un bon point de départ !
Maintenant, avant de plonger dans l'optimisation de cette méthode, vous pouvez discuter du style du code. Est-il suffisamment Pythonique ? Est-ce que cela nous importe ? Est-il lisible ?
Toutes ces questions peuvent ne pas sembler importantes au premier abord, mais, puisque nous travaillons en équipe, elles comptent. Avoir un style de codage est important car cela facilite la compréhension du code de chacun par les autres membres de l'équipe, et cela accélérera les revues et les refactorisations. De plus, le code est plus souvent lu qu'écrit, donc la lisibilité compte !
Vous pourriez être tenté de montrer vos compétences en Python et de réécrire la fonction précédente comme suit :
# pythonic_naive.py
def is_prime(x: int) -> bool:
return False if x in {0, 1} else all(x % i != 0 for i in range(2, x))
Je pense que c'est une bonne façon, pythonique, d'écrire cette fonction. Mais, puisque l'équipe est la chose la plus importante ici, ce changement devrait être discuté.
Il se peut que certains membres de l'équipe ne soient pas aussi compétents en Python. Peut-être que, dans ce cas, nous devrions optimiser pour la lisibilité et garder la fonction telle que nous l'avons écrite au début.
Un ajout plus intéressant, avant de passer à la performance, serait d'ajouter des docstrings à la fonction. Comme je l'ai dit précédemment, ce code sera probablement lu par vos membres d'équipe à l'avenir. Il est important, alors, de le rendre plus facile à comprendre pour eux (et pour vous-même dans le futur).
Peut-être que changer la fonction en quelque chose comme ce qui suit ajoutera plus de valeur :
# naive.py
def is_prime(x: int) -> bool:
"""Cette fonction prend un entier `x` comme
argument et vérifie s'il est premier ou non.
Args:
x (int): Le nombre entier à tester pour la primalité.
Returns:
bool: True si le nombre `x` est premier, False sinon.
"""
if x in [0, 1]:
return False
for i in range(2, x):
if x % i == 0:
return False
return True
Première optimisation
Jusqu'à ce point, nous avons une solution initiale qui fonctionne. Nous avons discuté de sujets importants tels que le style de code, la lisibilité et la documentation pour les développeurs. Ce sont toutes des choses importantes à considérer lorsque l'on travaille en équipe.
Mais nous n'avons toujours pas parlé de la performance de la solution ! Alors, il est probablement temps de le faire.
À ce stade, vous devriez probablement mentionner que cette fonction peut être implémentée pour qu'elle s'exécute beaucoup plus rapidement. Et c'est maintenant que vous montrez toute cette connaissance algorithmique qui est en vous.
La solution précédente a une complexité temporelle de O(x), où x est l'entier d'entrée que la fonction prend comme argument. Cela peut être optimisé à O(sqrt(x)) avec le code suivant :
# sqrt.py
import math
def is_prime(x: int) -> bool:
if x in [0, 1]:
return False
for i in range(2, int(math.sqrt(x)) + 1):
if x % i == 0:
return False
return True
Ou même comme ceci, sans importer la bibliothèque math :
# sqrt.py
def is_prime(x: int) -> bool:
if x in [0, 1]:
return False
i = 2
while i**2 <= x:
if x % i == 0:
return False
i += 1
return True
Je serais d'accord avec l'une ou l'autre alternative.
J'ai omis les docstrings dans l'implémentation précédente pour plus de clarté dans les changements de code. Mais il serait bien d'inclure la complexité temporelle de la fonction dans la documentation pour les développeurs. Plus vous pouvez donner d'informations sur votre code, mieux ce sera pour vos membres d'équipe et pour vous-même.
Vous faites du bon travail jusqu'à présent. Continuons !
Le deuxième problème de l'exemple d'entretien
Jusqu'à présent, j'ai pu évaluer que vous avez les compétences collaboratives nécessaires pour vous attaquer à des tâches faciles avec l'équipe. Il est maintenant temps de compliquer un peu les choses.
Voici l'énoncé du deuxième problème que je vous proposerais :
"Étant donné deux nombres entiers
LetR, comptez combien de nombres entiers premiers se trouvent dans l'intervalle[L, R]."
Une fois de plus, je recommanderais de discuter des priorités que l'équipe a fixées concernant cette tâche. Commencez simplement et parcourez le processus d'amélioration d'une solution initiale. Soulignez que l'optimisation prématurée n'est pas une bonne pratique.
Discutez également de la possibilité d'utiliser la solution que vous avez implémentée pour la tâche précédente pour résoudre celle-ci. Il est logique que si nous avons une fonction qui indique si un entier est un nombre premier ou non, nous pouvons l'utiliser pour chaque nombre dans une plage.
Et c'est quelque chose que vous devrez faire dans la vie réelle. Habituellement, lorsqu'une nouvelle tâche apparaît, l'équipe doit examiner les projets maintenus pour voir ce qui peut être réutilisé afin d'accélérer le processus d'implémentation. Si vous ne faites pas cela, vous pourriez finir par coder des fonctionnalités dupliquées.
Cela dit, une bonne solution initiale pour cette tâche pourrait être quelque chose comme ceci :
# range_primes.py
from sqrt import is_prime
def count_primes(l: int, r: int) -> int:
primes = 0
for i in range(l, r + 1):
if is_prime(i):
primes += 1
return primes
Cela est parfaitement acceptable, et vous avez montré que vous pouvez réutiliser des fonctionnalités précédentes pour en construire de nouvelles. Comme dans le premier exemple, vous pourriez être tenté de montrer vos compétences en Python et d'écrire cette fonction comme ceci :
# range_primes.py
from sqrt import is_prime
def count_primes(l: int, r: int) -> int:
return sum(bool(is_prime(i)) for i in range(l, r + 1))
Je recommande de ne pas opter pour cette implémentation Pythonique comme première option. Laissez-la pour la discussion, évaluez la lisibilité du code et analysez peut-être les différences de performance. N'oubliez pas les docstrings !
La section suivante est celle où les choses deviennent intéressantes. Continuez à lire. Nous sommes à l'étape finale...
Le problème le plus difficile de l'exemple d'entretien
Vous souvenez-vous lorsque j'ai dit précédemment que les contraintes présentes sur les sites de programmation compétitive sont difficiles à mapper aux exigences de la vie réelle ?
Voici comment je vous présenterais un défi difficile pour déterminer ces contraintes et implémenter la meilleure solution possible :
"Supposons qu'un client souhaite que nous fournissions la fonctionnalité de calcul des nombres premiers dans une plage en tant que service. Ils veulent que l'accent soit mis sur la performance car ils prévoient d'utiliser ce service très souvent. Comment l'implémenteriez-vous ?"
Si vous vous êtes entraîné à vos compétences algorithmiques pour les entretiens, vous avez probablement résolu des problèmes similaires à celui-ci quelques fois. La principale différence ici est le changement de contexte.
Au lieu de vous donner des instructions précises, des contraintes numériques et des formats d'entrée ou de sortie, je vous ai donné une description plus large de la tâche. Et mon objectif ici est le même qu'avant : vous faire interagir avec moi comme si nous étions des coéquipiers cherchant à résoudre ce problème à partir des quelques informations que nous connaissons.
Espérons qu'après avoir échangé quelques questions et réponses, nous pourrons traduire l'énoncé précédent, plus ambigu, en quelque chose de beaucoup plus familier :
"Étant donné un ensemble de requêtes de la forme
[L, R], répondez, pour chaque requête, combien de nombres entiers à l'intérieur de cette plage sont premiers."
Et cela a du sens car le client voulait utiliser ce service très souvent, comme indiqué dans la description.
Nous voulons nous concentrer sur la performance. Cela devrait être notre principale préoccupation lors de l'implémentation de la solution. Mais toujours, la meilleure façon d'arriver à une solution optimale est de commencer par une solution simple, d'analyser si nous répondons à nos exigences de performance et de continuer à améliorer progressivement. Voyons l'exemple complet.
Nous pourrions commencer par utiliser la solution que nous avons implémentée à l'étape précédente. Est-elle suffisamment bonne ?
Supposons que la plage maximale de nombres que nous aurons comme argument de notre fonction est [1, 10^6]. De plus, réalistement, supposons que le nombre de requêtes que notre service répondra par minute est d'environ 10^5.
Notre solution précédente a une complexité temporelle de O(sqrt(n)) pour déterminer si un nombre est premier. Si nous devions faire cela pour chaque nombre dans la plage, la complexité passe à O(n * sqrt(n)). En plus de cela, si nous faisons cela pour chaque requête, nous finirons avec une complexité temporelle encore plus élevée de O(q * n * sqrt(n)).
Substituez les variables précédentes par les valeurs les plus élevées qu'elles peuvent avoir, et vous obtiendrez que cette solution prendra environ 10^14 opérations pour répondre à toutes les requêtes. En supposant qu'un ordinateur peut effectuer environ 10^8 opérations élémentaires par seconde, il faudra environ 10^6 secondes pour les compléter toutes.
Note : Convertissez 10^6 secondes en jours. Vous serez émerveillé 🤯.
Cette solution est irréalisable si l'objectif est de prioriser la performance de notre solution. Voyons comment nous pouvons l'améliorer.
À ce stade, ce que j'attendrais, c'est que vous sortiez le meilleur de l'entraînement que vous avez eu sur toutes ces plateformes en ligne, et que vous me montriez une solution impressionnante. C'est le moment de montrer toutes vos compétences analytiques et algorithmiques.
Mais seulement maintenant, parce que je sais que vous êtes un joueur d'équipe.
La solution finale
Puisque ceci est un entretien simulée, je suis très intéressé de connaître votre approche pour résoudre ce dernier problème efficacement. Faites-moi savoir comment vous implémenteriez la solution ou partagez votre code sur GitHub – ne soyez pas timide.
Je vous garantis une chose : si vous pouvez arriver à ce point dans les entretiens réels, il ne sera probablement pas trop important si vous ne connaissez pas la solution optimale à ce problème. Cela ne signifie pas que vous ne devriez pas faire de votre mieux pour le résoudre, mais soyez assuré que vous avez déjà fait un très bon travail.
C'est tout ! Maintenant, je veux voir votre code.
Mots de la fin
Dans cet article, j'ai essayé de résumer certains des aspects que je considère comme les plus importants dans les entretiens de codage. J'ai placé un accent particulier sur la partie collaborative car je pense que la plupart des gens sous-estiment l'importance de cette compétence. C'est un must, surtout si vous voulez travailler en équipe avec d'autres développeurs.
J'ai essayé de vous guider à travers un entretien simulée où j'ai expliqué le processus de réflexion que je suivrais face à une tâche de codage standard lors d'un entretien. J'espère que cet exercice a été utile et qu'il pourra vous aider lors de votre prochain entretien (en tant que candidat ou en tant qu'intervieweur).
Partagez vos réflexions sur cet entretien simulée, et commençons une discussion fructueuse.
À bientôt ! 👋
Sources
- Les exemples de code utilisés dans l'article peuvent être trouvés ici.
- Indice pour le dernier problème : implémentation de l'algorithme du Crible d'Ératosthène.
👋 Bonjour, je suis Alberto, développeur logiciel chez doWhile, programmeur compétitif, enseignant et passionné de fitness.
🤗 Si vous avez aimé cet article, envisagez de le partager.
🔗 Tous les liens | Twitter | LinkedIn