1. Traversing an array is a key skill
If an array is a box with identical parts, then traversing an array is when you open the box and honestly count what’s inside: how many parts, which one is the biggest, whether there are defective ones, and how much they cost all together. Almost all basic array tasks boil down to one thing: iterate over the elements and carefully sum or check something.
In Kotlin, the for loop can iterate over array elements directly: for (x in a) { ... } is normal and expected style. But sometimes we need not only the element, but also its position (index). Then we iterate over indices: for (i in a.indices) { ... }.
Two traversal styles: by elements and by indexes
From the outside, both styles look almost the same, but they answer different questions. “By elements” reads like “take each element of the array and do something with it.” This is convenient for sum, counting, condition checks, and printing. “By indexes” reads like “iterate over array positions,” and it’s needed when it matters where exactly something is located: the index of the maximum, the position of a found value, in-place replacement of elements.
Let’s start with the simplest thing: we’ll make an array of steps for a week and just print it, so we can see the data with our own eyes (a programmer without console output is like a cook without a spoon).
fun main() {
val steps: IntArray = intArrayOf(4200, 5600, 3100, 8000, 7600, 9000, 1200)
for (x in steps) {
print("$x ")
}
println() // 4200 5600 3100 8000 7600 9000 1200
}
Notice: right now we’re iterating by values (x in steps). We don’t see indexes; x takes on all array values sequentially (one at a time).
2. Pattern No. 1: sum
Sum is the most popular algorithm in the world of beginner programmers, because it’s the first case where you realize that a loop is not “print println seven times,” but “do something important seven times.” We create a special variable (usually sum), give it a starting value (usually 0), and on each step add the next array element.
Logically, it looks like a small flowchart: initialize the sum, traverse all elements, update the sum, get the result. In real development, such diagrams are useful when your brain tries to skip steps and accidentally forgets initialization or updating.
flowchart TD
A[sum = 0] --> B{is there a next element?}
B -->|yes| C[sum = sum + x]
C --> B
B -->|no| D[print sum]
Now let’s implement this as a small project called StepStats.
fun main() {
val steps = intArrayOf(4200, 5600, 3100, 8000, 7600, 9000, 1200)
var sum = 0
for (x in steps) {
sum = sum + x
}
println("Total steps = $sum") // Total steps = 38500
}
An important nuance: sum is a var, because we change it. If you try to write val sum = 0, the compiler will stop you (and it will be right).
3. Average: sum and careful division
The average value is almost always based on the sum. First we compute the sum, then we divide by the number of elements. And this is where Kotlin, like a good programming language, reminds you: Int / Int produces an Int, meaning the fractional part will be dropped. This is not a bug; it’s strict integer math.
So if you want the average as a floating-point number, you need to “convert” at least one part of the expression to Double. The simplest approach: sum.toDouble().
fun main() {
val steps = intArrayOf(4200, 5600, 3100, 8000, 7600, 9000, 1200)
var sum = 0
for (x in steps) sum = sum + x
val avg = sum.toDouble() / steps.size
println("Average steps = $avg") // Average steps = 5500.0
}
Note: steps.size is the number of elements. If the array is for 7 days, it will be 7.
And yes, we got 5500.0, even though a “nice” value could look like 5500. That’s normal: Double is always displayed as a number with a decimal point.
4. Pattern No. 2: counting with a condition
Counting with a condition is very similar to simple summation, only instead of “add the element value,” we “add 1 if the condition is true.” This pattern appears everywhere: how many days were above the norm, how many grades are A’s, how many elements are zero, how many characters are letters.
Let’s add a task to StepStats: count how many days had at least 7000 steps (our goal).
fun main() {
val steps = intArrayOf(4200, 5600, 3100, 8000, 7600, 9000, 1200)
var goodDays = 0
for (x in steps) {
if (x >= 7000)
goodDays = goodDays + 1
}
println("Good days = $goodDays") // Good days = 3
}
Here we again use traversal by elements, because we care about the fact (the value), not the position.
Now let’s make a slightly more real-life variant: the user enters some value (a threshold), and we count how many days are above the threshold.
fun main() {
val steps = intArrayOf(4200, 5600, 3100, 8000, 7600, 9000, 1200)
print("Enter threshold: ")
val threshold = readln().toInt()
var count = 0
for (x in steps)
if (x >= threshold) count++
println("Days >= $threshold : $count")
}
Notice: count++ is the short form of “increase by 1.” Very convenient, especially for counters.
5. Pattern No. 3: search and index
Search is when we want to find out: “is there a value X in the array?” or “at what position is the first value X?” Here, we almost always need indexes, because finding the value is often not enough—it’s also important to know where it is.
And if we don’t find anything, we return -1. Why -1? Because this value definitely cannot be a valid array index (valid indexes start at 0).
One more important detail: if we’re looking for the first match, then after we find it we can stop using break.
Let’s start by searching for a value in the steps array: we’ll check whether there was a day with exactly 9000 steps, and if so, what its index is.
fun main() {
val steps = intArrayOf(4200, 5600, 3100, 8000, 7600, 9000, 1200)
val target = 9000
var index = -1
for (i in steps.indices) {
if (steps[i] == target)
{ index = i; break }
}
println("Index of $target = $index") // Index of 9000 = 5
}
This is the perfect time to use steps.indices—the range of array indexes. It helps you not think about “last index = size - 1.”
Let’s do one more practical example. For instance, find the first day when the step count was below some minimum (for example, less than 3000).
fun main() {
val steps = intArrayOf(4200, 5600, 3100, 8000, 7600, 9000, 1200)
var badIndex = -1
for (i in steps.indices) {
if (steps[i] < 3000)
{ badIndex = i; break }
}
println("First day with < 3000 steps: $badIndex") // First day with < 3000 steps: 6
}
If you want, you can add the text “not found” (when badIndex == -1), but for now the pattern itself is what matters.
6. Pattern No. 4: maximum
Maximum is also a very interesting pattern. Beginners often get caught by “let’s just assign max to zero.” Sometimes it works, and sometimes it doesn’t.
Let’s find the maximum value in the array.
fun main() {
val steps = intArrayOf(4200, 5600, 3100, 8000, 7600, 9000, 1200)
var max = 0
for (x in steps) {
if (x > max)
max = x
}
println("Max steps = $max") // Max steps = 9000
}
Everything works. And it works correctly. But there is a logical error here.
If the array contains negative numbers, then max = 0 breaks the logic: you’ll get a maximum of 0 even if it isn’t in the array. The correct universal rule is: if you are looking for a maximum, you should start with the first element of the array.
Here is the correct way to find the maximum number of steps.
fun main() {
val steps = intArrayOf(4200, 5600, 3100, 8000, 7600, 9000, 1200)
var max = steps[0] // max equals the first value of the array
for (x in steps) {
if (x > max)
max = x
}
println("Max steps = $max") // Max steps = 9000
}
An important takeaway follows: to access a[0], the array must be non-empty: size > 0. So either we know in advance that the array is not empty, or we must write a check if (a.size == 0).
Now let’s try to find not only the maximum, but also the day when the user set a record. For that, we need indexes again.
fun main() {
val steps = intArrayOf(4200, 5600, 3100, 8000, 7600, 9000, 1200)
var maxIndex = 0 // index of the starting value
var max = steps[maxIndex] // starting value for `max`
for (i in steps.indices) {
if (steps[i] > max)
{ max = steps[i]; maxIndex = i }
}
println("Max = $max at index $maxIndex") // Max = 9000 at index 5
}
Pay attention to the style: we update both the value and the index together. If you update only one, you’ll get “the maximum from one day, and the index from another,” and that’s not allowed.
7. Adding business logic
Now let’s do the most enjoyable part: we’ll take the patterns we already know and assemble a small “weekly report.” It’s important not to try to do “everything in one loop” at any cost: for a beginner, it’s easier to read two or three clear loops than one monster loop.
Let’s do this: compute the sum, average, maximum, and the number of days above 7000—and print it nicely.
fun main() {
val steps = intArrayOf(4200, 5600, 3100, 8000, 7600, 9000, 1200)
var sum = 0
var goodDays = 0
var max = steps[0]
for (x in steps) {
sum += x
if (x >= 7000) goodDays++
if (x > max) max = x
}
val avg = sum.toDouble() / steps.size
println("Total = $sum, avg = $avg, max = $max, goodDays = $goodDays")
// Total = 38500, avg = 5500.0, max = 9000, goodDays = 3
}
Below is a small table to summarize how the patterns are similar and how they differ:
| Pattern | What the result variable stores | Starting value | What happens in the loop |
|---|---|---|---|
| Sum | sum of elements (sum) | 0 | sum += x |
| Counting | count (count) | 0 | if (cond) count++ |
| Search | index (index) | -1 | on match, index = i and often break |
| Maximum | maximum value (max) | a[0] (only if the array is not empty) | if (x > max) max = x |
Empty array: there’s always a catch
An empty array is the situation where size == 0. Sometimes it appears intentionally (for example, “no data”), and sometimes accidentally (for example, you created IntArray(n), but n turned out to be 0).
With an empty array, almost all patterns work fine, except those where you access a[0] (maximum, minimum, and sometimes the starting point for something else).
So the rule is simple: if your algorithm requires accessing the first element, you must first check that the array is not empty.
Here is a safe template for finding the maximum:
fun main() {
val steps = intArrayOf() // empty
if (steps.size == 0) {
println("No data") // No data
} else {
var max = steps[0]
for (x in steps) if (x > max) max = x
println("Max = $max")
}
}
This example doesn’t do anything clever; it just honestly handles the case when there is no data.
8. Common mistakes when traversing arrays
Mistake No. 1: compute the average as sum / size and wonder why it has no fractional part.
If sum and size are Int, then the result will also be an Int. That’s why, when computing an average, you need to convert the sum (or the size) to Double, for example sum.toDouble() / size.
Mistake No. 2: start the maximum at 0 “because it’s easier.”
Sometimes it works by accident (if all numbers are non-negative), but as soon as negative values appear, the logic falls apart. The correct starting value for a maximum is the first array element a[0], but only in the branch where the array is definitely non-empty.
Mistake No. 3: forget break in a search and get “the last found index” instead of “the first.”
The most common variant: the search is written, but break is not used. As a result, the loop keeps going, and the index is overwritten again and again.
Mistake No. 4: mix up traversal by elements and by indexes, and then not understand why you “can’t get the position.”
for (x in a) does not give you the index—only the value. If you want the index, use for (i in a.indices) and access the value as a[i].
Mistake No. 5: use incorrect index bounds and accidentally go out of the array.
Classic: write for (i in 0..a.size) and get an out-of-bounds access on the last step. The most reliable option is a.indices, because it is exactly “about valid indexes.”
Mistake No. 6: not initialize the accumulator variable, or initialize it “incorrectly.”
For a sum, the start must be 0. For counting, also 0. For search, it’s convenient to start the index at -1. If you mix up starting values, the algorithm will produce strange results—sometimes even “almost correct,” which is especially tricky.
Mistake No. 7: try to solve the task “in one line” and lose readability.
Beginners sometimes do search, sum, maximum, and printing in one giant construct, and then they themselves can’t explain what’s happening. At this stage, it’s more important that the code reads linearly: initialization → loop → updates → result.