Java for Beginner
869 subscribers
1.01K photos
275 videos
14 files
1.69K links
Канал от новичков для новичков!
Изучайте Java вместе с нами!
Здесь мы обмениваемся опытом и постоянно изучаем что-то новое!

Наш YouTube канал - https://www.youtube.com/@Java_Beginner-Dev

Наш канал на RUTube - https://rutube.ru/channel/37896292/
Download Telegram
Отсутствие Tail-Call Optimization: архитектурный долг JVM

В функциональных языках программирования (Scheme, Haskell, Scala с определенными флагами) существует механизм, называемый оптимизацией хвостовой рекурсии (Tail-Call Optimization, TCO). Он позволяет преобразовать рекурсивный вызов в цикл на уровне машинного кода, если рекурсивный вызов является последней операцией в методе (хвостовой позицией).

Рассмотрим хвостовую версию факториала:
public static long factorialTail(int n, long accumulator) {
if (n <= 1) {
return accumulator;
}
// Рекурсивный вызов — последняя операция перед return
// Теоретически JVM могла бы освободить текущий фрейм перед вызовом
return factorialTail(n - 1, n * accumulator);
}


В языках с TCO такой код выполнялся бы с постоянным размером стека O(1), так как каждый новый вызов переиспользовал бы фрейм предыдущего. Однако JVM не поддерживает TCO. Это архитектурное решение, связанное с необходимостью сохранения точных трасс стека для отладки, профилирования и механизма SecurityManager. Каждый вызов factorialTail создает новый фрейм, и при достаточно большом n произойдет StackOverflowError.

Последствия отсутствия TCO:

- Невозможность безопасной рекурсии для больших n: Даже правильно написанная хвостовая рекурсия не спасает от переполнения стека. Вы не можете использовать рекурсию для обработки списка из миллиона элементов, даже если алгоритмически это хвостовой вызов.
- Предпочтение итерации: В Java культуре рекурсия считается менее идиоматичной, чем в функциональных языках. Циклы while и for предпочтительны для глубоких итераций, так как они используют постоянное количество памяти O(1).
- Ручное управление стеком: Для задач, естественно выражаемых через рекурсию (обход деревьев), но требующих обработки больших глубин, Java-разработчики вынуждены использовать явный стек (класс java.util.Stack или ArrayDeque), перекладывая управление памятью из стека вызовов в кучу (heap), где ограничения гораздо мягче.
- Накладные расходы:
Каждый рекурсивный вызов требует:
Выделения фрейма в стеке
Сохранения регистров процессора
Переключения контекста выполнения
Проверок безопасности (в некоторых JVM)
Это делает рекурсию медленнее итерации даже для малых глубин, хотя разница измеряется в наносекундах.


#Java #для_новичков #beginner #algorithm #recursion
👍4
Когда рекурсия естественна: домены применения

Несмотря на ограничения, рекурсия остается незаменимым инструментом для задач с древовидной структурой или редукцией к подзадачам.
Иерархические структуры данных: Файловая система, DOM-дерево HTML, абстрактные синтаксические деревья компилятора, организационные структуры компании — все это примеры данных, где каждый узел содержит ссылки на подчиненные узлы. Рекурсивная функция может обработать текущий узел и вызвать себя для каждого потомка, естественно следуя топологии данных.

Разделение задач: Алгоритмы типа "разделяй и властвуй" (merge sort, quick sort, бинарный поиск) естественно выражаются через рекурсию. Рекурсивный шаг здесь — это применение того же алгоритма к половинам массива.
Математические определения: Функции, определенные рекуррентно (числа Фибоначчи, комбинаторика, фракталы), читаются проще в рекурсивной форме, хотя эффективная реализация часто требует мемоизации или перехода к итерации.

Рассмотрим пример обхода файловой системы — классический случай, где рекурсия проявляет свою силу:
import java.io.File;

public class FileSystemTraversal {

/**
* Рекурсивно подсчитывает общий размер всех файлов в директории.
*
* @param directory корневая директория для анализа
* @return суммарный размер в байтах
*/
public static long calculateDirectorySize(File directory) {
// Базовый случай 1: несуществующий путь
if (!directory.exists()) {
return 0;
}

// Базовый случай 2: это файл, а не директория
// Возвращаем его размер, рекурсия останавливается
if (directory.isFile()) {
return directory.length();
}

// Базовый случай 3: пустая директория (опционально, обработается циклом)

// Рекурсивный шаг: получаем список содержимого
File[] children = directory.listFiles();
if (children == null) {
// Защита от null при отсутствии прав доступа
return 0;
}

long totalSize = 0;
// Для каждого элемента в директории вызываем себя рекурсивно
for (File child : children) {
// Рекурсивный вызов обрабатывает поддиректории на любую глубину
totalSize += calculateDirectorySize(child);
}

return totalSize;
}
}


В этом примере глубина рекурсии равна глубине вложенности директорий. Для типичной файловой системы это 10-20 уровней — безопасно для стека. Но при обработке архивов с глубокой вложенностью или символических ссылок, создающих циклы, необходима защита от бесконечной рекурсии (например, через Set посещенных путей).


