Binary Search

Halve a sorted range on every step to find a value, a boundary, or the smallest answer that works, in O(log n) time.

BeginnerRead first: Big-O & Complexity

How It Works

Binary search finds a target in a sorted array by checking the middle element and throwing away the half that can't hold the answer. Every step halves what's left, so a million elements take about 20 steps. That's O(log n) instead of the O(n) of scanning.

The version that won't let you down isn't "find x". It's find the first index where a condition becomes true. In sorted nums, the condition nums[i] >= target is false for a run at the start and true for everything after. The first true index is the lower bound. Exact search is lower bound plus one check. The first index where nums[i] > target is the upper bound, and the last copy of target, if there is one, sits just before it.

target = 5

index    0  1  2  3  4  5  6
nums     1  3  3  5  5  8  9
>= 5?    F  F  F  T  T  T  T
                  ^ first T: lower bound = 3

 lo  hi  mid  nums[mid]
  0   7    3          5   not < 5, so hi = mid = 3
  0   3    1          3   < 5, so lo = mid + 1 = 2
  2   3    2          3   < 5, so lo = mid + 1 = 3
  3   3                   lo == hi: the answer is 3
Everything left of lo is F and everything from hi on is T. Each step narrows the gap until lo and hi meet at the first T.

Lower Bound, Step by Step

Step 1 of 7

nums

  • mid, being tested
  • Best answer so far
  • Ruled out
  • Lower bound

Look for the first index where nums[i] >= 5 in the range [0, 7). hi starts past the end, because every value might be smaller.

All 7 steps as text
  1. Look for the first index where nums[i] >= 5 in the range [0, 7). hi starts past the end, because every value might be smaller.
  2. mid = 3 and nums[3] = 5 isn't below 5, so index 3 might be the answer. Keep it with hi = mid = 3.
  3. Indices 4 to 6 are out, since index 3 passes first. Index 3 waits at hi as the best answer so far. The search goes on in [0, 3).
  4. mid = 1 and nums[1] = 3 is below 5. Sorted order means everything left of it is too, so set lo = mid + 1 = 2.
  5. Indices 0 to 1 are out, all below 5. The search goes on in [2, 3).
  6. mid = 2 and nums[2] = 3 is below 5. Sorted order means everything left of it is too, so set lo = mid + 1 = 3.
  7. lo == hi == 3, so the range is empty and the loop stops. lowerBound returns 3, the first index where nums[i] >= 5.

The same framing works with no array at all. Binary search on the answer applies when a problem asks for the smallest (or largest) value that works, and "works" is monotonic: if a speed of 4 is fast enough, every faster speed is too. Pick a range that surely holds the answer, test the middle value with a check function, and keep the half that can still contain the first value that passes. That takes O(log R) checks for a range of R candidate values.

  • Sorted array. Exact match, first or last occurrence, insertion point.
  • Partly sorted array. In a rotated sorted array, one side of mid is always sorted, and that's enough to decide which half to keep.
  • Answer space. Minimum speed, minimum capacity, largest minimum distance: search the candidate values with a yes/no check.

When to Reach for It

  • The input is sorted, or was sorted and then rotated.
  • The constraints rule out O(n), or the question asks for O(log n).
  • You need the first or last position of a value, or where it would be inserted.
  • The question asks for the minimum or maximum value that satisfies a condition, and checking one candidate is easy.
  • A yes/no test is monotonic: once a guess passes, every bigger guess passes too.

Complexity

OperationTimeSpace
Search a sorted arrayO(log n)O(1)
Sort once, then answer q searchesO((n + q) log n)O(1) to O(n), depending on the sort
Binary search on the answer (R candidates, O(n) check)O(n log R)O(1)

The Template

Lower Bound

lowerBound returns the first index whose value is at least target, or nums.length if every value is smaller. It searches the half-open range [lo, hi), and hi starts at nums.length because "past the end" is a valid answer.

  • If nums[mid] < target, then mid and everything left of it are too small, so set lo = mid + 1.
  • Otherwise mid might be the answer. Keep it in range with hi = mid.
  • When lo == hi, the range is empty and lo is the answer: the first index that passed, or nums.length if none did.

search shows how exact search falls out of it. Lower bound lands on the first value that is at least target. If that index exists and holds target, you've found it. If not, target isn't in the array, and the index tells you where it would be inserted.

