Moving Average Filter
Problem Statement
A Moving Average Filter (also called a box filter) is one of the simplest smoothing techniques in image and signal processing. It replaces each pixel with the average of its neighboring pixels within a window.
Given a 1D signal (list of integers) and a window size k, compute the moving average for each valid position. Return the result as a list of floats rounded to 2 decimal places.
Application in CV
- Noise reduction in images (applied row-wise or column-wise)
- Temporal smoothing in video frames
- Pre-processing before edge detection
Constraints
- 1≤len(signal)≤10000
- 1≤k≤len(signal)
- −10000≤signal[i]≤10000
Example:
signal = [1, 2, 3, 4, 5], k = 3
[2.0, 3.0, 4.0]
Averages: (1+2+3)/3=2.0, (2+3+4)/3=3.0, (3+4+5)/3=4.0
1. Background Knowledge
The moving average filter (MAF) is a fundamental low-pass filter in signal processing that smooths data by replacing each value with the average of neighboring values in a fixed-size window of length k. For a 1D signal x=[x0​,x1​,…,xn−1​], the output yi​ at position i is:
yi​=k1​j=i∑i+k−1​xj​,i=0,1,…,n−kThis produces n−k+1 values, reducing high-frequency noise while preserving low-frequency trends. In computer vision (CV), it's applied row/column-wise for image denoising before tasks like edge detection (e.g., Canny). Prerequisites: basic array indexing, summation, floating-point division, and rounding to 2 decimals (round(value, 2) in Python).
2. Algorithm Approach
Naive approach: For each i, sum k elements directly → O(nk) time, acceptable for n≤104, k≤n.
Optimized sliding window: Use a prefix sum array s=0, s[i+1]=s[i]+xi​. Then yi​=ks[i+k]−s[i]​:
- Build prefix: O(n)
- Query each window: O(1)
- Total: O(n)
Recursive (cumulative average): Maintain running sum sum, update sum=sum−x[i−1]+x[i+k−1] → O(n) after O(k) init. Both optimizations beat naive for large k.
3. Step-by-Step Strategy
- Validate inputs: Ensure 1≤k≤n, n=len(signal).
- Choose method:
| Method | Pros | Cons | When to use |
|---|---|---|---|
| Naive sum | Simple, no edge cases | O(nk) slow for k ≈ n | k small (< 100) |
| Prefix sum | O(n), clean code | Extra O(n) space | General case |
| Recursive | O(n), O(1) space | Init overhead | Memory-tight |
- Compute averages: Generate list of length n−k+1.
- Format output: Convert to float, round to 2 decimals.
- Test edge cases: k=1 (identity), k=n (single avg).
Python prefix sum example:
def moving_average(signal, k):
n = len(signal)
if k > n or k < 1: raise ValueError("Invalid k")
prefix = * (n + 1)
for i in range(n):
prefix[i+1] = prefix[i] + signal[i]
return [round((prefix[i+k] - prefix[i]) / k, 2) for i in range(n - k + 1)]
4. Common Pitfalls
- Off-by-one indexing: Window ends at i+k−1, output length n−k+1 (not n).
- Integer division: Use float division (/k not //k); signal ints → avg float.
- Rounding: Apply round(avg, 2) after averaging, not per element.
- Edge handling: No padding specified → valid positions only (drop first/last incomplete windows).
- Empty/zero cases: n=1,k=1 → [signal]; negative values OK per constraints.
- Performance: Naive fails T.C. for n=104,k=104 (108 ops → timeout).
5. Time & Space Complexity
| Approach | Time | Space | Notes |
|---|---|---|---|
| Naive | O(nk) | O(n) output | 10^4 × 10^4 = 10^8 → borderline |
| Prefix/Recursive | O(n) | O(n) / O(1) extra | Optimal, scales perfectly |
| Output | - | O(n-k+1) = O(n) | Always required |
Best: Prefix sum for clarity, recursive for space. Both handle constraints efficiently.