Irbisa · Шпаргалка

Структуры данных и алгоритмы

Массивы, деревья, графы, сортировка, поиск, нотация Big O

Senior-разработчик · 24 вопроса

  1. 01

    Что такое нотация Big O и сколько стоят типичные операции над List, Map и Set в Dart?

    Big O описывает, как растут время или память при росте входа, без учёта констант: он отвечает на вопрос «если n удвоится, что станет со стоимостью?».

  2. 02

    Что такое стек и очередь и какие классы Dart вы используете для каждого из них?

    Стек работает по принципу «последним пришёл, первым ушёл» (LIFO), а очередь по принципу «первым пришёл, первым ушёл» (FIFO).

    import 'dart:collection'; // Stack: a plain List, push = add, pop = removeLastbool isBalanced(String s) {  const pairs = {')': '(', ']': '[', '}': '{'};  final stack = <String>[];…
  3. 03

    Как Set или Map превращают поиск дубликатов, проверку на анаграмму и two-sum из O(n²) в O(n)?

    Вместо того чтобы сравнивать каждый элемент с каждым, запоминайте уже увиденное в Set или Map на хешах.

    bool hasDuplicate(List<int> xs) {  final seen = <int>{};  for (final x in xs) {    if (!seen.add(x)) return true; // add returns false if already present  }  return false;…
  4. 04

    Когда связный список выигрывает у List в Dart и чем вы за это платите?

    Только когда у вас уже есть узел, рядом с которым вы вставляете или который удаляете: связный список перевешивает ссылки за O(1), а List сдвигает всё после индекса.

  5. 05

    {} в Dart уже создаёт LinkedHashMap. Когда вы выберете вместо него HashMap, SplayTreeMap или отсортированный List?

    Оставляйте стандартный LinkedHashMap; берите HashMap, когда порядок не важен, а памяти мало, SplayTreeMap, когда ключи должны быть отсортированы или нужны запросы по диапазону, и отсортированный List с бинарным поиском для данных, которые строятся один раз.

  6. 06

    Какие алгоритмы сортировки нужно знать, сколько они стоят и что использует List.sort в Dart?

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

  7. 07

    Отсортируйте пользователей по очкам по убыванию, затем по имени: как написать компаратор и что гарантирует List.sort в Dart?

    Сначала сравнивайте очки, переходя к имени только при равенстве, а направление меняйте перестановкой операндов: b.score.compareTo(a.score).

    import 'package:collection/collection.dart'; class User {  const User(this.name, this.score);  final String name;  final int score;…
  8. 08

    Вы перемешиваете колоду вопросов через sort((a, b) => Random().nextInt(3) - 1), а порядок выходит смещённым: какой алгоритм правильный?

    Случайный компаратор не перемешивает.

    import 'dart:math'; // wrong: a random comparator breaks the sort contract and is biased// questions.sort((a, b) => Random().nextInt(3) - 1); // right: List.shuffle is Fisher-Yates, O(n) and uniform…
  9. 09

    Эта функция должна вернуть индекс первой метки времени >= target, но на некоторых входах зависает: в чём ошибка и как её исправить?

    int firstAtLeast(List<int> a, int target) {  var lo = 0, hi = a.length;  while (lo < hi) {    final mid = (lo + hi) ~/ 2;    if (a[mid] < target) {      lo = mid;    } else {      hi = mid;    }  }  return lo;}

    Когда a[mid] < target, mid уже точно не ответ, но lo = mid его оставляет.

  10. 10

    Как приёмы «два указателя» и «скользящее окно» превращают проход за O(n²) в O(n)?

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

    import 'dart:math'; // Two pointers on a sorted list: a pair with the given sum, O(n)(int, int)? pairWithSum(List<int> sorted, int target) {  var l = 0, r = sorted.length - 1;  while (l < r) {…
  11. 11

    Экран статистики позволяет выбрать любой период по 5000 дневных итогов и заново суммирует его при каждом движении ползунка: что делает каждый запрос O(1)?

    Постройте один массив префиксных сумм (prefix[i + 1] = prefix[i] + values[i]), и сумма любого диапазона станет одним вычитанием, prefix[r] - prefix[l].

    // Build once, O(n); the extra leading 0 removes the l == 0 special caseList<int> buildPrefix(List<int> values) {  final prefix = List<int>.filled(values.length + 1, 0);  for (var i = 0; i < values.length; i++) {    prefix[i + 1] = prefix[i] + values[i];  }…
  12. 12

    Разверните строку в Dart. Почему очевидный однострочник ломается на эмодзи и как выглядит правильный вариант?

    String в Dart хранит последовательность кодовых единиц UTF-16, поэтому разворот по единицам разрывает каждый символ, которому нужно больше одной.

    import 'package:characters/characters.dart'; // ships with Flutter String reverseBroken(String s) => s.split('').reversed.join(); // wrong: code unitsString reverseRunes(String s) =>    String.fromCharCodes(s.runes.toList().reversed); // still breaks flags, accentsString reverse(String s) => s.characters.toList().reversed.join(); // right…
  13. 13

    Когда рекурсия в Dart переполняет стек и как мемоизация исправляет наивный рекурсивный Фибоначчи?

    Каждый вызов занимает кадр стека, поэтому глубина, растущая вместе со входом (длинный связный список, глубокое дерево папок), заканчивается StackOverflowError.

    // O(2^n): fib(40) makes over 300 million callsint fibNaive(int n) => n < 2 ? n : fibNaive(n - 1) + fibNaive(n - 2); // O(n) with a memo, but still n frames deep on the first callint fibMemo(int n, [Map<int, int>? memo]) {  memo ??= {};…
  14. 14

    Найдите баг: этот счётчик путей по сетке с мемоизацией возвращает верное число, но сетка 14×14 считается уже больше десяти секунд.

    /// Paths from the top-left to the bottom-right cell, moving right or down; '#' is a wall.int countPaths(List<String> grid) {  final memo = <List<int>, int>{};   int go(int r, int c) {    if (r >= grid.length || c >= grid[0].length || grid[r][c] == '#') return 0;    if (r == grid.length - 1 && c == grid[0].length - 1) return 1;    final key = [r, c];    final cached = memo[key];    if (cached != null) return cached;    return memo[key] = go(r + 1, c) + go(r, c + 1);  }   return go(0, 0);}

    [r, c] создаёт новый List при каждом вызове, а List сравнивается по ссылке, поэтому memo[key] никогда не находит прежнюю запись.

  15. 15

    Как обойти бинарное дерево через DFS и BFS и какой порядок обхода подходит для какой задачи?

    Почти любая задача на деревья решается одним из двух обходов: DFS уходит как можно глубже, прежде чем вернуться, BFS обходит дерево по уровням с помощью очереди.

    import 'dart:collection'; class TreeNode {  TreeNode(this.val, [this.left, this.right]);  final int val;  final TreeNode? left, right;…
  16. 16

    Этот BFS выдаёт правильный порядок, но на графе из 100 000 вершин работает секунды: что делает его квадратичным?

    List<int> bfsOrder(Map<int, List<int>> graph, int start) {  final queue = [start];  final visited = [start];  final order = <int>[];  while (queue.isNotEmpty) {    final node = queue.removeAt(0);    order.add(node);    for (final next in graph[node] ?? const <int>[]) {      if (!visited.contains(next)) {        visited.add(next);        queue.add(next);      }    }  }  return order;}

    Внутри цикла стоят две линейные операции: queue.removeAt(0) сдвигает все оставшиеся элементы, а visited.contains просматривает List целиком.

  17. 17

    Найдите баг: эта проверка находит цикл в зависимостях задач запуска, где цикла нет.

    /// Returns true if the task dependency graph contains a cycle.bool hasCycle(Map<String, List<String>> deps) {  final visited = <String>{};  bool visit(String task) {    if (visited.contains(task)) return true;    visited.add(task);    for (final dep in deps[task] ?? const <String>[]) {      if (visit(dep)) return true;    }    return false;  }  for (final task in deps.keys) {    if (!visited.contains(task) && visit(task)) return true;  }  return false;}

    Одно множество visited не отличает «уже полностью обойдён» от «ещё на текущем пути», поэтому любая задача, до которой дошли дважды (config в ромбе auth → config, db → config), считается циклом.

  18. 18

    Какие алгоритмы на графах спрашивают на интервью (кратчайший путь через BFS, Дейкстра, топологическая сортировка) и когда применять каждый?

    BFS находит кратчайший путь, когда все рёбра стоят одинаково, Дейкстра находит его, когда у рёбер неотрицательные веса, а топологическая сортировка упорядочивает граф зависимостей.

    import 'package:collection/collection.dart'; /// Shortest distance from [start] to every reachable node; weights must be >= 0.Map<String, int> dijkstra(    Map<String, List<(String, int)>> graph, String start) {  final dist = <String, int>{start: 0};…
  19. 19

    Как понять, что задача решается динамическим программированием, и как перейти от обычной рекурсии к DP?

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

    import 'dart:math'; // Top-down: the plain recursion plus a memo keyed by the state (amount left)int minCoinsMemo(List<int> coins, int amount) {  final memo = <int, int>{};  int solve(int rem) {…
  20. 20

    Этот генератор комбинаций с заданной суммой возвращает правильное число результатов, но все они пустые: почему и как добавить отсечение?

    List<List<int>> combinations(List<int> nums, int target) {  final results = <List<int>>[], path = <int>[];  void backtrack(int start, int remaining) {    if (remaining == 0) {      results.add(path);      return;    }    for (var i = start; i < nums.length; i++) {      path.add(nums[i]);      backtrack(i + 1, remaining - nums[i]);      path.removeLast();    }  }  backtrack(0, target);  return results;}

    results.add(path) каждый раз сохраняет один и тот же объект списка.

  21. 21

    Поступает поток из десяти миллионов событий, и нужно 100 событий с наибольшими очками. Какую структуру вы возьмёте и сколько это стоит?

    Держите min-кучу ровно из K элементов: добавляйте каждое новое значение и снимайте наименьшее, как только куча превысит K.

    import 'package:collection/collection.dart'; typedef Event = ({String id, int score}); Future<List<Event>> topK(Stream<Event> events, int k) async {  if (k <= 0) return const [];…
  22. 22

    Кэш картинок должен вытеснять давно не используемую запись за O(1): как построить его в Dart и что уже даёт обычный Map?

    Хеш-мапа плюс двусвязный список дают O(1) на get и put; в Dart обычный Map реализован как LinkedHashMap, он хранит порядок вставки, поэтому удаление и повторная вставка при каждом попадании и вытеснение keys.first делают то же самое в несколько строк.

    class LruCache<K, V> {  LruCache(this.capacity);  final int capacity;  // Default Map is a LinkedHashMap: keys.first is the least recently used  final _entries = <K, V>{};…
  23. 23

    Слияние дубликатов контактов: после миллиона слияний вопрос «это один и тот же человек?» должен отвечаться мгновенно. Какую структуру вы возьмёте?

    Union-Find, лес непересекающихся множеств (disjoint set): каждая запись указывает на родителя, find поднимается до корня, и две записи относятся к одному человеку ровно тогда, когда у них общий корень.

    class DisjointSet<T> {  final _parent = <T, T>{};  final _size = <T, int>{};   T find(T x) {    _parent.putIfAbsent(x, () => x);…
  24. 24

    Автодополнение по 80 000 названий городов на каждое нажатие заново гоняет startsWith, и поле подтормаживает: что вы построите вместо этого?

    Один раз постройте trie (префиксное дерево) вместо прохода на каждое нажатие: запрос проходит m узлов и сразу читает варианты, так что стоимость нажатия перестаёт зависеть от числа городов.

    import 'package:characters/characters.dart'; // ships with Flutter class TrieNode {  final children = <String, TrieNode>{};  final top = <String>[]; // best few completions, filled at index time}…