How can I optimize my code from O(n^2) to nlog(n)

Viewed 171

Given an array of numbers, arrange them in a way that yields the largest value. For example, if the given numbers are {54, 546, 548, 60}, the arrangement 6054854654 gives the largest value. And if the given numbers are {1, 34, 3, 98, 9, 76, 45, 4}, then the arrangement 998764543431 gives the largest value.

So, the provided function declaration is

string printLargest(vector<string> &arr)

The solution that I wrote is provided below.

string printLargest(vector<string> &arr) {
    for (int i=0; i<arr.size()-1; i++) {
        for (int j=i+1; j<arr.size(); j++) {
            string y = arr[i] + arr.at(j);
            string z = arr[j] + arr[i];
            if (z>y) swap(arr[j], arr[i]);
        }
    }
    string y="";
    for(string x:arr) y +=x;
    return y;
}

The online compiler says "Time Limit Exceeded" Please optimize your code and submit again. I think my solution take O(n^2). Expected Time Complexity: O(NlogN), How can I optimize my code?

4 Answers

This can be done using std::map


struct classcomp{ // The comparison class that allow std::map to do comparison between keys of type std::string
    bool operator() (const std::string &a, const std::string &b)const {return (a+b>b+a);}
    };
̀
std::string printLargest(std::vector<std::string> &arr) {
    std::map<std::string, unsigned int, classcomp> orderedString;  // a map of the form string : number of occurance in the vector
    for (auto i = arr.begin(); i != arr.end(); i++){  // O(n)
        if (orderedString.count(*i)) orderedString[*i]++;  // O(log(n)) or O(1) depending of the implementation of std::map
        else orderedString.insert(std::pair<std::string, unsigned int>(*i, 1));  // O(log(n)) or O(1) depending of the implementation of std::map
        }
    std::string r="";
    for (auto i = orderedString.begin(); i != orderedString.end(); i++){  //this works since our map container is such that the first element is the highest
        for (unsigned j=0; j < i->second; j++){  //The overall complexity is O(n)
            r+=i->first;
            }
        }
    return r;
    }
̀

The overall complexity is O(mnlog(n)) where m is the maximum length of a string in your vector and n the size of the vector itself

Solving this by hand, what I'd do is to take the numbers which start with the highest digits in their sequences, right? So, in other words, what I'd do by hand is to sort them by this criterion and then appending them.
As soon as you are able to describe the criterion, this becomes nothing more than a sorting algorithm, with the criterion as a custom comparator.

So basically, in the end, the code can look somewhat like:

inline bool better_digits(const string& a, const string& b);

string print_largest(vector<string> data)
{
    std::sort(data.begin(), data.end(), better_digits); // sort
    string result = std::accumulate(data.begin(), data.end(), std::string{}); // append
    return result;
}

In other words, I did the same as you already did, but with a better sorting algorithm, simply trusting that std::sort is efficient (which it is). No need to reinvent the wheel.

Note: the line with std::accumulate requires C++20. Otherwise, simply append by using a loop like you did in your own code.
Also, I removed the reference from the input to avoid the function having a side effect, but if it is allowed to, do that by all means.

The only thing left to do is to define better_digits. For that, we can use what you already did and what TUI lover also used:

inline bool better_digits(const string& a, const string& b)
{
    return a+b > b+a;
}

Note that I haven't compared my variant with that of that of TUI lover. That would prove to be quite interesting. I posted mine because I think it is more readable, but TUI lovers variant might easily be more efficient. (Both are Θ(nlogn), but the overall factor also matters.)

Here is an algorithm in O(ns) where n is the length of the array and s the maximum length of the strings. It uses the trie method. Instead of padding with spaces or zeros, it pads with fake numbers.

If the numbers 544, 54 are present in a group then 54 is equivalent to 545, and should go in front (we pad 54 with a fake 5 in its last digit).

To compare [5, 554, 45],

  • first round (most significant digit), splits it into [5, 554], [45]
  • second round [5, 554], [45]
  • third round [5], [554], [45] (because 5 are padded with fake 5s)

    
    def pad_with_fake(L):
        outL = []
        for x in L:
            while len(str(x)) < maxlen:
                lead = x[0]
                if lead not in fake_digits:
                    x = x + fake_digits[int(lead)]
                else:
                    x = x + lead
            outL.append(x)
        return outL
    
    def arrange_number_at_digit_position(pos,inL):
        outL = []
        for digit in digits:
            subL = []
            for x in inL:
                if str(x)[pos] == digit:
                    subL.append(x)
            if subL != []:
                outL.append(subL)
        return outL
            
    def arrange(inL):
        maxlen = max([len(l) for l in inL])
        i = 0
        outLs = [[inL]]
        while i < maxlen:
            outL = []
            for l in outLs[i]:
                outL = outL + (arrange_number_at_digit_position(i,l))
            outLs = outLs + [outL]
            i = i+1
        return outLs
    
    def main():
        inL = [559, 5, 55, 59, 549, 544, 54]
        L = [str(l) for l in inL]
        digits = [0,1,2,3,4,5,6,7,8,9]
        fake_digits = ['a','b','c','d','e','f','g','h','i','j']
        digits = ['9','j','8','i','7','h','6','g','5','f','4','e','3','d','2','c','1','b','a','0']
        L = pad_with_fake(L)
        outLs = arrange(L)
        for l in outLs:
            print(l)
        final_string = ""
        for l in outLs[-1]:
            final_string = final_string + "|" + l[0]
        for i in range(0,10):
            final_string = final_string.replace(fake_digits[i],"")
        print("input", inL, "--> output", final_string)
    main()

Example

[['559', '5ff', '55f', '59f', '549', '544', '54f']]
[['559', '5ff', '55f', '59f', '549', '544', '54f']]
[['59f'], ['559', '55f'], ['5ff'], ['549', '544', '54f']]
[['59f'], ['559'], ['55f'], ['5ff'], ['549'], ['54f'], ['544']]
input [559, 5, 55, 59, 549, 544, 54] --> output |59|559|55|5|549|54|544

You can try to use a faster sorting algorithm like merge sort which is O(n log n).

Alternatively, by using a trie this problem could be solved in O(s), where s is the sum of character counts across all strings.

Related