Home > Net >  Most efficient algorithm and BigO notation
Most efficient algorithm and BigO notation

Time:10-13

I am practicing some coding algorithms. In some interviews they don't only ask you to solve a problem, but to solve it in the most efficient way, and also specify the efficiency of the algorithm (aka Big O notation). I've always had issues measuring that efficiency, so I would really appreciate someone explaining how to calculate the efficiency of the algorithmor to point to some resources in order to check it (did not find very useful docs so far).

For example, take a look to this problem below. I have solved it in two different ways. using Java. The first way is using an imperative approach (which I find it more efficient as we don't need to iterate over the lists many times) and the second approach, with functional programming approach and stream API (that I find much less efficient in this case).

Could someone tell me with an explanation what is the Big O notation of both approaches, explaining the way to calculate it?

    package exercises;

import org.junit.Assert;
import org.junit.Test;

import java.util.*;
import java.util.stream.Collectors;


/**
 * You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order, and each of their nodes contains a single digit. Add the two numbers and return the sum as a linked list.
 *
 * You may assume the two numbers do not contain any leading zero, except the number 0 itself.
 */
public class AddTwoNumbers {

    //Most efficient implemention of the two
    private List<Integer> addTwoNumbersImplementation1(LinkedList<Integer> l1, LinkedList<Integer> l2) {

        Integer carry = 0;
        List<Integer> result = new ArrayList<Integer>();
        while (l1.size() > 0 || l2.size() > 0) {

            Integer l1Last = Optional.ofNullable(l1.pollLast()).orElse(0);
            Integer l2Last = Optional.ofNullable(l2.pollLast()).orElse(0);

            Integer partialResult = l1Last   l2Last   carry;

            if (partialResult >= 10) {
                result.add(Character.getNumericValue(partialResult.toString().charAt(1)));
                carry = 1;
            } else {
                result.add(partialResult);
                carry = 0;
            }
        }

        if(carry == 1) {result.add(1);}

        return result;
    }


    //Least efficient implemention of the two
    private List<Integer> addTwoNumbersImplementation2(LinkedList<Integer> l1, LinkedList<Integer> l2) {
        Integer n1 = Integer.parseInt(new StringBuffer(l1.stream().map(e -> e.toString()).collect(Collectors.joining())).reverse().toString());
        Integer n2 = Integer.parseInt(new StringBuffer(l2.stream().map(e -> e.toString()).collect(Collectors.joining())).reverse().toString());
        Integer result = n1   n2;
        return new StringBuffer(result.toString()).reverse().toString().chars().mapToObj(Character::getNumericValue).collect(Collectors.toList());
    }


    @Test
    public void test() {
        LinkedList<Integer> list1 = new LinkedList<>();
        list1.addAll(Arrays.asList(2,4,3));
        LinkedList<Integer> list2 = new LinkedList<>();
        list2.addAll(Arrays.asList(5,6,4));
        List<Integer> resultList = new LinkedList<>();
        resultList.addAll(Arrays.asList(7,0,8));
        Assert.assertEquals(resultList, addTwoNumbersImplementation1(list1, list2));
    }

    @Test
    public void test2() {
        LinkedList<Integer> list1 = new LinkedList<>();
        list1.add(0);
        LinkedList<Integer> list2 = new LinkedList<>();
        list2.add(0);
        List<Integer> resultList = new LinkedList<>();
        resultList.add(0);
        Assert.assertEquals(resultList, addTwoNumbersImplementation1(list1, list2));
    }

    @Test
    public void test3() {
        LinkedList<Integer> list1 = new LinkedList<>();
        list1.addAll(Arrays.asList(9,9,9,9,9,9,9));
        LinkedList<Integer> list2 = new LinkedList<>();
        list2.addAll(Arrays.asList(9,9,9,9));
        List<Integer> expected = new LinkedList<>();
        expected.addAll(Arrays.asList(8,9,9,9,0,0,0,1));
        Assert.assertEquals(expected, addTwoNumbersImplementation1(list1, list2));
    }


    @Test
    public void test4() {
        LinkedList<Integer> list1 = new LinkedList<>();
        list1.addAll(Arrays.asList(2,4,3));
        LinkedList<Integer> list2 = new LinkedList<>();
        list2.addAll(Arrays.asList(5,6,4));
        List<Integer> resultList = new LinkedList<>();
        resultList.addAll(Arrays.asList(7,0,8));
        Assert.assertEquals(resultList, addTwoNumbersImplementation2(list1, list2));
    }

    @Test
    public void test5() {
        LinkedList<Integer> list1 = new LinkedList<>();
        list1.add(0);
        LinkedList<Integer> list2 = new LinkedList<>();
        list2.add(0);
        List<Integer> resultList = new LinkedList<>();
        resultList.add(0);
        Assert.assertEquals(resultList, addTwoNumbersImplementation2(list1, list2));
    }

    @Test
    public void test6() {
        LinkedList<Integer> list1 = new LinkedList<>();
        list1.addAll(Arrays.asList(9,9,9,9,9,9,9));
        LinkedList<Integer> list2 = new LinkedList<>();
        list2.addAll(Arrays.asList(9,9,9,9));
        List<Integer> expected = new LinkedList<>();
        expected.addAll(Arrays.asList(8,9,9,9,0,0,0,1));
        Assert.assertEquals(expected, addTwoNumbersImplementation2(list1, list2));
    }
}

CodePudding user response:

For loops, the big O notation time is basically count the iterations. If you loop over an array, it's O(n) (n is the size of the list). If you have a loop inside a loop, its O(outer loop count * inner loop count). So the first algorithm is O(n) where n is the size of the array.

Also- what are you doing with

result.add(Character.getNumericValue(partialResult.toString().charAt(1)));
carry = 1;

Here's a much easier way to do it:

   result.add(partialResult%10);
   carry = partialResult/10;

This way also works with the exact same code if you're adding 3 or more arrays.

Your second one has a major flaw- it won't work for numbers that add up to over 2^32 (or 4 billionish). The first will. Ignoring that, it is an O(n) operation to do the map, then an O(n) operation to join them, then an O(n) operation to reverse it. So it's O(3n), which is the same as O(n).

That means complexity wise, they're the same. Timewise- the first algorithm will blow the second away, because the operation you're doing n times is so much more efficient, especially if you get rid of the needless string usage and use the code I showed you. Complexity is an approximation of time for comparison sake, but its not a direct correlation. It can only be used to compare algorithms with different classes (say O(n) vs O(n^2)) and is only valid at large n.

  • Related