From a98226dc5071b2164a7f8194eaa06d9373b63313 Mon Sep 17 00:00:00 2001 From: Fabio Salvini Date: Fri, 21 May 2021 20:10:51 +0200 Subject: [PATCH] Add sources. --- .gitignore | 31 ++++ Dockerfile | 63 ++++++++ README.md | 145 ++++++++++++++++++ pom.xml | 57 +++++++ src/main/java/com/fabiosalvini/Main.java | 31 ++++ .../permutations/ElementsInputReader.java | 36 +++++ .../permutations/PermutationsGenerator.java | 49 ++++++ .../permutations/PermutationsPrinter.java | 59 +++++++ .../com/fabiosalvini/utils/ArrayUtils.java | 17 ++ .../permutations/ElementsInputReaderTest.java | 20 +++ .../PermutationsGeneratorTest.java | 77 ++++++++++ .../permutations/PermutationsPrinterTest.java | 21 +++ 12 files changed, 606 insertions(+) create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/com/fabiosalvini/Main.java create mode 100644 src/main/java/com/fabiosalvini/permutations/ElementsInputReader.java create mode 100644 src/main/java/com/fabiosalvini/permutations/PermutationsGenerator.java create mode 100644 src/main/java/com/fabiosalvini/permutations/PermutationsPrinter.java create mode 100644 src/main/java/com/fabiosalvini/utils/ArrayUtils.java create mode 100644 src/test/java/com/fabiosalvini/permutations/ElementsInputReaderTest.java create mode 100644 src/test/java/com/fabiosalvini/permutations/PermutationsGeneratorTest.java create mode 100644 src/test/java/com/fabiosalvini/permutations/PermutationsPrinterTest.java diff --git a/.gitignore b/.gitignore index 2f7896d..ce719e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,32 @@ target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +/.idea/ +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6310d48 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,63 @@ +# Image to build the application +FROM ubuntu:bionic + +RUN \ + # Create builder user + groupadd builder && \ + useradd builder -s /bin/bash -m -g builder -G sudo && \ + echo 'builder:builder' |chpasswd && \ + mkdir /home/builder/app && \ + apt-get update && \ + # Install utilities + apt-get install -y \ + wget \ + curl \ + vim \ + git \ + zip \ + bzip2 \ + python \ + build-essential \ + software-properties-common \ + sudo && \ + # install OpenJDK 11 + add-apt-repository ppa:openjdk-r/ppa && \ + apt-get update && \ + apt-get install -y openjdk-11-jdk && \ + update-java-alternatives -s java-1.11.0-openjdk-amd64 && \ + # Install Maven + apt-get install -y maven && \ + # cleanup + apt-get clean && \ + rm -rf \ + /var/lib/apt/lists/* \ + /tmp/* \ + /var/tmp/* + +RUN \ + # fix builder user permissions + chown -R builder:builder \ + /home/builder && \ + # cleanup + rm -rf \ + /home/builder/.cache/ \ + /var/lib/apt/lists/* \ + /tmp/* \ + /var/tmp/* + +USER builder + +WORKDIR "/home/builder/app" + +# Copy sources +COPY . /home/builder/app/ + +# Compile project +RUN mvn package + +# Image to run the application +FROM openjdk:11.0.11-jre-slim + +COPY --from=0 /home/builder/app/target/permutations.jar /app/permutations.jar + +ENTRYPOINT ["java", "-jar", "/app/permutations.jar"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..c23c750 --- /dev/null +++ b/README.md @@ -0,0 +1,145 @@ +# Challenge + +Technical challenge for an interview at [Credimi S.p.A.](https://www.credimi.com/) + +Suppose you're given a file with a single string of comma separated numbers like the following + +```shell +$ cat input +123,456,789 +``` + +Your program must print to STDOUT all possible permutations with repetitions, like follows (actual order is not +relevant) + +```shell +$ cat input | > output +$ cat output +123,456,789 +123,789,456 +456,789,123 +456,123,789 +789,123,456 +789,456,123 +``` + +Your solution must print to STDOUT and any extraneous output will be considered an error (but you may print to STDERR at +your convenience). Your program will be tested against inputs of growing size, the more they can take, the better! + +The solution must contain documentation explaining time and space complexity of your implementation. + +You should use only the standard library of your language of choice, any additional requirement should be documented and +motivated. + +Java, Python, Scala or Rust are preferred. + +# Solution + +## Typo + +The challenge description contains an error. +It asks to print all possible permutations WITH repetitions, but the example shows the permutations without +repetition. +If we consider permutations with repetitions, we would also need an additional parameter that is the desired length of +the permutation. The typo is probably "with repetition" instead of "without repetition". + +## Algorithm + +The most efficient algorithm to compute all permutations is +[Heap's Algorithm](https://en.wikipedia.org/wiki/Heap%27s_algorithm). +It minimizes elements swapping: every permutation is generated interchanging a single pair of elements from the previous +one. + +Heap algorithm cannot be parallelized, we could use a less efficient algorithm with better average time assuming thread +count > 1 (see for example +[Ouellet Indexed](https://www.codeproject.com/Articles/1250925/Permutations-Fast-implementations-and-a-new-indexi)). +However, the bottleneck of our program is the writing to stdout, in some tests I saw that the multi-threaded version is +actually slower, because threads need to synchronize to write to the output. + +## Choices + +### Element types + +The challenge description does not specify the type of the input elements, they are just "numbers". +I decided to represent them with the Java primitive type `long`. +There is no noticeable performance improvement representing them with an `int` instead. + +### Repeated elements + +It's not specified what is the desired output if a number is repeated. +I've decided to not check for duplicate elements, since they affect the algorithm time and space complexity. +If one or more numbers is repeated, some permutations will be duplicated: + +```shell +$ echo "1,1" | +1,1 +1,1 +``` + +## Implementation + +I wrote the project in Java because it's the language where I have more experience. +It's a maven project that targets JDK11+. + +### Time and space complexity + +Given n elements, there are n! permutations. Heap's algorithm compute a new permutation in `O(1)` time. Each permutation +of n elements must also be printed to the stdout (`O(n)` time). +Time complexity is `O(n*n!)`. + +The program uses two `n` sized arrays and a fixed buffer. Space complexity is `O(n)`. + +### Classes + +I divided the program into three classes: + +- `ElementsInputReader`: to read the input elements. +- `PermutationsGenerator`: to compute all the elements permutations. +- `PermutationsPrinter`: to print permutations to the output. + +### Generics + +I didn't use generics to compute permutations of an array `T[]` because the program performance would be a lot worse +than using primitive types. + +## How to run the application + +### Compilation + +If you have JDK11+ and Maven installed, you can simply run + +```shell +mvn package +``` + +The jar file will be in `target/permutations.jar`. + +Otherwise, you can use the provided Dockerfile + +```shell +docker build -t permutations . +``` + +(Maven compilation fails if you had built the project locally, be sure to delete `target` directory). + +### Run + +If you have built the project using Docker, you can simply run it + +```shell +echo 1,2,3 | docker run --rm -i permutations +``` + +However running it with Docker has a big effect on performances. +To extract the jar file, run + +```shell +docker create --name permutations permutations +docker cp permutations:/app/permutations.jar . +``` + +Then run it with locally installed JRE (11+) + +```shell +echo 1,2,3 | java -jar permutations.jar +``` diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..495c4dd --- /dev/null +++ b/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + com.fabiosalvini + permutations + 1.0-SNAPSHOT + + + 11 + 11 + 5.7.2 + 2.22.2 + + + + + org.junit.jupiter + junit-jupiter-engine + ${junit-version} + test + + + org.junit.jupiter + junit-jupiter-params + ${junit-version} + test + + + + + ${artifactId} + + + org.apache.maven.plugins + maven-surefire-plugin + ${surefire-version} + + + org.apache.maven.plugins + maven-jar-plugin + + + + true + libs/ + com.fabiosalvini.Main + + + + + + + + diff --git a/src/main/java/com/fabiosalvini/Main.java b/src/main/java/com/fabiosalvini/Main.java new file mode 100644 index 0000000..54a8c12 --- /dev/null +++ b/src/main/java/com/fabiosalvini/Main.java @@ -0,0 +1,31 @@ +package com.fabiosalvini; + +import com.fabiosalvini.permutations.ElementsInputReader; +import com.fabiosalvini.permutations.PermutationsGenerator; +import com.fabiosalvini.permutations.PermutationsPrinter; + +public class Main { + + public static void main(String[] args) { + printPermutations(readElements()); + } + + private static long[] readElements() { + ElementsInputReader inputReader = new ElementsInputReader(System.in); + try { + return inputReader.readElements(); + } catch (Exception e) { + throw new RuntimeException("Error reading elements. Format is: 123,456,789,...", e); + } + } + + private static void printPermutations(long[] elements) { + try (PermutationsPrinter printer = new PermutationsPrinter(System.out)) { + PermutationsGenerator generator = new PermutationsGenerator(elements, printer); + generator.compute(); + } catch (Exception e) { + throw new RuntimeException("Error printing permutations", e); + } + } + +} diff --git a/src/main/java/com/fabiosalvini/permutations/ElementsInputReader.java b/src/main/java/com/fabiosalvini/permutations/ElementsInputReader.java new file mode 100644 index 0000000..993e90d --- /dev/null +++ b/src/main/java/com/fabiosalvini/permutations/ElementsInputReader.java @@ -0,0 +1,36 @@ +package com.fabiosalvini.permutations; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Arrays; + +/** + * Class to read the permutation elements from the given input stream. + * Elements must be parsable as long and be separated by a comma. + */ +public class ElementsInputReader { + + private static final String SEPARATOR = ","; + + private final InputStream inputStream; + + public ElementsInputReader(InputStream inputStream) { + this.inputStream = inputStream; + } + + /** + * Read the elements from the input stream. + * + * @return the array of elements. + * @throws IOException if elements cannot be read or they have the wrong format. + */ + public long[] readElements() throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + String line = reader.readLine(); + return Arrays.stream(line.split(SEPARATOR)) + .mapToLong(Long::parseLong) + .toArray(); + } +} diff --git a/src/main/java/com/fabiosalvini/permutations/PermutationsGenerator.java b/src/main/java/com/fabiosalvini/permutations/PermutationsGenerator.java new file mode 100644 index 0000000..74f31f7 --- /dev/null +++ b/src/main/java/com/fabiosalvini/permutations/PermutationsGenerator.java @@ -0,0 +1,49 @@ +package com.fabiosalvini.permutations; + +import java.util.function.Consumer; + +import static com.fabiosalvini.utils.ArrayUtils.swap; + +/** + * Class to generate all permutations of an array of elements. + *