Time O(log n)Space O(1)

def lower_bound(nums: list[int], target: int) -> int:
    """First index in sorted `nums` whose value is >= `target`, or len(nums)
    if every value is smaller."""
    lo, hi = 0, len(nums)  # Half-open [lo, hi): "past the end" is a valid answer.
    while lo < hi:
        mid = lo + (hi - lo) // 2
        if nums[mid] < target:
            lo = mid + 1  # mid and everything left of it are too small.
        else:
            hi = mid  # mid could be the answer, so keep it in range.
    return lo  # lo == hi: the one index left.


def search(nums: list[int], target: int) -> int:
    """Index of `target` in sorted `nums`, or -1. It's lower bound plus one check."""
    i = lower_bound(nums, target)
    return i if i < len(nums) and nums[i] == target else -1


if __name__ == "__main__":
    nums = [1, 3, 3, 5, 8]
    print(lower_bound(nums, 3))  # 1, the first of the two 3s
    print(lower_bound(nums, 4))  # 3, where 4 would go
    print(lower_bound(nums, 9))  # 5, past the end
    print(search(nums, 5))  # 3
    print(search(nums, 4))  # -1
// First index in sorted `nums` whose value is >= `target`, or nums.length
// if every value is smaller.
export function lowerBound(nums: number[], target: number): number {
  let lo = 0;
  let hi = nums.length; // Half-open [lo, hi): "past the end" is a valid answer.
  while (lo < hi) {
    const mid = lo + Math.floor((hi - lo) / 2);
    if (nums[mid] < target) lo = mid + 1; // mid and everything left of it are too small.
    else hi = mid; // mid could be the answer, so keep it in range.
  }
  return lo; // lo == hi: the one index left.
}

// Index of `target` in sorted `nums`, or -1. It's lower bound plus one check.
export function search(nums: number[], target: number): number {
  const i = lowerBound(nums, target);
  return i < nums.length && nums[i] === target ? i : -1;
}

const nums = [1, 3, 3, 5, 8];
console.log(lowerBound(nums, 3)); // 1, the first of the two 3s
console.log(lowerBound(nums, 4)); // 3, where 4 would go
console.log(lowerBound(nums, 9)); // 5, past the end
console.log(search(nums, 5)); // 3
console.log(search(nums, 4)); // -1
package main

import "fmt"

// lowerBound returns the first index in sorted nums whose value is >= target,
// or len(nums) if every value is smaller.
func lowerBound(nums []int, target int) int {
	lo, hi := 0, len(nums) // Half-open [lo, hi): "past the end" is a valid answer.
	for lo < hi {
		mid := lo + (hi-lo)/2
		if nums[mid] < target {
			lo = mid + 1 // mid and everything left of it are too small.
		} else {
			hi = mid // mid could be the answer, so keep it in range.
		}
	}
	return lo // lo == hi: the one index left.
}

// search returns the index of target in sorted nums, or -1. It's lower bound
// plus one check.
func search(nums []int, target int) int {
	i := lowerBound(nums, target)
	if i < len(nums) && nums[i] == target {
		return i
	}
	return -1
}

func main() {
	nums := []int{1, 3, 3, 5, 8}
	fmt.Println(lowerBound(nums, 3)) // 1, the first of the two 3s
	fmt.Println(lowerBound(nums, 4)) // 3, where 4 would go
	fmt.Println(lowerBound(nums, 9)) // 5, past the end
	fmt.Println(search(nums, 5))     // 3
	fmt.Println(search(nums, 4))     // -1
}
using System;

public class Solution
{
    // First index in sorted nums whose value is >= target, or nums.Length
    // if every value is smaller.
    public static int LowerBound(int[] nums, int target)
    {
        int lo = 0, hi = nums.Length; // Half-open [lo, hi): "past the end" is a valid answer.
        while (lo < hi)
        {
            int mid = lo + (hi - lo) / 2;
            if (nums[mid] < target) lo = mid + 1; // mid and everything left of it are too small.
            else hi = mid; // mid could be the answer, so keep it in range.
        }
        return lo; // lo == hi: the one index left.
    }

    // Index of target in sorted nums, or -1. It's lower bound plus one check.
    public static int Search(int[] nums, int target)
    {
        int i = LowerBound(nums, target);
        return i < nums.Length && nums[i] == target ? i : -1;
    }

