Acabo de conseguir un trabajo de desarrollador en Snapchat.

Hace aproximadamente un año, mientras estaba en Irak como oficial del ejército, comencé a programar por diversión. (Puedes leer la historia completa aquí). Bueno, después de estudiar mucho, conseguí mi primer trabajo como ingeniero de software en Snapchat (Snap) en Venice Beach.

La búsqueda de trabajo no fue fácil. Me enfrenté a muchos rechazos, pistas falsas y momentos de duda. Pero la experiencia me ayudó a desarrollar un marco mental para abordar actividades que tienen una alta probabilidad de éxito a largo plazo, pero una pequeña probabilidad de éxito en un día determinado, actividades como buscar un primer trabajo como ingeniero de software.

Porque encontrar mi trabajo en particular se debió principalmente a mucha suerte (buen momento, una conexión fortuita, un buen año de financiación para startups en Los Ángeles), describir los pasos específicos que tomé no sería muy útil para ti. Eso es porque hice las mismas cosas que todos te dicen que hagas:

  • construir proyectos paralelos
  • resolver problemas de práctica
  • construye tu red
  • y postula a un montón de trabajos

Las acciones que se toman, y el énfasis se pone en cada uno de ellos, difieren considerablemente en función de su personalidad y circunstancias específicas. Dicho esto, el marco mental al que llegué durante mi búsqueda de trabajo puede ayudarlo, independientemente de sus circunstancias.

Así que voy a compartir el proceso de pensamiento que finalmente condujo a mi marco mental, mientras les doy una rápida introducción a la programación dinámica al mismo tiempo. Espero que encuentres esto útil ?!

Cómo funciona una búsqueda de trabajo de desarrollador típica

Mientras buscaba mi primer trabajo de programación, leí bastantes relatos personales de cómo otros programadores autodidactas y graduados de bootcamp encontraron sus primeros trabajos. A partir de sus historias, la búsqueda de empleo parecía un modelo muy secuencial:

  1. aprender a codificar
  2. agudiza tus habilidades
  3. hacer algo de networking
  4. trabajar en problemas de práctica
  5. postularse a trabajos
  6. entrevista
  7. obtener ofertas de trabajo

En términos de estructura de datos, lo imaginé atravesando los nodos de una lista enlazada.

Creo que un defecto común cuando las personas relatan sus recuerdos (especialmente si han estado trabajando como ingenieros por un tiempo), es que ponen demasiado énfasis en las relaciones de causa-efecto entre las acciones específicas que tomaron y el resultado que ocurrió. :

Hice A, luego ocurrió B. Por tanto, A provocó que B.

Debido a que tienen el beneficio de la retrospectiva, su resultado parece determinista. Si solo sigue los mismos pasos, encontrará un buen trabajo.

Si. Y no. Según mi experiencia, a largo plazo , si estás realmente comprometido con la programación y te esfuerzas constantemente para mejorar, eventualmente encontrarás un trabajo digno de tus habilidades (independientemente de si tienes un título en Ciencias de la Computación de una determinada escuela en Palo Alto). La demanda de ingenieros de software es real y está creciendo. Pero a corto plazo , el proceso es súper aleatorio y se basa en muchas variables sobre las que no tiene visibilidad ni control: necesidades de contratación de la empresa, tendencias del mercado, para qué están contratando las empresas de tecnologías modernas.

Cuando comencé a buscar trabajo en Los Ángeles, envié un montón de solicitudes, tratando de encontrar algo.cualquier cosa. Hubiera codificado a cambio de comida gratis y camisetas si alguien me hubiera ofrecido la oportunidad. Estas son algunas de las primeras respuestas que obtuve:

Escribe un código Javascript limpio y agradable. Y fuiste súper amable y disfrutamos hablando contigo. Sin embargo, no lo vimos codificando de manera tan productiva como necesitábamos. Para avanzar con los candidatos junior, necesitamos ver un punto fuerte excepcional, y en este momento no vimos lo suficiente con ustedes . Esto significa que no podemos trabajar contigo. Todos pensamos muy bien de usted y cada uno disfrutó de entrevistarlo, con la firme convicción de que su motivación, ética de trabajo y curiosidad natural son exactamente lo que buscamos en un candidato. Desafortunadamente, dada la línea de tiempo de dónde nos encontramos logísticamente, estamos buscando a alguien con más experiencia actual en el desarrollo de front-end. Perdón por todos los retrasos. Este proceso es más complicado de lo que esperaba.Los actualizaré en algún momento de la próxima semana a medida que nos acerquemos a tomar una decisión.

Luego [silencio] durante muchas semanas.

Bueno, eso fue plátano. Hice un desafío de codificación que me llevó 6 horas y la empresa ni siquiera puede enviarme un correo electrónico de respuesta.

