Μόλις πήρα μια δουλειά προγραμματιστή στο Snapchat.

Πριν από περίπου ένα χρόνο, ενώ εργαζόμουν στο Ιράκ ως αξιωματικός του στρατού, άρχισα να κωδικοποιώ για διασκέδαση. (Μπορείτε να διαβάσετε ολόκληρη την ιστορία εδώ.) Λοιπόν, μετά από πολλές σπουδές, προσγειώθηκα την πρώτη μου δουλειά ως μηχανικός λογισμικού στο Snapchat (Snap) στην παραλία της Βενετίας.

Η αναζήτηση εργασίας δεν ήταν εύκολη. Αντιμετώπισα πολλές απορρίψεις, ψευδείς οδηγούς και στιγμές αμφιβολίας. Αλλά η εμπειρία με βοήθησε να αναπτύξω ένα νοητικό πλαίσιο για την προσέγγιση δραστηριοτήτων που έχουν μεγάλη πιθανότητα επιτυχίας μακροπρόθεσμα, αλλά μια μικρή πιθανότητα επιτυχίας οποιαδήποτε δεδομένη ημέρα - δραστηριότητες όπως η αναζήτηση πρώτης εργασίας ως μηχανικός λογισμικού.

Επειδή η εύρεση της συγκεκριμένης δουλειάς μου οφείλεται κυρίως στην τύχη (καλή χρονική στιγμή, μια τυχαία σύνδεση, μια καλή χρονιά χρηματοδότησης για νεοσύστατες επιχειρήσεις στο Λος Άντζελες), περιγράφοντας τα συγκεκριμένα βήματα που πήρα δεν θα ήταν εξαιρετικά χρήσιμα για εσάς. Αυτό συμβαίνει επειδή έκανα τα ίδια πράγματα που όλοι σας λένε να κάνετε:

  • κατασκευή δευτερευόντων έργων
  • επίλυση προβλημάτων πρακτικής
  • χτίστε το δίκτυό σας
  • και υποβάλετε αίτηση για έναν τόνο θέσεων εργασίας

Οι ενέργειες που κάνετε και η έμφαση που δίνετε σε κάθε μία, θα διαφέρουν σε μεγάλο βαθμό ανάλογα με την προσωπικότητά σας και τις συγκεκριμένες περιστάσεις. Αυτό είπε, το νοητικό πλαίσιο στο οποίο έφτασα κατά τη διάρκεια της αναζήτησης εργασίας μου μπορεί να σας βοηθήσει, ανεξάρτητα από τις περιστάσεις σας.

Θα μοιραστώ λοιπόν τη διαδικασία σκέψης που τελικά οδηγεί στο νοητικό μου πλαίσιο, ενώ ταυτόχρονα σας δίνω μια γρήγορη εισαγωγή στον δυναμικό προγραμματισμό. Ελπίζω να το βρείτε χρήσιμο ;!

Πώς λειτουργεί μια τυπική αναζήτηση εργασίας προγραμματιστή

Ενώ έψαχνα για την πρώτη μου δουλειά προγραμματισμού, διάβασα αρκετούς προσωπικούς λογαριασμούς για το πώς άλλοι αυτοδίδακτοι προγραμματιστές και αποφοίτοι bootcamp βρήκαν τις πρώτες δουλειές τους. Από τις ιστορίες τους, η αναζήτηση εργασίας φαινόταν σαν ένα πολύ διαδοχικό μοντέλο:

  1. μάθετε να κωδικοποιείτε
  2. ακονίστε τις ικανότητές σας
  3. κάντε κάποια δικτύωση
  4. εργαστείτε για προβλήματα πρακτικής
  5. ισχύουν για θέσεις εργασίας
  6. συνέντευξη
  7. λάβετε προσφορές εργασίας

Με όρους δομής δεδομένων, το απεικόνισα ως διασχίζοντας κόμβους συνδεδεμένης λίστας.

