Kotlin Language Features Related to Null Handling

Any software engineer with a Java background would find the null handling features in the Kotlin language interesting. Let's summarize this topic with some examples. Nullable types: In Kotlin, types are non-nullable by default. If you want a variable to be able to hold a null value, you need to explicitly declare its type as nullable using the Type? syntax. For example, String? denotes a nullable string, while String represents a non-nullable string. Safe calls (?.): Kotlin introduces the safe call operator (?.) for handling nullable types. It allows you to safely invoke a method or access a property on a nullable object. If the object is null, the expression returns null instead of throwing a NullPointerException. Example: data class Person(val name: String, val age: Int, val address: String?) fun main() {     // Create a person with a nullable address     val person1 = Person("John Doe", 25, "123 Main Street")     val person2 = Person("Jane Doe", 30,...

A Pattern Recognition Problem: Finding Line Segments Effectively In Java

Lets say we are given a set of n points and we need to find every line segment that connects 4 or more of the points.

We have an immutable data type for Point:

public class Point implements Comparable {

    private final int x;   
    private final int y;   
    //...

    public int compareTo(Point that) {

        if (that.y == y && that.x == x) {
            return 0;
        }
        if (y < that.y) {
            return -1;
        }
        if (y > that.y) {
            return 1;
        }
        if (x < that.x) {
            return -1;
        }
        // x > that.x
        return 1;
    }
}

So the comparison first looks at the y coordinate and then there is a tie-break on the x coordinate. 

    public double slopeTo(Point that) {

        if (that.x == x && that.y == y) {
            return Double.NEGATIVE_INFINITY;
        }

        return ((double) (that.y - y)) / (that.x - x);
    }

We define that if the coordinates are the same, slope is negative infinity. It is a good idea to search internet for detailed explanations of a slope and types of infinities.

One of the key insights is that we can use the order of the slopes to find points that form a line segment:

        public Comparator slopeOrder() {        
        return (o1, o2) -> {

            if (o1 == null) {
                return 1;
            }
            if (o2 == null) {
                return 1;
            }
            double slope1 = o1.slopeTo(this);
            double slope2 = o2.slopeTo(this);

            if (slope1 == slope2) {
                return o1.compareTo(o2);
            }
            if (slope1 > slope2) {
                return 1;
            }
            else {
                return -1;
            }
        };
    }

One by one, simple but important abstractions are guiding us to the solution. This method is a very nice example of using a Comparator. 

We will of course have a line segment class:

public class LineSegment {
    private final Point p;   
    private final Point q;   

Now here comes the first attempt to find the line segments: BRUTE FORCE!

public class BruteCollinearPoints {
    public BruteCollinearPoints(Point[] points) {    // finds all line segments containing 4 points
// some argument validations
//...

// Here we have O(N logN) complexity
        Arrays.sort(points);

        ArrayList result = new ArrayList<>();
        ArrayList lines = new ArrayList<>();

// Here we have O(N^4) complexity. 
        for (int i = 0; i < points.length; ++i) {
            for (int j = i + 1; j < points.length; ++j) {
                double slope1 = points[i].slopeTo(points[j]);
                for (int k = j + 1; k < points.length; ++k) {
                    double slope2 = points[i].slopeTo(points[k]);
                    if (slope1 != slope2) {
                        continue;
                    }
                    for (int l = k + 1; l < points.length; ++l) {
                        double slope3 = points[i].slopeTo(points[l]);
                        if (slope1 == slope2 && slope2 == slope3) {

                            Point[] segPoints = new Point[] {
                                    points[i], points[j], points[k], points[l]
                            };

                            lines.add(new Line(segPoints[0], segPoints[3]));
                        }
                    }
                }
            }
        }

        for (Line line : lines) {
            LineSegment lineSegment = new LineSegment(line.p1, line.p2);
            result.add(lineSegment);
        }

        LineSegment[] lineSegments = new LineSegment[result.size()];
        this.lineSegments = result.toArray(lineSegments);
    }    
}

Now this brute force algorithm tries to evaluate all the possible combinations. And it is not a suprise to get a very slow runtime, the result of a O(N^4) complexity.

The good news is, we have a working solution. We evaluated the performance and came to the conclusion that the performance is not acceptible. It is time to find another algorithm. This is the scientific approach that gave human race the ability to send rockets to the sky. 

Next: A sorting based solution that is faster:

- We will sort the points according to the slopes they makes with a point P.
- Then we will check if any 3 or more adjacent points in the sorted order have equal slopes with respect to P. If their slopes are equal then they form a line segment.
- We need to apply this method for each point. This algorithm is fast because it brings together the points that have the potential of creating a line segment.

public FastCollinearPoints(Point[] points) {
// argument validations
//...

        Arrays.sort(points);
        
        ArrayList allLines = new ArrayList<>();
        for (int i = 0; i < points.length; ++i) {
            ArrayList copy = new ArrayList<>(Arrays.asList(points));

            Point origin = copy.get(i);
            copy.sort(copy.get(i).slopeOrder());

            List currentPoints = new ArrayList<>();
            Double slope = null;
            for (int j = 0; j < copy.size(); ++j) {

                if (origin.equals(copy.get(j))) {
                    continue;
                }
                if (slope == null) {
                    slope = copy.get(j).slopeTo(origin);
                    currentPoints.add(copy.get(j));
                } else {
                    if (copy.get(j).slopeTo(origin) == slope) {
                        currentPoints.add(copy.get(j));
                    } else {
                        if (currentPoints.size() >= 3) {
                            Point start, end;
                            if (origin.compareTo(currentPoints.get(0)) < 0) {
                                start = origin;
                            } else {
                                start = currentPoints.get(0);
                            }
                            if (origin.compareTo(currentPoints.get(currentPoints.size() - 1)) > 0) {
                                end = origin;
                            } else {
                                end = currentPoints.get(currentPoints.size() - 1);
                            }

                            allLines.add(new Line(start, end));
                        }

                        currentPoints = new ArrayList<>();
                        currentPoints.add(copy.get(j));
                        slope = copy.get(j).slopeTo(origin);
                    }
                }
            }

            if (currentPoints.size() >= 3) {
                Point start, end;
                if (origin.compareTo(currentPoints.get(0)) < 0) {
                    start = origin;
                } else {
                    start = currentPoints.get(0);
                }
                if (origin.compareTo(currentPoints.get(currentPoints.size() - 1)) > 0) {
                    end = origin;
                } else {
                    end = currentPoints.get(currentPoints.size() - 1);
                }

                allLines.add(new Line(start, end));
            }
        }

        List usedLines = new ArrayList<>();
        ArrayList lineSegmentList = new ArrayList<>();

        for (Line line : allLines) {
            if (!usedLines.contains(line)) {
                LineSegment lineSegment = new LineSegment(line.start, line.end);
                lineSegmentList.add(lineSegment);
                usedLines.add(line);
            }
        }

        lineSegments = new LineSegment[lineSegmentList.size()];
        lineSegments = lineSegmentList.toArray(lineSegments);
    }

There is a little code duplication here and let us leave removing it to the reader as an exercise. There are of course implementation details that can best be understood by getting our hands dirty. Below are some unit tests:

    @Test
    public void shouldFindCollinearPoints() {
        Point[] points = new Point[] {
                new Point(0, 0),
                new Point(10, 10),
                new Point(20, 25),
                new Point(30, 30),
                new Point(40, 40)
        };

        FastCollinearPoints fastCollinearPoints = new FastCollinearPoints(points);
        assertEquals(1, fastCollinearPoints.numberOfSegments());
        assertEquals("(0, 0) -> (40, 40)", fastCollinearPoints.segments()[0].toString());
    }

@Test
    public void shouldFindLongestSegment() {
        FastCollinearPoints collinearPoints = new FastCollinearPoints(
                new Point[] {
                        new Point(9000, 9000),
                        new Point(8000, 8000),
                        new Point(7000, 7000),
                        new Point(6000, 6000),
                        new Point(5000, 5000),
                        new Point(4000, 4000),
                        new Point(3000, 3000),
                        new Point(2000, 2000),
                        new Point(1000, 1000),
                        });

        for (LineSegment segment : collinearPoints.segments()) {
            System.out.println(segment.toString());
            segment.draw();
        }
        assertEquals(1, collinearPoints.numberOfSegments());
        assertTrue("(9000, 9000) -> (1000, 1000)".equals(collinearPoints.segments()[0].toString())
                           || "(1000, 1000) -> (9000, 9000)".equals(collinearPoints.segments()[0].toString()
        ));
    }

@Test
    public void shouldHandleAPointThatBelongsToMultipleLines() {
        FastCollinearPoints collinearPoints = new FastCollinearPoints(
                new Point[] {
                        new Point(0, 0),
                        new Point(1, 1),
                        new Point(2, 2),
                        new Point(3, 3),
                        new Point(0, 1),
                        new Point(0, 2),
                        new Point(0, 3),
                        });

        assertEquals(2, collinearPoints.numberOfSegments());
    }

@Test
    public void shouldFindLinesWithSameSlope() {
        FastCollinearPoints collinearPoints = new FastCollinearPoints(
                new Point[] {
                        new Point(0, 0),
                        new Point(0, 1),
                        new Point(0, 2),
                        new Point(0, 3),
                        new Point(5, 1),
                        new Point(5, 2),
                        new Point(5, 3),
                        new Point(5, 4),
                        });

        assertEquals(2, collinearPoints.numberOfSegments());
    }

Complexity of this sorting based algorithm is O(N^2 log N). This is a very good example of using sorting to solve a pattern recognition problem. Sorting algorithms like merge sort and quick sort are one of the most important discoveries of our century.


* Thanks to Coursera and Princeton University for the beautiful assignments on Algorithms. Visit 
https://www.coursera.org/learn/algorithms-part1 for all the details and the original assignment descriptions.

Comments

Popular posts from this blog

Trie Data Structure and Finding Patterns in a Collection of Words

Virtual Memory

NOTES ON COMPUTER ARCHITECTURE: Some important concepts in computer architecture