Arrays
These notes cover arrays.
Static Arrays
An array is simply a contiguous block of memory:
Each cell is indexed from 0 to where is the number of discrete data objects stored; here, The block of memory can then be treated as an aggregate of scalar data. The size of this block depends on the number of bytes consumed by the scalar data. For example, suppose we declared four arrays, and
Each of and has a preset size.
array | element size | total size | start address | element |
---|---|---|---|---|
Initializing an array is akin to claiming property in RAM. That is, nothing else may touch the contiguous block we've said is ours. The right to claim property, however, isn't exclusively ours — other entities on the system bear the right as well. This means that, at any given moment, we have neighbors:
Above, and are other programs running separately from ours. Just as they aren't allowed to touch our block of memory, we aren't allowed to touch theirs.
static array. Let be a finite sequence of sets all initially empty, and let be an index. We call a static array if, and only if, the following properties hold. has a variable length denoted corresponding to the cardinality of If then is the first element, and if then is the last element. has a constant capacity, denoted which restricts as follows:
where is a constant called the base address, is the datatype size, and is an index.
Costs-benefit Analysis
Say we have some array To access some element , we perform one memory access to read/write the data. That is, a operation. This isn't always the case for connected data structures like linked lists.
Insertion
One operation where arrays perform poorly, however, is insertion. Suppose we're given the following array, with enough space allocated for an insertion:
We want to insert the value 6 at index 2, currently occupied by 7. We start with a pointer to the last valid index. Then, so long as is greater than the index we'd like to insert at, we right-shift the elements.
Once the condition no longer holds. We can now set
Below is an algorithm for this operation. For all future discussions, we will employ the operation below as the operation
array-insert
- Input: an array an index and an element
- Output: a mutated wherein
- Syntax:
- init
- do
start loop body
- if
end loop body
- return
From the algorithm above, we have the following property:
property. Let be a static array with with let be an index, let be an object. Given if then otherwise,
As we'll see later, linked lists handle insertion within in the worst case. Observe further that we assumed the array has enough memory for us to right-shift. Without such memory, we can assume that the right-most element is effectively lost upon shifting. This reveals both a gift and a curse to arrays: They're space-deterministic. Once declared, we cannot go back to the system and ask, "Actually, could you make that size ?"
Dynamic Arrays
Dynamic arrays are one solution to the static array's problem of space-determinism. Say we start with the following static array
When we first declared it was fixed to a capacity of 3, and we now have just two addresses remaining. The dynamic array's approach is as follows. When we initialize the fourth address, a function allocates a new array with a capacity of writes the contents of into disclaims and returns the base address of In the diagram below, we assume
command | state |
---|---|
define new array claiming 3 more addresses than | |
copy into | |
disclaim | |
return |
Problems
Deduplication
Given an array with integers stored in non-decreasing order — some of which are duplicated, write an algorithm that returns the first elements of without duplicates.
Consider the following array whose elements are sorted in non-decreasing order.
We want to remove duplicates in the array (here, 3 and 5). This operation is called deduplicating or deduping Let be the length of If then we return immediately, since we need at least 2 elements for a duplicate. We then set a pointer on the second element.
Next, we use a variable to keep track of the last index we saw a duplicate. At every case where (the current element is the same as its predecessor), we increment In doing so, we're effectively marking an index where the duplicated element should go to.
previous state | resulting state | |
---|---|---|
false | ||
false | ||
true | ||
false | ||
false | ||
true |
Once the loop is done, we return Here, we get corresponding to the first elements of without duplicates.
dedup
- Input: An array of non-decreasing integers containing duplicates.
- Output: The first elements of without duplicates.
- init
- do
begin loop body
- if then
- else
- if then
- if
end loop body
- return
Sequential Search
Write an algorithm that performs sequential search.
Algorithm: Sequential search.
- while do
- if return
- return
Binary Search
Write an algorithm that performs binary search.
Algorithm: Binary search.
- do
- if then return
- else if return
- else
- return
Maximum Value
Given an array of integers write a function that returns the largest integer in .
The standard iterative approach:
find-max
- Input: a non-empty array of integers
- Output: The largest integer,
- init
- if then
- if then
- goto
- return
As seen above, this algorithm has a worst-case time complexity of where is the number of elements. It is, however, memory efficient —
Maximum Subarray
Given an array of integers, find the subarray (containing at least one number) with the largest sum and return its sum. For example, the array returns given that the maximum subarray is
Finding the maximum subarray is a problem with many solutions. One solution is Kadane's algorithm:
kadane's algorithm
- Input: an array of integers
- Output: an integer
- init
- while do
- return
This algorithm runs on time. The variable is used to keep track of the current running sum, and the variable is used to keep track of previous running sums (both initially set to the 's first element). For each array element we compute If we update to take on the result. Then we compare against If (the new running sum) is greater than (the previous running sum), we update The process continues until we reach the last element. The problem: This algorithm, as written, will only work for positive integers. Below is a trace for
1 | 4 | 9 | 9 | ||
2 | -1 | 8 | 9 | ||
3 | 7 | 15 | 15 | ||
4 | 8 | 23 | 23 |
Pair Sum
Given an array and an integer write an algorithm that returns a pair of indices whose elements sum to The indices returned must be unique. For any given such a pair exists.
The approach below uses a hash table, denoted We use a variabe to keep track of where is an element of the array. At each iteration, we store the pair where is the index of Before we do so, however, we compute and check if contains If it does, then we've found a pair, and return the pair where is the value associated with the key
Why does this work? Because where and are elements of the array At each iteration, we see so what we need is a where (the current element at each iteration) is equal to
pair sum
- Input:
- an array of integers
- an integer
- Output: a pair corresponding to indices.
- init
- do
start loop body
- if then
- return
- if then
end loop body
- return
This approach has both a linear time complexity and a linear space complexity hash table implementation aside.
Pair Product
Given an array and an integer write an algorithm that returns a pair of indices whose elements multiply to the given target. The indices returned must be unique. For any given such a pair exists, and all elements of are less than
Similar to the pair sum problem, the approach below uses a hash table.
pair product
- Input: an array of integers
- Output: an integer
- init
- do
begin loop body
- if then
- if then return
- if then
end loop body
- return
This approach takes time, hash table implementation aside, alongside a space complexity.
Stock Timing
Let be an array of prices, where is the price of a given stock on the day. Write an algorithm below maximizes profit by choosing a single day to buy one stock, and choosing a different day in the future to sell that stock.
Conditions
- a non-empty array of prices where
- length of prices array
- while do
- if then
- else
- if then
- return
exhibit.
On day 2, the price is 1, and on day 5, the price is 6. Thus, the profit is:
which is the largest possible profit.
exhibit.
Here, no transactions are done and the maximum profit is 0.
Intersection
Given in two arrays and as arguments write an algorithm that returns a new array containing elements common to both arrays (i.e., ). The order of insertion is irrelevant, and neither nor contain duplicates.
A straightforward approach would be use nested loops, comparing each element of array against the elements of array The implementation is left as an exercise. Such an approach would yield where is the length of the first array and is the length of the second. The space complexity would also yield (the minimum of and ).
The approach below employs a hash table, keeping the time complexity down to where corresponds to the amount of hash table insertions (assumed to be a constant time operation, barring hash table implementation concerns), and corresponds to the amount of hash table lookups (again, assumed to be a constant time operation, barring hash table implementation concerns).
intersection
- Input: an array and an array
- Output: an array where
- init
- do
begin loop 1 body
- if then
end loop 1 body
- do
begin loop 2 body
- if then
- if then
- if then
end loop 2 body
- return
While this approach has a linear time complexity, it comes at the cost of an increased space complexity. With the straightforward approach, the space complexity was kept to Under the hash table approach, we now have The constant multiple can be significant at higher scales. Comparing the lengths of the two arrays to determine which array to push hash table elements to can help reduce the space consumed by the hash table
Five Sort
Given an array integers , write an algorithm that rearranges the elements of such that all instances of the integer 5 appear at the end of The algorithm should perform this operation in-place: Mutate the original array and return the mutated array upon completion. Elements that are not 5 may appear in any order in the output, so long as all 5s are at 's end.
The trick here is to use a two-pointer approach. We start with a pointer at the start, and a pointer at the end:
First, we check if is already pointing at a 5 (). In this case, So, we decrement (and continue decrementing) until no longer points at a 5 ().
The will move (and continue to move) until
Once we reach the state where and we have the two pointers swap their pointees:
After the swap is done, we decrement and increment
Now have and so we swap.
Increment and decrement again:
Now we have both pointers at the same index, so we end.
five sort
- Input: an array
- Output: mutated, such that, for all indices if and then
- init.
- do
start loop body
- if then
- else if then
-
swap
-
- else
- if then
end loop body
- return
The notion of "continue to move" is encapsulated in the strict branching above. That is, at each iteration, we have three mutually exclusive decisions: decrement swap and increment or just increment Notice the effect of this: We don't reach the state unless we've continued to move.
This algorithm has a linear time complexity, and a constant space complexity,