Scala Program to Implement Tim Sort

Scala Program to Implement Tim Sort

Tim Sort is a modern and highly efficient sorting algorithm that combines the best features of Merge Sort and Insertion Sort. It was originally designed for real-world data that often contains partially ordered sequences, making it faster than traditional algorithms in many practical scenarios. Tim Sort works by breaking the array into small sections called “runs,” sorting each run with Insertion Sort, and then merging the runs together using a Merge Sort-like approach. This combination makes it stable, efficient, and adaptable to different types of data.

Pluralsight Logo
Accelerate your tech career
with hands-on learning.
Whether you're a tech newbie or a total pro,
get the skills and confidence to land your next move.
Start 10-Day Free Trial

In practical programming, Tim Sort is used in languages like Python and Java for built-in sorting functions because of its speed and stability. Learning Tim Sort in Scala gives beginners a chance to see how combining simple sorting methods into a hybrid algorithm can produce powerful results. It also reinforces key concepts like array manipulation, recursion, and handling partially sorted data. Once you understand the logic, you can appreciate why Tim Sort is widely adopted in real-world applications where performance matters.

Program 1: Basic Tim Sort Implementation

This program demonstrates a simple version of Tim Sort that uses Insertion Sort for small subarrays and merges them iteratively.

object TimSortBasic {

  val RUN = 32

  def insertionSort(arr: Array[Int], left: Int, right: Int): Unit = {

    for (i <- left + 1 to right) {

      val temp = arr(i)
      var j = i - 1

      while (j >= left && arr(j) > temp) {
        arr(j + 1) = arr(j)
        j -= 1
      }

      arr(j + 1) = temp

    }

  }

  def merge(arr: Array[Int], l: Int, m: Int, r: Int): Unit = {

    val len1 = m - l + 1
    val len2 = r - m
    val left = arr.slice(l, m + 1)
    val right = arr.slice(m + 1, r + 1)

    var i = 0
    var j = 0
    var k = l

    while (i < len1 && j < len2) {

      if (left(i) <= right(j)) {
        arr(k) = left(i)
        i += 1
      } else {
        arr(k) = right(j)
        j += 1
      }

      k += 1

    }

    while (i < len1) {

      arr(k) = left(i)
      i += 1
      k += 1

    }

    while (j < len2) {

      arr(k) = right(j)
      j += 1
      k += 1

    }

  }

  def timSort(arr: Array[Int]): Unit = {

    val n = arr.length

    for (i <- 0 until n by RUN) {
      insertionSort(arr, i, Math.min((i + RUN - 1), n - 1))
    }

    var size = RUN

    while (size < n) {

      for (left <- 0 until n by 2 * size) {

        val mid = left + size - 1
        val right = Math.min((left + 2 * size - 1), n - 1)

        if (mid < right) merge(arr, left, mid, right)

      }

      size *= 2

    }

  }

  def main(args: Array[String]): Unit = {

    val data = Array(5, 21, 7, 23, 19, 3, 12, 8)

    timSort(data)

    println("Sorted Array: " + data.mkString(", "))

  }

}

This version works by first sorting small runs using Insertion Sort, then merging them iteratively. Beginners can see how breaking a large array into small, manageable pieces simplifies sorting and makes the overall algorithm efficient.

Program 2: Recursive Tim Sort

This program introduces recursion for merging runs, which can help beginners understand how recursion simplifies repeated merging tasks.

object TimSortRecursive {

  val RUN = 32

  def insertionSort(arr: Array[Int], left: Int, right: Int): Unit = {

    for (i <- left + 1 to right) {

      val temp = arr(i)
      var j = i - 1

      while (j >= left && arr(j) > temp) {
        arr(j + 1) = arr(j)
        j -= 1
      }

      arr(j + 1) = temp

    }

  }

