Leetjawnson & Jawnson

Réussis tes devoirs et examens dès maintenant avec Quizwiz!

Number of connected components

Anytime there's a graph that's not connected (or fully connected) -> make sure to run a loop through all of the nodes in the graph & then do your graph search (BFS or DFS)

What is a LRU Cache

Cache that discards the least recently used items first.

How can you tell if a graph is NOT bi-partite?

If there's an odd length cycle: You're visiting a neighbor from root/current node and it's at the same level -> we know it's not bipartite You keep a visited & levels set -> if a neighbor already visited and the same level bc that means theres an odd length cycle

How to find the runtime complexity of an algo involving decision trees?

See the branching factor call it B -> and raise to the height of the tree For ex: fib -> 2 ways to decide/branch -> -1 or -2 -> to the height or n given 2^n runtime complexity

Complexity for DFS on tree

T.c: O(n) -> number of nodes in the tree (will visit each once) S.c: O(h) -> max number of nodes on the call stack will be the height of the tree

K way merge

The k-way merge pattern helps to solve problems involving a list of sorted arrays. Here is what the pattern looks like: Insert the first element of each array in a min-heap. Next, remove the smallest element from the heap and add it to the merged array. Keep track of which array each element comes from. Then, insert the next element of the same array into the heap. Repeat steps 2 to 4 to fill the merged array in sorted order.

Looking for optimizations -> (e.g, find shortest path from one vertex to another)

Use BFS -> dijkstra's shortest path algo & minimum spanning tree

When to use heaps

When all you care about is the largest or smallest (frequent/ less frequent) elements and you do not neeed to support fast lookup, delete or search operations for arbitrary elements

How to tell if there's a cycle in an undirected graph

While doing graph search (BFS or DFS) -> keep an auxiliary: 1. visited set (for nodes that have been visited) and 2. parents map -> map of the current node to its parent if node that's currently being visited has already been visited + it's not the parent of prev -> there is a cycle

Backtracking + general template

