Trie Data Structure Space complexity - data-structures

I am wondering how to calculate the space complexity of the trie structure.
As I have been calculating that if the depth(length of word) is N and Pattern Size K(for small alphabets 26) and number of words: W
As per my understanding, it should be: K^N
Whereas many places it is written that is WxKxN.
Could you please elaborate on which is correct and How?

Worst case, it should be O(K^N)
Let's assume that word length is 1, then a single array of size k would be sufficient.
Ex : 'b' (position = 1) k = [null, pointer to another array of size k, null, null, null, ........]
Let's assume that the word length is 2, then we need to have an array of size k for each of the characters which are at the first position in the word
Ex: 'ba'
level 1 ('b') : [null, pointer to an array (lets call it Z) in level 2, null, null, null, ......]
level 2: Z (Second character 'a') : [pointer to another array of size k, null, null, .......]
Let's say we are inserting 'bc' then we are going to have another array of size k for 'c' at position 3 (Assuming you are inserting 'a' at 0, then 'b' at 1 and so on)
So at each level 0, we have an array of size k (size at level 0: K), at level 2 we have k array's of size k (size at level 1: k^2), at level 3 we have a k^2 number of array's of size k (size at level 3: k^3), and so on.
So the space complexity will be k + k^2 + k^3 + ..... k^N (N is word length). This is the worst-case time complexity.

Related

Regarding Hashing - Quadratic Probing Proof

I was looking into the collision resolution methods for hashing, especially in open addressing (eg. linear probing, quadratic probing). Linear probing is easy to understand because it refers something like,
index = hash(value)
for i, 0 -> SIZE
seek_index = (index + i) % SIZE
if map[seek_index] is EMPTY
//proceed insertion
But for Quadratic probing I wonder until when I need to search the empty slot?
index = hash(value)
for i, 0 -> SIZE // Is it should be up to SIZE ?
seek_index = (index + i*i) % SIZE
if map[seek_index] is EMPTY
//proceed insertion
If the limit is SIZE or anything else whats the proof that I will get the EMPTY cell if there in map?
Any reference will be appreciated.
There is no guarantee you'll probe every element in the array.
For example, consider SIZE=5. Then you'll probe (relative to index) at indexes 0, 1², 2², 3², 4², which (modulo 5) are 0, 1, 4, 4, 1. So if the empty spaces are at indexes 2 or 3 (relative to index), then you'll not find them.
Squares mod n are called "quadratic residues", and the number of quadratic residues modulo n cannot exceed n/2 + 1 (n even) or (n + 1)/2 (n odd).

"Lengthless" fast sorting algorithm