Recibir cada uno de estos correos electrónicos (y las numerosas no respuestas también) fue una experiencia muy dolorosa para mí. Pero nunca pierda la oportunidad de aprender algo útil de las dificultades . Al mostrarle el proceso de pensamiento que inspiró mi búsqueda de trabajo, espero que este artículo le brinde una herramienta para optimizar las decisiones que tome durante la búsqueda de trabajo y le dé inspiración para continuar avanzando hacia su objetivo.

"El dolor es inevitable, el sufrimiento es opcional" -Haruki Murakami

El problema de la mochila

Permítanme ilustrar los pasos que tomé para llegar a mi marco mental, usando una variación de una pregunta común en una entrevista de Ciencias de la Computación: el problema de la Mochila.

** ACTUALIZACIÓN: Puse mi código en un repositorio de GitHub con un pequeño conjunto de pruebas, lo que le permite jugar con el código y desarrollar una solución usted mismo. **

Aqui esta el problema:

Tiene un conjunto de actividades que puede elegir realizar para aumentar sus posibilidades de encontrar trabajo. Cada actividad requiere una cierta cantidad de tiempo, pero proporciona cierta cantidad de experiencia. Solo tenemos un tiempo limitado para prepararnos para la búsqueda de empleo, por lo que no podemos hacer todo. Nuestro objetivo es maximizar la cantidad de puntos de experiencia eligiendo el conjunto óptimo de actividades.

¿Cómo se escribe una función que elija el conjunto óptimo de actividades de una lista de actividades disponibles y una cantidad de tiempo limitada?

Solución 1: fuerza bruta

Reafirmando el problema, desea elegir el conjunto de actividades que:

  1. Se necesita una cantidad de tiempo para lograr que sea menor o igual al tiempo total que tiene disponible
  2. Maximiza los puntos de experiencia (XP) devueltos

La forma más intuitiva es utilizar el mismo algoritmo que utilizaríamos en la vida diaria. Probamos varias combinaciones de actividades, verificando si cumplía con nuestra restricción de ajuste dentro de un período de tiempo limitado. Seguiremos buscando en todas las combinaciones posibles y elegiremos la que maximice la XP.

Aquí está el código de este algoritmo:

El problema es que este enfoque es realmente complejo con respecto al tiempo, lo que significa que a medida que aumenta el tamaño de nuestra entrada (número de actividades que posiblemente podríamos elegir), la cantidad de tiempo que se necesita para calcular una solución aumenta a un ritmo mucho más rápido.

If we have 6 possible activities, we start by creating every possible combination with a single activity, giving us 6 combinations that contain one activity.

Then we have to create all possible combinations with 2 activities. For each of the original 6 combinations, we have to create a combination with each of the 5 remaining activities (you can only do each activity once).

Then to create all possible combinations with 3 activities, we have to take each of our combinations containing 2 activities and create a combination with each of the 4 remaining activities.

Eventually we’ll have something that looks like (6 * 5 * 4 *3 * 2 * 1), which is O(n!). Also, because we sum all the items in each combination every time to calculate the total time and XP, our end time complexity is O(n! * n).

Imagine that instead of running this algorithm on a computer that can execute trillions of operations a second, you have to run it on your limited brain, which actually takes 10 hours (in a very optimistic world) to do a side project to learn a new JavaScript MV* framework.

And also instead of a choice of 6 activities, you have thousands of possible things you could be doing to prepare for job search. (Just look up “how to code” on Google).

It is completely impractical to try every possible combination of activities to prepare yourself for job search. The lesson from this example is there is an almost infinite amount of things you could be doing that will increase your chances of finding a job, but you can’t try all of them. You need a better method to determine your optimal set of activities.

Backtracking

Obviously, as programmers (and hackers ?), we’re going to want to optimize our current solution somehow.

Let’s try the BUD approach from Cracking the Coding Interview by Gayle McDowell (an awesome prep resource, even if your job interviewers never ask algorithmic questions).

  1. What Bottlenecks does our brute force solution have?

When looking for the bottleneck, we’re usually trying to identify the most complex part of the process, i.e. the n! part of of our O(n! * n) algorithm.

The bottleneck, or most complex part of our job search problem is the fact that we have to dynamically create many different combinations and try them out. Every time we add another option, we have many more possible combinations to try out.

Now I have to admit I kind of led you down a false road. My job search problem, as a variation on the Knapsack Problem, is part of a set of problems called NP-Hard. In short, problems are NP-Hard when there is no known efficient way to solve the problem, or verify that that a solution to a problem is correct. So unless you’re a world changing computer scientist, you’re probably not going to figure out an objectively efficient way to combine all the activities.

