3. Data Access Layer: осознанный выбор между JPA, JDBC и jOOQ
В этом видео мы разбираем один из самых фундаментальных архитектурных выборов в backend-разработке — подход к работе с базой данных.
Речь пойдёт не о синтаксисе и не о "как написать код", а о trade-offs, ответственности и стоимости владения каждого подхода.
Мы реализуем один и тот же use-case сохранения заказа тремя способами:
🔹 JDBC — полный контроль и максимальная ответственность
🔹 jOOQ — типобезопасный SQL и compile-time гарантии
🔹 JPA (Hibernate) — абстракция, скорость разработки и экосистема Spring
🔹 И немного дебага как всегда)
А затем осознанно выбираем JPA как production-стратегию для проекта OrderHub.
Исходный код проекта на GitHub очень ждет Ваших звезд.
Ссылка на Youtube
Ссылка на Рутьюб
Смотрите, ставьте лайки, подписывайтесь на каналы!✌️
В этом видео мы разбираем один из самых фундаментальных архитектурных выборов в backend-разработке — подход к работе с базой данных.
Речь пойдёт не о синтаксисе и не о "как написать код", а о trade-offs, ответственности и стоимости владения каждого подхода.
Мы реализуем один и тот же use-case сохранения заказа тремя способами:
🔹 JDBC — полный контроль и максимальная ответственность
🔹 jOOQ — типобезопасный SQL и compile-time гарантии
🔹 JPA (Hibernate) — абстракция, скорость разработки и экосистема Spring
🔹 И немного дебага как всегда)
А затем осознанно выбираем JPA как production-стратегию для проекта OrderHub.
Исходный код проекта на GitHub очень ждет Ваших звезд.
Ссылка на Youtube
Ссылка на Рутьюб
Смотрите, ставьте лайки, подписывайтесь на каналы!
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥7👍1
Глава 4: Эффективный поиск. Бинарный и не только
Модификации и родственные методы
Ранее мы рассмотрели классический бинарный поиск, который эффективно отвечает на вопрос «есть ли элемент X в массиве?» и возвращает случайную позицию в случае дубликатов. Но реальные задачи редко сводятся к такому простому запросу. Что делать, если вам нужно найти все книги с заданным названием в каталоге, где дубликаты вполне закономерны (например, несколько экземпляров одного издания)? А как насчет поиска диапазона — всех книг, выпущенных между 2020 и 2023 годом включительно? Или если вы знаете, что данные распределены не просто монотонно, а равномерно, и хотите использовать это свойство для ускорения?
Эти вопросы приводят нас к модификациям классического алгоритма и его альтернативам. Каждая модификация сохраняет логарифмическую природу, но меняет инварианты и условия завершения.
Поиск границ в массиве с дубликатами
Проблема неоднозначности стандартной реализации
Представьте массив книг, отсортированных по названию:
Вызываем Arrays.binarySearch(titles, "Война и мир"). Результат? Не определен. Javadoc гарантирует лишь, что будет возвращен какой-то индекс из диапазона дубликатов, если они существуют. Это недопустимо, когда вам нужен первый экземпляр для отображения в UI или последний для расчета границ диапазона.
Инвариант для поиска первого вхождения
Чтобы гарантированно найти самую левую позицию элемента, мы меняем логику завершения. Классический поиск завершается при точном совпадении array[mid] == target. Мы же должны продолжать поиск в левой половине даже после нахождения совпадения, потому что там может скрываться еще более ранний дубликат.
Новый инвариант: алгоритм поддерживает две зоны — переднюю, где все элементы строго меньше target, и заднюю, где элементы больше или равны target. Когда цикл завершается, указатель left будет указывать на первый элемент, равный target, или на позицию вставки, если target отсутствует.
#Java #для_новичков #beginner #algorithm #sorted #binary
Модификации и родственные методы
Ранее мы рассмотрели классический бинарный поиск, который эффективно отвечает на вопрос «есть ли элемент X в массиве?» и возвращает случайную позицию в случае дубликатов. Но реальные задачи редко сводятся к такому простому запросу. Что делать, если вам нужно найти все книги с заданным названием в каталоге, где дубликаты вполне закономерны (например, несколько экземпляров одного издания)? А как насчет поиска диапазона — всех книг, выпущенных между 2020 и 2023 годом включительно? Или если вы знаете, что данные распределены не просто монотонно, а равномерно, и хотите использовать это свойство для ускорения?
Эти вопросы приводят нас к модификациям классического алгоритма и его альтернативам. Каждая модификация сохраняет логарифмическую природу, но меняет инварианты и условия завершения.
Поиск границ в массиве с дубликатами
Проблема неоднозначности стандартной реализации
Представьте массив книг, отсортированных по названию:
String[] titles = {
"Война и мир", "Война и мир", "Война и мир",
"Гарри Поттер", "Гарри Поттер",
"Мастер и Маргарита"
};Вызываем Arrays.binarySearch(titles, "Война и мир"). Результат? Не определен. Javadoc гарантирует лишь, что будет возвращен какой-то индекс из диапазона дубликатов, если они существуют. Это недопустимо, когда вам нужен первый экземпляр для отображения в UI или последний для расчета границ диапазона.
Инвариант для поиска первого вхождения
Чтобы гарантированно найти самую левую позицию элемента, мы меняем логику завершения. Классический поиск завершается при точном совпадении array[mid] == target. Мы же должны продолжать поиск в левой половине даже после нахождения совпадения, потому что там может скрываться еще более ранний дубликат.
Новый инвариант: алгоритм поддерживает две зоны — переднюю, где все элементы строго меньше target, и заднюю, где элементы больше или равны target. Когда цикл завершается, указатель left будет указывать на первый элемент, равный target, или на позицию вставки, если target отсутствует.
#Java #для_новичков #beginner #algorithm #sorted #binary
👍3
public class BinarySearchBounds {
/**
* Поиск первого вхождения целевого значения.
* Если элемент отсутствует, возвращает индекс, где он должен быть вставлен.
*
* @return индекс первого вхождения target или potential insertion point
*/
public static int findFirst(int[] array, int target) {
if (array == null) throw new IllegalArgumentException("Array cannot be null");
int left = 0;
int right = array.length - 1;
int result = -1; // Потенциальная позиция вставки
while (left <= right) {
int mid = left + (right - left) / 2;
int midValue = array[mid];
if (midValue < target) {
// Цель строго правее
left = mid + 1;
} else if (midValue > target) {
// Цель строго левее
right = mid - 1;
} else {
// Нашли совпадение, но продолжаем искать в левой половине
result = mid; // Запомнили потенциальный ответ
right = mid - 1; // Сдвинули границу влево
}
}
// Если result остался -1, target не найден. Можно вернуть left как точку вставки.
return result != -1 ? result : left;
}
/**
* Поиск последнего вхождения целевого значения.
*/
public static int findLast(int[] array, int target) {
if (array == null) throw new IllegalArgumentException("Array cannot be null");
int left = 0;
int right = array.length - 1;
int result = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
int midValue = array[mid];
if (midValue < target) {
left = mid + 1;
} else if (midValue > target) {
right = mid - 1;
} else {
result = mid; // Запомнили потенциальный ответ
left = mid + 1; // Ключевое отличие: сдвигаемся вправо
}
}
return result != -1 ? result : left - 1; // left указывает после последнего <= target
}
}Ключевые отличия от классического поиска:
Сохранение состояния: Переменная result запоминает последнюю удачную позицию. Это нарушает чистоту инварианта, но необходимо для корректности.
Асимметричные действия: При нахождении target мы не выходим, а сдвигаем границу в сторону, противоположную от искомой границы. Для findFirst — влево, для findLast — вправо.
Выходное значение: В отсутствие элемента метод возвращает не -1, а точку вставки. Это делает API более универсальным для построения диапазонных запросов.
Поиск последнего вхождения: симметричная логика
Метод findLast зеркально отражает findFirst. Когда находится совпадение, мы продолжаем поиск в правой половине, потому что там может быть еще один дубликат. После завершения цикла left указывает на первый элемент строго больше target, поэтому left - 1 — это последний элемент, меньший или равный target.
#Java #для_новичков #beginner #algorithm #sorted #binary
👍3
Поиск диапазона
От границ к диапазону: композиция операций
Теперь, когда у нас есть инструменты для поиска границ, задача «найти все книги, изданные в 2020-2023 годах» решается композицией:
Анализ производительности: Поиск диапазона требует двух бинарных поисков — O(log n) + O(log n) = O(log n). После этого мы получаем непосредственный доступ к результату за O(1). Если результатов много (k элементов), итоговая сложность O(log n + k). Это намного эффективнее линейного сканирования всей библиотеки O(n).
Важное замечание: Метод возвращает subList, который является view исходного массива. Изменения в исходном массиве отразятся в результате. Для защиты нужно скопировать: new ArrayList<>(Arrays.asList(...).subList(...)).
#Java #для_новичков #beginner #algorithm #sorted #binary
От границ к диапазону: композиция операций
Теперь, когда у нас есть инструменты для поиска границ, задача «найти все книги, изданные в 2020-2023 годах» решается композицией:
public class LibraryRangeSearch {
static class Book implements Comparable<Book> {
String title;
int year;
// Конструктор, геттеры...
@Override
public int compareTo(Book other) {
return Integer.compare(this.year, other.year);
}
}
/**
* Поиск всех книг в заданном диапазоне лет [startYear, endYear].
* Возвращает подмассив (views) для экономии памяти.
*/
public static List<Book> findBooksByYearRange(Book[] library, int startYear, int endYear) {
if (library == null || startYear > endYear) {
return Collections.emptyList();
}
// Создаем фиктивные книги-границы для поиска
Book startDummy = new Book("", startYear);
Book endDummy = new Book("", endYear);
// Находим первую книгу с year >= startYear
int leftIndex = findFirstIndex(library, startDummy);
// Находим последнюю книгу с year <= endYear
int rightIndex = findLastIndex(library, endDummy);
if (leftIndex == -1 || rightIndex == -1 || leftIndex > rightIndex) {
return Collections.emptyList();
}
// Возвращаем view для избежания копирования
return Arrays.asList(library).subList(leftIndex, rightIndex + 1);
}
// Адаптер для работы с Comparable объектами
private static int findFirstIndex(Book[] array, Book target) {
int left = 0, right = array.length - 1, result = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
int cmp = array[mid].compareTo(target);
if (cmp < 0) left = mid + 1;
else if (cmp > 0) right = mid - 1;
else { result = mid; right = mid - 1; }
}
return result != -1 ? result : left;
}
private static int findLastIndex(Book[] array, Book target) {
int left = 0, right = array.length - 1, result = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
int cmp = array[mid].compareTo(target);
if (cmp < 0) left = mid + 1;
else if (cmp > 0) right = mid - 1;
else { result = mid; left = mid + 1; }
}
return result != -1 ? result : left - 1;
}
}Анализ производительности: Поиск диапазона требует двух бинарных поисков — O(log n) + O(log n) = O(log n). После этого мы получаем непосредственный доступ к результату за O(1). Если результатов много (k элементов), итоговая сложность O(log n + k). Это намного эффективнее линейного сканирования всей библиотеки O(n).
Важное замечание: Метод возвращает subList, который является view исходного массива. Изменения в исходном массиве отразятся в результате. Для защиты нужно скопировать: new ArrayList<>(Arrays.asList(...).subList(...)).
#Java #для_новичков #beginner #algorithm #sorted #binary
👍3
Альтернативный подход: поиск нижней границы + линейное сбирание
Если вы знаете, что диапазон редко содержит много элементов (например, книги за конкретный год в библиотеке с редкими изданиями), можно оптимизировать:
Этот подход имеет сложность O(log n + k) в худшем случае, но с меньшими константами, потому что второй бинарный поиск заменен на последовательное чтение (кэш-приятно).
Интерполяционный поиск — когда данные говорят сами за себя
Интуиция: зачем всегда делить пополам?
Представьте телефонный справочник, где фамилии распределены равномерно по алфавиту. Ищете «Иванов». Классический бинарный поиск откроет справочник ровно посередине — на букве «М», затем на «Г», затем на «Д»... Вместо этого можно интерполировать: поскольку «И» находится примерно на 10% алфавита, сразу открыть страницу на 10% от общего объема.
Интерполяционный поиск заменяет слепое деление пополам на адресный расчет вероятной позиции на основе значений границ.
Математическая формула интерполяции
Если array[left] и array[right] известны, и target лежит между ними, то при равномерном распределении его позиция должна быть пропорциональна:
Это формула линейной интерполяции. Она вычисляет, насколько далеко target находится от левой границы в долях от общего диапазона значений, и применяет этот же коэффициент к индексам.
#Java #для_новичков #beginner #algorithm #sorted #binary
Если вы знаете, что диапазон редко содержит много элементов (например, книги за конкретный год в библиотеке с редкими изданиями), можно оптимизировать:
public static List<Book> findBooksByYearRangeOptimized(Book[] library, int startYear, int endYear) {
int startIdx = findFirstIndex(library, new Book("", startYear));
if (startIdx == -1 || startIdx >= library.length) return Collections.emptyList();
List<Book> result = new ArrayList<>();
for (int i = startIdx; i < library.length && library[i].year <= endYear; i++) {
result.add(library[i]);
}
return result;
}Этот подход имеет сложность O(log n + k) в худшем случае, но с меньшими константами, потому что второй бинарный поиск заменен на последовательное чтение (кэш-приятно).
Интерполяционный поиск — когда данные говорят сами за себя
Интуиция: зачем всегда делить пополам?
Представьте телефонный справочник, где фамилии распределены равномерно по алфавиту. Ищете «Иванов». Классический бинарный поиск откроет справочник ровно посередине — на букве «М», затем на «Г», затем на «Д»... Вместо этого можно интерполировать: поскольку «И» находится примерно на 10% алфавита, сразу открыть страницу на 10% от общего объема.
Интерполяционный поиск заменяет слепое деление пополам на адресный расчет вероятной позиции на основе значений границ.
Математическая формула интерполяции
Если array[left] и array[right] известны, и target лежит между ними, то при равномерном распределении его позиция должна быть пропорциональна:
mid = left + (target - array[left]) * (right - left) / (array[right] - array[left])
Это формула линейной интерполяции. Она вычисляет, насколько далеко target находится от левой границы в долях от общего диапазона значений, и применяет этот же коэффициент к индексам.
#Java #для_новичков #beginner #algorithm #sorted #binary
👍2
Реализация и инварианты
Критические условия применимости:
Равномерное распределение: Формула работает, если разность между соседними элементами примерно постоянна. Если данные сгущаются в некоторых зонах (например, 90% книг изданы в 2020-х, а остальные — растянуты на 50 лет), интерполяция будет постоянно ошибаться, откатываясь к бинарному поведению или хуже.
Отсутствие повторений на границах: Если array[left] == array[right], формула приводит к делению на ноль. В этом случае алгоритм должен деградировать к линейному поиску в этом поддиапазоне.
Целочисленное переполнение: Выражение (target - array[left]) * (right - left) может переполнить int при больших значениях. Для production-кода рекомендуется использовать long для промежуточных расчетов.
Анализ сложности и парадоксы
Средний случай: При равномерном распределении интерполяционный поиск достигает O(log log n). Это практически константа: для массива из 1 миллиарда элементов потребуется ~5 итераций. Это достигается за счет того, что каждый шаг не просто делит диапазон пополам, а приближается к цели экспоненциально быстро.
Худший случай: Если данные неравномерны (например, [1, 2, 3, 4, 5, 1000, 1001, 1002, 1003]), интерполяция может снова и снова попадать в «пустые» зоны, требуя O(n) сравнений. Это хуже бинарного поиска.
Практический вывод: Интерполяционный поиск имеет смысл применять только тогда, когда вы точно знаете характер распределения данных и уверены в его равномерности. В остальных случаях бинарный поиск более надежен. В Java стандартная библиотека не содержит интерполяционного поиска из-за его узкой применимости и риска деградации.
#Java #для_новичков #beginner #algorithm #sorted #binary
public class InterpolationSearch {
/**
* Интерполяционный поиск для равномерно распределенных целочисленных данных.
* ВНИМАНИЕ: требует, чтобы array[left] < array[right] и данные были равномерными!
*/
public static int search(int[] array, int target) {
if (array == null) throw new IllegalArgumentException("Array cannot be null");
int left = 0;
int right = array.length - 1;
// Условие array[left] <= target <= array[right] критично для формулы
while (left <= right && target >= array[left] && target <= array[right]) {
// Если диапазон схлопнулся, переходим к линейному поиску
if (array[left] == array[right]) {
if (array[left] == target) return left;
break; // Не найден
}
// Интерполяция позиции
// Предотвращаем деление на ноль проверкой выше
int mid = left + (target - array[left]) * (right - left) / (array[right] - array[left]);
// Защита от выхода за границы (возможна при неравномерных данных)
mid = Math.max(left, Math.min(mid, right));
int midValue = array[mid];
if (midValue < target) {
left = mid + 1;
} else if (midValue > target) {
right = mid - 1;
} else {
return mid;
}
}
// Пост-проверка границ
if (left <= right && array[left] == target) return left;
return -1;
}
}Критические условия применимости:
Равномерное распределение: Формула работает, если разность между соседними элементами примерно постоянна. Если данные сгущаются в некоторых зонах (например, 90% книг изданы в 2020-х, а остальные — растянуты на 50 лет), интерполяция будет постоянно ошибаться, откатываясь к бинарному поведению или хуже.
Отсутствие повторений на границах: Если array[left] == array[right], формула приводит к делению на ноль. В этом случае алгоритм должен деградировать к линейному поиску в этом поддиапазоне.
Целочисленное переполнение: Выражение (target - array[left]) * (right - left) может переполнить int при больших значениях. Для production-кода рекомендуется использовать long для промежуточных расчетов.
Анализ сложности и парадоксы
Средний случай: При равномерном распределении интерполяционный поиск достигает O(log log n). Это практически константа: для массива из 1 миллиарда элементов потребуется ~5 итераций. Это достигается за счет того, что каждый шаг не просто делит диапазон пополам, а приближается к цели экспоненциально быстро.
Худший случай: Если данные неравномерны (например, [1, 2, 3, 4, 5, 1000, 1001, 1002, 1003]), интерполяция может снова и снова попадать в «пустые» зоны, требуя O(n) сравнений. Это хуже бинарного поиска.
Практический вывод: Интерполяционный поиск имеет смысл применять только тогда, когда вы точно знаете характер распределения данных и уверены в его равномерности. В остальных случаях бинарный поиск более надежен. В Java стандартная библиотека не содержит интерполяционного поиска из-за его узкой применимости и риска деградации.
#Java #для_новичков #beginner #algorithm #sorted #binary
👍3
Что выведет код?
#Tasks
public class Task260126 {
public static void main(String[] args) {
int[] arr = {2, 4, 6, 8, 10, 12, 14, 16, 18, 1000};
int index = interpolationSearch260126(arr, 2);
System.out.println(index);
}
static int interpolationSearch260126(int[] arr, int target) {
int low = 0;
int high = arr.length - 1;
while (low <= high && target >= arr[low] && target <= arr[high]) {
int pos = low + ((target - arr[low]) * (high - low)) / (arr[high] - arr[low]);
if (arr[pos] == target) return pos;
if (arr[pos] < target) low = pos + 1;
else high = pos - 1;
}
return -1;
}
}#Tasks
👍2
👍3
Java for Beginner
3. Data Access Layer: осознанный выбор между JPA, JDBC и jOOQ В этом видео мы разбираем один из самых фундаментальных архитектурных выборов в backend-разработке — подход к работе с базой данных. Речь пойдёт не о синтаксисе и не о "как написать код", а о…
Ну хоть поделитесь, как вам видео? Полезно аль не очень?☺️
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8
Почему finalize() опасен даже если он “работает”? 🤓
Ответ:
finalize вызывается не сразу и не гарантирован вообще.
Объект может долго оставаться в памяти, даже если он логически мёртв. Ошибка в finalize может полностью блокировать сборку мусора для объекта. Кроме того, finalize выполняется в отдельном потоке GC, что усложняет отладку.
Именно поэтому finalize признан deprecated, а управление ресурсами должно выполняться явно через try-with-resources.
#собеседование
Ответ:
Объект может долго оставаться в памяти, даже если он логически мёртв. Ошибка в finalize может полностью блокировать сборку мусора для объекта. Кроме того, finalize выполняется в отдельном потоке GC, что усложняет отладку.
Именно поэтому finalize признан deprecated, а управление ресурсами должно выполняться явно через try-with-resources.
#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
История IT-технологий сегодня — 27 января
ℹ️ Кто родился в этот день
Лью́ис Кэ́рролл (англ. Lewis Carroll, настоящее имя Чарльз Лютвидж До́джсон, или Чарльз Латуидж До́джсон (традиционная русская передача; сам Кэрролл произносил свою фамилию Додсон, ˈdɒdsən[11], ряд современных словарей даёт произношение ˈdɒdʒsən[12][13]), Charles Lutwidge Dodgson; 27 января 1832 — 14 января 1898) — математик и логик, работал над логическими диограммами, алгеброй логики и теорией вероятностей; его логические идеи и подход к формализации рассуждений — часть исторического фундамента логики, на которой стоит теоретическая информатика. Наиболее известные произведения — «Приключения Алисы в Стране чудес» и «Алиса в Зазеркалье», а также юмористическая поэма «Охота на Снарка».
🌐 Знаковые события
1994 — Джим Кларк уходит из Silicon Graphics и создаёт Mosaic Communications (позже Netscape): компания стала одной из первых, кто коммерциализировал веб‑браузер, что сильно ускорило развитие и распространение интернета и веб‑технологий. Доля пользователей Netscape достигла пика в середине 1990-х годов, но к концу 2006 года упала до менее чем одного процента.
#Biography #Birth_Date #Events #27Января
Лью́ис Кэ́рролл (англ. Lewis Carroll, настоящее имя Чарльз Лютвидж До́джсон, или Чарльз Латуидж До́джсон (традиционная русская передача; сам Кэрролл произносил свою фамилию Додсон, ˈdɒdsən[11], ряд современных словарей даёт произношение ˈdɒdʒsən[12][13]), Charles Lutwidge Dodgson; 27 января 1832 — 14 января 1898) — математик и логик, работал над логическими диограммами, алгеброй логики и теорией вероятностей; его логические идеи и подход к формализации рассуждений — часть исторического фундамента логики, на которой стоит теоретическая информатика. Наиболее известные произведения — «Приключения Алисы в Стране чудес» и «Алиса в Зазеркалье», а также юмористическая поэма «Охота на Снарка».
1994 — Джим Кларк уходит из Silicon Graphics и создаёт Mosaic Communications (позже Netscape): компания стала одной из первых, кто коммерциализировал веб‑браузер, что сильно ускорило развитие и распространение интернета и веб‑технологий. Доля пользователей Netscape достигла пика в середине 1990-х годов, но к концу 2006 года упала до менее чем одного процента.
#Biography #Birth_Date #Events #27Января
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Глава 4: Эффективный поиск. Бинарный и не только
Практика
Сегодня мы применим знания о эффективном поиске на проекте «Библиотека».
Мы отсортируем список книг по названию, реализуем бинарный поиск для нахождения первой книги заданного автора (в отсортированном списке), и проведём сравнение времени выполнения линейного и бинарного поиска на коллекциях разных размеров. Это поможет наглядно увидеть преимущества логарифмического поиска O(log n) над линейным O(n), понять предпосылки (сортировка данных) и границы применимости (когда сортировка окупается).
Подготовка к уроку
Перед началом убедитесь, что проект готов, и вспомните ключевые концепции:
Бинарный поиск работает только на отсортированных данных, делит интервал пополам.
Линейный поиск — перебор O(n), всегда работает.
Сортировка O(n log n) — предпосылка для бинарного.
Откройте проект «Библиотека»: Убедитесь, что List<Book> books содержит достаточно книг (добавьте метод для генерации тестовых данных).
Импортируйте пакеты: java.util.Arrays (для бинарного поиска), java.util.Random (для генерации данных).
Генерация больших данных: Создайте метод generateBooks(int size) для создания списков размером 100, 10 000, 1 000 000 (используйте Random для title/author/year).
Отсортировать книги по названию
Обновите Book для Comparable (если не сделано): Реализуйте compareTo по title (this.title.compareTo(other.title)).
Создайте метод sortByTitle(): Используйте Collections.sort(books) — сортирует по Comparable (названию).
Вывод: После сортировки вызовите printAllBooks() для проверки.
Реализовать бинарный поиск для нахождения первой книги заданного автора
Отсортируйте по автору: Создайте Comparator<Book> byAuthor = Comparator.comparing(Book::getAuthor);, затем books.sort(byAuthor).
Реализуйте метод findFirstBookByAuthor(String author):
Используйте Arrays.binarySearch, но поскольку books — List, преобразуйте в массив или реализуйте вручную.
Вручную:
Если не найден — return null.
Проверка: После сортировки вызовите метод, выведите найденную книгу.
Сравнить время линейного и бинарного поиска на разных размерах
Реализуйте линейный поиск: Метод linearSearchByAuthor(String author) — for-each, если совпадение — return book.
Измерение времени: Используйте System.nanoTime() before/after.
Тест на размерах:
Для 100: generateBooks(100), sortByAuthor, time linear vs binary (поиск рандомного автора).
Для 10 000 и 1 000 000: Аналогично, усредните по 100 запускам.
Выводите: "Для n=[size]: Линейный: [time ns], Бинарный: [time ns]".
Анализ: Точка окупаемости сортировки + бинарный поиск vs множественный линейный поиск
Точка окупаемости — момент, когда стоимость сортировки + m бинарных поисков становится меньше m линейных поисков.
Расчёт:
Линейный: O(n) per search → m * n
Бинарный: O(n log n) sort + m * log n
Окупаемость: n log n + m log n < m n → m > (n log n) / (n - log n) ≈ log n (для больших n)
Пример: n = 1000, log n ≈ 10 — окупаемость после ~10 поисков.
n = 1 млн, log n ≈ 20 — после ~20 поисков.
Граница: Для малого m или n — линейный дешевле (нет сортировки).
В библиотеке: Если поиски редки — линейный; если часты — sort + binary.
#Java #для_новичков #beginner #algorithm #sorted #binary #практика
Практика
Сегодня мы применим знания о эффективном поиске на проекте «Библиотека».
Мы отсортируем список книг по названию, реализуем бинарный поиск для нахождения первой книги заданного автора (в отсортированном списке), и проведём сравнение времени выполнения линейного и бинарного поиска на коллекциях разных размеров. Это поможет наглядно увидеть преимущества логарифмического поиска O(log n) над линейным O(n), понять предпосылки (сортировка данных) и границы применимости (когда сортировка окупается).
Подготовка к уроку
Перед началом убедитесь, что проект готов, и вспомните ключевые концепции:
Бинарный поиск работает только на отсортированных данных, делит интервал пополам.
Линейный поиск — перебор O(n), всегда работает.
Сортировка O(n log n) — предпосылка для бинарного.
Откройте проект «Библиотека»: Убедитесь, что List<Book> books содержит достаточно книг (добавьте метод для генерации тестовых данных).
Импортируйте пакеты: java.util.Arrays (для бинарного поиска), java.util.Random (для генерации данных).
Генерация больших данных: Создайте метод generateBooks(int size) для создания списков размером 100, 10 000, 1 000 000 (используйте Random для title/author/year).
Отсортировать книги по названию
Обновите Book для Comparable (если не сделано): Реализуйте compareTo по title (this.title.compareTo(other.title)).
Создайте метод sortByTitle(): Используйте Collections.sort(books) — сортирует по Comparable (названию).
Вывод: После сортировки вызовите printAllBooks() для проверки.
Реализовать бинарный поиск для нахождения первой книги заданного автора
Отсортируйте по автору: Создайте Comparator<Book> byAuthor = Comparator.comparing(Book::getAuthor);, затем books.sort(byAuthor).
Реализуйте метод findFirstBookByAuthor(String author):
Используйте Arrays.binarySearch, но поскольку books — List, преобразуйте в массив или реализуйте вручную.
Вручную:
int low = 0, high = books.size() - 1;
while (low <= high) {
mid = (low + high) / 2; cmp = books.get(mid).getAuthor().compareTo(author);
if (cmp < 0) low = mid + 1;
else if (cmp > 0) high = mid - 1;
else { // Найден, найти первый:
while (mid > 0 && books.get(mid-1).getAuthor().equals(author))
mid--;
return books.get(mid);
}
}
Если не найден — return null.
Проверка: После сортировки вызовите метод, выведите найденную книгу.
Сравнить время линейного и бинарного поиска на разных размерах
Реализуйте линейный поиск: Метод linearSearchByAuthor(String author) — for-each, если совпадение — return book.
Измерение времени: Используйте System.nanoTime() before/after.
Тест на размерах:
Для 100: generateBooks(100), sortByAuthor, time linear vs binary (поиск рандомного автора).
Для 10 000 и 1 000 000: Аналогично, усредните по 100 запускам.
Выводите: "Для n=[size]: Линейный: [time ns], Бинарный: [time ns]".
Анализ: Точка окупаемости сортировки + бинарный поиск vs множественный линейный поиск
Точка окупаемости — момент, когда стоимость сортировки + m бинарных поисков становится меньше m линейных поисков.
Расчёт:
Линейный: O(n) per search → m * n
Бинарный: O(n log n) sort + m * log n
Окупаемость: n log n + m log n < m n → m > (n log n) / (n - log n) ≈ log n (для больших n)
Пример: n = 1000, log n ≈ 10 — окупаемость после ~10 поисков.
n = 1 млн, log n ≈ 20 — после ~20 поисков.
Граница: Для малого m или n — линейный дешевле (нет сортировки).
В библиотеке: Если поиски редки — линейный; если часты — sort + binary.
#Java #для_новичков #beginner #algorithm #sorted #binary #практика
👍4
Что выведет код?
#Tasks
public class Task270126 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("12345");
sb.append(sb.append("6"));
System.out.println(sb.toString());
}
}#Tasks
👍2
👍2
Что такое String, StringBuilder и StringBuffer? 🤓
Ответ:
String — неизменяемый (immutable) класс. Любая операция (конкатенация, замена) создает новый объект.
StringBuilder и StringBuffer — изменяемые классы для работы со строками. StringBuilder (появился в Java 5) работает быстрее, но не потокобезопасен.
StringBuffer — потокобезопасный (все методы синхронизированы), но медленнее.
Для однопоточных программ выбирают StringBuilder.
#собеседование
Ответ:
String
StringBuilder и StringBuffer — изменяемые классы для работы со строками. StringBuilder (появился в Java 5) работает быстрее, но не потокобезопасен.
StringBuffer — потокобезопасный (все методы синхронизированы), но медленнее.
Для однопоточных программ выбирают StringBuilder.
#собеседование
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
История IT-технологий сегодня — 28 января
ℹ️ Кто родился в этот день
Уильям Сьюард Берроуз (28 января 1857, Рочестер, Нью-Йорк — 14 сентября 1898) — американский изобретатель механического счётного устройства; его «adding machine» стала предшественником кассовых аппаратов и деловых калькуляторов, а сама компания Burroughs в XX веке вошла в число производителей компьютеров (часть истории мейнфреймов и деловых ЭВМ).
🌐 Знаковые события
1724 — указом Петра I основаны Петербургская академия наук и Петербургский университет.
#Biography #Birth_Date #Events #28Января
Уильям Сьюард Берроуз (28 января 1857, Рочестер, Нью-Йорк — 14 сентября 1898) — американский изобретатель механического счётного устройства; его «adding machine» стала предшественником кассовых аппаратов и деловых калькуляторов, а сама компания Burroughs в XX веке вошла в число производителей компьютеров (часть истории мейнфреймов и деловых ЭВМ).
1724 — указом Петра I основаны Петербургская академия наук и Петербургский университет.
#Biography #Birth_Date #Events #28Января
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Как то совсем незаметно канал преодолел отметку в 800 подписчиков 😎 .
Спасибо всем кто присоединяется❤️ , значит все таки, что-то интересное делаю)))
А тем кто с нами давно двойная благодарность, что не уходите👏
Спасибо всем кто присоединяется
А тем кто с нами давно двойная благодарность, что не уходите
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥11🍾7
Раздел 7. Алгоритмы
Глава 5: Рекурсия, деревья и введение в динамическое программирование
Принцип рекурсии и её опасности
Рекурсия — это методология решения задач, при которой функция вызывает сама себя для обработки подзадачи меньшего размера. В математике это называется рекуррентным соотношением, в программировании — рекурсивным вызовом. Главная идея состоит в том, чтобы свести решение сложной проблемы к решению аналогичной, но более простой проблемы, плюс некоторый шаг объединения результатов.
Представьте задачу вычисления суммы чисел от 1 до n. Итеративный подход использует цикл с аккумулятором. Рекурсивный подход говорит: сумма от 1 до n равна n плюс сумма от 1 до n-1. Это определение ссылается на само себя, но с меньшим аргументом. Такое самоподобие лежит в основе рекурсивного мышления.
Два столпа корректности: базовый случай и рекурсивный шаг
Любая корректная рекурсивная функция строится на двух неразрывно связанных компонентах:
Базовый случай (base case) — это условие, при котором рекурсия останавливается и функция возвращает конкретное значение без дальнейших вызовов самой себя. Это точка выхода из бесконечного цикла вызовов. Без базового случая функция будет вызывать себя вечно, пока не исчерпает системные ресурсы. Для суммы чисел от 1 до n базовым случаем является ситуация, когда n равно 0 или 1 — сумма пустого множества или одного элемента тривиальна.
Рекурсивный шаг (recursive step) — это логика, связывающая результат текущего вызова с результатом вызова функции от модифицированных аргументов. Здесь кроется математическая индукция: мы предполагаем, что функция работает корректно для меньших входных данных (индукционная гипотеза), и доказываем, что тогда она работает для текущих данных.
Важнейшим свойством рекурсивного шага является прогресс к базовому случаю. Каждый последующий вызов должен приближать нас к условию остановки. Если аргументы не изменяются или изменяются в сторону увеличения сложности, рекурсия никогда не завершится.
Рассмотрим классический пример вычисления факториала. Математически n! определен как произведение всех натуральных чисел от 1 до n, с дополнительным условием 0! = 1.
Инвариант рекурсии — условие, которое остается истинным на каждом уровне вызовов. Для факториала это утверждение, что при вызове factorial(k) мы находимся в процессе вычисления произведения чисел от k до n, где n — исходный аргумент верхнего уровня. Поддержание инварианта гарантирует корректность результата при возврате из глубины стека.
#Java #для_новичков #beginner #algorithm #recursion
Глава 5: Рекурсия, деревья и введение в динамическое программирование
Принцип рекурсии и её опасности
Рекурсия — это методология решения задач, при которой функция вызывает сама себя для обработки подзадачи меньшего размера. В математике это называется рекуррентным соотношением, в программировании — рекурсивным вызовом. Главная идея состоит в том, чтобы свести решение сложной проблемы к решению аналогичной, но более простой проблемы, плюс некоторый шаг объединения результатов.
Представьте задачу вычисления суммы чисел от 1 до n. Итеративный подход использует цикл с аккумулятором. Рекурсивный подход говорит: сумма от 1 до n равна n плюс сумма от 1 до n-1. Это определение ссылается на само себя, но с меньшим аргументом. Такое самоподобие лежит в основе рекурсивного мышления.
Два столпа корректности: базовый случай и рекурсивный шаг
Любая корректная рекурсивная функция строится на двух неразрывно связанных компонентах:
Базовый случай (base case) — это условие, при котором рекурсия останавливается и функция возвращает конкретное значение без дальнейших вызовов самой себя. Это точка выхода из бесконечного цикла вызовов. Без базового случая функция будет вызывать себя вечно, пока не исчерпает системные ресурсы. Для суммы чисел от 1 до n базовым случаем является ситуация, когда n равно 0 или 1 — сумма пустого множества или одного элемента тривиальна.
Рекурсивный шаг (recursive step) — это логика, связывающая результат текущего вызова с результатом вызова функции от модифицированных аргументов. Здесь кроется математическая индукция: мы предполагаем, что функция работает корректно для меньших входных данных (индукционная гипотеза), и доказываем, что тогда она работает для текущих данных.
Важнейшим свойством рекурсивного шага является прогресс к базовому случаю. Каждый последующий вызов должен приближать нас к условию остановки. Если аргументы не изменяются или изменяются в сторону увеличения сложности, рекурсия никогда не завершится.
Рассмотрим классический пример вычисления факториала. Математически n! определен как произведение всех натуральных чисел от 1 до n, с дополнительным условием 0! = 1.
public class RecursionFundamentals {
/**
* Вычисляет факториал числа n.
*
* @param n неотрицательное целое число
* @return n!
* @throws IllegalArgumentException если n отрицательно
*/
public static long factorial(int n) {
// Базовый случай: факториал 0 или 1 равен 1
// Это математическое определение, служащее якорем рекурсии
if (n <= 1) {
return 1;
}
// Защита от некорректного использования
if (n < 0) {
throw new IllegalArgumentException("Factorial undefined for negative numbers");
}
// Рекурсивный шаг: n! = n * (n-1)!
// Здесь мы полагаемся на то, что factorial(n-1) вернет правильный результат
// и умножаем его на n для получения текущего значения
return n * factorial(n - 1);
}
}Инвариант рекурсии — условие, которое остается истинным на каждом уровне вызовов. Для факториала это утверждение, что при вызове factorial(k) мы находимся в процессе вычисления произведения чисел от k до n, где n — исходный аргумент верхнего уровня. Поддержание инварианта гарантирует корректность результата при возврате из глубины стека.
#Java #для_новичков #beginner #algorithm #recursion
👍4
Стек вызовов: механизм жизнеобеспечения рекурсии
Чтобы понять опасности рекурсии, необходимо заглянуть под капот JVM и понять, как работает стек вызовов (call stack).
Стек вызовов — это область памяти, выделенная каждому потоку выполнения в Java. Он организован по принципу LIFO (Last In, First Out — последним пришел, первым вышел).
Каждый раз, когда метод вызывается, в стек помещается фрейм активации (stack frame), содержащий:
- Локальные переменные метода (примитивы и ссылки)
- Параметры метода
- Адрес возврата (куда передать управление после завершения метода)
- Ссылку на объект this (для нестатических методов)
- Служебную информацию для отладки (номера строк в таблице символов)
При рекурсивном вызове создается новый фрейм, который логически изолирован от предыдущего. Даже если это тот же метод с тем же именем, JVM видит его как отдельный экземпляр выполнения со своим набором локальных переменных. Это изоляция позволяет каждому уровню рекурсии работать со своими копиями данных, не мешая вышестоящим вызовам.
Размер стека вызовов ограничен. В Java по умолчанию он составляет 1 мегабайт для каждого потока (значение может варьироваться в зависимости от архитектуры и версии JVM, обычно от 256KB до 2MB). Этот размер фиксируется при создании потока и не может быть динамически расширен во время выполнения.
StackOverflowError: когда глубина превышает высоту
Если рекурсия слишком глубока, стек вызовов переполняется. JVM обнаруживает попытку записи за пределы выделенной области и выбрасывает java.lang.StackOverflowError. Это не checked exception и не unchecked runtime exception в традиционном смысле — это ошибка (Error), наследник VirtualMachineError, сигнализирующая о критическом состоянии ресурсов.
Важно понимать, что StackOverflowError — это неизлечимая в рамках текущего потока ситуация. После её возникновения стек разрушен, состояние объектов может быть неопределенным, и продолжать выполнение потока нельзя. Единственное разумное действие — завершить поток или всю программу.
Какова максимальная глубина рекурсии?
Это зависит от нескольких факторов:
- Размера стека (-Xss параметр JVM)
- Количества и размера локальных переменных в методе (чем больше данных во фрейме, тем быстрее заполнится стек)
- Архитектуры (64-битные системы имеют больший оверхед на ссылки)
Для простого метода типа факториала с одним параметром типа int глубина обычно составляет от нескольких тысяч до десятков тысяч вызовов. Например, попытка вычислить factorial(100000) практически гарантированно вызовет StackOverflowError.
#Java #для_новичков #beginner #algorithm #recursion
Чтобы понять опасности рекурсии, необходимо заглянуть под капот JVM и понять, как работает стек вызовов (call stack).
Стек вызовов — это область памяти, выделенная каждому потоку выполнения в Java. Он организован по принципу LIFO (Last In, First Out — последним пришел, первым вышел).
Каждый раз, когда метод вызывается, в стек помещается фрейм активации (stack frame), содержащий:
- Локальные переменные метода (примитивы и ссылки)
- Параметры метода
- Адрес возврата (куда передать управление после завершения метода)
- Ссылку на объект this (для нестатических методов)
- Служебную информацию для отладки (номера строк в таблице символов)
При рекурсивном вызове создается новый фрейм, который логически изолирован от предыдущего. Даже если это тот же метод с тем же именем, JVM видит его как отдельный экземпляр выполнения со своим набором локальных переменных. Это изоляция позволяет каждому уровню рекурсии работать со своими копиями данных, не мешая вышестоящим вызовам.
Размер стека вызовов ограничен. В Java по умолчанию он составляет 1 мегабайт для каждого потока (значение может варьироваться в зависимости от архитектуры и версии JVM, обычно от 256KB до 2MB). Этот размер фиксируется при создании потока и не может быть динамически расширен во время выполнения.
StackOverflowError: когда глубина превышает высоту
Если рекурсия слишком глубока, стек вызовов переполняется. JVM обнаруживает попытку записи за пределы выделенной области и выбрасывает java.lang.StackOverflowError. Это не checked exception и не unchecked runtime exception в традиционном смысле — это ошибка (Error), наследник VirtualMachineError, сигнализирующая о критическом состоянии ресурсов.
Важно понимать, что StackOverflowError — это неизлечимая в рамках текущего потока ситуация. После её возникновения стек разрушен, состояние объектов может быть неопределенным, и продолжать выполнение потока нельзя. Единственное разумное действие — завершить поток или всю программу.
Какова максимальная глубина рекурсии?
Это зависит от нескольких факторов:
- Размера стека (-Xss параметр JVM)
- Количества и размера локальных переменных в методе (чем больше данных во фрейме, тем быстрее заполнится стек)
- Архитектуры (64-битные системы имеют больший оверхед на ссылки)
Для простого метода типа факториала с одним параметром типа int глубина обычно составляет от нескольких тысяч до десятков тысяч вызовов. Например, попытка вычислить factorial(100000) практически гарантированно вызовет StackOverflowError.
public class StackDepthDemo {
private static int depth = 0;
public static void recursiveCall() {
depth++;
if (depth % 1000 == 0) {
System.out.println("Current depth: " + depth);
}
recursiveCall(); // Бесконечная рекурсия без базового случая
}
public static void main(String[] args) {
try {
recursiveCall();
} catch (StackOverflowError e) {
System.err.println("Stack overflow at depth: " + depth);
// Вывод примерно: Stack overflow at depth: 10800 (зависит от -Xss)
}
}
}#Java #для_новичков #beginner #algorithm #recursion
👍4
Отсутствие Tail-Call Optimization: архитектурный долг JVM
В функциональных языках программирования (Scheme, Haskell, Scala с определенными флагами) существует механизм, называемый оптимизацией хвостовой рекурсии (Tail-Call Optimization, TCO). Он позволяет преобразовать рекурсивный вызов в цикл на уровне машинного кода, если рекурсивный вызов является последней операцией в методе (хвостовой позицией).
Рассмотрим хвостовую версию факториала:
В языках с 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
В функциональных языках программирования (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