diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..49723a7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,18 @@ +name: CI +on: + pull_request: + push: +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4.1.1 + - name: Setup JDK + uses: actions/setup-java@v4.1.0 + with: + java-version: 17 + distribution: temurin + cache: sbt + - name: Build + run: sbt rebuild diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29ddfef --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.bsp +/.idea +/project/target +/project/project/target +/target \ No newline at end of file diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..6faf131 --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,4 @@ +version = 3.8.0 +runner.dialect = scala3 + +rewrite.trailingCommas.style = multiple diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..7be454a --- /dev/null +++ b/build.sbt @@ -0,0 +1,31 @@ +ThisBuild / version := "0.1.0-SNAPSHOT" + +ThisBuild / scalaVersion := "3.3.1" + +ThisBuild / scalacOptions ++= Seq( + "-encoding", + "utf8", + "--release:17", + "-deprecation", + "-Xfatal-warnings", +) + +lazy val root = (project in file(".")) + .settings( + name := "algorithms", + libraryDependencies ++= Seq( + "org.scalatest" %% "scalatest" % "3.2.19" % Test + ), + ) + +commands ++= Seq( + Command.command("build") { state => + "scalafmtCheckAll" :: + "scalafmtSbtCheck" :: + "test" :: + state + }, + Command.command("rebuild") { state => + "clean" :: "build" :: state + }, +) diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..04267b1 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.9.9 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..7d517ef --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") diff --git a/src/main/scala/com/github/skozlov/algorithms/sort/InsertionSort.scala b/src/main/scala/com/github/skozlov/algorithms/sort/InsertionSort.scala new file mode 100644 index 0000000..e929997 --- /dev/null +++ b/src/main/scala/com/github/skozlov/algorithms/sort/InsertionSort.scala @@ -0,0 +1,74 @@ +package com.github.skozlov.algorithms.sort + +import scala.annotation.tailrec +import scala.collection.mutable +import scala.math.Ordered.orderingToOrdered + +object InsertionSort { + + /** Sorts the input sequence putting elements to the output sequence. + * + * The sequences may be independent (then the input sequence isn't modified) + * or be the same sequence (then in-place sort is performed). + * + * This sort is stable. + * + * Time consumption: O(n2). + * + * Memory consumption: O(1). + * @param in + * input sequence which contains elements to sort + * @param out + * output sequence to put sorted elements into + * @throws IllegalArgumentException + * if the input and output sequences have different sizes + */ + def sort[A: Ordering]( + in: collection.IndexedSeq[A], + out: mutable.IndexedSeq[A], + ): Unit = { + require( + in.size == out.size, + s"in and out have different sizes: ${in.size} and ${out.size}", + ) + + if (in.nonEmpty) { + + /** Assuming that out[0:index-1] contains sorted in[0:index-1], inserts + * in[index] into out[0:index-1] so that out[0:index] contains sorted + * in[0:index]. + */ + def insert(index: Int): Unit = { + val elementToInsert = in(index) + + /** In out sequence, shifts elements which are greater than + * elementToInsert one position to the right, going right to left + * starting from startIndex. + * @return + * the index to insert elementToInsert into after the shift + */ + @tailrec + def shiftGreaterReturningTargetIndex(startIndex: Int): Int = { + val elementToCompare = out(startIndex) + if (elementToCompare > elementToInsert) { + out(startIndex + 1) = elementToCompare + if (startIndex == 0) { + 0 + } else { + shiftGreaterReturningTargetIndex(startIndex - 1) + } + } else startIndex + 1 + } + + val targetIndex = + shiftGreaterReturningTargetIndex(startIndex = index - 1) + out(targetIndex) = elementToInsert + } + + out(0) = in(0) + for (i <- 1 until in.size) { + insert(i) + } + } + } +} diff --git a/src/test/scala/com/github/skozlov/algorithms/Test.scala b/src/test/scala/com/github/skozlov/algorithms/Test.scala new file mode 100644 index 0000000..7f46548 --- /dev/null +++ b/src/test/scala/com/github/skozlov/algorithms/Test.scala @@ -0,0 +1,6 @@ +package com.github.skozlov.algorithms + +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +abstract class Test extends AnyFunSuite with Matchers diff --git a/src/test/scala/com/github/skozlov/algorithms/sort/SortTest.scala b/src/test/scala/com/github/skozlov/algorithms/sort/SortTest.scala new file mode 100644 index 0000000..d09701b --- /dev/null +++ b/src/test/scala/com/github/skozlov/algorithms/sort/SortTest.scala @@ -0,0 +1,73 @@ +package com.github.skozlov.algorithms.sort + +import com.github.skozlov.algorithms.Test + +class SortTest extends Test { + private val cases: Seq[Seq[(Int, Int)]] = { + Seq( + Seq.empty[Int], + Seq(1), + Seq(1, 2, 3), + Seq(1, 3, 2), + Seq(2, 1, 3), + Seq(2, 3, 1), + Seq(3, 1, 2), + Seq(3, 2, 1), + Seq(1, 3, 2, 3, 1), + ) map { _.zipWithIndex } + } + + private implicit val ordering: Ordering[(Int, Int)] = + Ordering.by[(Int, Int), Int](_._1) + + private val commonExpectedResults: Seq[Seq[Int]] = cases map { + _.map { _._1 }.sorted + } + + private val stableSortExpectedResults: Seq[Seq[(Int, Int)]] = cases map { + _.sorted(Ordering.by[(Int, Int), Int](_._1).orElseBy(_._2)) + } + + private def checkResults( + results: Seq[Seq[(Int, Int)]], + stableSort: Boolean, + ): Unit = { + if (stableSort) { + results shouldBe stableSortExpectedResults + } else { + (results map { _ map { _._1 } }) shouldBe commonExpectedResults + } + } + + private def testInPlaceSort( + sort: InsertionSort.type, + stable: Boolean, + ): Unit = { + val arrays: Seq[Array[(Int, Int)]] = cases map { _.toArray } + val results: Seq[Seq[(Int, Int)]] = for (array <- arrays) yield { + sort.sort(in = array, out = array) + array.toSeq + } + checkResults(results, stable) + } + + private def testImmutableInputSort( + sort: InsertionSort.type, + stable: Boolean, + ): Unit = { + val inputsAndOutputsAfterSort: Seq[(Array[(Int, Int)], Array[(Int, Int)])] = + for { + _case: Seq[(Int, Int)] <- cases + input: Array[(Int, Int)] = _case.toArray + output: Array[(Int, Int)] = Array.ofDim[(Int, Int)](_case.size) + _ = sort.sort(input, output) + } yield (input, output) + checkResults(inputsAndOutputsAfterSort map { _._2.toSeq }, stable) + (inputsAndOutputsAfterSort map { _._1.toSeq }) shouldBe cases + } + + test("insertion sort") { + testInPlaceSort(InsertionSort, stable = true) + testImmutableInputSort(InsertionSort, stable = true) + } +}