Skip to content

Latest commit

 

History

History
2029 lines (1481 loc) · 75.2 KB

File metadata and controls

2029 lines (1481 loc) · 75.2 KB

#FACEBOOK PREP

TOP RESOURCE: GAYLE SLIDES

"Interviewers rarely try to test knowledge of anything but the most basic Computer Science concepts. Instead, they are evaluating your ability to break down a tricky problem and to solve problems using what you do know."

BEHAVIOR QUESTIONS

BEHAVIOR

HINT: BE HUMBLE, BE A GREAT TEAMMATE, BE GREAT ENGINEER, BE PASSIONATE, BE KNOWLEDGEABLE, BE CHALLENGEABLE
- SHOW YOU'RE SMART AND YOU CAN CODE

PITCH

HINT: Memorize your CV: career, studies, accomplishment, hobbies, projects and past work.
I'M A MECHATRONICS ENGINEER WITH A PHD IN IT&C WITH A DISSERTATION FOCUSED ON AI AND MULTI-ROBOT CONTROL. 
SINCE GRAD SCHOOL, I'VE BEEN DOING TECH FOR SOCIAL GOOD INCLUDING CIC.MX, A NON-PROFIT AWARDED BY ERIC
SCHMIDT'S NEW DIGITAL ERA GRANTS, WHICH WERE GIVEN TO THOSE PROMISING SOCIAL CAUSES WITH TECH BASIS. I'VE
DONE SEVERAL GEOREFERENCING PROJECTS CLOSE TO GOVERNMENT AND CIVIL SOCIETY ORGANIZATIONS, THOSE CONFORM MY 
STRONGEST BASE IN SQL AND POSTGRESQL DATABASES. THEN I LEFT CIC.MX TO BUILD A STARTUP WITH A COUPLE OF 
FRIENDS WHERE WE ARE BUILDING AN ALL-ACCESS API AND EASY TO READ DATA VISUALIZATIONS FROM OUR STATE-RUN
ELECTRICITY PROVIDER. WE'VE SUCCESSFULLY PULLED DATA FROM EVERY USER AND THE SYSTEM IS READY TO SCALE AS
ENGAGEMENT COMES. IN FACT THIS PROJECT MOTIVATED ME TO BUILD A MATERIAL-DESIGN BOILERPLATE, WHICH IS NOW
MY BIGGEST AND MOST PROMISING TECH ADVENTURE FOR EMPOWERING STARTUPS, ENTREPRENEURS AND NON-PROFITS DEV
TEAMS. THIS IS MOST OF MY TIME OUT OF WORK THAT IS STILL RELATED TO TECHNOLOGY, BESIDES, I'VE JUST 
BECOME A DAD !
* DEEP DIVE INTO DISSERTATION AS A HARD|COOL PROJECT (HASHTABLE & JSON BASED TRANSFERS AND ALLOCATIONS)
* DEEP DIVE INTO MBOILERPLATE AS A HARD|COOL PROJECT (NOSQL, CLOUD BASED, SCALABLE, EXTENDIBLE, LOW-COST: SHOW 
  HOW I LOVE TO BUILD STUFF FAST)
* THINK OF INCLUDING NIGHTS AND WEEKENDS: OPENCITYAPPS.MX, IPAB, EDUCTIVISM, EMULATED CDMA, NETWORK PALS...

PROCESS

1. GET INTERVIEW OUTFIT DRY CLEANED IF NECESSARY
2. REHEARSE STORIES FROM THE INTERVIEW PREP GRID
3. RE-READ 5 ALGORITH APPROACHES
4. CONTINUE TO PRACTICE UNTIL DAY OF INTERVIEW
5. REVIEW LIST OF TYPICAL MISTAKES
6. PRINT 10 COPIES OF MOST-UPDATED RESUME AND PUT THEM INTO FOLDER
7. AFTER: WRITE THANK YOU NOTE TO RECRUITER
DRESS CODE: KHAKIS, SLACKS OR NICE JEANS, POLO SHIRT OR DRESS SHIRT.

FACEBOOK

1. BE ENTREPRENEURIAL
2. ENHANCE A LOT MBOILERPLATE, SHOW HOW I LOVE TO BUILD STUFF FAST.
3. DEMONSTRATE "NINJA-SKILLS": 
	3.1 HACK TOGETHER ELEGANT AND SCALABLE SOLUTIONS
	3.2 SHOW HOW YOU GET THINGS DONE
4. SHOW PASSION FOR AQUILA AND THE HARDWARE + SOFTWARE + HUMANITIES CROSSROADS

SAMPLE QUESTIONS

1. Tell Me About a Time When You Gave a Presentation to a Group of People Who Disagreed with You
2. Tell Me About the Biggest Mistake You Made on a Past Project
3. Tell Me About a Time When You Had to Deal with a Teammate Who Was Underperforming
4. Tell Me About a Time When You had to Make a Controversial Decision
5. Tell Me About a Time When You had to Use Emotional Intelligence to Lead

TECHNICAL QUESTIONS

SKIR:

Scope the problem, Key clues and components, Issues, Repairs

DRIVE:

Lead the process and be open about issues, Do pseudocode before code

TEAMWORK:

Tweak, be open to feedback

PRACTICE:

Interview questions, coding on whiteboard, algorithms, design patterns

<span style="padding:5px; border-radius:4px; background-color:#7EB932; color:white;padding-left:20px; padding-right:40px"">KNOWLEDGE QUESTIONS

HINT: REVIEW CS FUNDAMENTALS AND KEY CONCEPTS
	
- Think on how-to SQL indexing, how-to design patterns, how-to webapp2 

MEMORY (STACK VS HEAP)
**The Stack**: *global and local scope variables*
    What is the stack? 
    It's a special region of your computer's memory that stores temporary 
    variables created by each function (including the main() function). The 
    stack is a "FILO" (first in, last out) data structure, that is managed and 
    optimized by the CPU quite closely. Every time a function declares a new 
    variable, it is "pushed" onto the stack. Then every time a function exits, 
    all of the variables pushed onto the stack by that function, are freed (that 
    is to say, they are deleted). Once a stack variable is freed, that region of 
    memory becomes available for other stack variables.
	
    The advantage of using the stack to store variables, is that memory is
    managed for you. You don't have to allocate memory by hand, or free it once 
    you don't need it any more. What's more, because the CPU organizes stack 
    memory so efficiently, reading from and writing to stack variables is very 
    fast.
	
    To summarize the stack:

		- the stack grows and shrinks as functions push and pop local variables
		- there is no need to manage the memory yourself, variables are 
		  allocated and freed automatically
		- the stack has size limits
		- stack variables only exist while the function that created them, is 
		  running
		  
	Pros and cons:
	
		  very fast access
		  don't have to explicitly de-allocate variables
		  space is managed efficiently by CPU, memory will not become fragmented
		  local variables only
		  limit on stack size (OS-dependent)
		  variables cannot be resized

