Train station distance from center equals N miles - arrays

I have a very very huge array of positive integer numbers which represent how far a certain train station is away from the center, for example:
S = {10, 200, 1000, 1500, 2019, 2200}
Train station S[0] is 10 miles away from the center. S is always sorted in ascending order, at any point in time, also before the algorithm starts. Just simply always.
I want to find a function which checks if there exist two train station with a distance of exactly N miles.
For example:
N = 1300 would give me true because 1500 - 200 = 1300.
First approach
Iterate over S and check for each element if the distance to another element is N. This gives me two loops and I guess O(n^2). I don't want O(n^2) because the array can be so huge it needs better performance.
Other approaches
I did a lot of research but all I found was that O(n) is possible. I want to have this time complexity. My solution looks like this, but unfortunately it does not work out at all.
int a[] = {10, 200, 1000, 1500, 2019, 2200};
int size = 6;
int left = 0;
int right = size - 1;
int x, y, distance, tempdis;
int N = 1300;
while(left < right)
{
x = a[left];
y = a[right];
tempdis = x - y;
distance = tempdis < 0 ? tempdis*(-1) : tempdis;
if(distance == N)
{
printf("found pair: %d %d\n", left, right);
break;
}
if(distance > N)
left++;
if(distance < N)
right--;
}

You can achieve linear time (O(n)) by only incrementing two pointers, i and j. You want to find i and j such that a[j] - a[i] == N. The logic is simple:
if a[j] - a[i] < N: increment j (distance gets larger)
if a[j] - a[i] > N: increment i (distance gets smaller)
That's all! In code:
int i = 0;
int j = 0;
while ((a[j] - a[i] != N) && j < size) { // size is length of a
if (a[j] - a[i] < N) {
j++;
} else {
i++;
}
}
if (j < size) {
printf("found pair: %d %d\n", i, j);
}
Handwaving proof of correctness: in principle, we should check each a[j] against all a[i] that could potentially give a solution. That is, for each j, we check a range p_j <= i <= q_j, such that a[j] - a[p_j] > N and a[j] - a[q_j] < N. If there is a solution involving j, it must be found in that range of i values.
Now, this algorithm almost does that, with one exception: sometimes we increment j multiple times in a row, so we clearly did not check it against a whole range. We increment j again, because a[j] - a[i] < N. However, if that happens, we also know that a[j] - a[i-1] > N. I leave it up to you to verify this.
This means that we check j against the range of all i values that can potentially give a solution. And thus the result is correct.
We have two pointers. In each step, one pointer is incremented. The size of the larger of the 2 (j) is bounded by n, so this runs in O(n) time.

sum should be changed to distance
And it should be:
if(distance < N) {
right--;
left--;
}
Not just
if(distance < N)
right--;

There is an error in this block of code:
if (distance < N)
right--;
You also want to decrement the value of left here:
if (distance < N)
{
right--;
left--;
}
Also, the variable sum in line:12 should be distance.

Related

What is the number of maximum comparisons intersection algorithm can perform?

So I have this algorithm below which finds common elements in two SORTED arrays. Given that both arrays have m and n lengths respectively, I need to find the maximum amount of comparisons this alrorithm is going to do.
int printIntersection(int arr1[], int arr2[], int m, int n)
{
int i = 0, j = 0;
while (i < m && j < n)
{
if (arr1[i] < arr2[j])
i++;
else if (arr2[j] < arr1[i])
j++;
else /* if arr1[i] == arr2[j] */
{
cout << arr2[j] << " ";
i++;
j++;
}
}
}
I think that the complexity of this algorithm is O(m+n). Correct me if I'm wrong. So, would the maximum amount of comparisons be m+n or m*n? Or none of them?
The worst case would be if n > m, the first (m - 1) elements are equal and the last element arr1[m - 1] is greater than all remaining elements of arr2. Then the first if will always fail, the code will have to pass through all elements of arr2, resulting in (2 * n) comparisons.
But Big O notation does not indicate an exact number of operations, but rather the rate of its growth. In these terms this algorithm it still linear with regard to length of the whole input, and that is written as O(n).

Need more Efficient way to read subarrays

