9
\$\begingroup\$

I had to solve the following problem:

Consider an algorithm that takes as input a positive integer n. If n is even, the algorithm divides it by two, and if n is odd, the algorithm multiplies it by three and adds one. The algorithm repeats this, until n is one.
For example, the sequence for n=3 is as follows:

3 → 10 → 5 → 16 → 8 → 4 → 2 → 1

Your task is to simulate the execution of the algorithm for a given value of n.

Input

The only input line contains an integer n.

Output

Print a line that contains all values of n during the algorithm.

Constraints

1 ≤ n ≤ 10⁶

And this is my code. Let me know what you think:

#include <stdio.h>
#include <inttypes.h>
 
#define MAX_NUMBER 1000000 /* max number to evaluate  */
 
int main( void ) {
    
    /* an unsigned int 64 bytes, portable!  */
    uint64_t n = 0;
 
    /* input */
    do { 
        
        /* scanf() verification; if scanf() doesn't return 1, 
        there's an input problem: the input isn't a number*/
        if ( scanf("%" PRIu64, &n) != 1 ) {
 
            printf("INVALID INPUT, NOT NUMBER\n"); /* say to the user that the input isn't valid  */
            
            while ( getchar() != '\n' ) {} /* "clear" stdin */
 
            /*******************************************************
 
            NOTE: if you're not interesed in why I wrote this statement, 
            don't read this.
            i.e.: if my input is "3dg", the final input is 3dg\n
            but this scanf() uses %d, so it only reads until 3, 
            and "dg\n" are in stdin, so this is basically trash.
            getchar() uses stdin, so basically "clean" stdin. 
            Once time this while is 0, stdin is empty
 
            *******************************************************/
 
            n = -1; /* set n as a invalid input  */
        }
 
    } while( n < 1 || n > MAX_NUMBER );
 
    /* algorithm */
    while ( n != 1 ) {
 
        printf("%" PRIu64 " ", n); /* print the value for create a line of all values */
 
        if ( n & 1 ){ /* odd number */
            n *= 3;
            ++n;
        }else{        /* even number */
            n = n >> 1; /* divides by 2 */
        }
 
    }
 
    printf("%" PRIu64 "\n", n); /* final value: 1 */
 
    return 0;
}
\$\endgroup\$
11
  • 1
    \$\begingroup\$ Are you concerned that n *= 3; may overflow? \$\endgroup\$
    – chux
    Commented Jan 26 at 7:25
  • 2
    \$\begingroup\$ n = -1; Unsigned integers don't like being set-to or tested-for being negative... Might be best to just leave it to its initial value 0... (Kudos for initialising when declaring!) Keep at it... \$\endgroup\$
    – Fe2O3
    Commented Jan 26 at 8:45
  • 6
    \$\begingroup\$ Just a quick note. All your comments are simply repeating what the attached code does. They add no value and only make the code harder to read. Comments should explain why something is done when that why is not clearly evident from the code itself. The mark of a good programmer is knowing when that comment is needed (i.e. the ability to put yourself in other people's position). Pretty much all of the comments could be removed if you added your question description as a file-level comment. \$\endgroup\$ Commented Jan 26 at 11:11
  • 1
    \$\begingroup\$ @Fe2O3 AH! my bad... I've seen that errors are often -1, and that's what I did here, but I forgot that I'm working with an unsigned type (cool nickname btw, it's very chemical ;)) \$\endgroup\$ Commented Jan 26 at 18:13
  • 3
    \$\begingroup\$ @Fe2O3 Your bet is very likely to succeed, yet if scanf("%" PRIu64, &n) failed due to a rare input error of stdin (e.g. "12 <keyboard communication failure>), IMO, the spec is squishy enough to leave n is an indeterminate state. Only a pedantic concern. \$\endgroup\$
    – chux
    Commented Jan 26 at 23:35

4 Answers 4

11
\$\begingroup\$

In programming I think commendable practices should be internalised, to be followed out of habit, avoiding needless trouble.

I hold that program source code and parts from it have a way to turn up without "external documentation" that may have accompanied it.

Without a specification in the code, it may be impossible

  • to argue about correctness
  • to modify/substitute an implementation without breaking existing applications (that relied on documented interface …)

TorbenPutkonen put advice on how to comment well enough .

One good practice is to separate user interface (getting start values) from business logic/code/processing(tripling&incrementing/halving):

This helps to keep procedures short so they can be viewed "at a glance".

(There is a slight problem with this practice and the task at hand: is outputting each number business or interaction? And a slightly non-beginner technique: pass a procedure to process the numbers to a client's (call site's) liking.)

