Lec 08 - Multi-d Array, Efficiency

Slides:

Lecture Slides

Multi-dimensional Array

Pointer to a Fixed-Size Array

Why we need this part?

In C, the normal method for us to declare an array is as follows:

long a[20];

However, as we have seen earlier, due to "array-decay", a will decay to a memory address (not a pointer). So, we cannot assign a to other memory address. For example, the following code in C is illegal

long a[20];
long b[20];
a = b; // Illegal

So, to "solve" this problem (actually for teaching only), C allows us to have a pointer that points to an array. Note: Not just an element, but the whole array. We can do so with:

long a[20];
long (*ptr)[20];
ptr = &a;

However, you still need to pay attention that the following code to define a fixed-size array are not exactly the same!

long (*a)[20];

long *a[20];

The first one defines a pointer to a fixed-size array of length 20 (Actually it allocates a space of 20 long on the stack first, then it defines a pointer pointing to that memory location). While the second defines an array of 20 pointers, and in this case, these 20 pointers point to a long. (But in other cases, it can point to another array too)

Pointer to a Fixed-array

Array Name Decay (Multidimensional Array)

In a multi-dimensional array, the Array Name Decay we have seen before still applies, but now it becomes a bit trickier.

Since an array in C consists only of a contiguous region of memory that stores the elements of the array, the address of an array is the same as the address of the first of the array. The following five statements would print out exactly the same values.

cs1010_println_pointer(matrix);        // address of a 1D array
cs1010_println_pointer(matrix[0]);     // address of a long 
cs1010_println_pointer(&matrix);       // address of a 2D array 
cs1010_println_pointer(&matrix[0]);    // address of a 1D array
cs1010_println_pointer(&matrix[0][0]); // address of a long

From this example, by observing each pair of the new lines that have the same meaning. We can see the essence of array decay is: If we have a 2-D array matrix, then matrix will decay to &matrix[0]. Similarly, matrix[0] will decay to &matrix[0][0].

Add an & operator can be considered as .

A Fixed-Size Array of Dynamically Allocated Array

Contiguous Memory Allocation

In the code below, 10 is the num_of_rows. What we have done here is to allocate a chunk of memory with size num_of_cols * 10 once. After that, we iteratively point the remaining 9 pointers to the correct position.

double *buckets[10];
size_t num_of_cols = cs1010_read_size_t();
buckets[0] = calloc(num_of_cols * 10, sizeof(double));
for (size_t i = 1; i < 10; i += 1) {
  buckets[i] = buckets[i - 1] + num_of_cols;
}
Contiguous Memory Allocation

To free this kind of 2-D array, just use

free(buckets[0])
Why we cannot use free(buckets) here?

Accoding to the Linux Programmer's Manual, void free(void *ptr) should follow:

The free() function frees the memory space pointed to by ptr, which must have been returned by a previous call to malloc(), calloc(), or realloc(). Otherwise, or if free(ptr) has already been called before, undefined behavior occurs. If ptr is NULL, no operation is performed.

In our case, due to array-decay, buckets is not a "heap-object" returned by malloc(), calloc(), or realloc(). Rather, it is an "stack-object", so we cannot pass buckets directly into free(). If so, we will get warnings from the compiler.

Dynamically Size 2D Array

This existence of this part in my note is only to serve as a example for Why we cannot use free(buckets) here?

Use the following code to allocate a dynamically size 2D array, we should free as shown in the code snippet.

double **canvas;
size_t num_of_rows = cs1010_read_size_t();
size_t num_of_cols = cs1010_read_size_t();
canvas = calloc(num_of_rows, sizeof(double *));
if (canvas == NULL) {
  cs1010_println_string("unable to allocate array");
  return 1;
}
canvas[0] = calloc(num_of_rows * num_of_cols, sizeof(double));
if (canvas[0] == NULL) {
  cs1010_println_string("unable to allocate array");
  free(canvas);
  return 1;
}

for (size_t i = 1; i < num_of_rows; i += 1) {
  canvas[i] = canvas[i-1] + num_of_cols;
}

// free
free(canvas[0]);
free(canvas);

Note that here we can free(canvas) because according to Line 4, it is a "heap-object".

Efficiency

I believe the examples in the notes for this part is pretty complete and well documented. Personally thinking, get yourself familiar with the math equation, especially review for the problem sets and examples are pretty enough.

Comparing Rate of Growth

Given two functions f(n)f(n) and g(n)g(n), how do we determine which one has a higher rate of growth? We say that f(n)f(n) grows faster than g(n)g(n) if we can find a n0n_0, such that f(n)>cg(n)f(n)>cg(n) for all n>n0n>n_0 and for some constant cc.

For instance, which one grows faster? f(n)=nnf(n)=n^n or g(n)=2ng(n)=2^n? Pick n=1n=1, we have f(1)<g(1)f(1)<g(1). Pick n=2n=2, we have f(2)=g(2)f(2)=g(2). Pick n=3n=3, we have f(3)>g(3)f(3)>g(3) now, and we can see that for any n>3n>3, nn>2nn^n>2^n, so we can conclude that f(n)f(n) grows faster than g(n)g(n).

Time Complexity for Recursive Functions

Here I only talk about how to expand the recurrence relation. For example, we have

T(n)=2T(n2)+1=4T(n4)+2+1=8T(n8)+4+2+1=2iT(n2i)+2i1++4+2+1\begin{aligned} T(n) &= 2T\left(\frac{n}{2}\right) + 1 \\ &= 4T\left(\frac{n}{4}\right) + 2 + 1 \\ &= 8T\left(\frac{n}{8}\right) + 4 + 2 + 1 \\ &= 2^i T\left(\frac{n}{2^i}\right) + 2^{i-1} + \dots + 4 + 2 + 1 \end{aligned}

To reach the base case, n2i=1n\cdot2^{-i}=1, so 2i=n2^i=n, and 2i1=n/22^{i-1}=n/2. We have,

T(n)=nT(1)+n2++4+2+1=O(n)+n2++4+2+1\begin{aligned} T(n) &= nT(1) + \frac{n}{2} + \dots + 4 + 2 + 1 \\ &= O(n) + \frac{n}{2} + \dots + 4 + 2 + 1 \end{aligned}

This term n2++2+1\frac{n}{2}+\cdots+2+1 is a geometric series with a coefficient of 1 and a common ratio of 2. Its sum can be represented as follows, which is less than nn

n2++2+1=2log2n2121=n21\begin{aligned} \frac{n}{2} + \cdots + 2 + 1 &=\frac{2^{log_2\frac{n}{2}}-1}{2-1} \\ &=\frac{n}{2}-1 \end{aligned}

When you expand the gemetric sequence, suppose the common ratio is qq and the first term is a0a_0. Then the sum can be expressed as a0(qn1)q1\frac{a_0\cdot(q^n-1)}{q-1}, where nn is the number of terms in this geometric sequence.

Knowing the last term in the geometric sequence, a quick way to get the number of terms in the geometric sequence is to do the logarithmic operation. e.g. Suppose the last term is ana_n, then n=logq(an)n=log_q(a_n).

Last updated