VII-Technical Questions

Please indicate the source: http://blog.csdn.net/gaoxiangnumber1

Welcome to my github: https://github.com/gaoxiangnumber1

How to Prepare

  • For each problem, do the following:
    1. Try to solve the problem on your own. Hints are provided at the back of this book, but push yourself to develop a solution with as little help as possible. Make sure to think about the space and time efficiency.
    2. Write the code on paper.
    3. Test your code on paper. This means testing the general cases, base cases, error cases, and so on. You’ll need to do this during your interview.
    4. Type your paper code into a computer. Start a list of all the errors you make so that you can keep these in mind during the actual interview.
  • Try to do as many mock interviews as possible. You and a friend can take turns giving each other mock interviews. Though your friend may not be an expert interviewer, he or she may still be able to walk you through a coding or algorithm problem. You’ll also learn a lot by experiencing what it’s like to be an interviewer.

What You Need To Know

Core Data Structures, Algorithms, and Concepts

  • Here’s a list of the absolute, must-have knowledge. For each of these topics, make sure you understand how to use and implement them and, where applicable, the space and time complexity.
Data StructuresAlgorithmsConcepts
Linked ListsBreadth-First SearchBit Manipulation
Trees, Tries, &GraphsDepth-First SearchMemory(Stack vs. Heap)
Stacks & QueuesBinary SearchRecursion
HeapsMerge SortDynamic Programming
Vectors / ArrayListQuick SortBig O Time & Space
Hash Table//
  • Practicing implementing the data structures and algorithm on paper, and then on a computer, is a great exercise. It will help you learn how the internals of the data structures work, which is important for many interviews.

Powers of 2 Table

  • The table below is useful for many questions involving scalability or any sort of memory limitation. Memorizing this table can be useful.
  • You could use this table to quickly compute that a bit vector mapping every 32-bit integer to a boolean value could fit in memory on a typical machine. There are 232 integers and each integer takes one bit in the bit vector, we need 232 bits(or 229 bytes) to store this mapping. That’s about half a gigabyte of memory, which can be held in memory on a typical machine.

Walking Through a Problem

  • The below flowchart walks you through how to solve a problem. Use this in your practice.
    1. Listen. Pay very close attention to any information in the problem description. You probably need it all for an optimal algorithm.
    2. Example. Most examples are too small or are special cases. Debug your example. Is there any way it’s a special case? Is it big enough?
    3. Brute Force. Get a brute-force solution as soon as possible. Don’t worry about developing an efficient algorithm yet. State a naive algorithm and its runtime, then optimize from there. Don’t code yet though!
    4. Optimize. Walk through brute force with BUD optimization or try some of these ideas:
      (1) Look for any unused info. You usually need all the information in a problem.
      (2) Solve it manually on an example, then reverse engineer your thought process. How did you solve it?
      (3) Solve it “incorrectly” and then think about why the algorithm fails. Can you fix those issues?
      (4) Make a time vs. space trade-off. Hash tables are especially useful!
    5. Walk through. Now you have an optimal solution, walk through your approach in detail. Make sure you understand each detail before you start coding.
    6. Implement. Your goal is to write beautiful code. Modularize your code from the beginning and refactor to clean up anything that isn’t beautiful. Keep talking: your interviewer wants to hear how you approach the problem.
    7. Test. Test in this order:
      (1) Conceptual test. Walk through your code like you would for a detailed code review.
      (2) Unusual or non-standard code.
      (3) Hot spots, like arithmetic and null nodes.
      (4) Small test cases. It’s much faster than a big test case and just as effective.
      (5) Special cases and edge cases.
      And when you find bugs, fix them carefully!

What to Expect

  • Listen for guidance from the interviewer. The level of interviewer participation depends on your performance, the difficulty of the question, what the interviewer is looking for, and the interviewer’s own personality.
  • When you’re given a problem(or when you’re practicing), work your way through it using the approach below.

