simonmukiiza
simonmukiiza

Reputation: 39

How to improve the time complexity of this algorithm that has nested loops?

Input:

Given an array of integers T = [t0, t1, t2, ... tk] representing a row where each element is the maximum waiting time. And an array E = [e0, e1, e2, ... ei] representing all possible expiration times. Finally, an integer K is given which is the maximum size of a container.

Problem:

For each expiration time E, it is necessary to obtain the position of the last element T that was able to enter the container of size K, and each waiting time in T must be greater than or equal to the expiration time E to be able to enter the container.

Example Case 1:

Input:

K = 2; T = [1, 4, 4, 3, 1, 2, 6]; E = [1, 2, 3, 4, 5, 6, 7]

Output:

Kth = [2, 3, 3, 3, 0, 0, 0]

Explanation:

In E[0], the expiration time is 1, so the 2nd in row T will be the last to enter the container, so Kth[0] = 2nd;

In E[1], the expiration time is 2, so the 3rd in row T will be the last to enter the container since the 1st element has expired, so Kth[1] = 3rd;

In E[2], the expiration time is 3, so the 3rd in row T will be the last to enter the container since the 1st element has expired, so Kth[2] = 3rd;

In E[3], the expiration time is 4, so the 3rd in row T will be the last to enter the container since the 1st element has expired, so Kth[3] = 3rd;

In E[4], the expiration time is 5, in this case almost all elements of T except the last one have expired, however, as it was not possible to complete the container, it must return position 0, therefore Kth[4] = 0;

And so on for E[5] and E[6].

Here is the code I've been able to come up with but it runs in O(E * T) time which is too slow for the performance constraints:

public static List<Integer> kthElement(int k, List<Integer> T, List<Integer> E) {
    List<Integer> kthElements = new ArrayList<>();

    for (int i = 0; i < E.size(); i++) {
        System.out.println(E.get(i));

        int currentElement = 0;
        int elementsFilled = 0;

        for (int j = 0; j < T.size(); j++) {
            if (elementsFilled >= k || k > T.size()) {
                break;
            }

            if (T.get(j) >= E.get(i)) {
                elementsFilled++;
                currentElement = j + 1;
            }
        }

        if (elementsFilled >= k) {
            kthElements.add(currentElement);
        } else {
            kthElements.add(0);
        }

    }

    return kthElements;
}

How can I improve the performance of this algorithm?

Upvotes: 1

Views: 137

Answers (1)

John
John

Reputation: 827

I think you can easily do this in O(E + T) instead of O(E x T).

The loop is quite simple, and will appear to be O(E x T) at first glance because of the nesting, but the inner loop never gets reset within the outer loop so it is indeed O(E + T).

I will assume that E < 65536 in the following code.

    List<Integer> out = new ArrayList<>();
    int inPos = 0;
    int kCnt = 0;
    int last = 0;
    int[] kTracker = new int[65536];
    for (int i = 0; i < E.size(); i++)
    {
        // Remove Expired
        kCnt -= kTracker[i];

        // Fill Container
        while (kCnt < K && inPos < T.length)
        {
            int exp = i + T.get(inPos);
            if (exp < kTracker.length)
            {
                // don't bother tracking if > E.max, as it won't affect the output.
                kTracker[exp]++;
            }
            last = inPos;
            kCnt++;
            inPos++;
        }

        // record output
        out.add(last);
    }

If there aren't guarantees on E.max, but there are guarantees on T.max, and we constrain memory, then we can implement a rolling array instead.

For example, if T.max = 10, we can use the below code to replace bits from the code above...

// Creating a rolling kTracker
int kTrackerNow = 0;
int[] kTracker = new int[10];
...
// Expiring elements
kCnt -= kTracker[kTrackerNow];
kTracker[kTrackerNow] = 0;

// Progressing to the next time period
kTrackerNow = (kTrackerNow + 1) % kTracker.length;

// Tracking a new item from T[inPos]
kTracker[(kTrackerNow + T[inPos]) % kTracker.length]++;

Finally, if we don't have any guarantees on the input, we can use HashMap<> to replace the tracking array. As the HashMap will perform memory allocation for each and every element, it will be far slower than the above 2 solutions, but it can deal with all kinds of input without restriction.

    List<Integer> out = new ArrayList<>();
    int inPos = 0;
    int kCnt = 0;
    int last = 0;
    Map<Integer, Integer> kTracker = new HashMap<>();
    for (int i = 0; i < E.size(); i++)
    {
        // Remove Expired
        Integer removed = kTracker.remove(i);
        if (removed != null)
        {
            kCnt -= removed;
        }

        // Fill Container
        while (kCnt < K && inPos < T.length)
        {
            kTracker.put(i + T.get(inPos), kTracker.getOrDefault(i + T.get(inPos), 0) + 1);
            last = inPos;
            kCnt++;
            inPos++;
        }

        // record output
        out.add(last);
    }

Upvotes: 1

Related Questions