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.