Basic Algorithms — Counting Inversions | by Keita Miyaki | Towards Data Science

Even with significant advancement in computing capacities in recent years, it is still costly to operate on large datasets. Writing cost-efficient codes is, thus, one of the critical skills for data scientists. While an algorithm whose cost is linear to the input data size may be not so different from one that costs square, the former offers a huge advantage when the dataset increases to millions.
One basic way to write more efficient algorithms is the split-conquer method, where we write a nested function that applies the same function to the subset of the input recursively. In this article, we will compare split-conquer method against brute-force one in counting inverted pairs in a random array.
We are given an array of numbers without any duplication and asked to count inversions. Inversion is a case in which for two integers i<j, ith element in the array is larger than jth element.
If the array consists of (2,3,8,6,1), we can find five inversions of (2,1), (3,1), (8,6), (8,1), and (6,1).
The simplest and easiest way is to compare all the pairs in the array. With the array length n, we can just compare n(n-1)/2 pairs, and the cost is Θ(n2). Here is a simple coding sample with less than ten lines:
def bf_inversion_count(array):    count = 0    for i in range(len(array)):        j = i + 1        while j < len(array):            if array[i] > array[j]:                count +=1            j += 1    return count
Let’s think of a better and more efficient way. We can modify the merge-sort algorithm. A divide-conquer algorithm would cost:
T(n) = 2T(n/2) + f(n)
where the total cost T(n) is sum of cost for two half-size arrays T(n/2) and the cost to count inversions between two arrays f(n). We should be able to make f(n)=Θ(n) so that T(n)=Θ(nlgn).
Here is my sample code, which is mostly merge-sort code with counting lines added.
def sc_inversion_count(array):        length = len(array)    if length == 1:         return array, 0    if length == 2:        if array[0] > array[1]:            return np.array([array[1], array[0]]), 1        else:            return array, 0        elif length > 2:        array_l = array[:length//2]        array_r = array[length//2:]        array_l_df = sc_inversion_count(array_l)        array_r_df = sc_inversion_count(array_r)        array_l_sorted = array_l_df[0]        array_r_sorted = array_r_df[0]        length_l = len(array_l)        length_r = len(array_r)        count = array_l_df[1] + array_r_df[1]        l = 0        r = 0                sorted_list = []                for i in range(length):            if r == length_r:                sorted_list.append(array_l_sorted[l])                l += 1            elif l == length_l:                sorted_list.append(array_r_sorted[r])                r += 1                                         elif array_l_sorted[l] > array_r_sorted[r]:                sorted_list.append(array_r_sorted[r])                r += 1                count += len(array_l_sorted) - l                            elif array_l_sorted[l] < array_r_sorted[r]:                sorted_list.append(array_l_sorted[l])                l += 1                return np.array(sorted_list), count
Now we can compare how efficient the split-conquer method is versus the brute-force one. As the size of the array increases, obviously split-conquer method offers huge advantages in terms of calculation costs. This result clearly describes the importance of writing algorithms efficiently.