  def merge(arr: Array[Int], l: Int, m: Int, r: Int): Unit = {

    val left = arr.slice(l, m + 1)
    val right = arr.slice(m + 1, r + 1)
    var i, j, k = 0
    var leftIndex = l

    while (i < left.length && j < right.length) {

      if (left(i) <= right(j)) {
        arr(leftIndex) = left(i)
        i += 1
      } else {
        arr(leftIndex) = right(j)
        j += 1
      }

      leftIndex += 1

    }

    while (i < left.length) { arr(leftIndex) = left(i); i += 1; leftIndex += 1 }
    while (j < right.length) { arr(leftIndex) = right(j); j += 1; leftIndex += 1 }

  }

  def timSortRecursive(arr: Array[Int], l: Int, r: Int): Unit = {

    if (r - l + 1 <= RUN) {
      insertionSort(arr, l, r)
      return
    }

    val m = l + (r - l) / 2

    timSortRecursive(arr, l, m)
    timSortRecursive(arr, m + 1, r)
    merge(arr, l, m, r)

  }

  def main(args: Array[String]): Unit = {

    val data = Array(30, 10, 50, 20, 60, 40, 70)

    timSortRecursive(data, 0, data.length - 1)

    println("Sorted Array: " + data.mkString(", "))

  }

}

By using recursion, the algorithm handles repeated merging automatically. Beginners can appreciate how recursion simplifies merging logic while maintaining the hybrid nature of Tim Sort.

Program 3: Tim Sort for Strings

This example shows how Tim Sort can handle string arrays, using lexicographical order for comparison.

object TimSortStrings {

  val RUN = 32

  def insertionSort(arr: Array[String], left: Int, right: Int): Unit = {

    for (i <- left + 1 to right) {

      val temp = arr(i)
      var j = i - 1

      while (j >= left && arr(j) > temp) {
        arr(j + 1) = arr(j)
        j -= 1
      }

      arr(j + 1) = temp

    }

  }

  def merge(arr: Array[String], l: Int, m: Int, r: Int): Unit = {

    val left = arr.slice(l, m + 1)
    val right = arr.slice(m + 1, r + 1)
    var i, j, k = 0
    var index = l

    while (i < left.length && j < right.length) {

      if (left(i) <= right(j)) { arr(index) = left(i); i += 1 }
      else { arr(index) = right(j); j += 1 }
      index += 1

    }

    while (i < left.length) { arr(index) = left(i); i += 1; index += 1 }
    while (j < right.length) { arr(index) = right(j); j += 1; index += 1 }

  }

  def timSort(arr: Array[String]): Unit = {

    val n = arr.length

    for (i <- 0 until n by RUN) insertionSort(arr, i, Math.min(i + RUN - 1, n - 1))

    var size = RUN

    while (size < n) {

      for (left <- 0 until n by 2 * size) {

        val mid = left + size - 1
        val right = Math.min(left + 2 * size - 1, n - 1)

        if (mid < right) merge(arr, left, mid, right)

      }

      size *= 2

    }

  }

  def main(args: Array[String]): Unit = {

    val words = Array("banana", "apple", "orange", "grape", "cherry")

    timSort(words)

    println("Sorted Words: " + words.mkString(", "))

  }

}

Sorting strings shows that Tim Sort is versatile. Beginners can observe how comparing elements works for different data types while keeping the same hybrid algorithm logic.

Program 4: Tim Sort with Custom Run Size

This program allows experimenting with different run sizes to optimize performance depending on dataset size.

object TimSortCustomRun {

  def insertionSort(arr: Array[Int], left: Int, right: Int): Unit = {

    for (i <- left + 1 to right) {

      val temp = arr(i)
      var j = i - 1

      while (j >= left && arr(j) > temp) { arr(j + 1) = arr(j); j -= 1 }

      arr(j + 1) = temp

    }

  }

  def merge(arr: Array[Int], l: Int, m: Int, r: Int): Unit = {

    val left = arr.slice(l, m + 1)
    val right = arr.slice(m + 1, r + 1)
    var i = 0; var j = 0; var k = l

    while (i < left.length && j < right.length) {

      if (left(i) <= right(j)) { arr(k) = left(i); i += 1 } 
      else { arr(k) = right(j); j += 1 }
      k += 1

    }

    while (i < left.length) { arr(k) = left(i); i += 1; k += 1 }
    while (j < right.length) { arr(k) = right(j); j += 1; k += 1 }

  }