    public static void Main()
    {
        int[] nums = { 1, 3, 3, 5, 8 };
        Console.WriteLine(LowerBound(nums, 3)); // 1, the first of the two 3s
        Console.WriteLine(LowerBound(nums, 4)); // 3, where 4 would go
        Console.WriteLine(LowerBound(nums, 9)); // 5, past the end
        Console.WriteLine(Search(nums, 5)); // 3
        Console.WriteLine(Search(nums, 4)); // -1
    }
}
public class Solution {
    // First index in sorted nums whose value is >= target, or nums.length
    // if every value is smaller.
    static int lowerBound(int[] nums, int target) {
        int lo = 0, hi = nums.length; // Half-open [lo, hi): "past the end" is a valid answer.
        while (lo < hi) {
            int mid = lo + (hi - lo) / 2;
            if (nums[mid] < target) lo = mid + 1; // mid and everything left of it are too small.
            else hi = mid; // mid could be the answer, so keep it in range.
        }
        return lo; // lo == hi: the one index left.
    }

    // Index of target in sorted nums, or -1. It's lower bound plus one check.
    static int search(int[] nums, int target) {
        int i = lowerBound(nums, target);
        return i < nums.length && nums[i] == target ? i : -1;
    }

    public static void main(String[] args) {
        int[] nums = {1, 3, 3, 5, 8};
        System.out.println(lowerBound(nums, 3)); // 1, the first of the two 3s
        System.out.println(lowerBound(nums, 4)); // 3, where 4 would go
        System.out.println(lowerBound(nums, 9)); // 5, past the end
        System.out.println(search(nums, 5)); // 3
        System.out.println(search(nums, 4)); // -1
    }
}

Worked Problems

Each solution stays folded until you open it, so you can try the problem first. Every one is a complete program in Python, TypeScript, Go, C# and Java, run and checked against its expected output.

Search in Rotated Sorted Array

MEDIUMLeetCode 33

Problem

A sorted array of distinct integers has been rotated: some prefix was moved to the end, so [0, 1, 2, 4, 5, 6, 7] might now read [4, 5, 6, 7, 0, 1, 2]. Given the rotated array and a target, return the target's index, or -1 if it's missing. Your solution must run in O(log n).

Example: nums = [4, 5, 6, 7, 0, 1, 2] and target = 0 returns 4.

Show Solution

Approach

The array as a whole isn't sorted, but split it at any mid and at least one half is. If nums[lo] <= nums[mid], the left half from lo to mid is in order. Otherwise the drop from the largest value back to the smallest sits in the left half, so the right half from mid to hi is in order.

A sorted half is easy to test: target can only be in it if it lies between that half's two ends. If it does, search there. If not, it can only be in the other half. Either way you discard half the range, so the search stays O(log n).

Time O(log n)Space O(1)

def search_rotated(nums: list[int], target: int) -> int:
    """Index of `target` in a rotated sorted array of distinct values, or -1."""
    lo, hi = 0, len(nums) - 1  # Closed [lo, hi]: we return as soon as mid matches.
    while lo <= hi:
        mid = lo + (hi - lo) // 2
        if nums[mid] == target:
            return mid
        if nums[lo] <= nums[mid]:
            # The left half nums[lo..mid] is sorted. Keep it only if target fits in it.
            if nums[lo] <= target < nums[mid]:
                hi = mid - 1
            else:
                lo = mid + 1
        else:
            # The right half nums[mid..hi] is sorted. Keep it only if target fits in it.
            if nums[mid] < target <= nums[hi]:
                lo = mid + 1
            else:
                hi = mid - 1
    return -1


if __name__ == "__main__":
    print(search_rotated([4, 5, 6, 7, 0, 1, 2], 0))  # 4
    print(search_rotated([4, 5, 6, 7, 0, 1, 2], 3))  # -1
    print(search_rotated([1], 0))  # -1
    print(search_rotated([5, 1, 3], 3))  # 2
