Add sources.

This commit is contained in:
Fabio Salvini 2021-05-21 20:10:51 +02:00
parent f06335a021
commit a98226dc50
12 changed files with 606 additions and 0 deletions

31
.gitignore vendored
View File

@ -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/

63
Dockerfile Normal file
View File

@ -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"]

145
README.md Normal file
View File

@ -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 | <your program> > 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" | <my program>
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
```

57
pom.xml Normal file
View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.fabiosalvini</groupId>
<artifactId>permutations</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<junit-version>5.7.2</junit-version>
<surefire-version>2.22.2</surefire-version>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit-version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>${artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>libs/</classpathPrefix>
<mainClass>com.fabiosalvini.Main</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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.
* <p>
* 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<long[]> 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<long[]> 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++;
}
}
}
}

View File

@ -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.
* <p>
* Example: [123,456,789] => "123,456,789\n" (new line separator is platform-dependent).
*/
public class PermutationsPrinter implements Consumer<long[]>, 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();
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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<long[]> elementsGenerator() {
List<long[]> elementsList = new LinkedList<>();
ArrayList<Long> 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<String> permutations = new HashSet<>();
Consumer<long[]> 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;
}
}

View File

@ -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);
}
}