Практические рекомендации по безопасной рекурсии

Анализ глубины перед реализацией:
Перед написанием рекурсивного метода оцените максимальную глубину вызовов. Если данные могут содержать тысячи уровней вложенности (например, парсинг JSON с глубокой вложенностью объектов), используйте итеративный подход с явным стеком.

Проверка базового случая: Всегда убедитесь, что базовый случай достижим. Для числовых аргументов это обычно проверка на ноль или единицу. Для структур данных — проверка на null или пустоту.
Защита от циклов: При обходе графов или файловых систем с символическими ссылками используйте ThreadLocal<Set> или передавайте Set<VisitedNode> через параметры для отслеживания посещенных узлов.

Размер стека: Для специфических задач с умеренной, но значительной глубиной (например, 5000 уровней) можно увеличить размер стека через флаг -Xss2m (2 мегабайта). Однако это лечит симптом, а не причину, и не масштабируется.

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


#Java #для_новичков #beginner #algorithm #recursion
👍5
Что выведет код?

public class Task280126 {
public static void main(String[] args) {

System.out.println(f(7));

}

static int f(int n) {

if (n <= 1) return 1;
return f(n - 2) + f(n - 1);

}
}


#Tasks
👍2
Варианты ответа:
Anonymous Quiz
18%
8
12%
13
53%
21
18%
36
👍1
Что такое static? 🤓

Ответ:

Ключевое слово static означает, что поле или метод принадлежит самому классу, а не конкретному экземпляру (объекту) этого класса.

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

Статический блок инициализации (static {}) выполняется при загрузке класса в память JVM.



#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
История IT-технологий сегодня — 29 января

ℹ️ Кто родился в этот день

Джозеф Бернард Крускал-младший ( / ˈ k r ʌ s k əl / ; 29 января 1928 — 19 сентября 2010) — математик, статистик и компьютерный учёный; автор алгоритма Крускала для поиска минимального остовного дерева (MST), используемого в сетевых задачах, маршрутизации, кластеризации и многих алгоритмах в графах.


🌐 Знаковые события

1992 — начался спор Линуса Торвальдса и Эндрю Таненбаума — «Линукс устарел».

Главный тезис Таненбаума: будущие ОС должны строиться на микроядрах, а монолитные ядра — вчерашний день, «шаг назад в 70‑е», поэтому Linux уже устарел на момент создания; кроме того, он критиковал привязку Linux к x86 и сомневался в его переносимости.

Позиция Торвальдса: монолитное ядро (с модульностью) проще и практичнее для реальной рабочей системы, чем тогдашние экспериментальные микроядра; он подчеркивал, что Linux уже работает на реальном железе и людям удобнее получить доступную, быструю систему, чем идеальную архитектуру «когда‑нибудь».

Задним числом особенно интересно читать аргументы Таненбаума о том, что Linux «не взлетит», на фоне того, как всё сложилось в реальности.


#Biography #Birth_Date #Events #29Января
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
Глава 5: Рекурсия, деревья и введение в динамическое программирование

Рекурсия на практике: обход деревьев


Деревья в информатике — это не только природные объекты, но и фундаментальная абстракция для моделирования иерархических связей. Файловая система, DOM-документа, организационная структура компании, дерево разбора выражений — все они являются узлами, связанными отношениями «родитель — дети».

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

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


Файловая система — подсчет общего размера

Файловая система — это дерево, где узлами являются каталоги, а листьями — файлы. Каждый каталог содержит массив подчиненных элементов. Задача подсчета общего размера естественно рекурсивна: размер каталога равен сумме размеров всех его дочерних элементов, где для файла возвращается прямое значение, а для подкаталога — рекурсивный вызов.

Рекурсивная реализация с полной обработкой ошибок

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;

