Facebook Pixel

2471. Minimum Number of Operations to Sort a Binary Tree by Level

Problem Description

You have a binary tree with unique node values. Your task is to make the values at each level of the tree sorted in strictly increasing order from left to right.

In each operation, you can select any two nodes that are at the same level (same distance from the root) and swap their values. You need to find the minimum number of such swap operations required to achieve the sorted order at every level.

The level of a node is defined as the number of edges between that node and the root node. For example, the root is at level 0, its children are at level 1, and so on.

The solution uses a level-order traversal (BFS) to process the tree level by level. For each level, it collects all node values and determines the minimum number of swaps needed to sort them. The key insight is to use discretization - mapping each value to its target position in the sorted order, then performing cycle detection to count the minimum swaps.

The helper function f(t) calculates the minimum swaps for an array by:

  1. Creating a mapping of each value to its sorted position
  2. Replacing values with their target indices
  3. Counting swaps needed to place each element at its correct index using cycle decomposition

For example, if a level has values [7, 6, 8, 5], the sorted order would be [5, 6, 7, 8]. The algorithm would determine that 2 swaps are needed to transform the original arrangement into the sorted one.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: A binary tree is a special type of graph with nodes (tree nodes) and edges (parent-child relationships).

Is it a tree?

  • Yes: The problem explicitly states we're working with a binary tree.

DFS?

  • Not quite: While DFS could traverse the tree, the problem requires processing nodes level by level. We need to work with all nodes at the same level simultaneously to determine the minimum swaps needed. This naturally points us toward a level-order traversal approach.

However, let's reconsider our path through the flowchart since we need level-by-level processing:

Is it a graph?

  • Yes: Binary tree is a graph structure.

Is it a tree?

  • No (for this specific requirement): Although it's technically a tree, we need to process it level-wise rather than depth-wise, so let's explore other paths.

Is the problem related to directed acyclic graphs?

  • No: This is about sorting values at each level, not about DAG properties.

Is the problem related to shortest paths?

  • No: We're not finding paths; we're sorting values at each level.

Does the problem involve connectivity?

  • No: All nodes are already connected in a tree structure.

Does the problem have small constraints?

  • No: The tree can be large, and we need an efficient approach.

BFS?

  • Yes: BFS is perfect for level-order traversal, which allows us to process all nodes at each level together.

Conclusion: The flowchart leads us to BFS (Breadth-First Search) for this problem. BFS naturally processes the tree level by level, which is exactly what we need to collect all values at each level, determine the minimum number of swaps to sort them, and sum up the operations across all levels.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we need to sort values at each level of a binary tree, the first insight is that we must process the tree level by level. This immediately suggests using BFS since it naturally visits all nodes at depth d before moving to depth d+1.

Once we have all values at a particular level, we face a classic problem: what's the minimum number of swaps needed to sort an array? This is where the key insight comes in.

Consider an array like [7, 6, 8, 5]. To sort it into [5, 6, 7, 8], we need to figure out which elements need to move where. The clever approach is to think about this as a permutation problem. Each element knows where it wants to go in the sorted order.

We can map each value to its target position: 7 should be at index 2, 6 at index 1, 8 at index 3, and 5 at index 0. This creates cycles in our permutation. For example, if we follow where each element wants to go: position 0 → position 2 → position 3 → position 0, forming a cycle.

The mathematical insight is that for a cycle of length k, we need exactly k-1 swaps to put all elements in their correct positions. Why? Because each swap fixes one element's position, and the last element automatically falls into place when all others are fixed.

The discretization step (replacing values with their target indices) simplifies this process. Instead of dealing with actual values like 7, 6, 8, 5, we work with indices [2, 1, 3, 0], making it easier to detect when an element is in its correct position (when arr[i] == i).

The algorithm repeatedly finds elements not in their correct position and swaps them toward their target, counting each swap. This greedy approach of always swapping an element directly to where another misplaced element sits ensures we achieve the minimum number of swaps by breaking cycles optimally.