1. Listen Carefully

  • Listen carefully to the problem and Make sure:
    1. You hear the problem correctly.
    2. You’ve mentally recorded any unique information.
      Ask questions about anything you’re unsure about.
  • “Given two arrays that are sorted, find ..”
    You need to know that the data is sorted. The optimal algorithm for the sorted situation is probably different from the optimal algorithm for the unsorted situation.
  • “Design an algorithm to be run repeatedly on a server that …”
    The server/to-be-run-repeatedly situation is different from the run-once situation. Perhaps this means that you cache data? Or perhaps it justifies some reasonable pre-computation on the initial data set?
  • It’s unlikely that your interviewer would give you this information if it didn’t affect the algorithm.
  • Write information on the paper/whiteboard, otherwise you will forget key details after 10 minutes into developing an algorithm. Your first algorithm doesn’t need to use the information. But if you find yourself stuck, or you’re still working to develop something more optimal, ask yourself if you’ve used all the information in the problem.

2. Draw an Example

  • When you hear a question, get out of your chair, go to the whiteboard, and draw an example. You should create an example that is:
    1. Specific. It should use real numbers or strings(if applicable to the problem).
    2. Sufficiently large. Most examples are too small, by about 50%.
    3. Not a special case. It’s very easy to inadvertently draw a special case. If there’s any way your example is a special case, you should fix it.

3. State a Brute Force

  • Once you have an example done, state a brute force. It’s okay that this initial solution is terrible. Explain what the space and time complexity is, and then dive into improvements. If you don’t state the brute force, your interviewer may think that you’re struggling to see even the easy solution.
  • Despite being possibly slow, a brute force algorithm is valuable to discuss. It’s a starting point for optimizations, and it helps you wrap your head around the problem.

4. Optimize

  • Once you have a brute force algorithm, you should work on optimizing it. A few techniques that work well are:
    1. Look for any unused information. Did your interviewer tell you that the array was sorted? How can you leverage that information?
    2. Use a fresh example. Sometimes, seeing a different example will unclog your mind or help you see a pattern in the problem.
    3. Solve it “incorrectly”. Having an incorrect solution might help you find a correct solution. For example, if you’re asked to generate a random value from a set such that all values are equally likely, an incorrect solution might be one that returns a semi-random value: Any value could be returned, but some are more likely than others. You can then think about why that solution isn’t perfectly random. Can you re-balance the probabilities?
    4. Make time vs. space tradeoff. Sometimes storing extra state about the problem can help you optimize the runtime.
    5. Pre-compute information. Is there a way that you can reorganize the data(sorting, etc.) or compute some values upfront that will help save time in the long run?
    6. Use a hash table. Hash tables should be at the top of your mind.
    7. Think about the best conceivable runtime(discussed on page 72).
  • Walk through the brute force with these ideas in mind and look for BUD(page 67).

5. Walk Through

  • After you’ve nailed down an optimal algorithm, don’t just dive into coding. Take a moment to solidify your understanding of the algorithm. Whiteboard coding is very slow. So is testing your code and fixing it. Thus, you need to make sure that you get it as close to “perfect” in the beginning as possible.
  • Walk through your algorithm and get a feel for the structure of the code. You can write pseudocode: basic steps(“(1) Search array. (2) Find biggest. (3) Insert in heap”) or brief logic(“if p < q, move p. else move q”) can be valuable.
  • If you don’t understand exactly what you’re about to write, you’ll struggle to code it. It will take you longer to finish the code, and you’re more likely to make major errors.

6. Implement

  • Now that you have an optimal algorithm and you know exactly what you’re going to write, go ahead and implement it.
  • Start coding in the far top left corner of the whiteboard. Avoid “line creep”(匍匐前进) where each line of code is written an awkward slant. It makes your code look messy and can be confusing when working in a whitespace-sensitive language, like Python.
  • Write beautiful code:
    1. Modularized code. This shows good coding style and it makes things easier for you. If your algorithm uses a matrix initialized to {{ 1, 2, 3}, { 4, 5, 6}, …}, don’t waste your time writing this initialization code. Just pretend you have a function InitlncrementalMatrix(int size). Fill in the details later if you need to.
    2. Error checks. A good compromise is to add a todo and then explain out loud what you’d like to test.
    3. Use other classes/structs where appropriate. If you need to return a list of start and end points from a function, you could do this as a two-dimensional array. It’s better to do this as a list of StartEndPair(or possibly Range) objects. You don’t necessarily have to fill in the details for the class. Just pretend it exists and deal with the details later if you have time.
    4. Good variable names. Long variable names can be slow to write. A good compromise is to abbreviate it after the first usage. You can use start_child the first time, and then explain to your interviewer that you will abbreviate this as sc after this.
  • If you see something you can refactor later on, then explain this to your interviewer and decide whether or not it’s worth the time to do so. Usually it is, but not always.
  • If you get confused(which is common), go back to your example and walk through it again.

