7

I have a list of Booking which contains startDate and endDate. I have to find the day which is the busiest in terms of bookings.

class Booking {
    Date startDate;
    Date endDate;
}

Example:

2016-10-12 to 2016-10-18
2016-10-11 to 2016-10-15
2016-10-13 to 2016-10-14
2016-10-12 to 2016-10-13

From these dates, it is obvious that 2016-10-13 was booked on all 4 times.

The solution that comes to my mind is:

  • iterate through all the dates from minimum startDate to maximum endDate in the list
  • keep a count of all number of bookings on all dates.
  • finally, return the date having maximum count.

But this is not the efficient solution. How can I find busiest day efficiently?

8
  • 2
    "From these dates, it is obvious that 2016-10-12 was booked on all 4 times." False Commented Feb 23, 2017 at 15:13
  • 1
    @PatrickParker yeah, it looks like it is 2016-10-13 which is booked on all 4 times. Commented Feb 23, 2017 at 15:15
  • @PatrickParker on 12th as well as on 13th November, 4 rooms were booked. If we assume that check-in happens at 00:00 hours and check-out happens at 23:59 hours
    – Jainendra
    Commented Feb 23, 2017 at 15:19
  • 1
    Does this help? Commented Feb 23, 2017 at 15:21
  • 2
    @Jaguar I guess it doesn't have any effect on the required algorithms, but it is definitely not "obvious" that 00:00 on the 13th should be considered as the 12th. Commented Feb 23, 2017 at 15:35

7 Answers 7

3

You can convert each Booking object as an interval in the number line, and then consider the problem as the problem of finding the point on the number line which is contained by maximum number of the intervals.

You can convert the date to number just by appending the year, month and day values, like this:

2016-10-12 -> 20161012

Then you can follow along this technique. Here is what I did, without the parts of converting the Date to int:

class Main {
    public static int findBusiest (int[] starts, int[] ends) {
        Arrays.sort(starts);
        Arrays.sort(ends);
        int n = starts.length;

        int in = 1, max = 1, time = starts[0];
        int i = 1, j = 0;

        while (i < n && j < n) {
            if (starts[i] <= ends[j]) {
                in++;

                if (in > max) {
                    max = in;
                    time = starts[i];
                }
                i++;
            }
            else {
                in--;
                j++;
            }
        }

        return time;
    }

    public static void main(String[] args) {

        int[] starts = { 20161012, 20161011, 20161013, 20161012 };
        int[] ends = { 20161018, 20161015, 20161014, 20161013 };

        System.out.println(findBusiest(starts, ends));
    }
}

Outputs:

20161013
1
  1. for simplicity, give each date their index (for example min date has index 0, the next day 1 and so on), and initialize array filled with zeros
  2. iterate through all ranges and for start date index increment element in array, and decrement for the end date. (for example if some date d meets 3 times as start of the range and 5 time as the end of the range, there should be -2)
  3. now, iterate through all dates from the beginning of the array, and add current element to your counter (basically, the value of counter at the date d, is how often it's inside ranges)
  4. the answer is the max counter value

Algorithm works O(n) where n is the number of days between minDate and maxDate

PS. Solution mentioned by Patrick Parker in this post also works, but it will require O(m * log(m)) time, where m is the number of ranges. So you should choose solution depending on task specifications.

4
  • 2
    for start date index increment element in the array, and decrement for the end date why are we doing this?
    – Jainendra
    Commented Feb 23, 2017 at 15:30
  • If I checked-in and checked-out on the same date, algorithm should still consider that day as busy.
    – Jainendra
    Commented Feb 23, 2017 at 15:33
  • 1
    I actually gave this answer in an interview once for a similar problem. The issue is that you're using a lot of space to make that date array. Instead, you can sort all of the dates first (also while keeping track of which is a start date and which is an end date) and then return the start date that has the highest count after doing what you describe in step 2. In an interview you'd obviously want to explain the trade offs between using more space and having a higher order of growth for your algorithm.
    – Alex S
    Commented Feb 23, 2017 at 15:33
  • @Jaguar, basically, if at the index i is number 5, it will mean that, comparing to the previous date with index i-1, there will be 5 more ranges opened (that can happened, for example, if 8 ranges started at that time, and 3 ended, so, element will be incremented 8 times and decremented 3 times).
    – eldarg
    Commented Feb 23, 2017 at 15:42
1

I would like to point out a property which might set you in the right direction.

The most frequent day(s) will either be an endpoint (start or end date), or they will be tied with an endpoint.

So if it is enough to find one day out of the tied days, you need only to consider days which fall on an endpoint.

Now, how will you increment the frequency for each end point reliably? By processing in order. But it is not enough to process start and end in order of start. starts and ends must both be considered in date order.

1

Well it is quite ugly, but we can do it using stream if you have a List of Booking

class Booking {
      LocalDate start;
      LocalDate end;
}

and we have a List

List<Booking> bookings = new ArrayList<>();
bookings.add(new Booking(LocalDate.of(2016, 10, 12),LocalDate.of(2016, 10, 18)));
bookings.add(new Booking(LocalDate.of(2016, 10, 11),LocalDate.of(2016, 10, 15)));
bookings.add(new Booking(LocalDate.of(2016, 10, 13),LocalDate.of(2016, 10, 14)));
bookings.add(new Booking(LocalDate.of(2016, 10, 12),LocalDate.of(2016, 10, 13)));

Now we can iterate over the list and for each booking get all the dates from start to end:

Stream<LocalDate> dateRanges = bookings.stream().flatMap(booking ->
        Stream.iterate(booking.start, d -> d.plusDays(1))
                .limit(ChronoUnit.DAYS.between(booking.start, booking.end) + 1)
);

We have all the dates, let's count how many times each date appear in the new stream.

Map<LocalDate, Long> datesFrequency = dateRanges.peek(System.out::println).
collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));