Learn more about Tree, Breadth-First Search and Binary Tree patterns.

Solution Approach

The solution implements BFS combined with a cycle decomposition algorithm for minimum swaps. Let's walk through the implementation step by step:

1. Level-Order Traversal using BFS

We start with a queue containing the root node and process the tree level by level:

q = deque([root])
while q:
    t = []  # Store values at current level
    for _ in range(len(q)):
        node = q.popleft()
        t.append(node.val)
        if node.left:
            q.append(node.left)
        if node.right:
            q.append(node.right)

For each level, we collect all node values into array t. The inner loop processes exactly the nodes at the current level before moving to the next.

2. Discretization for Minimum Swaps

The helper function f(t) calculates minimum swaps needed to sort array t:

def f(t):
    n = len(t)
    m = {v: i for i, v in enumerate(sorted(t))}
    for i in range(n):
        t[i] = m[t[i]]

This discretization step maps each value to its target index in the sorted array. For example, if t = [7, 6, 8, 5], then sorted(t) = [5, 6, 7, 8], and the mapping becomes:

  • 5 → 0, 6 → 1, 7 → 2, 8 → 3

After transformation, t becomes [2, 1, 3, 0], where each number represents where that element should be in the sorted array.

3. Cycle Detection and Swapping

ans = 0
for i in range(n):
    while t[i] != i:
        swap(t, i, t[i])
        ans += 1

This is the core algorithm that counts minimum swaps. For each position i:

  • If t[i] != i, the element is not in its correct position
  • We swap it with the element at position t[i] (where it wants to go)
  • Continue swapping until position i has the correct element

The key insight: when we swap t[i] with t[t[i]], we're directly placing one element in its correct position while potentially starting to fix another cycle. This greedy approach ensures minimum swaps.

4. Accumulating Results

ans = 0
while q:
    # ... process level
    ans += f(t)
return ans

We sum up the minimum swaps needed for each level to get the total operations required for the entire tree.

Time Complexity: O(n log n) per level due to sorting, where n is the number of nodes at that level. Overall O(N log W) where N is total nodes and W is the maximum width of the tree.

Space Complexity: O(W) where W is the maximum width of the tree, needed for the queue and temporary arrays.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to illustrate the solution approach.

Consider this binary tree:

        4
       / \
      7   3
     / \   \
    8   6   5

Step 1: Level-Order Traversal

We process the tree level by level using BFS:

  • Level 0: [4] - Already sorted, 0 swaps needed
  • Level 1: [7, 3] - Not sorted (should be [3, 7])
  • Level 2: [8, 6, 5] - Not sorted (should be [5, 6, 8])

Step 2: Processing Level 1 - Values [7, 3]

Apply the discretization process:

  1. Sort the values: [3, 7]
  2. Create mapping: 3 → 0, 7 → 1
  3. Transform array: [7, 3] becomes [1, 0]
    • 7 maps to index 1 (its target position)
    • 3 maps to index 0 (its target position)

Now count swaps for [1, 0]:

  • At index 0: t[0] = 1 ≠ 0, so swap t[0] with t[1]
  • After swap: [0, 1] - both elements are now in correct positions
  • Result: 1 swap needed

Step 3: Processing Level 2 - Values [8, 6, 5]

Apply discretization:

  1. Sort the values: [5, 6, 8]
  2. Create mapping: 5 → 0, 6 → 1, 8 → 2
  3. Transform array: [8, 6, 5] becomes [2, 1, 0]
    • 8 maps to index 2
    • 6 maps to index 1
    • 5 maps to index 0

Count swaps for [2, 1, 0]:

  • At index 0: t[0] = 2 ≠ 0, swap with t[2][0, 1, 2]
  • At index 1: t[1] = 1 = 1, already correct
  • At index 2: t[2] = 2 = 2, already correct
  • Result: 1 swap needed