The problem statement asks the number of such subarrays where i < j < k, such that sum of any two numbers should be greater than or equal to the third in the subarray:
What I did:
I ran a loop from i=0 till n-2:
and the basic logic I used was if the first two elements in the sorted subarray are greater than or equal to the maximum, then all pairs will be greater than any element. and every time I get the subarray, I add the next element into it and set those three variables again. Am passing 15/20 TCs other am getting TLE:
Constraints:
1<=n<=10^5
1<=ai<=10^9
for(int i=0;i<n-2;i++)
{
int r=i+2;
vector<int> temp(inp.begin()+i,inp.begin()+r+1);
sort(temp.begin(),temp.end());
max_elem=temp[1];min_elem=temp[0];
int maximum=temp[temp.size()-1];
//cout<<max_elem<<" "<<min_elem<<"\n";
while(r<n && max_elem+min_elem >= maximum)
{
//cout<<max_elem<<" "<<min_elem<<" "<<inp[r]<<"\n";
cnt++;
r++;
if(inp[r]<min_elem) {max_elem=min_elem;min_elem=inp[r];}
else if(inp[r]<max_elem) max_elem=inp[r];
else if(inp[r]>maximum) maximum=inp[r];
}
}
cout<<cnt<<"\n";
Sample TC:
I1:
5
7 6 5 3 4
O1:
6
Explanation:
6 subarrays fulfill the conditions: (7,6,5),(7,6,5,3),(7,6,5,3,4),(6,5,3),(6,5,3,4),(5,3,4).
I2:
5
1 2 3 5 6
O2:
3
Explanation:
(1,2,3),(2,3,5),(3,5,6) --(NOTE: 1,2,3,5 isn't the ans coz 1+2 < 5 )
A naive approach to do this is this is as the following. Your logic is correct and it is what I implemented. I changed the sort (NlogN) with a single pass (N) finding only the 2 smallest and largest numbers. I haven't compiled the code and not sure it works as intended. It has the overall complexity of (N*N*N).
Execution time can be improved by doing some extra checks:
min1 + min2 >= maxcondition can be checked after each inner (k) loop, breaking if it violates for single case.
If condition is not satisfied for say subarray 4-7, there is no need to check any other substring including 4-7. By storing violating cases and checking against them before each loop, overall execution time can be improved.
int min1;
int min2;
int max;
int count = 0;
for(int i = 2; i < n; i++){
for(int j = 0; j < i - 2; j++){
max = -1;
min1 = min2 = 1000000000;
for(int k = j; k <= i; k++){
if(inp[k] > max)
max = inp[k];
if(inp[k] < min1){
min1 = inp[k];
continue;
}
if(inp[k] < min2){
min2 = inp[k];
}
}
if(min1 + min2 >= max)
count++;
}
}
There might be some bugs, but here is the general idea for a O(n log n) solution:
We keep a windows of elements from startIdx to endIdx. If its a valid subarray, it means we can expand it, we can add another element to it, so we increase endIdx. If its not valid, it wouldnt be valid no matter how much we expand it, so we need to reduce it by increasing startIdx.
pseudocode:
multiset<int> nums;
int startIdx = 0, endIdx = 0;
int sol = 0;
while(endIdx != inp.size()) {
if (endIdx - startIdx < 3) {
nums.add(inp[endIdx]);
endIdx++;
} else {
if (nums.lowestElement() + nums.secondLowestElement() < nums.highestElement()) {
nums.remove(nums.find(inp[startIdx]));
startIdx++;
} else {
sol += endIdx - startIdx - 2; // amount of valid subarrays ending in inp[endIdx - 1]
nums.add(inp[endIdx]);
endIdx++;
}
}
}

How to find left and right subarray for each index which follows below mentioned criteria?

There is a sequence problem wherein for each index
i in the array we define two quantities.
Let r be the maximum index such that
r>=i and sub-array from i to r (inclusive) is either non-decreasing or non-increasing.
Let l be the minimum index such that l<=i and sub-array from l to i (inclusive) is either non-decreasing or non-increasing.
Now, we define points of an index i to be equal to
max(|Ai−Al|,|Ai−Ar|).
Note that l and r can be different for each index.
The task of the problem is to find the index of the array A which have the maximum points.
My Logic :
First scan all the elements in the array .
For every index find l and r which either follows an increasing or decreasing sequence and then calculate the maximum point for that index.
My problem is that this is taking O(N^2) time.
Can the problem be done in less time?
Two consecutive identical number have the same point and does not affect the point of any other point, so it is possible to assume that this scenario does not exist.
So consider a input array a which has no consecutive identical numbers, it can be assumed that the longest none-decreasing or none-increasing in sub sequence are [0, I1] [I1, I2] ... [Ix, n - 1], which is denoted by index and n is the length of the array. Each decreasing sub sequence is followed by a increasing sub sequence and vice versa.
For any Ii, the point with index Ii have point equal to max(|AIi - AI(i - 1)|, |AIi - AI(i + 1)|). Any index between Ii and I(i + 1) have point less than Ii and I(i + 1) and do not have to be considered.
So we only need to find out the maximum value between all AIi andAI(i + 1) .
After a huge lot of attempts, I finally got my program accepted (mainly because the difference between two int 32 are not necessarily in the range of a signed int 32 range), and the code is as follows.
#include <stdio.h>
#define MAXN 200002
long long a[MAXN];
long long abs(long long n)
{
if (n >= 0)
return n;
return -n;
}
long long find_score(int size)
{
int i = 0;
long long maximum_score = 0;
while (i < size - 1)
{
//Jump over consecutive indentical numbers
while (a[i + 1] == a[i])
{
if (i < size - 1)
i++;
else
break;
}
int j = i + 1;
int inc_or_dec = a[j] > a[i];
while (j < size - 1 && (!((a[j + 1] > a[j]) ^ inc_or_dec) || a[j + 1] == a[j]))j++;
if (abs(a[j] - a[i]) > maximum_score)
maximum_score = abs(a[j] - a[i]);
i = j;
}
return maximum_score;
}
int main()
{
int n;
scanf("%d", &n);
while (n--)
{
int num;
scanf("%d", &num);
for (int i = 0; i < num; i++)
{
scanf("%lld", a + i);
}
printf("%lld\n", find_score(num));
}
while (1);
return 0;
}
Glad to know if there are any "implementation defined" problems in my code.

Algorithm to find a consecutive sub-sequence whose sum would be a asked number M from a sequence of numbers in O(n)

Lets say we have an array of positive numbers and we were given a value M. Our goal is to find if there is a consecutive sub sequence in the array of positive numbers such that the sum of the sequence is exactly equal to sum M. If A[1],A[2],....A[n] is an array then we have to find if there exist i and j such that A[i]+...+A[j] = M.
I am trying to get the O(n) solution using greedy approach.
I believe you can solve this in linear time with a pointer chasing algorithm.
Here's the intuition. Start off a pointer at the left side of the array. Keep moving it to the right, tracking the sum of the elements you've seen so far, until you either hit exactly M (done!), your total exceeds M (stop for now, adding in more elements only makes it worse), or you hit the end of the array without reaching at least M (all the elements combined are too small). If you do end up in a case where the sum exceeds M, you can be guaranteed that no subarray starting at the beginning of the array adds up to exactly M, since you tried all of them and they were either too small or too big.
Now, start a second pointer at the first element and keep advancing it forward, subtracting out the current element, until you either get to exactly M (done!), you reach the first pointer (stop for now), or the total drops below M (stop for now). All the elements you skipped over with this pointer can't be the starting point of the subarray you're looking for. At this point, start marching the first pointer forward again.
Overall, each pointer advances at most n times and you do O(1) work per step, so this runs in time O(n). Plus, it uses only O(1) space, which is as good as it's going to get!
This is a standard two pointer problem. First of all, create an array, prefix that will store the prefix sum of the given array, say arr.
So
prefix[i] = arr[1] + .. + arr[i]
Start with two pointers, lower and upper. Initialize them as
lower = 0
upper = 1
(Note: Initialize prefix[0] to 0)
Now, try to understand this code:
lower = 0, upper = 1;
while(upper <= n) { // n is the number of elements
if(prefix[upper] - prefix[lower] == m) {
return true;
} else if(prefix[upper] - prefix[lower] > m) {
lower++;
} else {
upper++;
}
}
return false;
Here we are using the fact that the array consists of positive integers,
hence prefix is increasing
Assume that the subarray with indices X ≤ i < Y might be the solution.
You start with X = 1, Y= 1, sum of elements = 0.
As long as the sum is less than M, and Y <= n, increase the sum by array [Y] and replace Y with Y + 1.
If the sum is equal to M, you found a solution.
If the sum is less than M, you remove array elements at the start: As long as the sum is greater than M, subtract array [X] from the sum and replace X with X + 1. If the sum became equal to M, you have a solution. Otherwise you start with the first loop.
(edited: see templatetypedef's comment)
Use the two indices approach: increase the lower index if subsequence too small otherwise increase higher index.
Example:
void solve(int *a, int n, int M) {
if (n <= 0) return;
int i, j, s;
i = 0, j = 0, s = a[j];
while (j < n) {
if (s == M) {
printf("%dth through %dth elements\n", i + 1, j + 1);
return;
} else if (s < M) {
j++;
s += a[j];
} else {
s -= a[i];
i++;
}
}
}
public class FindSumEquals {
public static void main(String[] args) {
int n = 15;
System.out.println("Count is "+ findPossible(n));
}
private static int findPossible(int n) {
int temp = n;
int arrayLength = n / 2 + 2;
System.out.println("arrayLength : " + arrayLength) ;
int a [] = new int[arrayLength];
int count = 0;
for(int i = 1; i < arrayLength; i++){
a[i] = i + a[i - 1];
}
int lower = 0, upper = 1;
while(upper <= arrayLength - 1) {
if(a[upper] - a[lower] == temp) {
System.out.println("hello - > " + ++lower + " to "+ upper);
upper++;
count++;
} else if(a[upper] - a[lower] > temp) {
lower++;
} else {
upper++;
}
}
return count;
}
}

Determining the complexities given codes

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).

Resources