Taro Logo

Count Hills and Valleys in an Array

Easy
Meta logo
Meta
8 views
Topics:
Arrays

You are given a 0-indexed integer array nums. An index i is part of a hill in nums if the closest non-equal neighbors of i are smaller than nums[i]. Similarly, an index i is part of a valley in nums if the closest non-equal neighbors of i are larger than nums[i]. Adjacent indices i and j are part of the same hill or valley if nums[i] == nums[j].

Note that for an index to be part of a hill or valley, it must have a non-equal neighbor on both the left and right of the index.

Return the number of hills and valleys in nums.

For example:

nums = [2,4,1,1,6,5]

Output: 3

Explanation:

  • Index 0: There is no non-equal neighbor of 2 on the left, so index 0 is neither a hill nor a valley.
  • Index 1: The closest non-equal neighbors of 4 are 2 and 1. Since 4 > 2 and 4 > 1, index 1 is a hill.
  • Index 2: The closest non-equal neighbors of 1 are 4 and 6. Since 1 < 4 and 1 < 6, index 2 is a valley.
  • Index 3: The closest non-equal neighbors of 1 are 4 and 6. Since 1 < 4 and 1 < 6, index 3 is a valley, but note that it is part of the same valley as index 2.
  • Index 4: The closest non-equal neighbors of 6 are 1 and 5. Since 6 > 1 and 6 > 5, index 4 is a hill.
  • Index 5: There is no non-equal neighbor of 5 on the right, so index 5 is neither a hill nor a valley.

There are 3 hills and valleys so we return 3.

Could you provide an efficient solution to determine the number of hills and valleys in a given array, taking into account the specified conditions and constraints?

Solution


Clarifying Questions

When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:

  1. Can the input array contain duplicate values, and if so, how should they be handled when determining if a point is a hill or valley?
  2. What is the expected return value if the input array is null or empty?
  3. Are the numbers in the input array integers? What is the range of possible values for each number?
  4. For an element to be considered a hill or valley, does it need to be strictly greater/less than its neighbors, or is greater/less than or equal to sufficient?
  5. If the first or last element is a hill or valley, how do you define its neighbors (e.g., treat them as negative infinity, positive infinity, or ignore them)?

Brute Force Solution

Approach

Imagine you're looking at a landscape made of blocks, some taller and some shorter. To find the hills and valleys the most straightforward way is to look at each block one by one and compare it to its neighbors.

Here's how the algorithm would work step-by-step:

  1. Start by looking at the second block in the landscape.
  2. Check if this block is taller than both the block before it and the block after it. If it is, then it's a hill, so count it.
  3. If the block is not a hill, check if it's shorter than both the block before it and the block after it. If it is, then it's a valley, so count it.
  4. If it's neither a hill nor a valley, then it's just a slope, so don't count it.
  5. Move to the next block in the landscape and repeat the process.
  6. Continue doing this until you have checked every block except the very first and very last block (as these cannot be hills or valleys).
  7. The total number of hills and valleys you counted is the answer.

Code Implementation

def count_hills_and_valleys_brute_force(blocks):
    number_of_hills_and_valleys = 0
    # We skip the first and last elements.
    for index in range(1, len(blocks) - 1):

        # Compare current block with its neighbors
        previous_block = blocks[index - 1]
        current_block = blocks[index]
        next_block = blocks[index + 1]

        # Check for hill condition
        if current_block > previous_block and current_block > next_block:
            number_of_hills_and_valleys += 1

        # Check for valley condition
        elif current_block < previous_block and current_block < next_block:
            number_of_hills_and_valleys += 1

    return number_of_hills_and_valleys

Big(O) Analysis

