1

I couldn't find any documentation on this for Scala, but a lot for other programming languages which never or rarely use recursion.

The sequence should be allowed to be empty and contain doubles.

val nonEmpthySeq = Seq(1,2,3,4,4,4,5,6,7,7,67,8,9,9,10)
val oneElementSeq = Seq(4)
val empthySeq = Seq()

What I tried :

I can't write an answer to this, as my question is supposedly a duplicate.

Using pattern matching

def secondSmallest(xs: Seq[Int]): Option[Int] = xs match {
  case Nil => None
  case `xs` => Some(xs.distinct.sorted.apply(1))
}

super clean one-liner

def secondSmallest(xs: Seq[Int]): Option[Int] =
    Try(xs.distinct.sorted.apply(1)).toOption

Both return

secondSmallest(nonEmpthySeq)
secondSmallest(oneElementSeq)
secondSmallest(empthySeq)
res0: Option[Int] = Some(2)
res1: Option[Int] = None
res2: Option[Int] = None

res1 explaination:

x::Nil, for Seq(4) in secondSmallest(oneElementSeq) has to be Noneas there is logically no second highest element in the list, so it has to be None.

If you want the one element in case there is only one, you have to handle it with case x :: Nil => Some(x).

def secondSmallest(xs: Seq[Int]): Option[Int] = xs match {
  case Nil => None
  case x :: Nil => Some(x)
  case `xs` => Some(xs.distinct.sorted.apply(1))
}
blkpingu
  • 1,556
  • 1
  • 18
  • 41
  • 1
    Could someone please explain me how this is a duplicate of the linked answer? – Andrey Tyukin Jul 06 '19 at 22:35
  • @MarioGalic vote reopen if think this question was wrongly closed and vote against it. This is how SO works – blkpingu Jul 06 '19 at 22:42
  • @AndreyTyukin I see that, but apparently this question is a duplicate and already has an answer. If you are under the impression this is not the case you are free to vote against it. – blkpingu Jul 06 '19 at 22:44
  • @MarioGalic thanks. that val bothered me. this is much cleaner. updating the answer – blkpingu Jul 07 '19 at 00:16
  • @MarioGalic my answer was flawed anyway, as there isn't a second highest in `x::Nil` so it has to be `None`. – blkpingu Jul 07 '19 at 00:31
  • Surely the obvious answer is `xs.distinct.sorted.drop(1).headOption`? – Tim Jul 07 '19 at 06:38
  • Yes, that would be a great answer if this hadn’t be marked as a duplicate. Thankfully, one could vote to reopen, in case they feel that way. – blkpingu Jul 07 '19 at 09:20

3 Answers3

3

Quick and dirty

list.distinct.sorted.lift(1)

The lift part deals with the case that there is no entry at position 1.


Proper linear solution

This here works in O(n), scanning through the list exactly once:

def secondLargest[A: Ordering](as: Seq[A]): Option[A] = {
    val bottom = Option.empty[A]
    val ord = implicitly[Ordering[Option[A]]]
    import ord._
    as.foldLeft((bottom, bottom)) {
      case ((largest, second), x) =>
        val wrapped = Some(x)
        if (wrapped > largest) (wrapped, largest)
        else if (wrapped > second) (largest, wrapped)
        else (largest, second)
    }._2
}

It keeps two Option[A]'s with two largest elements while scanning through the sequence. The comparison on Option[A] works because Ordering provides an implicit that adjoins a None as a bottom element to any ordering on type A (this is what ord is for).

Example:

println(secondLargest(Seq("foo", "bar", "baz")))
println(secondLargest(Seq(4, 7, 2, 5, 9, 6, 4)))

Prints:

// Some(baz)
// Some(7)

Note that all solutions based on eager sorting are at least O(n*log(n)), which is not good, because there is a Quick-Select algorithm that can find the k-largest element in expected linear time.


Edit

Oh, well... If you want the second smallest, reverse the ordering:

