The code is much improved from the previous version. Well done! Here are a few more things that may help you further improve your code.
Don't cast result of malloc
The malloc
call returns a void *
and one of the special aspects of C is that such a type does not need to be cast to be converted into another pointer type. So for example, this line:
int *new_arr = (int *)malloc(sizeof(int)*arr_size);
could be shortened to this:
int *new_arr = malloc(arr_size * sizeof *new_arr);
Note also that we don't need to repeat int
here. This makes it easier to keep it correct if, for example, we wanted to change to long *
.
Check the return value of malloc
If the program runs out of memory, a call to malloc
can fail. The indication for this is that the call will return a NULL
pointer. You should check for this and avoid dereferencing a NULL
pointer (which typically causes a program crash).
Eliminate special handling
Instead of this:
//left most element special handling
new_arr[0]=1;
//swipe up
for(size_t i=1; i<arr_size; i++) {
mult_prefix *= nums[i-1];
new_arr[i] = mult_prefix;
}
//swipe down
for(long j=arr_size-2; j>=0; j--) {
mult_suffix *= nums[j+1];
new_arr[j] *= mult_suffix;
}
Here's how I'd write it:
static const int multiplicative_identity = 1;
// calculate product of preceding numbers for each i
for (size_t i = arr_size; i; --i) {
*result++ = prod;
prod *= *nums++;
}
prod = multiplicative_identity;
// calculate product of succeeding numbers for each i,
// starting from the end, and multiply by current index
for (size_t i = arr_size; i; --i) {
*(--result) *= prod;
prod *= *(--nums);
}
return result;
There are a couple of things worth noting here. First, is that there is no need for a special case when written this way. Second, the use of pointers simplifies the code and makes it more regular. Third, many processors have a special instruction for looping down and/or checking for zero which tends to make counting down ever so slightly faster than counting up. Fourth, there is no reason not to use the passed value nums
as a pointer since the pointer is a local copy (even though the contents are not). In this particular case, since we increment the pointer to the end, moving the other direction is trivially simple since the pointers are already where we need them for both result
and nums
.
Consider a generic version
What if we wanted to create a similar function, but one that does the sum instead of the product? It's not at all needed for this project, but worth thinking about because of both the mathematics and the code. You will see that I called the constant multiplicative_identity
. Simply put, an identity element of an operation over a set is the value that, when combined by the operation with any other element of the set yields the same value. So for example, \$n * 1 = n\$ for all real values of \$n\$ and \$n + 0 = n\$ for all real values of \$n\$. This suggests a generic routine:
int* exclusive_op(const int* nums, size_t arr_size, int (*op)(int, int), int identity)
{
int* result = malloc(arr_size * sizeof(int));
if (result == NULL || arr_size == 0) {
return NULL;
}
int prod = identity;
// calculate op of preceding numbers for each i
for (size_t i = arr_size; i; --i) {
*result++ = prod;
prod = op(prod, *nums++);
}
prod = identity;
// calculate op of succeeding numbers for each i,
// starting from the end, and multiply by current index
for (size_t i = arr_size; i; --i) {
--result;
*result = op(*result, prod);
prod = op(prod, *(--nums));
}
return result;
}
Now we can define functions with which to use this generic version:
int add(int a, int b) {
return a+b;
}
int mult(int a, int b) {
return a*b;
}
int multmod3(int a, int b) {
return (a*b)%3;
}
int summod3(int a, int b) {
return (a+b)%3;
}
struct {
int (*op)(int, int);
int identity;
} ops[] = {
{ mult, 1 },
{ add, 0 },
{ multmod3, 1 },
{ summod3, 0 },
};
Using that array of struct
s, we could produce the same effect as your find_product_arr
by using this wrapper function:
int *generic(const int *nums, size_t arr_size) {
return exclusive_op(nums, arr_size, ops[0].op, ops[0].identity);
}
As you can see with the last two functions, this works with any operation that is both associative and that has an identity value.
Create some test code
How do you know if your results are correct? One way to do that is to write some test code. As I commented on your earlier code, it wasn't very efficient but was obviously correct. That is a nice foundation on which to create test code to make sure that your new version still produces correct results. Here's one way to do that. First, we need a way to compare the returned result against a known correct version:
bool compare(size_t size, const int* result, const int* expected)
{
for (size_t i = 0; i < size; ++i) {
if (result[i] != expected[i]) {
return false;
}
}
return true;
}
Now we can get fancy with colors and a test array:
#define RED "\033[31m"
#define GREEN "\033[32m"
#define WHITE "\033[39m"
int main(void)
{
struct {
size_t array_size;
int in[5];
int expected[5];
} test[] = {
{ 5, { 1, 2, 3, 4, 5 }, { 120, 60, 40, 30, 24 } },
{ 4, { 1, 2, 3, 4, 5 }, { 24, 12, 8, 6, 0 } },
{ 3, { 1, 2, 3, 4, 5 }, { 6, 3, 2, 0, 0 } },
{ 2, { 1, 2, 3, 4, 5 }, { 2, 1, 0, 0, 0 } },
{ 1, { 1, 2, 3, 4, 5 }, { 1, 0, 0, 0, 0 } },
{ 1, { 0, 2, 3, 4, 5 }, { 1, 0, 0, 0, 0 } },
{ 5, { 1, 2, 2, 4, 5 }, { 80, 40, 40, 20, 16 } },
{ 5, { 9, 2, 2, 4, 5 }, { 80, 360, 360, 180, 144 } },
{ 5, { 0, 2, 0, 4, 5 }, { 0, 0, 0, 0, 0 } },
{ 5, { 7, 2, 0, 4, 5 }, { 0, 0, 280, 0, 0 } },
{ 5, { -1, -1, -1, -1, -1 }, { 1, 1, 1, 1, 1 } },
{ 4, { -1, -1, -1, -1, -1 }, { -1, -1, -1, -1, -1 } },
{ 2, { INT_MAX, INT_MIN, 0, 0, 0 }, { INT_MIN, INT_MAX, 0, 0, 0 } },
};
const size_t test_count = sizeof(test)/sizeof(test[0]);
const char* function_names[] = { "original", "find_product_arr", "generic" };
int *(*functions[])(const int*, size_t) = { original, find_product_arr, generic };
const size_t function_count = sizeof(functions)/sizeof(functions[0]);
for (size_t i = 0; i < test_count; ++i) {
for (size_t j = 0; j < function_count; ++j) {
int *result = functions[j](test[i].in, test[i].array_size);
bool ok = compare(test[i].array_size, result, test[i].expected);
printf("%s: %20.20s { %lu, {",
(ok ? GREEN " OK" WHITE: RED "BAD" WHITE),
function_names[j],
test[i].array_size
);
dump(test[i].in, test[i].array_size);
printf("}, {");
dump(test[i].expected, test[i].array_size);
printf("} }");
if (ok) {
printf("\n");
} else {
printf(", got " RED "{" );
dump(result, test[i].array_size);
printf("}" WHITE "\n");
}
free(result);
}
}
}
Is it overkill? Probably, but if I saw such code associated with a function like yours, I'd be much more likely to both use it as is with confidence and also to modify it or write a new version with the expectation of being able to test it quickly and accurately.