More realistic C++: find minimum and maximum among several numbers

in #programming3 years ago

In a previous post I've given an idea about how to sum two or more numbers in modern C++.

Now the problem is again the same from Coding Practice: Write a program in C++ to find out maximum and minimum number from given integer numbers using conditional statement if, but let's generalise it.

Problem: Write a program in C++ to find the maximum and the minimum among a list of numbers

In the previous remake we've already seen how to get a list (as array, vector or via function arguments) and do something with it. In particular, we iterated over its elements to sum them.

Now, we can use the same idea. But let's start with a basic tool: a function that, given two numbers, returns the bigger one. But which type of numbers? Let's generalize it: anything that can be compared and has an ordering

template<class T>
T gmax(T&& a, T&& b) {
    return a >= b ? a : b;
}

This is the basic building block.

The original “problem” imposed the use of an if statement. As an assignment to learn to program, it doesn't make sense (again, tell me who's your teacher and I'll complain with her/him, because she's not pushing her students to think about the problem and solve it with the tools they should already know).

But anyway, do not forget that the ternary operator ?: is just an “expression if”, that is, an expression with the value of a if a >= b is true, and b otherwise (else).

With this function at hand, if we had four numbers, we could display the biggest by nesting gmax like this:

std::cout << gmax(6530, gmax(590, gmax(28640, 32))) << "\n";

But this doesn't scale well, at least when we do it by hand. We could do it automatically noticing the “structure” of the expression. We can consider gmax as a binary operator. Let's think about it as a + and let's write the expression

6530 + 590 + 28640 + 32

If our + were really a +, we know how we could compute the expression: we would sum the first two:

7120 + 28640 + 32

Then we would go on with the result (7120) + the next number in the list (28640),

35760 + 32

and I leave you the last step. You can see there's a structure, and we can apply an algorithm. And what if we interpret + as “get the greater among the two operand”? Then 6530 + 590 would have 6530 as result; then we were left with:

6530 + 28640 + 32

We would compute 6530 + 28640, where + is our “get the greater” operator, and the result would be 28640. And I left the last step to you again…

In the previous article we used accumulate, which does something like this: it applies the binary operator + almost like we did above (it's called left folding, until it produces a final result. But indeed you can give your own binary operator to the function! We could give it our gmax!

Like this:

  std::vector<int> v{6530, 590, 28640, 32};
  std::cout << std::accumulate(v.cbegin(), v.cend(), v[0], 
      [](auto a, auto b) {return gmax(a,b); }) << "\n";

Here I've used a lambda because gmax() is a template and we need to “push” a little bit the type inference. Had we a simple function like this:

int hmax(int a, int b)
{
    return a >= b ? a : b;
}

we could have written:

std::cout << std::accumulate(v.cbegin(), v.cend(), v[0], hmax) << "\n";

I've already explained what accumulate does; we also did a little bit of its work by hand with those four numbers. Also in the previous article about the sum, you should have asked yourself: why (T)0? Well, because 0 is the “neuter” value of the addition, that is a + 0 == a. And accumulate needs a starting value (guess why as an exercise).

Now, if our operator is gmax which returns the greater of two numbers, what should be the “init” for accumulate? It can't be 0. If it were 0, the gmax of -10, -20 would be 0, which is wrong.

That's why I used the first value: max between A and A is always A, hence we are safe!

If we replace gmax with a similar function gmin, we have the minimum we wanted.

Ok, now let's suppose we can't use accumulate. Let's do it “by hand”, iterating. We want a function, a piece of code we can reuse: avoid writing your code with pieces just put there. Try to think about functions that you use to “compose” other functions and then you put together to give your solution.

If generalized, we are indeed searching for an implementation of accumulate… But let's think of another idea: we want both minimum and maximum: we can iterate once, and compute both values as we go!

template<class T>
void minmax(const std::vector<T>& v, T& rmin, T& rmax)
{
        rmin = v[0];
        rmax = v[0];
        for (auto val : v) {
                if (rmax < val) rmax = val;
                if (rmin > val) rmin = val;
        }
}

Unless we use tuples, we can't return two values from a function, hence I've used arguments (by reference, so that we can modify the caller's variable).

And you use this function like this:

    std::vector<int> v{6530, 590, 28640, 32};
//...
    int rmin, rmax;
    minmax(v, rmin, rmax);
    std::cout << "min " << rmin << "\nmax " << rmax << "\n"; 

You should be able to put altogether in a single source file. Here it is:

#include <iostream>
#include <vector>
#include <numeric>

template<class T>
T gmax(T&& a, T&& b)
{
    return a >= b ? a : b;
}

int hmax(int a, int b)
{
    return gmax(a, b);
}


template<class T>
void minmax(const std::vector<T>& v, T& rmin, T& rmax)
{
   rmin = v[0];
   rmax = v[0];
   for (auto val : v) {
           if (rmax < val) rmax = val;
           if (rmin > val) rmin = val;
   }
}

int main()
{
   std::cout << gmax(6530, gmax(590, gmax(28640, 32))) << "\n";
   std::vector<int> v{6530, 590, 28640, 32};
   std::cout << std::accumulate(v.cbegin(), v.cend(), v[0],
                           [](auto a, auto b) {return gmax(a,b); }) << "\n";
   std::cout << std::accumulate(v.cbegin(), v.cend(), v[0], hmax) << " **\n";
   int rmin, rmax;
   minmax(v, rmin, rmax);
   std::cout << "min " << rmin << "\nmax " << rmax << "\n";
   return 0;
}

There can be something unexplained here (like the &&, which is not the logical AND…), but you got the idea… I hope! Or at least, you see there's a lot to learn and try harder to grasp the basic first things that will make you master modern C++.

Note: since we've used template (except in hmax…), you can use those functions also for double, or long, or for any type which can be compared and has an ordering.

image.png