7. Test

  • You shouldn’t “submit” code in an interview without testing it.
  • What many candidates do is take their earlier example and test it against their code. That might discover bugs, but it’ll take a really long time to do so. Instead, try this approach:
    1. Start with a “conceptual” test. A conceptual test means just reading and analyzing what each line of code does. Think about it like you’re explaining the lines of code for a code reviewer. Does the code do what you think it should do?
    2. Weird(自然的) looking code. Double check that line of code that says x = length - 2. Investigate that for loop that starts at i = 1. While you undoubtedly did this for a reason, it’s really easy to get it just slightly wrong.
    3. Hot spots. Base cases in recursive code. Integer division. Null nodes in binary trees. The start and end of iteration through a linked list. Double check that stuff.
    4. Small test cases. Don’t use that big 8-element array from the algorithm part. Instead, use a 3 or 4 element array. It’ll likely discover the same bugs, but it will be much faster to do so.
    5. Special cases. Test your code against null or single element values, the extreme cases, and other special cases.
  • When you find bugs(and you probably will), you should fix them. But don’t just make the first correction you think of. Instead, carefully analyze why the bug occurred and ensure that your fix is the best one.

Optimize & Solve Technique 1: Look for BUD

  • “BUD” is a acronym for: Bottlenecks, Unnecessary work, Duplicated work.
    These are three of the most common things that an algorithm can “waste” time doing. You can walk through your brute force looking for these things. When you find one of them, you can then focus on getting rid of it. If it’s still not optimal, you can repeat this approach on your current best algorithm.

Bottlenecks

  • A bottleneck is a part of your algorithm that slows down the overall runtime. There are two common ways this occurs:
    1. You have one-time work that slows down your algorithm. Suppose you have a two-step algorithm where you first sort the array and then you find elements. The first step is O(N log N) and the second step is O(N). Perhaps you could reduce the second step to O(log N) or O(1), but would it matter? Not too much. It’s not a priority, as the O(N log N) is the bottleneck. Until you optimize the first step, your overall algorithm will be O(N log N).
    2. You have a chunk of work that’s done repeatedly, like searching. Perhaps you can reduce that from O(N) to O(log N) or O(1).
  • Example: Given an array of distinct integer values, count the number of pairs of integers that have difference k. E.g, array {1, 7, 5, 9, 2, 12, 3} and difference k = 2, there are four pairs with difference 2: (1, 3), (3, 5), (5, 7), (7, 9).
  • A brute force algorithm is to go through the array, starting from the first element, and then search through the remaining elements(which will form the other side of the pair). For each pair, compute the difference. If the difference equals k, increment a counter of the difference.
  • The bottleneck is the repeated search for the “other side” of the pair.
    Optimization: We already know the other side of x, it’s x + k or x - k. If we sorted the array, we could find the other side for each of the N elements in O(log N) time by binary search.
  • We now have a two-step algorithm, where both steps take O(N log N) time. Now, sorting is the new bottleneck.
    Optimization: Throw everything in the array into the hash table. Then, to look up if x + k or x - k exist in the array, we look it up in the hash table. We can do this in O(N) time.

Unnecessary Work

  • Example: Print all positive integer solutions to the equation a3 + b3 = c3 + d3 where a, b, c, and d are integers between 1 and 1000.
  • A brute force solution has four nested for loops. Something like:
1 n = 1000
2 for a from 1 to n
3   for b from 1 to n
4       for c from 1 to n
5           for d from 1 to n
6               if a3 + b3 == c3 + d3
7                   print a, b, c, d
  • It’s unnecessary to continue checking for other possible values of d. Only one could work. We should at least break after we find a valid solution.
1 n = 1000
2 for a from 1 to n
3   for b from 1 to n
4       for c from 1 to n
5           for d from 1 to n
6               if a3 + b3 == c3 + d3
7                   print a, b, c, d
8                   break // break out of d's loop
  • This won’t make a meaningful change to the runtime, our algorithm is still O(N4), but it’s a good fix to make.
  • If there’s only one valid d value for each(a, b, c), then we can just compute it.
