1. Introduction
In this tutorial, we’ll show how to arrange a numeric array so that its negative elements come before its positive ones.
In doing so, we’ll require the solution to keep the relative order of the same-sign elements.
2. Problem Statement
Let’s say we have an array . Our goal is to rearrange it into so that all elements for are negative, and all that come after , if any, are positive.
Simultaneously, we want to keep the relative order in the negative and positive sub-arrays. So, for any , it should hold that and or that , but was to the left of in the original array.
For example, if , the correct rearrangement is .
Since , we’ll include any element equal to zero in the positive sub-array.
3. The Solution With Two Queues
We can start with two empty queues: for the positive and for the negative elements of . We populate them by iterating over and placing its elements into the queues based on their signs.
After that, we append to to get :
algorithm SolutionWithQueues(a):
// INPUT
// a = a numeric array [a1, a2, ..., an]
// OUTPUT
// The array containing the elements of a sorted according to their signs
// while keeping the relative order of the same-sign numbers.
queue_negative , queue_positive <- make two empty queues
for i <- 1, 2, ..., n:
if a[i] < 0:
Put a[i] into queue_negative
else:
Put a[i] into queue_positive
a` <- append queue_positive to queue_negative
return a`
Since we iterate over the input array once and concatenate two queues whose sizes sum to , the time complexity is . The only requirement is that storing a number in a queue be a constant-time operation.
The space complexity is also linear because we need space for and memory for queues.
4. The Solution With Merging
If we don’t want to take extra memory, we can rearrange the array using an algorithm similar to MergeSort.
First, we split the array into pairs of consecutive elements:
If is odd, we’ll let be the last sub-array. For simplicity, we’ll assume is even since that won’t affect the correctness of this approach but will make it easier to explain.
In the next step, we swap the elements in each sub-array if the first one is positive and the second negative. So, we ensure that the signs are:
Afterward, we merge and arrange consecutive pairs of previously processed sub-arrays. That way, we arrange sub-arrays with four elements:
Processing larger and larger sub-arrays, we eventually get to rearrange the entire input array .
4.1. Merging Two Sub-Arrays
Let’s focus on two consecutive sub-arrays of in an iteration of the rearrangement process:
Let be the left’s sub-array negative part, and let contain its positive elements. Similar goes for the right sub-array and its parts and .
To merge the two sub-arrays to be in line with our requirements, we need to swap with while preserving relative orders in both:
First, we reverse and separately. Then, we reverse them together. To see why that works, let and .
So, we start with this:
If we first reverse the sub-arrays separately, we get:
Reversing them together gives us:
which is what we want to achieve.
4.2. Iterative MergeArrange
Let () be the sub-array containing the elements of starting with and ending with (if , it stops at ). Here’s the pseudocode of our iterative MergeSort-like approach we call MergeArrange:
algorithm MergeArrange(a):
// INPUT
// a = an array of numbers
// OUTPUT
// The array containing the elements of a sorted according to their signs
// while keeping the relative order of the same-sign numbers.
l <- 1 // the length of sub-arrays
m <- ceiling(n / 2) // the number of sub-arrays
while m > 1:
for i <- 1, 1 + 2l, 1 + 4l, ..., 1 + 2l floor(n / 2ℓ):
Reverse(a[i : i + l - 1])
Reverse(a[i + l : i + 2l - 1])
Reverse(a[i : i + 2l - 1])
ℓ <- 2l
m <- ceiling(n / l)
return a
algorithm Reverse(a, i, j):
// INPUT
// a = an array to be reversed
// i = starting index of the sub-array
// j = ending index of the sub-array
// OUTPUT
// None
left <- i
right <- j
while left < right:
swap a[left] with a[right]
left <- left + 1
right <- right - 1
We start with sub-arrays of length to sort the consecutive pairs. Then, we double the length iteratively until covering the entire array.
In each iteration, the left sub-arrays for merging start at indices for some integer since two consecutive sub-arrays together contain elements. The corresponding right sub-arrays start at elements to the right.
4.3. Example
Let’s use the example from the start:
In the first iteration, , so we have this split:
After merging, we get:
In the second iteration, , so we merge and arrange the consecutive pairs:
In the third iteration, , so we process the remaining two sub-arrays of length 4:
Now, we get . That means there’s only one sub-array, i.e., the entire array. Therefore, we stop since it’s in the order we want it to be.
4.4. Complexity
There are at most iterations, each of which makes at most swaps. So, the time complexity is .
We use only a constant number of auxiliary variables for looping and swapping, so the extra space complexity is . However, the total space complexity is still since we need memory to store the initial array.
So, our MergeArrange algorithm has the same complexity as MergeSort. That isn’t a surprise because it works like bottom-up MergeSort sorting its input array using instead of the actual values .
In fact, any stable sorting algorithm using to sort the will do the job.
5. Conclusion
In this article, we showed two ways to rearrange a numeric array so that its negative elements get before its positive ones and those equal to zero, keeping the relative order of the elements in the same sub-array.
Our first solution uses two queues and runs in time and space. The algorithm we derived from MergeSort uses additional memory but runs in time.