Complementary to processing start values read from standard input is processing command line/program arguments (int main(int argc, char* argv[]).

Using PRIu64 looks a solid practice.

The use of whitespace is slightly inconsistent (e.g. before opening braces) - one more thing a contemporary IDE can take care of.

Two ways to detect overflow short of processing an "unlimited" integer representation like digit strings:

  • compare to (UINT64_MAX-1)/3 before multiplication
  • handle tripling by two additions, checking sum to be larger than addend

(For an idea whether this is needed I turned to oeis.org.)

\$\endgroup\$
1
  • \$\begingroup\$ Note that checking that the sum has grown doesn’t work for signed numbers (undefined behaviour). \$\endgroup\$
    – gnasher729
    Commented Jan 27 at 13:17
10
\$\begingroup\$

How subtle is that? (* NEW *)

People reading this Q&A represent the full spectrum of experience, from novices to battle-hardened professionals. Perhaps not-unnoticed, but, oddly, so far unreported is:

//      if ( scanf("%" PRIu64, &n) != 1 ) {
        if ( scanf("%" SCNu64, &n) != 1 ) {

The almost invisible, likely inconsequential this time, use of the wrong macro is typical of one class of coding bugs: How can something that looks so right be so wrong?

Guidelines and support tools (like the descendants of lint) do their best to help. Heed them, but always recognised C will allow you to write foolish code.
"With great power comes great responsibility"


Review

OP is off to a pretty good start on their C adventure.
Very minor layout (whitespace) inconsistencies, not worthy of mention.

As noted in comments below the question, n = -1; is ill-advised.
The negative constant with the unsigned variable creates unnecessary tension.

My first response (in a comment) suggests omitting this statement completely. The considered, expert response regarding this, from @Chux, shows that using n = 0; would ensure that execution would always be predictable.


Incantations

A while() loop and a large (peevish) comment have been used to read and discard 'rubbish' characters that a user may have typed on the same line.

Suggested:

if( scanf( "%" PRIu64 "%*[^\n]", &n ) != 1 ) {

would similarly dispose of possible trailing rubbish, LEAVING only the 'whitespace' character LF (linefeed) in the input buffer. In MANY (but not ALL) cases, this residual whitespace will not cause problems for subsequent scanf() invocations. As the OP's experiences with scanf() broaden and diversify, they are advised to return to the documentation often in order to solidify and consolidate their concepts of its behaviour.

Consider using this format specification (incantation called a "scanset") with scanf() instead of an explicit while() and getchar()...
Mixed use of scanf() with C's other input functions (eg: getchar(), and especially fgets()) can be ill-advised.

One other thing:

while ( getchar() != '\n' ) {} /* "clear" stdin */

takes no account of the possibility that EOF is returned (until forever) by getchar(). There are a number of situations where stdin will not be providing a LF. (Things are never as simple as they first appear!)

Personality: I enjoy discovering uplifting 'Easter Eggs' of authors' personalities in embedded comments (and frequently try my best to be 'unbusinesslike', too.) Consider your reader(s). If straying from 'business', make sure your comment seeks to lighten the reader's day, not darken it...


Code to the problem statement

... the algorithm multiplies it by three and adds one.

            n = ( 3 * n ) + 1; // parentheses superfluous

The compiler will likely emit the same machine code for this version.

Seemingly trivial here, 'direct mapping' between a mathematical formula and its expression in a programming language becomes more important when those formula are more complex.


// comments

/* This (block) style of commenting works, but is considered 'old-fashioned'. */

//
// You are free to use whichever style you prefer in your own programs.
// When working with code in a group, stick to one style agreed to by the majority.
//

Tip:
A recent project was peppered with several tracing (aka "print debugging") printf() statements similar to:

                ...
/**/            printf( "yadda yadda %d\n", varX );
                ...

The /**/ at the margin marked these lines as distinct from the surrounding code.

While there are various (proper) ways to facilitate dis-/en-abling these helpful statements, it was quick-and-dirty to simply add/remove one '/' from the front of these lines:

//**/            printf( "yadda yadda %d\n", varX );  // entire line now 'disabled'

'Print debugging' (to trace the flow or intermediary results of working code) is another 'tool for the toolbox' that may be useful to you someday.


Been there; done that

Coding a tool to play with the Collatz Conjecture is a valid exercise. Its precepts are simple, and its behaviour 'curious' to observe. (It has been empirically tested using 'seeds' up to some very large number of digits without encountering any dragons or unicorns. An amateur is unlikely to have the compute power available to push the proven boundary much higher.)

Noteworthy is the 4 -> 2 -> 1 'loop' at its end. Spot the 22 -> 21 -> 20 happening?

Rather than using base-10 (decimal) to report the sequence of calculated values, octal output provides a possibility to recognise patterns, especially in the lower digits of the results.

The OP might allow the user enter the 'seed' as either decimal ("255") or octal ("0377").
Make this a learning experience.

\$\endgroup\$
4
  • \$\begingroup\$ The compiler I’m using will warn me for an incorrect format string. It will not warn if I used the wrong macro that by coincidence generates the right format string on my implementation but not on another one. \$\endgroup\$
    – gnasher729
    Commented Jan 27 at 22:51
  • \$\begingroup\$ @gnasher729 Fine... The macro is replaced by the sophisticated-but-unsophisticated preprocessor. That 'note' is simply to alert the OP to not be too casual about apparent success... The next release of the preprocessor might be more sensitive and critical... and the 100K lines of code in the codebase suddenly stop compiling... Cheers! :-) \$\endgroup\$
    – Fe2O3
    Commented Jan 28 at 0:05
  • \$\begingroup\$ Did you intentionally use PRI instead of SCN in your "suggested" code in "Incantations" section? Or did you make the same error that you identified with your first observation? \$\endgroup\$ Commented May 4 at 8:47
  • \$\begingroup\$ @TobySpeight Yes, the code snippet I used there, was taken from the OP's code, then extended to show the use of the scanset to 'swallow' the rest of the input buffer... It was only when looking over this answer that I realised that SCN... was also needed, leading to the prepended paragraphs. It's an almost invisible (and, for now, harmless) deficiency I left in place, hoping to 'sensitise' the reader(s) to be aware of the issue... Cheers! :-) \$\endgroup\$
    – Fe2O3
    Commented May 4 at 10:45
2
\$\begingroup\$

This comment is an exaggeration:

    /* an unsigned int 64 bytes, portable!  */
    uint64_t n = 0;

It's portable only across targets that can provide such a type with no padding bits. Although that includes the vast majority of platforms being manufactured today, truly portable code will use uint_fast64_t (or perhaps uint_least64_t), which the C standard says must be available everywhere.


There's no good reason for the input limit (MAX_NUMBER) to be a preprocessor macro. Prefer to use a constant, which makes it strongly typed and controls the scope.


We could avoid the need for this comment:

            n = n >> 1; /* divides by 2 */

If we wrote that as n /= 2, it would be self-documenting (any reputable compiler will emit the same object code for both expressions).

\$\endgroup\$
1
\$\begingroup\$

There's definitely an opportunity to factor your code out into functions which will make it easier to manage than a single monolithic main.

As an example of what greybeard has suggested, imagine a get_uint64 function. There's nothing particularly special about this function. The logic should be self-explanatory.

uint64_t get_uint64(
    uint64_t lower_bound,
    uint64_t upper_bound,
    char *err_msg
) {
    for (;;) {
        uint64_t n;
        if (scanf("%" PRIu64, &n) == 1 && 
            n >= lower_bound &&
            n <= upper_bound
        ) {
            return n;
        }

        fprintf(stderr, "%s\n", err_msg);
        while (getchar() != '\n') {}            
    }
}

But this cuts a big chunk out of your main function and makes it much easier to read.

It also means that you can address any concerns about the implementation of get_uint64 without worrying about impacting the rest of your program. You may, for instance, wish to replace scanf and getchar with a call to fgets and sscanf.

#include <stdio.h>
#include <inttypes.h>
 
#define MAX_NUMBER 1000000 
 
int main(void) {
    uint64_t n = get_uint64(1, MAX_NUMBER, "INVALID INPUT, NOT NUMBER");
 
    while ( n != 1 ) {
        printf("%" PRIu64 " ", n); 
 
        if (n & 1) { 
            n *= 3;
            ++n;
        } else {        
            n = n >> 1; 
        }
    }
 
    printf("%" PRIu64 "\n", n); 
 
    return 0;
}

You might next create a collatz_next function.

uint64_t collatz_next(uint64_t n) {
    if (n & 1) { return n * 3 + 1; }
    else { return n / 2; }
}

At which point we can write the following, including swapping the while loop for a for loop.

#include <stdio.h>
#include <inttypes.h>
 
#define MAX_NUMBER 1000000 
 
int main(void) {
    uint64_t n = get_uint64(1, MAX_NUMBER, "INVALID INPUT, NOT NUMBER");
 
    for (; n != 1; n = collatz_next(n)) {
        printf("%" PRIu64 " ", n);
    }
 
    printf("%" PRIu64 "\n", n);
 
    return 0;
}
\$\endgroup\$
3
  • \$\begingroup\$ Have to DV... This answer has very little "review of OP's code" in it... Plus, it suggests several "solutions" (like mixing scanf() with getchar()) that are ill-advised. Stackoverflow is the community where writing solutions for the OP is accepted... \$\endgroup\$
    – Fe2O3
    Commented May 3 at 23:29
  • 2
    \$\begingroup\$ Upvote from me: "divide into independent functions" is a key teaching point here. \$\endgroup\$ Commented May 5 at 6:39
  • \$\begingroup\$ Suggestion: If one wants to factor this program into separate functions, it would likely be a good idea to, for instance, prompt the user to enter a seed value in the acceptable range, and, perhaps, to wrap the output values rather than printing all on one line. (A linear list of decimal values is very difficult to evaluate.) Perhaps main() should have another loop so that the user does not have to re-start the program for different seed values... \$\endgroup\$
    – Fe2O3
    Commented May 5 at 13:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.