I've been looking into sorting algorithms. So far, all the sorting algorithms I've found either rely on a known length (pretty much all sort algos. I can't use them because "proper" length is O(n)), or are slower than quicksort (e.g. insertion sort).
In Lua, there are 2 concepts of length:
Proper sequence length
Is O(n)
Used by ipairs etc
Sequence length
Is O(log n)
Has holes (nil values)
Used by table.insert etc
I've looked into heapsort, but heapsort needs to build a heap, then sort. It doesn't do both as a single operation, which means it still suffers from the O(n) length problem.
With insertion sort, you just run the insertion sort algorithm until you hit the first nil. This sorts only the "proper sequence" part of a table (that is, the keys from 1 to n without any nil values), but insertion sort is slower than quicksort.
Are there any in-place sorting algorithms that, like insertion sort, don't depend on length, but with performance comparable to that of quicksort?
Example insertion sort code (with some help from wikipedia):
function isort(t)
-- In-place insertion sort that never uses the length operator.
-- Stops at the first nil, as expected. Breaks if you use "for i = 1, #t do"
for i in ipairs(t) do
local j = i
while j > 1 and t[j-1] > t[j] do
t[j], t[j-1] = t[j-1], t[j]
j = j - 1
end
end
end
local t = {6, 5, 3, 1, 7, 2, 4, nil, 1, 1, 8, 3, 4, nil, nil, 1}
isort(t)
io.write("{")
if #t > 0 then
io.write(tostring(t[1]))
for i=2, #t do
io.write(", ")
io.write(tostring(t[i]))
end
end
io.write("}\n")
-- stdout:
-- {1, 2, 3, 4, 5, 6, 7, nil, 1, 1, 8, 3, 4, nil, nil, 1}
Since the sort itself must take at least O(n log n), an extra O(n) scan doesn't seem like it would invalidate the algorithm. Using quadratic algorithms such as insertion or bubble sort is false economy.
You could use the heapsort variant where you simply iteratively insert into a growing heap, rather than using the O(n) buildheap algorithm. Heapsort is definitely O(n log n), even if you build the heap incrementally, but I doubt whether it is competitive with quicksort. (It's definitely competitive with insertion sort for large inputs, particularly large inputs in reverse order.)
You can see pseudocode for standard heapsort in Wikipedia. My pseudocode below differs in that it doesn't require the size of the array as a parameter, but instead returns it as the result. It also uses 1-based vectors rather than 0-based, since you are using Lua, so a is assume to run from a[1] to a[count] for some value of count.
procedure heapsort(a):
input: an array of comparable elements
output: the number of elements in the array.
(Heapify successive prefixes of the array)
count ← 1
while a has an element indexed by count:
siftup(a, count)
count ← count + 1
count ← count - 1
(Extract the sorted list from the heap)
i ← count
while i > 1:
swap(a, 1, i)
i ← i - 1
siftdown(a, i)
return count
siftup and siftdown are the standard heap functions, here presented in the 1-based version. The code provided uses a standard optimization in which the sifting is done with a single rotation instead of a series of swaps; this cuts the number of array references significantly. (The swap in the heapsort procedure could be integrated into siftdown for a slight additional savings but it obscures the algorithm. If you wanted to use this optimization, change val ← a[1] to val ← a[count + 1]; a[count + 1] ← a[1] and remove the swap from heapsort.)
In a 1-based heap, the parent of node i is node floor(i/2) and the children of node i are nodes 2i and 2i+1. Recall that the heap constraint requires that every node be no less than its parent. (That produces a minheap, which is used to produce a descending sort. If you want an ascending sort, you need a maxheap, which means changing the three value comparisons below from > to <.)
procedure siftup(a, count):
input: a vector of length count, of which the first count - 1
elements satisfy the heap constraint.
result: the first count elements of a satisfy the heap constraint.
val ← a[count]
loop:
parent ← floor(count / 2)
if parent == 0 or val > a[parent]:
a[count] ← val
return
else
a[count] ← a[parent]
count ← parent
procedure siftdown(a, count):
input: a vector of length count which satisfies the heap constraint
except for the first element.
result: the first count elements of a satisfy the heap constraint.
val ← a[1]
parent ← 1
loop :
child ← 2 * parent
if child < count and a[child] > a[child + 1]:
child ← child + 1
if count < child or not (val > a[child]):
a[parent] ← val
return
else
a[parent] ← a[child]
parent ← child

Same number of 0s and 1s algorithm [duplicate]

This question already has answers here:
Finding the largest subarray with equal number of 0's and 1's
(11 answers)
Closed 7 years ago.
I'm trying to solve the following problem:
Given an binary array containing only 0s and 1s, find the largest subarray which contain equal no of 0s and 1s.
Examples:
Input: arr[] = {1, 0, 1, 1, 1, 0, 0,0,1}
Output: 1 to 8 (Starting and Ending indexes of output sub array)
I could only think of an O(n^2) solution (i.e. the obvious way of starting an array at each subposition and then checking all remaining elements for having the same number of 0s and 1s).
Can somebody figure out a better solution for this problem?
One teensy tiny note on wording: you say find the longest subarray, which implies uniqueness, but even in your example there is more than one ( 0 to 7 or 1 to 8). It would be worded better as "find a subarray of maximal length" or similar. But that's a non-issue.
As for a faster algorithm, first define a new array swap by replacing each instance of a 0 with a -1. This can be done in O(n) time. For your example, we would have
1, -1, 1, 1, 1, -1, -1, -1, 1
Now define another array sum such that sum[i] is the sum of all values swap[0], swap[1], ..., swap[i]. Equivalently,
sum[0] = swap[0];
for i > 1, sum[i] = sum[i-1] + swap[i]
Which again is in O(n) time. So your example becomes
1, 0, 1, 2, 3, 2, 1, 0, 1
Now for an observation. If the number of 1s is equal to the number of 0s in the subarray (arr[i], ..., arr[j]) then in the first new array the 1s will cancel with the corresponding -1, so the sum of all values (swap[i], ..., swap[j]) will be equal to 0. But this is then equal to
swap[0] + swap[1] + ... + swap[j] - (swap[0] + swap[1] + ... + swap[i-1]),
which in turn is equal to
sum[j] - sum[i-1].
Although note we have to be careful if i is equal to 0 because otherwise we are out of the array's bounds. This is an easy check to implement.
Now we have reduced the problem to finding when sum[j] - sum[i-1] is equal to 0. But this is equivalent to finding values j and i such that sum[j] = sum[i-1].
Since we know that for all values in sum they lie between -n and n (where n is the size of the initial array) you can now create another a pair of arrays min and max of size 2n+1. Here, the indices of min and max correspond to potential values of sum, where min[0] will hold the smallest index i for which sum[i] = -n, and min[1] will hold the smallest index i for which sum[i] = -n+1, and so on. Similarly max will hold the largest index. This can also be achieved in linear time. After this step, max[i] and min[i] will correspond to values for which sum[min[i]] = i = sum[max[i]].
Now all you have to do is find the largest value of max[k] - min[k], and this will give you from above i = min[k] + 1 and j = max[k], the indices of a maximal subarray containing an equal number of 0s and 1s. This is also O(n).
I sorta sketched this out a bit roughly, so you have to be careful when i = 0, but that's easily accounted for. Each step is however O(n), so there's your more efficient algorithm.
I believe this can be solved n O(n) using a weight balanced binary tree structure.

Trying to understand "LibrarySort"?

I am reading "Insertion Sort is O(nlogn) by Michael A. Bender , Martín Farach-Colton , Miguel Mosteiro" and I don't quite understand how the algorithm works and how to implement it even with the help of Wikipedia. The following is the description of the algorithm extracted from the original article.
1) Let A be an n-element array to be sorted. These elements are inserted one at
a time in random order into a sorting array S of size (1 + ε)n.
So the first step is creating array of size (1 + ε)n. Let ε = 1, then I need to create an array with twice the size of the original array.
2) The insertions proceed in log(n) rounds as follows.
Each round doubles the number of elements inserted into S and doubles the prefix of S where elements reside.
I understand that there will be outer loop that will loop log(n) time. Each round, I need to double the number of elements from A (original array) to S array. What I don't really understand is "double the prefix of S".
3) Specifically, round ith ends when element 2i is inserted and the elements are rebalanced. Before the rebalance, the 2i elements are in the first (1 + ε)2i positions.
A rebalance moves them into the first (2 + 2ε)2i positions, spreading
the elements as evenly as possible. We call 2 + 2ε the spreading factor.
From what I understand is that for every round, we will do "rebalance". "rebalance" will uniformly spread the original element in S array so that it leaves some gap between the element. The formula to spread the element is: k = i * (1 + ε) where i is old index, and k is a new index.
4) The insertion of 2i−1 intercalated elements within round ith is performed the
brute force way: search for the target position of the element to be inserted by
binary search (amongst the 2i−1 support positions in S), and move elements
of higher rank to make room for the new element. Not all elements of higher
rank need to be moved, only those in adjacent array positions until the nearest
gap is found.
This part shows how to insert each element into S array. First, use binary search to search for where the element should belongs. Then, shift the higher rank until it hit the gap.
This is the translation of the algorithm from what I understand (where A is array to sort and array start with index of 1):
def LibrarySort(A)
n ← length(A)
S ← array of size (1 + ε) * n
for i ← 1 to n
S[i] = null
for i ← 1 to floor(log(n) + 1)
for j ← 2i - 1 to 2i
index = binarysearch(S, A[j])
insert(S, A[j], index)
rebalance()
Then for insertion() function takes 3 parameters: array, item to insert, and location.
def insert(S, item, index)
if S[index] != null
tmp ← S[index]
i ← index + 1
while i <= length(S) and S[i] != null
swap(tmp, S[i])
i++
S[index] ← item
Questions
Is what I understand correct?
What is "double the prefix of S"?
Ad "double the prefix of S": The array (memory) is allocated once at the beginning to the size of (1 + ε) n, where n is total number of elements to be sorted. But the elements are added gradually and as they are added, they are not spread across the whole array, but only some prefix of it. When m elements are rebalanced, they are spread across first (1 + ε) m elements of the array. This is the prefix. When m doubles, so does (1 + ε) m.
Ad correctness: I see one slight mistake:
The formula to spread the element is: k = i * (1 + ε) where i is old index, and k is a new index.
The quoted description does not say what the formula is, but it can't be this one. Because this would map array of length m to length (1 + ε) m, but the description says you are mapping array of length (1 + ε) m to array of length 2 (1 + ε) m.
A simple expression would be k = 2 i where i is old index, but that would not spread the elements evenly. To spread the elements evenly, the formula is k = (2 + 2 ε) i, but i is index excluding any gaps.

