Before attempting this problem, you should be comfortable with:
Queues/Deques - Understanding FIFO data structures for maintaining a sliding window of elements
Sliding Window - Tracking a fixed-size window of recent elements as new data arrives
Circular Arrays - Using modular arithmetic to implement efficient fixed-size buffers
1. Array or List
Intuition
The most straightforward way to compute a moving average is to store all incoming values in a list and calculate the average of the last size elements each time. When a new value arrives, we append it to the list, then sum the most recent values up to the window size. This approach is simple to implement but recalculates the sum from scratch on every call.
Algorithm
Initialize an empty list to store all incoming values and save the window size.
When next(val) is called, append the new value to the list.
Calculate the sum of the last size elements (or all elements if fewer than size exist).
Return the sum divided by the number of elements in the window (minimum of list length and size).
classMovingAverage:def__init__(self, size:int):
self.size = size
self.queue =[]defnext(self, val:int)->float:
size, queue = self.size, self.queue
queue.append(val)# calculate the sum of the moving window
window_sum =sum(queue[-size:])return window_sum /min(len(queue), size)
classMovingAverage{int size;List queue =newArrayList<Integer>();publicMovingAverage(int size){this.size = size;}publicdoublenext(int val){
queue.add(val);// calculate the sum of the moving windowint windowSum =0;for(int i =Math.max(0, queue.size()- size);
i < queue.size();++i
) windowSum +=(int) queue.get(i);return(windowSum *1.0)/Math.min(queue.size(), size);}}
classMovingAverage{private:int size;
vector<int> queue;public:/** Initialize your data structure here. */MovingAverage(int size){this->size = size;}doublenext(int val){
queue.push_back(val);// calculate the sum of the moving windowint windowSum =0;for(int i =max(0,(int)queue.size()- size); i < queue.size();++i)
windowSum += queue[i];return windowSum *1.0/min((int)queue.size(), size);}};
classMovingAverage{/**
* @param {number} size
*/constructor(size){this.size = size;this.queue =[];}/**
* @param {number} val
* @return {number}
*/next(val){const size =this.size;const queue =this.queue;
queue.push(val);// calculate the sum of the moving windowconst windowSum = queue.slice(-size).reduce((sum, num)=> sum + num,0);return windowSum / Math.min(queue.length, size);}}
publicclassMovingAverage{privateint size;privateList<int> queue;publicMovingAverage(int size){this.size = size;this.queue =newList<int>();}publicdoubleNext(int val){
queue.Add(val);// calculate the sum of the moving windowint windowSum =0;for(int i = Math.Max(0, queue.Count - size); i < queue.Count; i++){
windowSum += queue[i];}return(double)windowSum / Math.Min(queue.Count, size);}}
type MovingAverage struct{
size int
queue []int}funcConstructor(size int) MovingAverage {return MovingAverage{size: size, queue:[]int{}}}func(ma *MovingAverage)Next(val int)float64{
ma.queue =append(ma.queue, val)
start :=len(ma.queue)- ma.size
if start <0{
start =0}
windowSum :=0for i := start; i <len(ma.queue); i++{
windowSum += ma.queue[i]}
count :=len(ma.queue)if count > ma.size {
count = ma.size
}returnfloat64(windowSum)/float64(count)}
Where N is the size of the moving window and M is the number of calls made to next.
2. Double-ended Queue
Intuition
Instead of recalculating the sum each time, we can maintain a running sum and a queue that holds only the elements within the current window. When the window is full and a new element arrives, we remove the oldest element from both the queue and the running sum, then add the new element. This way, each next() call runs in constant time.
Algorithm
Initialize a deque (double-ended queue), a running sum window_sum, and a count of elements seen.
When next(val) is called:
Increment the count and add the new value to the queue.
If the count exceeds the window size, remove the front element from the queue and subtract it from the running sum.
Add the new value to the running sum.
Return the running sum divided by the current window size (minimum of count and size).
classMovingAverage:def__init__(self, size:int):
self.size = size
self.queue = deque()# number of elements seen so far
self.window_sum =0
self.count =0defnext(self, val:int)->float:
self.count +=1# calculate the new sum by shifting the window
self.queue.append(val)
tail = self.queue.popleft()if self.count > self.size else0
self.window_sum = self.window_sum - tail + val
return self.window_sum /min(self.size, self.count)
classMovingAverage{int size, windowSum =0, count =0;Deque queue =newArrayDeque<Integer>();publicMovingAverage(int size){this.size = size;}publicdoublenext(int val){++count;// calculate the new sum by shifting the window
queue.add(val);int tail = count > size ?(int) queue.poll():0;
windowSum = windowSum - tail + val;return(windowSum *1.0)/Math.min(size, count);}}
classMovingAverage{private:int size, windowSum =0, count =0;
std::deque<int> queue;public:MovingAverage(int size){this->size = size;}doublenext(int val){++count;// calculate the new sum by shifting the window
queue.push_back(val);int tail = count > size ? queue.front():0;if(count > size) queue.pop_front();
windowSum = windowSum - tail + val;returnstatic_cast<double>(windowSum)/ std::min(size, count);}};
Time complexity per next() call is O(1). Therefore, the total time complexity for M calls is O(M)
Space complexity: O(N)
Where N is the size of the moving window and M is the number of calls made to next.
3. Circular Queue with Array
Intuition
A circular queue (ring buffer) avoids the overhead of shifting elements when removing from the front. We use a fixed-size array where the head pointer wraps around when it reaches the end. The element being overwritten is the one leaving the window, so we subtract it from the running sum before adding the new value. This achieves constant time per operation with minimal memory overhead.
Algorithm
Initialize a fixed-size array of length size, a head pointer, a running sum, and a count of elements seen.
When next(val) is called:
Increment the count.
Calculate the tail position as (head + 1) % size, which points to the oldest element that will be replaced.
Subtract the value at the tail position from the running sum and add the new value.
Move the head pointer forward: head = (head + 1) % size.
Store the new value at the head position.
Return the running sum divided by the current window size (minimum of count and size).
classMovingAverage:def__init__(self, size:int):
self.size = size
self.queue =[0]* self.size
self.head = self.window_sum =0# number of elements seen so far
self.count =0defnext(self, val:int)->float:
self.count +=1# calculate the new sum by shifting the window
tail =(self.head +1)% self.size
self.window_sum = self.window_sum - self.queue[tail]+ val
# move on to the next head
self.head =(self.head +1)% self.size
self.queue[self.head]= val
return self.window_sum /min(self.size, self.count)
classMovingAverage{int size, head =0, windowSum =0, count =0;int[] queue;publicMovingAverage(int size){this.size = size;
queue =newint[size];}publicdoublenext(int val){++count;// calculate the new sum by shifting the windowint tail =(head +1)% size;
windowSum = windowSum - queue[tail]+ val;// move on to the next head
head =(head +1)% size;
queue[head]= val;return(windowSum *1.0)/Math.min(size, count);}}
classMovingAverage{private:int size;int head =0;int windowSum =0;int count =0;
vector<int> queue;public:MovingAverage(int size){this->size = size;
queue =vector<int>(size);}doublenext(int val){++count;// calculate the new sum by shifting the windowint tail =(head +1)% size;
windowSum = windowSum - queue[tail]+ val;// move on to the next head
head =(head +1)% size;
queue[head]= val;return windowSum *1.0/min(size, count);}};
classMovingAverage{/**
* @param {number} size
*/constructor(size){this.size = size;this.queue =newArray(this.size).fill(0);this.head =0;this.windowSum =0;// number of elements seen so farthis.count =0;}/**
* @param {number} val
* @return {number}
*/next(val){this.count +=1;// calculate the new sum by shifting the windowconst tail =(this.head +1)%this.size;this.windowSum =this.windowSum -this.queue[tail]+ val;// move on to the next headthis.head =(this.head +1)%this.size;this.queue[this.head]= val;returnthis.windowSum / Math.min(this.size,this.count);}}
Time complexity per next() call is O(1). Therefore, the total time complexity for M calls is O(M)
Space complexity: O(N)
Where N is the size of the moving window and M is the number of calls made to next.
Common Pitfalls
Dividing by Window Size Before It Is Full
When fewer than size elements have been added, the average should be calculated using the actual count of elements, not the window size. Dividing by size when only a few elements exist produces incorrect results. For example, with size = 3 and only one element 5, the average should be 5.0, not 5/3 = 1.67.
Not Removing the Oldest Element When Window Is Full
When using a running sum approach, forgetting to subtract the element leaving the window causes the sum to grow unbounded. After receiving more than size elements, each new element should trigger removal of the oldest one from both the queue and the running sum. Failing to do this results in the average including more elements than the window size allows.