Приведи пример алгоритма с неочевидной (плохо оцениваемой) временной сложностью.

Ответ

Частая ошибка — не учитывать сложность операций внутри цикла, что приводит к некорректной общей оценке.

Пример 1: Использование contains внутри цикла по массиву

// Задача: Удалить дубликаты из массива.
// Плохое решение: O(n²)
func removeDuplicatesBad<T: Equatable>(from array: [T]) -> [T] {
    var result: [T] = []
    for element in array {               // O(n) итераций
        if !result.contains(element) {   // O(m) для каждого элемента, где m растет до n
            result.append(element)       // В среднем O(1)
        }
    }
    return result
    // Общая сложность: O(n * m) → в худшем случае O(n²)
}

Почему оценка плохая: Наивный разработчик может посчитать только внешний цикл O(n), забыв, что contains для массива — это линейный поиск O(m). В худшем случае (все элементы уникальны) m растет вместе с n.

Пример 2: Конкатенация строк в цикле

// Задача: Собрать строку из массива.
// Плохое решение: O(n²)
func buildStringBad(from words: [String]) -> String {
    var result = ""
    for word in words {          // O(n) итераций
        result += word           // O(k) где k — длина текущей строки `result`
    }
    return result
    // Общая сложность: O(n + k1 + k2 + ...) ~ O(n²), так как строка `result` постоянно копируется.
}

Исправление (хорошая оценка и эффективный алгоритм):

// Хорошее решение: O(n) с использованием Set для проверки уникальности.
func removeDuplicatesGood<T: Hashable>(from array: [T]) -> [T] {
    var seen = Set<T>()          // Поиск в Set в среднем O(1)
    var result: [T] = []
    for element in array {       // O(n) итераций
        if seen.insert(element).inserted { // O(1)
            result.append(element)         // O(1)
        }
    }
    return result
    // Общая сложность: O(n)
}

// Хорошее решение для строк: O(n)
func buildStringGood(from words: [String]) -> String {
    return words.joined() // Использует внутренний буфер для эффективной конкатенации.
}

Вывод: Всегда анализируйте сложность вложенных операций. Операции с массивами (contains, first, insert(at: 0)) часто имеют O(n), что в цикле дает O(n²).

Ответ 18+ 🔞

А, слушай, вот это вообще классика, блядь! Люди ж как думают? Цикл по массиву — значит O(n), и всё, пиздец, задача решена. А потом приходит массив на десять тыщ элементов, и приложение накрывается медным тазом, а они сидят и чешут репу: «Ну что за хуйня, на айфоне шестом тормозит?»

Смотри, вот тебе прямо учебник по тому, как на ровном месте наделать O(n²), даже не заметив.

Пример первый: охотник за дубликатами, блядь

func removeDuplicatesBad<T: Equatable>(from array: [T]) -> [T] {
    var result: [T] = []
    for element in array {               // O(n) — ну да, очевидно же, ёпта!
        if !result.contains(element) {   // А вот это, сука, O(m)! И m растёт с каждой итерацией!
            result.append(element)
        }
    }
    return result
    // Итого: O(n * m) → а в худшем случае, когда все элементы уникальны, это O(n²), ядрёна вошь!
}

В чём подвох, блядь? Ну, цикл-то O(n), это каждый дурак скажет. А вот contains для массива — это же линейный поиск, ёпта! Он каждый раз пробегает по всему текущему result. Сначала там 0 элементов, потом 1, потом 2... и так до n. В сумме получается не O(n), а O(n²), потому что ты внутри цикла делаешь ещё одну хуйню, которая сама по себе растёт. Это как пытаться вычерпать лодку дырявым ведром — чем больше вычерпал, тем больше работы, блядь.

Пример второй: строитель строк-мучитель

func buildStringBad(from words: [String]) -> String {
    var result = ""
    for word in words {          // O(n) — опять мимо кассы, чувак!
        result += word           // А это, блядь, O(k)! Где k — длина ВСЕЙ текущей строки result!
    }
    return result
    // Итог: O(n²), потому что строка — неизменяемая сука, и каждый `+=` копирует её целиком!
}

Вот это вообще шедевр, блядь. Человек думает: «Я ж просто буквы добавляю». А на деле, каждый раз, когда ты делаешь result += word, система берёт ВСЮ старую строку result, копирует её в новое место, добавляет word и выкидывает старую. Представь, ты собрал строку в 10 тысяч символов и добавляешь слово «привет». Скопировать 10 тысяч символов — это O(k), ёпта! И так на каждой итерации. Пиздец и ужас.

А вот как надо, блядь, чтобы не было мучительно больно:

// Хорошее решение: O(n) с использованием Set. Он ищет за O(1), ёбана в рот!
func removeDuplicatesGood<T: Hashable>(from array: [T]) -> [T] {
    var seen = Set<T>()          // Волшебная структура, поиск в среднем O(1)
    var result: [T] = []
    for element in array {       // O(n) итераций
        if seen.insert(element).inserted { // O(1) — вот это да, блядь!
            result.append(element)         // O(1)
        }
    }
    return result
    // Общая сложность: O(n) — вот так-то, сука!
}

// Хорошее решение для строк: O(n), потому что умные дядьки в Apple уже всё придумали.
func buildStringGood(from words: [String]) -> String {
    return words.joined() // Использует внутренний буфер и не копирует всё каждый раз, хитрая жопа!
}

Вывод, блядь, который надо выжечь себе на подкорке: Нельзя смотреть только на внешний цикл. Надо смотреть, что ты делаешь ВНУТРИ него на каждой итерации. Если внутри есть contains по массиву, first(where:), insert(at: 0) или конкатенация строк — это почти наверняка O(n), которое, будучи внутри другого O(n), даёт тебе O(n²). И тогда твоё приложение начинает бздеть и тормозить на больших данных, а ты остаёшься в полном, блядь, недоумении. Думай головой, э бошка!