Arrays
Definition
An array is a fixed-size collection of data stored in a contiguous block of memory.
Compexities
- Access & update at index
i
takes $O(1)$ time- Deleting or inserting at index
i
requires all elements afteri
to be moved => $O(n)$ time- Appending is handled by resizing when the pre-allocated space is insufficient => $O(n)$ worst case. Since this doesn't happen often the amortized complexity is $O(1)$.
Python
Implemented as type list
in Python (both a vector and an array)
Working with arrays
- With arrays you can operate efficiently on both ends.
- Slice arrays with
A[i:j:k]
, selecting[i,j)
, with steps of sizek
; negative entries are allowed- Instantiate a 2D array with
M = [[col for col in range(5)] for row in range(3)]
- From
bisect
usebisect(A)
,bisect_left(A)
, orbisect_right(A)
to conduct binary search- From
copy
usecopy(A)
orlist(A)
to make a shallow copy, alternatively make adeepcopy(A)
; these only work on compound objects, those which contain sub-objects (lists, sets, dicts, etc.), but not ints or strings- Reverse and array in-place with
A.reverse()
, create an iterator withreversed(A)
, or use slicingA[::-1]
- Sort an array in-place with
A.sort()
or make a shallow copy withsorted(A)
- Delete and element with
del A[i]
Key functions from the
random
library:
randrange(a, b)
: return a random integer in range [a, b)randint(a, b)
: return a random integer in range [a, b]choice(seq)
: return a random element from the sequencechoices(seq, k=k)
: return a k-sized list of random elements from the sequencerandom()
: return a random float in range [0.0, 1.0)
Rust
Use Vec<T>
for a vector. Use [T]
for an array, e.g., let a = [0_f32; 64];
will create an array of 64 floating point zeros. - Be aware of the differences between using .iter()
and .into_iter()
. See Essential Rust Syntax Explained.
Essential Operations
Attention
Implicitly moving out of a
Vec
is not allowed as it would leave it in an invalid state — one element is moved out, the others are not. If you have a mutableVec
, you can use a method likeVec::remove
to take a single value out -- from this post.
Basics. Basic array-style operations are conducted via
.len()
- Gets the length of the vector.push(x)
- Appends an element to the back of the vector.pop()
- Removes an element from the back of the vector, returns it.append(other)
- Extends/concatenates the two vectors together.insert(i, x)
- Insertsx
at indexi
(operation shifts remaining elements right).remove(i)
Removes the element ati
(operation shifts remaining elements left).contains(x)
- True if the vector containsx
(operation is $O(n)$).join(x)
- Joins the elements given the same typex
.get(x)
- Gets a reference to an element or sub-slice.sort()
- Sorts the elements in $O(n \text{ log } n)$ time.
Replace value in-place. Removing and inserting from an array is wasteful because all of the elements have to be shifted. Instead, use std::mem::replace
, or access it directly (not recommended).
let mut v = vec![1, 2, 3, 4];
let got = std::mem::replace(&mut v[2], 42); // -> [1, 2, 42, 4], got = 3
// OR replace directly, but will panic out of bounds
v[1] = 56; // -> [1, 56, 42, 4]
let mut v = vec![1, 2, 3, 4];
let got = std::mem::replace(&mut v[2], 42); // -> [1, 2, 42, 4], got = 3
// OR replace directly, but will panic out of bounds
v[1] = 56; // -> [1, 56, 42, 4]
Looping. When looping over a vector you must use either .into_iter()
or .iter()
. The following code doesn't compile because v
is consumed by into_iter()
, and therefore cannot be used with iter()
later without a clone()
.
let v = vec![1, 2, 3, 4, 5];
// `into_iter()` consumes the original vector returning an owned variable
// `iter()` returns a reference to each variable
for x in v.into_iter() { ... } // => i32
for x in v.iter() { ... } // => &i32 notice this is a reference
let v = vec![1, 2, 3, 4, 5];
// `into_iter()` consumes the original vector returning an owned variable
// `iter()` returns a reference to each variable
for x in v.into_iter() { ... } // => i32
for x in v.iter() { ... } // => &i32 notice this is a reference
Filtering. Use .filter()
or .retain()
to remove values from the vector.
let nums: Vec<i32> = vec![3, 1, 3, 4, 4, 55, 4, 66];
let filt: Vec<i32> = nums.into_iter().filter(|&x| x < 10).collect();
let filt: Vec<&i32> = nums.iter().filter(|&x| *x < 10).collect();
// OR use in-place methods
let mut nums: Vec<i32> = vec![3, 1, 3, 4, 4, 55, 4, 66];
nums.retain(|&x| x < 10);
let nums: Vec<i32> = vec![3, 1, 3, 4, 4, 55, 4, 66];
let filt: Vec<i32> = nums.into_iter().filter(|&x| x < 10).collect();
let filt: Vec<&i32> = nums.iter().filter(|&x| *x < 10).collect();
// OR use in-place methods
let mut nums: Vec<i32> = vec![3, 1, 3, 4, 4, 55, 4, 66];
nums.retain(|&x| x < 10);
Removing duplicates.
// Option 1: Sort O(n log n) and deduplicate O(n)
let mut nums: Vec<i32> = vec![3, 1, 3, 4, 4, 55, 4, 66];
nums.sort();
nums.dedup();
// Option 2: Convert to set and back to vector
let mut nums: Vec<i32> = vec![3, 1, 3, 4, 4, 55, 4, 66];
let set: HashSet<i32> = nums.into_iter().collect();
let vec: Vec<i32> = set.into_iter().collect();
// Option 1: Sort O(n log n) and deduplicate O(n)
let mut nums: Vec<i32> = vec![3, 1, 3, 4, 4, 55, 4, 66];
nums.sort();
nums.dedup();
// Option 2: Convert to set and back to vector
let mut nums: Vec<i32> = vec![3, 1, 3, 4, 4, 55, 4, 66];
let set: HashSet<i32> = nums.into_iter().collect();
let vec: Vec<i32> = set.into_iter().collect();
Problems
5.1 Dutch National Flag Problem
I successfully found the brute force solution. The trick that I missed on this question was that I only used two pointers, but three pointers were needed into the array. One for lesser, one for equal, and one for larger. I also ran into the difficulty that I didn't look ahead to how the sorting would resolve itself, but instead presumed that it would have to be sorted on the initial switch. This caused a complicated if-elif-else
block to try and handle everything.
To solve this problem you can either use two passes, with two pointers each time. On the first pass, keep track of the left-most element larger than the pivot, and then move an index pointer to the right until you find an element smaller than the pivot, then switch them. It's okay that you'll have pivot values interspersed with larger values, because you do another pass. On the second pass, loop through from the right, keeping track of the rightmost value equal to the pivot, then move an index pointer until you find one larger than the pivot, swap them, and increment the pivot-equal index pointer.
The one-pass solution requires three pointers as mentioned above, and you combine the above two-pass solution into an if-block on each iteration that has three cases. Smaller, equal (then increment) or larger, if not equal, swap them.
5.2 Increment integer array
Got this one, fast version, first try, good work!
5.6 Buy and sell a stock once OR LC 121 Best time to buy and sell stock
Solution: Create variables for the max difference and current min. With a single iteration conduct the following steps, (i) update the current difference and update the max difference if it is larger, (ii) update the min price if the current price is lower. This problem can be solved in $O(n)$ time and $O(1)$ additional space.
Trick: Focus on the minimum price and the maximum difference, not the global max and min values---the target max may not be the global max. You cannot focus on the global max and min values directly because there is an intrinsic ordering that needs to be accounted for, e.g. the stock needs to be bought at a min prior to selling at a max. If you only keep track of the max and min values, then it's difficult to ensure that the min comes before the max.
Alternative: This problem can also be solved with a two-pointer approach whereby the right pointer moves faster than the left pointer and searches for maximums and the left pointer remains at the current global minimum.
Tip
Focus on the output entity. The fact that the question asks to return maximum profit hints that this variable should be tracked instead of variables that can be used to derive the max profit, such as min and max. Follow the output.
5.12 Sample random data
5.17 Sudoku checker problem
When indexing a native Python 2D array X[i][j]
accesses the ith row at the jth column
5.18 Spiral ordering of a 2D array
Did this one correctly with NumPy (after some time though), the difficultly wasn't particularly the solution, but instead it was coding it correctly. Author recommends taking only n-1 for each spiralled row/column.
- Useful tools:
filter
to remove all the zerosset
to remove duplicates, and check if equal to the length of list prior
LC 11 Container with Most Water
My first solution (C++) wasn't quite right. I implemented the pseudocode correctly, but my approach failed to search all possible combinations.
I did use an array-based approach, this is good. I also used a two-pointer approach, as is recommended for the best solution. However, I started both pointers at the left-most index. I should've noted that I should put the pointers at the left-most and right-most placements (as far apart as possible), and slowly move them together, calculating if any new areas are larger than the current max area.
Rust solution
use std::cmp::{min, max};
impl Solution {
pub fn max_area(height: Vec<i32>) -> i32 {
let mut li = 0;
let mut ri = height.len() - 1;
let mut max_area = 0;
let mut curr_area = 0;
while li < ri {
// Be aware of the usize and i32 casting required here
curr_area = min(height[li], height[ri]) * (li as i32 - ri as i32).abs();
max_area = max(max_area, curr_area);
if height[li] < height[ri] {
li = li + 1;
} else {
ri = ri - 1;
}
}
max_area as i32
}
}
use std::cmp::{min, max};
impl Solution {
pub fn max_area(height: Vec<i32>) -> i32 {
let mut li = 0;
let mut ri = height.len() - 1;
let mut max_area = 0;
let mut curr_area = 0;
while li < ri {
// Be aware of the usize and i32 casting required here
curr_area = min(height[li], height[ri]) * (li as i32 - ri as i32).abs();
max_area = max(max_area, curr_area);
if height[li] < height[ri] {
li = li + 1;
} else {
ri = ri - 1;
}
}
max_area as i32
}
}
LC 21 Merge Two Sorted Lists
Solution: Start by creating an output linked list with curr = dummy_head = ListNode()
and conduct all further operations on curr
. Loop over both input lists until one is exhausted (using a while loop with an AND operator) to update curr
according to whether the head of list1
or list2
is smaller. Linked list operations including append and move are required here. Once one of the lists is exhausted append the remaining head to curr
to complete the list. Finally, return dummy_head.next
to mark the head of the resulting linked list (since curr
now points to the tail).
- One loop => time complexity of $O(n)$
- Output linked list => space complexity of $O(n)$ with no additional space complexity besides the output
Alternative (not recommended): Before conducting the while
loop, iterative over both input lists and determine which one has that largest tail, then swap accordingly such that list1
will always be exhausted first. This removes the need to use an AND operator in the while
condition, but adds additional null case conditions and runtime overhead (due to extra iterations). Time and space complexities remain the same.
LC 125 Valid Palindrome
Solution: Use a two tracker approach initializing the index trackers with left, right = 0, len(s) - 1
. Create an alphanumeric lookup with alphanumeric = set("abcdefghijklmnopqrstuvwxyz0123456789")
. Set the string to lowercase. Finally, loop over the string while left < right
. At each iteration check that the characters pointed to by the left and right trackers are alphanumeric, if not, increment the appropriate tracker and continue
. If both characters are alpha numeric, check if they're different, if they are then it's not a palindrome and return False
. At the end of the loop, return True
.
Checking alphanumeric-ness in Python
- Instead of creating an alphanumeric lookup, strings in Python are equipped with the
.isalnum()
method which will return true if the character/string is alphanumeric and false otherwise.
LC 733 Flood Fill
Solution: This is sort of an array and sort of a graph search problem. Do DFS given the starting point, make sure to establish variables for visited
nodes and the starting_color
so you know which elements to ignore when you traverse into them.
Trick: Ensure that you evaluate conditionals for grid bounds. The trick to this problem is more so clean coding than conceptual difficulty, that doesn't mean it's dead-beat simple however.
LC 169 Majority Element
Solution 1: Sorting. Sort the array and return the element in the middle.
- Time complexity $O(n \text{ log } n)$
- Space complexity O(1)
Solution 2: Hash table. Populate a hash table with the frequencies of the elements and then return the key that matches the maximum value.
- Time complexity O(n + w) from two passes, one to populate the hash table O(n) and one to get the max key O(w) where w is the number of unique entries.
- Space complexity O(w) for the hash table
Solution 3: Moore's voting algorithm. See here for a full explanation. This involves an iterative increment and decrement approach whereby the majority element always becomes the final candidate. Start with a count
and candidate
. On each iteration, if count == 0
, make the the new candidate and increment, if the current value doesn't equal the candidate, decrement the count. If the current value does match the candidate, increment the count. In the end the candidate will be correct. Keep these sorts of incremenet/decrement algorithms in mind as they sometimes crop up.
LC 53 Maximum Subarray
This question tests your ability to identify either a dynamic programming approach, or a very specific algorithmic approach to the problem.
Solution 1: Kadane's algorithm. Establish max_sum = float('-inf')
and current_sum = 0
variables. Loop over the array, on each iteration update the current sum, if the current sum is greater than the max sum, update the max sum. If at any time the current sum becomes negative, then set the current sum to 0—the most recently seen elements are reducing the current sum and cannot be contained within the maximum subarray.
Solution 2: Dynamic programming. Here, the key to the divide and conquer approach is to hold the largest sum of the elements already seen, and compare that to the current element. Is the largest sum already seen, plus the current element larger than the current element alone? Update to whichever is larger, and if the new largest sum is the globally largest, update the outer variable max_sum
.
LC 57 Insert Interval
This problem tests your ability to merge overlapping intervals and manage complex insertion orders.
Solution: Create a result = []
to store the output. Loop over the intervals, if the interval is before the new interval, append it to result
, if it's within the interval, take the max and min of the current bounds to 'stretch' the new interval. If the interval is after the new interval (non-overlapping) then append the new interval, and update the new interval to the current interval. Since subsequent intervals are guaranteed not to overlap, each new interval will be appended correctly.
LC 542 01 Matrix
This questions tests your understanding of grid traversal and multi-source BFS traversal. See this solution to the problem.
...redo...
Trick: Recognizing that this is a BFS problem is essential. DFS can be used for the similar LC 733 Flood Fill problem, however, you will run into issues regarding how each grid square is approached, e.g., if you approach a 1 through three previous 1's, you might set that square to 3 even though it's surrounded on 3 sides by 0s.
LC 15 Three Sum
This question tests your ability to recognize that, if you fix one value, it becomes the classic Two Sum problem. There are additional complexities to this problem. On LeetCode you need to solve it in a specific way or else you'll receive a timeout error, even if the complexity is $O(n^2)$ as per the recommended solution.
Solution: Sort the array. Next implement a two-pointer algorithm where you fix each entry one (resulting in $O(n^2)$ time complexity). Starting from either end, if you find that the resulting sum is negative, move the left pointer up (to increase the total sum) or if it's positive, move the right pointer down (to reduce the total sum).
Trick 1: Sort the array.