Time Complexity
O(n)The algorithm iterates through the input array nums of size n, examining each element except the first and last. For each element, it performs a constant number of comparisons (two comparisons) to determine if it's a hill or a valley. Therefore, the number of operations is directly proportional to the size of the input array, resulting in a linear time complexity. The total operations can be approximated as examining (n-2) elements, each with a constant time comparison, which simplifies to O(n).
Space Complexity
O(1)The algorithm iterates through the input array, but the plain English explanation does not mention creating any new data structures or storing any intermediate results besides the counters for hills and valleys. The operations only involve comparing elements in place and incrementing counters. Thus, the auxiliary space used is constant, independent of the input size N (the size of the array).

Optimal Solution

Approach

To efficiently count hills and valleys, we only need to compare each number to its neighbors. The key is to ignore flat plateaus and treat them as part of either a hill or a valley depending on the surrounding numbers.

Here's how the algorithm would work step-by-step:

  1. Go through the list of numbers, one by one.
  2. For each number, look at the numbers to its left and right.
  3. If a number is bigger than both its neighbors, or if it is smaller than both its neighbors, mark it as a possible hill or valley.
  4. If a number is equal to one or both of its neighbors, extend the 'flat' area to include these equal neighbors.
  5. Keep expanding the 'flat' area until you reach a number that's different.
  6. Compare the number at the end of the flat area to the numbers before the flat area, and that determines the hill or valley
  7. Count the number of hills and valleys found by using this 'smart check' strategy.

Code Implementation

def count_hills_and_valleys(numbers):
    number_of_hills_and_valleys = 0
    length_of_numbers = len(numbers)

    for i in range(1, length_of_numbers - 1):
        left_index = i - 1
        right_index = i + 1

        # Find the closest different number on the left
        while left_index >= 0 and numbers[left_index] == numbers[i]:
            left_index -= 1

        # Find the closest different number on the right
        while right_index < length_of_numbers and numbers[right_index] == numbers[i]:
            right_index += 1

        # If either boundary goes out of range, it's not a hill/valley
        if left_index < 0 or right_index >= length_of_numbers:
            continue

        #Determine if current number is hill or valley
        if numbers[i] > numbers[left_index] and numbers[i] > numbers[right_index]:
            number_of_hills_and_valleys += 1

        if numbers[i] < numbers[left_index] and numbers[i] < numbers[right_index]:
            number_of_hills_and_valleys += 1

    return number_of_hills_and_valleys

Big(O) Analysis

Time Complexity
O(n)The algorithm iterates through the array once. For each element, it may extend a 'flat' area, but this extension only involves traversing elements adjacent to the current element and equal to it. Each element is visited and potentially extended from at most once during the main iteration. Therefore, the number of operations is directly proportional to the number of elements, n, resulting in a linear time complexity of O(n).
Space Complexity
O(1)The provided plain English explanation describes an iterative process that primarily involves comparisons and updates of a few variables. It does not suggest the creation of any auxiliary data structures like lists, maps, or sets. The algorithm appears to use a constant number of variables to keep track of indices and comparisons, independent of the input array's size, N. Therefore, the auxiliary space complexity is O(1), indicating constant space.

Edge Cases

CaseHow to Handle
Empty or null input arrayReturn 0, as there are no hills or valleys in an empty array.
Array with fewer than 3 elementsReturn 0, as a hill or valley requires a peak/trough surrounded by two neighbors.
Array with all identical valuesReturn 0, because no element strictly satisfies the hill/valley condition when all neighbors are equal.
Consecutive duplicate valuesSkip over consecutive duplicate values to find the next distinct neighbor for comparison.
Integer overflow during calculations if values are extremely largeEnsure to use appropriate data types (e.g., long) to prevent overflow during comparison.
Alternating high and low values (e.g., sawtooth pattern)The algorithm should correctly identify each peak and trough as a hill or valley, respectively.
Input array containing negative numbers and/or zeroThe comparison logic should work correctly regardless of whether the numbers are positive, negative, or zero.
Very large array sizes exceeding available memoryEnsure algorithm complexity stays linear to prevent excessive memory allocation errors.