Алгоритмы и структуры данных на Kotlin: практическое руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Алгоритмы и структуры данных на Kotlin: от теории к практике

Kotlin — современный, лаконичный и безопасный язык, который активно используется для разработки под Android, бэкенда и даже нативных приложений. Однако его мощь раскрывается полностью только при правильном применении алгоритмов и структур данных. В этой статье мы разберем ключевые концепции на практических примерах, которые помогут вам писать эффективный и поддерживаемый код.

1. Базовые структуры данных: списки, стеки и очереди

Начнем с фундамента. Kotlin предоставляет богатую стандартную библиотеку, но понимание того, как работают структуры данных «под капотом», критически важно.

Связный список (LinkedList)

Реализуем простой односвязный список вручную. Это поможет понять принципы работы с памятью и ссылками.

class Node<T>(var value: T, var next: Node<T>? = null)

class LinkedList<T> {
    private var head: Node<T>? = null
    private var tail: Node<T>? = null

    fun add(value: T) {
        val node = Node(value)
        if (head == null) {
            head = node
            tail = node
        } else {
            tail?.next = node
            tail = node
        }
    }

    fun printAll() {
        var current = head
        while (current != null) {
            print("${current.value} ")
            current = current.next
        }
        println()
    }
}

// Использование
fun main() {
    val list = LinkedList<Int>()
    list.add(1)
    list.add(2)
    list.add(3)
    list.printAll() // Вывод: 1 2 3
}

Стек на основе массива

Стек работает по принципу LIFO (Last In, First Out). Реализуем его с помощью изменяемого списка Kotlin.

class Stack<T> {
    private val elements = mutableListOf<T>()

    fun push(item: T) = elements.add(item)
    fun pop(): T? = if (elements.isNotEmpty()) elements.removeAt(elements.size - 1) else null
    fun peek(): T? = elements.lastOrNull()
    fun isEmpty() = elements.isEmpty()
}

// Пример: проверка скобок
fun isBalanced(expression: String): Boolean {
    val stack = Stack<Char>()
    for (char in expression) {
        when (char) {
            '(', '{', '[' -> stack.push(char)
            ')' -> if (stack.pop() != '(') return false
            '}' -> if (stack.pop() != '{') return false
            ']' -> if (stack.pop() != '[') return false
        }
    }
    return stack.isEmpty()
}

fun main() {
    println(isBalanced("({[]})")) // true
    println(isBalanced("({[})"))  // false
}

2. Алгоритмы сортировки: от пузырька до быстрой

Сортировка — одна из самых изучаемых тем. Рассмотрим три классических алгоритма, которые легко реализовать на Kotlin.

Пузырьковая сортировка (Bubble Sort)

Простая, но неэффективная для больших массивов (O(n²)).

fun bubbleSort(arr: IntArray) {
    val n = arr.size
    for (i in 0 until n - 1) {
        for (j in 0 until n - i - 1) {
            if (arr[j] > arr[j + 1]) {
                val temp = arr[j]
                arr[j] = arr[j + 1]
                arr[j + 1] = temp
            }
        }
    }
}

fun main() {
    val array = intArrayOf(64, 34, 25, 12, 22, 11, 90)
    bubbleSort(array)
    println(array.joinToString()) // 11, 12, 22, 25, 34, 64, 90
}

Быстрая сортировка (Quick Sort)

Один из самых быстрых алгоритмов в среднем случае (O(n log n)). Использует рекурсию и разделение массива.

fun quickSort(arr: IntArray, low: Int, high: Int) {
    if (low < high) {
        val pi = partition(arr, low, high)
        quickSort(arr, low, pi - 1)
        quickSort(arr, pi + 1, high)
    }
}

fun partition(arr: IntArray, low: Int, high: Int): Int {
    val pivot = arr[high]
    var i = low - 1
    for (j in low until high) {
        if (arr[j] < pivot) {
            i++
            val temp = arr[i]
            arr[i] = arr[j]
            arr[j] = temp
        }
    }
    val temp = arr[i + 1]
    arr[i + 1] = arr[high]
    arr[high] = temp
    return i + 1
}

fun main() {
    val array = intArrayOf(10, 7, 8, 9, 1, 5)
    quickSort(array, 0, array.size - 1)
    println(array.joinToString()) // 1, 5, 7, 8, 9, 10
}

3. Деревья и хеш-таблицы: ключ к быстрому поиску

Эти структуры данных обеспечивают эффективный доступ к данным. Рассмотрим бинарное дерево поиска и простую хеш-таблицу.

Бинарное дерево поиска (BST)

Каждый узел имеет не более двух потомков. Левый потомок меньше родителя, правый — больше.

class TreeNode<T : Comparable<T>>(var value: T) {
    var left: TreeNode<T>? = null
    var right: TreeNode<T>? = null
}

class BinarySearchTree<T : Comparable<T>> {
    private var root: TreeNode<T>? = null

    fun insert(value: T) {
        root = insertRec(root, value)
    }

    private fun insertRec(node: TreeNode<T>?, value: T): TreeNode<T> {
        if (node == null) return TreeNode(value)
        if (value < node.value) node.left = insertRec(node.left, value)
        else if (value > node.value) node.right = insertRec(node.right, value)
        return node
    }

    fun search(value: T): Boolean {
        return searchRec(root, value)
    }

    private fun searchRec(node: TreeNode<T>?, value: T): Boolean {
        if (node == null) return false
        return when {
            value == node.value -> true
            value < node.value -> searchRec(node.left, value)
            else -> searchRec(node.right, value)
        }
    }

    fun inorder() {
        inorderRec(root)
        println()
    }

    private fun inorderRec(node: TreeNode<T>?) {
        if (node != null) {
            inorderRec(node.left)
            print("${node.value} ")
            inorderRec(node.right)
        }
    }
}

fun main() {
    val bst = BinarySearchTree<Int>()
    bst.insert(50)
    bst.insert(30)
    bst.insert(70)
    bst.insert(20)
    bst.insert(40)
    bst.inorder() // 20 30 40 50 70                

Блоги

Рекомендаций по книгам