Your approach is quite reasonable. Separate the data, process them separately, and combine them at the end. Can we improve the code? Absolutely. Can space and time be improved? Yes, to a point.
You open a Scanner
at the start, and manually .close()
it at the end. This is error-prone. If sc.nextInt()
raises an exception due to illegal input, this block of code will exit without reaching the .close()
statement, and the scanner will be left open. It is better to use a try-with-resources statement to open Closeable
resources, and have the resource closed properly whether or not the block completes normally or raises an exception.
try(Scanner sc = new Scanner(System.in) {
// Use the scanner here
}
// Scanner is automatically closes when execution exits the above block.
Storage space:
int size = sc.nextInt();
List<Integer> list = new ArrayList<>();
An ArrayList<>
is a fantastic storage container. You can add items to it, and it will automatically grow as needed. But it can be inefficient in two ways.
First, although it will grow as required, this regrowth is both time-consuming and memory consuming. It takes time to allocate the memory, and additional time to copy entries from the original to the new storage area. If each reallocation doubles the storage space, then during the regrowth at least 50% extra space is allocated; reallocating the storage from 100 to 200 entries temporarily requires 100+200=300 entries of space! Finally, if only 101 storage locations were actually needed, if not explicitly freed by the application, the extra 99 entries remain allocated.
This is all completely unnecessary. You've just read in the number of values the ArrayList<>
will contain. Why not use that value to precisely allocate an ArrayList<>
of the proper capacity?
int size = sc.nextInt();
List<Integer> list = new ArrayList<>(size);
No reallocations. No wasted space. Except...
The second inefficiency comes from storing primitive types in generic containers. You can't just store an int
, the value has to be first "boxed" into an Integer
object, which takes both time and additional memory.
Since you are storing int
values, and the exact number is known ahead of time, the advantages of the flexibility of ArrayList<>
are not needed. This could be done with an int[]
array.
int size = sc.nextInt();
int[] list = new int[size];
for (int i = 0; i < size; i++) {
list[i] = sc.nextInt();
}
Since Integer
objects are not being stored, this takes significantly less memory, and creates only one heap object, instead of size+1
heap objects. It is also faster, since access an array doesn't required the overhead of the add()
or get()
function calls.
Similar comments will, of course, apply to the sortListOrder()
method. While you don't know the number of elements which will be stored in either oddList
or evenList
, you do know the maximum: when all the numbers are odd or even. You can pre-allocate for this worst case:
List<Integer> oddList = new ArrayList<>(list.size());
List<Integer> evenList = new ArrayList<>(list.size());
While each list may be larger than needed, we know exactly \$O(2N)\$ memory is required. The advantage here is time; no ArrayList<>
resizing is required.
You sort the evenList
. No complaints there.
You sort the oddList
. And then you reverse the list the hard way, by allocating yet another ArrayList<>
without pre-sizing it (new ArrayList<>(list.size())
since you end up adding the even numbers into it as well), iterating over oddList
, and .add()
-ing the items one-at-a-time.
Better (as mentioned by @mtj) would be using Collections.reverse(oddList);
to reverse oddList
in-place without creating another temporary list.
Perhaps even better would be to sort the list using a comparator which produced the list in reverse order:
Collections.sort(oddList, Collections.reverseOrder());
Then, the sorted even list is appended to the reverse sorted odd list, using .addAll()
. Finally, the result is returned using return reversedList;
, which is odd because the function is supposed to be sorting a list of numbers, not reversing them, so perhaps a better variable name is in order.
Can we improve this further? Certainly. This can be done in-place, with no additional storage required.
- partition the list into
k
odd elements at the start, and N-k
even elements at the end.
- sort the sublist of odd elements
list.subList(0, k)
using Collections.reverseOrder()
.
- sort the sublist of even elements
list.subList(k, N)
No joining of the two sorted lists is required, because the lists were never separated.
Alternately, it can be done with a custom sort operation, which sorts:
- an odd number always "less than" an even number,
- if both numbers are even: in ascending order
- if both numbers are odd: in descending order
Or it can be done using appropriate preprocessing and post processing, to achieve the desired ordering. For instance, convert all odd numbers to negative numbers, sort in the list in ascending order, and then turn the negative numbers back into positive ones. For example, done using a Stream
:
jshell> IntStream.of(0, 4, 5, 3, 7, 2, 1).
...> map(i -> i%2 != 0 ? -i : i). // 0, 4, -5, -3, -7, 2, -1
...> sorted(). // -7, -5, -3, -1, 0, 2, 4
...> map(Math::abs). // 7, 5, 3, 1, 0, 2, 4
...> toArray();
$1 ==> int[7] { 7, 5, 3, 1, 0, 2, 4 }