1 n = 1000
2 for a from 1 to n
3   for b from 1 to n
4       for c from 1 to n
5           d = pow(a3 + b3 - c3, 1/3) // Will round to int
6           if a3 + b3 == c3 + d3 // Validate that the value works
7               print a, b, c, d
  • The if statement on line 6 is important. Line 5 will always find a value for d, but we need to check that it’s the right integer value. This will reduce our runtime from O(N4) to O(N3).

Duplicated Work

  • Using the same problem and brute force algorithm as above, let’s look for duplicated work. The algorithm operates by iterating through all (a, b) pairs and then searching all (c, d) pairs to find if there are any matches to that (a, b) pair.
  • Why do we keep on computing all (c, d) pairs for each (a, b) pair? We should create the list of (c, d) pairs. Then, when we have an (a, b) pair, find the matches within the (c, d) list. We can locate the matches by inserting each (c, d) pair into a hash table that maps from the sum to the pair(or the list of pairs that have that sum).
1 n = 1000
2 for c from 1 to n
3   for d from 1 to n
4       result = c3 + d3
5       append (c, d) to list at value map[result]
6 for a from 1 to n
7   for b from 1 to n
8       result= a3 + b3
9       list= map.get(result)
10      for each pair in list
11          print a, b, pair
  • Once we have the map of all the (c, d) pairs, we don’t need to generate the (a, b) pairs since each (a, b) will already be in the map.
1 n = 1000
2 for c from 1 to n
3   for d from 1 to n
4       result = c3 + d3
5       append(c, d) to list at value map[result]
6
7 for each result, list in map
8   for each pair1 in list
9       for each pair2 in list
10          print pair1, pair2
  • This will take our runtime to O(N2).

Optimize & Solve Technique 2: DIY(Do It Yourself)

  • When you get a question, try working it through intuitively on a real example. Often a bigger example will be easier.
  • Example: Given a smaller string s and a bigger string b, design an algorithm to find all permutations of the shorter string within the longer one. Print the location of each permutation.
  • Extraordinarily slow algorithm: Generate all permutations of s and then look for each in b. Since there are S! permutations, this will take O(S! * B) time, where S is the length of s and B is the length of b.
  • Give yourself a big example, like this one:
    s: abbc
    b: cbabadcbbabbcbabaabccbabc
    Where are the permutations of s within b?
  • Almost everyone takes one of two approaches:
    1. Walk through b and look at sliding windows of 4 characters(s.length = 4). Check if each window is a permutation of s.
    2. Walk through b. Every time you see a character in s, check if the next 4 characters are a permutation of s.
  • Depending on the implementation of the “is this a permutation” part, you’ll probably get a runtime of either O(B * S), O(B * Slog S), or O(B * S2). None of these are the most optimal algorithm(there is an O(B) algorithm: Use prime number multiple?).
  • Try this approach when you’re solving questions. Use a nice, big example and manually solve it for the specific example. Then, think hard about how you solved it. Reverse engineer your own approach.

Optimize & Solve Technique 3: Simplify and Generalize

  • With Simplify and Generalize, we implement a multi-step approach.
    1. Simplify or tweak some constraint, such as the data type.
    2. Solve this new simplified version of the problem.
    3. Once have an algorithm for the simplified problem, try to adapt it for the complex version.
  • Example: A ransom note can be formed by cutting words out of a magazine to form a new sentence. How would you figure out if a ransom note(represented as a string) can be formed from a given magazine(string)?
  • To simplify the problem, we can modify it so that we are cutting characters out of a magazine instead of whole words.
  • We can solve the simplified ransom note problem with characters by creating an array and counting the characters. Each spot in the array corresponds to one letter. First, we count the number of times each character in the ransom note appears, and then we go through the magazine to see if we have all of those characters.
  • When we generalize the algorithm, rather than creating an array with character counts, we create a hash table that maps from a word to its frequency.