public class FileSizeCalculator {

/**
* Вычисляет общий размер всех файлов в каталоге и его подкаталогах.
*
* @param rootPath корневой каталог для анализа
* @return суммарный размер в байтах
* @throws IllegalArgumentException если rootPath не является каталогом или не существует
*/
public static long calculateSize(Path rootPath) {
if (rootPath == null) {
throw new IllegalArgumentException("Path cannot be null");
}

// Проверяем существование и тип файловой системы
if (!Files.exists(rootPath)) {
throw new IllegalArgumentException("Path does not exist: " + rootPath);
}

if (!Files.isDirectory(rootPath)) {
throw new IllegalArgumentException("Path is not a directory: " + rootPath);
}

try {
return calculateRecursive(rootPath);
} catch (IOException e) {
// Преобразуем checked exception в unchecked для удобства API
throw new UncheckedIOException("Failed to traverse directory: " + rootPath, e);
}
}

/**
* Внутренний рекурсивный метод.
*
* @param currentPath текущий узел для анализа
* @return суммарный размер поддерева
*/
private static long calculateRecursive(Path currentPath) throws IOException {
// Базовый случай 1: это файл
if (Files.isRegularFile(currentPath)) {
try {
return Files.size(currentPath);
} catch (SecurityException | IOException e) {
// В реальном приложении логгируем ошибку доступа
// и продолжаем, а не прокидываем исключение
System.err.println("Cannot access file: " + currentPath + " - " + e.getMessage());
return 0;
}
}

//случай 2: это директория, обрабатываем её содержимое
long totalSize = 0;

// Используем try-with-resources для безопасного закрытия DirectoryStream
try (DirectoryStream<Path> stream = Files.newDirectoryStream(currentPath)) {
for (Path child : stream) {
// Рекурсивный вызов для каждого дочернего элемента
// Глубина рекурсии = уровень вложенности директорий
totalSize += calculateRecursive(child);
}
} catch (NotDirectoryException e) {
// Это не должно случиться из-за проверки выше, но защита от race condition
return 0;
} catch (SecurityException e) {
System.err.println("Access denied to directory: " + currentPath);
return 0;
}

return totalSize;
}
}
👍3
Ключевые детали реализации:
Два базовых случая: файл (возвращаем размер) и нефайл (обрабатываем детей). Операция Files.isRegularFile() требует минимальных системных вызовов и не читает весь файл в память.
Защита от null: Files.newDirectoryStream() может вернуть null или бросить исключение, если доступ запрещен.
Обработка символических ссылок: Текущая реализация следует по символическим ссылкам, что может создать циклы и StackOverflowError.

Для защиты нужно добавить Set<Path> посещенных узлов:
private static long calculateRecursiveSafe(Path currentPath, Set<Path> visited) throws IOException {
// Проверка на цикл
if (!visited.add(currentPath.toRealPath())) {
System.err.println("Cycle detected, skipping: " + currentPath);
return 0;
}

if (Files.isRegularFile(currentPath)) {
return Files.size(currentPath);
}

long totalSize = 0;
try (DirectoryStream<Path> stream = Files.newDirectoryStream(currentPath)) {
for (Path child : stream) {
totalSize += calculateRecursiveSafe(child, visited);
}
}

// Важно: не удаляем из visited при возврате, так как путь может быть повторен через другую ссылку
return totalSize;
}



Анализ производительности и памяти


Время: O(N), где N — общее количество файлов и каталогов. Каждый узел посещается ровно один раз.
Память (стек): O(h), где h — максимальная глубина вложенности. Для домашней директории h обычно < 50, для серверов — < 500. Безопасно для 1MB стека.
Память (куча): O(h) на множество visited, если используется защита от циклов.

Иерархия жанров библиотеки — поиск в поддереве

Представим модель библиотеки, где жанры организованы в дерево:
Литература
├── Художественная
│ ├── Фантастика
│ │ ├── Космическая
│ │ └── Альтернативная история
│ └── Детектив
└── Научная
└── Программирование
└── Java


Каждый узел содержит название и список поджанров.

Задача: найти все книги, относящиеся к жанру «Фантастика», включая все поджанры.

Модель данных
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class GenreNode {
private final String name;
private final List<GenreNode> subgenres;
private final List<Book> books; // Книги непосредственно этого жанра

public GenreNode(String name) {
this.name = name;
this.subgenres = new ArrayList<>();
this.books = new ArrayList<>();
}

// Методы добавления поджанров и книг...
public void addSubgenre(GenreNode subgenre) {
subgenres.add(subgenre);
}

public void addBook(Book book) {
books.add(book);
}

public String getName() {
return name;
}

public List<GenreNode> getSubgenres() {
return Collections.unmodifiableList(subgenres);
}

public List<Book> getBooks() {
return Collections.unmodifiableList(books);
}
}

class Book {
final String title;
final String author;

Book(String title, String author) {
this.title = title;
this.author = author;
}
}


#Java #для_новичков #beginner #algorithm #recursion
👍3
Рекурсивный поиск по поддереву
import java.util.List;
import java.util.ArrayList;

public class GenreSearchService {

/**
* Находит все книги в указанном жанре и его поджанрах.
*
* @param root корень дерева жанров
* @param targetGenreName имя целевого жанра
* @return список всех книг в поддереве
*/
public static List<Book> findAllBooksInGenre(GenreNode root, String targetGenreName) {
if (root == null || targetGenreName == null) {
throw new IllegalArgumentException("Parameters cannot be null");
}

List<Book> result = new ArrayList<>();
findGenreAndCollectBooks(root, targetGenreName, result);
return result;
}

/**
* Рекурсивный обход с поиском целевого жанра.
*
* @param current текущий узел
* @param target имя искомого жанра
* @param accumulator список для накопления результатов
* @return true, если целевой жанр найден в текущем поддереве
*/
private static boolean findGenreAndCollectBooks(GenreNode current, String target, List<Book> accumulator) {
// Базовый случай 1: мы в целевом узле
boolean isTarget = current.getName().equals(target);

// Если нашли целевой жанр, добавляем все книги этого узла
if (isTarget) {
accumulator.addAll(current.getBooks());
}

// Рекурсивный шаг: обходим все поджанры
// Даже если текущий узел не целевой, его потомки могут иметь целевой жанр
for (GenreNode child : current.getSubgenres()) {
boolean foundInChild = findGenreAndCollectBooks(child, target, accumulator);

// Если целевой жанр найден в поддереве, добавляем книги текущего узла
// Это зависит от бизнес-логики: если нужны ТОЛЬКО книги целевого жанра и его поджанров,
// но не промежуточных узлов, логика меняется
if (foundInChild) {
accumulator.addAll(current.getBooks());
}
}

// Возвращаем true, если целевой жанр найден в этом поддереве
return isTarget;
}
}


