-1

The disadvantages of 1-indexing are well-known. However, our hand is sometimes forced by our choice of language and we have to convert algorithms that were intended for a 0-indexed language to being 1-indexed. When my hand is forced in this way, I find that I struggle immensely. My intuition tells me that I should be able to solve all of my problems by just adding or subtracting one wherever it matters, but I find that this is hardly ever the case. For example, suppose that I wanted to adapt this OEIS sequence (another of Dijkstra's, like the link above) for a 1-indexed language. In 0-indexing, the rules are simple:

  • a(0) = 0
  • a(1) = 1
  • For n >= 2:
    • If n is even a(n) = a(n/2)
    • Otherwise, a(n) = a((n-1)/2) + a((n+1)/2)

In fact, the page even shows some Python code that does this in one line: def a(n): return n if n<2 else a(n/2) if n%2==0 else a((n - 1)/2) + a((n + 1)/2).

However, adapting this to a 1-indexed language appears non-trivial. We could add 1 to every argument of the function a, but it's still non-obvious what to do with our conditions. For example, it's not clear if we should "add 1" to "if n is even" and swap it for "if n+1 is even, i.e. if n is odd". One could even go further and ask if a(n)=a(n/2) should be swapped for a(n+1)=a((n+1)/2) or what we should do with "for n>=2".

Hopefully, I've shown that the process of converting 0-indexed code to 1-indexed code has the potential to confuse somebody. So, my question is this - why is this process non-trivial? What, fundamentally, is preventing the creation of a foolproof set of guidelines for adapting such code?

2
  • 9
    There are only two hard things in computer science: cache invalidation, naming things, and off-by-one errors. Commented Dec 5, 2020 at 2:46
  • 1
    I'd say you need to take the semantics of the rules into account. For example, if the OEIS sequence you provided is not meant as some array indexing scheme, then making it start from 1 doesn't even make sense (I think) without specifying what properties you want to preserve, and in what way (as you are now working with a completely different sequence). What I'm suggesting is that, in the abstract, there isn't necessarily an obvious thing to do - you need extra context to frame the problem. Commented Dec 10, 2020 at 9:40

5 Answers 5

5
+50

Index-transformations are a simple but tedious thing.

  1. Write the code with "natural" indexing for the algorithm.
  2. Write down the transformation rule for the individual indices to get "natural" indexing for the language.
  3. Substitute in the new indices.
  4. Simplify the resulting mess.
  5. Optionally, rename the new variables to the old pre-transformation names.

None of the steps are in any way complicated, but execution must be exacting, and when doing it manually, dropping some detail is very easy, especially when combining them.

Thus, expect additional errors due to the unnatural indexing needed, and expand testing.

1
  • 1
    I think what makes it so hard is that even though "Simplify the resulting mess." is a simple and common-place optimization pass done by compilers, the results are internal and transient, and can't easily be used to rewrite the source code in the new convention (0-based or 1-based). Thus, the simplification of the index math at the source code level ends up being done by hand, which is where the mistakes come in. Commented Dec 4, 2020 at 22:20
5

It's still trivial. Everywhere you have a(something) you change it to a(something+1).

Leave n alone. Even n before is still even n after, but it affects an odd array element, because you changed a(n) to a(n+1).

You can then simplify the mathematical expressions if you wish. It's the simplification that is non-trivial.

4
  • 1
    Is ignoring n really that trivial? Moving the a(0) case to a(1) worries me. As does writing a line like for(i in 3:n) {if(even(i)){a(i+1)=a(i/2+1) else a(i+1)=a(1+(i-1)/2)+a(1+(i+1)/2)} Commented Dec 4, 2020 at 21:19
  • I think you're fundamentally right but specifically wrong. "Just move the relevant bits one index up" is trivial, but figuring out what parts are relevant is not. A key part in solving our example is making the observation that the 0-indexed even branch is an odd branch in 1-indexing. Another pitfall is that we must notice that the left-hand side of our equations, a(i)=[whatever], must not change, whereas we must "change all a([some function of i]) to a([some function of i+1])" on the right. Overall, the path to if(even(i)){a(i)=a(i/2)+a((i+2)/2)} else a(i)=a((i+1)/2 seems non-trivial. Commented Dec 5, 2020 at 13:59
  • 3
    @J.Mini the relevant bits are precisely the argument to a() and absolutely nothing else. Commented Dec 5, 2020 at 17:41
  • @J.Mini: Just think about the myCorrectIndex value. Setting its value is easy (the old index + or - 1 depending on which way you're correcting). The usage of myCorrectIndex in the actual calculation remains unchanged between the two ways of handling indices. I think you're getting yourself turned around by trying to do it all in one go and failing to keep track of which part of the formula belongs to the original formula and which part is an index-correcting calculation. Commented Dec 11, 2020 at 0:20
4

I understand that OP is looking for a canonical answer to the following question:

Why is converting 0-indexed code to 1-indexed code non-trivial?

But there can be no answer to this question (canonical or otherwise) because, as has already been noted in some other answers, the conversion process is trivial. I will provide a particularly simple description of it in order to emphasise the point, and to pursue another angle that does at least provide an answer for another question the OP asks.

Say you are required to work in a language with 1-indexed arrays (such as Lua, Julia, etc) but your algorithm is described in terms of working on a 0-indexed array which, for the sake of argument, let's call A.

Define two functions:

function get_A(index)
    return A[index+1]
end

function set_A(index, value)
    A[index+1] = value
end

Now, everywhere your algorithm fetches a value from A at some index, call get_A with that index instead. And everywhere your algorithm updates the value at some index in A, call set_A with the index and the new value instead.

If other operations, such as those based on slices, are required, they can be constructed in the same manner as get_A and set_A.

Several of the other answers mention refactoring, but refactoring is a bit of a red herring. It is only required if the performance you get when using these functions is insufficient for your needs -- and bear in mind that many optimizing compilers are quite capable of inlining functions like these and producing efficient code from them. In cases where readability for sake of assurance of correctness is more important than performance, it would actually be wiser to not simplify the expressions in the source code.

In that light, to give an answer for the other question:

What, fundamentally, is preventing the creation of a foolproof set of guidelines for adapting such code?

What's preventing it is that sometimes clarity trumps performance and sometimes performance trumps clarity, and since we can't guess which is going to be the more important requirement for any particular piece of software, and we can't guess how good the chosen compiler is going to be at optimizing the arithmetic under the hood, we can't create a single set of guidelines for adapting 0-indexed code to 1-indexed code that could reasonably be called "foolproof".

2

Get a book about refactoring.

First step: You replace an array with indices from 0 to n-1 with an array with indices from 1 to n, and add 1 to every array index.

Second step: If you have a loop where an index used in this array runs in the range k1 ≤ index < k2, you change the range to k1 + 1 ≤ index < k2 + 1, and subtract 1 from every use of index. That should remove the "+1"s you used in the first step.

Third step: Wherever an index is calculated, or wherever it is passed to a function, add 1 to the index and subtract 1 where the index is used.

Best not to do this alone, but to have a colleague doing the exact same thing; then you diff your results and they must be identical. Be disciplined and don't touch anything else. Do a small number of changes, and when you and your colleague agree, commit one of them. If you don't agree and can't figure out where you went wrong, throw it away and start over.

3
  • Does this need any steps about changing conditionals? Or are they covered by an earlier step? Commented Dec 7, 2020 at 19:24
  • Am I misreading? Step 1 seems to be two steps: "Step 0: Make the 0-indexed array 1-indexed" and "Step 1: Find every use of an array index in your code and add 1". Commented Dec 7, 2020 at 19:53
  • @J.Mini I changed the steps from zero based to one based :-) Commented Dec 10, 2020 at 23:49
1

TL;DR

A programming language that uses one-based indexing imposes the condition that zero (0) is not a valid index for the language's built-in array type (or, matrix type).

Programming languages generally do not restrict the range of integer arguments passed into functions and data structures. In other words, function calls and data structures are generally not required to follow the one-based indexing rule.


Many programming languages encourage the convention that functions and data structures should mimic the way arrays work whenever applicable, so as to save the programmer-user from having to determine what convention to use every single time.

Whenever it doesn't make sense for certain functions and data structures to mimic a built-in array's behavior, then don't.


It is, therefore, the responsibility for a programmer proficient in that language to figure out how to implement what needs to be done in a least burdensome way.

A programming language that uses one-based indexing will generally need to specify how to resolve these issues idiomatically.


Code sample in MATLAB:

% someVector is a MATLAB row vector
someVector = zeros(1, 6);

if true
    % valid MATLAB code
    someVector(1:6) = -1:-1:-6; 
else
    % invalid, matrix index cannot be zero
    someVector(0) = 0.99; 
end

%% ......

% someFunction is a MATLAB function
function result = someFunction(a)
    if a == 0
        result = 0.99;
    else
        result = -a;
    end
end

% Zero is a valid value when used as a function argument
b = someFunction(0);

%% ......

% MATLAB has a dictionary type too.
cm = containers.Map('KeyType', 'int32', 'ValueType', 'any');

% Zero is a value in int32. It is valid to use zero as a key.
cm(0) = 0.99;

A prime example is the Fast Fourier Transform (an implementation of Discrete Fourier Transform), or more generally the representation of any signal processing code.

Each element of the array is supposed to represent the coefficient for Z0, Z1, Z2, and so on.

It is obvious that MATLAB's one-based indexing will cause tremendous trouble to the novice user, because the coefficient for Z0 cannot be stored in a vector at index 0. It can only be stored at index 1.

Yet, MATLAB users simply swallowed their complaints. A convention has evolved that every dependent (signal) variable should have a corresponding independent (domain) variable, and that nobody should assume that the values inside an independent variable starts at one.


In the following MATLAB code example, a vector (sequence) named k is created as the input to the mathematical function "z raised to power k". The output is saved in a vector (sequence) named 'y'.

Observe that, the elements in y and the elements in k are in correspondence. Instead of expecting that y(30) containing the value z30, a MATLAB user first finds the element in k that contain 30, then finds the corresponding element in y. A typical usage is thus

y30 = y(k == 30);

Such usage may look tedious, but it is largely hidden from the everyday user's view because they are taken care of inside library functions.

One could argue that this is computationally inefficient. The one-line code above entails a whole-array equality comparison in order to find the index of the element in k that equals 30, and then use the index to read the element from y. (If the array length is one million, and a programmer uses this one-line code a million times, the time complexity is proportional to one million squared.)

(Disclaimer: The following example is intended to illustrate the array indexing issue. It is not intended to illustrate signal processing concepts.)


% vector length
n = 128;

% a row vector of length 128, with content 0, 1, 2, ... 127
k = 0 : n-1;

% let's create some complex number
z = exp(0.005j);

% let's compute z raised to the power of something
y = z .^ k;

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.