Optimize & Solve Technique 4: Base Case and Build

  • With Base Case and Build, we solve the problem first for a base case(e.g., n = 1) and then try to build up from there. When we get to more complex cases(say, n = 4), we try to build those using the prior solutions.
  • Example: Design an algorithm to print all permutations of a string. Assume all characters are unique.
  • Consider a test string abcdefg:
    Case “a” –> {“a”}
    Case “ab” –> {“ab”, “ba”}
    Case “abc” –> ?
  • If we had the answer to P(“ab”), we can stick c in at every point to generate P(“abc”):
    P(“abc”) = insert “c” into all locations of all strings in P(“ab”)
    P(“abc”) = insert “c” into all locations of all strings in {“ab”,”ba”}
    P(“abc”) = merge({“cab”, “acb”, “abc”}, {“cba”, abca”, bac”})
    P(“abc”) = {“cab”, “acb”, “abc”, “cba”, “bca”, bac”}
  • Now that we understand the pattern, we can develop a general recursive algorithm. We generate all permutations of a string s1…sn by “chopping off” the last character and generating all permutations of s1…sn-1. Once we have the list of all permutations of s1…sn-1, we iterate through this list. For each string in it, we insert sn into every location of the string.

Optimize & Solve Technique 5: Data Structure Brainstorm

  • We can run through a list of data structures and try to apply each one. This approach is useful because solving a problem may be trivial once it occurs to us to use, say, a tree.
  • Example: Numbers are randomly generated and stored into an expanding array. How would you keep track of the median?
  • Data structure brainstorm might look like the following:
    1. Linked list: No. Linked lists tend not to do well with accessing and sorting numbers.
    2. Array: Maybe, but you already have an array. Could you keep the elements sorted? That’s expensive. Let’s hold off on this and return to it if it’s needed.
    3. Binary tree: Possible, since binary trees do well with ordering. If the binary search tree is perfectly balanced, the top might be the median. But if there’s an even number of elements, the median is the average of the middle two elements.
    4. Heap: Heap is good at basic ordering and keeping track of max and mins. If you had two heaps, you could keep track of the bigger half and the smaller half of the elements.
      (1) The bigger half is kept in a min heap, such that the smallest element in the bigger half is at the root.
      (2) The smaller half is kept in a max heap, such that the biggest element of the smaller half is at the root.
      (3) With two heaps, you have the potential median elements at the roots. If the heaps are not the same size, you can re-balance the heaps by popping an element off the one heap and pushing it onto the other.

Best Conceivable Runtime(BCR)

  • The best conceivable runtime is the best runtime you could conceive of a solution to a problem having. You can prove that there is no way you could beat the BCR.

An Example of How to Use BCR

  • Question: Given two sorted arrays, find the number of elements in common. The arrays are the same length and each has all distinct elements. E.g.:
    A: 13 27 35 40 49 55 59
    B: 17 35 39 40 55 58 60
  • A brute force algorithm is to start with each element in A and search for it in B. This takes O(N2) time since for each of N elements in A, we need to do an O( N) search in B.
  • The BCR is O(N) because we know we will have to look at each element at least once and there are 2N total elements.
  • Let’s think about where we are now. We have an O(N2) algorithm and we want to do better than that, as fast as O(N).
    Brute Force: O(N2)
    Optimal Algorithm: ?
    BCR: O(N)
  • What is between O(N2) and O(N)? Infinite things. We could theoretically have an algorithm that’s O(N log(log(log(log(N))))). However, both in interviews and in real life, that runtime doesn’t come up a whole lot.
    —Try to remember this for your interview because it throws a lot of people off. Runtime is not a multiple choice question. It’s common to have a runtime that’s O(log N), O(N), O(N log N), O(N2) or O(2N). You shouldn’t assume that something has a particular runtime by sheer process of elimination. In fact, those times when you’re confused about the runtime and so you want to take a guess—those are the times when you’re likely to have a less common runtime. Maybe the runtime is O(N2K), where N is the size of the array and K is the number of pairs. Derive, don’t guess.
  • Most likely, we’re driving towards an O(N) algorithm or an O(N log N) algorithm. What does that tell us? If we imagine our current algorithm’s runtime as O(N * N), then getting to O(N) or O(N * log N) mean reducing that second O(N) in the equation to O(1) or O(log N).
    —This is one way that BCR can be useful. We can use the runtimes to get a “hint” for what we need to reduce.
  • The second O(N) comes from searching. We can use binary search to find an element in a sorted array in O(log N) time. We now have an improved algorithm: O(N log N).
    Brute Force: O(N2)
    Improved Algorithm: O(N log N)
    Optimal Algorithm: ?
    BCR: O(N)
  • Doing better means reducing that O(log N) to O(1). The BCR is telling us that we will never have an algorithm that’s faster than O(N). Therefore, any work we do in O(N) time is a “freebie”, it won’t impact our runtime.
    —This is another place where BCR can be useful. Any work you do that’s less than or equal to the BCR is “free” in the sense that it won’t impact your runtime.
  • One of the tips on page 64 suggests pre-computing or doing upfront work. In this case, we can throw everything in B into a hash table. This will take O(N) time. Then, we go through A and look up each element in the hash table. This look up is O(1), so our runtime is O(N).
  • Suppose our interviewer hits us with a question: “Can we do better?”
    No, not in terms of runtime. We have achieved the fastest possible runtime, therefore we cannot optimize the big O time. We could potentially optimize the space complexity.
    —This is another place where BCR is useful. It tells us that we’re “done” in terms of optimizing the runtime, and we should therefore turn efforts to the space complexity.
  • We would have achieved the exact same runtime if the data wasn’t sorted. So why did the interviewer give us sorted arrays?
  • We’re now looking for an algorithm that:
    1. Operate in O(1) space. We already have an O(N) space algorithm with optimal runtime, so we need to drop the hash table.
    2. Operate in O(N) time.
    3. Use the fact that the arrays are sorted.
  • Our best algorithm that doesn’t use extra space was the binary search one. Let’s think about optimizing that.