But that’s ok!!! Sometimes, in interviews and job search, we follow false leads. As long as we learn something from the process, we haven’t really wasted time. Even if we can’t find an overall efficient way to solve the problem, we can still find a more efficient way that we’re currently using.

So let’s move on.

2. Is my algorithm doing Unnecessary work or Duplicated Work?

This is where we can make major gains on our solution.

One thing we should change is that for every possible combination, we have to iterate through all the activities in the set to calculate the total XP and total time from that set of activities. This is duplicated work, because we’re adding up the same values over and over.

If we just saved the total XP and time of the combination in a variable, we could just add the XP and time of each new activity we add to to the total. This would take our solution from O(n! * n) to O(n!).

This is helpful, but doesn’t fundamentally make our problem too much faster to run.

What other optimization could we do?

We’re also calculating a lot of combinations that could not possibly lead to a valid solution. This is unnecessary work.

For reference here is the list of activities again:

const ACTIVITIES = [ {name: 'side-project', time: 10, xp: 12}, {name: 'algorithms', time: 3, xp: 7}, {name: 'networking', time: 1, xp: 0.5}, {name: 'exercise', time: 2, xp: 1.5}, {name: 'systems design', time: 4, xp: 4}, {name: 'making CSS codepens', time: 3, xp: 4}];

Let’s say we have 8 hours total to prepare for our job search. How would our brute force algorithm check combinations?

Based on the order of the ACTIVITIES array, we would first consider a set just including the side-project object. There is no valid solution containing the side-project activity because it takes 10 hours to do and we only have 8 hours total.

But our brute force algorithm (being brute force) doesn’t know that, and will then check every possible combination we can create with side-project.

So it will check if [side-project, algorithms] is a valid solution. It is not.

And it will check if [side-project, algorithms, networking] is valid. It is not.

And it will check if [side-project, algorithms, networking, exercise] is valid. It is not.

See how much unnecessary work we’re doing?

What if we could give our algorithm a little bit of intelligence, so it can check if our current state (the activities we currently have selected) can lead to a valid solution? If the activities we currently have selected can lead to a valid solution (specifically, if our selected set of activities takes less or equal time than the total time we have as a parameter to the function) then we keep selecting new activities and checking if they’re valid.

If not, we stop and unselect the last activity we selected.

For example, if we have 8 hours total, we will first check to see if a combination containing just side-projects can possibly lead to a valid solution. As we determined before, it cannot, because it takes up more time than we currently have.

So we unselect side-projects, and try out different combinations starting with algorithms. By checking to see if our current selected activities could lead to a valid solution, we’re avoiding having to check any of the combinations containing side-projects, because they could not possible lead to a valid solution.

This approach is called backtracking. We check to see if where we are could lead to a valid solution, if not, we go back one step and try to make a different choice.

Here is the code:

This solution implements the two optimizations that we discussed earlier:

  1. Keeping track of total XP and time so we can calculate it in O(1) instead of summing the entire set every time in O(n)
  2. Checking whether our current set will lead to a valid solution before we recursively add a new item

While backtracking saves a lot of work it doesn’t really reduce the overall runtime complexity of our algorithm. It’s still O(n!), because we’re still recursively checking most possible combinations.

But implementing the backtracking algorithm has probably given you a clue on how to continue working on the problem. In the brute force solution, we had to assemble and check the entire combination for each possible combination. With backtracking, we get to check if the path we’re on will lead to a valid solution, before we assemble the entire combination.

Hmmmmm…..

Is there a way to consider only whether or not we should add another activity to our set? This would be a much easier problem than trying to create the entire combination at once. It would allow us to break up our hard problem (finding the optimal combination) to a series of smaller problems (deciding whether or not to add a single activity).

Dynamic Programming

Dynamic programming is a method where we can divide our big optimization problem (what combination of activities should I choose?) into a series of manageable decision problems (should I include this activity in my optimal solution or not?). We divide and conquer.

Dynamic programming is a common way to solve NP-Hard problems like the Knapsack problem, and coincidentally also a good way to think about job search. It’s hard to determine what combination of activities will make you ready for job search. There’s no efficient way to find the optimal combination or to check if your current choice is optimal.

But it’s a lot easier to break your time period down into individual days and weeks, and try to figure out which activities you should be doing for each small period of time.

To solve our job search problem using dynamic programming, we break the problem up into a series of smaller problems (how do I optimize a smaller period of time?) and then take the solution from each of the smaller problems and combine them into a larger solution.

Sounds confusing? Let’s walk through it:

const ACTIVITIES = [ {name: 'side-project', time: 10, xp: 12}, {name: 'algorithms', time: 3, xp: 7}, {name: 'networking', time: 1, xp: 0.5}, {name: 'exercise', time: 2, xp: 1.5}, {name: 'systems design', time: 4, xp: 4}, {name: 'making CSS codepens', time: 3, xp: 4}];

