
Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: March 18, 2024
When dealing with lists, we’ll sometimes need to identify which of the elements is the last that satisfies a condition.
The reason why this is an interesting problem is that we don’t want to traverse the whole list. That rules out approaches like partitioning the list or reversing it to then find the first element that satisfies the condition.
Three things to keep in mind before implementing a solution:
First, a little boilerplate, to add a method lastWhen() to Lists:
implicit class Wrapper[T](val list: List[T]) extends AnyVal {
/** Extension method that gives List instances the possibility to find their
* last element that satisfies a given predicate.
*
* @param predicate
* the condition for which the last satisfying element will be searched
* for
* @return
* index (zero-based) of the last element that satisfies the condition,
* or a negative number if it's not found
*/
def lastWhen(predicate: T => Boolean): Int =
useRecursiveScan(list, predicate)
// TODO actual implementation goes here
}
A very simple (even a bit naïve) approach to solve this problem is reversing the list, and then counting how many elements don’t satisfy the predicate:
private def useReverse(list: List[T], predicate: T => Boolean): Int =
list.size - list.reverse.takeWhile(predicate(_) == false).size - 1
The main problem here is that the whole list needs to be reversed just to start discarding elements from the beginning — very inefficient. Is there a better way? Sure, there is!
Let’s write a tail-recursive implementation to find the last element that satisfies a predicate:
@tailrec
private def useRecursiveScan(list: List[T], predicate: T => Boolean): Int =
if (list.isEmpty) -1
else if (predicate(list.last)) list.size - 1
else useRecursiveScan(list.take(list.size - 1), predicate)
This recursive method stops when the list is empty (returning a negative value), or when the last element of the list satisfies the predicate (returning as an index the size of the list minus one). Otherwise, it calls itself recursively, passing the list without its last element and the same predicate as arguments.
It’s worth noting, in this implementation, that the method is tail recursive, so no concerns about the performance of excessive memory consumption should worry us.
More often than not, the APIs of the native libraries offer us efficient solutions to our basic problems. In our current case, we could avail of the method LastIndexWhere() of Lists (and sequences in general), which does exactly what we need! Let’s see it in action:
private def useNativeLibrary(list: List[T], predicate: T => Boolean): Int =
list.lastIndexWhere(predicate)
In any case, if we use ScalaTest, we could implement basic unit tests (extending AnyWordSpec):
"A last element finder" should {
"fail to find last elements in an empty list" in {
val list = List.empty[Char]
assert(list.lastWhen(_ == 'c') < 0)
}
"fail find last elements if they are not there" in {
val list = List('a', 'b', 'c', 'd', 'c', 'e')
assert(list.lastWhen(_ == 'n') < 0)
}
"find last element as last if all are the same" in {
val list = List('a', 'a', 'a', 'a', 'a')
assertResult(4)(list.lastWhen(_ == 'a'))
}
"find last element (happy case)" in {
val list = List('a', 'b', 'c', 'd', 'c', 'e')
assertResult(4)(list.lastWhen(_ == 'c'))
}
This is a simple problem, yet it has the potential to teach us something: that just coding the first simple algorithm we can think of is not good enough. We should strive to provide an efficient solution. The first solution that we found is both tail-recursive and avoids scanning the whole list to find that last satisfying element.
The second solution is even simpler and relies on the good practices that the library gives us for free.