+ * The computation is based on Heap's algorithm (https://en.wikipedia.org/wiki/Heap%27s_algorithm). + */ +public class PermutationsGenerator { + + private final long[] elements; + private final int[] indexes; + private final Consumer consumer; + + /** + * Create a new generator. + * + * @param elements the array of elements. + * @param consumer the consumer that will receive every permutation. + */ + public PermutationsGenerator(long[] elements, Consumer consumer) { + this.elements = elements; + this.consumer = consumer; + indexes = new int[elements.length]; + } + + /** + * Compute all permutations using Heap's algorithm. + */ + public void compute() { + consumer.accept(elements); + int i = 0; + while (i < elements.length) { + if (indexes[i] < i) { + swap(elements, i % 2 == 0 ? 0 : indexes[i], i); + consumer.accept(elements); + indexes[i]++; + i = 0; + } else { + indexes[i] = 0; + i++; + } + } + } + +} diff --git a/src/main/java/com/fabiosalvini/permutations/PermutationsPrinter.java b/src/main/java/com/fabiosalvini/permutations/PermutationsPrinter.java new file mode 100644 index 0000000..cd36abf --- /dev/null +++ b/src/main/java/com/fabiosalvini/permutations/PermutationsPrinter.java @@ -0,0 +1,59 @@ +package com.fabiosalvini.permutations; + +import java.io.*; +import java.util.function.Consumer; + +/** + * Class to print permutations represented with arrays to the given output stream. + * Each permutation is printed in a new line with each element separated by a comma. + *

+ * Example: [123,456,789] => "123,456,789\n" (new line separator is platform-dependent). + */ +public class PermutationsPrinter implements Consumer, Closeable { + + // Defining the separator as a constructor parameter incurs in a performance penalty. + private static final String SEPARATOR = ","; + + private final BufferedWriter out; + + public PermutationsPrinter(OutputStream outStream) { + this.out = new BufferedWriter(new OutputStreamWriter(outStream)); + } + + @Override + public void accept(long[] elements) { + String result = formatElements(elements); + try { + out.write(result); + out.newLine(); + } catch (IOException e) { + throw new RuntimeException("Error writing permutation to the output stream", e); + } + } + + /** + * Flush and close the output stream. + * + * @throws IOException if the stream cannot be closed. + */ + @Override + public void close() throws IOException { + out.close(); + } + + /** + * Format the given permutation to a string. + * + * @param elements the permutation. + * @return a string where each element is separated by {@value #SEPARATOR}. + */ + private String formatElements(long[] elements) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < elements.length - 1; i++) { + sb.append(elements[i]); + sb.append(SEPARATOR); + } + sb.append(elements[elements.length - 1]); + return sb.toString(); + } +} diff --git a/src/main/java/com/fabiosalvini/utils/ArrayUtils.java b/src/main/java/com/fabiosalvini/utils/ArrayUtils.java new file mode 100644 index 0000000..d9cadb9 --- /dev/null +++ b/src/main/java/com/fabiosalvini/utils/ArrayUtils.java @@ -0,0 +1,17 @@ +package com.fabiosalvini.utils; + +public class ArrayUtils { + + /** + * In-place swap of two array elements. + * + * @param array the input array. + * @param i index of the first element. + * @param j index of the second element. + */ + public static void swap(long[] array, int i, int j) { + long tmp = array[i]; + array[i] = array[j]; + array[j] = tmp; + } +} diff --git a/src/test/java/com/fabiosalvini/permutations/ElementsInputReaderTest.java b/src/test/java/com/fabiosalvini/permutations/ElementsInputReaderTest.java new file mode 100644 index 0000000..8c1f22f --- /dev/null +++ b/src/test/java/com/fabiosalvini/permutations/ElementsInputReaderTest.java @@ -0,0 +1,20 @@ +package com.fabiosalvini.permutations; + +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +public class ElementsInputReaderTest { + + @Test + public void readElements() throws IOException { + byte[] input = "123,456,789".getBytes(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(input); + ElementsInputReader reader = new ElementsInputReader(inputStream); + long[] elements = reader.readElements(); + assertArrayEquals(new long[]{123, 456, 789}, elements); + } +} diff --git a/src/test/java/com/fabiosalvini/permutations/PermutationsGeneratorTest.java b/src/test/java/com/fabiosalvini/permutations/PermutationsGeneratorTest.java new file mode 100644 index 0000000..95fcdcf --- /dev/null +++ b/src/test/java/com/fabiosalvini/permutations/PermutationsGeneratorTest.java @@ -0,0 +1,77 @@ +package com.fabiosalvini.permutations; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.*; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + + +public class PermutationsGeneratorTest { + + /** + * Generate a stream of arrays to be used in parametrized tests. + */ + @SuppressWarnings("unused") + private static Stream elementsGenerator() { + List elementsList = new LinkedList<>(); + ArrayList elements = new ArrayList<>(); + for (int i = 1; i < 10; i++) { + elements.add((long) i); + elementsList.add( + elements.stream().mapToLong(num -> num).toArray() + ); + } + return elementsList.stream(); + } + + @ParameterizedTest + @MethodSource("elementsGenerator") + public void permutationsAreUniqueAndWithoutDuplicates(long[] elements) { + Set permutations = new HashSet<>(); + Consumer consumer = (perm) -> { + assertFalse(hasDuplicates(perm), "Permutation contains duplicated elements"); + String str = toString(perm); + assertFalse(permutations.contains(str), "Permutation " + str + " is duplicated"); + permutations.add(str); + }; + PermutationsGenerator generator = new PermutationsGenerator(elements, consumer); + generator.compute(); + assertEquals(factorial(elements.length), permutations.size(), "Wrong permutations count"); + } + + /** + * Returns true if the input array has duplicated elements. + */ + private boolean hasDuplicates(long[] elements) { + return Arrays.stream(elements).distinct().toArray().length != elements.length; + } + + /** + * Convert the input array into a string, so it can be stored in a Set to check for duplicates. + */ + private String toString(long[] elements) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < elements.length - 1; i++) { + sb.append(elements[i]); + sb.append(","); + } + sb.append(elements[elements.length - 1]); + return sb.toString(); + } + + /** + * Compute n!. + */ + private long factorial(int n) { + long factorial = 1; + for (int i = 1; i <= n; i++) { + factorial = factorial * i; + } + return factorial; + } +} diff --git a/src/test/java/com/fabiosalvini/permutations/PermutationsPrinterTest.java b/src/test/java/com/fabiosalvini/permutations/PermutationsPrinterTest.java new file mode 100644 index 0000000..66e825e --- /dev/null +++ b/src/test/java/com/fabiosalvini/permutations/PermutationsPrinterTest.java @@ -0,0 +1,21 @@ +package com.fabiosalvini.permutations; + +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PermutationsPrinterTest { + + @Test + public void permutationsFormat() throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (PermutationsPrinter printer = new PermutationsPrinter(outputStream)) { + printer.accept(new long[]{123, 456, 789}); + } + String result = outputStream.toString(); + assertEquals("123,456,789" + System.lineSeparator(), result); + } +}