What’s the optimal solution if we have a total time of t=0 (zero) to prepare?

If we have zero time, we can’t do any activities, so return an empty set, [].

Ok, now what’s the optimal solution is we have a total time of t=1?

First, let’s see what activities are possible to do: we can’t do a side-project (time t=10) or study algorithms (time t=3). The only thing we can do is networking (time t=1).

So we need to decide if adding networking to the optimal solution for time t=0 will lead to an optimal solution.

If we add networking, we come out with a total XP of 0.5, not bad.

If we don’t add networking, we can’t really do anything else, so we come out with a total XP of 0.

0.5 is still better than 0, so if we only have a total time of t=1, we should do networking. The optimal solution for time t=1 is [networking]

What’s the optimal solution for time t=2?

What activities are possible with time t=2, that we haven’t already considered? Just exercise.

If we choose to add exercise, which takes time t=2, we no longer have any time to do anything else, so our solution is [exercise], which leads to 1.5 XP.

We compare the optimal solution including exercise (which leads to 1.5XP) and the optimal solution not including exercise (which leads to 0.5XP). Since the solution containing exercise is better, we choose that one (In real life, I also feel that with very limited time, some self-care is always more useful than more prep ?).

Now here is where it gets really interesting: What’sthe optimal solution for time t=3?

Again, what activities are possible for time t=3?

We have the option to choose from [algorithms, exercise, networking].

If we choose algorithms which takes time t=3, we have no time to do anything else, so one possible solution is [algorithms].

If we choose exercise which takes time t=2, we have t=1 time left to do something else? How do we know what to choose for the remaining time?

We know the optimal solution for time t=1, is [networking], so we don’t have to calculate it again. We know we can’t do better than the optimal solution for time t=1.

So one possible solution is [exercise, networking].

Again we compare all the possible solutions and see that the best we can do is [algorithms].

This is the basic structure of a dynamic programming solution: at each amount of time, we test the decision of whether or not to add a specific activity. We compare all possible solutions, and figure out the optimal one.

Solutions for greater amounts of time build upon the optimal solutions for the same problem with a smaller amount of time. This allows us to call the dynamic programming function recursively.

For my example I chose to sort the array of activities by the time it takes to complete them (least to greatest). This allows us the quickly determine which items are possible in the given time because they are sorted by time.

Below is the code:

Wooooo! If you made it through that example the first time, then you’re a way faster learner than I am. I hope it was an interesting in finding alternate ways to solve hard algorithmic questions!

Finally, what is the purpose of this series of three examples you might ask?

Not only did I stealthily give you some practice working on a question very similar to the ones you might be asked in technical interviews, I showed you the steps that I took to come to my mental framework.

There are an almost infinite combinations of activities you could be doing, and there’s no efficient way to determine the optimal set of activities you should do. A lot of paths don’t lead to a valid solution, just like a lot of job applications and interviews won’t lead to a job.

You could try every possible combination of job search activities (brute force), but since we are human beings with finite time, this isn’t an efficient way to arrive at our goal.

We could optimize our approach by evaluating at each step whether or not our approach will lead to our goal (backtracking). For example, if we are constantly reaching out to third-party recruiters to help us find job leads, and the recruiters haven’t been very helpful in generating interviews, maybe we should backtrack and consider a different activity.

Lastly, since job search is not a one day affair, we could try to optimize each day and combine days together (dynamic programming). This way we have a manageable problem to deal with (should I study algorithms today?) versus a really hard one (what should I do for the next month to prepare myself for job search?).

Finally, I just want to point out that with all 3 approaches, even though they were not objectively efficient, we did eventually reach a solution. While in the middle of job search, it’s really important to remember that in the long term, you will achieve your goal, and to keep pushing forward each day.

My advice for handling your developer job search

I’m going to succumb to my temptation to give you two pieces of advice from my experience.

  1. It’s super hard to judge your own performance during interviews and coding challenges — so just focus on the process. You won’t know during the interview or immediately afterward whether you’re doing well or poorly.
  2. Success or failure are fleeting and shouldn’t determine your happiness.

Si estás buscando tu primer trabajo como programador, espero que leer esto te haya resultado útil o al menos inspirador. Mira, ¡un tonto sin talento como yo encontró un gran trabajo! Buena suerte y me gustaría terminar compartiendo el mejor consejo que me dieron durante mi búsqueda de trabajo:

“No se preocupe si es lo suficientemente bueno, si le gusta programar y si está dispuesto a trabajar lo suficiente. Si haces esas dos cosas, lo lograrás ". - parafraseado de Edgar Pabon en el podcast Breaking Into Startups

Gracias por leer y buena suerte con tu búsqueda de empleo.