Проблема текущей реализации: Она добавляет книги промежуточных узлов, когда находит цель в поддереве. Для задачи «все книги в поддереве Фантастики» это неправильно.

Исправим:
/**
* Версия 2: Корректный поиск всех книг в поддереве целевого жанра.
*/
public static List<Book> findAllBooksInSubtree(GenreNode root, String targetGenreName) {
List<Book> result = new ArrayList<>();
GenreNode targetNode = findNode(root, targetGenreName);
if (targetNode != null) {
collectAllBooksInSubtree(targetNode, result);
}
return result;
}

/**
* Первый проход: находим целевой узел.
*/
private static GenreNode findNode(GenreNode current, String target) {
if (current.getName().equals(target)) {
return current;
}

for (GenreNode child : current.getSubgenres()) {
GenreNode found = findNode(child, target);
if (found != null) {
return found; // Остановка при первом нахождении
}
}

return null;
}

/**
* Второй проход: собираем все книги в поддереве.
*/
private static void collectAllBooksInSubtree(GenreNode current, List<Book> accumulator) {
accumulator.addAll(current.getBooks());

for (GenreNode child : current.getSubgenres()) {
collectAllBooksInSubtree(child, accumulator);
}
}


#Java #для_новичков #beginner #algorithm #recursion
👍2
Оптимизация одним проходом:

Можно совместить поиск и сбор:

private static boolean collectIfTarget(GenreNode current, String target, List<Book> accumulator) {
// Проверяем, является ли текущий узел целевым
boolean isTargetNode = current.getName().equals(target);

// Если мы уже внутри целевого поддерева (аккумулятор в режиме сбора)...
// Или если текущий узел — цель, начинаем сбор
if (isTargetNode || (accumulator.size() > 0 && accumulator.get(accumulator.size() - 1) == null)) {
accumulator.addAll(current.getBooks());
}

// Рекурсивно обходим детей
for (GenreNode child : current.getSubgenres()) {
collectIfTarget(child, target, accumulator);
}

return isTargetNode;
}


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


Анализ сложности

Время: O(N), где N — общее количество узлов. В худшем случае ищем до самого последнего узла.
Память (стек): O(h), где h — высота дерева. Для сбалансированного дерева h = log N, для вырожденного (списка) h = N.

Сравнение рекурсивного и итеративного обхода

Итеративная версия с явным стеком
import java.util.ArrayDeque;
import java.util.Deque;

public class IterativeTreeTraversal {

/**
* Итеративный подсчет размера каталога с использованием ArrayDeque.
* Использует кучу (heap) вместо стека вызовов.
*/
public static long calculateSizeIterative(Path rootPath) throws IOException {
if (!Files.isDirectory(rootPath)) {
return Files.size(rootPath);
}

long totalSize = 0;
Deque<Path> stack = new ArrayDeque<>();
stack.push(rootPath);

while (!stack.isEmpty()) {
Path current = stack.pop();

// Если это файл, добавляем размер
if (Files.isRegularFile(current)) {
totalSize += Files.size(current);
continue;
}

// Если это директория, добавляем всех детей в стек
try (DirectoryStream<Path> stream = Files.newDirectoryStream(current)) {
for (Path child : stream) {
stack.push(child);
}
} catch (IOException e) {
System.err.println("Error reading " + current + ": " + e.getMessage());
}
}

return totalSize;
}

/**
* Итеративный поиск книг в поддереве жанров.
*/
public static List<Book> findBooksIterative(GenreNode root, String targetGenre) {
List<Book> result = new ArrayList<>();

// Находим целевой узел
GenreNode targetNode = null;
Deque<GenreNode> nodeStack = new ArrayDeque<>();
nodeStack.push(root);

while (!nodeStack.isEmpty() && targetNode == null) {
GenreNode current = nodeStack.pop();

if (current.getName().equals(targetGenre)) {
targetNode = current;
break;
}

// Добавляем детей в порядке обратном, чтобы обходить в исходном порядке
List<GenreNode> children = current.getSubgenres();
for (int i = children.size() - 1; i >= 0; i--) {
nodeStack.push(children.get(i));
}
}

if (targetNode == null) {
return result;
}

// Собираем все книги в поддереве
nodeStack.clear();
nodeStack.push(targetNode);

while (!nodeStack.isEmpty()) {
GenreNode current = nodeStack.pop();
result.addAll(current.getBooks());

// Добавляем всех детей
for (GenreNode child : current.getSubgenres()) {
nodeStack.push(child);
}
}

return result;
}
}


#Java #для_новичков #beginner #algorithm #recursion
👍3
Сравнительный анализ