Νομίζω ότι ένα κοινό ελάττωμα όταν οι άνθρωποι αφηγούνται τις αναμνήσεις τους (ειδικά αν εργάζονται ως μηχανικός για λίγο), είναι ότι δίνουν μεγάλη έμφαση στις σχέσεις αιτίου-αποτελέσματος μεταξύ των συγκεκριμένων ενεργειών που έλαβαν και του αποτελέσματος που συνέβη :

Έκανα το Α και μετά το Β συνέβη. Επομένως, το Α προκάλεσε το Β.

Επειδή έχουν το όφελος της εκ των υστέρων, το αποτέλεσμα τους φαίνεται ντετερμινιστικό. Εάν ακολουθήσετε μόνο τα ίδια βήματα, θα βρείτε καλή δουλειά.

Ναί. Και όχι. Από την εμπειρία μου, μακροπρόθεσμα , εάν είστε πραγματικά αφοσιωμένοι στον προγραμματισμό και πιέζετε συνεχώς τον εαυτό σας να βελτιωθεί, τελικά θα βρείτε μια εργασία που αξίζει τις δεξιότητές σας (ανεξάρτητα από το αν έχετε πτυχίο Πληροφορικής από ένα συγκεκριμένο σχολείο στο Πάλο Άλτο). Η ζήτηση για μηχανικούς λογισμικού είναι πραγματική και αυξάνεται μόνο. Ωστόσο, βραχυπρόθεσμα , η διαδικασία είναι εξαιρετικά τυχαία και βασίζεται σε πολλές μεταβλητές για τις οποίες δεν έχετε ορατότητα ή δεν ελέγχετε: ανάγκες προσλήψεων εταιρειών, τάσεις αγοράς, ποιες εταιρείες τεχνολογίας ισχίου προσλαμβάνουν επί του παρόντος.

Όταν άρχισα να ψάχνω για θέσεις εργασίας στο Λος Άντζελες, έστειλα έναν τόνο αιτήσεων, προσπαθώντας να βρω κάτι -Οτιδήποτε. Θα είχα κωδικοποιήσει σε αντάλλαγμα δωρεάν φαγητό και μπλουζάκια αν κάποιος μου είχε προσφέρει την ευκαιρία. Εδώ είναι μερικές από τις πρώτες απαντήσεις που έλαβα:

Γράφετε ωραίο καθαρό κώδικα Javascript. Και ήσασταν εξαιρετικά φιλικοί και απολαύσαμε να σας μιλήσουμε. Ωστόσο, δεν σας είδαμε να κωδικοποιείτε τόσο παραγωγικά όσο χρειαζόμασταν. Για να προχωρήσουμε με τους κατώτερους υποψηφίους, πρέπει να δούμε ένα εξαιρετικό δυνατό σημείο και δεν είδαμε τόσο μεγάλη δύναμη μαζί σας σε αυτό το σημείο. Αυτό σημαίνει ότι δεν μπορούμε να συνεργαστούμε μαζί σας. Όλοι πιστεύουμε πολύ από εσάς και ο καθένας απόλαυσε τη συνέντευξη σας, με την πεποίθηση ότι η οδήγησή σας, η ηθική εργασίας και η φυσική περιέργεια είναι ακριβώς αυτό που αναζητούμε σε έναν υποψήφιο. Δυστυχώς, δεδομένου του χρονοδιαγράμματος του πού είμαστε λογιστικά, αναζητούμε κάποιον με πιο πρόσφατη εμπειρία στην ανάπτυξη front-end. Συγγνώμη για όλες τις καθυστερήσεις. Αυτή η διαδικασία είναι πιο περίπλοκη από ό, τι περίμενα.Θα σας ενημερώσω κάποια στιγμή την επόμενη εβδομάδα καθώς πλησιάζουμε στη λήψη απόφασης.

Τότε [σιωπή] για πολλές εβδομάδες.

Λοιπόν αυτό ήταν μπανάνες. Έκανα μια πρόκληση κωδικοποίησης που μου πήρε 6 ώρες και η εταιρεία δεν μπορεί καν να μου στείλει ένα email απάντησης;