def secondSmallest[A: Ordering](as: Seq[A]): Option[A] =
  secondLargest(as)(implicitly[Ordering[A]].reverse)

println(secondSmallest(Seq("aa", "ca", "ab", "bc", "cc"))) // Some(ab)
println(secondSmallest(Seq(4, 7, 2, 5, 9, 6, 4)))          // Some(4)
Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
  • @MarioGalic The body of the question mentioned "secondHighest" dozen of times, so I went for "secondHighest". Not a huge difference... Oh, wait, now I broke your answer?... – Andrey Tyukin Jul 07 '19 at 13:13
  • uhm, well, I totally meant second smallest and somehow got comfused along the way in my answering attempts. sorry. the questions headline is to be considered with more weight than the body. fixed the method signatures. – blkpingu Jul 07 '19 at 13:24
  • 1
    @TobiasKolb Added the `secondSmallest` method. Also added a quick-and-dirty one-liner, if you prefer those at the expense of the performance... – Andrey Tyukin Jul 07 '19 at 13:36
2

WARNING: Note Andrey's comment before following this answer.


Here is a solution I stole from Tim in the comments:

def secondHighest(xs:Seq[Int]): Option[Int] = {
  xs.distinct.sorted.init.lastOption
}

and

def secondLowest(xs:Seq[Int]): Option[Int] = {
  xs.distinct.sorted.drop(1).headOption
}

My own misguided attempt was

Try(xs.distinct.sorted.apply(1)).toOption
Mario Galic
  • 47,285
  • 6
  • 56
  • 98
  • 2
    Again: `sorted` is `O(n*log(n))`, this is not good. On arbitrary sequences, `init` and `drop` might require the reallocation of the memory and copy (almost) the entire sequence, this is also not good. Finally, `Try(...apply).toOption` does weird things to the stack and catches exceptions, which is also weird. – Andrey Tyukin Jul 07 '19 at 13:34
  • 1
    Sorry if I'm nitpicking here, when I tried to find a proper duplicate target yesterday, I found more than enough examples of how NOT to do it :D Everyone is sorting those crazily gigantic lists like there is no tomorrow. I have to admit that it was utterly annoying that after a decade of answering questions about accessing elements in arrays, it seems nearly impossible to find a proper duplicate target for selecting the second largest element on SO. That's not how it is supposed to be, imho... – Andrey Tyukin Jul 07 '19 at 13:38
  • 1
    to be fair, your answer is great but I had to shift acceptance to Andrey to avoid people using the sort approach for reasons you understand. Please leave the answer up anyway – blkpingu Jul 07 '19 at 15:29
1

From the PriorityQueue ScalaDocs page:

This class implements priority queues using a heap.

From the Wikipedia page on the heap data structure:

The Heap data structure can be used to efficiently find the kth smallest (or largest) element in an array.

That being the case, perhaps we can generalize the problem without sacrificing too much efficiency.

def fromTop[A:Ordering](xs :Seq[A], offset :Int) :Option[A] = {
  val pq = collection.mutable.PriorityQueue(xs:_*)
  for (_ <- 1 until offset) {
    val init = pq.headOption
    while (pq.nonEmpty && pq.head == init.get)
      pq.dequeue()
  }
  pq.headOption
}

def fromBottom[A:Ordering](xs :Seq[A], offset :Int) :Option[A] =
  fromTop(xs, offset)(implicitly[Ordering[A]].reverse)

testing:

fromTop(Vector.empty[Int], 2)            //res0: Option[Int] = None
fromTop(Vector(9, 88), 0)                //res1: Option[Int] = Some(88)
fromTop(Vector('a','c','t','u','p'), 3)  //res2: Option[Char] = Some(p)

fromBottom(List(true,false,true), 2)     //res3: Option[Boolean] = Some(true)
fromBottom(List(1,2,3), 4)               //res4: Option[Int] = None
fromBottom(List(1,1,1,2,2,2,3,3,3), 2)   //res5: Option[Int] = Some(2)
jwvh
  • 50,871
  • 7
  • 38
  • 64