and finally let's find the maxim - the most frequent date:

Optional<Map.Entry<LocalDate, Long>> mostFrequent = datesFrequency.entrySet().
stream().max((o1, o2) -> o1.getValue().compareTo(o2.getValue()));

In this case result will be Optional[2016-10-13=4];

0

For every {startDate, endDate} record generate two tuples {startDate,'start'}, {endDate, 'end'}. Sort these on the date first, and the second value second, making sure that end goes after start. (As I understand the intervals are inclusive, so you don't want to discount the ending reservation before accounting for the one that starts on the day that the previous one ends).

Then go over the tuples in the order as described above, incrementing a counter with each start, decrementing with each end, and keeping track of the maximum so far. The maximum is the most booked date.

Complexity is O(n*log(n)).

0

Whilst this may not be the most efficient way to do it, unless you're working with millions of dates, this will be quite fast. I added a few thousand dates in the example below and it did not take long on my system.

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;

public class Days {
    Days() {
        long startTime = System.nanoTime();
        SimpleDateFormat ft = new SimpleDateFormat ("yyyy-MM-dd");
        ArrayList<Booking> dates = new ArrayList<Booking>();
        try {
            dates.add(new Booking(ft.parse("2016-10-14"), ft.parse("2016-10-18")));
            dates.add(new Booking(ft.parse("2016-11-11"), ft.parse("2018-12-15")));
            dates.add(new Booking(ft.parse("2016-10-13"), ft.parse("2016-10-14")));
            dates.add(new Booking(ft.parse("2016-10-5"), ft.parse("2016-10-13")));
            dates.add(new Booking(ft.parse("2016-10-12"), ft.parse("2020-10-13")));
            dates.add(new Booking(ft.parse("2016-10-10"), ft.parse("2018-10-13")));
            dates.add(new Booking(ft.parse("2015-10-12"), ft.parse("2016-11-13")));
            dates.add(new Booking(ft.parse("2016-09-12"), ft.parse("2016-12-13")));
            dates.add(new Booking(ft.parse("2016-08-12"), ft.parse("2016-10-18")));
            dates.add(new Booking(ft.parse("2016-10-01"), ft.parse("2016-10-26")));
            dates.add(new Booking(ft.parse("2016-02-03"), ft.parse("2016-10-28")));
            dates.add(new Booking(ft.parse("2016-10-04"), ft.parse("2016-12-28")));
            dates.add(new Booking(ft.parse("2015-10-05"), ft.parse("2056-10-16")));
            dates.add(new Booking(ft.parse("2012-10-12"), ft.parse("2016-10-14")));
            dates.add(new Booking(ft.parse("2011-10-12"), ft.parse("2017-02-18")));
            dates.add(new Booking(ft.parse("2016-10-06"), ft.parse("2018-10-13")));
            dates.add(new Booking(ft.parse("2016-10-12"), ft.parse("2019-10-13")));
        } catch (ParseException e) {
            e.printStackTrace();
        }

        ArrayList<Date> datesMult = new ArrayList<Date>();

        for(Booking b : dates) {
            datesMult.addAll(b.getDates());
        }

        HashSet<Date> datesOnce = new HashSet<Date>(datesMult);

        int highestCount = -1;
        Date mostUsed = new Date();

        for(Date d : datesOnce) {
            int count = Collections.frequency(datesMult, d);
            if(count > highestCount) {
                highestCount = count;
                mostUsed = d;
            }
        }

        System.out.println("The most common used date is " + ft.format(mostUsed) + " and it got used " + highestCount + " times");
        long endTime = System.nanoTime();

        long duration = (endTime - startTime);
        System.out.println("This took " + duration + " nanoseconds");
    }

    private class Booking {
        Date startDate;
        Date endDate;

        Booking(Date d1, Date d2) {
            startDate = d1;
            endDate = d2;
        }

        public ArrayList<Date> getDates() {
            ArrayList<Date> d = new ArrayList<Date>();
            d.add(startDate);
            Calendar c = Calendar.getInstance();
            while(true) {
                c.setTime(startDate);
                c.add(Calendar.DATE, 1);  // number of days to add
                startDate = c.getTime();
                if(startDate.compareTo(endDate) == -1) {
                    d.add(startDate);
                } else if(startDate.compareTo(endDate) == 0) {
                    d.add(endDate);
                    return d;
                }
            }
        }
    }

    public static void main(String[] args) {
        new Days();
    }
}
1
  • Note baao's looks like a more refined version of this
    – Dan
    Commented Feb 23, 2017 at 15:55
0

I'm not sure if this is the most performant solution, but you could simply create a range of dates, check for frequency, and return that with the highest frequency:

public static Date getMaxOccurence(Booking[] booking) {
    List<Date> dates = new ArrayList<Date>();
    Date max = new Date();
    int freq = 0;
    for (Booking b : booking) {
        Calendar calendar = new GregorianCalendar();
        calendar.setTime(b.getStartDate());

        while (calendar.getTime().before(b.getEndDate())) {
            Date result = calendar.getTime();
            dates.add(result);
            int curr = Collections.frequency(dates, result);
            if (curr > freq) {
                freq = curr;
                max = result;
            }
            calendar.add(Calendar.DATE, 1);
        }
    }
    return max;
}

ideone demo

Not the answer you're looking for? Browse other questions tagged or ask your own question.