Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow language switching with literate mode #52

Merged
merged 5 commits into from
Mar 12, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion book/analysis/an-anagram-detection-example.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,10 @@ made up of symbols from the set of 26 lowercase alphabetic characters.
Our goal is to write a boolean function that will take two strings and
return whether they are anagrams.

<!-- litpy analysis/anagrams.py -->
<!-- language python -->
<!-- literate analysis/anagrams.py -->
<!-- /language -->

<!-- language javascript -->
<!-- literate analysis/anagrams.js -->
<!-- /language -->
245 changes: 245 additions & 0 deletions book/analysis/anagrams.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
/*
Solution 1: Checking Off
------------------------

Our first solution to the anagram problem will check to see that each
character in the first string actually occurs in the second. If it is
possible to “checkoff” each character, then the two strings must be
anagrams. Checking off a character will be accomplished by replacing it
with the special JavaScript value `null`. However, since strings in JavaScript
are immutable, the first step in the process will be to convert the
second string to an array. Each character from the first string
can be checked against the characters in the list and if found, checked off by
replacement. An implementation of this strategy may look like this:
*/

function anagramCheckingOff(string1, string2) {
if (string1.length != string2.length) {
return false;
}

var string2ToCheckOff = string2.split("");

for (var i = 0; i < string1.length; i++) {
var letterFound = false;
for (var j = 0; j < string2ToCheckOff.length; j++) {
if (string1[i] === string2ToCheckOff[j]) {
string2ToCheckOff[j] = null;
letterFound = true;
break;
}
}
if (!letterFound) {
return false;
}
}

return true;
}

anagramCheckingOff('abcd', 'dcba') // => True
anagramCheckingOff('abcd', 'abcc') // => False

/*
To analyze this algorithm, we need to note that each of the `n`
characters in `s1` will cause an iteration through up to `n` characters
in the list from `s2`. Each of the `n` positions in the list will be
visited once to match a character from `s1`. The number of visits then
becomes the sum of the integers from 1 to `n`. We recognized earlier that
this can be written as

$$
\sum_{i=1}^{n} i = \frac {n(n+1)}{2}
$$

$$
= \frac {1}{2}n^{2} + \frac {1}{2}n
$$

As $$n$$ gets large, the $$n^{2}$$ term will dominate the $$n$$ term and the
$$\frac {1}{2}$$ can be ignored. Therefore, this solution is $$O(n^{2})$$.


Solution 2: Sort and Compare
----------------------------

Another solution to the anagram problem will make use of the fact that
even though `string1` and `string2` are different, they are anagrams only if
they consist of exactly the same characters. So, if we begin by sorting each
string alphabetically, from a to z, we will end up with the same string
if the original two strings are anagrams. Below is a possible
implementation of this strategy. First, we convert each string to an array using
the string method `split`, and then we use the array method `sort` which
lexographically sorts an array in place and then returns the array. Finally, we
loop through the first string, checking to make sure that both strings contain
the same letter at every index.
*/

function anagramSortAndCompare(string1, string2) {
if (string1.length !== string2.length) {
return false;
}

var sortedString1 = string1.split("").sort();
var sortedString2 = string2.split("").sort();

for (var i = 0; i < sortedString1.length; i++) {
if (sortedString1[i] !== sortedString2[i]) {
return false;
}
}

return true;
}

anagramSortAndCompare('abcde', 'edcba') // => True
anagramSortAndCompare('abcde', 'abcd') // => False