Читаемость и поддерживаемость:

Рекурсия: Естественно отражает структуру дерева. Код короче, интуитивно понятен. Подходит для алгоритмов с разделением задач (divide-and-conquer).
Итерация: Требует ручного управления стеком. Легче допустить ошибку в порядке обхода. Однако в Java культуре итерация часто считается более «идиоматичной» из-за отсутствия TCO.

Производительность:
Скорость: Рекурсия медленнее на 5-15% из-за накладных расходов на создание фреймов, проверку безопасности, сохранение регистров. Итерация работает как цикл while.
Локальность: Рекурсия хуже кэшируется, так как фреймы разбросаны в памяти. Итерация с ArrayDeque использует непрерывный массив с высокой локальностью.
Вызовы: Каждый рекурсивный вызов — это отдельный invocation в профилировщике. При больших N это замедляет JIT-компиляцию.

Память:
Рекурсия: O(h) в стеке вызовов (ограниченный ресурс). Каждый фрейм занимает 32-128 байт в зависимости от аргументов и локальных переменных.
Итерация: O(h) в куче (heap), где ограничений практически нет. ArrayDeque может расти до Integer.MAX_VALUE элементов. Один элемент стека — Path или GenreNode — занимает 8 байт для ссылки + размер объекта.

Надежность:
Стековерфлов: Рекурсия может упасть с StackOverflowError на глубине 1000-10000. Итерация не имеет этого ограничения.
Ошибки доступа: В рекурсии ошибка в одной ветви прерывает весь обход. В итерации легче продолжить после ошибки.

Параллелизм:
Рекурсия: Сложно распараллелить, так как каждый поток имеет свой стек вызовов. Нужно вручную создавать задачи для ForkJoinPool.
Итерация: Легко разделить работу между потоками, разбив стек на части.


#Java #для_новичков #beginner #algorithm #recursion
👍3
Что выведет код?

import java.util.*;

public class Task290126 {
static class Node290126 {
int value;
Node290126 left, right;
Node290126(int value) { this.value = value; }
}

public static void main(String[] args) {
Node290126 root = new Node290126(1);
root.left = new Node290126(2);
root.right = new Node290126(3);
root.left.left = new Node290126(4);
root.left.right = new Node290126(5);
root.right.left = new Node290126(6);
root.right.right = new Node290126(7);

List<Integer> result = new ArrayList<>();
preorderTraversal(root, result);
System.out.println(result);
}

static void preorderTraversal(Node290126 node, List<Integer> result) {
if (node == null) return;
result.add(node.value);
preorderTraversal(node.left, result);
preorderTraversal(node.right, result);
}
}


#Tasks
👍3
Что такое HashSet и как он реализован внутри? 🤓

Ответ:

HashSet
— это коллекция, которая не хранит дубликаты элементов.

Внутри он реализован на основе HashMap, где значение элемента HashSet становится ключом в этой HashMap, а в качестве значения в HashMap используется фиктивный объект-заглушка (static final Object PRESENT = new Object()).

Таким образом, все уникальность и быстрый доступ (O(1) в среднем) обеспечиваются за счет механизмов хеширования, как в HashMap. Порядок элементов не гарантируется.



#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
История IT-технологий сегодня — 30 января

ℹ️ Кто родился в этот день

Ду́глас Карл Энгельба́рт (англ. Douglas Carl Engelbart; 30 января 1925, Портленд (Орегон) — 2 июля 2013, Атертон, Калифорния) — один из первых исследователей человеко-машинного интерфейса и изобретатель компьютерной мыши. В ряду других его изобретений — графический пользовательский интерфейс, гипертекст, текстовый редактор, групповые онлайн-конференции.


🌐 Знаковые события

1982 — первый массовый вирус для ПК Elk Cloner: 15‑летний Ричард Скрента написал вирус для Apple II, распространявшийся через загрузочные дискеты; это один из первых широко распространённых примеров самораспространяющегося кода и важная веха в истории компьютерных вирусов и антивирусов.

2007 — релиз Microsoft Windows Vista: новая версия Windows с UAC, Aero, изменённой моделью драйверов и улучшенной безопасностью; несмотря на спорную репутацию, Vista принесла в экосистему Windows ряд фундаментальных изменений, которые позже доехали в более доведённом виде до Windows 7 и дальше.

2009 — с космодрома Плесецк с помощью ракеты-носителя «Циклон-3» запущен российский космический аппарат «Коронас-Фотон».


#Biography #Birth_Date #Events #30Января
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥5
Раздел 7. Алгоритмы

Глава 5: Рекурсия, деревья и введение в динамическое программирование


От рекурсии к динамическому программированию

Ранее мы рассмотрели рекурсию как естественный инструмент для работы с иерархическими структурами. Но в мире вычислений есть задачи, где прямое применение рекурсии превращает элегантный код в тормозной механизм, пожирающий процессорное время и память. Классический пример такого провала — вычисление чисел Фибоначчи.