1. Do a binary search in B for A[0] = 13. Not found.
2. Do a binary search in B for A[1] = 27. Not found.
3. Do a binary search in B for A[2] = 35. Found at B[l].
4. Do a binary search in B for A[3] = 40. Found at B[5].
5. Do a binary search in B for A[4] = 49. Not found.
6. ...
  • Think about BUD. The bottleneck is the searching. It’s not necessary to do a binary search. We can do a linear search: the linear search in B picks up where the last one left off.
1. Do a linear search in B for A[0] = 13. Start at B[0] = 17. Stop at B[0] = 17. Not found.
2. Do a linear search in B for A[1] = 27. Start at B[0] = 17. Stop at B[l] = 35. Not found.
3. Do a linear search in B for A[2] = 35. Start at B[l] = 35. Stop at B[l] = 35.Found.
4. Do a linear search in B for A[3] = 40. Start at B[2] = 39.Stop at B[3] = 40. Found.
5. Do a linear search in B for A[4] = 49. Start at B[3] = 40. Stop at B[ 4] = 55. Found.
6. ...
  • This algorithm operates in O(N) time and O(1) space. We have now reached the BCR and have minimal space. We know that we cannot do better.
    —This is another way we can use BCR. If you reach the BCR and have O(1) additional space, you can’t optimize the big O time or space.

Handling Incorrect Answers

  • One of the most dangerous rumors is that candidates need to get every question right. That’s not quite true.
  • First, responses to interview questions shouldn’t be thought of as “correct” or “incorrect”. When I evaluate how someone performed in an interview, I never think, “How many questions did they get right?” Rather, it’s about how optimal their final solution was, how long it took them to get there, how much help they needed, and how clean was their code. There is a range of factors.
  • Second, your performance is evaluated in comparison to other candidates. For example, if you solve a question optimally in 15 minutes, and someone else solves an easier question in five minutes, did that person do better than you? Maybe not. If you are asked easy questions, then you might be expected to get optimal solutions quickly. But if the questions are hard, then a number of mistakes are expected.
  • Third, many questions are too difficult to expect even a strong candidate to immediately spit out the optimal algorithm. The questions I tend to ask would take strong candidates typically 20 to 30 minutes to solve.

When You’ve Heard a Question Before

  • If you’ve heard a question before, admit this to your interviewer.
    1. Your interviewer is asking you these questions in order to evaluate your problem solving skills. If you already know the question, then you aren’t giving them the opportunity to evaluate you.
    2. Your interviewer may find it highly dishonest if you don’t reveal that you know the question.

The “Perfect” Language for Interviews

  • Many top companies are more interested in how well you solve the problems than whether you know a specific language. If you have several good languages, you should keep in mind the following.

Prevalence

  • It is ideal for your interviewer to know the language you’re coding in.