// Index of `target` in a rotated sorted array of distinct values, or -1.
export function searchRotated(nums: number[], target: number): number {
  let lo = 0;
  let hi = nums.length - 1; // Closed [lo, hi]: we return as soon as mid matches.
  while (lo <= hi) {
    const mid = lo + Math.floor((hi - lo) / 2);
    if (nums[mid] === target) return mid;
    if (nums[lo] <= nums[mid]) {
      // The left half nums[lo..mid] is sorted. Keep it only if target fits in it.
      if (nums[lo] <= target && target < nums[mid]) hi = mid - 1;
      else lo = mid + 1;
    } else {
      // The right half nums[mid..hi] is sorted. Keep it only if target fits in it.
      if (nums[mid] < target && target <= nums[hi]) lo = mid + 1;
      else hi = mid - 1;
    }
  }
  return -1;
}

console.log(searchRotated([4, 5, 6, 7, 0, 1, 2], 0)); // 4
console.log(searchRotated([4, 5, 6, 7, 0, 1, 2], 3)); // -1
console.log(searchRotated([1], 0)); // -1
console.log(searchRotated([5, 1, 3], 3)); // 2
package main

import "fmt"

// searchRotated returns the index of target in a rotated sorted array of
// distinct values, or -1.
func searchRotated(nums []int, target int) int {
	lo, hi := 0, len(nums)-1 // Closed [lo, hi]: we return as soon as mid matches.
	for lo <= hi {
		mid := lo + (hi-lo)/2
		if nums[mid] == target {
			return mid
		}
		if nums[lo] <= nums[mid] {
			// The left half nums[lo..mid] is sorted. Keep it only if target fits in it.
			if nums[lo] <= target && target < nums[mid] {
				hi = mid - 1
			} else {
				lo = mid + 1
			}
		} else {
			// The right half nums[mid..hi] is sorted. Keep it only if target fits in it.
			if nums[mid] < target && target <= nums[hi] {
				lo = mid + 1
			} else {
				hi = mid - 1
			}
		}
	}
	return -1
}

func main() {
	fmt.Println(searchRotated([]int{4, 5, 6, 7, 0, 1, 2}, 0)) // 4
	fmt.Println(searchRotated([]int{4, 5, 6, 7, 0, 1, 2}, 3)) // -1
	fmt.Println(searchRotated([]int{1}, 0))                   // -1
	fmt.Println(searchRotated([]int{5, 1, 3}, 3))             // 2
}
using System;

public class Solution
{
    // Index of target in a rotated sorted array of distinct values, or -1.
    public static int SearchRotated(int[] nums, int target)
    {
        int lo = 0, hi = nums.Length - 1; // Closed [lo, hi]: we return as soon as mid matches.
        while (lo <= hi)
        {
            int mid = lo + (hi - lo) / 2;
            if (nums[mid] == target) return mid;
            if (nums[lo] <= nums[mid])
            {
                // The left half nums[lo..mid] is sorted. Keep it only if target fits in it.
                if (nums[lo] <= target && target < nums[mid]) hi = mid - 1;
                else lo = mid + 1;
            }
            else
            {
                // The right half nums[mid..hi] is sorted. Keep it only if target fits in it.
                if (nums[mid] < target && target <= nums[hi]) lo = mid + 1;
                else hi = mid - 1;
            }
        }
        return -1;
    }

    public static void Main()
    {
        Console.WriteLine(SearchRotated(new[] { 4, 5, 6, 7, 0, 1, 2 }, 0)); // 4
        Console.WriteLine(SearchRotated(new[] { 4, 5, 6, 7, 0, 1, 2 }, 3)); // -1
        Console.WriteLine(SearchRotated(new[] { 1 }, 0)); // -1
        Console.WriteLine(SearchRotated(new[] { 5, 1, 3 }, 3)); // 2
    }
}
public class Solution {
    // Index of target in a rotated sorted array of distinct values, or -1.
    static int searchRotated(int[] nums, int target) {
        int lo = 0, hi = nums.length - 1; // Closed [lo, hi]: we return as soon as mid matches.
        while (lo <= hi) {
            int mid = lo + (hi - lo) / 2;
            if (nums[mid] == target) return mid;
            if (nums[lo] <= nums[mid]) {
                // The left half nums[lo..mid] is sorted. Keep it only if target fits in it.
                if (nums[lo] <= target && target < nums[mid]) hi = mid - 1;
                else lo = mid + 1;
            } else {
                // The right half nums[mid..hi] is sorted. Keep it only if target fits in it.
                if (nums[mid] < target && target <= nums[hi]) lo = mid + 1;
                else hi = mid - 1;
            }
        }
        return -1;
    }