Числа Фибоначчи определяются простейшей рекуррентной формулой: F(0) = 0, F(1) = 1, а для n > 1 выполняется F(n) = F(n-1) + F(n-2). Это определение так и просится быть преобразованным в рекурсивную функцию. Однако наивная реализация скрывает в себе экспоненциальную бомбу. Мы разберем, почему это происходит, как мемоизация превращает катастрофу в триумф, и как эти идеи ложатся в основу парадигмы динамического программирования.


Наивная рекурсия и экспоненциальная сложность

Вот как выглядит буквальная имплементация определения Фибоначчи:
public class NaiveFibonacci {

/**
* Вычисляет n-е число Фибоначчи наивной рекурсией.
*
* @param n порядковый номер, начиная с 0 (F(0)=0, F(1)=1)
* @return n-е число Фибоначчи
* @throws IllegalArgumentException если n отрицательный
*/
public static long fibonacci(int n) {
// Базовые случаи, соответствующие математическому определению
if (n < 0) {
throw new IllegalArgumentException("n must be non-negative");
}
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}

// Рекурсивный шаг: F(n) = F(n-1) + F(n-2)
// Ключевой момент: два вызова на каждом уровне
return fibonacci(n - 1) + fibonacci(n - 2);
}
}


Код выглядит правильно, компилируется и дает правильные результаты для малых n. Но попробуйте вызвать fibonacci(50) и приготовьтесь ждать. Не секунды, а минуты. А fibonacci(100) на обычном железе не завершится за разумное время.

Дерево вызовов: визуализация катастрофы

Чтобы понять, почему это происходит, нужно построить дерево вызовов.

Для fibonacci(5) оно выглядит так:
fibonacci(5)
├── fibonacci(4)
│ ├── fibonacci(3)
│ │ ├── fibonacci(2)
│ │ │ ├── fibonacci(1) = 1
│ │ │ └── fibonacci(0) = 0
│ │ ├── fibonacci(1) = 1
│ │ └── 1 + 1 = 2
│ ├── fibonacci(2)
│ │ ├── fibonacci(1) = 1
│ │ └── fibonacci(0) = 0
│ └── 2 + 1 = 3
├── fibonacci(3)
│ ├── fibonacci(2)
│ │ ├── fibonacci(1) = 1
│ │ └── fibonacci(0) = 0
│ ├── fibonacci(1) = 1
│ └── 1 + 1 = 2
└── 3 + 2 = 5


Заметьте: fibonacci(2) вычисляется три раза, fibonacci(3) — дважды. При увеличении n это дублирование становится катастрофическим. Дерево вызовов — не дерево, а граф, где один и тот же узел достигается множеством путей. Но наивная рекурсия это не знает и вычисляет повторно каждый раз.


Математический анализ сложности: O(2ⁿ)

Количество вызовов функции можно описать рекуррентным соотношением T(n) = T(n-1) + T(n-2) + O(1), что очень похоже на саму последовательность Фибоначчи. Можно доказать индукцией, что T(n) ≥ F(n). А поскольку F(n) ≈ φⁿ / √5, где φ ≈ 1.618 — золотое сечение, получаем экспоненциальный рост.

Более грубая, но наглядная оценка: пусть каждый вызов порождает два новых. Тогда на глубине n будет не более 2ⁿ узлов. Действительное число вызовов немного меньше из-за сокращения базовых случаев, но порядок остается экспоненциальным — O(2ⁿ).

Практические измерения (на JVM с отключенным JIT для чистоты эксперимента):
fibonacci(30) — ~2.7 миллиона вызовов, ~10 мс
fibonacci(40) — ~3.3 миллиарда вызовов, ~1000 мс
fibonacci(50) — ~4.1 триллионов вызовов, ~120 секунд
fibonacci(100) — вызовов больше, чем атомов на Земле

Это не медленный алгоритм — это неработающий алгоритм. Любая задача, где n может быть 50 или больше, становится неприемлемой.


#Java #для_новичков #beginner #algorithm #recursion
👍4
Мемоизация — кэширование как спасение

Мемоизация
— это техника сохранения результатов дорогих вызовов функций и повторного использования вместо повторного вычисления. Это не специфично для рекурсии, но в контексте рекурсивных алгоритмов она трансформирует экспоненциальное время в линейное.

Идея проста: перед вычислением проверяем, есть ли результат в кэше. Если есть — возвращаем его. Если нет — вычисляем, кэшируем, возвращаем.

Реализация наивной мемоизации через HashMap
import java.util.HashMap;
import java.util.Map;

public class MemoizedFibonacci {

// Кэш для хранения вычисленных значений. Map<аргумент, результат>
// Используем long для n, т.к. int может переполниться при больших значениях
private static final Map<Long, Long> memo = new HashMap<>();

// Инициализация базовых случаев в кэше
static {
memo.put(0L, 0L);
memo.put(1L, 1L);
}

/**
* Потоко-НЕБЕЗОПАСНАЯ версия. Для многопоточности использовать ConcurrentHashMap.
*/
public static long fibonacci(long n) {
if (n < 0) {
throw new IllegalArgumentException("n must be non-negative");
}

// Проверка кэша: если значение уже вычислено, возвращаем мгновенно
if (memo.containsKey(n)) {
return memo.get(n);
}

// Рекурсивный шаг с мемоизацией
// Вычисляем только один раз, результат сохраняется в memo
long result = fibonacci(n - 1) + fibonacci(n - 2);

// Сохраняем в кэш перед возвратом
memo.put(n, result);

return result;
}

/**
* Метод для сброса кэша в тестах. В продакшене так делать не стоит.
*/
public static void clearCache() {
memo.clear();
memo.put(0L, 0L);
memo.put(1L, 1L);
}
}