Step 4: Calculate Total

Total swaps = 0 (level 0) + 1 (level 1) + 1 (level 2) = 2 swaps

The algorithm correctly identifies that we need:

  1. One swap at level 1 to exchange 7 and 3
  2. One swap at level 2 to rearrange the values (the cycle detection found that swapping index 0 with index 2 fixes the entire permutation)

This demonstrates how the combination of BFS traversal and cycle decomposition efficiently finds the minimum number of swaps needed to sort each level of the tree.

Solution Implementation

1# Definition for a binary tree node.
2# class TreeNode:
3#     def __init__(self, val=0, left=None, right=None):
4#         self.val = val
5#         self.left = left
6#         self.right = right
7
8from collections import deque
9from typing import Optional, List
10
11class Solution:
12    def minimumOperations(self, root: Optional[TreeNode]) -> int:
13        """
14        Calculate minimum number of operations to make each level of binary tree sorted.
15        Each operation is a swap of two nodes at the same level.
16      
17        Args:
18            root: Root node of the binary tree
19          
20        Returns:
21            Minimum number of swap operations needed
22        """
23      
24        def swap_elements(array: List[int], index_i: int, index_j: int) -> None:
25            """Swap two elements in an array."""
26            array[index_i], array[index_j] = array[index_j], array[index_i]
27      
28        def count_swaps_to_sort(level_values: List[int]) -> int:
29            """
30            Count minimum swaps needed to sort an array using cycle detection.
31          
32            Args:
33                level_values: List of values at a particular tree level
34              
35            Returns:
36                Minimum number of swaps to sort the array
37            """
38            length = len(level_values)
39          
40            # Create mapping from value to its target sorted position
41            sorted_values = sorted(level_values)
42            value_to_target_index = {value: index for index, value in enumerate(sorted_values)}
43          
44            # Replace each value with its target position index
45            for i in range(length):
46                level_values[i] = value_to_target_index[level_values[i]]
47          
48            # Count swaps needed by placing each element at its correct position
49            swap_count = 0
50            for i in range(length):
51                # Keep swapping until element at index i is in correct position
52                while level_values[i] != i:
53                    target_position = level_values[i]
54                    swap_elements(level_values, i, target_position)
55                    swap_count += 1
56          
57            return swap_count
58      
59        # Perform level-order traversal using BFS
60        queue = deque([root])
61        total_operations = 0
62      
63        while queue:
64            # Process all nodes at current level
65            current_level_values = []
66            level_size = len(queue)
67          
68            for _ in range(level_size):
69                node = queue.popleft()
70                current_level_values.append(node.val)
71              
72                # Add children to queue for next level
73                if node.left:
74                    queue.append(node.left)
75                if node.right:
76                    queue.append(node.right)
77          
78            # Add swaps needed for current level to total
79            total_operations += count_swaps_to_sort(current_level_values)
80      
81        return total_operations
82
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 *     int val;
5 *     TreeNode left;
6 *     TreeNode right;
7 *     TreeNode() {}
8 *     TreeNode(int val) { this.val = val; }
9 *     TreeNode(int val, TreeNode left, TreeNode right) {
10 *         this.val = val;
11 *         this.left = left;
12 *         this.right = right;
13 *     }
14 * }
15 */
16class Solution {
17    /**
18     * Calculates the minimum number of operations to sort each level of a binary tree.
19     * Uses BFS to traverse level by level and counts swaps needed to sort each level.
20     * 
21     * @param root The root of the binary tree
22     * @return Total minimum operations needed to sort all levels
23     */
24    public int minimumOperations(TreeNode root) {
25        // Queue for level-order traversal (BFS)
26        Deque<TreeNode> queue = new ArrayDeque<>();
27        queue.offer(root);
28        int totalOperations = 0;
29      
30        // Process each level of the tree
31        while (!queue.isEmpty()) {
32            List<Integer> currentLevel = new ArrayList<>();
33            int levelSize = queue.size();
34          
35            // Extract all nodes at current level and add their children to queue
36            for (int i = 0; i < levelSize; i++) {
37                TreeNode currentNode = queue.poll();
38                currentLevel.add(currentNode.val);
39              
40                // Add left child if exists
41                if (currentNode.left != null) {
42                    queue.offer(currentNode.left);
43                }
44                // Add right child if exists
45                if (currentNode.right != null) {
46                    queue.offer(currentNode.right);
47                }
48            }
49          
50            // Calculate minimum swaps needed for this level
51            totalOperations += calculateMinimumSwaps(currentLevel);
52        }
53      
54        return totalOperations;
55    }
56  
57    /**
58     * Calculates the minimum number of swaps needed to sort a list.
59     * Uses cycle detection approach to find minimum swaps.
60     * 
61     * @param values List of integers to be sorted
62     * @return Minimum number of swaps needed
63     */
64    private int calculateMinimumSwaps(List<Integer> values) {
65        int size = values.size();
66      
67        // Create sorted version of the list
68        List<Integer> sortedValues = new ArrayList<>(values);
69        sortedValues.sort((a, b) -> a - b);
70      
71        // Map each value to its target position in sorted array
72        Map<Integer, Integer> valueToTargetIndex = new HashMap<>();
73        for (int i = 0; i < size; i++) {
74            valueToTargetIndex.put(sortedValues.get(i), i);
75        }
76      
77        // Create array with target positions for each element
78        int[] targetPositions = new int[size];
79        for (int i = 0; i < size; i++) {
80            targetPositions[i] = valueToTargetIndex.get(values.get(i));
81        }
82      
83        // Count swaps by placing each element in its correct position
84        int swapCount = 0;
85        for (int i = 0; i < size; i++) {
86            // Keep swapping until element at position i is correct
87            while (targetPositions[i] != i) {
88                swap(targetPositions, i, targetPositions[i]);
89                swapCount++;
90            }
91        }
92      
93        return swapCount;
94    }
95  
96    /**
97     * Swaps two elements in an array.
98     * 
99     * @param array The array containing elements to swap
100     * @param firstIndex Index of first element
101     * @param secondIndex Index of second element
102     */
103    private void swap(int[] array, int firstIndex, int secondIndex) {
104        int temp = array[firstIndex];
105        array[firstIndex] = array[secondIndex];
106        array[secondIndex] = temp;
107    }
108}
109
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 *     int val;
5 *     TreeNode *left;
6 *     TreeNode *right;
7 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14    int minimumOperations(TreeNode* root) {
15        // Initialize queue for level-order traversal
16        queue<TreeNode*> nodeQueue;
17        nodeQueue.push(root);
18        int totalOperations = 0;
19      
20        // Lambda function to calculate minimum swaps needed to sort an array
21        auto calculateMinSwaps = [](vector<int>& levelValues) {
22            int size = levelValues.size();
23          
24            // Create a sorted copy of the level values
25            vector<int> sortedValues(levelValues.begin(), levelValues.end());
26            sort(sortedValues.begin(), sortedValues.end());
27          
28            // Map each value to its target position in sorted array
29            unordered_map<int, int> valueToTargetIndex;
30            for (int i = 0; i < size; ++i) {
31                valueToTargetIndex[sortedValues[i]] = i;
32            }
33          
34            // Replace each value with its target index
35            for (int i = 0; i < size; ++i) {
36                levelValues[i] = valueToTargetIndex[levelValues[i]];
37            }
38          
39            // Count minimum swaps using cycle detection
40            int swapCount = 0;
41            for (int i = 0; i < size; ++i) {
42                // Keep swapping until element is in correct position
43                while (levelValues[i] != i) {
44                    swap(levelValues[i], levelValues[levelValues[i]]);
45                    ++swapCount;
46                }
47            }
48          
49            return swapCount;
50        };
51      
52        // Process tree level by level
53        while (!nodeQueue.empty()) {
54            vector<int> currentLevelValues;
55            int levelSize = nodeQueue.size();
56          
57            // Process all nodes at current level
58            for (int i = 0; i < levelSize; ++i) {
59                TreeNode* currentNode = nodeQueue.front();
60                nodeQueue.pop();
61              
62                // Add current node's value to level values
63                currentLevelValues.emplace_back(currentNode->val);
64              
65                // Add children to queue for next level
66                if (currentNode->left) {
67                    nodeQueue.push(currentNode->left);
68                }
69                if (currentNode->right) {
70                    nodeQueue.push(currentNode->right);
71                }
72            }
73          
74            // Add minimum swaps needed for current level to total
75            totalOperations += calculateMinSwaps(currentLevelValues);
76        }
77      
78        return totalOperations;
79    }
80};
81
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 *     val: number
5 *     left: TreeNode | null
6 *     right: TreeNode | null
7 *     constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 *         this.val = (val===undefined ? 0 : val)
9 *         this.left = (left===undefined ? null : left)
10 *         this.right = (right===undefined ? null : right)
11 *     }
12 * }
13 */
14
15/**
16 * Calculates the minimum number of operations needed to make each level of a binary tree sorted.
17 * Uses BFS to traverse the tree level by level and counts swaps needed to sort each level.
18 * 
19 * @param root - The root node of the binary tree
20 * @returns The total number of swap operations needed
21 */
22function minimumOperations(root: TreeNode | null): number {
23    // Initialize queue for BFS traversal
24    const queue: TreeNode[] = [root!];
25    let totalOperations: number = 0;
26  
27    // Process tree level by level
28    while (queue.length > 0) {
29        const levelSize: number = queue.length;
30        const currentLevelValues: number[] = [];
31      
32        // Extract all nodes from current level and add their children to queue
33        for (let i = 0; i < levelSize; i++) {
34            const currentNode: TreeNode = queue.shift()!;
35            currentLevelValues.push(currentNode.val);
36          
37            // Add left child to queue if it exists
38            if (currentNode.left) {
39                queue.push(currentNode.left);
40            }
41          
42            // Add right child to queue if it exists
43            if (currentNode.right) {
44                queue.push(currentNode.right);
45            }
46        }
47      
48        // Count minimum swaps needed to sort current level using selection sort
49        for (let i = 0; i < levelSize - 1; i++) {
50            let minIndex: number = i;
51          
52            // Find the minimum element in the unsorted portion
53            for (let j = i + 1; j < levelSize; j++) {
54                if (currentLevelValues[j] < currentLevelValues[minIndex]) {
55                    minIndex = j;
56                }
57            }
58          
59            // Swap if minimum element is not already in correct position
60            if (i !== minIndex) {
61                [currentLevelValues[i], currentLevelValues[minIndex]] = 
62                    [currentLevelValues[minIndex], currentLevelValues[i]];
63                totalOperations++;
64            }
65        }
66    }
67  
68    return totalOperations;
69}
70