The Heap: global scope variables making use of pointers ( * )

	What is the heap? 
	The heap is a region of your computer's memory that is not managed 
	automatically for you, and is not as tightly managed by the CPU. It is a 
	more free-floating region of memory (and is larger). To allocate memory on 
	the heap, you must use malloc() or calloc(), which are built-in C 
	functions. Once you have allocated memory on the heap, you are responsible 
	for using free() to deallocate that memory once you don't need it any more. 
	If you fail to do this, your program will have what is known as a memory 
	leak. That is, memory on the heap will still be set aside (and won't be 
	available to other processes). As we will see in the debugging sectio, 
	there is a tool called valgrind that can help you detect memory leaks.
	
	Unlike the stack, the heap does not have size restrictions on variable size 
	(apart from the obvious physical limitations of your computer). Heap memory 
	is slightly slower to be read from and written to, because one has to use 
	pointers to access memory on the heap.
	
	Unlike the stack, variables created on the heap are accessible by any 
	function, anywhere in your program. Heap variables are essentially global 
	in scope.
	
	Pros and cons:
		variables can be accessed globally
		no limit on memory size
		(relatively) slower access
		no guaranteed efficient use of space, memory may become fragmented over time as blocks of memory are allocated, 
		then freed
		you must manage memory (you're in charge of allocating and freeing variables)
		variables can be resized using realloc()

Memory management in Python

	Memory management in Python involves a private heap containing all Python objects and data structures. 
	The management of this private heap is ensured internally by the Python memory manager. The Python memory manager 
	has different components which deal with various dynamic storage management aspects, like sharing, segmentation,
	preallocation or caching.
	
	It is important to understand that the management of the Python heap is performed by the interpreter itself and that 
	the user has no control over it, even if she regularly manipulates object pointers to memory blocks inside that 
	heap. The allocation of heap space for Python objects and other internal buffers is performed on demand by the 
	Python memory manager through the Python/C API functions.
	
	locals() & gloabls() built-in functions output a dictionary of all in-memory variables	

BIG-O


BIT MANIPULATION


	Implementation:
		 N = int('10000000000',2) 
		 M = int('10011',2) 
		 binary_m = bin(M)  #str in the form '0b10011'
	
	x << y
	Returns x with the bits shifted to the left by y places (and new bits on the right-hand-side are zeros).This is the same as multiplying x by 2**y.

	x >> y
	Returns x with the bits shifted to the right by y places. This is the same as dividing x by 2**y.

	x & y
	Does a "bitwise and". Each bit of the output is 1 if the corresponding bit of x AND of y is 1, otherwise it's 0.

	x | y
	Does a "bitwise or". Each bit of the output is 0 if the corresponding bit of x AND of y is 0, otherwise it's 1.

	~ x
	Returns the complement of x - the number you get by switching each 1 for a 0 and each 0 for a 1. This is the same as -x - 1.

	x ^ y
	Does a "bitwise exclusive or". Each bit of the output is the same as the corresponding bit in x if that bit in y is 0, and it's the complement of the bit in x if that bit in y is 1.

POWER OF 2


	2^7 = 128 
	2^10 = ~1000 (1KB)
	2^20 = ~1000000 (1MB)
	2^30 = ~1000000000 (1GB)
	2^40 = ~1000000000000 (1TB)

RECURSION AND MEMOIZATION / DYNAMIC PROGRAMMING


**Typical use:**
    When there's a problem that can be built off of sub-problems. 
    
    All recursive code can be implemented iteratively, although sometimes the code to do so is much more complex. Before diving into recursive code, ask yourself how hard it would be to implement it iteratively, and discuss the trade-offs with your interviewer. 
    
    Hints for recursion:
    
    "Design an algorithm to compute the nth..."
    
    "Write code to list the first n"..."
    
    "Implement a method to compute all.. ."
    
    EXAMPLE: 
    
    def hanoi(n, source, helper, target):
		print "hanoi( ", n, source, helper, target, " called"
		if n > 0:
    		# move tower of size n - 1 to helper:
    		hanoi(n - 1, source, target, helper)
    		# move disk from source peg to target peg
    		if source[0]:
        		disk = source[0].pop()
        		print "moving " + str(disk) + " from " + source[1] + " to " + target[1]
        		target[0].append(disk)
    		# move tower of size n-1 from helper to target
    		hanoi(n - 1, helper, source, target)
    		
    source = ([4,3,2,1], "source")
	target = ([], "target")
	helper = ([], "helper")
	print source, helper, target
	hanoi(len(source[0]),source,helper,target)
	print source, helper, target

How-to Bottom-Up Recursion:

    1. Solve a simple case for single element.
    
    2. Extend previous solution for two elements.
    
    3. Then extend for three elements and so on. Typically fourth case is enough.
    
    The key here is to think about how you can build the solution for one case 
    off of the previous case.

How-to Top-Down Recursion:

	1. Think how to divide proble for case N into subproblems.
	
	Be careful of overlap between the cases.

Slow recursive

	def fib(n):
		return n if n < 2 else fib(n-2) + fib(n-1)

Memoized

Memoisation is a technique used in computing to speed up programs. This is accomplished by memorizing the 
	calculation results of processed input such as the results of function calls. If the same input or a function call 
	with the same parameters is used, the previously stored results can be used again and unnecessary calculation are 
	avoided. In many cases a simple array is used for storing the results, but lots of other structures can be used as 
	well, such as associative arrays, called hashes in Perl or dictionaries in Python.

	def memoize(f):
		memo = {}
		def helper(x):
    		if x not in memo:            
        		memo[x] = f(x)
    		return memo[x]
		return helper

	@memoize
	def fib(n):
		return n if n < 2 else fib(n-2) + fib(n-1)

MATH AND PROBABILITY


**Hints**
	1. Be careful with the difference in precision between floats and doubles.
	2. Don't assume that a value (such as the slope of a line) is an int unless you've been told so.
	3. Unless otherwise specified, do not assume that events are independent (or mutually exclusive). You should be careful, therefore, of blindly multiplying or adding probabilities.

Prime numbers

Every positive integer can be decomposed into a product of primes. Thus, in order for a number x to divide a number y (written x\y, or mod(y, x) = 0), all primes in x's prime factorization must be in y's prime factorization.
	
A list of primes a.k.a. The Sieve of Eratosthenes in Python:
	
	def sieveOfEratosthenes(n):
		"""return the list of the primes < n."""
		
		if n <= 2:
    		return []
		sieve = range(3, n, 2)
		top = len(sieve)
		for si in sieve:
    		if si:
        		bottom = (si*si - 3) // 2
        		if bottom >= top:
            		break
        		sieve[bottom::si] = [0] * -((bottom - top) // si)
		return [2] + [el for el in sieve if el]

Probability

A conditional 'intersection' of probabilities is calculated as the product of two or more probabilities (logical AND). Example given 1 to 10 (inclusive):
	
		P(A and B)=P(A)*P(B)
		P(x is even and x <= 5)
		= P(x is even given x <= 5) P(x <= 5) = (2/5) * (1/2)
		= 1/5
	
	
A conditional 'join' of probabilities is calculated as the sum of two or more probabilities (logical OR) minus the intersection. Example given 1 to 10 (inclusive):
		
		P(A or B)=P(A)+P(B)-P(A and B)
		P(x is even or x <=5)
		= P(x is even) + P(x <= 5) - P(x is even and x <= 5)
		= (1/2) + (1/2) - (1/5)
		= 4/5
	
Remember: 
	 - independence means P(A and B)= P(A)*P(B).
	 - mutual exclusivity means P(A or B)=P(A)+P(B), since P(A and B)=0.	

Permutations: (order does matter)

In mathematics, the notion of permutation relates to the act of arranging all the members of a set into some sequence or order, or if the set is already ordered, rearranging (reordering) its elements, a process called permuting.
	
	# (n)!/(n-r)! for n elements and r selections
	permutations('ABCD', 2)	 	
	AB AC AD BA BC BD CA CB CD DA DB DC

Combinations: (order doesn't matter).

Combinations refer to the combination of n things taken k at a time without repetition. To refer to combinations in which repetition is allowed, the terms k-selection, k-multiset, or k-combination with repetition are often used.

	#(n)!/(r!(n-r)!) for n elements and r selections
	combinations('ABCD', 2)	 	
	AB AC AD BC BD CD
	
	#(n+r-1)!/(r!(n-1)!) for n elements and r selections
	combinations_with_replacement('ABCD', 2) 
	AA AB AC AD BB BC BD CC CD DD

OBJECT ORIENTED DESIGN (REAL-LIFE OBJECTS)


The implementation of classes and methods to sketch out technical problems or real-life objects.

How-to approach OOD

	STEP 1. Handle ambiguity
		
			Ask for clarifications, refer any possible assumption.
			Who's gonna use? How's gonna be used? 
			Most detailed perspective should ask for who, what, where, when, how and why.
	
	STEP 2. Define the core objects
	
			Just after having a clear picture of what we're designing, lets list the core objects that conform our modeled solution.
			
	STEP 3. Analyze relationships
	
			Having more or less decided on our core objects, we now want to analyze the relationships between the objects. 
			Which objects are members of which other objects? 
			Do any objects inherit from any others? 
			Are relationships many-to-many or one-to-many?
	
	STEP 4. Investigate actions
	
			At this point, you should have the basic outline of your object-oriented design. What remains is to consider the key actions that the objects will take and how they relate to each other. You may find that you have forgotten some objects, and you will need to update your design.
			
	STEP 5. Shall we use any Design Pattern ?

Python class pattern

	class Progression(object):
		""" <CLASS DOCUMENTATION> Iterator producing a generic progression.
		Default iterator produces the whole numbers 0, 1, 2, ...
		"""

		def __init__(self, start=0):
			"""<CONSTRUCTOR> Initialize current to the first value of the progression."""
			self._current = start

		def _advance(self):
			""" <INTERNAL METHOD WITH NO RETURN> Update self. current to a new value.
			This should be overridden by a subclass to customize progression.
			By convention, if current is set to None, this designates the
			end of a finite progression.
			"""
			self._current += 1

		def __next__(self):
			""" <INTERNAL METHOD WITH RETURN> Return the next element, or else raise StopIteration error."""
			if self._current is None: # our convention to end a progression
				raise StopIteration( )
			else:
				answer = self._current # record current value to return
				self._advance( ) # advance to prepare for next time
			return answer # return the answer

		def __iter__(self):
			""" <CONVENTIONAL METHOD> By convention, an iterator must return itself as an iterator."""
			return self

		def __str__(self):
			""" <CONVENTIONAL METHOD> By convention, reduce string representation of vector."""
			return "'" + self + "'"

		def print_progression(self, n):
			""" <OUTPUT METHOD> Print next n values of the progression."""
			print(' '.join(str(self.__next__()) for j in range(n)))

	class ArithmeticProgression(Progression): # inherits from Progression
		"""Iterator producing an arithmetic progression."""

		def __init__(self, increment=1, start=0):
			"""<INHERITANCE> Create a new arithmetic progression.
			increment the fixed constant to add to each term (default 1)
			start the first term of the progression (default 0)
			"""
			super(ArithmeticProgression, self).__init__(start) # initialize base class
			self._increment = increment	

		def _advance(self): # override inherited version
			"""Update current value by adding the fixed increment."""
			self._current += self._increment
			
			
	def main():
		print( "Default progression:" )
		Progression().print_progression(10)
		Progression().print_progression(8)
		print( "Arithmetic progression with increment 5:" )
		ArithmeticProgression(5).print_progression(10)
		print( "Arithmetic progression with increment 5 and start 2:" )
		ArithmeticProgression(5, 2).print_progression(10)

	if __name__ == '__main__':
		main()

Singleton design pattern

	The Singleton pattern ensures that a class has only one instance and ensures access to the instance through the application without instantiating it again.
	
	# the python metaclass approach
	------
	
	class Singleton(type):
		def __call__(cls, *args, **kwargs):
    		try:
        		return cls.__instance
    		except AttributeError:
        		cls.__instance = super(Singleton, cls).__call__(*args, **kwargs)
        		return cls.__instance

	class MySingleton(object):
		__metaclass__ = Singleton

Factory design pattern

	The Factory pattern deals with the problem of creating objects without specifying the exact class of the object to be created. You give a parameter in order to know which class to instantiate.

	class A(object):
		def __init__(self):
		self.a = "Hello"

	class B(object):
		def __init__(self):
		self.a = " World"

	myfactory = {
		"greeting" : A, 
		"subject" : B,
	}

	>>> print myfactory["greeting"]().a
	Hello

THREADS AND LOCKS


In computer science, a thread is used to run processes concurrently, meaning 2 or more sequences of programmed instructions could be running at the same time, sharing memory and the values of its variables at any given moment.
	Coordinating threads demands for additionals methods that ensure the concurrency won't affect the final desired result. Here come synchronization concepts such as: lock, rlock, condition, semaphore, events and queues.
	
		1. First of all, threading in python is through the threading.py standard module using the method .start(), i.e.:
		
			import threading
			class FetchUrls(threading.Thread):
				"""
					Thread checking URLs.
				"""
				
				def __init__(self, urls, output):
    				"""
    					Constructor.

    					@param urls list of urls to check
   						@param output file to write urls output
    				"""
    				threading.Thread.__init__(self)
    				self.urls = urls
    				self.output = output

				def run(self):
    				"""
    					Thread run method. Check URLs one by one.
    				"""
    				while self.urls:
        				url = self.urls.pop()
        				req = urllib2.Request(url)
        				try:
            				d = urllib2.urlopen(req)
        				except urllib2.URLError, e:
            				print 'URL %s failed: %s' % (url, e.reason)
        				self.output.write(d.read())
        				print 'write done by %s' % self.name
        				print 'URL %s fetched by %s' % (url, self.name)
				
		2. Lock, used to enforce mutual exclusion and concurrency control to avoid a mess if doing same action like writing the same file, i.e.:
		
			class FetchUrls(threading.Thread):
				...

				def __init__(self, urls, output, lock):
   					...
    				self.lock = lock

				def run(self):
    				...
    				while self.urls:
        				...
        				self.lock.acquire()
        				print 'lock acquired by %s' % self.name
        				self.output.write(d.read())
        				print 'write done by %s' % self.name
        				print 'lock released by %s' % self.name
        				self.lock.release()
        				...

DATABASES


SQL Normalization: database that reduces data duplicity.
SQL Denormalization: databse that looks for highly scalable systems and faster queries (less joins).

NoSQL Leverages continual change in place, and speed of deployment.

DESIGN (recall How-to approach OOD)

SMALL DATABASE
			
		1. Handle ambiguity, find the level of detail or how general you should design.
		2. Define core objects, be aware that this information is typically the one that is redundant in denormalized systems.
		3. Analyze relationships, outline core objects and their relations including one-to-many, one-to-one, many-to-many.
		4. Investigate actions, define the most typical queries needed and re-structure if necessary.
		
		
LARGE DATABASE
	
		1. Follow the previous process but be redundant with most needed columns, denormalize your design. 
		2. Don't be scared about duplicates, they're better than slow systems.

INDEXES

A database index is a data structure that improves the speed of data retrieval operations on a database table at the cost of additional writes and storage space to maintain the index data structure.
	
Indexes are special lookup tables that the database search engine can use to speed up data retrieval. Simply put, an index is a pointer to data in a table. An index in a database is very similar to an index in the back of a book.
	
Typical use implies trees or dictionaries with unique keys (B+Trees) that endup being bitmaps that identify data in a simpler representation.

SHARDING

A database shard is a horizontal partition of data in a database or search engine. Each individual partition is referred to as a shard or database shard. Each shard is held on a separate database server instance, to spread load. 
	
Pros: reduced index size, which generally improves search performance. 
	
Cons: heavier reliance on the interconnect between servers, increased latency when querying, issues of consistency, more complex failover and backups.

NoSQL

Focuses on large-volume and semi-structured data. Alleviates the problems that RDBMS imposes and makes it easy to work with large sparse data, but in turn takes away the power of transactional integrity and flexible indexing and querying.

MongoDB 4th most popular in the world, pretty much as Google's Datastore NDB, PostgreSQL and Cassandra. Leverage JSON or Binary-JSON (and even YAML and XML) structures instead of traditional relational databases (RDBMS). Useful in big data and real-time web apps and really attractive for programming modern web applications, which are subject to continual change in place, and speed of deployment is an important issue.

Google's Datastore NDB: Schemaless access, with SQL-like querying.
	
Classes:
	
	Column - JSONs with fixed properties: name, value and timestamp
		
	Document - most popular, flexible, designed to offer a richer experience with modern programming techniques
		
	Key-value - associative arrays or tuples
		
	Graph - semantic queries and graph structures.
		
	Multi-model - may use graph, relational and key-value models.

TASK QUEUES


Tasks queues are an efficient and powerful tool for background processing; they allow your application to define tasks, add them to a queue, and then use the queue to process them in aggregate.
Taskqueue lets applications to perform work outside of a user request, initiated by a user request. If an app needs to execute some background work, it can use the Taskqueues to organize that work into small, discrete units, called tasks. The app adds tasks to taskqueues to be executed later.

WEBAPP2 (Python WSGI)


A lightweight framework that allows you quickly build simple web applications for the Python 2.7 runtime. webapp2 is compatible with the WSGI standard for Python web applications.
WSGI is a specification for a standardized interface between Web servers and Python Web frameworks/applications. The goal is to provide a relatively simple yet comprehensive interface capable of supporting all interactions between a Web server and a Web framework.

Pros:
	- I no longer have to worry about making my application run in multiple Web servers: most servers, and all frameworks, are WSGI compliant.
	- WSGI is Python's Answer to Ruby On Rails

DESIGN AND SCALABILITY [ARCHITECTURE DESIGN SKILLS]

HINT: WHAT WHOULD YOU DO AT WORK ? BE REAL: GIVE AWAY THE WEBAPP, DATASTORE AND GOOGLE CLOUD SKILLS.

- Know Twitter, Facebook, Google, Amazon design architectures and why's.

FOLLOW-THROUGH
1. SCOPE THE PROBLEM
	1.1 IDENTIFY ALL POSSIBLE FEATURES AND FUNCTIONALITIES OF THE SYSTEM
2. STRUCTURE THE ARCHITECTURE
	2.1 USE WHITEBOARD TO DRAW ALL YOUR PIECES: FRONT-END, ROUTES, HANDLERS, AND MODELS
3. IDENTIFY KEY ISSUES
	3.1 IS YOUR APP WIRTE-HEAVY OR READ-HEAVY ? WHAT DOES THIS MEANS?
	3.2 HAVE YOU DETECTED BOTTLENECKS? 
	3.3 SHOULD WE DISTRIBUTE THE DATABASE?
4. RESOLVE THE ISSUES
	4.1 THIS IS A GREAT MOMENT TO ASK FOR FEEDBACK

MODELING


Explains the best practices from twitter, amazon, google and facebook architectures.

Twitter

.

Amazon

.

Google

.

Facebook

.

SCALABILITY & MEMORY LIMITS


**STEP BY STEP APPROACH**
	1. MAKE BELIEVE. 
		Pretend that the data can all fit on one machine and there are no memory limitations. 
		How would you solve the problem? 
		This answer to this question will provide the general outline for your solution.
	2. GET REAL.
		 How much data can you fit on one machine, and what problems will occur when you split (shard) the data up? 
		 Common problems include figuring out how to logically divide the data up, 
		 and how one machine would identify where to look up a different piece of data.
	3. SOLVE PROBLEMS.
		Finally, think about how to solve the issues you identified in Step 2. 
		Remember that the solution for each issue might be to actually remove the issue entirely, 
		or it might be to simply mitigate the issue

HINTS FOR SHARDING

	* By Order of Appearance: advantage of never using more machines than are necessary. However, depending on the problem and our data set, our lookup table may be more complex and potentially very large.		
	
	* By Hash Value: : (1) pick some sort of key relating to the data, (2) hash the key, (3) mod the hash value by the number of machines, and (4) store the data on the machine with that value. That is, the data is stored on machine #[mod(hash(key), N)].
	
	* By Actual Value: we may be able to reduce system latency by using information about what the data represents, for example geographic divisions.
	
	* Arbitrarily: Frequently, data just gets arbitrarily broken up and we implement a lookup table to identify which machine holds a piece of data. While this does necessitate a potentially large lookup table, it simplifies some aspects of system design and can enable us to do better load balancing.

SORTING AND SEARCHING


Understanding the common sorting and searching algorithms is incredibly valuable, as many of sorting and searching problems are tweaks of the well-known algorithms. A good approach is therefore to run through the different sorting algorithms and see if one applies particularly well.

SORTING

The most commonly used in interviews are:

* Mergesort
* Quicksort
* Bucket sort

Python sort and sorted methods use mergesort algorithms, thus being quasilinear in complexity.

SEARCHING

Master the following algorithms:

* Binary search
* Breadth first search (BFS)
* Depth first search (DFS)
* Binary tree traversal

UNIT TESTING


Unit testing is a software development process in which the smallest testable parts of an application, called units, are individually and independently scrutinized for proper operation.
It implies units associated with control data, usage procedures, and operating procedures, to be tested to determine whether they are fit for use.

In Python, there is the standard module unittest.py, i.e.:

	import unittest

	class TestUM(unittest.TestCase):

		def setUp(self):
    		pass

		def test_numbers_3_4(self):
    		self.assertEqual( 3*4, 12)

		def test_strings_a_3(self):
    		self.assertEqual( 'a'*3, 'aaa')

		def test_split(self):
  			s = 'hello world'
  			self.assertEqual(s.split(), ['hello', 'world'])
  			# check that s.split fails when the separator is not a string
  			with self.assertRaises(TypeError):
    			s.split(2)

	if __name__ == '__main__':
		unittest.main()

GOOGLE DATASTORE AND APPENGINE SCALABILITY


As previously referred, Google's Datastore is a managed, NoSQL, schemaless database for storing non-relational data. Cloud Datastore automatically scales as you need it and supports transactions as well as robust, SQL-like queries. It runs in the available instance of a given application.
App Engine applications are powered by any number of dynamic instances at a given time, depending on the volume of incoming requests. As requests for your application increase, so do the number of dynamic instances.

The App Engine scheduler decides whether to serve each new request with an existing instance (either one that is idle or accepts concurrent requests), put the request in a pending request queue, or start a new instance for that request. The decision takes into account the number of available instances, how quickly your application has been serving requests (its latency), and how long it takes to spin up a new instance.

Additionally, datastore scales by sharding rows onto separate tablet servers, and these rows are lexicographically ordered by key.

ALGORITHMS AND PROBLEM SOLVING [CODING SKILLS]

HINT: SHOW ANALYTICAL SKILLS, KEEP TRYING, COMMUNICATE YOUR THINKING, MAKE TRADEOFFS

- Master Big-O (Complexity), Trees and Graphs, MergeSort, BFS & DFS, Binary Search, Heaps, Recursion
- Python DS & Modules: List, Set, Dictionary, Itertools, Random, Collections, Heapq, Time; Functional Programming

FOLLOW-THROUGH
1. LISTEN
	1.1 PATTERN MATCHING: WHAT PROBLEM IS THIS SIMILAR TO ?
	1.2 GET CLUES
2. EXAMPLE
3. BRUTE FORCE
4. OPTIMIZE
	4.1 (B)OTTLENECKS
	4.2 (U)NNECESSARY WORK 
	4.3 (D)UPLICATE WORK
5. EXPLAIN|WALKTHROUGH
6. IMPLEMENT|BEAUTIFY CODE: METHODS AND CLASSES
	6.1 CORRECT CODE
	6.2 EFFICIENT: BIG-O PERFORMANCE, METHODS RE-USE
	6.3 SIMPLE AND MODULAR
	6.4 READABLE
	6.5 MAINTAINABLE
	6.6 BALANCED USE OF DATA STRUCTURES
7. TEST
	7.1 ANALYZE: LOOKS WEIRD ? ERROR HOT SPOTS?
	7.2 TEST CASES: SMALL, EDGE, BIGGER

FIVE ALGORITHM APPROACHES


**1. EXAMPLIFY**
	Write out specific examples until finding|deriving a rule of simplification or formula. Think of x equations (examples) and y variables.

2. PATTERN MATCHING

	Think of familiarity and experience with past problems and modify solution in mind to the related problem. Most of the times a clue is found while carefully listening the problem description. Example: Clock angle.

3. SIMPLIFY & GENERALIZE

	This is a multi-step approach useful when constraints are given. First we simplify a constraint such as the amount of data or memory, and solve for this simpler problem. Once we had it, we adapt the earlier solution for the more complex version. Example: Magazine words

4. BASE CASE AND BUILD

	This is a useful approach when you find a clue for recursion. Try first to solve for n=1, then n=1 and n=2, and it's very possible for you to find a recursive approach when solved to n=4. Example: Permutations

5. DATA STRUCTURE BRAINSTORM

	It's hacky and works. Use different data structures as examples and try to apply each one. You'll be surprised of how easy is to find the best data structure for your problem. Example: Median in growing array.

DATA STRUCTURES


* LIST
*Description:*

	The list type is a container that holds a number of other objects,
	in a given order. The list type implements the sequence protocol,
	and also allows you to add and remove objects from the sequence.

*Implementation:*

	l = [1,2,3]
	l = [1,'word',3]
	l[0] = 1	   		

*Operations:*

	Operation     | Example      | Complexity       | Notes
	--------------+--------------+---------------+-------------------------------
	Index         | l[i]         | O(1)	         |
	Store         | l[i] = 0     | O(1)	         |
	Length        | len(l)       | O(1)	         |
	Append        | l.append(5)  | O(1)	         |
	Pop	          | l.pop()      | O(1)          | list as a stack (LIFO), same as l.pop(-1)
          		  | deque(l).popleft()           | list as a queue using collections.deque (FIFO)
	Clear         | l.clear()    | O(1)	         | similar to l = []

	Slice         | l[a:b]       | O(b-a)	     | l[1:5]:O(l)/l[:]:O(len(l)-0)=O(N)
	Extend        | l.extend(...)| O(len(...))   | depends only on len of extension
	Construction  | list(...)    | O(len(...))   | depends on length of argument

	check ==, !=  | l1 == l2     | O(N)          | O(len(string)) applies for strings
	Insert        | l[a:b] = ... | O(N)          |
          		  | l.insert(i,e)| O(N)	         | insert element e at zero-based index i
	Delete        | del l[i]     | O(N)	         | 
	Remove        | l.remove(...)| O(N)	         | 
	Containment   | x in/not in l| O(N)	         | searches list
	Copy          | l.copy()     | O(N)	         | Same as l[:] which is O(N)
	Pop           | l.pop(0)     | O(N)          | 
	Index of      | l.index(e)   | O(N)          | return the zero-index index of element e
	Find          | l.find(e)    | O(N)          | return the zero-index index of element e or -1
	Index of      | l.rindex(e)  | O(N)          | return the zero-index index of element e from right to left
	Find          | l.rfind(e)   | O(N)          | return the zero-index index of element e or -1 from right to left
	Count         | l.count(e)   | O(N)          | return the occurrence of element e in l

	Extreme value | min(l)/max(l)| O(N)	         |
	Reverse	      | l.reverse()  | O(N)	         |
	Iteration     | for v in l:  | O(N)          |
            		l[::2]       | O(N)          | give all even indexes starting from 0
            		l[1::2]      | O(N)          | give all odd indexes starting from 1
            		l[::-1]      | O(N)          | reverses list

	Sort          | l.sort()     | O(N Log N)    | key/reverse change original l
          		  | sorted(l)    | O(N Log N)    | key/reverse doesn't change original l
	Multiply      | k*l          | O(k N)        | 5*l is O(N): len(l)*l is O(N**2)
   
*Typical use:*

    When you need a mixed collection of data all in one place.
   
    When the data needs to be ordered.
   
    When your data requires the ability to be changed or extended. Remember, 
    lists are mutable.
   
    When you don't require data to be indexed by a custom value. Lists are 
    numerically indexed and to retrieve an element, you must know its numeric 
    position in the list.
   
    When you need a stack or a queue. Lists can be easily manipulated by 
    appending/removing elements from the beginning/end of the list.
   
    When your data doesn't have to be unique. For that, you would use sets.
   
*Problem clues:*

	For regular list:
	
	For the stack implementation (append(), pop(), LIFO):
	
	For the deque implementation (append(), popleft(), FIFO):
	
	
* <span style="color: #FF4081">TUPLE</span>

	*Description:*

   		Tuples support all operations from lists that do not mutate the data structure
   		(and with the same complexity classes).

	*Implementation:*

		>>> l = (1,2,3)
		>>> l = (1,'word',3)
		>>> l[0]
		1
		>>> l[0] = 1
		TypeError: 'tuple' object does not support item assignment    
  • SET

    Description:

      A set is an unordered collection with no duplicate elements. 
    	Basic uses include membership testing and eliminating duplicate entries. 
    	Set objects also support mathematical operations like union, intersection, 
    	difference, and symmetric difference. Sets have many more operations that 
    	are O(1) compared with lists and tuples.
    	
    	Not needing to keep values in a specific order (which lists/tuples require)
    	allows for faster operations.
    	
    	Frozen sets support all operations that do not mutate the data structure 
    	(and with the same complexity classes).	   		
    

    Implementation:

      s = set([1,2,3])
      
      
      EXAMPLES
      >>> A = {1, 2, 3, 3}
      >>> A
      set([1, 2, 3])
      >>> B = {3, 4, 5, 6, 7}
      >>> B
      set([3, 4, 5, 6, 7])
      >>> A | B     						# also works like A.union(B)
      set([1, 2, 3, 4, 5, 6, 7])
      >>> A & B     						# also works like A.intersection(B)
      set([3])
      >>> A - B     						# also works like A.difference(B)
      set([1, 2])
      >>> B - A     						# also works like B.difference(A)
      set([4, 5, 6, 7])
      >>> A ^ B     						# also works like A.symmetric_difference(B)
      set([1, 2, 4, 5, 6, 7])
      >>> (A ^ B) == ((A - B) | (B - A))
      True
      >>> A.issubset(B)					# test whether every element in s is in t
      False
      >>> A.issuperset(B)					# test whether every element in t is in s
      False
      >>> some_list = ['a', 'b', 'c', 'b', 'd', 'm', 'n', 'n']
      >>> duplicates = set([x for x in some_list if some_list.count(x) > 1])
      >>> print(duplicates)
      set(['b', 'n'])	   		
    

    Operations:

      Operation     | Example      | Complexity    | Notes
      --------------+--------------+---------------+-------------------------------
      Length        | len(s)       | O(1)	         |
      Add           | s.add(e)     | O(1)	         | adds element e as an insert(0,e)
      Containment   | x in/not in s| O(1)	         | compare to list/tuple - O(N)
      Remove        | s.remove(5)  | O(1)	         | compare to list/tuple - O(N)
      Discard       | s.discard(5) | O(1)	         | 
      Pop           | s.pop()      | O(1)	         | compare to list - O(N), pops leftmost or FIFO
      Clear         | s.clear()    | O(1)	         | similar to s = set()
      Update        | s.update(t)  | O(len(s)+len(t))| return set s with elements added from t
    
      Construction  | set(...)     | len(...)      |
      check ==, !=  | s != t       | O(min(len(s),lent(t))
      <=/<          | s <= t       | O(len(s1))    | issubset
      >=/>          | s >= t       | O(len(s2))    | issuperset s <= t == t >= s
      Union         | s | t        | O(len(s)+len(t)) | concatenation
      Intersection  | s & t        | O(min(len(s),lent(t))  | coincidence elements, using &= returns set s keeping only elements also found in t
      Difference    | s - t        | O(len(t))     | s minus elements in t that are in s, using -= returns set s after removing elements found in t
      Symmetric Diff| s ^ t        | O(len(s))     | concatenation without coincidence elements, using ^= returns set s with elements from s or t but not both
    
      Iteration     | for v in s:  | O(N)          |
      Copy          | s.copy()     | O(N)	         |
    

    Typical use:

      When you need a unique set of data: Sets check the unicity of elements 
      based on hashes.
    	When your data constantly changes: Sets, just like lists, are mutable.
    	When you need a collection that can be manipulated mathematically: With 
    	sets it's easy to do operations like difference, union, intersection, etc.
    	When you don't need to store nested lists, sets, or dictionaries in a data 
    	structure: Sets don't support unhashable types.
    

    Problem clues:

      .
    
  • DICTIONARY

    Description:

      Dictionaries are sometimes found in other languages as “associative 
      memories” or “associative arrays”. Unlike sequences, which are indexed by a 
      range of numbers, dictionaries are indexed by keys, which can be any 
      immutable type; strings and numbers can always be keys. Most dict 
      operations are O(1).
      
      It is best to think of a dictionary as an unordered set of key:value 
      pairs, with the requirement that the keys are unique (within one 
      dictionary). A pair of braces creates an empty dictionary: {}. Placing
      a comma-separated list of key:value pairs within the braces adds initial
      key:value pairs to the dictionary; this is also the way dictionaries are
      written on output.
      
      The main operations on a dictionary are storing a value with some key and 
      extracting the value given the key. It is also possible to delete a
      key:value pair with del. If you store using a key that is already in use, 
      the old value associated with that key is forgotten. It is an error to 
      extract a value using a non-existent key.
      
      The keys() method of a dictionary object returns a list of all the keys 
      used in the dictionary, in arbitrary order (if you want it sorted, just 
      apply the sorted() function to it). To check whether a single key is in the 
      dictionary, use the in keyword. The values() method returns a list of all 
      values used.	   		
    

    Implementation:

      d = {'api_key': 'mwkMqTWFnK0LzJHyfkeBGoS2hr2KG7WhHqSGX0SbDJ4', 
      		'container': 'CONTENTSHERE', 'channel': 'CHANNELHERE'}
      
      d = dict([('as',1),('king',10)])   		
    

    Operations:

      Operation     | Example      | Complexity    | Notes
      --------------+--------------+---------------+-------------------------------
      Index         | d[k]         | O(1)	         | if k doesn't exists it throws KeyError
      Store         | d[k] = v     | O(1)	         |
      Length        | len(d)       | O(1)	         |
      Delete        | del d[k]     | O(1)	         |
      get/setdefault| d.method     | O(1)	         |
            		  | d.get(k)     | O(1)          | if k doesn't exists it throws None
      Pop           | d.pop(k)     | O(1)	         |
      Pop item      | d.popitem()  | O(1)	         | pops the first element as key,value tuple (FIFO)
      Clear         | d.clear()    | O(1)	         | similar to s = {} or = dict()
      Views         | d.keys()     | O(1)          | list of keys
            		  | d.values()   | O(1)          | list of values
            		  | d.items()    | O(1)	         | list of tuples as (key,value)
    
      Construction  | dict(...)    | len(...)      |
    
      Iteration     | for k in d:  | O(N)          | all forms: keys, values, items
      				for k,v in d.iteritems()     | for key, value pairs as k and v
    

    Typical use:

      When you need a logical association between a key:value pair.
      When you need fast lookup for your data, based on a custom key.
      When your data is being constantly modified. Remember, dictionaries are 
      mutable.
    

    Problem clues:

      .
    
    • ORDEREDDICT

      Implementation:

        >>> from collections import OrderedDict
        >>> d = {'banana': 3, 'apple':4, 'pear': 1, 'orange': 2}
        >>> OrderedDict(sorted(d.items(), key=lambda t: t[0]))
        OrderedDict([('apple', 4), ('banana', 3), ('orange', 2), ('pear', 1)])
        >>> OrderedDict(sorted(d.items(), key=lambda t: t[1]))
        OrderedDict([('pear', 1), ('orange', 2), ('banana', 3), ('apple', 4)])
      
    • DEFAULTDICT

      Implementation:

        >>> import collections
        >>> tree = lambda: collections.defaultdict(tree)
        >>> some_dict = tree()
        >>> some_dict['colours']['favourite'] = "yellow"    #if this wasn't a defaultdict it'll have raised KeyError
        >>> import json
        >>> print(json.dumps(some_dict))
        {"colours": {"favourite": "yellow"}}
      
    • GRAPH

      Description:

        .	   		
      

      Implementation:

        """ A Python Class
        A simple Python graph class, demonstrating the essential 
        facts and functionalities of graphs.
        """
      
      
        class Graph(object):
      
            def __init__(self, graph_dict={}):
                """ initializes a graph object """
                self.__graph_dict = graph_dict
      
            def vertices(self):
                """ returns the vertices of a graph """
                return list(self.__graph_dict.keys())
      
            def edges(self):
                """ returns the edges of a graph """
                return self.__generate_edges()
      
            def add_vertex(self, vertex):
                """ If the vertex "vertex" is not in 
                    self.__graph_dict, a key "vertex" with an empty
                    list as a value is added to the dictionary. 
                    Otherwise nothing has to be done. 
                """
                if vertex not in self.__graph_dict:
                    self.__graph_dict[vertex] = []
      
            def add_edge(self, edge):
                """ assumes that edge is of type set, tuple or list; 
                    between two vertices can be multiple edges! 
                """
                edge = set(edge)
                (vertex1, vertex2) = tuple(edge)
                if vertex1 in self.__graph_dict:
                    self.__graph_dict[vertex1].append(vertex2)
                else:
                    self.__graph_dict[vertex1] = [vertex2]
      
            def __generate_edges(self):
                """ A static method generating the edges of the 
                    graph "graph". Edges are represented as sets 
                    with one (a loop back to the vertex) or two 
                    vertices 
                """
                edges = []
                for vertex in self.__graph_dict:
                    for neighbour in self.__graph_dict[vertex]:
                        if {neighbour, vertex} not in edges:
                            edges.append({vertex, neighbour})
                return edges
      
            def __str__(self):
                res = "vertices: "
                for k in self.__graph_dict:
                    res += str(k) + " "
                res += "\nedges: "
                for edge in self.__generate_edges():
                    res += str(edge) + " "
                return res
      
            def find_path(self, start_vertex, end_vertex, path=[]):
                """ find a path from start_vertex to end_vertex 
                    in graph """
                graph = self.__graph_dict
                path = path + [start_vertex]
                if start_vertex == end_vertex:
                    return path
                if start_vertex not in graph:
                    return None
                for vertex in graph[start_vertex]:
                    if vertex not in path:
                        extended_path = self.find_path(vertex, 
                                                       end_vertex, 
                                                       path)
                        if extended_path: 
                            return extended_path
                return None
      
            def find_all_paths(self, start_vertex, end_vertex, path=[]):
                """ find all paths from start_vertex to 
                    end_vertex in graph """
                graph = self.__graph_dict 
                path = path + [start_vertex]
                if start_vertex == end_vertex:
                    return [path]
                if start_vertex not in graph:
                    return []
                paths = []
                for vertex in graph[start_vertex]:
                    if vertex not in path:
                        extended_paths = self.find_all_paths(vertex, 
                                                             end_vertex, 
                                                             path)
                        for p in extended_paths: 
                            paths.append(p)
                return paths
      
            def vertex_degree(self, vertex):
                """ The degree of a vertex is the number of edges connecting
                    it, i.e. the number of adjacent vertices. Loops are counted 
                    double, i.e. every occurence of vertex in the list 
                    of adjacent vertices. """ 
                adj_vertices =  self.__graph_dict[vertex]
                degree = len(adj_vertices) + adj_vertices.count(vertex)
                return degree
      
            def find_isolated_vertices(self):
                """ returns a list of isolated vertices. """
                graph = self.__graph_dict
                isolated = []
                for vertex in graph:
                    print(isolated, vertex)
                    if not graph[vertex]:
                        isolated += [vertex]
                return isolated
      
            def delta(self):
                """ the minimum degree of the vertices """
                min = 100000000
                for vertex in self.__graph_dict:
                    vertex_degree = self.vertex_degree(vertex)
                    if vertex_degree < min:
                        min = vertex_degree
                return min
                
            def Delta(self):
                """ the maximum degree of the vertices """
                max = 0
                for vertex in self.__graph_dict:
                    vertex_degree = self.vertex_degree(vertex)
                    if vertex_degree > max:
                        max = vertex_degree
                return max
      
            def degree_sequence(self):
                """ calculates the degree sequence """
                seq = []
                for vertex in self.__graph_dict:
                    seq.append(self.vertex_degree(vertex))
                seq.sort(reverse=True)
                return tuple(seq)
      
            def density(self):
                """ method to calculate the density of a graph """
                g = self.__graph_dict
                V = len(g.keys())
                E = len(self.edges())
                return 2.0 * E / (V *(V - 1))
      
        if __name__ == "__main__":
      
            g = { "a" : ["d"], "b" : ["c"], "c" : ["b", "c", "d", "e"], "d" : ["a", "c"], "e" : ["c"],"f" : []}
      
            graph = Graph(g)			   		   
            			     
            # or maybe
            >>> from collections import defaultdict
            >>> z = defaultdict()
        	>>> z["a"]=["d"]
        	>>> z["c"]=["b","c","d","e"]
        	>>> z["b"]=["c"]
        	>>> z["e"]=["c"]
        	>>> z["d"]=["a","c"]
        	>>> z["f"]=[]
        	
        	graph = Graph(dict(z))
      

      Operations:

        graph.vertices()
        graph.edges()
        graph.add_vertex("z")
        graph.add_edge({"a","z"})
        graph.density()
        graph.find_all_paths("a", "e")
      

      Typical use:

        .
      

      Problem clues:

        .
      
    • TREES

        Binary Tree vs. Binary Search Tree
        When given a binary tree question, many candidates assume that the interviewer means binary search tree. Be sure to ask whether or not the tree is a binary search tree. A binary search tree imposes the condition that, for all nodes, the left children are less than or equal to the current node, which is less than all the right nodes.
      
        Balanced vs. Unbalanced		
        While many trees are balanced, not all are. Ask your interviewer for clarification on this issue. If the tree is unbalanced, you should describe your algorithm in terms of both the average and the worst case time. Note that there are multiple ways to balance a tree, and balancing a tree implies only that the depth of subtrees will not vary by more than a certain amount. It does not mean that the left and right subtrees are exactly the same size.
      
        Full and Complete
        Full and complete trees are trees in which all leaves are at the bottom of the tree, anD all non-leaf nodes have exactly two children. Note that full and complete trees are extremely rare, as a tree must have exactly 2n - 1 nodes to meet this condition
        
        Binary Tree Traversal
        Prior to your interview, you should be comfortable implementing in-order, post-order, and pre-order traversal. The most common of these, in-order traversal, works by visiting the left side, then the current node, then the right.
      

      BINARY TREE

      Description:

        .	   		
      

      Implementation:

        class BinaryTree():
      
        	def __init__(self,rootid):
        		self.left = None
        		self.right = None
        		self.rootid = rootid
      
        	def getLeftChild(self):
        		return self.left
        	
        	def getRightChild(self):
        		return self.right
      
        	def setNodeValue(self,value):
        		self.rootid = value
      
        	def getNodeValue(self):
        		return self.rootid	   		
      

      Operations:

        .
      

      Typical use:

        .
      

      Problem clues:

        .
      

      TRIE

      Description:

        A trie is a variant of an n-ary tree in which characters are stored at each node. Each path down the tree may represent a word.	   		
      

      Implementation:

        .	   		
      

      Operations:

        .
      

      Typical use:

        .
      

      Problem clues:

        .
      

PYTHON TRICKS & BUILT-INS


args and kwargs

*args : additional arguments

>>> def test_var_args(f_arg, *argv):
....	print "first normal arg:", f_arg
....	for arg in argv:
....    	print "another arg through *argv :", arg

>>> test_var_args('yasoob','python','eggs','test')

>>> first normal arg: yasoob
>>> another arg through *argv : python
>>> another arg through *argv : eggs
>>> another arg through *argv : test

*kwargs : additional keyed arguments

>>> def greet_me(**kwargs):
....	if kwargs is not None:
....    	for key, value in kwargs.iteritems():
....        	print "%s == %s" %(key,value)

>>> greet_me(name="yasoob")
name == yasoob

Slicing

>>> a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> a[2:8]
[2, 3, 4, 5, 6, 7]

>>> a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> a[-4:-2]
[7, 8]

>>> a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> a[::2]
[0, 2, 4, 6, 8, 10]
>>> a[2:8:2]
[2, 4, 6]

>>> a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> a[::-1]
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

Enumerate

Similar to a dictionary, enumerate returns numeric-keyed value pairs as a list of tuples.
	
>>> seasons = ['Spring', 'Summer', 'Fall', 'Winter']
>>> list(enumerate(seasons))
[(0, 'Spring'), (1, 'Summer'), (2, 'Fall'), (3, 'Winter')]

Flattening

>>> a = [1, 2, [3, 4], [[5, 6], [7, 8]]]
>>> flatten = lambda x: [y for l in x for y in flatten(l)] if type(x) is list else [x]
>>> flatten(a)
[1, 2, 3, 4, 5, 6, 7, 8]		

Zip

This function returns a list of tuples, where the i-th tuple contains the i-th element from each of the argument sequences or iterables. The returned list is truncated in length to the length of the shortest argument sequence.

>>> a = [1, 2, 3]
>>> b = ['a', 'b', 'c']
>>> z = zip(a, b)
>>> z
[(1, 'a'), (2, 'b'), (3, 'c')]
>>> zip(*z)
[(1, 2, 3), ('a', 'b', 'c')]

>>> x= [1,2,3]
>>> y=[4,5,6]
>>> [a+b for a,b in zip(x,y)]
[5, 7, 9]
	
#grouping
>>> a = [1, 2, 3, 4, 5, 6]
>>> group_adjacent = lambda a, k: zip(*([iter(a)] * k))
>>> group_adjacent(a, 3)
[(1, 2, 3), (4, 5, 6)]

#n grams
>>> from itertools import islice
>>> def n_grams(a, n):
...     z = (islice(a, i, None) for i in range(n))
...     return zip(*z)
...
>>> a = [1, 2, 3, 4, 5, 6]
>>> n_grams(a, 3)
[(1, 2, 3), (2, 3, 4), (3, 4, 5), (4, 5, 6)]
	
#inverting dict
>>> m = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
>>> dict(zip(m.values(), m.keys()))
{1: 'a', 2: 'b', 3: 'c', 4: 'd'}

Filter

>>> def f(x): return x % 3 == 0 or x % 5 == 0
>>> filter(f, range(2, 25))
[3, 5, 6, 9, 10, 12, 15, 18, 20, 21, 24]
>>> number_list = range(-5, 5)
>>> less_than_zero = list(filter(lambda x: x < 0, number_list))
>>> less_than_zero
[-5, -4, -3, -2, -1]

Map

>>> def cube(x): return x*x*x
>>> map(cube, range(1, 11))
[1, 8, 27, 64, 125, 216, 343, 512, 729, 1000]
>>> items = [1, 2, 3, 4, 5]
>>> squared = map(lambda x: x**2, items)
[1, 4, 9, 16, 25]
# list comprehension approach

Reduce

>>> def add(x,y): return x+y
>>> reduce(add, range(1, 11))
55

List comprehension

Matrix of Zeros (WxL): 
>>> zero_grid = [ [0] * W for j in range(L) ]

Filtering:
>>> [x for x in range(2,25) if x % 3 == 0 or x % 5 == 0]
[3, 5, 6, 9, 10, 12, 15, 18, 20, 21, 24]

Mapping:
>>> [x*x*x for x in range(1,11)]
[1, 8, 27, 64, 125, 216, 343, 512, 729, 1000]

Dictionary comprehension

>>> m = {x: 'A' + str(x) for x in range(10)}
>>> m
{0: 'A0', 1: 'A1', 2: 'A2', 3: 'A3', 4: 'A4', 5: 'A5', 6: 'A6', 7: 'A7', 8: 'A8', 9: 'A9'}
>>> {v: k for k, v in m.items()}
{'A1': 1, 'A0': 0, 'A3': 3, 'A2': 2, 'A5': 5, 'A4': 4, 'A7': 7, 'A6': 6, 'A9': 9, 'A8': 8}

Exceptions

Exception handling is an art which once you master grants you immense powers.

try:
	file = open('test.txt', 'rb')
except Exception as e:
	print('An error occurred: %s' % (format(e.args[-1])))
finally:
	print("This would be printed even if no exception occurs!")

Lambdas

Lambdas are one line functions. They are also known as anonymous functions in some other languages. You might want to use lambdas when you don’t want to use a function twice in a program. They are just like normal functions and even behave like them.

>>> add = lambda x, y: x + y
>>> print(add(3, 5))
8

>>> a = [(1, 2), (4, 1), (9, 10), (13, -3)]
>>> a.sort(key=lambda x: x[1])
>>> print(a)
# Output: [(13, -3), (4, 1), (1, 2), (9, 10)]

Built-ins

DIVMOD
>>> divmod(5,2)
(2, 1)
>>> 5 % 2
1

SORTING
>>> sorted([5, 2, 3, 1, 4])
[1, 2, 3, 4, 5]
>>> sorted([5, 2, 3, 1, 4], reverse=True)
[5, 4, 3, 2, 1]
>>> sorted("This is a test string from Andrew".split(), key=str.lower)
['a', 'Andrew', 'from', 'is', 'string', 'test', 'This']
>>> student_tuples = [
	('john', 'A', 15),
	('jane', 'B', 12),
	('dave', 'B', 10),
]
>>> sorted(student_tuples, key=lambda student: student[2])   # sort by age
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
# same works for named objects:
>>> class Student:
    def __init__(self, name, grade, age):
        self.name = name
        self.grade = grade
        self.age = age
    def __repr__(self):
        return repr((self.name, self.grade, self.age))
>>> student_objects = [
	Student('john', 'A', 15),
	Student('jane', 'B', 12),
	Student('dave', 'B', 10),
]
>>> sorted(student_objects, key=lambda student: student.age)   # sort by age
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]

COMPLETE LIST 	
abs()	divmod()	input()	open()	staticmethod(
all()	enumerate()	int()	ord()	str()
any()	eval()	isinstance()	pow()	sum()
basestring()	execfile()	issubclass()	print()	super(
bin()	file()	iter()	property()	tuple()
bool()	filter()	len()	range()	type()
bytearray()	float()	list()	raw_input()	unichr()
callable()	format()	locals()	reduce()	unicode()
chr()	frozenset()	long()	reload()	vars()
classmethod()	getattr()	map()	repr()	xrange()
cmp()	globals()	max()	reversed()	zip()
compile()	hasattr()	memoryview()	round()	__import__()
complex()	hash()	min()	set()
delattr()	help()	next()	setattr()
dict()	hex()	object()	slice()
dir()	id()	oct()	sorted()		

PYTHON USEFUL STANDARD IMPORTS

Itertools (python module)

itertools (https://docs.python.org/2.7/library/itertools.html) 

		This module implements a number of iterator building blocks inspired by constructs from APL, Haskell, and SML. Each has been recast in a form suitable for Python.

	  	itertools.product('ABCD', repeat=2)	 					AA AB AC AD BA BB BC BD CA CB CC CD DA DB DC DD
		itertools.permutations('ABCD', 2)	 					AB AC AD BA BC BD CA CB CD DA DB DC
		itertools.combinations('ABCD', 2)	 					AB AC AD BC BD CD
		itertools.combinations_with_replacement('ABCD', 2)	 	AA AB AC AD BB BC BD CC CD DD
		itertools.product([0,1],repeat = 2)						00 01 10 11

EXAMPLE FOR COUNTING ALL PERMUTATIONS IN A GIVEN STRING
>>> import itertools
>>> z = []
>>> for i in itertools.permutations('abc',3): z.append(i)
>>> z
[('a', 'b', 'c'), ('a', 'c', 'b'), ('b', 'a', 'c'), ('b', 'c', 'a'), ('c', 'a', 'b'), ('c', 'b', 'a')]
>>> z_list = [a+b+c for a,b,c in z]
>>> z_list
['abc', 'acb', 'bac', 'bca', 'cab', 'cba']
>>> s = "abcabcabc"
>>> counter = 0
>>> for i in z_list: counter += s.count(i)
>>> counter
7

Random (python module)

random (https://docs.python.org/2.7/library/random.html)
	
	This module implements pseudo-random number generators for various distributions.
	For integers, uniform selection from a range. For sequences, uniform selection of a random element, a function 
	to generate a random permutation of a list in-place, and a function for random sampling without replacement.

EXAMPLE 

>>> random.random()        # Random float x, 0.0 <= x < 1.0
0.37444887175646646
>>> random.uniform(1, 10)  # Random float x, 1.0 <= x < 10.0
1.1800146073117523
>>> random.randint(1, 10)  # Integer from 1 to 10, endpoints included
7
>>> random.randrange(0, 101, 2)  # Even integer from 0 to 100
26
>>> random.choice('abcdefghij')  # Choose a random element
'c'
>>> items = [1, 2, 3, 4, 5, 6, 7]
>>> random.shuffle(items)
>>> items
[7, 3, 2, 5, 6, 4, 1]
>>> random.sample([1, 2, 3, 4, 5],  3)  # Choose 3 elements
[4, 1, 5]

Collections (python module)

collections (https://docs.python.org/2.7/library/collections.html)

		This module implements specialized container datatypes providing alternatives to Python’s general purpose built-in containers, dict, list, set, and tuple.

		collections.Counter			dict subclass for counting hashable objects	
		collections.deque			list-like container with fast appends and pops on either end	
		collections.OrderedDict		dict subclass that remembers the order entries were added
		
		collections.defaultdict		dict subclass useful for trees (calls a factory function)	

EXAMPLE COUNTER
>>> Counter('abracadabra').most_common(3)
[('a', 5), ('r', 2), ('b', 2)]
>>> import re
>>> words = re.findall(r'\w+', open('hamlet.txt').read().lower())
>>> Counter(words).most_common(4)
[('the', 1143), ('and', 966), ('to', 762), ('of', 669)]

EXAMPLE DEQUE
>>> queue = deque(["Eric", "John", "Michael"])
>>> queue.append("Terry")           # Terry arrives
>>> queue.append("Graham")          # Graham arrives
>>> queue.popleft()                 # The first to arrive now leaves
'Eric'
>>> queue.popleft()                 # The second to arrive now leaves
'John'
>>> queue                           # Remaining queue in order of arrival
deque(['Michael', 'Terry', 'Graham'])
>>> Q = deque([5, 2, 1, 3])
>>> Q.rotate(3)
deque([2, 1, 3, 5])
>>> def moving_average(iterable, n=3):
		# moving_average([40, 30, 50, 46, 39, 44]) --> 40.0 42.0 45.0 43.0
		# http://en.wikipedia.org/wiki/Moving_average
		it = iter(iterable)
		d = deque(itertools.islice(it, n-1))
		d.appendleft(0)
		s = sum(d)
		for elem in it:
    		s += elem - d.popleft()
    		d.append(elem)
    		yield s / float(n)

EXAMPLE ORDEREDDICT
>>> d = {'banana': 3, 'apple':4, 'pear': 1, 'orange': 2}
>>> OrderedDict(sorted(d.items(), key=lambda t: t[0]))
OrderedDict([('apple', 4), ('banana', 3), ('orange', 2), ('pear', 1)])
>>> OrderedDict(sorted(d.items(), key=lambda t: t[1]))
OrderedDict([('pear', 1), ('orange', 2), ('banana', 3), ('apple', 4)])

EXAMPLE DEFAULTDICT
>>> import collections
>>> tree = lambda: collections.defaultdict(tree)
>>> some_dict = tree()
>>> some_dict['colours']['favourite'] = "yellow"    #if this wasn't a defaultdict it'll have raised KeyError
>>> import json
>>> print(json.dumps(some_dict))
{"colours": {"favourite": "yellow"}}

Heapq (python module)

heapq (https://docs.python.org/2.7/library/heapq.html)
	
	This module provides an implementation of the heap queue algorithm, also known as the priority queue algorithm.
	Heaps are binary trees for which every parent node has a value less than or equal to any of its children. This 
	implementation uses arrays for which heap[k] <= heap[2*k+1] and heap[k] <= heap[2*k+2] for all k, counting 
	elements from zero. For the sake of comparison, non-existing elements are considered to be infinite. The 
	interesting property of a heap is that its smallest element is always the root, heap[0].

EXAMPLE

>>> from heapq import heappush, heappop
>>> def heapsort(iterable):
...     h = []
...     for value in iterable:
...         heappush(h, value)
...     return [heappop(h) for i in range(len(h)]
>>> heapsort([1, 3, 5, 7, 9, 2, 4, 6, 8, 0])
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Time (python module)

time (https://docs.python.org/2.7/library/time.html)

	This module is useful for testing complexity and computational cost in terms of time.

EXAMPLE

>>> from time import time
>>> def InsertionSort(A):
...		start_time = time() # record the starting time
...		for j in range(1, len(A)):
...			key = A[j]
...			i = j - 1
...			while (i >=0) and (A[i] > key):
...				A[i+1] = A[i]
...				i = i - 1
...			A[i+1] = key
...		end_time = time() # record the ending time
...		elapsed = end_time - start_time
...		print "Elapsed time (ms): %s" % (elapsed)
...		return A

Math (python module)

math (https://docs.python.org/2/library/math.html)

	This module is always available. It provides access to the mathematical functions defined by the C standard.
	
EXAMPLE

>>> import math
>>> math.pi
3.141592653589793
>>> math.sin(math.pi/2)
1.0
>>> math.log(4,2)
2.0
>>> math.factorial(4)
24

re (python module)

re (https://docs.python.org/2/howto/regex.html)

	This module provides Perl-style regular expression patterns.

>>> import re
>>> words = re.findall(r'\w+', open('hamlet.txt').read().lower())

>>> p = re.compile('\d+')
>>> p.findall('12 drummers drumming, 11 pipers piping, 10 lords a-leaping')
['12', '11', '10']

>>> p = re.compile(r'(\b\w+)\s+\1')
>>> p.search('Paris in the the spring').group()
'the the'

>>> p = re.compile( '(blue|white|red)')
>>> p.sub( 'colour', 'blue socks and red shoes')
'colour socks and colour shoes'

ALGORITHMS


**SORT**
Is worth to refer that python exercises of sorting algorithms provide the following leading results:
	1) Numpy
    2) Native
    3) Quicksort list comprehension
    4) Mergesort
  • MERGE-SORT

      Merge sort is a recursive algorithm that continually splits a list in half. If the list is empty or has one item, it is sorted by definition (the base case). If the list has more than one item, we split the list and recursively invoke a merge sort on both halves.
      
      def mergeSort(alist):
      	print("Splitting ",alist)
      	if len(alist)>1:
      		mid = len(alist)//2
      		lefthalf = alist[:mid]
      		righthalf = alist[mid:]
    
      		mergeSort(lefthalf)
      		mergeSort(righthalf)
    
      		i=0
      		j=0
      		k=0
      		while i < len(lefthalf) and j < len(righthalf):
          		if lefthalf[i] < righthalf[j]:
              		alist[k]=lefthalf[i]
              		i=i+1
          		else:
              		alist[k]=righthalf[j]
              		j=j+1
          		k=k+1
    
      		while i < len(lefthalf):
          		alist[k]=lefthalf[i]
          		i=i+1
          		k=k+1
    
      		while j < len(righthalf):
          		alist[k]=righthalf[j]
          		j=j+1
          		k=k+1
      	print("Merging ",alist)
    
      alist = [54,26,93,17,77,31,44,55,20]
      mergeSort(alist)
      print(alist)
    
  • QUICK-SORT

      Quick sort is a recursive algorithm that continually splits a list according to a pivotValue. If the list is empty or has one item, it is sorted by definition (the base case). If the list has more than one item, we split the list with all values below pivot in one side and all values above pivot on another side, and recursively invoke a quick sort on both side.
      
      Python methods work faster in Big-O, but an implementation of quick sort can be:
      
      def quickSort(list):
      	"""Quicksort using list comprehensions"""
      	if list == []: 
      		return []
      	else:
      		pivot = list[0]
      		lesser = quickSort([x for x in list[1:] if x < pivot])
      		greater = quickSort([x for x in list[1:] if x >= pivot])
      		return lesser + [pivot] + greater
    
      alist = [54,26,93,17,77,31,44,55,20]
      alist = quickSort(alist)
      print(alist)
    

SEARCH

  • GRAPH SEARCH

      Breadth first and depth first are useful graph traversal and searching algorithms. 
      
      DFS is typically the easiest if we want to visit every node in the graph, or at least visit every node until we find whatever we're looking for. However, if we have a very large tree and want to be prepared to quit when we get too far from the original node, DFS can be problematic; we might search thousands of ancestors of the node, but never even search all of the node's children. In these cases, BFS is typically preferred
      
      This is an excellent representation in python. Given the graph implementation in section Data Structures:
      
      		def find_path(self, start_vertex, end_vertex, path=[]):
      	        """ find a path from start_vertex to end_vertex 
      	            in graph """
      	        graph = self.__graph_dict
      	        path = path + [start_vertex]
      	        if start_vertex == end_vertex:
      	            return path
      	        if start_vertex not in graph:
      	            return None
      	        for vertex in graph[start_vertex]:
      	            if vertex not in path:
      	                extended_path = self.find_path(vertex, 
      	                                               end_vertex, 
      	                                               path)
      	                if extended_path: 
      	                    return extended_path
      	        return None
    
      	    def find_all_paths(self, start_vertex, end_vertex, path=[]):
      	        """ find all paths from start_vertex to 
      	            end_vertex in graph """
      	        graph = self.__graph_dict 
      	        path = path + [start_vertex]
      	        if start_vertex == end_vertex:
      	            return [path]
      	        if start_vertex not in graph:
      	            return []
      	        paths = []
      	        for vertex in graph[start_vertex]:
      	            if vertex not in path:
      	                extended_paths = self.find_all_paths(vertex, 
      	                                                     end_vertex, 
      	                                                     path)
      	                for p in extended_paths: 
      	                    paths.append(p)
      	        return paths
    
  • BINARY SEARCH

      After sorting an array, binary search is a useful algorithm based on divide and conquer.
      
      		def binary_search(S, target):
      			"""Return True if target found, else False."""
      			start = 0
      			stop = len(S)
      			if start >= stop: # zero elements in slice
      				return False
      			else: # two or more elements in slice
      				mid = (start + stop) // 2
      			if target == S[mid]:
      				return True
      			elif target > S[mid]:
      				return binary_search(S[mid+1:], target)
      			else:
      				return binary_search(S[:mid-1], target)
    



THE OFFER

HINT: HANDLE THE POST-INTERVIEW STRESS
- Think of deadlines and extensions if necessary
- Handle with care if you think to decline, keep a line of communication open

AND YOUR REAL SALARY IS...


+ OFFER SALARY
This is your periodical income.
  • SIGNING BONUS, RELOCATION AND ONE-TIME PERKS

    When comparing offers, it's wise to amortize this cash over three years (or however long you expect to stay).

  • COST OF LIVING DIFFERENCE

    Silicon Valley, for example, is about 20 to 30% more expensive than Seattle.

  • ANNUAL BONUS

    This ranges from anywhere from 3% to 30%.

  • STOCK OPTIONS AND GRANTS

    Equity compensation can form another big part. Amortize this cash over three years and lump that value into salary.

  • PROMOTION PLAN

    This is not money in your pocket, but it can or can't be in your near future in the company.

  • HAPPINESS FACTOR

    Think of the product, the culture, working hours, and if possible meet your Manager and Teammates.