Что изменилось в дереве вызовов?

При fibonacci(5) первый вызов вычислит все подзначения от 0 до 5 и сохранит их в memo. Последующие вызовы из глубины рекурсии не пойдут дальше одного уровня, так как fibonacci(3) уже в кэше. Дерево вызовов превращается в прямолинейный граф с N узлов.

Анализ сложности:
Время: O(n). Каждое значение от 0 до n вычисляется ровно один раз. Всего n+1 вычислений, каждое — O(1).
Память: O(n) на кэш + O(h) на стек вызовов. Для Фибоначчи h = n в худшем случае, так что суммарно O(n). Но теперь память расходуется в куче (heap), где ограничения гораздо мягче стека.

На практике fibonacci(100) выполняется за < 1 мс, а fibonacci(1000) — за несколько микросекунд, хотя результат уже не помещается в long (можно использовать BigInteger).


Потоковая безопасность и жизненный цикл кэша


В примере выше memo — это статическое поле, общее для всех вызовов.

Это создает несколько проблем:
Потоконебезопасность: Одновременный вызов из двух потоков может повредить HashMap. Решение: ConcurrentHashMap или ThreadLocal<Map>.
Утечка памяти: Кэш живет вечно, захватывая память. Решение: использовать WeakHashMap или явный Cache из библиотеки (Caffeine, Guava).
Тестируемость: Тесты влияют друг на друга через общий кэш. Решение: инстанцировать кэш вне метода и передавать как параметр ( dependency injection).

Правильный production-ready подход:
public class FibonacciService {
private final Map<Long, Long> memo = new ConcurrentHashMap<>();

public FibonacciService() {
memo.put(0L, 0L);
memo.put(1L, 1L);
}

public long fibonacci(long n) {
// computeIfAbsent — атомарная операция, потокобезопасная
return memo.computeIfAbsent(n, key -> fibonacci(key - 1) + fibonacci(key - 2));
}
}



#Java #для_новичков #beginner #algorithm #recursion
👍2🔥1
Динамическое программирование сверху вниз

Мемоизация — это техника.

Динамическое программирование (Dynamic Programming, DP) — это парадигма решения задач оптимизации, основанная на двух принципах:
Оптимальная подструктура: Оптимальное решение задачи содержит в себе оптимальные решения подзадач. Для Фибоначчи: F(n) оптимально вычисляется через оптимальные F(n-1) и F(n-2).
Перекрывающиеся подзадачи: Разные подзадачи содержат одни и те же еще более мелкие подзадачи. Фибоначчи — идеальный пример: F(n-1) и F(n-2) оба нуждаются в F(n-3).

Top-down DP (сверху вниз) — это именно рекурсия с мемоизацией. Мы начинаем с верхней задачи и по мере погружения кэшируем результаты.

Инвариант динамического программирования

В любом DP-алгоритме существует таблица состояний (state table). В мемоизированной версии это Map. Важно правильно определить ключ состояния — минимальный набор параметров, однозначно определяющих подзадачу. Для Фибоначчи это просто n. Для более сложных задач это может быть кортеж параметров.

Bottom-up подход: альтернатива top-down

Есть и другой способ DP — bottom-up (снизу вверх).

Мы не рекурсивно спускаемся, а итеративно строим таблицу от базовых случаев к цели:
public static long fibonacciBottomUp(long n) {
if (n < 0) throw new IllegalArgumentException();
if (n == 0) return 0;
if (n == 1) return 1;

long[] dp = new long[(int)n + 1];
dp[0] = 0;
dp[1] = 1;

for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}

return dp[(int)n];
}


Для Фибоначчи можно сжать память до O(1), храня только два последних значения:
public static long fibonacciOptimized(long n) {
if (n < 0) throw new IllegalArgumentException();
if (n == 0) return 0;

long a = 0, b = 1;
for (long i = 2; i <= n; i++) {
long next = a + b;
a = b;
b = next;
}
return b;
}


Это уже не DP, а простая математическая оптимизация, но она показывает, что DP — промежуточный этап от наивной рекурсии к оптимальному решению.


Практические применения DP в библиотечных системах

Задача: оптимальное размещение книг на полках