Language Readability

  • Even if your interviewer doesn’t know your programming language, they should be able to basically understand it. Some languages are more naturally readable than others, due to their similarity to other languages.

Potential Problems

  • Some languages open you up to potential issues. For example, using C++ means that, in addition to all the usual bugs you can have in your code, you can have memory management and pointer issues.

Verbosity

  • Some languages are more verbose than others. Java for example is a verbose language as compared with Python.
// Python:
1 diet {"left": 1, "right": 2, "top": 3, "bottom": 4};

// Java: 
l HashMap<String, Integer> diet = new HashMap<String, Integer>().
2 diet.put("left", 1);
3 dict.put("right", 2);
4 diet.put("top", 3);
5 dict.put("bottom", 4);
  • Some of the verbosity of Java can be reduced by abbreviating code. I could imagine a candidate on a whiteboard writing something like this:
1   HM<S, I> diet = new HM<S, I>().
2   diet.put("left", 1);
3           "right", 2
4           "top", 3
5           "bottom", 4
  • The candidate would need to explain the abbreviations, but most interviewers wouldn’t mind.

Ease of Use

  • Some operations are easier in some languages than others. For example, in Python, you can easily return multiple values from a function. In Java, the same action would require a new class. This can be handy for certain problems.
  • This can be mitigated by just abbreviating code or presuming methods that you don’t actually have. For example, if one language provides a function to transpose a matrix and another language doesn’t, this doesn’t make the first language much better to code in(for a problem that needs such a function). You could assume that the other language has a similar method.

What Good Coding Looks Like

  • Good code has the following properties:
    1. Correct: The code should operate correctly on all expected and unexpected inputs.
    2. Efficient: The code should operate as efficiently as possible in terms of both time and space. This “efficiency” includes both the asymptotic(big O) efficiency and the practical, real-life efficiency. That is, a constant factor get dropped when you compute the big O time, but in real life, it can very much matter.
    3. Simple: Code should be short, so it is possible for a developer to write quickly.
    4. Readable: A different developer should be able to read your code and understand what it does and how it does it. Readable code has comments where necessary, but it implements things in an easily understandable way. That means that your fancy code that does a bunch of complex bit shifting is not necessarily good code.
    5. Maintainable: Code should be adaptable to changes during the life cycle of a product and should be easy to maintain by developers.
  • Striving for these aspects requires a balancing act. For example, it’s advisable to sacrifice some degree of efficiency to make code more maintainable, and vice versa.
  • You should think about these elements as you code during an interview. The following aspects of code are specific ways to demonstrate the earlier list.

Use Data Structures Generously

  • Suppose you were asked to write a function to add two mathematical expressions which are of the form “Axa + Bxb + …”(where the coefficients and exponents can be any positive or negative real number). The interviewer also adds that she doesn’t want you to have to do string parsing, so you can use whatever data structure you’d like to hold the expressions.
  • Bad Implementation
    A bad implementation would be to store the expression as a single array of doubles, where the kth element corresponds to the coefficient of the xk term in the expression. This structure is problematic because it could not support expressions with negative or non-integer exponents. It would also require an array of 1000 elements to store just the expression x1000
1 int[] sum(double[] expr1, double[] expr2) {
2
3 }
  • Less Bad Implementation
    A less bad implementation would be to store the expression as a set of two arrays, coefficients and exponents. Under this approach, the terms of the expression are stored in any order, but “matched” such that the ith term of the expression is represented by coefficients[i] * x exponents[i], i.e., if coefficients[p] = k and exponents[p] = m, then the pth term is k*xm.
    You need to keep track of two arrays for just one expression. Expressions could have undefined values if the arrays were of different lengths. And returning an expression is annoying because you need to return two arrays.
1 ??? sum(double[] coeffsl, double[] expon1, double[] coeffs2, double[] expon2) {
2
3 }
  • Good Implementation.
    A good implementation is to design your own data structure for the expression.
class ExprTerm
{
    double coefficient;
    double exponent;
}

ExprTerm[] sum(ExprTerm[] exprl, ExprTerm[] expr2)
{
    ...
}

Appropriate Code Reuse

  • Suppose you were asked to write a function to check if the value of a binary number(passed as a string) equals the hexadecimal representation of a string.
  • An elegant implementation of this problem leverages code reuse.