/*
At first glance you may be tempted to think that this algorithm is
$$O(n)$$, since there is one simple iteration to compare the *n*
characters after the sorting process. However, the two calls to the
Python `sorted` method are not without their own cost. Sorting is
typically either $$O(n^{2})$$ or $$O(n\log n)$$, so the sorting
operations dominate the iteration. In the end, this algorithm will have
the same order of magnitude as that of the sorting process.

Solution 3: Brute Force
-----------------------

A *brute force* technique for solving a problem typically tries to
exhaust all possibilities. For the anagram detection problem, we can
simply generate a list of all possible strings using the characters from
`s1` and then see if `s2` occurs. However, there is a difficulty with
this approach. When generating all possible strings from `s1`, there are
$$n$$ possible first characters, $$n-1$$ possible characters for the second
position, $$n-2$$ for the third, and so on. The total number of candidate
strings is $$n*(n-1)*(n-2)*...*3*2*1$$, which is $$n!$$. Although some of
the strings may be duplicates, the program cannot know this ahead of
time and so it will still generate $$n!$$ different strings.

It turns out that $$n!$$ grows even faster than $$2^{n}$$ as *n* gets large.
In fact, if `s1` were 20 characters long, there would be
$$20!$$ or 2,432,902,008,176,640,000 possible candidate strings. If we
processed one possibility every second, it would still take us
77,146,816,596 years to go through the entire list. This is probably not
going to be a good solution.

Solution 4: Count and Compare
-----------------------------

Our final solution to the anagram problem takes advantage of the fact
that any two anagrams will have the same number of a’s, the same number
of b’s, the same number of c’s, and so on. In order to decide whether
two strings are anagrams, we will first count the number of times each
character occurs. Since there are 26 possible characters, we can use a
array of 26 counters, one for each possible character. Each time we see a
particular character, we will increment the counter at that position. In
the end, if the two arrays of counters are identical, the strings must be
anagrams. Here is a possible implementation of the strategy:

*/

function anagramCountCompare(string1, string2) {

function getLetterPosition(letter) {
return letter.charCodeAt() - 'a'.charCodeAt();
}

// No "clean" way to prepopulate an array in JavaScript with 0's
var string1LetterCounts = new Array(26);
var string2LetterCounts = new Array(26);

for (var i = 0; i < string1.length; i++) {
var letterPosition = getLetterPosition(string1[i]);
if (!string1LetterCounts[letterPosition]) {
string1LetterCounts[letterPosition] = 1;
} else {
string1LetterCounts[letterPosition]++;
}
}

for (var i = 0; i < string2.length; i++) {
var letterPosition = getLetterPosition(string2[i]);
if (!string2LetterCounts[letterPosition]) {
string2LetterCounts[letterPosition] = 1;
} else {
string2LetterCounts[letterPosition]++;
}
}

for (var i = 0; i < string1LetterCounts.length; i++) {
if (string1LetterCounts[i] !== string2LetterCounts[i]) {
return false;
}
}

return true;
}

anagramCountCompare('apple', 'pleap') // => True
anagramCountCompare('apple', 'applf') // => False

/*
Again, the solution has a number of iterations. However, unlike the
first solution, none of them are nested. The first two iterations used
to count the characters are both based on $$n$$. The third iteration,
comparing the two lists of counts, always takes 26 steps since there are
26 possible characters in the strings. Adding it all up gives us
$$T(n)=2n+26$$ steps. That is $$O(n)$$. We have found a linear order of
magnitude algorithm for solving this problem.

Those with greater familiarity with JavaScript may note that the counting
strategy we implemented in `anagramCountCompare` could be much more
succinctly approached using the `reduce` method of arrays. Note that we are
required to add an additional check that the strings are of the same length
since our dictionary comparison loop will not account for string2 having
additional characters.
*/

function anagramCountCompareWithReduce(string1, string2) {

function letterCountReducer(letterCounts, letter) {
if (letterCounts[letter]) {
letterCounts[letter]++;
} else {
letterCounts[letter] = 0;
}
return letterCounts;
}

var string1LetterCounts = string1.split('').reduce(letterCountReducer, {});
var string2LetterCounts = string2.split('').reduce(letterCountReducer, {});


for (var letter in string1LetterCounts) {
if (string1LetterCounts[letter] !== string2LetterCounts[letter]) {
return false;
}
}

return string1.length === string2.length;
}

anagramCountCompareWithReduce('apple', 'pleap') // => True
anagramCountCompareWithReduce('apple', 'applf') // => False

/*
It is worth noting that `anagramCounterCompareWithReduce` is also $$O(n)$$, but
that it is impossible to determine this without understanding the
implementation of `Array.reduce` method.

Before leaving this example, we need to say something about space
requirements. Although the last solution was able to run in linear time,
it could only do so by using additional storage to keep the two dictionaries of
character counts. In other words, this algorithm sacrificed space in
order to gain time.

This is a common tradeoff. On many occasions you will need to make
decisions between time and space trade-offs. In this case, the amount of
extra space is not significant. However, if the underlying alphabet had
millions of characters, there would be more concern. As a software
engineer, when given a choice of algorithms, it will be up to you to
determine the best use of computing resources given a particular
problem.
*/
2 changes: 1 addition & 1 deletion book/deques/palindrome-checker.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ items, we will eventually either run out of characters or be left with a
deque of size 1 depending on whether the length of the original string
was even or odd. In either case, the string must be a palindrome. A complete implementation for this strategy may look like:

<!-- litpy deques/palindromes.py -->
<!-- literate deques/palindromes.py -->
2 changes: 1 addition & 1 deletion book/graphs/depth-first-search.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ collection: graphs
position: 8
---

<!-- litpy graphs/depth_first_search.py -->
<!-- literate graphs/depth_first_search.py -->
2 changes: 1 addition & 1 deletion book/graphs/dijkstras-algorithm.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ collection: graphs
position: 10
---

<!-- litpy graphs/dijkstras_algorithm.py -->
<!-- literate graphs/dijkstras_algorithm.py -->
2 changes: 1 addition & 1 deletion book/graphs/knights-tour.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ collection: graphs
position: 7
---

<!-- litpy graphs/knights_tour.py -->
<!-- literate graphs/knights_tour.py -->
2 changes: 1 addition & 1 deletion book/graphs/prims-spanning-tree-algorithm.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ The Python code to implement Prim’s algorithm is shown below. Prim’s
algorithm is similar to Dijkstra’s algorithm in that they both use a
priority queue to select the next vertex to add to the growing graph.

<!-- litpy graphs/prims_spanning_tree.py -->
<!-- literate graphs/prims_spanning_tree.py -->

The following sequence of diagrams shows the algorithm in operation on our
sample tree. We begin with the starting vertex as A. Looking at the neighbors
Expand Down
2 changes: 1 addition & 1 deletion book/graphs/word-ladder.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ collection: graphs
position: 6
---

<!-- litpy graphs/word_ladder.py -->
<!-- literate graphs/word_ladder.py -->
2 changes: 1 addition & 1 deletion book/lists/implementing-an-ordered-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ seen previously with unordered lists. We will subclass `UnorderedList` and
leave the `__init__` method intact as once again, an empty list will be
denoted by a `head` reference to `None`.

<!-- litpy lists/ordered_list.py -->
<!-- literate lists/ordered_list.py -->

Analysis of Linked Lists
------------------------
Expand Down
2 changes: 1 addition & 1 deletion book/lists/implementing-an-unordered-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ first item can tell us where the second is, and so on. The external
reference is often referred to as the **head** of the list. Similarly,
the last item needs to know that there is no next item.

<!-- litpy lists/unordered_list.py -->
<!-- literate lists/unordered_list.py -->
2 changes: 1 addition & 1 deletion book/queues/implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ Just like with a stack, it is possible to “use a Python list as a queue”. Ag

_Unlike_ with a stack, the performance implication of using a Python list as a queue is significant. The implementation shown below uses `insert(0, item)` to enqueue a new item, which will be an $$O(n)$$ operation.

<!-- litpy queues/queue.py -->
<!-- literate queues/queue.py -->

In practice, many Python programmers will use the standard library’s `collections.deque` class to achieve $$O(1)$$ enqueues and dequeues. We will cover deques in depth in the next chapter; for now consider deques to be a combination of a stack and a queue, enabling $$O(1)$$ pushing and popping from both ends.
2 changes: 1 addition & 1 deletion book/queues/simulating-hot-potato.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ until only one name remains (the size of the queue is 1).

A possible implementation of this simulation is:

<!-- litpy queues/hot_potato.py -->
<!-- literate queues/hot_potato.py -->

Note that in this example the value of the counting constant is greater
than the number of names in the list. This is not a problem since the
Expand Down
2 changes: 1 addition & 1 deletion book/recursion/calculating-the-sum-of-a-list-of-numbers.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ collection: recursion
position: 2
---

<!-- litpy recursion/sum_of_numbers.py -->
<!-- literate recursion/sum_of_numbers.py -->
2 changes: 1 addition & 1 deletion book/recursion/converting-an-integer-to-a-string.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ side of the diagram.

Below is a Python implementation of this algorithm for any base between 2 and 16.

<!-- litpy recursion/base_conversion.py -->
<!-- literate recursion/base_conversion.py -->

Notice that we check for the base case where `n` is less than
the base we are converting to. When we detect the base case, we stop
Expand Down
Loading