Algorithm to determine indices i..j of array A containing all the elements of another array B

I came across this question on an interview questions thread. Here is the question:
Given two integer arrays A [1..n] and
B[1..m], find the smallest window
in A that contains all elements of
B. In other words, find a pair < i , j >
such that A[i..j] contains B[1..m].
If A doesn't contain all the elements of
B, then i,j can be returned as -1.
The integers in A need not be in the same order as they are in B. If there are more than one smallest window (different, but have the same size), then its enough to return one of them.
Example: A[1,2,5,11,2,6,8,24,101,17,8] and B[5,2,11,8,17]. The algorithm should return i = 2 (index of 5 in A) and j = 9 (index of 17 in A).
Now I can think of two variations.
Let's suppose that B has duplicates.
This variation doesn't consider the number of times each element occurs in B. It just checks for all the unique elements that occur in B and finds the smallest corresponding window in A that satisfies the above problem. For example, if A[1,2,4,5,7] and B[2,2,5], this variation doesn't bother about there being two 2's in B and just checks A for the unique integers in B namely 2 and 5 and hence returns i=1, j=3.
This variation accounts for duplicates in B. If there are two 2's in B, then it expects to see at least two 2's in A as well. If not, it returns -1,-1.
When you answer, please do let me know which variation you are answering. Pseudocode should do. Please mention space and time complexity if it is tricky to calculate it. Mention if your solution assumes array indices to start at 1 or 0 too.
Thanks in advance.
Complexity
Time: O((m+n)log m)
Space: O(m)
The following is provably optimal up to a logarithmic factor. (I believe the log factor cannot be got rid of, and so it's optimal.)
Variant 1 is just a special case of variant 2 with all the multiplicities being 1, after removing duplicates from B. So it's enough to handle the latter variant; if you want variant 1, just remove duplicates in O(m log m) time. In the following, let m denote the number of distinct elements in B. We assume m < n, because otherwise we can just return -1, in constant time.
For each index i in A, we will find the smallest index s[i] such that A[i..s[i]] contains B[1..m], with the right multiplicities. The crucial observation is that s[i] is non-decreasing, and this is what allows us to do it in amortised linear time.
Start with i=j=1. We will keep a tuple (c[1], c[2], ... c[m]) of the number of times each element of B occurs, in the current window A[i..j]. We will also keep a set S of indices (a subset of 1..m) for which the count is "right" (i.e., k for which c[k]=1 in variant 1, or c[k] = <the right number> in variant 2).
So, for i=1, starting with j=1, increment each c[A[j]] (if A[j] was an element of B), check if c[A[j]] is now "right", and add or remove j from S accordingly. Stop when S has size m. You've now found s[1], in at most O(n log m) time. (There are O(n) j's, and each set operation took O(log m) time.)
Now for computing successive s[i]s, do the following. Increment i, decrement c[A[i]], update S accordingly, and, if necessary, increment j until S has size m again. That gives you s[i] for each i. At the end, report the (i,s[i]) for which s[i]-i was smallest.
Note that although it seems that you might be performing up to O(n) steps (incrementing j) for each i, the second pointer j only moves to the right: so the total number of times you can increment j is at most n. (This is amortised analysis.) Each time you increment j, you might perform a set operation that takes O(log m) time, so the total time is O(n log m). The space required was for keeping the tuple of counts, the set of elements of B, the set S, and some constant number of other variables, so O(m) in all.
There is an obvious O(m+n) lower bound, because you need to examine all the elements. So the only question is whether we can prove the log factor is necessary; I believe it is.
Here is the solution I thought of (but it's not very neat).
I am going to illustrate it using the example in the question.
Let A[1,2,5,11,2,6,8,24,101,17,8] and B[5,2,11,8,17]
Sort B. (So B = [2,5,8,11,17]). This step takes O(log m).
Allocate an array C of size A. Iterate through elements of A, binary search for it in the sorted B, if it is found enter it's "index in sorted B + 1" in C. If its not found, enter -1. After this step,
A = [1 , 2, 5, 11, 2, 6, 8, 24, 101, 17, 8] (no changes, quoting for ease).
C = [-1, 1, 2, 4 , 1, -1, 3, -1, -1, 5, 3]
Time: (n log m), Space O(n).
Find the smallest window in C that has all the numbers from 1 to m. For finding the window, I can think of two general directions:
a. A bit oriented approach where in I set the bit corresponding to each position and finally check by some kind of ANDing.
b. Create another array D of size m, go through C and when I encounter p in C, increment D[p]. Use this for finding the window.
Please leave comments regarding the general approach as such, as well as for 3a and 3b.
My solution:
a. Create a hash table with m keys, one for each value in B. Each key in H maps to a dynamic array of sorted indices containing indices in A that are equal to B[i]. This takes O(n) time. We go through each index j in A. If key A[i] exists in H (O(1) time) then add an value containing the index j of A to the list of indices that H[A[i]] maps to.
At this point we have 'binned' n elements into m bins. However, total storage is just O(n).
b. The 2nd part of the algorithm involves maintaining a ‘left’ index and a ‘right’ index for each list in H. Lets create two arrays of size m called L and R that contain these values. Initially in our example,
We also keep track of the “best” minimum window.
We then iterate over the following actions on L and R which are inherently greedy:
i. In each iteration, we compute the minimum and maximum values in L and R.
For L, Lmax - Lmin is the window and for R, Rmax - Rmin is the window. We update the best window if one of these windows is better than the current best window. We use a min heap to keep track of the minimum element in L and a max heap to keep track of the largest element in R. These take O(m*log(m)) time to build.
ii. From a ‘greedy’ perspective, we want to take the action that will minimize the window size in each L and R. For L it intuitively makes sense to increment the minimum index, and for R, it makes sense to decrement the maximum index.
We want to increment the array position for the minimum value until it is larger than the 2nd smallest element in L, and similarly, we want to decrement the array position for the largest value in R until it is smaller than the 2nd largest element in R.
Next, we make a key observation:
If L[i] is the minimum value in L and R[i] is less than the 2nd smallest element in L, ie, if R[i] were to still be the minimum value in L if L[i] were replaced with R[i], then we are done. We now have the “best” index in list i that can contribute to the minimum window. Also, all the other elements in R cannot contribute to the best window since their L values are all larger than L[i]. Similarly if R[j] is the maximum element in R and L[j] is greater than the 2nd largest value in R, we are also done by setting R[j] = L[j]. Any other index in array i to the left of L[j] has already been accounted for as have all indices to the right of R[j], and all indices between L[j] and R[j] will perform poorer than L[j].
Otherwise, we simply increment the array position L[i] until it is larger than the 2nd smallest element in L and decrement array position R[j] (where R[j] is the max in R) until it is smaller than the 2nd largest element in R. We compute the windows and update the best window if one of the L or R windows is smaller than the best window. We can do a Fibonacci search to optimally do the increment / decrement. We keep incrementing L[i] using Fibonacci increments until we are larger than the 2nd largest element in L. We can then perform binary search to get the smallest element L[i] that is larger than the 2nd largest element in L, similar for the set R. After the increment / decrement, we pop the largest element from the max heap for R and the minimum element for the min heap for L and insert the new values of L[i] and R[j] into the heaps. This is an O(log(m)) operation.
Step ii. would terminate when Lmin can’t move any more to the right or Rmax can’t move any more to the left (as the R/L values are the same). Note that we can have scenarios in which L[i] = R[i] but if it is not the minimum element in L or the maximum element in R, the algorithm would still continue.
Runtime analysis:
a. Creation of the hash table takes O(n) time and O(n) space.
b. Creation of heaps: O(m*log(m)) time and O(m) space.
c. The greedy iterative algorithm is a little harder to analyze. Its runtime is really bounded by the distribution of elements. Worst case, we cover all the elements in each array in the hash table. For each element, we perform an O(log(m)) heap update.
Worst case runtime is hence O(n*log(m)) for the iterative greedy algorithm. In the best case, we discover very fast that L[i] = R[i] for the minimum element in L or the maximum element in R…run time is O(1)*log(m) for the greedy algorithm!
Average case seems really hard to analyze. What is the average “convergence” of this algorithm to the minimum window. If we were to assume that the Fibonacci increments / binary search were to help, we could say we only look at m*log(n/m) elements (every list has n/m elements) in the average case. In that case, the running time of the greedy algorithm would be m*log(n/m)*log(m).
Total running time
Best case: O(n + m*log(m) + log(m)) time = O(n) assuming m << n
Average case: O(n + m*log(m) + m*log(n/m)*log(m)) time = O(n) assuming m << n.
Worst case: O(n + n*log(m) + m*log(m)) = O(n*log(m)) assuming m << n.
Space: O(n + m) (hashtable and heaps) always.
Edit: Here is a worked out example:
A[5, 1, 1, 5, 6, 1, 1, 5]
B[5, 6]
H:
{
5 => {1, 4, 8}
6 => {5}
}
Greedy Algorithm:
L => {1, 1}
R => {3, 1}
Iteration 1:
a. Lmin = 1 (since H{5}[1] < H{6}[1]), Lmax = 5. Window: 5 - 1 + 1= 5
Increment Lmin pointer, it now becomes 2.
L => {2, 1}
Rmin = H{6}[1] = 5, Rmax = H{5}[3] = 8. Window = 8 - 5 + 1 = 4. Best window so far = 4 (less than 5 computed above).
We also note the indices in A (5, 8) for the best window.
Decrement Rmax, it now becomes 2 and the value is 4.
R => {2, 1}
b. Now, Lmin = 4 (H{5}[2]) and the index i in L is 1. Lmax = 5 (H{6}[1]) and the index in L is 2.
We can't increment Lmin since L[1] = R[1] = 2. Thus we just compute the window now.
The window = Lmax - Lmin + 1 = 2 which is the best window so far.
Thus, the best window in A = (4, 5).
struct Pair {
int i;
int j;
};
Pair
find_smallest_subarray_window(int *A, size_t n, int *B, size_t m)
{
Pair p;
p.i = -1;
p.j = -1;
// key is array value, value is array index
std::map<int, int> map;
size_t count = 0;
int i;
int j;
for(i = 0; i < n, ++i) {
for(j = 0; j < m; ++j) {
if(A[i] == B[j]) {
if(map.find(A[i]) == map.end()) {
map.insert(std::pair<int, int>(A[i], i));
} else {
int start = findSmallestVal(map);
int end = findLargestVal(map);
int oldLength = end-start;
int oldIndex = map[A[i]];
map[A[i]] = i;
int _start = findSmallestVal(map);
int _end = findLargestVal(map);
int newLength = _end - _start;
if(newLength > oldLength) {
// revert back
map[A[i]] = oldIndex;
}
}
}
}
if(count == m) {
break;
}
}
p.i = findSmallestVal(map);
p.j = findLargestVal(map);
return p;
}

Resources