Time and Space Complexity

Time Complexity: O(n × log n) where n is the total number of nodes in the binary tree.

The algorithm performs a level-order traversal of the tree using BFS. For each level:

  • We traverse all nodes at that level once to collect their values: O(k) where k is the number of nodes at that level
  • Inside function f, we sort the values to create the mapping: O(k × log k)
  • We create a mapping dictionary: O(k)
  • We transform the array using the mapping: O(k)
  • We perform cycle detection and swapping to count minimum swaps: O(k)

Since we process each level separately and the dominant operation per level is sorting (O(k × log k)), and considering that the sum of all nodes across all levels equals n, the overall time complexity is O(n × log n).

Space Complexity: O(n)

The space is used for:

  • The BFS queue which can hold at most O(w) nodes where w is the maximum width of the tree (in the worst case of a complete binary tree, w = n/2)
  • The temporary array t for each level: O(w)
  • The mapping dictionary m in function f: O(k) where k is the number of nodes at the current level
  • The sorted array created during sorting: O(k)

Since the maximum width of a binary tree is O(n) in the worst case, the overall space complexity is O(n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Modifying the Original Array During Discretization

One critical pitfall in this solution is that the count_swaps_to_sort function modifies the input array level_values in-place during the discretization step. This can cause issues if:

  • You need to use the original values later
  • You're debugging and want to trace the original level values
  • The function is called multiple times with the same array reference

The Problem:

# This modifies level_values directly!
for i in range(length):
    level_values[i] = value_to_target_index[level_values[i]]

Solution: Create a copy of the array before modification:

def count_swaps_to_sort(level_values: List[int]) -> int:
    length = len(level_values)
  
    # Create a working copy to avoid modifying the original
    working_array = level_values.copy()
  
    # Create mapping from value to its target sorted position
    sorted_values = sorted(working_array)
    value_to_target_index = {value: index for index, value in enumerate(sorted_values)}
  
    # Replace each value with its target position index in the copy
    for i in range(length):
        working_array[i] = value_to_target_index[level_values[i]]
  
    # Count swaps on the working copy
    swap_count = 0
    for i in range(length):
        while working_array[i] != i:
            target_position = working_array[i]
            swap_elements(working_array, i, target_position)
            swap_count += 1
  
    return swap_count

2. Handling Duplicate Values

Although the problem states that node values are unique, a common pitfall when adapting this algorithm for general use is not handling duplicates properly. If duplicates exist, the discretization mapping {value: index} would overwrite indices for duplicate values.

The Problem:

# This fails with duplicates like [3, 5, 3, 4]
m = {v: i for i, v in enumerate(sorted(t))}  # Second 3 overwrites the first

Solution: Use a more sophisticated mapping that tracks multiple indices:

def count_swaps_with_duplicates(level_values: List[int]) -> int:
    n = len(level_values)
    # Create pairs of (value, original_index) to handle duplicates
    indexed_values = [(val, i) for i, val in enumerate(level_values)]
    sorted_pairs = sorted(indexed_values)
  
    # Create position mapping
    position = [0] * n
    for target_idx, (_, original_idx) in enumerate(sorted_pairs):
        position[original_idx] = target_idx
  
    # Count swaps using position array
    swap_count = 0
    for i in range(n):
        while position[i] != i:
            target = position[i]
            position[i], position[target] = position[target], position[i]
            swap_count += 1
  
    return swap_count

3. Empty Tree or Single Node Edge Cases

While the current implementation handles these correctly, it's easy to forget edge cases when modifying the code:

Potential Issue: Not checking if root is None or handling single-node trees efficiently.

Best Practice: Add explicit checks for clarity:

def minimumOperations(self, root: Optional[TreeNode]) -> int:
    # Handle empty tree
    if not root:
        return 0
  
    # Single node tree needs no swaps
    if not root.left and not root.right:
        return 0
  
    # Continue with BFS...

4. Integer Overflow in Cycle Detection

When working with very large trees or adapting this for different data types, the cycle detection loop could theoretically run infinitely if there's a bug in the discretization logic.

Prevention: Add a safety check:

def count_swaps_to_sort(level_values: List[int]) -> int:
    length = len(level_values)
    max_iterations = length * length  # Safety limit
    iterations = 0
  
    # ... discretization code ...
  
    swap_count = 0
    for i in range(length):
        while working_array[i] != i:
            if iterations > max_iterations:
                raise ValueError("Cycle detection exceeded maximum iterations")
            target_position = working_array[i]
            swap_elements(working_array, i, target_position)
            swap_count += 1
            iterations += 1
  
    return swap_count
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

How many ways can you arrange the three letters A, B and C?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More