The question is to find a pair of integers (a,b) from a set M of unsigned integers, where a-b is a multiple of n. Given a positive integer n, which is less than the length (m) of set M.
Here is the snippet I have written.
I am not too sure about the time complexity of this algorithm w.r.t the length of M and the value of n. In the exlude function, worst case is O(m). Then it is within a for loop over m, then O(m^2). In addition, X initialization scales with n, so O(n) here. In total: O(m^2) + O(n), ignoring the other O(1)s. Is this correct?
Also, should I take r = x % n as O(1)?
Any coding related advices on the codes here are welcome!!! Big thx!
//array X is intialized of size n, all -1. Here the code is omitted.
for (int i = 0; i < m; i++)
{
if (currentLength > 1)
{
index = rand() % currentLength;
x = setM[index];
exclude(setM, index, ¤tLength);
r = x % n;
if (X[r] == -1)
{
X[r] = x;
}
else
{
printf("The pair: (%i, %i)\n", X[r], x);
break;
}
}
else
{
break;
}
currentLength -= 1;
}
// to exclude an element based on index, then shift all elements behind by 1 slot
void exclude(int* array, int index, int* length_ptr)
{
if (index != *length_ptr - 1)
{
for (int i = index; i < *length_ptr - 1; i++)
{
array[i] = array[i + 1];
}
}
}
Also, should I take r = x % n as O(1)?
Yes, it's O(1)
I am not too sure about the time complexity of this algorithm ... In total: O(m^2) + O(n)?
Well, kind of but there is more to it than that. The thing is that m and n is not independent.
Consider the case n = 2 and let m be increasing. Your formula would give O(m^2) but is that correct? No. Since there will only be 2 possible results from % n (i.e. 0 and 1) the loop for (int i = 0; i < m; i++) can only run 3 times before we have a match. No matter how much you increase m there can never be more than 3 loops. In each of these loops the exclude function may move near m elements in worst case. In general the for (int i = 0; i < m; i++) can never do more than n+1 loops.
So for m being larger than n you rather have O(n*m) + O(n). When keeping n constant this turns into just O(m). So your algorithm is just O(m) with respect to m.
Now consider the case with a constant m and a large increasing n. In this case your formula gives O(m^2) + O(n). Since m is constant O(m^2) is also constant so your algorithm is just O(n) with respect to n.
Now if you increase both m and n your formula gives O(m^2) + O(n). But since m and n are both increased, O(m^2) will eventually dominate O(N), so we can ignore O(N). In other words, your algorithm is O(M^2) with respect to both.
To recap:
O(m) for constant n and increasing m
O(n) for constant m and increasing n
O(m^2) for increasing n and increasing m
Any coding related advices on the codes here are welcome
Well, this index = rand() % currentLength; is just a bad idea!
You should always test the last element in the array, i.e. index = currentLength - 1;
Why? Simply because that will turn exclude into O(1). In fact you won't even need it! The exclude will happen automatically when doing currentLength -= 1;
This change will improve complexicity like
O(1) for constant n and increasing m
O(n) for constant m and increasing n
O(m)+O(n) for increasing n and increasing m
The O(m)+O(n) can be said to be just O(m) (or just O(n)) as you prefer. The main thing is that it is linear.
Besides that you don't need currentLength. Change the main loop to be
for (int i = m-1; i >= 0; --i)
and use i as index. This simplifies your code to:
for (int i = m-1; i >= 0; --i)
{
r = setM[i] % n;
if (X[r] == -1)
{
X[r] = setM[i];
}
else
{
printf("The pair: (%i, %i)\n", X[r], setM[i]);
break;
}
}
You need to find two numbers x and y that x%n==y%n.
It easy. Use a hash table with a key of x %n. Add consequtive numbers from the set until you find a duplicate. It would be the desired pair. Complexity is O(M).
int f1(int N) {
int Sum, i, j, k;
Sum = 0;
for (i = 0; i < N; i++)
for (j = 0; j < i * i; j++)
for (k = 0; k < j; k++)
Sum++;
return Sum;
}
int f2(int N) {
int Sum, i, j;
Sum = 0;
for (i = 0; i < 10; i++)
for (j = 0; j < i; j++)
Sum += j * N;
return Sum;
}
What are the complexities of f1 and f2?
I have no idea about the complexity of f1 and I think the complexity of f2 should be O(1) since the number of iterations is constant. It is correct?
Your first function has the complexity O(N^(1+2+2)) = O(N^5).
In the first loop i goes from 0 on N, the second one j loops over a limit that depends on N^2 and in the 3rd one k loops on an interval whose size depends on N^2 as well.
The function F2 is constant time, so O(1) because the loops do not have any degree of liberty.
This kind of stuff is studied in the courses of algorithms at the topic "complexity".
There is also another kind of measurement of complexity of algorithms, based on omega-notation.
The complexity of f1 is in O(n^5) since
for(i=0; i<N; i++) //i has a upper bound of n
for(j=0; j<i*i; j++) //j has a upper bound of i^2, which itself has the upper bound of n, so this is n^2
for(k=0; k<j; k++) //k has a upper bound of j, which is n^2
Sum++; //constant
So the complete upper bound is n * n^2 * n^2 which is n^5 so f1 is in O(n^5).
for (i = 0; i < 10; i++) //upper bound of 10 so in O(10) which is in O(1)
for (j = 0; j < i; j++) //upper bound of i so also O(1)
Sum += j * N; //N is just a integer here, and multiplication is a constant operation independent of the size of N, so also O(1)
So f2 is in O(1*1*1) which is simply O(1).
Note all assignments and declarations are also constant.
BTW since Sum++ has no side effects and with the according loops develops a series we know a solution for (math yay), a programmer or optimal compiler optimiser could reduce f1 to a constant program using the gaussian sum formula (n*n+n) / 2, so sum could be just calculated by something like (N*N + N ) / 2 * (N*N*N*N + N*N) / 2) * 2 , however my formula does not consider starting at 0.
Using sigma notation:
f1:
The outer loop runs from 0 to N, the one inside it runs from 0 to i^2 and the last one runs from 0 to j, and inside we only have one operation so we are summing 1. Thus we get:
1+1+1... j times gives 1*j=j, thus we get:
Using the rule of the summation of natural numbers but we replace n (in the Wikipedia article) with i^2 so we get:
The reason for the approximation is because when finding the time complexity of a function and we have the addition of multiple powers we take the highest one. This just makes the math simpler. For example f(n)=(n^3+n^2+n)=O(n^3) (supposing that f(n) represents the maximal running time required by the given algorithm depending on the input size n) .
And using the formula for the summation of the first N numbers to 4th power we get (look at the note in the end):
Thus the time complexity for f1 is O(n^5).
f2:
Using the same method we get:
But this just gives a constant which doesn't depend on n thus the time complexity for f2 is O(1).
note:
When we have a summation of the first N numbers that are to the K power, the time complexity of it would be N^(K+1), so you obviously don't need to remember the formula. For example:
Can you guys please tell me what's the time complexity of f3?
I'm thinking sqrt(n).log(n) but the official answer is n.
Any ideas why?
#define PARTS 4
void f3(int n) {
if (n < 4)
return;
for (int i = 0; i * i < n; i++)
printf("%d", i);
for (int i = 0; i < PARTS; i++)
f3(n / PARTS);
}
The complexity depends on that of printf: if you can assume that printf("%d",i) has a constant cost, then the time complexity seems to be O(N + k.sqrt(N)), with k=-1. Since sqrt(N) is dominated by N, this simplifies into O(N).
cost(4*N) = 4*cost(N) + sqrt(4*N)
4*N + k*sqrt(4*N) = 4*N + 4*k*sqrt(N) + 2*sqrt(N)
2*k = 4*k+2
k = -1
If printf("%d",i) has a complexity of log(i), taking into account the number of digits produced by the conversion of i to base 10, the overall complexity is more difficult to assess: k.sqrt(N) becomes k.log(N).sqrt(N), which is still dominated by N.
what is best way to find just sum of all elements of array whose index divisible by i with least complexity.
I have written below code. But thats brute force. Can i get better than that
#include<stdio.h>
int main() {
int n, q;
int mod = 1000000000 + 7;
scanf("%d", &n);
int arr[n+1];
int i;
for (i = 1; i <= n ; ++i) {
scanf("%d", &arr[i]);
}
int p;
scanf("%d", &p);
int sum = 0;
int j;
for(j = p; j <= n; j = j+p) {
sum = (sum + arr[j]) % mod;
}
printf("%d\n",sum);
return 0;
}
You remark that your example implementation is "brute force" and ask whether you can "do better". Brute force usually implies an approach that is simple to implement but performs substantially more work or uses substantially more memory than is theoretically necessary. It suggests devoting overwhelming resources in place of efficient operation. Often, "substantially more" boils down to such approaches having higher asymptotic complexity than the best possible approaches.
Your example implementation is not like that. Adding n / p arbitrary numbers requires n / p operations, so O(n) is the least asymptotic complexity an algorithm for the task can have. That is the asymptotic complexity of your implementation, so it cannot be improved in that sense.
Furthermore, your implementation appears to perform about as few overall operations as you could hope for. Consider this naive, alternative, worse implementation of the summation loop:
for(j = 1; j <= n; j++) {
if (j % p == 0) {
sum = (sum + arr[j]) % mod;
}
}
That could be viewed as a somewhat more direct translation of the requirement into C code. Although it's still only O(n), it might reasonably be characterized as a brute force implementation because of the (p-1)-fold excess of increments to j and the n computations of j % p, both of which your implementation avoids.
Bottom line: no, there is no substantially more efficient implementation than the one you present.
Given a snipplet of code, how will you determine the complexities in general. I find myself getting very confused with Big O questions. For example, a very simple question:
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
System.out.println("*");
}
}
The TA explained this with something like combinations. Like this is n choose 2 = (n(n-1))/2 = n^2 + 0.5, then remove the constant so it becomes n^2. I can put int test values and try but how does this combination thing come in?
What if theres an if statement? How is the complexity determined?
for (int i = 0; i < n; i++) {
if (i % 2 ==0) {
for (int j = i; j < n; j++) { ... }
} else {
for (int j = 0; j < i; j++) { ... }
}
}
Then what about recursion ...
int fib(int a, int b, int n) {
if (n == 3) {
return a + b;
} else {
return fib(b, a+b, n-1);
}
}
In general, there is no way to determine the complexity of a given function
Warning! Wall of text incoming!
1. There are very simple algorithms that no one knows whether they even halt or not.
There is no algorithm that can decide whether a given program halts or not, if given a certain input. Calculating the computational complexity is an even harder problem since not only do we need to prove that the algorithm halts but we need to prove how fast it does so.
//The Collatz conjecture states that the sequence generated by the following
// algorithm always reaches 1, for any initial positive integer. It has been
// an open problem for 70+ years now.
function col(n){
if (n == 1){
return 0;
}else if (n % 2 == 0){ //even
return 1 + col(n/2);
}else{ //odd
return 1 + col(3*n + 1);
}
}
2. Some algorithms have weird and off-beat complexities
A general "complexity determining scheme" would easily get too complicated because of these guys
//The Ackermann function. One of the first examples of a non-primitive-recursive algorithm.
function ack(m, n){
if(m == 0){
return n + 1;
}else if( n == 0 ){
return ack(m-1, 1);
}else{
return ack(m-1, ack(m, n-1));
}
}
function f(n){ return ack(n, n); }
//f(1) = 3
//f(2) = 7
//f(3) = 61
//f(4) takes longer then your wildest dreams to terminate.
3. Some functions are very simple but will confuse lots of kinds of static analysis attempts
//Mc'Carthy's 91 function. Try guessing what it does without
// running it or reading the Wikipedia page ;)
function f91(n){
if(n > 100){
return n - 10;
}else{
return f91(f91(n + 11));
}
}
That said, we still need a way to find the complexity of stuff, right? For loops are a simple and common pattern. Take your initial example:
for(i=0; i<N; i++){
for(j=0; j<i; j++){
print something
}
}
Since each print something is O(1), the time complexity of the algorithm will be determined by how many times we run that line. Well, as your TA mentioned, we do this by looking at the combinations in this case. The inner loop will run (N + (N-1) + ... + 1) times, for a total of (N+1)*N/2.
Since we disregard constants we get O(N2).
Now for the more tricky cases we can get more mathematical. Try to create a function whose value represents how long the algorithm takes to run, given the size N of the input. Often we can construct a recursive version of this function directly from the algorithm itself and so calculating the complexity becomes the problem of putting bounds on that function. We call this function a recurrence
For example:
function fib_like(n){
if(n <= 1){
return 17;
}else{
return 42 + fib_like(n-1) + fib_like(n-2);
}
}
it is easy to see that the running time, in terms of N, will be given by
T(N) = 1 if (N <= 1)
T(N) = T(N-1) + T(N-2) otherwise
Well, T(N) is just the good-old Fibonacci function. We can use induction to put some bounds on that.
For, example, Lets prove, by induction, that T(N) <= 2^n for all N (ie, T(N) is O(2^n))
base case: n = 0 or n = 1
T(0) = 1 <= 1 = 2^0
T(1) = 1 <= 2 = 2^1
inductive case (n > 1):
T(N) = T(n-1) + T(n-2)
aplying the inductive hypothesis in T(n-1) and T(n-2)...
T(N) <= 2^(n-1) + 2^(n-2)
so..
T(N) <= 2^(n-1) + 2^(n-1)
<= 2^n
(we can try doing something similar to prove the lower bound too)
In most cases, having a good guess on the final runtime of the function will allow you to easily solve recurrence problems with an induction proof. Of course, this requires you to be able to guess first - only lots of practice can help you here.
And as f final note, I would like to point out about the Master theorem, the only rule for more difficult recurrence problems I can think of now that is commonly used. Use it when you have to deal with a tricky divide and conquer algorithm.
Also, in your "if case" example, I would solve that by cheating and splitting it into two separate loops that don; t have an if inside.
for (int i = 0; i < n; i++) {
if (i % 2 ==0) {
for (int j = i; j < n; j++) { ... }
} else {
for (int j = 0; j < i; j++) { ... }
}
}
Has the same runtime as
for (int i = 0; i < n; i += 2) {
for (int j = i; j < n; j++) { ... }
}
for (int i = 1; i < n; i+=2) {
for (int j = 0; j < i; j++) { ... }
}
And each of the two parts can be easily seen to be O(N^2) for a total that is also O(N^2).
Note that I used a good trick trick to get rid of the "if" here. There is no general rule for doing so, as shown by the Collatz algorithm example
In general, deciding algorithm complexity is theoretically impossible.
However, one cool and code-centric method for doing it is to actually just think in terms of programs directly. Take your example:
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
System.out.println("*");
}
}
Now we want to analyze its complexity, so let's add a simple counter that counts the number of executions of the inner line:
int counter = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
System.out.println("*");
counter++;
}
}
Because the System.out.println line doesn't really matter, let's remove it:
int counter = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
counter++;
}
}
Now that we have only the counter left, we can obviously simplify the inner loop out:
int counter = 0;
for (int i = 0; i < n; i++) {
counter += n;
}
... because we know that the increment is run exactly n times. And now we see that counter is incremented by n exactly n times, so we simplify this to:
int counter = 0;
counter += n * n;
And we emerged with the (correct) O(n2) complexity :) It's there in the code :)
Let's look how this works for a recursive Fibonacci calculator:
int fib(int n) {
if (n < 2) return 1;
return fib(n - 1) + fib(n - 2);
}
Change the routine so that it returns the number of iterations spent inside it instead of the actual Fibonacci numbers:
int fib_count(int n) {
if (n < 2) return 1;
return fib_count(n - 1) + fib_count(n - 2);
}
It's still Fibonacci! :) So we know now that the recursive Fibonacci calculator is of complexity O(F(n)) where F is the Fibonacci number itself.
Ok, let's look at something more interesting, say simple (and inefficient) mergesort:
void mergesort(Array a, int from, int to) {
if (from >= to - 1) return;
int m = (from + to) / 2;
/* Recursively sort halves */
mergesort(a, from, m);
mergesort(m, m, to);
/* Then merge */
Array b = new Array(to - from);
int i = from;
int j = m;
int ptr = 0;
while (i < m || j < to) {
if (i == m || a[j] < a[i]) {
b[ptr] = a[j++];
} else {
b[ptr] = a[i++];
}
ptr++;
}
for (i = from; i < to; i++)
a[i] = b[i - from];
}
Because we are not interested in the actual result but the complexity, we change the routine so that it actually returns the number of units of work carried out:
int mergesort(Array a, int from, int to) {
if (from >= to - 1) return 1;
int m = (from + to) / 2;
/* Recursively sort halves */
int count = 0;
count += mergesort(a, from, m);
count += mergesort(m, m, to);
/* Then merge */
Array b = new Array(to - from);
int i = from;
int j = m;
int ptr = 0;
while (i < m || j < to) {
if (i == m || a[j] < a[i]) {
b[ptr] = a[j++];
} else {
b[ptr] = a[i++];
}
ptr++;
count++;
}
for (i = from; i < to; i++) {
count++;
a[i] = b[i - from];
}
return count;
}
Then we remove those lines that do not actually impact the counts and simplify:
int mergesort(Array a, int from, int to) {
if (from >= to - 1) return 1;
int m = (from + to) / 2;
/* Recursively sort halves */
int count = 0;
count += mergesort(a, from, m);
count += mergesort(m, m, to);
/* Then merge */
count += to - from;
/* Copy the array */
count += to - from;
return count;
}
Still simplifying a bit:
int mergesort(Array a, int from, int to) {
if (from >= to - 1) return 1;
int m = (from + to) / 2;
int count = 0;
count += mergesort(a, from, m);
count += mergesort(m, m, to);
count += (to - from) * 2;
return count;
}
We can now actually dispense with the array:
int mergesort(int from, int to) {
if (from >= to - 1) return 1;
int m = (from + to) / 2;
int count = 0;
count += mergesort(from, m);
count += mergesort(m, to);
count += (to - from) * 2;
return count;
}
We can now see that actually the absolute values of from and to do not matter any more, but only their distance, so we modify this to:
int mergesort(int d) {
if (d <= 1) return 1;
int count = 0;
count += mergesort(d / 2);
count += mergesort(d / 2);
count += d * 2;
return count;
}
And then we get to:
int mergesort(int d) {
if (d <= 1) return 1;
return 2 * mergesort(d / 2) + d * 2;
}
Here obviously d on the first call is the size of the array to be sorted, so you have the recurrence for the complexity M(x) (this is in plain sight on the second line :)
M(x) = 2(M(x/2) + x)
and this you need to solve in order to get to a closed form solution. This you do easiest by guessing the solution M(x) = x log x, and verify for the right side:
2 (x/2 log x/2 + x)
= x log x/2 + 2x
= x (log x - log 2 + 2)
= x (log x - C)
and verify it is asymptotically equivalent to the left side:
x log x - Cx
------------ = 1 - [Cx / (x log x)] = 1 - [C / log x] --> 1 - 0 = 1.
x log x
Even though this is an over generalization, I like to think of Big-O in terms of lists, where the length of the list is N items.
Thus, if you have a for-loop that iterates over everything in the list, it is O(N). In your code, you have one line that (in isolation all by itself) is 0(N).
for (int i = 0; i < n; i++) {
If you have a for loop nested inside another for loop, and you perform an operation on each item in the list that requires you to look at every item in the list, then you are doing an operation N times for each of N items, thus O(N^2). In your example above you do in fact, have another for loop nested inside your for loop. So you can think about it as if each for loop is 0(N), and then because they are nested, multiply them together for a total value of 0(N^2).
Conversely, if you are just doing a quick operation on a single item then that would be O(1). There is no 'list of length n' to go over, just a single one time operation.To put this in context, in your example above, the operation:
if (i % 2 ==0)
is 0(1). What is important isn't the 'if', but the fact that checking to see if a single item is equal to another item is a quick operation on a single item. Like before, the if statement is nested inside your external for loop. However, because it is 0(1), then you are multiplying everything by '1', and so there is no 'noticeable' affect in your final calculation for the run time of the entire function.
For logs, and dealing with more complex situations (like this business of counting up to j or i, and not just n again), I would point you towards a more elegant explanation here.
I like to use two things for Big-O notation: standard Big-O, which is worst case scenario, and average Big-O, which is what normally ends up happening. It also helps me to remember that Big-O notation is trying to approximate run-time as a function of N, the number of inputs.
The TA explained this with something like combinations. Like this is n choose 2 = (n(n-1))/2 = n^2 + 0.5, then remove the constant so it becomes n^2. I can put int test values and try but how does this combination thing come in?
As I said, normal big-O is worst case scenario. You can try to count the number of times that each line gets executed, but it is simpler to just look at the first example and say that there are two loops over the length of n, one embedded in the other, so it is n * n. If they were one after another, it'd be n + n, equaling 2n. Since its an approximation, you just say n or linear.
What if theres an if statement? How is the complexity determined?
This is where for me having average case and best case helps a lot for organizing my thoughts. In worst case, you ignore the if and say n^2. In average case, for your example, you have a loop over n, with another loop over part of n that happens half of the time. This gives you n * n/x/2 (the x is whatever fraction of n gets looped over in your embedded loops. This gives you n^2/(2x), so you'd get n^2 just the same. This is because its an approximation.
I know this isn't a complete answer to your question, but hopefully it sheds some kind of light on approximating complexities in code.
As has been said in the answers above mine, it is clearly not possible to determine this for all snippets of code; I just wanted to add the idea of using average case Big-O to the discussion.
For the first snippet, it's just n^2 because you perform n operations n times. If j was initialized to i, or went up to i, the explanation you posted would be more appropriate but as it stands it is not.
For the second snippet, you can easily see that half of the time the first one will be executed, and the second will be executed the other half of the time. Depending on what's in there (hopefully it's dependent on n), you can rewrite the equation as a recursive one.
The recursive equations (including the third snippet) can be written as such: the third one would appear as
T(n) = T(n-1) + 1
Which we can easily see is O(n).
Big-O is just an approximation, it doesn't say how long an algorithm takes to execute, it just says something about how much longer it takes when the size of its input grows.
So if the input is size N and the algorithm evaluates an expression of constant complexity: O(1) N times, the complexity of the algorithm is linear: O(N). If the expression has linear complexity, the algorithm has quadratic complexity: O(N*N).
Some expressions have exponential complexity: O(N^N) or logarithmic complexity: O(log N). For an algorithm with loops and recursion, multiply the complexities of each level of loop and/or recursion. In terms of complexity, looping and recursion are equivalent. An algorithm that has different complexities at different stages in the algorithm, choose the highest complexity and ignore the rest. And finally, all constant complexities are considered equivalent: O(5) is the same as O(1), O(5*N) is the same as O(N).