Η λήψη καθενός από αυτά τα μηνύματα ηλεκτρονικού ταχυδρομείου (και οι πολλές μη απαντήσεις) ήταν μια πολύ οδυνηρή εμπειρία για μένα. Αλλά μην χάνετε ποτέ την ευκαιρία να μάθετε κάτι χρήσιμο από τις δυσκολίες . Δείχνοντας σας τη διαδικασία σκέψης που ενέπνευσε η αναζήτηση εργασίας μου, ελπίζουμε ότι αυτό το άρθρο θα σας δώσει ένα εργαλείο για τη βελτιστοποίηση των επιλογών που κάνετε κατά την αναζήτηση εργασίας και θα σας δώσει έμπνευση για να συνεχίσετε να προωθείτε τον στόχο σας.

«Ο πόνος είναι αναπόφευκτος, ο πόνος είναι προαιρετικός» -Haruki Murakami

Το πρόβλημα του σακιδίου

Επιτρέψτε μου να παρουσιάσω τα βήματα που έκανα για να φτάσω στο διανοητικό μου πλαίσιο, χρησιμοποιώντας μια παραλλαγή μιας κοινής ερώτησης συνέντευξης στην Πληροφορική: το πρόβλημα του Knapsack

** ΕΝΗΜΕΡΩΣΗ: Έβαλα τον κωδικό μου σε ένα repo GitHub με μια μικρή δοκιμαστική σουίτα, επιτρέποντάς σας να παίξετε με τον κώδικα και να αναπτύξετε μια λύση μόνοι σας. **

Εδώ είναι το πρόβλημα:

Έχετε ένα σύνολο δραστηριοτήτων που μπορείτε να επιλέξετε να κάνετε για να αυξήσετε τις πιθανότητες εύρεσης εργασίας. Κάθε δραστηριότητα απαιτεί ένα συγκεκριμένο χρονικό διάστημα, αλλά παρέχει κάποια εμπειρία. Έχουμε περιορισμένο χρόνο μόνο για προετοιμασία για την αναζήτηση εργασίας, οπότε δεν μπορούμε να κάνουμε τα πάντα. Στόχος μας είναι να μεγιστοποιήσουμε τον αριθμό των πόντων εμπειρίας επιλέγοντας το βέλτιστο σύνολο δραστηριοτήτων.

Πώς γράφετε μια συνάρτηση που θα επιλέξει το βέλτιστο σύνολο δραστηριοτήτων από μια λίστα διαθέσιμων δραστηριοτήτων και ένα περιορισμένο χρονικό διάστημα;

Λύση 1: Brute Force

Restating the problem, you want to choose the set of activities that:

  1. Takes an amount of time to accomplish that is less than or equal to the total time you have available
  2. Maximizes experience points (XP) returned

The most intuitive way is to use the same algorithm we would utilize in daily life. We would try out various combinations of activities, checking to see if it met our constraint of fitting within a limited amount of time. We would keep searching through all possible combinations and choose the one that maximizes XP.

Here is the code for this algorithm:

The problem is that this approach is really complex with respect to time, meaning as the size of our input (number of activities we could possibly choose) increases, the amount of time it takes to calculate a solution increases at a much faster rate.

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.

Αν ψάχνετε για την πρώτη σας δουλειά προγραμματισμού, ελπίζω ότι η ανάγνωση ήταν χρήσιμη ή τουλάχιστον εμπνευσμένη για εσάς - κοίτα, ένα ταλέντο duffer σαν εμένα βρήκε μια εξαιρετική δουλειά! Καλή τύχη εκεί έξω και θα ήθελα να κλείσω κοινοποιώντας τις καλύτερες συμβουλές που μου δόθηκαν κατά την αναζήτηση εργασίας:

«Μην ανησυχείς για το αν είσαι αρκετά καλός, ανησυχείς για το αν σου αρέσει ο προγραμματισμός και αν είσαι πρόθυμος να δουλέψεις αρκετά σκληρά. Εάν κάνετε αυτά τα δύο πράγματα, θα τα καταφέρετε. " - παραφράστηκε από τον Edgar Pabon στο podcast Breaking Into Startups

Ευχαριστούμε που διαβάσατε και καλή τύχη με την αναζήτηση εργασίας σας!