-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathnotes
2911 lines (2901 loc) · 208 KB
/
notes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
• Study guide:
• General:
• To do:
• Repeat the problem out loud, slowly but confidently.
• Ask questions, check assumptions, know what the constraints are.
• Talk out the problem!
• Work out examples on the whiteboard, devising some kind of brute force algorithm along the way.Don't stay silent here.
• Start with naive simplest brute force solution.
• Check edge cases and do null checks.
• Work out examples using the code/algorithm; keep track of state by hand.
• When debugging, don't rush. Don't guess and check—make sure I know exactly what went wrong and how to fix it. Preventatively: don't try to be too clever.
• Know time/space complexity: http://bigocheatsheet.com/
• Heuristics and guidelines:
• Always consider hash tables (dictionaries) with their O(1)-ness.
• "Tip: using a dictionary is the most common way to get from a brute force approach to something more clever. It should always be your first thought."
• Sets are also extremely useful if all I care about is unique elements and membership testing.
• Don't be afraid of using one even for graph traversals. Worry about optimizing space later.
• If at all array-related, try sorting first.
• If search-related, consider binary search.
• "Binary search teaches us that when a list is sorted or mostly sorted:
• The value at a given index tells us a lot about what's to the left and what's to the right.
• We don't have to look at every item in the list. By inspecting the middle item, we can "rule out" half of the list.
• We can use this approach over and over, cutting the problem in half until we have the answer. This is sometimes called "divide and conquer.""
• For binary search, consider floor and ceiling as exclusive walls.
• Start with a brute force solution, look for repeat work in that solution, and modify it to only do that work once.
• "Instead, we started off by coming up with a slow (but correct) brute force solution and trying to improve from there. We looked at what our solution actually calculated, step by step, and found some repeat work. Our final answer came from brainstorming ways to avoid doing that repeat work."
• Space-time trade-off! That is, for better time complexity, try using auxiliary data structures. E.g., do something in a single pass over an array—O(N) time—by using a hash table—O(N) space—vs. doing something in nested passes—O(N^2)—without using any extra space—O(1).
• What information can I store to save time? Don't be scared of using tuples.
• Counting sort (where the items are stored as keys/indices and the frequency is the value) is a big example here.
• Another example: O(1) get_max method for a Stack class stores extra information (the max at and below each element) to save time (instead of iterating through the stack O(N)).
• Remember that I can use two pointers:
• E.g.: to get the midpoint in a linked list by having one pointer go twice as fast, or in a sum array problem by having the pointers work inward from either end, or to test if a string is a palindrome.
• Also, the "stick" approach where to get the first node in a cycle, you start one pointer n nodes ahead of the other, where n is the length of the cycle.
• Don't forget that I can also start at the center and expand outward, like in the palindromic substring problem: https://leetcode.com/problems/longest-palindromic-substring/description/.
• Try a greedy solution:
• Iterate through the problem space taking the best solution "so far" (running sum etc.) until the end.
• Optimal if the problem has "optimal substructure," which means stitching together optimal solutions to subproblems yields an optimal solution.
• Also usually necessary for O(N) solutions.
• "Suppose we could come up with the answer in one pass through the input, by simply updating the 'best answer so far' as we went. What additional values would we need to keep updated as we looked at each item in our input, in order to be able to update the 'best answer so far' in constant time?"
• Does solving the problem for size (N – 1) make solving it for size N any easier? If so, try to solve recursively and/or with dynamic programming.
• Using the max/min function can help a lot in recursive or dynamic programming problems.
• To use, always think about starting from the end, i.e., if I had an answer for everything but the last something (element, cell, subproblem, etc.), what more do I need? Basically the n+1 step of an induction proof.
• Think: at step n, what happens if I *do* include this thing? What happens if I don't? What do I need to compare and take the max of? And how do I use my answer to step n-1?
• Start with a simple cache! Then move on to a tabular building-from-bottom-up approach.
• Always think recursion for binary trees. But remember that iterative solutions are usually better because they avoid possible stack overflows and can be more space-efficient.
• A lot of problems can be treated as graph problems and/or use breadth-first or depth-first traversal. And if the problem involves parsing or reversal in some way, consider using a stack.
• Any time you repeatedly have to take the min or max of a dynamic collection, think heaps. (If you don’t need to insert random elements, prefer a sorted array.)
• Python has a useful heapq class (min-heap with 0-based indexing).
• Particularly useful if there are multiple such possible min/max values and some of them are in the "past" (precluding only having variables for the lowest 1-2 elements).
• If you have a lot of strings, try putting them in a prefix tree / trie.
• To improve time complexity, also consider how various complexities match to solution structures and try working backwards from a target runtime:
• "If we're going to do better than O(N^2), we're probably going to do it in either O(N lg N) or O(N). O(N lg N) comes up in sorting and searching algorithms where we're recursively cutting the list in half. It's not obvious that we can save time by cutting the list in half here. Let's first see how well we can do by looping through the list only once."
• Remember that time complexity is asymptotic, so don't worry about iterating through a list multiple times—it's still O(N).
• "Starting with a target runtime and working backward from there can be a powerful strategy for all kinds of coding interview questions.":
• We started with an O(N^2) brute force solution and wondered if we could do better.
• We knew to beat O(N^2), we'd probably do O(N) or O(N lg N), so we started thinking of ways we might get an O(N lg N) runtime.
• lg(N) usually comes from iteratively cutting stuff in half, so we arrived at the final algorithm by exploring that idea.
• Try simplifying or otherwise restating what the question is asking for. Sometimes an explicit restatement (instead of relying on implicit assumptions about a plan of attack) makes things much easier.
• "The requirement of "the difference between the depths of any two leaf nodes is no greater than 1" implies that we'll have to compare the depths of all possible pairs of leaves. That'd be expensive—if there are n leaves, there are O(N^2) possible pairs of leaves.
• But we can simplify this requirement to require less work. For example, we could equivalently say:
• "The difference between the min leaf depth and the max leaf depth is 1 or less"
• "There are at most two distinct leaf depths, and they are at most 1 apart""
• Break up a problem into independent cases and draw out sample inputs for the cases.
• "Breaking things down into cases is another strategy that really helped us here.
• Notice how simple finding the second largest node got when we divided it into two cases:
• The largest node has a left subtree.
• The largest node does not have a left subtree.
• Whenever a problem is starting to feel complicated, try breaking it down into cases.
• It can be really helpful to actually draw out sample inputs for those cases. This'll keep your thinking organized and also help get your interviewer on the same page."
• Not quite the same as N-1, but sometimes a divide-and-conquer approach is what is necessary. If I know the answer for exclusive parts of the problem, can I somehow combine to get the final answer?
• Avoid passing copies of subarrays, instead pass low and high indices.
• For puzzle problems or anything where we can enumerate all possible solutions and there's a notion of a partial candidate solution, consider backtracking.
• "Backtracking is an important tool for solving constraint satisfaction problems, such as crosswords, verbal arithmetic, Sudoku, and many other puzzles. It is often the most convenient (if not the most efficient) technique for parsing, for the knapsack problem and other combinatorial optimization problems."
• Backtracking is a powerful technique. It allows us to try all possible configurations/solutions, but in an optimal and incremental order, so that we can eliminate wholesale bad approaches. Basically we generate partial solutions at each iteration: if it passes the constraint check, we recur deeper (DFS style); if not, we continue / "prune" that entire branch and backtrack up the search tree to an unexplored branch. We generate new solutions through a loop breadth-wise and explore them further through DFS recurrence. The n queens problem is a typical example.
• Just remember, we need a quick test to see if our candidate solution so far satisfies our constraints, and then we use DFS to explore further.
• Choose, explore, unchoose. The unchoose part is critical.
• If it is anything to do with even/odd-ness, consider "canceling" even elements out. Don't need to track exact numbers.
• Especially if a boolean function, think about how we can short-circuit and return early.
• Also important for breaking out of loops early.
• For backtracking (not "real" backtracking) or reconstructing going backward, think about what additional information we'd need.
• "The tricky part was backtracking to assemble the path we used to reach our end_node. In general, it's helpful to think of backtracking as two steps:
• Figuring out what additional information we need to store in order to rebuild our path at the end (how_we_reached_nodes, in this case).
• Figuring out how to reconstruct the path from that information."
• Bitwise Operations:
• Integers are represented as a string of bits (binary digits). Either big endian (akin to most significant place value at left) or little endian (akin to most significant place value at right). So three-hundred twenty-one as 321 or 123.
• If unsigned integer, the number of bits used (the width) determines the numbers that can be encoded: 2^n.Unsigned means that the number is always positive; negatives aren't supported.
• If signed integer, 4 methods; most common is 2's complement where negating a number (whether positive or negative) is done by inverting all the bits and then adding 1.
• This has the advantage of having only one representation of 0 (instead of negative 0 and positive 0).
• With 2's complement: "a signed integral type with n bits [...] represent[s] numbers from −2^(n−1) through 2^(n−1) − 1." So with 8 bits, the numbers from -128 to 127 can be encoded.
• Java supports the following 2's complement signed numerical primitives (does not support unsigned numbers really):Also called integral data types.
• byte -- 8 bitsE.g.: from −128 to 127.
• short -- 16 bitschar -- unsigned 16 bits
• int -- 32 bits
• long -- 64 bits
• Python supports 4 primary numeric types:https://docs.python.org/2/library/stdtypes.html
• int -- Implemented using C's long, giving at least 32 bits of precision.
• float -- Implemented using C's double, number of bits of precision depends on machine.
• long -- Unlimited precision; expands to the limit of the available memory (not limited by number of bits).
• complex -- Have a real and imaginary part, each of which is a float.
• There are also fractions -- that hold rationals -- and decimal -- that hold floating-point numbers with user-definable precision.
• Arithmetic:
• Adding works as normal. Just remember that 1 + 1 = 10, so gotta carry the 1.
• With subtraction, as normal but just be careful with borrowing from the left. If substracting 1 from 0 (0 - 1), borrow from the first place to the left where it's 1 - 0. If have to borrow multiple times, the leftmost digit (the one I'm really borrowing from) becomes a 0, the intervening digits get a decimal place added to them and then since they get borrowed from too a 1 is subtracted (so a 0 becomes 10 then 1 and a 1 becomes 11 then 10), and the original digit that needed to be increased becomes a 10.http://sandbox.mc.edu/~bennet/cs110/pm/sub.html
• Multiplication can be done with left bitshifts. So because 3 * 5 is equivalent to 3 * (4 + 1), you can bitshift 3 to the left 2 places (which is 3 * 2^2 = 3 * 4) and then add 3 (3 * 1) back in.
• Division can be done with right bitshifts, but just remember that it's integer division--rounded down basically.
• Bitwise operations:
• & = AND
• ^ = XOR
• | = (inclusive) OR
• x << n = left-shifts x by n places (0s appended at the end). Equivalent to x * 2^n
• x >> n = right-shifts x by n places using sign extension. Equivalent to x / 2^n (integer division).In Python, right-shift 0-fills.
• x >>> n = right-shifts x by n places where 0s are shifted into the leftmost spots.Only in Java.
• ~ = flips bits (unary operator)
• Bit facts & tricks:Since operations are bitwise (occur bit by bit), these are all equivalent to their series-equivalents. E.g.: x ^ 0 is the same as x ^ 0s. If a statement is true for a single bit, it's true for a sequence of bits.
• ^ (XOR)
• x ^ 0 = x
• x ^ 1 = ~x~ = negation
• x ^ x = 0
• & (AND)
• x & 0 = 0
• x & 1 = x
• x & x = x
• | (inclusive OR)
• x | 0 = x
• x | 1 = 1
• x | x = x
• Swapping two values without a temporary variable:
• a = a ^ b
• b = a ^ b
• a = a ^ b
• E.g.: a = 2, b = 3; a = 0010, b = 0011.
• a = a ^ b = 0010 ^ 0011 = 0001
• b = a ^ b = 0001 ^ 0011 = 0010
• a = a ^ b = 0001 ^ 0010 = 0011
• a = 3, b = 2.
• Common (and not-so-common) bit tasks:
• Get the value of the bit at i in num. Specifically, return whether the ith bit is 1.
• boolean getBit(int num, int i) {
• return ((num & (1 << i)) != 0);
• }
• First left-shift 1 by i to get something like 00100000. Then AND with num to clear (set to 0) all the bits except the ith bit. If that value is not 0, then it's 1 and return true. Else it's 0 so return false.
• To be more precise, the ANDing doesn't quite clear. It just makes it so that there are only two cases: all the bits are 0 or all the bits except the ith bit is 0. In the first case, the ith bit must be 0; in the second, it must be 1.
• Set num's ith bit to 1.
• int setBit(int num, int i) {
• return num | (1 << i);
• }
• First left-shift 1 by i to again get something like 00100000. Then OR with num so that the ith bit will become 1 and the other values will not change. Recall that x OR-ing with 1 will always result in 1 and that x OR-ing with 0 will not change x.
• Clear num's ith bit. Specifically, set the ith bit to 0.
• int clearBit(int num, int i) {
• int mask = ~(1 << i);
• return num & mask;
• }
• Do something like the inverse of set (naturally). Left-shift 1 by i to again get something like 00100000. Then invert it so it looks like 11011111. AND with num: ANDing with 1 doesn't change anything, so the only bit that will change is the ith one, which will become a 0 if it isn't already.
• Update num's ith bit with v. Specifically, set the ith bit to v.
• int updateBit(int num, int i, int v) {
• int mask = ~(1 << i);
• return (num & mask) | (v << i);
• }
• The first part is equivalent to the clear bit method. Create a mask that looks like 11011111 then AND with num to clear the ith bit. Next, left-shift v by i bits so that v becomes a number where the ith bit is equal to what v was and all the other bits are 0. Finally, OR with the modified num (ith bit cleared) so that num's ith bit becomes 1 if v is 1 or is otherwise left as 0--recall that no other bits in num are modified since ORing with a 0 does not change the original.
• Clear (unset) rightmost set bit. That is, change rightmost 1 to 0.
• int clearRightmost(int num) {
• return num & (num-1);
• }
• So, e.g., a 12 (1100) becomes a 1000 since ANDing 1100 and 1011 is 1000.
• This forms the basis of the operation to determine parity.
• Calculate parity. That is, return 1 (true) if the number of 1s is odd and 0 otherwise.EPI 5.1. #link http://www.geeksforgeeks.org/write-a-c-program-to-find-the-parity-of-an-unsigned-integer/ and https://graphics.stanford.edu/~seander/bithacks.html#OperationCounting
• Naive method:
• boolean getParity(int num) {
• boolean parity = false;
• while (num) {
• parity = !parity;
• num = num & (num - 1);
• }
• return parity;
• }
• Each time through the loop (until num is 0), clear the rightmost set bit and invert parity. Every odd number of clears, parity will be true / 1, so in the end, parity will be 1 if the number of 1s is odd and 0 otherwise.
• Time complexity is the number of set bits.
• We can also get the rightmost bit and xor with a 0/1 result. If 0, result doesn't change. Otherwise, result flips.
• Not-so-naive method uses a precomputed lookup table. So for example the table could have the parities of the values from 0 through 15 inclusive. And then to find the parity of a 64 bit number, you would go 16 bits at a time by right shifting and XORing the 16 bit parities together.
• This works because bitwise operations are associative (i.e., the grouping doesn't matter).
• Get the Hamming distance.https://leetcode.com/problems/hamming-distance/#/description
• "The Hamming distance between two integers is the number of positions at which the corresponding bits are different.
• Given two integers x and y, calculate the Hamming distance."
• def hammingDistance(self, x, y):
• # Result has 1 only in places where corresponding bits are different.
• z = x ^ y
• ct = 0
• # Count the number of 1s.
• while z:
• ct += 1
• # Unsets the rightmost 1.
• z &= z - 1
• return ct
• Arrays Dictionaries and Strings:
• Hash tables are important! Note that insertion and find/lookup proceed through identical starting steps: hash the key and go to that table location. This enables the O(1) complexity for both.
• Chaining is the common way to handle collisions. Just means each position is actually the head of a linked list.
• Open addressing is the other method. Deletions are tricky with this.
• Java has built-in hashcodes.
• Python's dictionaries are implemented using hash tables. They are simply arrays whose indexes are obtained using a hash function on the keys.
• Motivation:
• "Think of a hash map as a "hack" on top of an array to let us use flexible keys instead of being stuck with sequential integer "indices."
• All we need is a function to convert a key into an array index (an integer). That function is called a hashing function."
• For the common case where I increment a key if it exists, but set it to 0 otherwise, use the get method:
• my_dict[some_key] = my_dict.get(some_key, 0) + 1
• Arrays offer easy random access but hard modification (have to move elements around).
• In the worst case a larger array has to be reallocated too (but amortizes out).
• In Java:
• Use StringBuilder for expensive string concatenation. And for easy string reversal.
• Everything is initialized to 0, including arrays.
• Remember that chars are also ints.
• Python only really has lists; arrays are just thin wrappers over C arrays. Always use lists unless have a really good explicit reason to need C-style arrays.
• Linked Lists:
• All a linked list really is is the head node (which points to the next node, and so on).
• To reverse a linked list, just need to remember to store next and update prev and curr pointers at each iteration:
• nxt = curr.next
• curr.next = prev
• prev = curr
• curr = nxt
• To recursively reverse:
• base case is null node or null node.next
• remember to set head.next.next to head and then head.next = None
• think about the origin link list : 1->2->3->4->5.Now assume that the last node has been reversed.Just like this:
1->2->3->4<-5.And this time you are at the node 3 , you want to change 3->4 to 3<-4 , means let 3->next->next = 3.(3->next is 4 and 4->next = 3 is to reverse it)
• Deleting a node n is just setting the references to skip n: prev.next = n.next;
• Can also delete a curr node even without a prev pointer: curr.data = curr.next.data; curr.next = curr.next.next;Basically, copy (and overwrite) next's data to curr and delete next.
• Just make sure to check for the null pointer (!!) and be aware of the head pointer.
• Linked lists offer easy modification but non-existent random access.
• An important technique is the "runner" one:
• Have two pointers iterating through the list, with one pointer either ahead by a fixed amount or actually moving faster.
• For example, to determine the midpoint of a linked list, have two pointers such that one of them jumps 2 nodes at once and the other just 1. When the fast pointer hits the end, the slow one will be in the middle of the list.
• To determine if a linked list has a cycle, do the same thing where 1 pointer moves ahead 2 nodes at a time. If there's a cycle, the runner will eventually equal the normal curr node. If not, will hit null node.
• https://www.wikiwand.com/en/Cycle_detection#/Tortoise_and_hare
• def hasCycle(self, head):
• slow = fast = head
• while fast and fast.next:
• slow = slow.next
• fast = fast.next.next
• if slow == fast: return True
• return False
• then, once fast = slow, reset slow to head of list, and move forward both pointers 1 at a time. once they equal again, that's the start of the cycle. then move the fast pointer 1 at a time, once they equal for the 3rd time, that's the length of the cycle
• can also use this to detect duplicates in an array given certain constraints
• http://keithschwarz.com/interesting/code/?dir=find-duplicate
• https://leetcode.com/problems/find-the-duplicate-number/description/
• def detectCycle(self, head):
• slow = fast = head
• has_cycle = False
• while fast and fast.next:
• slow = slow.next
• fast = fast.next.next
• if slow == fast:
• has_cycle = True
• break
• if not has_cycle: return None
• # the point where they meet again, resetting slow to head, is the cycle start
• while head != fast:
• head = head.next
• fast = fast.next
• return head
• or alternatively, if don't need the start index and just want cycle length, move fast 1 node at a time until hits slow again
• A fixed amount (also called the stick approach) use case would be to determine the start of a cycle if you know the length of the cycle. ? what does it mean?
• Recursion can often help.
• Example problems and model code:
• Sort linked list using O(1) space and O(n log n) time:https://leetcode.com/problems/sort-list/description/
• an easy to understand solution here: https://leetcode.com/problems/sort-list/discuss/46714/Java-merge-sort-solution
• def sortList(self, head):
• # base case
• if not head or not head.next: return head
• # get middle (if even, get 1st middle)
• slow = fast = head
• while fast.next and fast.next.next:
• slow = slow.next
• fast = fast.next.next
• # split list in two and sort halves
• h2 = slow.next
• slow.next = None
• left = self.sortList(head)
• right = self.sortList(h2)
• # then merge sorted halves
• return self.merge_sorted_lists(left, right)
• def merge_sorted_lists(self, h1, h2):
• if None in (h1, h2): return h1 or h2
• if h1.val <= h2.val:
• h1.next = self.merge_sorted_lists(h1.next, h2)
• return h1
• else:
• h2.next = self.merge_sorted_lists(h1, h2.next)
• return h2
• Stacks and Queues:
• Stacks are last-in first-out (LIFO), like a stack of dinner plates or trays.
• Queues are first-in first-out (FIFO), like a queue at an amusement park ride.
• Both are easily implemented with a linked list.
• With a stack, there's only one access point: the top (the linked list's head), and nodes point down / toward the tail.
• With a queue, there are two: the first node (the head [of the line]) and the last (the tail). Nodes point back / toward the tail and are added to the tail (!).
• Priority queues are neat structures (technically ADTs) where ordering is determined not by insertion time, but by some priority field:
• Support insertion, find-min (or find-max), and delete-min (delete-max) operations.
• The idea is that if I consistently just want the highest priority element, the priority queue will take care of that for me at insertion time, instead of my having to manually re-sort at extraction time.
• Backing data structures include heaps (and balanced binary trees).
• Generally implemented by keeping an extra pointer to the highest priority element, so on insertion, update iff new element is higher priority, and on deletion, delete then use find-min (or find-max) to restore the pointer.
• Popping everything from a stack and pushing it all to another stack reverses the ordering.Pushing everything back of course reverts the ordering.
• Two stacks are Turing-complete.
• (Binary) Trees:
• A tree is in part defined as a node (the root), which holds some value, together with references to other nodes (the children, themselves trees). Because this also defines a directed graph, these conditions are added:
• at most 1 reference can point to any given node (i.e., a node has no more than 1 parent)
• and no node in the tree points toward the root.
• As such, a tree is really just a special case of a directed graph (digraph):
• The graph definition: a connected (directed in this context, but undirected in, e.g., the context of a spanning tree) graph with no cycles.Formally, directed means that the tree is a rooted tree.
• Binary trees (branching factor = 2) are recursively built from nodes that have pointers to a left and right child.
• Binary trees aren't necessarily binary search trees (BSTs), which require that the left children are less than or equal to the current node which is less than all the right nodes.
• Traversals:
• Depth-first traversal (DFS) prefixes are in reference to when the root/current node is visited. So pre-order traversal means first root, then left children, then right; post-order means first left, then right, then root. In-order traversal will give a sorted list if it's a valid BST. Make sure to recurse fully when traversing.
• DFS generally will hit leaves first/faster.
• In-order traversal pseudocode is simple:
• inorder(left)
• visit(root)
• inorder(right)
• Same for the other traversals:
• visit(root)
• preorder(left)
• preorder(right)
• Only one kind of breadth-first traversal—the level order traversal. This traversal visits nodes by levels from top to bottom and from left to right. Uses a queue.
• To choose between the two, we want to use DFS if we need to hit a leaf fast and BFS if we're more concerned with a "middle" level closer to the root. But if we need to print all leaf nodes, then by necessity they're the same.
• Balanced trees are better. It also doesn't mean perfectly balanced.
• Depth and height:
• A node's depth = the number of edges from the node to the tree's root node. A root node will have a depth of 0. Rise up from depth
• A node's height = the number of edges on the longest path from the node to a leaf. A leaf (root?) node will have a height of 0.
• The depth and height of a tree are equal.
• Full and complete is a tree where all leaves are at the bottom and each non-leaf node has exactly 2 children (sometimes called a perfect tree as in http://www.cs.gettysburg.edu/~ilinkin/courses/Fall-2012/cs216/notes/bintree.pdf).Rare because then must have exactly (2^n) - 1 nodes where n is the number of levels, or (2^ (h+1)) - 1 where h is the height.
• Full = every node other than the leaves has 2 children. I.e., every node either has 0 or 2 children.
• Complete = every level (except maybe the last) is completely filled and all nodes are as far left as possible. I.e., the tree is as compact as possible.
• In a perfect tree, h = O(log n) because h = log2 (n + 1) − 1 and we drop the less significant terms.
• Algo tips:
• For DFS, use a stack as always.
• To validate BST, in-order traversal and comparing to sorted version is O(N lg N)*, but the better O(N) way is to recurse down the tree and set floor and ceiling conditions.
• *Actually we can check if a list is sorted in O(N) time (we don't need to re sort) so same thing.
• For lowest common ancestor (LCA), the LCA is just the first node that is in between the keys I'm looking for. E.g., if a root node is greater than both key nodes, then I know the LCA has to be on the left side of the tree.
• In-order successor:
• Important in a lot of binary tree operations.
• The next node in an in-order traversal; i.e., the node with the smallest key that's still greater than the current node's key.
• Pseudo-pseudocode:
• If current node has a right child, go to it then as far left as possible. Must be smallest key in right subtree.
• Otherwise, must travel up the tree (its left child / subtree is by definition smaller, i.e., can't be successor):
• If current node is the left child of its parent, then the parent must be the successor.
• Otherwise, keep going up until escape the right subtree and hit the middle. I.e., when the current node is a left child, its parent must be the in-order successor. We're looking for its ("upper-right") parent.
• Heaps:
• A natural implementation of the priority queue ADT (the other being a balanced binary tree).
• The highest priority element (whether min or max) recursively sits at the top of the heap.
• Works by maintaining a partial ordering, so less expensive than fully sorted, but more valuable than no ordering.
• A complete tree where the highest (or lowest) priority element is always stored at the root—hence a heap. A max heap is one where a node is always greater than or equal to its children; a min heap is one where a node is always lesser than or equal to its children.
• Are used to implement priority queues—queues where the ordering is based on priority, not on position in queue—and to do heap sort.
• There is no implied ordering within a level (that is, no ordering between siblings) and so a heap is not a sorted structure.
• Insertion and deletion (need to respect the heap ordering property) are both O(log N) where N is the number of nodes.
• (Binary) heaps are implemented in arrays.
• Note that any binary tree can be so implemented, but that it makes particular sense for heaps, because heaps are guaranteed to be complete trees and thus the array implementation will be compact.
• If a non-complete tree is implemented in an array, then there will be empty slots because a node may have only 1 child and the tree may go on quite a bit deeper than that level.
• Saves space because can use array position to implicitly maintain the ordering property, instead of storing pointers.
• In the array implementation:
• Let n be the number of elements in the heap and i be an arbitrary valid index of the array storing the heap. If the tree root is at index 0, with valid indices 0 through n − 1, then each element a at index i has:
• children at indices 2i + 1 and 2i + 2
• and its parent at floor((i − 1) ∕ 2).
• Alternatively, if the tree root is at index 1, with valid indices 1 through n, then each element a at index i has:1-basing sacrifices a tiny amount of space to simplify index arithmetic.
• children at indices 2i and 2i +1
• and its parent at index floor(i ∕ 2).
• Operations:
• Both insert and remove — remove only removes the root since that's all we care about — are done to an end of the heap to maintain the compact shape. Then, to modify the ordering property, the heap is traversed (respectively, up-heap / sift-up and down-heap / sift-down). Both operations take O(log N) time.
• Up-heap (or sift-up) is used, e.g., to restore the heap ordering when a new element is added to the end/bottom:
• Compare the added element with its parent; if they are in the correct order, stop.
• If not, swap the element with its parent and return to the previous step.
• Down-heap (or sift-down) is used, e.g., to restore the heap ordering when the root is deleted/removed and replaced with the last element (on the last level):
• Compare the new root with its children; if they are in the correct order, stop.
• If not, swap the element with one of its children and return to the previous step (for the newly ordered subtree). (Swap with its smaller child in a min-heap and its larger child in a max-heap.)So basically, swap with higher priority child.
• Heapify, given a complete binary tree, turns the data structure into a heap:
• We want the heap property to obtain, so we need to ensure that each parent node is greater (or lesser, if min heap) than its children.
• Starting at the last parent node (take the last node and use index arithmetic to figure out the last parent), call down-heap on it. This will make the subtree rooted at this parent node a heap.
• Continue on for all the other parent nodes.
• Where the heap is 1-based implemented:Mostly from visualgo.
• for (i = arr.length / 2; i >= 1; i--)
• DownHeap(arr[i])
• Heapsort just entails copying elements to be sorted into an array, heapifying it, then taking the max (root) out each time and putting it at the end of the result array.
• This can be done in-place (in the same array) where one section is for the heap and the other for the sorted list. Thus, each iteration of the algorithm will reduce the heap by 1 and increase the sorted list by 1.
• Heapsort is similar to insertion sort in that each step of the algorithm takes the max element from the unsorted section and moves it to the sorted section.
• Note that heaps are not binary search trees, so we can't efficiently find a given key using binary search.
• This is because the order property only works between parent and children, not between siblings.
• Self-balancing binary trees: TODO later, first cover the breadth.
• Binary trees have performance proportional to their height, so keeping the height small maximizes performance. In the worst case, a tree approximates a linked list; a maximally unbalanced tree is just a linked list — where the height is 1 less than the number of nodes.
• Specifically, a balanced tree has performance O(log n) (think binary search), but an unbalanced tree has performance O(n) (think linked list).
• Self-balancing trees thus guarantee balanced-ness and consequently efficient performance.
• Red-black trees:http://www.eternallyconfuzzled.com/tuts/datastructures/jsw_tut_rbtree.aspx
• Guarantees searching, insertion, and deletion in O(log n).
• Each node has a color, red or black, associated with it. How the tree is "painted" ensures balance. When modified, the tree is re-arranged and re-painted to keep the necessary color properties.
• Originally an abstraction of symmetric binary B-trees, also called 2-3-4 or 2-4 trees. Red-black trees are really isomorphic to 2-4 trees; i.e., they are equivalent data structures.https://www.cs.princeton.edu/~rs/AlgsDS07/09BalancedTrees.pdf https://www.wikiwand.com/en/2%E2%80%933%E2%80%934_tree
• In a 2-4 tree, there are 3 kinds of nodes:
• 2-node: 1 key and (if internal etc.) 2 children
• 3-node: 2 keys and 3 children
• 4-node: 3 keys and 4 children
• All leaves are at the same depth, the bottom level. So, every path from root to leaf is the same length ensuring perfect balance.
• Leaf nodes have no data.
• Red-black trees have the following properties:https://www.topcoder.com/community/data-science/data-science-tutorials/an-introduction-to-binary-search-and-red-black-trees/
• 1) every node is either red or black;
• 2) all leaves are black;Leaves can either be explicit nodes with no data or just NIL nodes.
• 3) if a node is red, both its children are black; and
• 4) every path from a given node n to a descendant leaf has the same number of black nodes (not counting node n). We call this number the black height of n, which is denoted by bh(n).
• These explicit properties give rise to this critical property: the path from the root to the farthest leaf is no more than twice as long as the path from the root to the nearest leaf, ensuring that the tree is roughly height-balanced.
• This limits the height of any red-black tree, ensuring that even in the worst case, search, insert, and deletion are still efficient.
• The balance property is guaranteed through the combination of properties 3 and 4:
• For a red–black tree T, let B be the number of black nodes in property 4 (since every path has the same number of black nodes). Let the shortest possible path from the root of T to any leaf consist of B black nodes. Longer possible paths may be constructed by inserting red nodes. However, property 3 makes it impossible to insert more than 1 consecutive red node. Therefore, ignoring any black NIL leaves, the longest possible path consists of 2*B nodes, alternating black and red (this is the worst case).
• The shortest possible path has all black nodes, and the longest possible path alternates between red and black nodes. Since all maximal paths have the same number of black nodes, by property 4, this shows that no path is more than twice as long as any other path.
• A red-black tree can be converted to a 2-4 tree by remembering that red nodes are part of ("horizontally" linked within) a logical 2-4 B-tree node and black nodes separate the logical 2-4 nodes with normal vertical links:
• So to convert a red-black tree to a 2-4 form, just move the red nodes up so that they become horizontally linked with their parent black node to form a logical 2-4 node.
• All leaf nodes will be at the same depth.
• See http://www.wikiwand.com/en/Red%E2%80%93black_tree for an example.
• Search and other read-only operations are the same as those used to BST. However, insertion and deletion are much more complex, because they may violate red-black properties and then must be fixed with color changes and rotations.These fixes are quite quick in practice. E.g., the color changes are amortized O(1).
• A tree rotation is a binary tree operation that changes the structure without affecting the order of the elements. That is, the in-order traversal is the same pre- and post-rotation.
• http://www.wikiwand.com/en/Tree_rotation
• Essentially, a right rotation (the root moves right) on a root node X has X become the right child of the new root, which will be X's former left child.
• Order in the descendants is maintained through realizing that the right child of a left child of a root node can become the left child of the root without violating any binary tree constraints.
• Insertion and deletion have a number of cases that must be handled.
• AVL trees:http://www.wikiwand.com/en/AVL_tree
• More rigidly balanced than red-black trees, leading to slower insertion and removal, but faster search. So better for read-heavy use cases.
• Critical property: the heights of the two child subtrees of any node differ by at most one; if at any time they differ by more than one, rebalancing (with tree rotations) is done to restore this property.
• Retrieval, insertion, and deletion, as with red-black trees, are O(log n) in the worst case.
• A trie (or radix tree or prefix tree) is a n-ary tree variant in which characters are stored at each node, such that each path down the tree may represent a word:
• Unlike a binary search tree, no node in the tree stores the key associated with that node; instead, its position in the tree defines the key with which it is associated.
• All the descendants of a node have a common prefix of the string associated with that node, and the root is associated with the empty string.
• https://www.wikiwand.com/en/Trie
• Note that tries do not have to be keyed by character strings; other ordered lists can work too, e.g., permutations on a list of digits.
• The implementation here is better https://brilliant.org/wiki/tries/
• Can just implement as a dictionary of (nested) dictionaries . Or of course using classes and OO. E.g. for dicts:
• root = {}
• for word in strs:
• node = root
• for char in word:
• if char not in node:
• node[char] = {}
• node = node[char]
• # mark end of word
• node[''] = 1
• node = root
• prefix = ''
• # if there's more than 1 key, then divergence / not common ancestor
• while len(node.keys()) == 1:
• # if reached the end of any word, the LCA can't be longer
• if '' in node: return prefix
• char = list(node.keys())[0]
• prefix += char
• node = node[char]
• return prefix
• Links:
• http://www.wikiwand.com/en/Tree_(data_structure)
• this is a pretty good general reference: http://www.cs.cmu.edu/~adamchik/15-121/lectures/Trees/trees.html.
• A good explanation here https://brilliant.org/wiki/tries/ and https://medium.com/basecs/trying-to-understand-tries-3ec6bede0014
• Graphs:
• The graph ADT comprises a set of vertices (or nodes) together with a set of pairs of vertices, the edges. A vertex can be any arbitrary abstract object.
• Graphs are an extremely powerful concept. Linked lists, trees, state transition diagrams, etc. are all just cases of graphs.
• Terminology:
• Direction:
• Directed graph (or digraph) = a graph where the edges have a direction associated with them. I.e., each edge is now an ordered pair of vertices.
• In conceptual terms, direction implies that relations between vertices are not symmetric. For example, a graph of friends would be undirected, since X being friends with Y implies that Y is friends with X. And on the other hand, a graph of Twitter followers would be directed, since X being a follower of Y does not imply that Y is a follower of X.
• Directed acyclic graph (or DAG) = a digraph with no (directed) cycles.https://www.wikiwand.com/en/Directed_acyclic_graph
• Many important applications, e.g., systems of events or partial events, scheduling, causal structures.
• A rooted tree, as in the ADT, is a DAG.
• Work flowy is essentially a directed acyclic graph!
• Topological sort (or topological ordering):
• A topological sort of a digraph is just the linear ordering of its vertices such that for every directed edge ab, a comes somewhere before b.
• Any DAG has at least one topological ordering.
• The canonical case is scheduling: the vertices of the graph may represent tasks to be performed, and the edges may represent constraints that one task must be performed before another (prerequisites). Thus, a topological ordering is just a valid sequence for the tasks.
• A reverse postorder traversal provides a topological ordering. Probably the author means this https://www.geeksforgeeks.org/topological-sorting/
• this is also good but the approach is different https://www.interviewcake.com/concept/java/topological-sort, aka kahn's
algo for topological sort. https://www.geeksforgeeks.org/topological-sorting-indegree-based-solution/ . works on the theory that
for a DAG there has to be atleast one node with indegree 0 and out degree also 0. the proof of this is , since the graph is acyclic
there would be a longest path from a node u to another node v, and if u is the starting node and v is the ending node then the
in-degree of u is 0 and out degree of v is 0(as they are the first and last nodes on the path).
1 find a node with indegree of 0
2 add that to the topsort list
3 remove the node in step 2
4 repeat for other remaining nodes
check implementation on the interviewcake link.
• Multiple edges & loops:
• Multiple (or parallel) edges = 2 or more edges that connect the same 2 vertices.
• Loop = an edge that connects a vertex to itself.
• Multigraph = a graph that can have multiple edges and (depending on the convention) sometimes loops.
• Simple graph = a graph that cannot have multiple edges or loops.
• Connectivity:
• Connected graph = graph with no vertices by themselves; i.e., there is some path between every pair of vertices / every pair of the graph's vertices is connected.
• A connected component is a maximal subgraph that is (1) connected and (2) not connected to any vertices in the rest of the supergraph.example
• Subgraph of a graph G = graph whose vertices are a subset of G's vertices and whose edges are a subset of G's edges. Note that this is subset, not proper subset.
• Tracking the connected components of a changing graph is a straightforward partitioning problem, which can be naturally solved with the application of disjoint-set (or union-find) data structures. https://www.wikiwand.com/en/Disjoint-set_data_structure
• Maximal such subgraph because otherwise there would be "overlapping" connected components. This constrains it such that each vertex and each edge belongs to exactly one connected component. Mathematically, connected components partition their supergraph.
• A digraph is strongly connected if every pair of vertices (x and y) has a directed path both from x to y and from y to x. Otherwise, it is merely (weakly) connected.
• The strong components of a digraph are just the maximal strongly connected subgraphs.
• Kosaraju's algorithm and Tarjan's strongly connected components algorithm are 2 popular ways to compute a digraph's strongly connected components.
• A vertex cut (or separating set) = the set of vertices whose removal would make the graph disconnected.Note that this is not the same as a cut.
• Easy to compute connectivity: if the number of nodes visited through either BFS or DFS equals the number of vertices, then the graph is connected.
• Weighted graphs have weights (or costs or distances) associated with their edges. So 2 paths can have the same number of edges (same path length), but have different total weights (or distances) because their edges' weights differ.
• Cut:
• A partition of a graph's vertices into two disjoint subsets.
• Also defines a cut-set, the set of edges that have 1 endpoint in each subset of the partition. I.e., the edges that cross the cut (also called a crossing edge). Sometimes a cut is identified as its cut-set.
• The size / weight of a cut is the (total) weight of the cut-set, the edges crossing the cut. If unweighted, the size is the number of such crossing edges.
• Degree of a node is the number of edges connected to it. If directed graph, there is an indegree and an outdegree.
• A Hamiltonian path is a path that visits each vertex exactly once. Similarly, a Hamiltonian cycle (or circuit) is a Hamiltonian path that is a cycle. Determining whether such a path exists in a graph is known as the NP-complete Hamiltonian path problem.
• Coloring:
• A graph coloring is just assigning colors to the nodes in a graph.
• A legal coloring is when no two adjacent / connected nodes have the same color.
• Finding the fewest number of colors we can use for a legal coloring (the chromatic number) is an NP problem.
• Three primary implementation methods / data structures:https://www.wikiwand.com/en/Graph_(abstract_data_type) and http://www.cs.rochester.edu/~nelson/courses/csc_173/graphs/implementation.html
• Adjacency list:https://www.wikiwand.com/en/Adjacency_list
• Each vertex is associated with an unordered list that contains its neighbors.
The graph pictured above has this adjacency list representation:
a adjacent to b,c
b adjacent to a,c
c adjacent to a,b
• More efficient than the adjacency matrix in returning a given vertex's neighbors, O(1); less efficient in testing whether two vertices are neighbors, O(|V|), because in the worst case the whole list of neighbors has to be scanned.|V| is the number of vertices.
• Slow in removing a vertex / edge, because must find all vertices / edges. because the same vertex might be present in all the entries in the worst case.
• To represent weights, each node in the neighbor list could also contain the weight.
• In practice the simplest way is this; in Python, just use defaultdicts:
• default dict is the same as a simple dict in python, just has a factory method which in invoked when a key does not exist and is retrieved. in that case the key is inserted into the default dict.
• Graph is a dict of nodes where each node is mapped to a set of its neighbors.
• Weights is a dict where each origin and dest-node is a tuple that is mapped to the weight val(e.g {(u,v): w} where u and v are nodes and w is the weight). (Or could do it where the set of neighbors is a tuple of (neighbor, weight) eg {u: set(v,w), etc} )
• But then the problem is I could have two edges between the same nodes but with different weights. *Could* be desirable though.
• Code:the python zip operation is used below, to understand eg here
a = ("John", "Charles", "Mike")
b = ("Jenny", "Christy", "Monica")
x = zip(a, b)
output:
(('John', 'Jenny'), ('Charles', 'Christy'), ('Mike', 'Monica'))From https://leetcode.com/problems/evaluate-division/description/
• # initialize graph where each node is mapped to a set of its neighbors
• # and each node 2-tuple is mapped to its result
• G, W = defaultdict(set), defaultdict(float)
• for (a, b), val in zip(equations, values):
• G[a], G[b] = G[a].union({b}), G[b].union({a})
• W[a, b], W[b, a] = val, 1.0 / val
A very good explanation here: https://leetcode.com/problems/evaluate-division/discuss/88275/Python-fast-BFS-solution-with-detailed-explantion
• Adjacency matrix:https://www.wikiwand.com/en/Adjacency_matrix
• A matrix where each non-diagonal entry Aij is the number of edges from vertex i to vertex j, and the diagonal entry Aii, depending on the convention, is either once or twice the number of edges (loops) from vertex i to itself.Undirected graphs often use the latter convention of counting loops twice, whereas directed graphs typically use the former convention.
• Sometimes the matrix value is just a boolean, if there can be no parallel edges (i.e., the graph is a simple graph, not a multigraph).
• In a graph without loops, the diagonal in the matrix will have all zero entries.
• Adjacency matrices are space-efficient, because each matrix entry only requires one bit. However, for a sparse graph (few edges), adjacency lists use less space because they do not represent nonexistent edges.
• More efficient than the adjacency list in determining whether two vertices are neighbors, O(1); less efficient in getting all of a given vertex's neighbors because the entire row must be scanned, O(|V|).
• Slow to add or remove vertices, because the matrix must be resized / copied.
• To represent weights, the matrix value could be the weight of that edge.
• The third (less common but very simple) implementation method is just objects and pointers:
• Naturally, Nodes / Vertex[es] can be represented as objects, with pointers to neighbor nodes. If a list of those pointers is kept, it becomes very similar to an adjacency list (http://stackoverflow.com/questions/5886274/comparing-object-graph-representation-to-adjacency-list-and-matrix-representatio).
• Also, Edges are another natural object that can be used.
• Graph traversals:http://www.wikiwand.com/en/Graph_traversal and for implementations in Python http://eddmann.com/posts/depth-first-search-and-breadth-first-search-in-python/
• Depth-first search (DFS):
• Depth-first search is like walking through a corn maze. You explore one path, hit a dead end, and go back and try a different one.
• DFS visits the child nodes before visiting the sibling nodes; i.e., it traverses the depth of any particular path before exploring its breadth.
• Pro: usually less memory than BFS and easily implemented with recursion.
• Con: doesn't necessarily get shortest path.
• Pseudo-pseudocode:
• Visit node r and then iterate through each of r's unvisited neighbors.
• When visiting a neighbor n, immediately visit all of his neighbors. And so on.Thus depth-first. So neighbor n and its children are exhaustively searched before r moves on to its other adjacent nodes.
• If no neighbors, i.e., a dead end, backtrack (pop the stack) until reach unvisited node.
• Python code:to understand the yield keyword used in python , in context of storing the path generated by the dfs or bfs, read https://stackoverflow.com/questions/231767/what-does-the-yield-keyword-dohttp://eddmann.com/posts/depth-first-search-and-breadth-first-search-in-python/
• Adjacency list implementation:
• graph = {'A': set(['B', 'C']),
• 'B': set(['A', 'D', 'E']),
• 'C': set(['A', 'F']),
• 'D': set(['B']),
• 'E': set(['B', 'F']),
• 'F': set(['C', 'E'])}
• Iterative:
• def dfs(graph, start):
• visited, stack = set(), [start]
• while stack:
• vertex = stack.pop()
• if vertex not in visited:
• visited.add(vertex)
• stack.extend(graph[vertex] - visited) #Using Python’s overloading of the subtraction operator to remove items from a set, we are able to add only the unvisited adjacent vertices.
• return visited
• dfs(graph, 'A') # init call.
• iterative with path storage: from eddmann.com
def dfs_paths(graph, start, goal):
stack = [(start, [start])]
while stack:
(vertex, path) = stack.pop()
for next in graph[vertex] - set(path):
if next == goal:
yield path + [next]
else:
stack.append((next, path + [next]))
list(dfs_paths(graph, 'A', 'F')) # [['A', 'C', 'F'], ['A', 'B', 'E', 'F']]
• Recursive:
• def dfs(graph, start, visited=None):
• if visited is None:
• visited = set()
• visited.add(start)
• for next in graph[start] - visited:
• dfs(graph, next, visited)
• return visited
• Easy to iteratively implement with a stack. Just make sure to "mark" visited nodes to prevent infinite loops.
• Also easy to recursively implement (also via a stack, i.e., the program's call stack).
• Breadth-first search (BFS):
• Breadth-first search is like throwing a stone in the center of a pond. The nodes you explore "ripple out" from the starting point.
• BFS explores the neighbor nodes first, before moving to the next "level."
• Pro: will always find the shortest path.
• Con: usually requires more memory than DFS.
• Pseudo-pseudocode:
• Visit node r and then iterate through each of r's unvisited neighbors.
• When visiting a neighbor n, add its neighbors to queue, but don't visit them yet.
• Visit all of node r's neighbors before visiting any of r's "(grand)children."
• Python code:http://eddmann.com/posts/depth-first-search-and-breadth-first-search-in-python/
• Adjacency list implementation:
• graph = {'A': set(['B', 'C']),
• 'B': set(['A', 'D', 'E']),
• 'C': set(['A', 'F']),
• 'D': set(['B']),
• 'E': set(['B', 'F']),
• 'F': set(['C', 'E'])}
• Iterative:
• def bfs(graph, start):
• visited, queue = set(), [start]
• while queue:
• vertex = queue.pop(0)
• if vertex not in visited:
• visited.add(vertex)
• queue.extend(graph[vertex] - visited)
• return visited
• Implement iteratively with a queue and mark visited as always.
• "Remember that breadth-first uses a queue and depth-first uses a stack (could be the call stack or an actual stack object). That's not just a clue about implementation, it also helps with figuring out the differences in behavior. Those differences come from whether we visit nodes in the order we see them (first in, first out) or we visit the last-seen node first (last in, first out)."
• DFS is the easiest if want to visit every node.
• But BFS can get shortest path(s) first.
• Both traversals (for the entire graph) take time linear to the size of the graph (number of vertices + number of edges), O(|V| + |E|) and space linear to the number of vertices.
• In other words: runtime is O(N + M) where M is the number of edges (we check each neighbor twice per edge).
• Note that just as trees are a special case of graphs, tree traversals are a special case of graph traversals.
• Two common graph problems:
• Shortest-path tree (or single-source shortest paths (SSSP) problem):
• A shortest-path tree rooted at vertex v is a spanning tree T of the graph G such that the path distance from v to any other vertex u in the tree is the shortest path from v to u in G. I.e., the shortest-path tree rooted at v is the tree that comprises the shortest paths from v to every node.
• A spanning tree of G is a tree (a connected undirected graph with no cycles) T such that (1) every vertex in G belongs to it and (2) every edge in T belongs to G.
• Must be a spanning tree because otherwise it wouldn't actually be a tree of paths from the root node to all nodes.
• Simpler, related version is finding the shortest path from one node to another. But finding the shortest path from the former to all nodes takes the same amount of time.
• In an unweighted graph or a graph in which all edge weights are 1, the shortest-path tree is isomorphic to the BFS tree.
• Be careful to remember that edge lengths or distances are the same as edge weights. So if the graph is a weighted one, the path distance from vertex a to b is actually the sum of the edge weights of that path.
• If the weights are 1 or nonexistent, then the path distance is just the number of edges from 1 node to another.
• Algorithms:
• Dijkstra's algorithm:https://www.wikiwand.com/en/Dijkstra's_algorithm
• Fastest shortest-path tree / single-source shortest paths algorithm for [di]graphs with nonnegative edge weights.
• Pseudo-pseudocode:http://www.egr.unlv.edu/~larmore/Courses/CSC269/pathing
• The distance of node Y is the distance from the root node to Y. First assign some initial distance values and then seek to greedily improve them step by step.
• 1) Assign to every node an initial tentative distance value: set it to 0 for our root node and to infinity (float('inf')) for all other nodes.
• 2) Assign the root node as current. Create an unvisited node set and put all nodes in it.
• 3) For the current node c, consider all of its unvisited neighbors (e.g., d) and calculate their tentative distances: tentativeDist(d) = tentativeDist(c) + c.getEdgeLength(d). Compare the newly calculated tentative distance to the current assigned value and assign the smaller one. For example, if the current node A is marked with a distance of 6, and the edge connecting it with a neighbor B has length 2, then the distance to B (through A) will be 6 + 2 = 8. If B was previously marked with a distance greater than 8 then change it to 8. Otherwise, keep the current value.
• This process is called relaxation; we are relaxing the edges by trying to find if the tentative distances of c's neighbors could be improved by diverting the path through c.
• 4) When we are done considering / updating all of the neighbors of the current node, mark the current node as visited and remove it from the unvisited set. A visited node will never be checked again and it is now in the shortest-path tree. Its tentative distance value is the final distance value. I.e., it is now the actual distance or cost of the shortest path from the source root node to the current just-visited node.
• 5) The algorithm is done if the destination node has been marked visited (for the simple case where we are calculating shortest path between 2 nodes) or if the unvisited set is empty or if the smallest tentative distance among the nodes in the unvisited set is infinity (occurs when there is no connection between the initial node and remaining unvisited nodes).
• 6) Select the unvisited node that is marked with the smallest tentative distance, and set it as the new current node, then go back to step 3.
• Implementation:
• "The simplest implementation of the Dijkstra's algorithm stores vertices of set Q in an ordinary linked list or array, and operation Extract-Min(Q) is simply a linear search through all vertices in Q. In this case, the running time is O(V^2).
• For sparse graphs, that is, graphs with much less than V^2 edges, Dijkstra's algorithm can be implemented more efficiently by storing the graph in the form of adjacency lists and using a binary heap or Fibonacci heap as a priority queue to implement the Extract-Min function. With a binary heap, the algorithm requires O((E+V)lgV) time, and the Fibonacci heap improves this to O(E + VlgV)."
• Pretty understandable Python implementation: http://stackoverflow.com/questions/22897209/dijkstras-algorithm-in-python
• Specifically the prof answer with helper functions.
• More complicated but better: https://gist.github.com/econchick/4666413
• Need to have a predecessor or previous-like reference for each node to actually build a tree. Otherwise, just returns each node and its distance from the root node. Or could build path as go through algorithm, as in the gist implementation above.
• Re: predecessors, getting a predecessor subgraph is a pretty nice way to form the actual (shortest-path) tree:
• The edges in such a subgraph are (preds[v],v), where preds is the array of predecessors for each vertex.
• In the algorithm, whenever relaxation happens, just add the correct predecessor. E.g., if vertex u's distance value can be improved by going through w, then preds[u] = w.
• Then can form shortest-path tree easily at the end.
• https://www.cs.auckland.ac.nz/software/AlgAnim/dijkstra.html
• With a min-priority queue to get the unvisited node with the smallest tentative distance, can run in O( |E| + |V| log |V| ).
• Not capable of handling negative weights (Bellman-Ford can, however).
• Known as a greedy algorithm because at each stage, it greedily selects the unvisited minimum-weight node.
• Supposed to be for directed graphs.
• Dijkstra's is a special case of A*.
• A* (A star) is an extended version of Dijkstra's that uses heuristics to guide its search and be a best-first search.http://www.wikiwand.com/en/A*_search_algorithm
• Dijkstra's is just A* where the heuristic / evaluation function for each node is null.
• Bellman-Ford algorithm:http://www.wikiwand.com/en/Bellman%E2%80%93Ford_algorithm
• Slower than Dijkstra's but does handle negative edge weights. And can report the existence of negative cycles.A negative cycle means that there is no shortest or cheapest path, because any presumably shortest path could be made shorter by going around the cycle again.
• Similar to Dijkstra's in that it depends on relaxation: each node's distance starts out as an overestimate and is iteratively improved through edge relaxation (diverting the old path through a new node to improve the distance).
• Different than Dijkstra's in that at each step the relaxation process is done on all edges at once, instead of just on the outgoing edges from some current node. Thus, this relaxation is done |V|-1 times.
• Implementation:
• "Simply put, the algorithm initializes the distance to the source to 0 and all other nodes to infinity. Then for all edges, if the distance to the destination can be shortened by taking the edge, the distance is updated to the new lower value. At each iteration i that the edges are scanned, the algorithm finds all shortest paths of at most length i edges. Since the longest possible path without a cycle can be |V|-1 edges, the edges must be scanned |V|-1 times to ensure the shortest path has been found for all nodes. A final scan of all the edges is performed and if any distance is updated, then a path of length |V| edges has been found which can only occur if at least one negative cycle exists in the graph."
• Minimum spanning tree (MST):https://www.wikiwand.com/en/Minimum_spanning_tree and http://algs4.cs.princeton.edu/43mst/
• A minimum spanning tree is a spanning tree (a subgraph that is a tree and that connects all the vertices) that has the lowest total weight.
• Properties:
• If all edge weights are the same, then every spanning tree is a MST.
• On the contrary, if all edge weights are distinct, then there is 1 unique MST.
• There are |V| - 1 edges in a spanning tree.
• If unconnected graph, can still use MST algorithms to find the MST for each connected component, together forming a minimum spanning forest.
• If weights are positive (don't have to be), then the MST is just the subgraph with minimal weight that connects all the vertices. I.e., don't have to say it's a tree (which has no cycles), since those subgraphs which have cycles are necessarily greater weight.
• Algorithms:
• Note that:
• Cuts are very important, specifically, the cut property (the basis of the algorithms): Given any cut in an edge-weighted graph (with distinct weights), the crossing edge of minimum weight is in the MST of the graph.http://algs4.cs.princeton.edu/43mst/
• Simple greedy algorithm:
• The following method colors black all edges in the the MST of any connected edge-weighted graph with V vertices: Starting with all edges colored gray, find a cut with no black edges, color its minimum-weight edge black, and continue until V-1 edges have been colored black.http://algs4.cs.princeton.edu/43mst/
• Prim's algorithm:
• Prim's algorithm works by attaching a new edge to a single growing tree at each step: Start with any vertex as a single-vertex tree; then add V-1 edges to it, always taking next (coloring black) the minimum-weight edge that connects a vertex on the tree to a vertex not yet on the tree (a crossing edge for the cut defined by tree vertices).
• This is a relatively simple method, but the crucial step is efficiently detecting the minimum-weight crossing edge to be added at each step:
• Lazy way:
• Just maintain a priority queue (PQ) of the crossing edges (priority is obviously by minimum weight). Each time a crossing edge is added to the MST, a vertex is also added. The new crossing edges to be added to the PQ are just the edges from the new vertex to all non-MST vertices. However, there might be old edges in the PQ which are now ineligible (i.e., they are non-crossing and now connect two MST vertices). This way is lazy since it leaves these edges in the PQ.
• Eager way:
• The first improvement is to delete ineligible edges from the PQ, such that only crossing edges from the growing MST to the rest of the graph are present. But we can do even better. Since (1) we are only concerned with the minimal crossing edge and since (2) when we add a new edge / vertex v to the MST, the only possible improvement for each non-tree vertex w is that adding v brings w closer to the tree, we really just need to keep track of the minimum-weight edge and check whether the addition of v to the tree necessitates that we update that minimum (because of an edge v-w that has lower weight), which we can do as we process each edge. In other words, we maintain on the priority queue just one edge for each non-tree vertex: the shortest edge that connects it to the tree.
• Linear time for a graph is O(N + M) where N is the number of nodes and M is the number of edges.
• E.g., it's the runtime for BFS *and* DFS.
• Edge cases in graph problems are nodes with no edges, cycles, and loops (single-node cycles).
• DAGs and topological sorts are important concepts to know:
• Topological sort is relatively easy to implement, just remember the definition (for vertices u, v where u has a directed edge to v, u has to come before v in the topological ordering) and use a stack.
• Start at a given vertex, add to visited set, recursively (depth-first) visit and explore unvisited neighbors, when have fully explored v's neighbors add v to stack, and pop all at end for topologically sorted ordering.
• Also think of it as a modification of DFS:
• "We can modify DFS to find Topological Sorting of a graph. In DFS, we start from a vertex, we first print it and then recursively call DFS for its adjacent vertices. In topological sorting, we use a temporary stack. We don’t print the vertex immediately, we first recursively call topological sorting for all its adjacent vertices, then push it to a stack. Finally, print contents of stack. Note that a vertex is pushed to stack only when all of its adjacent vertices (and their adjacent vertices and so on) are already in stack."
• Sorting and Searching:
• Binary search is indispensable and also surprisingly tricky to get right:
• https://leetcode.com/problems/search-insert-position/description/
• consider l and r to be walls (i.e., exclusive not inclusive) around my target value
• that way can just set l or r to be the guess/median value
• also to get the median, do floor + (distance between divided by two)
• def searchInsert(self, nums, target):
• """
• :type nums: List[int]
• :type target: int
• :rtype: int
• """
• l, r = -1, len(nums)
• while l + 1 < r:
• m = l + ((r - l) / 2)
• curr = nums[m]
• if target == curr:
• return m
• elif target > curr:
• l = m
• else:
• r = m
• return l + 1
• could also do:
• while l <= r:
• m = l + (r - l) / 2
• curr = nums[n]
• if target == curr:
• return m
• elif target > curr:
• l = m + 1
• else:
• r = m - 1
• return -1
• Bubble sort and selection sort suck. Both O(n^2).
• Merge sort and quick sort are both good and, in the average case, O(n Log(n)).
• Merge sort:
• Worse case is O(n Log(n)).
• From Algorithms class:
• like quicksort in that it recursively splits array and builds it back in right order (both also divide-and-conquer)
• divide array into 2 halves, recursively sort halves, merge results
• recursive calls then sorting action (contra quicksort); "on the way up"; only way sorting occurs is through these "on the way up" merges
• pro: guaranteed N log N performance
• con: extra space proportional to N (aux array for merging)
• time efficiency: average, best, and worst are all O(N log N); does require some aux space
• All the work happens in the merging where consider both arrays and pick the smallest element to copy to main array each time. Base case is merging two 1-element arrays (or null arrays) because these are sorted by definition.
• Simplified pseudocode:
• if len(list) < 2: return list
• sorted_first = mergesort(first_half)
• sorted_second = mergesort(second_half)
• return merge(sorted_first, sorted_second)
• Most important part is merging the sorted lists. For this, just add smallest element each time until one of the lists is empty, and then copy any remaining elements over.
• Usually stable.
• Quicksort:
• Worse case is O(n^2) when array already sorted, but small constant factor and can be in-place. Can also mitigate / check for the worst degenerate case.
• From Algorithms class:
• array rearranged such that, when two subarrays sorted, the whole array is ordered (contra merge where array is broken into 2 subarrays to be sorted and then combined to make whole ordered array)
• recursive calls happen after working on whole array
• partition/pivot not necessarily in middleOr necessarily the median value, leading to the worst case performance.
• improvements: insertion sort for tiny arrays, median of 3, randomize beforehand
• pros: average case N log N, in-place so small usage of memory (small aux stack), shorter inner loop so fast in practice as well
• cons: worst case is quadratic, happens with already sorted array (where pivot = first item)
• time efficiency: average & best are O(N log N), worst is O(N^2), small constant factor
• To partition, have two pointers at each end working toward each other. Stop pointers when each reaches a value out of place. Swap them.
• This results in all the smaller (than some x) values on the left side and all the larger values on the right side—though each side is not itself sorted yet.
• Simplified pseudocode:
• pivot = partition(list)
• quicksort(list[:pivot])
• quicksort(list[pivot:])
• Usually not stable.
• A good resource is here: #link http://www.algolist.net/Algorithms/Sorting/Quicksort
• And also, of course, visualgo: http://visualgo.net/
• Difference is that in mergesort, the sorting action happens on the way up (when ordered subarrays are merged), whereas in quicksort, the sorting happens on the way down when the (sub)array is split and the low and high elements are separated and the pivot element is placed in its correct final position.
• "The difference between the two is basically which linear operation they leverage. QuickSort is efficient because you can partition a list into items that come before and items that come after a given item in linear time. MergeSort is efficient because given two already sorted lists you can combine the two, and maintain sorted order, in linear time."
• Dynamic Programming & Memoization:
• Top-down recursion can be memory-intensive because of building up the call stack. Can avoid by going bottom-up and using DP.Current point.
• DP often involves making a binary decision at each (bottom-up) step, e.g., do I include this coin / item / character in my knapsack / sum / subsequence. If I do include it, is that more optimal (value or stock market profit or so on) than if I don't, where each calculation usually makes use of previously calculated optima or solutions to subproblems.
• To get those previous locally optimal calculations, we generally use a matrix or list to store the previous solutions. But sometimes that can store more state than we really need, see e.g. the bottom-up Fibonacci implementation where we can store the answers to the previous subproblems we need in just 2 variables.
• Always think: at step n, what happens if I *do* include this thing? What happens if I don't? What do I need to compare and take the max of? And how do I use my answer to step n-1?
• Interview Cake:
• "Dynamic programming is kind of like the next step up from greedy . You're taking that idea of "keeping track of what we need in order to update the best answer so far," and applying it to situations where the new best answer so far might not just have to do with the previous answer, but some other earlier answer as well.
• So as you can see in this problem, we kept track of all of our previous answers to smaller versions of the problem (called "subproblems") in a big list called ways_of_doing_n_cents.
• Again, same idea of keeping track of what we need in order to update the answer as we go, like we did when storing the max product of 2, min product of 2, etc in the highest product of 3 question. Except now the thing we need to keep track of is all our previous answers, which we're keeping in a list.
• We built that list bottom-up, but we also talked about how we could do it top-down and memoize. Going bottom-up is cleaner and usually more efficient, but often it's easier to think of the top-down version first and try to adapt from there."
• When we memoize (cache a subproblem's answer / recursive function's output so we don't need to recompute it again), the memoization dictionary probably needs to be outside the function to save/share state. I.e., make a class. Interview Cake #5: https://www.interviewcake.com/question/python/coin. Don't always have to, can sometimes just pass a default list or dict (be careful of the default mutable gotcha here).
• Memoization is top-down depth-first and DP is bottom-up breadth-first:
• http://stackoverflow.com/questions/27609246/if-memoization-is-top-down-depth-first-and-dp-is-bottom-up-breadth-first-what?rq=1
• http://blog.racket-lang.org/2012/08/dynamic-programming-versus-memoization.html
• Tushar Roy on the 0-1 knapsack problem: https://www.youtube.com/watch?v=8LusJS5-AGo
• Advanced Algorithms & Data Structures & Math:Incomplete.
• Dynamic Programming overview:
• General approach from recursion:
• Find the recurrence relation.
• Start with top-down recursion, then use a cache to memoize.
• Then to save space, move to bottom-up DP with a table.
• And then finally, try to use O(1) space with just variables.
• https://leetcode.com/problems/house-robber/discuss/156523/From-good-to-great.-How-to-approach-most-of-DP-problems.
• Another one demonstrating the evolution: https://leetcode.com/problems/decode-ways/discuss/30451/Evolve-from-recursion-to-dp
• Emblematic problems:
• Easy:
• https://leetcode.com/problems/house-robber/description/
• code:
• def rob(self, nums):
• prev1 = prev2 = 0
• for n in nums:
• prev1, prev2 = max(prev2 + n, prev1), prev1
• return prev1
• Medium:
• https://leetcode.com/problems/decode-ways/description/
• code:
• def numDecodings(self, s):
• # dp[i] =
• # dp[i-1] if dp[i] != 0
• # + dp[i-2] if 9 < dp[i-2:i] < 27
• if not s or s[0] == '0': return 0
• pre = cur = 1
• for i, c in enumerate(s[1:]):
• if c == '0': cur = 0
• if 9 < int(s[i:i+2]) < 27:
• cur = cur + pre
• pre = cur - pre
• else: pre = cur
• return cur
•
• Knuth-Morris-Pratt:
• the key is that if we have a prefix == suffix match in the substring up to the pattern[i] where pattern[i] stopped matching with text[j], then the suffix suf must also be the last characters we saw in text, text[ j-len(suf) : j ]; and since this substring suf is equal to some prefix pre, we can slide the pattern over, match up pre with text[ j-len(suf) : j ], and start trying to match again at pattern[ len(p) ] and text[j]
• https://www.wikiwand.com/en/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm
• Knuth–Morris–Pratt(KMP) Pattern Matching(Substring search)
•
• Bloom filters:
• http://prakhar.me/articles/bloom-filters-for-dummies/
• Tells you either that a value is possibly in the set or that it's definitely not in the set.
• I.e., some false positives, but guaranteed no false negatives. Probabilistic data structure.
• Trie and suffix trees:
• Union-find / disjoint-set:
• Network connectivity, connected components
• Basic idea is that each distinct subset is identified by its root, so if two elements have the same root, they're in the same set; and to merge, just need to make one subset's root the other subset's
• https://www.cs.princeton.edu/~rs/AlgsDS07/01UnionFind.pdf
• Useful for questions where I need to merge disjoint sets or network-like problems etc. like https://leetcode.com/problems/number-of-islands-ii/description/:
• clear answer: https://leetcode.com/problems/number-of-islands-ii/discuss/75459/JavaPython-clear-solution-with-UnionFind-Class-(Weighting-and-Path-compression)
• https://leetcode.com/problems/number-of-islands-ii/discuss/75468/Compact-Python.
• also https://leetcode.com/problems/friend-circles/submissions/
• Union-find template code:
• class UnionFind(object):
• def __init__(self, n):
• self.count = n
• self.parent = [_ for _ in xrange(n)]
• self.rank = [1 for _ in xrange(n)]
• def find(self, k):
• while k != self.parent[k]:
• self.parent[k] = self.parent[self.parent[k]]
• k = self.parent[k]
• return k
• def union(self, k1, k2):
• k1, k2 = self.find(k1), self.find(k2)
• if k1 == k2: return
• # ensure k1 is smaller component
• if self.rank[k1] > self.rank[k2]:
• k1, k2 = k2, k1
• self.parent[k1] = k2
• self.rank[k2] += self.rank[k1]
• self.count -= 1
• A*:
• LRU/LFU cache:Least recently used item is invalidated / evicted when cache is full.
• https://www.kunxi.org/blog/2014/05/lru-cache-in-python/
• The idea here is to use an OrderedDict and when returning an item, pop and re-insert to maintain the order invariant. popitem(last=False) lets us pop off the tail, i.e., in FIFO order.
• from collections import OrderedDict
• class LRUCache:
• def __init__(self, capacity):
• self.capacity = capacity
• self.cache = OrderedDict()
• def get(self, key):
• if key not in self.cache: return -1
• val = self.cache.pop(key)
• self.cache[key] = val
• return val
• def set(self, key, value):
• try:
• self.cache.pop(key)
• except KeyError:
• if len(self.cache) == self.capacity:
• self.cache.popitem(last=False)
• self.cache[key] = value
• Without OrderedDict, would have to create my own. Basically would have a normal dictionary and then a doubly linked list. The dictionary would map each key to its node; there would be dummy head and tail pointers; and I'd add to the tail.prev and remove/invalidate from the head.next:
• https://leetcode.com/problems/lru-cache/discuss/45926/Python-Dict-+-Double-LinkedList
• like this:
• class LLNode(object):
• def __init__(self, key=0, val=0):
• self.key = key
• self.val = val
• self.prev = None
• self.next = None
• class LRUCache(object):
• def __init__(self, capacity):
• # dict mapping key to LLNode
• self.cache = {}
• # dummy nodes to simplify
• # head -next-> node <-prev- tail
• # remove from head, add to tail
• self.head = LLNode()
• self.tail = LLNode()
• self.head.next = self.tail
• self.tail.prev = self.head
• self.capacity = capacity
• def get(self, key):
• if key not in self.cache: return -1
• node = self.cache[key]
• new_n = LLNode(key, node.val)
• self._remove(node)
• self._add(new_n)
• self.cache[key] = new_n
• return new_n.val
• def put(self, key, value):
• if key in self.cache:
• self._remove(self.cache[key])
• elif len(self.cache) >= self.capacity:
• to_remove = self.head.next
• self.cache.pop(to_remove.key)
• self._remove(to_remove)
• node = LLNode(key, value)
• self._add(node)
• self.cache[key] = node
• def _remove(self, node):
• p, n = node.prev, node.next
• p.next = n
• n.prev = p
• def _add(self, node):
• p = self.tail.prev
• p.next = node
• node.prev = p
• node.next = self.tail
• self.tail.prev = node
• harder version is LFUCache where least frequently used item is evicted (so need to keep track of frequency / # of accesses) *and* ties are broken by least recent toohttps://leetcode.com/problems/lfu-cache/description/
• use two dicts, one of which is an OrderedDict
• from collections import defaultdict, OrderedDict
• class LFUNode(object):
• def __init__(self, val, freq=1):
• self.val = val
• self.freq = freq
• class LFUCache(object):
• def __init__(self, capacity):
• self.remain = capacity
• # need OrderedDict to break freq ties w LRU
• self.freq2node = defaultdict(OrderedDict)
• self.least_freq = 1
• self.key2node = defaultdict(LFUNode)
• def _update(self, key, val=None):
• node = self.key2node[key]
• if not val: val = node.val
• freq = node.freq
• self.freq2node[freq].pop(key)
• if not len(self.freq2node[self.least_freq]):
• self.least_freq += 1
• node.val, node.freq = val, freq + 1
• self.freq2node[freq+1][key] = node
• self.key2node[key] = node
• def get(self, key):
• if key not in self.key2node: return -1
• self._update(key)
• return self.key2node[key].val
• def put(self, key, value):
• if key in self.key2node:
• self._update(key, value)
• return
• self.key2node[key] = LFUNode(value)
• self.freq2node[1][key] = LFUNode(value)
• if not self.remain:
• # pop FIFO
• evicted = self.freq2node[self.least_freq].popitem(last=False)
• self.key2node.pop(evicted[0])
• else: self.remain -= 1
• self.least_freq = 1
• Minimum edit distance:
• Minimum Edit Distance Dynamic Programming
•
• Key ideas are that the edit distance of two strings x, y has to be least the edit distance of their prefixes, e.g. x[:-1], y; insertion and deletion are "symmetric"; if the current characters are the same, the edit distance at that point is just the edit distance of the two strings minus the current character; if not, they're the min of the diagonal, previous column and previous row edit distances + 1 (to represent the operation itself)
• i.e., assume I did delete, so I look at the previous column for that edit distance, then add 1 for the current deletion; if I replaced, then it's the diagonal plus 1 etc.
• Fisher-Yates shuffle:
• import random
• def inplace_shuffle(lst):
• n = len(lst)
• if n < 2: return lst
• for i in xrange(n - 1):
• repl = random.randint(i, n - 1)
• if i != repl: lst[i], lst[repl] = lst[repl], lst[i]
• The naive algorithm of swapping every number with any other number fails because the number of possible orderings it outputs is n^n, but the number of actual distinct possible orderings is n!, so by the pigeonhole principle, some distinct permutations / "buckets" will be overrepresented.
• Dijkstra's:
• Boyer-Moore majority vote algorithm:
• O(n) time and O(1) space algo for finding majority element if it exists
• used in LC #277 Find the Celebrity https://leetcode.com/problems/find-the-celebrity/description/
•
• https://www.wikiwand.com/en/Boyer%E2%80%93Moore_majority_vote_algorithm