Представьте: у вас есть полки разной высоты и книги разной толщины. Нужно разместить книги так, чтобы минимизировать пустое пространство. Это вариация задачи о рюкзаке с ограничениями.
/**
* Динамическое программирование для оптимального размещения.
*
* Состояние: dp[i][w] = минимальное незаполненное пространство
* после размещения первых i книг на полках общей ширины w.
*/
public class ShelfOptimizer {

public static int optimize(List<Integer> bookThicknesses, int shelfWidth) {
int n = bookThicknesses.size();
int[][] dp = new int[n + 1][shelfWidth + 1];

// Инициализация
for (int w = 0; w <= shelfWidth; w++) {
dp[0][w] = w; // Без книг осталось все свободное пространство
}

// Заполнение таблицы
for (int i = 1; i <= n; i++) {
int thickness = bookThicknesses.get(i - 1);
for (int w = 0; w <= shelfWidth; w++) {
if (thickness > w) {
// Книга не помещается — переносим предыдущее состояние
dp[i][w] = dp[i - 1][w];
} else {
// Выбираем лучшее: либо не кладем книгу, либо кладем
dp[i][w] = Math.min(
dp[i - 1][w], // Не кладем
dp[i - 1][w - thickness] // Кладем, освобождаем оставшееся место
);
}
}
}

return dp[n][shelfWidth];
}
}


Это классический bottom-up DP: строим таблицу, каждая ячейка определяется предыдущими.

#Java #для_новичков #beginner #algorithm #recursion
👍1
Поиск кратчайшего пути в графе книг

Если книги связаны рекомендациями «после прочтения этой, читайте эту», можно найти кратчайшую цепочку от книги A до книги B через DP:

/**
* Определение длины кратчайшего пути от start до end.
* dp[v] = минимальная длина пути от start до v.
*/
public static int shortestPathLength(Map<String, List<String>> graph, String start, String end) {
Map<String, Integer> dp = new HashMap<>();
dp.put(start, 0);

// BFS + DP гибрид
Queue<String> queue = new ArrayDeque<>();
queue.add(start);

while (!queue.isEmpty()) {
String current = queue.poll();
int currentDist = dp.get(current);

if (current.equals(end)) {
return currentDist;
}

for (String neighbor : graph.getOrDefault(current, List.of())) {
if (!dp.containsKey(neighbor) || dp.get(neighbor) > currentDist + 1) {
dp.put(neighbor, currentDist + 1);
queue.add(neighbor);
}
}
}

return -1; // Путь не найден
}

Это пример DP, где состояние — это вершина графа.


Подводные камни и best practices

1. Выбор ключа состояния
Ключ должен быть минимальным и однозначным. Для задачи «подсчет путей в сетке» ключ — это (x, y). Для задачи «редактирование строки» — (i, j), где i и j — позиции в строках. Если ключ слишком большой (содержит лишние поля), кэш раздувается. Если неоднозначный — получите неверные результаты.

2. Порядок вычислений и рекурсивные циклы
Если подзадачи зависят друг от друга (например, в графе с циклами), naive top-down DP может зациклиться. Решение:
Добавить состояние "в процессе" в кэш и бросать исключение при повторном входе
Использовать bottom-up
Применять алгоритмы для графов с циклами (topological sort)

3. Память vs время
DP всегда торгует памятью на время. Для задач с большим количеством состояний (например, dp[1000][1000][1000]) память может стать bottlenek'ом. Тогда применяют:
Оптимизацию памяти: хранить только нужные строки таблицы
Встречный поиск (meet-in-the-middle): разбить задачу на две половины
Аппроксимацию: отказаться от точного DP в пользу эвристик

4. Не всё можно решить DP

DP требует оптимальной подструктуры и перекрывающихся подзадач. Если решение задачи не складывается из решений подзадач (например, задача о ранце с взаимозависимыми вещами), DP неприменим.

5. Состояние гонки в мемоизации
В многопоточной среде используйте ConcurrentHashMap.computeIfAbsent, который атомарно вычисляет значение, если его нет. Но будьте осторожны: рекурсивный вызов внутри лямбды может заблокировать bucket и вызвать deadlock при рекурсивных зависимостях.

Раздел 6: От DP к современным практикам
Кэширование в распределенных системах
В микросервисах, управляющих библиотекой, DP-подход применяется через распределенный кэш (Redis, Memcached). Состояние — ключ в Redis, рекурсия — это серийный вызов сервисов. Но теперь нужно думать о сетевых задержках, а не только о процессоре.
Машинное обучение и DP
В NLP для разбора предложений используется CYK-алгоритм — DP по грамматике. В компьютерном зрении для выравнивания изображений — динамическое искажение времени (DTW). DP — фундаментальная парадигма не только в классических алгоритмах.

#Java #для_новичков #beginner #algorithm #recursion
👍1
Что выведет код?

import java.util.HashMap;
import java.util.Map;

public class Task300126 {
private static Map<Integer, Integer> cache = new HashMap<>();

public static void main(String[] args) {
System.out.println(fib300126(5));
System.out.println(fib300126(3));
System.out.println(fib300126(5));
}

static int fib300126(int n) {
if (cache.containsKey(n)) {
return cache.get(n);
}

int result;
if (n <= 1) {
result = n;
} else {
result = fib300126(n - 1) + fib300126(n - 2);
}

cache.put(n, result);
return result;
}
}


#Tasks
👍1