boolean compareBinToHex(String binary, String hex)
{
    int nl = convertFromBase(binary, 2);
    int n2 = convertFromBase(hex, 16);
    if (nl< 0 || n2 < 0)
    {
        return false;
    }
    return nl == n2;
}

int convertFromBase(String number, int base)
{
    if (base< 2 || (base> 10 && base!= 16))
    {
        return -1;
    }
    int value = 0;
    for (int i= number.length() - 1; i >= 0; i--)
    {
        int digit = digitToValue(number.charAt(i));
        if (digit< 0 || digit>= base)
        {
            return -1;
        }
        int exp= number.length() - 1 - i;
        value += digit * Math.pow(base, exp);
    }
    return value;
}

int digitToValue(char c)
{
    ...
}
  • We could have implemented separate code to convert a binary number and a hexadecimal code, but this makes our code harder to write and harder to maintain. Instead, we reuse code by writing one convertFromBase method and one digitToValue method.

Modular

  • Writing modular code means separating isolated chunks of code out into their own methods. This helps keep the code more maintainable, readable, and testable.
  • Imagine you are writing code to swap the minimum and maximum element in an integer array. You could implement it all in one method like this:
void swapMinMax(int[] array)
{
    int minindex= 0;
    for (int i= 1; i < array.length; i++)
    {
        if (array[i] < array[minindex])
        {
            minindex = i;
        }
    }

    int maxindex= 0;
    for (int i = 1; i < array.length; i++)
    {
        if (array[i] > array[maxindex])
        {
            maxindex= i;
        }
    }

    int temp = array[minindex];
    array[minindex] = array[maxindex];
    array[maxindex] = temp;
}
  • You could implement in a more modular way by separating the relatively isolated chunks of code into their own methods.
void swapMinMaxBetter(int[] array)
{
    int minindex = getMinindex(array);
    int maxindex= getMaxindex(array);
    swap(array, minindex, maxindex);
}

int getMinindex(int[] array) {...}
int getMaxindex(int[] array) {...}
void swap(int[] array, int m, int n) {...}
  • The modular code is easily testable because each component can be verified separately. As code gets more complex, it becomes increasingly important to write it in a modular way. This will make it easier to read and maintain.

Flexible and Robust

  • Just because your interviewer only asks you to write code to check if a normal tic-tac-toe board has a winner, doesn’t mean you must assume that it’s a 3*3 board. Why not write the code in a general way that implements it for an N*N board?
  • Writing flexible, general-purpose code means using variables instead of hard-coded values or using templates/generics to solve a problem. If we can write our code to solve a more general problem, we should.
  • Of course, there is a limit. If the solution is much more complex for the general case, and it seems unnecessary at this point in time, it may be better just to implement the simple, expected case.

Error Checking

  • A careful coder doesn’t make assumptions about the input, she validates that the input is what it should be either through ASSERT or if statements.
  • Example: convert a number from its base i representation to an int.
int convertToBase(String number, int base)
{
    if (base < 2 || (base > 10 && base != 16))
    {
        return -1;
    }
    int value = 0;
    for (int i = number.length() - 1; i >= 0; i--)
    {
        int digit = digitToValue(number.charAt(i));
        if (digit < 0 || digit >= base)
        {
            return -1;
        }
        int exp= number.length() - 1 - i;
        value = + digit * Math.pow(base, exp);
    }
    return value;
}
  • In line 2, we check to see that base is valid(we assume that bases greater than 10, other than base 16, have no standard representation in string form). In line 6, we do another error check: making sure that each digit falls within the allowable range.
    Checks like these are critical in production code and, therefore, in interview code as well.
  • Of course, writing error checks can waste precious time in an interview. The important thing is to point out that you would write the checks. If the error checks are much more than a quick if-statement, it may be best to leave some space where the error checks would go and indicate to your interviewer that you’ll fill them in when you’re finished with the rest of the code.

Don’t Give Up!

  • It’s important that you step up and eagerly meet a tricky problem head-on. Remember that interviews are supposed to be hard. It shouldn’t be a surprise when you get a really tough problem. For extra “points” show excitement about solving hard problems.

Please indicate the source: http://blog.csdn.net/gaoxiangnumber1

Welcome to my github: https://github.com/gaoxiangnumber1

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值