1998. GCD Sort of an Array
Problem Description
You have an integer array nums
that you want to sort in non-decreasing order (ascending order). However, you can only swap elements under a specific condition.
The swapping rule is: You can swap two elements nums[i]
and nums[j]
only if their greatest common divisor (GCD) is greater than 1. This means the two numbers must share at least one common factor other than 1.
For example:
- You can swap 6 and 9 because
gcd(6, 9) = 3 > 1
- You can swap 4 and 10 because
gcd(4, 10) = 2 > 1
- You cannot swap 3 and 7 because
gcd(3, 7) = 1
You can perform any number of these valid swaps. The question asks whether it's possible to sort the entire array into non-decreasing order using only these constrained swaps.
The key insight is that if two numbers share a common factor greater than 1, they can be swapped. Additionally, if number A can swap with B, and B can swap with C, then A and C are in the same "connected component" - they can eventually be rearranged among themselves through a series of swaps.
The solution uses Union-Find (Disjoint Set Union) to group numbers that can be swapped directly or indirectly. First, it finds all prime factors for each number in the array. Then it unions all numbers that share common prime factors. Finally, it checks if each number in the original array can reach its target position in the sorted array through the swap connections.
The function returns true
if the array can be sorted using the allowed swaps, and false
otherwise.
Intuition
The first observation is that if two numbers share a common factor greater than 1, they can be swapped. But this creates a transitive relationship - if number A can swap with B (they share factor 2), and B can swap with C (they share factor 3), then A and C can eventually swap positions through B as an intermediary.
This leads to thinking about connected components. Numbers that can swap directly or indirectly form groups. Within each group, we can rearrange the numbers in any order we want through a series of swaps.
For example, consider [6, 10, 15]
:
- 6 and 10 share factor 2, so they can swap
- 6 and 15 share factor 3, so they can swap
- 10 and 15 share factor 5, so they can swap
- All three numbers are in the same group and can be arranged in any order
The key insight is: for the array to be sortable, each number must be able to reach its target position in the sorted array. This means that for each position, the number currently there and the number that should be there (in the sorted array) must be in the same connected component.
To efficiently track these connections, we can use Union-Find. But how do we connect numbers? We could check every pair of numbers for gcd > 1
, but that's expensive.
A better approach: two numbers share a factor greater than 1 if and only if they share at least one prime factor. So we can:
- Find all prime factors for each number
- Group numbers by their prime factors
- Numbers sharing any prime factor belong to the same component
Finally, we check if the sorting is possible by verifying that each number can reach its sorted position through the connection network we've built.
Learn more about Union Find, Math and Sorting patterns.
Solution Approach
The solution uses Union-Find (Disjoint Set Union) data structure combined with prime factorization to solve this problem efficiently.
Step 1: Initialize Union-Find Structure
n = 10**5 + 10
p = list(range(n))
We create a parent array p
where initially each element is its own parent. The size 10^5 + 10
covers the maximum possible value in the constraints.
Step 2: Build Prime Factor Mapping Using Sieve
f = defaultdict(list)
mx = max(nums)
for i in range(2, mx + 1):
if f[i]:
continue
for j in range(i, mx + 1, i):
f[j].append(i)
This implements a modified Sieve of Eratosthenes. For each number from 2 to the maximum value in nums
:
- If
f[i]
is empty, theni
is prime - We mark all multiples of
i
(includingi
itself) as havingi
as a prime factor - After this loop,
f[num]
contains all prime factors ofnum
Step 3: Union Numbers with Common Prime Factors
def find(x):
if p[x] != x:
p[x] = find(p[x])
return p[x]
for i in nums:
for j in f[i]:
p[find(i)] = find(j)
The find
function implements path compression for efficient lookups. For each number in nums
, we union it with all its prime factors. This cleverly connects all numbers sharing any prime factor:
- If numbers A and B both have prime factor P, both will be unioned with P
- This puts A and B in the same component
Step 4: Verify Sortability
s = sorted(nums)
for i, num in enumerate(nums):
if s[i] != num and find(num) != find(s[i]):
return False
return True
We create the sorted version of the array. Then for each position:
- If the current number
nums[i]
differs from what should be theres[i]
- We check if they're in the same connected component using
find
- If they're not connected (can't be swapped), return
False
- If all positions can be satisfied, return
True
The time complexity is O(n log n + m log m)
where n
is the array length and m
is the maximum value, due to sorting and the sieve. The space complexity is O(m)
for the Union-Find structure and prime factor storage.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with the array nums = [6, 10, 15]
.
Step 1: Initialize Union-Find We create a parent array where each element points to itself initially:
p[6] = 6
,p[10] = 10
,p[15] = 15
, etc.
Step 2: Find Prime Factors Using the sieve approach, we find prime factors for each number up to 15:
f[6] = [2, 3]
(6 = 2 × 3)f[10] = [2, 5]
(10 = 2 × 5)f[15] = [3, 5]
(15 = 3 × 5)
Step 3: Union Numbers with Their Prime Factors For each number, we union it with its prime factors:
For 6
with prime factors [2, 3]
:
- Union 6 with 2:
p[6] = 2
- Union 6 with 3:
p[2] = 3
(since find(6) = 2)
For 10
with prime factors [2, 5]
:
- Union 10 with 2: Since find(2) = 3, we set
p[10] = 3
- Union 10 with 5:
p[3] = 5
(connecting the whole component)
For 15
with prime factors [3, 5]
:
- Union 15 with 3: Since find(3) = 5, we set
p[15] = 5
- Union 15 with 5: Already connected
Now all numbers 6, 10, and 15 are in the same component (all eventually point to 5).
Step 4: Check if Sorting is Possible
- Original:
[6, 10, 15]
- Sorted:
[6, 10, 15]
(already sorted!) - Position 0:
nums[0] = 6
,sorted[0] = 6
- same number, OK - Position 1:
nums[1] = 10
,sorted[1] = 10
- same number, OK - Position 2:
nums[2] = 15
,sorted[2] = 15
- same number, OK
Return true
- the array can be sorted (it's already sorted).
Let's try a different example where swapping is needed: nums = [10, 6, 15]
Using the same Union-Find structure from above:
- Original:
[10, 6, 15]
- Sorted:
[6, 10, 15]
- Position 0:
nums[0] = 10
,sorted[0] = 6
- find(10) = 5, find(6) = 5 - same component, can swap ✓
- Position 1:
nums[1] = 6
,sorted[1] = 10
- find(6) = 5, find(10) = 5 - same component, can swap ✓
- Position 2:
nums[2] = 15
,sorted[2] = 15
- same number, OK
Return true
- the array can be sorted through valid swaps.
For a negative example, consider nums = [3, 7]
:
f[3] = [3]
(3 is prime)f[7] = [7]
(7 is prime)- After unions: 3 and 7 remain in separate components
- Original:
[3, 7]
, Sorted:[3, 7]
- already sorted, returnstrue
But if we had nums = [7, 3]
:
- Original:
[7, 3]
, Sorted:[3, 7]
- Position 0:
nums[0] = 7
,sorted[0] = 3
- find(7) = 7, find(3) = 3 - different components, cannot swap ✗
- Return
false
- cannot sort this array
Solution Implementation
1class Solution:
2 def gcdSort(self, nums: List[int]) -> bool:
3 # Initialize Union-Find parent array for all possible values
4 max_value = 10**5 + 10
5 parent = list(range(max_value))
6
7 # Dictionary to store prime factors for each number
8 prime_factors = defaultdict(list)
9
10 # Find the maximum value in nums to limit our factorization
11 max_num = max(nums)
12
13 # Sieve-like approach to find prime factors for all numbers up to max_num
14 for prime_candidate in range(2, max_num + 1):
15 # Skip if we've already found factors for this number
16 if prime_factors[prime_candidate]:
17 continue
18
19 # Mark all multiples of this prime
20 for multiple in range(prime_candidate, max_num + 1, prime_candidate):
21 prime_factors[multiple].append(prime_candidate)
22
23 # Union-Find find function with path compression
24 def find(x):
25 if parent[x] != x:
26 parent[x] = find(parent[x]) # Path compression
27 return parent[x]
28
29 # Union all numbers that share prime factors
30 for number in nums:
31 # Connect this number with all its prime factors
32 for prime_factor in prime_factors[number]:
33 parent[find(number)] = find(prime_factor)
34
35 # Check if the array can be sorted
36 sorted_nums = sorted(nums)
37
38 # Verify each position: if elements differ, they must be in the same component
39 for index, original_num in enumerate(nums):
40 target_num = sorted_nums[index]
41 # If numbers are different and not in the same connected component, cannot sort
42 if target_num != original_num and find(original_num) != find(target_num):
43 return False
44
45 return True
46
1class Solution {
2 private int[] parent;
3
4 public boolean gcdSort(int[] nums) {
5 // Initialize Union-Find structure with maximum possible value
6 int maxValue = 100010;
7 parent = new int[maxValue];
8
9 // Initialize each element as its own parent (self-representative)
10 for (int i = 0; i < maxValue; i++) {
11 parent[i] = i;
12 }
13
14 // Map to store prime factors for each number
15 Map<Integer, List<Integer>> primeFactorsMap = new HashMap<>();
16
17 // Find the maximum value in the array to limit our computation
18 int maxNum = 0;
19 for (int num : nums) {
20 maxNum = Math.max(maxNum, num);
21 }
22
23 // Sieve-like approach to find prime factors for all numbers up to maxNum
24 for (int prime = 2; prime <= maxNum; prime++) {
25 // Skip if we've already processed this number as a composite
26 if (primeFactorsMap.containsKey(prime)) {
27 continue;
28 }
29
30 // Mark all multiples of this prime and add prime to their factor list
31 for (int multiple = prime; multiple <= maxNum; multiple += prime) {
32 primeFactorsMap.computeIfAbsent(multiple, k -> new ArrayList<>()).add(prime);
33 }
34 }
35
36 // Union numbers that share common prime factors
37 for (int num : nums) {
38 List<Integer> primeFactors = primeFactorsMap.get(num);
39 if (primeFactors != null) {
40 for (int primeFactor : primeFactors) {
41 // Union the number with its prime factor
42 parent[find(num)] = find(primeFactor);
43 }
44 }
45 }
46
47 // Create a sorted copy of the original array
48 int[] sortedArray = new int[nums.length];
49 System.arraycopy(nums, 0, sortedArray, 0, nums.length);
50 Arrays.sort(sortedArray);
51
52 // Check if elements that need to be swapped are in the same connected component
53 for (int i = 0; i < nums.length; i++) {
54 // If element is not in its sorted position and not connected to its target position
55 if (sortedArray[i] != nums[i] && find(nums[i]) != find(sortedArray[i])) {
56 return false;
57 }
58 }
59
60 return true;
61 }
62
63 // Find operation with path compression for Union-Find
64 int find(int x) {
65 if (parent[x] != x) {
66 // Path compression: make all nodes point directly to root
67 parent[x] = find(parent[x]);
68 }
69 return parent[x];
70 }
71}
72
1class Solution {
2public:
3 vector<int> parent;
4
5 bool gcdSort(vector<int>& nums) {
6 // Initialize Union-Find structure with size 100010
7 int maxSize = 100010;
8 parent.resize(maxSize);
9 for (int i = 0; i < maxSize; ++i) {
10 parent[i] = i;
11 }
12
13 // Find the maximum element in nums
14 int maxElement = 0;
15 for (int num : nums) {
16 maxElement = max(maxElement, num);
17 }
18
19 // Build prime factorization map using Sieve of Eratosthenes approach
20 // primeFactors[i] contains all prime factors of number i
21 unordered_map<int, vector<int>> primeFactors;
22 for (int i = 2; i <= maxElement; ++i) {
23 // Skip if already processed (not a prime)
24 if (!primeFactors[i].empty()) {
25 continue;
26 }
27 // Mark all multiples of i with i as a prime factor
28 for (int j = i; j <= maxElement; j += i) {
29 primeFactors[j].push_back(i);
30 }
31 }
32
33 // Union numbers that share common prime factors
34 // Numbers with GCD > 1 will be in the same connected component
35 for (int num : nums) {
36 for (int primeFactor : primeFactors[num]) {
37 // Union the number with its prime factors
38 parent[find(num)] = find(primeFactor);
39 }
40 }
41
42 // Create sorted version of the original array
43 vector<int> sortedNums = nums;
44 sort(sortedNums.begin(), sortedNums.end());
45
46 // Check if each element can reach its sorted position
47 // Elements can be swapped if they're in the same connected component
48 for (int i = 0; i < nums.size(); ++i) {
49 if (sortedNums[i] != nums[i] && find(sortedNums[i]) != find(nums[i])) {
50 // Element needs to move but can't reach target position
51 return false;
52 }
53 }
54
55 return true;
56 }
57
58 // Find operation with path compression for Union-Find
59 int find(int x) {
60 if (parent[x] != x) {
61 parent[x] = find(parent[x]); // Path compression
62 }
63 return parent[x];
64 }
65};
66
1let parent: number[];
2
3function gcdSort(nums: number[]): boolean {
4 // Initialize Union-Find structure with size 100010
5 const maxSize = 100010;
6 parent = new Array(maxSize);
7 for (let i = 0; i < maxSize; i++) {
8 parent[i] = i;
9 }
10
11 // Find the maximum element in nums
12 let maxElement = 0;
13 for (const num of nums) {
14 maxElement = Math.max(maxElement, num);
15 }
16
17 // Build prime factorization map using Sieve of Eratosthenes approach
18 // primeFactors[i] contains all prime factors of number i
19 const primeFactors: Map<number, number[]> = new Map();
20 for (let i = 2; i <= maxElement; i++) {
21 // Skip if already processed (not a prime)
22 if (primeFactors.has(i)) {
23 continue;
24 }
25 // Mark all multiples of i with i as a prime factor
26 for (let j = i; j <= maxElement; j += i) {
27 if (!primeFactors.has(j)) {
28 primeFactors.set(j, []);
29 }
30 primeFactors.get(j)!.push(i);
31 }
32 }
33
34 // Union numbers that share common prime factors
35 // Numbers with GCD > 1 will be in the same connected component
36 for (const num of nums) {
37 const factors = primeFactors.get(num) || [];
38 for (const primeFactor of factors) {
39 // Union the number with its prime factors
40 parent[find(num)] = find(primeFactor);
41 }
42 }
43
44 // Create sorted version of the original array
45 const sortedNums = [...nums].sort((a, b) => a - b);
46
47 // Check if each element can reach its sorted position
48 // Elements can be swapped if they're in the same connected component
49 for (let i = 0; i < nums.length; i++) {
50 if (sortedNums[i] !== nums[i] && find(sortedNums[i]) !== find(nums[i])) {
51 // Element needs to move but can't reach target position
52 return false;
53 }
54 }
55
56 return true;
57}
58
59// Find operation with path compression for Union-Find
60function find(x: number): number {
61 if (parent[x] !== x) {
62 parent[x] = find(parent[x]); // Path compression
63 }
64 return parent[x];
65}
66
Time and Space Complexity
Time Complexity: O(n * log(log(n)) + m * α(n) * f + m * log(m))
Where:
n
is the maximum value in nums (used as the upper bound for sieve)m
is the length of the nums arrayα(n)
is the inverse Ackermann function (practically constant)f
is the average number of prime factors per number
Breaking down the operations:
- Prime factorization using sieve:
O(n * log(log(n)))
- The nested loops iterate through all numbers up tomx
and mark their factors, which follows a harmonic series pattern similar to the Sieve of Eratosthenes - Union operations:
O(m * f * α(n))
- For each number in nums, we perform union operations for all its prime factors - Sorting:
O(m * log(m))
- Standard sorting of the nums array - Final verification:
O(m * α(n))
- For each element, we perform find operations
The dominant term is typically O(n * log(log(n)))
when n
is large relative to m
.
Space Complexity: O(n + m)
Breaking down the space usage:
- Parent array
p
:O(n)
- stores parent pointers for values up to 10^5 + 10 - Factor dictionary
f
:O(n * log(n))
in worst case - each number can have at mostO(log(n))
prime factors - Sorted array
s
:O(m)
- copy of the input array
The dominant term is O(n)
due to the parent array initialization.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Inefficient Prime Factorization
A common mistake is using naive factorization for each number individually, which leads to timeout for large inputs.
Incorrect approach:
def get_prime_factors(n):
factors = []
for i in range(2, int(n**0.5) + 1):
while n % i == 0:
factors.append(i)
n //= i
if n > 1:
factors.append(n)
return factors
# Then calling this for each number - O(n * sqrt(m))
for num in nums:
factors = get_prime_factors(num)
# ... union operations
Why it fails: This approach has O(n * sqrt(m)) complexity for factorization alone, where n is array length and m is the maximum value. For arrays with many large numbers, this becomes prohibitively slow.
Solution: Use the sieve-based approach shown in the main solution, which pre-computes all prime factors in O(m log m) time once, rather than factorizing each number separately.
2. Incorrect Union-Find Implementation
Forgetting path compression or implementing union incorrectly can cause wrong results or poor performance.
Common mistakes:
# Mistake 1: No path compression
def find(x):
while parent[x] != x:
x = parent[x]
return x
# Mistake 2: Direct assignment without find
for num in nums:
for prime in prime_factors[num]:
parent[num] = prime # Wrong! Should use find()
Solution: Always use path compression and ensure both elements are reduced to their roots before union:
def find(x):
if parent[x] != x:
parent[x] = find(parent[x]) # Path compression
return parent[x]
# Correct union
parent[find(num)] = find(prime)
3. Memory Limit Issues with Large Range
Creating arrays for the full range up to 10^5 when the actual maximum is much smaller wastes memory.
Better approach:
# Dynamically size based on actual maximum
max_num = max(nums)
parent = list(range(max_num + 1))
4. Missing Edge Cases
Not handling special cases like arrays with duplicate numbers or single elements.
Solution: The algorithm naturally handles these cases, but be aware:
- Duplicate numbers will always be in the same component (they share all prime factors)
- Single element arrays always return
True
- Arrays already sorted return
True
(each element is compared with itself)
Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?
Recommended Readings
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Want a Structured Path to Master System Design Too? Don’t Miss This!