    public static void main(String[] args) {
        System.out.println(searchRotated(new int[] {4, 5, 6, 7, 0, 1, 2}, 0)); // 4
        System.out.println(searchRotated(new int[] {4, 5, 6, 7, 0, 1, 2}, 3)); // -1
        System.out.println(searchRotated(new int[] {1}, 0)); // -1
        System.out.println(searchRotated(new int[] {5, 1, 3}, 3)); // 2
    }
}

Koko Eating Bananas

MEDIUMLeetCode 875

Problem

Koko has piles of bananas and h hours. Each hour she picks one pile and eats up to k bananas from it. If the pile has fewer than k left, she finishes it and waits out the rest of the hour. Find the smallest whole number k that lets her eat every banana within h hours. You can assume h is at least the number of piles.

Example: piles = [3, 6, 7, 11] and h = 8 returns 4. At speed 4 the piles take 1, 2, 2 and 3 hours, 8 in total. At speed 3 they take 10.

Show Solution

Approach

There's no sorted array here. What's sorted is the answer space. Speeds run from 1 to max(piles), and the test "can she finish in h hours at speed k?" is monotonic, because eating faster never takes longer. So the answers look like F F F T T T, and you want the first T. That's lower bound, run over speeds instead of indexes.

piles = [3, 6, 7, 11], h = 8

speed     1   2   3   4   5   6  ...  11
hours    27  15  10   8   8   6  ...   4
<= 8?     F   F   F   T   T   T  ...   T
                      ^ slowest speed that works: 4

max(piles) is a safe upper end: at that speed every pile takes exactly one hour, and h is at least the number of piles. Going faster never helps. Each check walks every pile once, so a guess costs O(n), and there are O(log m) guesses, where m is the largest pile.

Time O(n log m), m = largest pileSpace O(1)

def min_eating_speed(piles: list[int], h: int) -> int:
    """Slowest whole-number speed that lets Koko eat every pile within `h` hours."""
    lo, hi = 1, max(piles)  # At max(piles) every pile takes one hour.
    while lo < hi:
        mid = lo + (hi - lo) // 2
        if hours_needed(piles, mid) <= h:
            hi = mid  # Fast enough: the answer is mid or slower.
        else:
            lo = mid + 1  # Too slow, and so is every speed below mid.
    return lo