  def timSort(arr: Array[Int], RUN: Int): Unit = {

    val n = arr.length

    for (i <- 0 until n by RUN) insertionSort(arr, i, Math.min(i + RUN - 1, n - 1))

    var size = RUN

    while (size < n) {

      for (left <- 0 until n by 2 * size) {

        val mid = left + size - 1
        val right = Math.min(left + 2 * size - 1, n - 1)

        if (mid < right) merge(arr, left, mid, right)

      }

      size *= 2

    }

  }

  def main(args: Array[String]): Unit = {

    val data = Array(34, 7, 23, 32, 5, 62, 32)

    timSort(data, 16)

    println("Sorted Array: " + data.mkString(", "))

  }

}

This approach allows beginners to experiment with the run size, showing how performance can be affected by small parameter changes. It reinforces the idea of tuning algorithms for different datasets.

Program 5: Tim Sort for Floating Point Numbers

This example adapts Tim Sort for decimal numbers, demonstrating its versatility beyond integers.

object TimSortFloat {

  val RUN = 32

  def insertionSort(arr: Array[Double], left: Int, right: Int): Unit = {

    for (i <- left + 1 to right) {

      val temp = arr(i)
      var j = i - 1

      while (j >= left && arr(j) > temp) { arr(j + 1) = arr(j); j -= 1 }

      arr(j + 1) = temp

    }

  }

  def merge(arr: Array[Double], l: Int, m: Int, r: Int): Unit = {

    val left = arr.slice(l, m + 1)
    val right = arr.slice(m + 1, r + 1)
    var i = 0; var j = 0; var k = l

    while (i < left.length && j < right.length) {

      if (left(i) <= right(j)) { arr(k) = left(i); i += 1 } 
      else { arr(k) = right(j); j += 1 }
      k += 1

    }

    while (i < left.length) { arr(k) = left(i); i += 1; k += 1 }
    while (j < right.length) { arr(k) = right(j); j += 1; k += 1 }

  }

  def timSort(arr: Array[Double]): Unit = {

    val n = arr.length

    for (i <- 0 until n by RUN) insertionSort(arr, i, Math.min(i + RUN - 1, n - 1))

    var size = RUN

    while (size < n) {

      for (left <- 0 until n by 2 * size) {

        val mid = left + size - 1
        val right = Math.min(left + 2 * size - 1, n - 1)

        if (mid < right) merge(arr, left, mid, right)

      }

      size *= 2

    }

  }

  def main(args: Array[String]): Unit = {

    val data = Array(3.4, 1.2, 5.6, 2.3, 4.1)

    timSort(data)

    println("Sorted Array: " + data.mkString(", "))

  }

}

Sorting floating point numbers shows the flexibility of Tim Sort. Beginners can see that the same logic applies to decimals, reinforcing the idea that hybrid algorithms are adaptable to various data types.

Frequently Asked Questions (FAQ)

This section addresses common questions learners have when first exploring Tim Sort in Scala.

Q1: Why is Tim Sort faster than Merge Sort for real data?
Because it takes advantage of existing ordered sequences (runs) in the data, reducing unnecessary comparisons.

Q2: Can Tim Sort handle strings or floating-point numbers?
Yes, it works with any data that can be compared.

Q3: Is Tim Sort stable?
Yes, it preserves the relative order of equal elements.

Q4: How do you choose the run size?
Typical run sizes range from 32 to 64, but it can be adjusted depending on data size and type.

Q5: Why is Tim Sort used in Python and Java?
It combines efficiency, stability, and adaptability to partially ordered datasets, making it ideal for practical applications.

Conclusion

Tim Sort is a powerful and practical hybrid sorting algorithm that combines the best features of Insertion Sort and Merge Sort. By learning multiple implementations in Scala—including arrays, recursion, strings, floating-point numbers, and custom run sizes—beginners can understand both the logic and the adaptability of this algorithm.

Practicing Tim Sort with different types of data helps reinforce concepts like hybrid sorting, array manipulation, and efficient merging. Exploring its variations in Scala will give you confidence to apply hybrid sorting techniques to real-world problems and make your code faster and more reliable.

Scroll to Top