Wikipedia: Backtracking is a general algorithm for finding solutions to some computational problems, notably constraint satisfaction problems, that incrementally builds candidates to the solutions, and abandons a candidate ("backtracks") as soon as it determines that the candidate cannot possibly be completed to a valid solution. void backtrack(arguments) { if (condition == true) { // Condition when we should stop our exploration. result.push_back(current); return; } for (int i = num; i <= last; i++) { current.push_back(i); // Explore candidate. backtrack(arguments); current.pop_back(); // Abandon candidate. } }

to avoid key error for dictionaries/ hashmaps use what

can use .get(key we're trying to get, & default val if it doesnt exist)

shallow vs deep copy

result.append(my_list[:]) and result.append(my_list.copy()) are both used to make a copy of the my_list and append it to the result list. However, they work slightly differently. result.append(my_list[:]) creates a shallow copy of the my_list. This means that a new list is created, but the elements within the new list are references to the original elements in my_list. This means that if the original elements in my_list are modified, the changes will be reflected in the new list as well. On the other hand, result.append(my_list.copy()) creates a deep copy of the my_list. This means that a new list is created, and new objects are created for all the elements in the new list. This ensures that if the original elements in my_list are modified, the changes will not be reflected in the new list. In most cases, result.append(my_list.copy()) is the preferred method to use, as it creates a new copy of the list and avoids any potential issues caused by modifying the original list.

Finding middle of a linked list

slow and fast pointers is good for this, works out for both even and odd lists (make sure to take into account length of lists into account) (they don't start at the same spots) slow, fast = head, head.next while fast and fast.next: slow = slow.next fast = fast.next.next

sort() vs sorted()

sort() in-place List.sort() returns None sorted() - returns new sorted list

Sliding Window

tip: explicitly set conditions for: shrinking window growing window to get to intermediary goal or solution really think about the condition for expanding - it should be written explicitly like longest substring for char replacement

Problem solving techniques:

1. use knowledge of time complexities to your advantage - you can even list them out and cross them off to see if you can approach a problem by doing it a certain way -> use the problem info and constraints to your advantage can you eliminate an operation? usually a trade in for space -> trade space for time and vice versa 2. Say you're completely stuck on a problem -> try to solve a simpler problem and then build up (maybe one that you already know- for example if it has to do with longest palindromes, maybe you start w/ asking yourself, how do you find out if a string in general is a palindrome?? When trying to solve simple problem, try to solve multiple ones, draw them out, notice their differences and maybe you can draw some inferences that help you solve the problem!! 3/

Interview structure

1.Ask clarifying questions: - What's the return type? - how do we handle edge cases? - most common: empty/null - all of same type (0s for product for example) - okay... so we're trying to do X and return Y that's correct? - run through some example test cases if none are provided - make sure to confirm w/ interviewer 2.run through what the brute force solution and state run time and space time complextiy -> use problem constraints and characteristics to brainstorm of more optimal 3. explain solution and get buy in from interviewer - explictly write down run time and space time 4. start coding solution but give high level overview of code structure 5. code solution 6. walk through to check for bugs 7. walk through test case

Linked Hashtable

A hybrid between a linked list and a hashtable, two observations: lookup for a key in LL is O(N) Hashtables have no ordering and so linked hashtable solves these problems

Why do we use tables/ arrays for coming up with bottom up approaches for DP?

Dynamic programming is a method for solving problems by breaking them down into smaller subproblems and solving those subproblems in a way that allows us to build up a solution to the original problem. One common way to implement dynamic programming solutions is to use tables or arrays to store the results of the subproblems, which allows us to reuse those results and avoid recalculating them. Using tables or arrays for dynamic programming is often referred to as the "bottom-up" approach, because it starts with the smallest subproblems and works its way up to the larger ones. This is in contrast to the "top-down" approach, which involves solving the larger subproblems first and then using their solutions to solve the smaller ones. There are several reasons why we might use tables or arrays for dynamic programming: Efficiency: By storing the results of the subproblems in a table or array, we can avoid recalculating them, which can save a significant amount of time and make the overall solution more efficient. Space optimization: Using a table or array can help to reduce the space complexity of the solution, because we only need to store the results of the subproblems that we have already solved, rather than storing the entire recursion tree. Clarity: Tables or arrays can make the solution easier to understand, because they clearly show the relationships between the subproblems and the overall solution. Overall, using tables or arrays for dynamic programming can be a useful way to solve problems efficiently and effectively, by allowing us to reuse the results of the subproblems and build up a solution in a structured and organized way.

When to use different problem solving techniques based off constraints

Here are some constraints that might suggest the use of specific algorithms or problem-solving techniques, along with example problems for each technique: Backtracking: The problem involves finding all possible solutions to a problem, rather than just the optimal one The problem requires you to explore a large search space and prune branches that do not lead to a solution Example problem: The N-Queens problem, which involves placing N queens on an NxN chessboard such that no queen can attack any other queen. Topological Sort: The problem involves a set of tasks that have dependencies on each other The problem requires you to find the order in which the tasks should be completed Example problem: Scheduling tasks in a project management system, where each task has a set of dependencies on other tasks. Breadth-First Search: The problem involves finding the shortest path between two nodes in a graph The problem requires you to explore the graph level by level, rather than depth first Example problem: Finding the shortest path between two cities on a map. Depth-First Search: The problem involves exploring a tree or graph in a recursive manner The problem requires you to explore as deep as possible into the tree or graph before backtracking Example problem: Finding all the paths between two nodes in a graph. Sliding Window: The problem involves finding a pattern or subset within a larger dataset The problem requires you to maintain a window of fixed size as you scan through the data Example problem: Finding the longest substring with no repeating characters in a string. Two Pointers: The problem involves traversing a sorted dataset and finding a pattern or subset within it The problem requires you to maintain two pointers that move towards each other as you scan through the data Example problem: Finding the longest subarray with a sum less than or equal to a given value. Slow and Fast Pointers: The problem involves traversing a linked list or array and finding a pattern or subset within it The problem requires you to maintain two pointers that move at different speeds as you scan through the data Example problem: Finding the middle node in a linked list. Stacks: The problem involves reversing the order of elements in a dataset The problem requires you to use a last-in, first-out (LIFO) data structure to store and retrieve the elements Example problem: Evaluating a mathematical expression written in postfix notation. Queues: The problem involves processing elements in the order in which they were added to a dataset The problem requires you to use a first-in, first-out (FIFO) data structure to store and retrieve the elements Example problem: Implementing a round-robin scheduling algorithm for a CPU. Kadane's Algorithm: The problem involves finding the maximum sum of a subarray within a larger array The problem requires you to maintain a running sum as you scan through the array Example problem: Finding the maximum sum of a contiguous subarray in an array of integers. Quick Select: The problem involves finding the kth smallest element in an unsorted array The problem requires you to use a divide-and-conquer approach to efficiently find the element Example problem: Finding the median of a large dataset. Cyclic Sort: The problem involves sorting a dataset that contains a limited range of

Top k elements

Many problems ask us to find the top, the smallest, or the most/least frequent kk elements in an unsorted list of elements. To solve such problems, sorting the list takes O(n \log(n))O(nlog(n)) time, then finding the kk elements takes O(k)O(k) time. However, the top kk elements pattern can allow us to solve the problem using O(n. \log k)O(n.logk) time without sorting the list first. Which data structure can we use to solve such problems? The best data structure to keep track of the smallest or largest kk elements is heap. With this pattern, we either use a max-heap or a min-heap to find the smallest or largest kk elements, respectively. Iterating the complete list takes O(n)O(n) time, and the heap takes O(\log k)O(logk) time for insertion. However, we get the O(1)O(1) access to the kk elements using the heap.

2 Characteristics or requirements to solve DP (simplified)

Optimal sub-structure = USING sub problems to solve the problem & Overlapping sub-problems = RE-USING subproblems why is mergesort not DP? bc although it has optimal sub-structure, there are no overlapping problems -> we're tackling different portions of the array and they don't overlap OVERALL: DP = USE + RE-USE Subproblems. If you're not REUSING subproblems, it's NOT DP. For example, Merge Sort. You might be wondering - what are the different ways we can use subproblems? That's a skill you build with practice. We saw two such ways in the video: By using f(n-1) + f(n-2) in Fibonacci By using subarrays -> mergeSort(a[0..10]) = merge(mergeSort(a[0..5]), mergeSort(a[6..10]))

Merging intervals

Pattern for problems that deal with overlapping intervals. The key is to understand the scenarios for how two intervals may overlap: interval a interval b 1. Intervals 1 and 2 don't overlap. Interval 1 ends before the start of Interval 2 2. Interval 1 and Interval 2 overlap. Interval 2 ends after Interval 1 3. Interval 2 completely overlaps Interval 1 4. Interval 1 and Interval 2 overlap. Interval 1 ends after Interval 2 5. Interval 1 completely overlaps Interval 2 6. Interval 1 and 2 don't overlap. Interval 1 starts after the end of Interval 2:

Tree hints based on constraints

Sure! Here is an expanded list of problem-solving techniques and hints that might suggest their use when working with binary tree problems: Traversals: If the problem requires you to traverse the tree in a specific order (e.g., in-order, pre-order, post-order), you might consider using a depth-first search or a top-down traversal. If the problem requires you to traverse the tree level by level, you might consider using a breadth-first search or a bottom-up traversal. Recursion: If the problem involves dividing the tree into smaller subproblems and solving them recursively, you might consider using recursive techniques. If the problem requires you to perform the same operation on all nodes in the tree, you might consider using recursive techniques. Stacks and Queues: If the problem requires you to store data in a tree-like structure and perform operations in logarithmic time, you might consider using a binary search tree. If the problem involves traversing the tree and maintaining a stack or queue of data, you might consider using stack- or queue-based data structures. Backtracking: If the problem involves finding a specific path or sequence within the tree, you might consider using backtracking techniques. If the problem requires you to find all possible solutions to a problem, you might consider using backtracking techniques. Topological Sort: If the problem involves finding a linear ordering of the nodes in the tree that respects the dependencies between them, you might consider using topological sort techniques. If the problem requires you to find a sequence of steps that must be followed to solve the problem, you might consider using topological sort techniques. Dynamic Programming: If the problem involves solving subproblems and storing the results to avoid re-computation, you might consider using dynamic programming techniques. If the problem can be divided into overlapping subproblems, you might consider using dynamic programming techniques.

Built in sorting method in python

The built-in .sort() method in Python uses an algorithm called Timsort, which is a combination of Insertion sort and Merge sort. Timsort has an average time complexity of O(n log n) for sorting, which makes it one of the fastest sorting algorithms for real-world data. Regarding the space complexity, it depends on the implementation of the sort method. The default implementation of the .sort() method in Python is not in-place and creates a new list to store the sorted elements. This means that the space complexity is O(n). However, if you sort the list in-place using the sorted() function or the .sort() method with the key or reverse argument, the space complexity would be O(1), since no additional memory is used to store the sorted list. So, to summarize, the built-in .sort() method in Python has an average time complexity of O(n log n) and a space complexity of O(n) when sorting a list in the default way, but if you sort the list in-place, the space complexity would be O(1). this is something you discuss with interviewer (usually they don't consider it taking more than constant space)n

When to use K Way merge

Yes, if both these conditions are fulfilled:The problem involves a set of sorted arrays, or a matrix of sorted rows or sorted columns that need to be merged, either for the final solution, or as an intermediate step.The problem asks us to find the k^{th}kth smallest or largest element in a set of sorted arrays or linked lists. No, if either of these conditions are fulfilled:The input data structures are neither arrays, nor linked lists.The data is not sorted, or it's sorted but not according to the criteria relevant to solving the problem.

When to use Slow and fast pointer algorithm

Yes, if either of these conditions is fulfilled: -Either as an intermediate step, or as the final solution, the problem requires identifying: -the first x \space \%x % of the elements in a linked list, or, the element at the kk-way point in a linked list, for example, the middle element, or the element at the start of the second quartile, etc. -the k^{th}kth last element in a linked list -Solving the problem requires detecting the presence of a cycle in a linked list. -Solving the problem requires detecting the presence of a cycle in a sequence of symbols. No, if either of these conditions is fulfilled: -The input data cannot be traversed in a linear fashion, that is, it's neither in an array, nor in a linked list, nor in a string of characters. -The problem can be solved with two pointers traversing an array or a linked list at the same pace.

what if you check if two hashmaps are equal?

Yes, when you check if two dictionaries (also known as hash maps) are equal in Python using the equality operator ==, it checks if both dictionaries have the same exact key-value mapping. If both dictionaries have the same keys with the same values, then the dictionaries are considered equal, and the expression map1 == map2 would evaluate to True. Example: map1 = {'a': 1, 'b': 2} map2 = {'a': 1, 'b': 2} if map1 == map2: print("The dictionaries are equal" ) else: print("The dictionaries are not equal") This will output: The dictionaries are equal

Dynamic Programming

an algorithmic paradigm that finds the solution to an optimization problem by recursively breaking down the problem into overlapping subproblems and combining their solutions with the help of a recurrence relation or Many computational problems are solved using a divide-and-conquer approach recursively. In these problems, we see an optimal substructure, i.e., the solution to a smaller problem helps us solve the bigger one We've discussed that we need to save the computations, but how can we save and use them? There are two approaches in dynamic programming that we can use to solve the problems: 1.Top-down approach with memoization In the top-down approach, we store all the results of solved subproblems in an array or hash map and use them wherever we encounter an overlapping subproblem during the recursion. 2.Bottom-up approach with tabulation The bottom-up approach allows us to not use recursion as we used it in the top-down approach. The bottom-up approach uses an n-dimensional array to store the results of solved subproblems and use it whenever we encounter an overlapping subproblem.

DFS (graph)

depth-first search def dfs(node): if visited[node] == -1: return False if visited[node] == 1: return True visited[node] = -1 process node for neighbor in node's neighbors: if neighbor not in visited: dfs(neighbor) visited[node] = 1

get the frequency or count of something... think

hashmap.., for example how often character x comes up in string s

Heap functions

heapq.heapify(L) -> transforms list L into heap in place heapq.nlargest(k,L) -> (heapq.nsmallest(k,L) -< returns k largest (smallest) eleemnts in L heapq.heappush(h,e)-> pushes new element onto heap heapq.heappop -> pops smallest element from the heap heapq.heappushpop(h,a) pushes a on the heap and then pops and returns the smallest element

top down vs bottom up dfs (trees)

in top down DFS, as info was passed top down from parent to child, that info was paxkaged inside the arguments of the dfs function in bottomup DFS, as info is collected bottom up from child to parent , that info will be packaged inside the return vals from the dfs function

how to sort based off xth element in sublist - list of lists

listname.sort(key=lambda x: x[3])

more string stuff, lowercase, alphanumeric,

.lower() => returns a lowercase string .alnum()= returns bool whether it's alphanumeric REMEMBER ()()()()() since it's a method

3 sum

1. sort since 2 sum II is done sorted 2. extra dimension so we'll also have an extra poiinter, this added dimension also makes it reasonable that run time to shoot for O(n^2) since twosum ii is O(n) 3. take care of duplicates (by sorting this makes it alot easier) 1. main pointer that's moving from left to right 2. the additional left and right ptr that's essentially two sum ii can handle both of the above by moving the left over once

Bipartite Graph

A bi-partite graph is a graph where nodes in a graph can be separated into 2 groups such that no 2 nodes are in the same group Other phrasing: '2 color problem' -> given graph -> color its nodes with two colors, red/blue such that no two neighbors are the same color

divide and conquer algorithm

A divide and conquer algorithm is a strategy of solving a large problem by: 1.Divide: Divide the given problem into sub-problems using recursion. 2.Conquer: Solve the smaller sub-problems recursively. If the subproblem is small enough, then solve it directly. 3.Combine: Combine the solutions of the sub-problems that are part of the recursive process to solve the actual problem.

BFS (tree)

AKA level order traversal -> going level by level -> use a queue + while loop & keep track of the len of the current level

Looking or keeping in mind duplicates

Always think of a hashset for duplicates -> set() in python do not include duplicates -> O(1) lookup, insertion, and deletion

Time complexity for BFS & DFS

BSF uses Queue to find the shortest path. DFS uses Stack to find the shortest path. Time Complexity of BFS = O(V+E) where V is vertices and E is edges. Time Complexity of DFS is also O(V+E) where V is vertices and E is edges,The run time for both DFS and BFS is different for the different representation of the graph. Graphs can be represented in two ways: adjacency matrix and adjacency list. For adjacency matrix representation, each vertex of the graph has one row and one column. The total size of the matrix is |𝑉2||V2|. Each position of the matrix is visited once for both DFS as well as BFS, therefore the time complexity is given by |𝑉2||V2| where V is the number of vertices in the graph. For adjacency list representation, for each vertex, there is a linked list representation with adjacent vertices and each position in the linked list is visited once for both DFS and BFS. As there are V vertices and E edges in the graph, the time complexity for this representation is given by ||V|+|E||

What is backtracking

Backtracking is a general algorithm for finding solutions to some computational problems, notably constraint satisfaction problems, that incrementally builds candidates to the solutions, and abandons a candidate ("backtracks") as soon as it determines that the candidate cannot possibly be completed to a valid solution

BFS (graph)

Breadth first search Think queue * -> collections.deque([node]) For trees -> make sure to check if left/right is valid before adding to tree def bfs(node): q = collections.deque([node]) visiting[node] = -1 while q: curr = q.popleft() for neighbor in curr's neighbors: if neighbor not in visited: mark as visiting add neighbor to queue mark curr as visited

Reorder list You are given the head of a singly linked-list. The list can be represented as: L0 → L1 → ... → Ln - 1 → Ln Reorder the list to be on the following form: L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → ...

Break it down via several problems, use basic building blocks for linked lists -> reverse linked list, slow and fast pointer (to find the midpoint) and merge lists Pointer manipulation and edgecases are important (even/odd lists for example)

General approach for solving DP problems

Dynamic programming is a technique for solving problems by breaking them down into smaller subproblems and storing the solutions to these subproblems to avoid having to solve them multiple times. It is particularly useful for solving problems that can be divided into overlapping subproblems, where the solution to a given subproblem can be used to solve other subproblems. Here is a general framework for tackling dynamic programming problems: Identify the subproblems: The first step in solving a dynamic programming problem is to identify the subproblems that make up the larger problem. This typically involves breaking the problem down into smaller and smaller pieces until you reach a point where the subproblems are small enough to be solved easily. Determine the optimal substructure: The next step is to determine whether the problem exhibits an "optimal substructure," which means that the solution to a given subproblem can be used to solve other subproblems. This is a key characteristic of dynamic programming problems, and is what allows us to avoid having to solve the same subproblem multiple times. Develop a recursive solution: Once you have identified the subproblems and determined that the problem exhibits an optimal substructure, you can begin developing a recursive solution to the problem. This typically involves writing a recursive function that takes a subproblem as input and returns the solution to that subproblem. Store the solutions to the subproblems: To avoid having to solve the same subproblem multiple times, you can store the solutions to the subproblems in a table or array. This is known as "memoization." Implement a bottom-up solution: Once you have a recursive solution and a way to store the solutions to the subproblems, you can implement a bottom-up solution to the problem. This involves starting with the smallest subproblems and working your way up to the larger subproblems, using the stored solutions to avoid having to solve the same subproblem multiple times.

What is a heap + its operations complexity

Heap is a specialized binary tree =? complete binary tree (filled at every level except maybe last from left to right) in which its keys satisfy the heap property -> the key at each node (if it's a max heap) is at least as great as the keys stored at its children 'Priority Queue' -> A heap is a specific data structure that is used to implement a priority queue. A priority queue is an abstract data type that stores elements with associated priorities, and allows for efficient retrieval and removal of the element with the highest priority (i.e., the "maximum" element). A heap is one way to implement a priority queue, and it has the property that the maximum element is always at the root of the heap. A heap is implemented as a complete binary tree, in which each node has a key (also called a priority) that is greater than or equal to the keys of its children. This property is known as the heap property. There are two types of heaps: max-heaps and min-heaps. In a max-heap, the maximum element is always at the root, and in a min-heap, the minimum element is always at the root. Max -> logN insertions, O(1) lookup for max element an logN deletion of max element and same for min element

Modified BS - find first occurrence in sorted array

If we find the target and what comes directly before it is also a target -> we know that can't be the first occurrence so we treat the current target like it's the rest of the array that's no longer apart of searchable set ... if( (arr[mid] > target) or (a[mid] == target and (mid > 0 and a[mid - 1] == target)): high = mid - 1

Detecting cycle in directed graph

If you're currently visiting a node & you encounter another node that's in the visiting state -> you've encountered a cycle. This can be implemented easily by introducing states to your visited set/map -> not visited = 0 visiting = -1 visited = 1

For DFS, what should you remember wrt aux data structure to use?

In a depth-first search (DFS) algorithm, it is necessary to keep track of the nodes that have been visited in order to avoid getting stuck in an infinite loop. This is because DFS involves recursively exploring the children of a node, and if a node is revisited during the search, it can result in an infinite loop. By keeping a visited set, you can mark each node as visited as it is encountered, and then check the visited set before exploring the children of a node. This ensures that you do not revisit a node that has already been explored, and helps to prevent the search from getting stuck in an infinite loop. Overall, the visited set is an essential component of a DFS algorithm, as it helps to ensure that the search terminates and that the algorithm runs in a reasonable amount of time.

3 questions to ask if backtracking can be used (N queens example)

Now, let's go through a checklist of questions to determine if backtracking can help. Q:Can partial solutions be constructed? A:Yes, partial solutions can be constructed. By placing a certain number of queens on the board, we can partially construct a solution. Q:Can those partial solutions be verified as valid or invalid? A; Yes, we can verify whether a partial solution is valid or invalid by checking if any two queens threaten each other. This can be sped up by acknowledging that the original queens do not threaten each other, and only checking if a newly added queen threatens any of the other queens. Q:Can the solution be verified as complete? A:Yes, the solution is complete when all N queens have been placed on the board. What's great about backtracking is that when the solution is complete, it is also correct, because all incorrect solution paths have been ruled out beforehand.

Topological Sort

ONLY possible for DAGs -> directed acyclic graphs -> directed + no cycles An ordering of nodes within a graph Use a combination of DFS + auxiliary stack [add in dependency graph for visual]

Linked Lists

Often have simple brute force solution O(n) but have subtler solutions that have O(1) space that use the existing list nodes themselves consider using a dummy node to avoid having to check for empty lists and encounter less bugs Problems on lists are often conceptually simple but more about getting the code right / cleanly coding it Algos operating on singly linked lists benefit from slow and fast pointers

Modified BS - find target T, if T is not in array return closest

Record and move on approach When we're doing B.S, we're searching for exact, there's no notion for 'what's closest' So everytime you have a mid -> you check if that mid is better than the current estimate that we have Everytime we have a new iteration, we're theoretically moving closer to the target

Trees

Recursive algos are well suited for trees bc of the nature of each child is a tree (root or subroot with children) and that goes down recursively for all children Some tree problems have simple brute force solutions that use O(n) space complexity, but subtler solutions that use the nodes themselves for O(1) space Consider left and right-skewed trees when doing complexity analysis -> note that O(h) complexity (h = height of the trees) translates to O(log(n)) for balanced trees but O(n) for skewed trees Easy to mistake of treating a node w/ a single child as a leaf

DFS (trees)

Recursive depth first search via binary trees, three different orderings order = order of the root wrt children pre-order: in-order post-order: pre-order: def pre_order(node): if not root: return print(node) pre_order(node.left) pre_order(node.right) in-order: def in_order(node): if not root: return in_order(node.left) print(node) in_order(node.right) post-order: def post_order(node): if not root: return post_order(node.left) post_order(node.left) print(node)

In what case does allocating extra space actually result to a O(1) space complexity?

Remember, if you allocate some space like some auxillary hashmap that doesnt grow wrt input size, for example, you create a dict that maps 5 words to 5 numbers for ALL inputs, that's still considered O(1) space!

Array hints/ types of problems

Sure! Here is an expanded list of problem-solving techniques and hints that might suggest their use when working with array problems: Sorting: If the problem requires you to sort the array or find elements in a sorted order, you might consider using a sorting algorithm such as quicksort, merge sort, or heapsort. If the problem requires you to find the top or bottom elements of an array, you might consider using a sorting algorithm or a min/max heap data structure. If the problem requires you to find the median or percentile of an array, you might consider using a sorting algorithm or the quick select algorithm. Searching: If the problem requires you to search for a specific element in the array, you might consider using a search algorithm such as binary search or linear search. If the problem requires you to determine whether an element exists in the array, you might consider using a search algorithm or a hash table data structure. If the problem requires you to find the position of an element in the array, you might consider using a search algorithm or an array-based data structure such as a queue or stack. Two Pointers: If the problem involves traversing a sorted array and finding a pattern or subset within it, you might consider using the two pointers technique. If the problem requires you to find a pair of elements with a specific property (e.g., a pair of numbers that add up to a given sum), you might consider using the two pointers technique. Slow and Fast Pointers: If the problem involves traversing a linked list or array and finding a pattern or subset within it, you might consider using the slow and fast pointers technique. If the problem requires you to find the middle node in a linked list, you might consider using the slow and fast pointers technique. Sliding Window: If the problem involves finding a pattern or subset within a larger dataset, you might consider using the sliding window technique. If the problem requires you to maintain a window of fixed size as you scan through the data,

heapify runtime

The runtime complexity of the heapify() function in Python's heapq module is O(n), where n is the number of elements in the list being heapified. This means that the function takes linear time to build a heap from a list of n elements. The heapify() function is used to transform a list into a heap, in-place, in linear time. It is implemented using the bottom-up heap construction algorithm, which involves starting at the last non-leaf node and successively sifting down each element until the heap property is restored. Overall, the heapify() function is an efficient way to build a heap from a list of elements, with a runtime complexity of O(n).

How can you tell if a graph is bi-partite?

There are NO odd length cycles

Analyzing structure (i.g., looking for cycles or connected components) of a graph

Use DFS

Merge 2 sorted lists

Use two pointers -> pick smaller val & update pointer, use dummy node to simplify code def merge_sorted_lists(L1, L2): dummy_head, tail = ListNode() while L1 and L2: if L1.data < L2.data: tail.next, L1 = L1, L1.next else: tail.next, L2 = L2, L2.next tail.next = L1 or L2 return dummy_head.next

Backtracking template

Very similar to DFS def back_track(node, state): if state is a solution: res.append(state) return for i in range(x): if i is a solution: state.append(i) back_track(i + 1, state) state.pop()

Two pointer approach

What is it: the two pointers pattern uses two pointers to iterate over an array or list until the conditions of the problem are satisfied. OR Essentially, the two pointers pattern is an application of the prune-and-search strategy, in which, at every step, we're able to safely prune—that is, eliminate—a set of possible solutions. When to use it: The input data can be traversed in a linear fashion, that is, it's in an array, in a linked list, or in a string of characters. The input data is sorted, or else, arranged in a way that is relevant to the problem, such as numeric data sorted in ascending or descending order, or characters arranged symmetrically.

Does Dynamic Programming match my problem?

YES, if the problem exhibits both of these characteristics: 1.Overlapping subproblems, that is, we can use the results of one subproblem when solving another, possibly larger subproblem. 2.Optimal substructure, that is, if the final solution can be constructed from the optimal solutions to its subproblems. NO, if either of these conditions is fulfilled: 1.The problem has non-overlapping subproblems. 2.The optimal substructure property is violated.

When to use top K elements

Yes, if both of these conditions are fulfilled: -We need to find the ***largest, smallest, most frequent, or least frequent subset*** of elements in an unsorted list. -This may be the requirement of the final solution, or it may be necessary as an intermediate step toward the final solution.\ No, if any of these conditions is fulfilled: -The input data structure does not support random access. -The input data is already sorted according to the criteria relevant to solving the problem. -If only 11 extreme value is required, that is, k = 1k=1, as that problem can be solved in O(n)O(n) with a simple scan through the input array.

pythonic way to get the index of the max element of a list (first occurrence and final)

first occurrence my_list = [1, 4, 2, 8, 5] max_index = my_list.index(max(my_list)) final my_list = [1, 9, 9, 2, 3, 6] max_index = max(i for i, x in enumerate(my_list) if x == max(my_list))

what's the runtime of iterating through all subarrays?

for each element in the array: we'll traverse every other element this turns out to be n+(n-1)+(n-2)+(n-3).... -> this turns out to be (n*(n-1))/2 which is O(n^2)

Creating 2d array and initializing based off some condition

m,n = 3, 4 array = [[initial_value if condition else other_value for j in range(n)] for i in range(m)] m being rows n being cols so first cols [] enclosed by broader rows [] [[cols]rows] for the above example 3x4, if we wanted to only initialize the first row and first col we can use the following code For example, if you want to create a 2D array of size 3x4 and initialize the first row and first column to 0 and the rest of the elements to 9, you can use this template like this: m,n = 3, 4 array = [[0 if (i==0 or j==0) else 9 for j in range(n)] for i in range(m)] You can also use any other logical statement you want for the condition, for example m,n = 3, 4 array = [[0 if (i+j)%2 == 0 else 9 for j in range(n)] for i in range(m)] This creates a 2D array of size 3x4, where the elements that are located in the even positions (i+j is even) are initialized to 0, and the rest of the elements are initialized to 9.

intuition for tortoise and hare algo

the key idea is that the pointers start at the same location, but they move forward at different speeds. If there is a cycle, the two are bound to meet at some point in the traversal. To understand the concept, think of two runners on a track. While they start from the same point, they have different running speeds. If the race track is a circle, the faster runner will overtake the slower one after completing a lap. On the other hand, if the track is straight, the faster runner will end the race before the slower one, hence never meeting on the track again. The fast and slow pointers pattern uses the same intuition.


Ensembles d'études connexes

Microbiology Final Exam 3 (Viruses and Fungi)

View Set

Penny Book Ch. 16 GYN Anatomy of the Female Pelvis

View Set

Module 11,12,13 (study this one) Business continuity *come back after

View Set

ENT Chapter 1, 2, & 3 Smartbook ?'s

View Set