def hours_needed(piles: list[int], speed: int) -> int:
    """Hours to finish every pile at `speed` bananas per hour."""
    # Round up: a partly eaten pile still costs a whole hour.
    return sum((pile + speed - 1) // speed for pile in piles)


if __name__ == "__main__":
    print(min_eating_speed([3, 6, 7, 11], 8))  # 4
    print(min_eating_speed([30, 11, 23, 4, 20], 5))  # 30
    print(min_eating_speed([30, 11, 23, 4, 20], 6))  # 23
// Slowest whole-number speed that lets Koko eat every pile within `h` hours.
export function minEatingSpeed(piles: number[], h: number): number {
  let lo = 1;
  let hi = Math.max(...piles); // At max(piles) every pile takes one hour.
  while (lo < hi) {
    const mid = lo + Math.floor((hi - lo) / 2);
    if (hoursNeeded(piles, mid) <= h) hi = mid; // Fast enough: the answer is mid or slower.
    else lo = mid + 1; // Too slow, and so is every speed below mid.
  }
  return lo;
}

// Hours to finish every pile at `speed` bananas per hour.
function hoursNeeded(piles: number[], speed: number): number {
  let hours = 0;
  // Round up: a partly eaten pile still costs a whole hour.
  for (const pile of piles) hours += Math.ceil(pile / speed);
  return hours;
}

console.log(minEatingSpeed([3, 6, 7, 11], 8)); // 4
console.log(minEatingSpeed([30, 11, 23, 4, 20], 5)); // 30
console.log(minEatingSpeed([30, 11, 23, 4, 20], 6)); // 23
package main

import (
	"fmt"
	"slices"
)

// minEatingSpeed returns the slowest whole-number speed that lets Koko eat
// every pile within h hours.
func minEatingSpeed(piles []int, h int) int {
	lo, hi := 1, slices.Max(piles) // At max(piles) every pile takes one hour.
	for lo < hi {
		mid := lo + (hi-lo)/2
		if hoursNeeded(piles, mid) <= h {
			hi = mid // Fast enough: the answer is mid or slower.
		} else {
			lo = mid + 1 // Too slow, and so is every speed below mid.
		}
	}
	return lo
}

// hoursNeeded returns the hours to finish every pile at speed bananas per hour.
func hoursNeeded(piles []int, speed int) int {
	hours := 0
	for _, pile := range piles {
		// Round up: a partly eaten pile still costs a whole hour.
		hours += (pile + speed - 1) / speed
	}
	return hours
}

func main() {
	fmt.Println(minEatingSpeed([]int{3, 6, 7, 11}, 8))       // 4
	fmt.Println(minEatingSpeed([]int{30, 11, 23, 4, 20}, 5)) // 30
	fmt.Println(minEatingSpeed([]int{30, 11, 23, 4, 20}, 6)) // 23
}
using System;
using System.Linq;

public class Solution
{
    // Slowest whole-number speed that lets Koko eat every pile within h hours.
    public static int MinEatingSpeed(int[] piles, int h)
    {
        int lo = 1;
        int hi = piles.Max(); // At max(piles) every pile takes one hour.
        while (lo < hi)
        {
            int mid = lo + (hi - lo) / 2;
            if (HoursNeeded(piles, mid) <= h) hi = mid; // Fast enough: the answer is mid or slower.
            else lo = mid + 1; // Too slow, and so is every speed below mid.
        }
        return lo;
    }

    // Hours to finish every pile at speed bananas per hour. A long, because
    // slow speeds over many big piles overflow an int.
    static long HoursNeeded(int[] piles, int speed)
    {
        long hours = 0;
        // Round up: a partly eaten pile still costs a whole hour.
        foreach (int pile in piles) hours += (pile + speed - 1) / speed;
        return hours;
    }

    public static void Main()
    {
        Console.WriteLine(MinEatingSpeed(new[] { 3, 6, 7, 11 }, 8)); // 4
        Console.WriteLine(MinEatingSpeed(new[] { 30, 11, 23, 4, 20 }, 5)); // 30
        Console.WriteLine(MinEatingSpeed(new[] { 30, 11, 23, 4, 20 }, 6)); // 23
    }
}
import java.util.Arrays;

public class Solution {
    // Slowest whole-number speed that lets Koko eat every pile within h hours.
    static int minEatingSpeed(int[] piles, int h) {
        int lo = 1;
        int hi = Arrays.stream(piles).max().getAsInt(); // At max(piles) every pile takes one hour.
        while (lo < hi) {
            int mid = lo + (hi - lo) / 2;
            if (hoursNeeded(piles, mid) <= h) hi = mid; // Fast enough: the answer is mid or slower.
            else lo = mid + 1; // Too slow, and so is every speed below mid.
        }
        return lo;
    }

    // Hours to finish every pile at speed bananas per hour. A long, because
    // slow speeds over many big piles overflow an int.
    static long hoursNeeded(int[] piles, int speed) {
        long hours = 0;
        // Round up: a partly eaten pile still costs a whole hour.
        for (int pile : piles) hours += (pile + speed - 1) / speed;
        return hours;
    }

    public static void main(String[] args) {
        System.out.println(minEatingSpeed(new int[] {3, 6, 7, 11}, 8)); // 4
        System.out.println(minEatingSpeed(new int[] {30, 11, 23, 4, 20}, 5)); // 30
        System.out.println(minEatingSpeed(new int[] {30, 11, 23, 4, 20}, 6)); // 23
    }
}

Common Pitfalls

  • Mixing the two loop styles. A half-open range uses while lo < hi with hi = mid. A closed range uses while lo <= hi with hi = mid - 1. Mixing them loops forever or skips the answer.
  • Writing lo = mid instead of lo = mid + 1. Since mid rounds down, once hi = lo + 1 the range stops shrinking and the loop never ends.
  • Reading nums[i] right after a lower bound without checking i < nums.length. When every value is smaller than the target, the answer is past the end.
  • Choosing an answer range that doesn't contain the answer, such as starting Koko's speeds at 0 (a division by zero) or ending them below max(piles).
  • Overflowing inside the check. Koko's total hours at a slow speed can pass 2 billion, so add them up in a 64-bit integer.

Practice

More problems to try on your own.