Я потратил большую часть мая и июня на подготовку к собеседованию, и под подготовкой к собеседованию я имею в виду много работы с Leetcode. Я решил решить большинство проблем в Kotlin, так как я подал заявку на должности Android, и заметил, что Kotlin, который я писал, сильно отличался от Kotlin, который я использую на работе. Я решил скомпилировать обзор фрагментов кода, которые часто возникали во время подготовки к собеседованию, в шпаргалку, а затем глубоко погрузиться в фрагменты кода в серии из 5 сообщений Medium. Шпаргалку можно найти здесь.

В части 1 я расскажу о нескольких общих типах данных, которые часто встречаются в вопросах алгоритмов и структур данных - как их инициализировать, как выполнять с ними некоторые общие операции и некоторые варианты использования. Мы перейдем к следующему:

Вы можете найти Часть 2 здесь, Часть 3 здесь, 4 - здесь и Часть 5 - здесь.

MutableList

Это структура данных, которую я использовал чаще всего, и вы, вероятно, уже знакомы с ней. Я хочу отметить, что вы можете легко использовать его как стек или очередь, если столкнетесь с проблемой, которая требует этого; Обычно я использую MutableList для представления очереди при поиске в ширину.

// Create an empty MutableList
val list1 = mutableListOf<Int>()
// Create a MutableList with elements [0, 1, 2, 3, 4]
val list2 = mutableListOf(0, 1, 2, 3, 4)

Использование MutableList в качестве стека

  • add() добавляет элемент в конец списка и может использоваться как push
  • removeLast() удаляет последний элемент из списка и возвращает его, может использоваться как pop
  • last() возвращает последний элемент, не удаляя его, и может использоваться как peek
val stack: MutableList<Int> = mutableListOf()
// push
stack.add(1)
stack.add(2)
// peek
stack.last() // returns 2
// pop
stack.removeLast() // returns 2
stack.removeLast() // returns 1

Использование MutableList в качестве очереди

  • add() можно использовать как enqueue
  • removeAt(0) удаляет первый элемент из списка и возвращает его, может использоваться как dequeue
  • get(0) возвращает первый элемент и может использоваться как peek
val queue: MutableList<Int> = mutableListOf()
// enqueue
queue.add(1)
queue.add(2)
// peek
queue[0] // returns 1
// dequeue
queue.removeAt(0) // returns 1
queue.removeAt(0) // returns 2

Множество

Массивы кажутся простыми, но когда я начал готовиться к собеседованию, я понял, что не очень хорошо их знаю, потому что вместо этого использую списки. Когда проблема Leetcode имела ввод массива или ожидал вывода массива, мне приходилось искать в Google.

В Kotlin массивы имеют фиксированный размер и поддерживают get(), set() и size, а также большинство функций сбора Kotlin. Вы можете использовать [], также известный как оператор индексации, для более лаконичного получения и установки. Вы не можете изменять размер или удалять элементы. Я считаю массивы полезными для задач, когда вы хотите отслеживать фиксированное количество счетчиков или флагов.

Есть два способа инициализировать массивы. Вы можете использовать arrayOf<T>():

// Create an array with elements [0, 1, 2, 3, 4]
val array1 = arrayOf<Int>(0, 1, 2, 3, 4)

Или используйте Array<T>(n) { initFunction }. Это создаст массив размером n, где каждый элемент создается с использованием initFunction. initFunction может быть просто значением по умолчанию, например -1 для массива Ints или блока кода.

// Create an array with elements [0, 0, 0, 0, 0]
val array2 = Array<Int>(5) { 0 }
// Create an array with elements [0, 2, 4, 6, 8]
// The initFunction block can access the index as a parameter.
// Here, each element is initialized by taking its index and multiplying it by 2.
val array3 = Array<Int>(5) { index -> 2 * index }

Также существуют специальные методы arrayOf для следующих примитивных типов: double , float , long , int , char , short , byte , boolean, которые обеспечивают лучшую производительность, избегая накладных расходов на бокс Kotlin:

// Create an array with elements [0, 1, 2, 3, 4]
val intArray1 = intArrayOf(0, 1, 2, 3, 4)
// Create an array with elements [0, 0, 0, 0, 0]
val intArray2 = IntArray(5) { 0 }
// Create an array with elements [true, true, false, true, true]
val booleanArray1 = booleanArrayOf(true, true, false, true, true)
// Create an array with elements [true, false, true, false, true]
// Each element is initialized by by whether or not its index is even
val booleanArray2 = BooleanArray(5) { index -> index % 2 == 0 }

Текущая сумма 1d массива - это пример простой задачи Leetcode, в которой вы можете использовать IntArray для отслеживания результатов:

/**
* Problem description: Given an array nums. We define a running sum 
* of an array as runningSum[i] = sum(nums[0]…nums[i]). Return the 
* running sum of nums.
*
* Example:
* Input: nums = [1,2,3,4]
* Output: [1,3,6,10]
* Explanation: Running sum is obtained as follows: [1, 1+2, 1+2+3, 
* 1+2+3+4].
** /
fun runningSum(nums: IntArray): IntArray {
    val results = IntArray(nums.size) { 0 }
    results[0] = nums[0]
    for (i in 1 until nums.size) {
        results[i] = results[i - 1] + nums[i]
    }
    return results
}

HashMap

HashMaps представляют собой набор пар ключ-значение, где ключи уникальны, а карта может иметь только одно значение для каждого ключа. Они часто используются в задачах собеседования для хранения информации. Например, HashMap<Node, Boolean> может хранить, какие узлы в графе уже были посещены, а HashMap<String, Int> может хранить количество различных слов, появляющихся в предложении.

Несколько полезных функций:

  • clear() удаляет все элементы с карты.
  • containsKey(key: K) возвращает истину, если карта содержит указанный ключ.
  • remove(key: K) удаляет указанный ключ и соответствующее ему значение из этой карты.
  • getOrPut(key: K, defaultValue: () -> V) возвращает значение для данного ключа, если он есть на карте. Если это не так, вызовите функцию defaultValue, поместите ее результат на карту под заданным ключом и верните его.
// Create an empty HashMap
val map1 = hashMapOf<String, Int>()
// Create a HashMap with initial values
val map2 = hashMapOf(
    "Never Let Me Go" to 2005,
    "A Little Life" to 2015
)
// Two ways to insert a key-value pair
map2.put("The Name of the Wind", 2007)
map2["The Bell Jar"] = 1963
// Two ways to retrieve a value
map2.get("The Name of the Wind") // returns 2007
map2["The Bell Jar"] // returns 1963
// Using getOrPut()
map2.getOrPut("The Name of the wind", 1990) // Returns 2007
map2.getOrPut("1984", 1949) // Inserts new entry and returns 1949

PriorityQueue

PriorityQueues полезны, когда вы хотите, чтобы элементы обрабатывались на основе их приоритета. Этот класс реализован с использованием кучи. Они часто используются в задачах собеседования, которые требуют K-го наибольшего, K-го наименьшего, наиболее частого K и т. Д.

Вы можете инициализировать его с помощью PriorityQueue<T>(), где для определения приоритета будет использоваться естественный порядок типа T, или с помощью пользовательского Comparator, например PriorityQueue<T> { (a: T, b: T) -> Int }, где вы указываете собственный способ определения приоритета.

Несколько полезных функций:

  • add(element: E) и offer(element: E) оба вставляют элемент, и вы можете использовать любой из них; PriorityQueue имеет две функции, которые делают одно и то же, потому что он реализует два интерфейса, Collection и Queue. Коллекции используют add() для вставок, тогда как очереди используют offer() .
  • peek() возвращает, но не удаляет заголовок очереди.
  • poll() возвращает и удаляет заголовок очереди.
// Create a PriorityQueue pq where the maximum value Int has highest 
// priority
val pq = PriorityQueue<Int> {
    a, b -> b - a
}
// Add 2, 1, 3 to the pq
pq.offer(2)
pq.add(1)
pq.offer(3)

pq.peek() // returns 3 but does not remove it from pq
// pq will be empty after 3 iterations
while(pq.isNotEmpty()) {
    pq.poll() // returns 3, then 2, then 1
}

Компаратор

Интерфейс Comparator<T> предоставляет функцию сравнения для установления общего порядка между экземплярами типа T. Это часто бывает полезно для технических собеседований, требующих сортировки или определения приоритетов, потому что большинство алгоритмов сортировки Kotlin позволяют передавать Comparator.

Вы можете использовать функцию compareBy() Kotlin, чтобы создать Comparator, который использует последовательность переданных вами функций для вычисления результатов сравнения. Последовательность функций оценивается последовательно на парах значений, и как только результаты для двух значений, возвращаемых функцией, не сравниваются как равные, возвращается результат, а остальные функции для этой пары пропускаются.

val customComparator = compareBy<T> { 
    ...
}

Вот пример Comparator, который сравнивает Ints по абсолютному значению:

val absComparator = compareBy<Int>{ Math.abs(it) }
val nums: MutableList<Int> = mutableListOf(-1, 3, 7, -5, 3)
nums.sortWith(absComparator)
// Or if you only need to use the comparator once, you can pass in 
// the call to compareBy() directly.
val nums: MutableList<Int> = mutableListOf(-1, 3, 7, -5, 3)
nums.sortWith(compareBy{ Math.abs(it) })

Вот пример Comparator, который сравнивает пользовательский класс Card по свойству цвета, а BLACK карточки «лучше», чем RED карточки. Мы будем использовать свойство number, чтобы разорвать связи:

enum class Color { RED, BLACK }
data class Card(val number: Int, val color: Color)
val cardComparator = compareBy(
    // Compare by color first 
    { if(it.color == BLACK) 1 else 0 }, 
    // If the results of color comparison are equal, compare by 
    // number
    { it.number }
)
val cards: MutableList<Card> = mutableListOf(
    Card(4, Color.RED), 
    Card(2, Color.BLACK), 
    Card(1, Color.RED), 
    Card(3, Color.BLACK)
)
cards.sortWith(cardComparator) 
// cards becomes:
// [Card(1, RED), Card(4, RED), Card(2, BLACK), Card(3, BLACK)]

Если сравнение сложно, вы можете создать объект, который явно реализует интерфейс Comparator и переопределит функцию compare() . Он должен возвращать ноль, если аргументы равны, отрицательное число, если первый аргумент меньше второго, или положительное число, если первый аргумент больше второго.

object CustomComparator: Comparator<T>{ 
        override fun compare(a: T, b: T): Int { 
            ...
            return ...
        }
}

Вот как будет выглядеть объект Comparator при сравнении карт:

object cardComparator: Comparator<Card>{ 
        override fun compare(a: Card, b: Card): Int { 
            if (a.color == b.color) {
                return a.number - b.number
            } else if (a.color == Color.BLACK) {
                // Case where a is BLACK and b is RED
                return 1
            } else {
                // Case where a is RED and b is BLACK                 
                return -1
            }
        }
}

Сопоставимый

Для настраиваемых классов вы также можете сделать так, чтобы ваш класс расширил интерфейс Comparable и переопределил compareTo() для наложения общего порядка, а не для создания отдельного объекта. Вы можете использовать вспомогательную функцию Kotlin compareValuesBy(), которая, как и compareBy(), использует последовательность переданных вами функций для вычисления результатов сравнения. Поэтому вместо того, чтобы использовать sortWith() и передавать свой собственный компаратор, вы можете просто использовать sort(), и функция будет знать, как выполнить полное упорядочение.

enum class Color { RED, BLACK }
data class Card(val number: Int, val color: Color)
        : Comparable<Card> {
    override operator fun compareTo(other: Card): Int {
        compareValuesBy(
            this, 
            other, 
            // Compare by color first
            { it.color }, 
            // If the results of color comparison are equal, 
            // compare by number
            { it.number }
        ) 
    }
}
val cards: MutableList<Card> = mutableListOf(
    Card(4, Color.RED), 
    Card(2, Color.BLACK), 
    Card(1, Color.RED), 
    Card(3, Color.BLACK)
)
cards.sort() 
// cards becomes:
// [Card(1, RED), Card(4, RED), Card(2, BLACK), Card(3, BLACK)]

Опять же, у вас есть возможность реализовать compareTo() без помощника compareValuesBy(), если вам будет понятнее:

data class Card(val number: Int, val color: Color)
        : Comparable<Card> {
    override operator fun compareTo(other: Card): Int {
        if (this.color == other.color) {
            return this.number - other.number
        } else if (this.color == Color.BLACK) {
            return 1
        } else {
            return -1
        }
    }
}

И это первая часть! В Части 2 я рассмотрю полезные функции коллекции Kotlin. Вот ссылка на шпаргалку, снова охватывающую все 5 частей.

Щелкните 👏, чтобы сказать «спасибо!» и помогите другим найти эту статью.

Чтобы быть в курсе отличных новостей о Kt. Academy, подписывайтесь на рассылку новостей, следите за Twitter и следите за нами на Medium.

Если вам нужна мастерская Kotlin, узнайте, чем мы можем вам помочь: kt.academy.