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
Хотел уточнить - на последнем виде про бота нет ни одного лайка... Все так плохо? Может удалить это видео?
Anonymous Poll
4%
Да, все плохо. Лучше удали
67%
Не смотрел...
30%
Вроде все хорошо, щас поставлю лайк)))
👍1
Почему лямбды не всегда лучше обычных методов? 🤓

Ответ:

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

Их используют для простых операций, а не как замену полноценным методам.


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


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

Никого не нашел, поэтому вот:

Ракеш Ша́рма (хинди राकेश शर्मा; род. 13 января 1949, Патиала, Пенджаб, Индия) — первый индийский космонавт и 138-й человек в мире, совершивший полёт в космос. Герой Советского Союза (1984).


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

Не нашел(


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

Глава 1: Основы анализа алгоритмов

Пространственная сложность и компромиссы

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

Пространственная сложность измеряется в тех же асимптотических обозначениях (Big O, Ω, Θ), что и временная, но фокусируется на потреблении памяти. Она включает три основные составляющие:

Входные данные: обязательная память

Любой алгоритм должен хранить входные данные, над которыми он работает. В асимптотическом анализе память под входные данные обычно считается необходимой и включается в общую оценку.
public class InputMemoryExample {
// Метод суммирует элементы массива
// Память: O(n) для хранения входного массива
public static int sumArray(int[] array) {
int sum = 0;
for (int value : array) {
sum += value;
}
return sum;
}

// Метод создает копию массива
// Память: O(n) для входных данных + O(n) для копии = O(n)
public static int[] copyAndModify(int[] array) {
int[] copy = new int[array.length]; // Дополнительная память O(n)
for (int i = 0; i < array.length; i++) {
copy[i] = array[i] * 2;
}
return copy;
}
}


Для алгоритмов, которые модифицируют входные данные in-place (на месте), дополнительная память может быть минимальной. Однако часто такие алгоритмы требуют, чтобы входные данные можно было изменять, что не всегда допустимо в реальных системах.


Вспомогательные структуры данных

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

public class AuxiliaryStructures {
// Алгоритм поиска двух чисел с заданной суммой
// Версия 1: Без дополнительной памяти, но O(n²) времени
public static int[] findPairNaive(int[] array, int targetSum) {
for (int i = 0; i < array.length; i++) {
for (int j = i + 1; j < array.length; j++) {
if (array[i] + array[j] == targetSum) {
return new int[]{array[i], array[j]};
}
}
}
return null; // Память: O(1) вспомогательной
}

// Версия 2: С хеш-таблицей, O(n) времени, но O(n) памяти
public static int[] findPairOptimized(int[] array, int targetSum) {
Set<Integer> seen = new HashSet<>(); // Вспомогательная структура O(n)
for (int num : array) {
int complement = targetSum - num;
if (seen.contains(complement)) {
return new int[]{complement, num};
}
seen.add(num);
}
return null;
}
}


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


#Java #для_новичков #beginner #algorithm #bigO
👍2
Стек вызовов

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

public class RecursionMemory {
// Рекурсивное вычисление факториала
// Пространственная сложность: O(n) из-за глубины рекурсии
public static long factorialRecursive(int n) {
if (n <= 1) return 1;
return n * factorialRecursive(n - 1); // Каждый вызов добавляет фрейм в стек
}

// Итеративное вычисление факториала
// Пространственная сложность: O(1)
public static long factorialIterative(int n) {
long result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}

// Рекурсивный обход дерева в глубину (DFS)
// Пространственная сложность: O(h), где h - высота дерева
public static void dfs(TreeNode node) {
if (node == null) return;

// Обработка узла
System.out.println(node.value);

// Рекурсивный обход детей
dfs(node.left);
dfs(node.right);

// В худшем случае (вырожденное дерево) h = n, сложность O(n)
// В сбалансированном дереве h = log n, сложность O(log n)
}
}


Для рекурсивных алгоритмов критически важно оценивать максимальную глубину рекурсии. Переполнение стека (StackOverflowError) происходит, когда глубина рекурсии превышает ограничение стека вызовов JVM (обычно несколько тысяч фреймов).


Компромисс «Время vs Память»

Компромисс время-память (time-memory trade-off) — фундаментальный принцип проектирования алгоритмов, утверждающий, что можно уменьшить время выполнения за счет увеличения потребления памяти, и наоборот. Этот компромисс проявляется в различных аспектах разработки программного обеспечения.

Хеш-таблица vs Отсортированный список

Рассмотрим две принципиально разные структуры данных для реализации словаря (map) и проанализируем их характеристики.

Хеш-таблица (HashMap в Java):
public class HashTableAnalysis {
// HashMap обеспечивает в среднем O(1) для операций put, get, remove
// Но требует значительной дополнительной памяти
public static void hashTableDemo() {
Map<String, Integer> hashMap = new HashMap<>(1000);

// Вставка: вычисление хеша O(1) + обработка коллизий
hashMap.put("key1", 100);
hashMap.put("key2", 200);

// Поиск: вычисление хеша O(1) + доступ к bucket
Integer value = hashMap.get("key1");

// Память: массив buckets (capacity) + узлы для пар ключ-значение
// Коэффициент загрузки (load factor) по умолчанию 0.75
// При capacity=16 и 12 элементах массив будет расширен
}
}


Характеристики хеш-таблицы:

Время доступа: O(1) в среднем, O(n) в худшем случае (при плохой хеш-функции)
Память: O(n) для хранения элементов + O(capacity) для массива buckets
Особенности: требует хорошей хеш-функции, не сохраняет порядок элементов

#Java #для_новичков #beginner #algorithm #bigO
👍2
Отсортированный список (используя TreeMap или ручную сортировку):
public class SortedListAnalysis {
// TreeMap обеспечивает O(log n) для операций
// Но требует меньше дополнительной памяти
public static void sortedMapDemo() {
// TreeMap основан на красно-черном дереве
Map<String, Integer> treeMap = new TreeMap<>();

// Вставка: O(log n) для поиска позиции + O(1) для вставки (с балансировкой)
treeMap.put("key1", 100);
treeMap.put("key2", 200);

// Поиск: O(log n) для обхода дерева
Integer value = treeMap.get("key1");

// Память: только узлы дерева, без массива buckets
// Каждый узел содержит ссылки на детей, цвет, ключ и значение
}

// Альтернатива: отсортированный ArrayList с бинарным поиском
public static class SortedArrayList<K extends Comparable<K>, V> {
private List<Entry<K, V>> list = new ArrayList<>();

// Вставка с поддержанием порядка: O(n) для поиска позиции + O(n) для сдвига
public void put(K key, V value) {
int index = Collections.binarySearch(
list.stream().map(e -> e.key).collect(Collectors.toList()),
key
);

if (index >= 0) {
list.set(index, new Entry<>(key, value));
} else {
int insertionPoint = -index - 1;
list.add(insertionPoint, new Entry<>(key, value));
}
}

// Поиск: O(log n) бинарным поиском
public V get(K key) {
int index = Collections.binarySearch(
list.stream().map(e -> e.key).collect(Collectors.toList()),
key
);
return index >= 0 ? list.get(index).value : null;
}
}
}


Характеристики отсортированных структур:
Время доступа: O(log n) для деревьев, O(n) для вставки в список
Память: O(n) только для элементов, без значительных накладных расходов
Особенности: сохраняют порядок элементов, позволяют диапазонные запросы

Количественное сравнение

Рассмотрим практический сценарий: необходимо хранить 1 миллион пар ключ-значение в памяти.

Хеш-таблица (HashMap):
Память: ~48 байт на entry (в 64-битной JVM с Compressed OOPs)
Общая память: 1,000,000 × 48 байт ≈ 48 MB + массив buckets (16 MB) ≈ 64 MB
Время поиска: ~100 наносекунд в среднем

TreeMap (красно-черное дерево):
Память: ~56 байт на узел (дополнительные ссылки и цвет)
Общая память: 1,000,000 × 56 байт ≈ 56 MB
Время поиска: log₂(1,000,000) ≈ 20 сравнений × 50 нс ≈ 1000 нс

Отсортированный ArrayList с бинарным поиском:
Память: ~32 байт на entry (только ключ и значение)
Общая память: 1,000,000 × 32 байт ≈ 32 MB
Время поиска: log₂(1,000,000) ≈ 20 сравнений × 10 нс ≈ 200 нс
Но время вставки: O(n) ≈ 500,000 сдвигов в среднем

Выбор зависит от паттерна доступа:
Частые вставки и удаления: HashMap или TreeMap
Частые поиски, редкие модификации: отсортированный ArrayList
Ограниченная память: отсортированный ArrayList


#Java #для_новичков #beginner #algorithm #bigO
👍4
Требование предсказуемого времени доступа: TreeMap (гарантирует O(log n))

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

Амортизированный анализ — это метод оценки средней производительности операции в наихудшей последовательности вызовов. Он особенно полезен для структур данных, где редкие дорогие операции "оплачиваются" многочисленными дешевыми операциями.

Динамический массив (ArrayList): классический пример

Реализация ArrayList в Java демонстрирует ключевые принципы амортизированного анализа.

Рассмотрим внутреннее устройство и стратегию роста:

// Упрощенная реализация ArrayList для демонстрации принципов
public class AmortizedArrayList<E> {
private static final int DEFAULT_CAPACITY = 10;
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

private Object[] elementData;
private int size;

public AmortizedArrayList() {
this.elementData = new Object[DEFAULT_CAPACITY];
this.size = 0;
}

// Амортизированная сложность добавления: O(1)
public boolean add(E element) {
ensureCapacityInternal(size + 1); // Может вызвать дорогое расширение
elementData[size++] = element;
return true;
}

private void ensureCapacityInternal(int minCapacity) {
if (minCapacity - elementData.length > 0) {
// Требуется расширение массива
grow(minCapacity);
}
}

private void grow(int minCapacity) {
int oldCapacity = elementData.length;

// Стандартная стратегия роста: увеличение на 50%
int newCapacity = oldCapacity + (oldCapacity >> 1);

if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}

if (newCapacity - MAX_ARRAY_SIZE > 0) {
newCapacity = hugeCapacity(minCapacity);
}

// Самая дорогая операция: копирование всех элементов
elementData = Arrays.copyOf(elementData, newCapacity);

// В этот момент выполняется O(n) операций
// Но эта стоимость распределяется (амортизируется) по предыдущим дешевым операциям
}

// Метод получения элемента: всегда O(1)
@SuppressWarnings("unchecked")
public E get(int index) {
if (index >= size) {
throw new IndexOutOfBoundsException();
}
return (E) elementData[index];
}
}


Анализ амортизированной стоимости

Для анализа амортизированной сложности операции add() рассмотрим последовательность из n операций добавления.

Метод агрегирования (учетных издержек):

Каждая обычная операция add() стоит 1 единицу (запись в массив)

При расширении массива с capacity k до capacity 1.5k:
Копирование k элементов: k единиц
Эти k единиц "распределяются" по предыдущим k/2 операциям
Амортизированная стоимость на операцию: (стоимость всех операций) / n

Рассмотрим рост массива по мере добавления элементов:
Начальная capacity: 10
Расширения при: 10 → 15 → 22 → 33 → 49 → 73 → 109 → ...

Последовательность стоимостей для 100 операций add():
90 операций стоят 1 единицу
10 операций расширения стоят: 10 + 15 + 22 + 33 + 49 + 73 + 109 = 311 единиц
Общая стоимость: 90 + 311 = 401 единиц
Амортизированная стоимость на операцию: 401 / 100 ≈ 4.01

Таким образом, хотя некоторые операции очень дороги (O(n)), их средняя стоимость в длинной последовательности остается константной O(1).

Метод потенциала: формальный анализ

Более формальный подход — метод потенциала. Определим потенциал Φ как величину, пропорциональную разнице между capacity и size:
Φ = 2 × (capacity - size)
При каждой операции add():
Фактическая стоимость: 1 (обычная запись) или k+1 (при расширении)
Изменение потенциала: ΔΦ
Амортизированная стоимость: фактическая стоимость + ΔΦ

Можно доказать, что амортизированная стоимость каждой операции ограничена константой, что формально доказывает O(1) амортизированную сложность.


#Java #для_новичков #beginner #algorithm #bigO
👍2
Практические следствия амортизированного анализа

Если известно примерное количество элементов, можно задать начальную capacity, избежав нескольких расширений:
// Плохо: множественные расширения при добавлении 1000 элементов
List<Integer> list = new ArrayList<>(); // capacity=10
for (int i = 0; i < 1000; i++) {
list.add(i); // Расширения при 10...
}

// Хорошо: одно расширение или вообще без расширений
List<Integer> optimizedList = new ArrayList<>(1000); // capacity=1000
for (int i = 0; i < 1000; i++) {
optimizedList.add(i); // Без расширений
}



Амортизация в других структурах: Принцип амортизации применяется в:

Динамических таблицах хеширования: расширение buckets
Стеках с мультипопом: операция multipop
Деревьях со сбалансированным слиянием: операции union-find


Реальные компромиссы в современных системах

Кэширование — это практическое применение компромисса время-память. Различные стратегии кэширования представляют разные точки на спектре этого компромисса.
public class CacheTradeoff {
// LRU (Least Recently Used) кэш
// Использует больше памяти для поддержания порядка доступа
public static class LRUCache<K, V> {
private final int capacity;
private final Map<K, V> map;
private final Deque<K> accessOrder;

public LRUCache(int capacity) {
this.capacity = capacity;
this.map = new HashMap<>(capacity);
this.accessOrder = new LinkedList<>();
}

public V get(K key) {
V value = map.get(key);
if (value != null) {
// Обновляем порядок: O(n) операция
accessOrder.remove(key);
accessOrder.addFirst(key);
}
return value;
}

public void put(K key, V value) {
if (map.size() >= capacity) {
// Удаляем наименее использованный
K lruKey = accessOrder.removeLast();
map.remove(lruKey);
}
map.put(key, value);
accessOrder.addFirst(key);
}
}

// Простой кэш с фиксированным размером
// Использует меньше памяти, но может иметь хуже hit ratio
public static class SimpleCache<K, V> {
private final Map<K, V> map;
private final int maxSize;

public SimpleCache(int maxSize) {
this.maxSize = maxSize;
this.map = new HashMap<>(maxSize);
}

public V get(K key) {
return map.get(key); // O(1), но нет обновления порядка
}

public void put(K key, V value) {
if (map.size() >= maxSize) {
// Удаляем случайный элемент (на практике сложнее)
Iterator<K> it = map.keySet().iterator();
if (it.hasNext()) {
map.remove(it.next());
}
}
map.put(key, value);
}
}
}


Сжатие данных: время на распаковку vs экономия памяти


Сжатие данных — это еще одна форма компромисса, где экономия памяти достигается за счет времени на сжатие и распаковку.
public class CompressionTradeoff {
// Быстрое сжатие (LZ4), но меньшая степень сжатия
public byte[] compressFast(byte[] data) {
// LZ4: высокая скорость, средняя степень сжатия
// Подходит для компрессии "на лету"
return lz4Compress(data);
}

// Медленное сжатие (Zstandard с высоким уровнем), но лучшее сжатие
public byte[] compressHigh(byte[] data) {
// Zstandard уровень 19: медленно, но максимальное сжатие
// Подходит для архивов, где сжатие выполняется один раз
return zstdCompress(data, 19);
}

// Выбор стратегии в зависимости от контекста
public byte[] compressAdaptive(byte[] data, boolean prioritizeSpeed) {
if (prioritizeSpeed) {
return compressFast(data); // Для сетевой передачи
} else {
return compressHigh(data); // Для долговременного хранения
}
}
}


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

import java.util.TreeSet;

public class Task130126 {
static class Item130126 implements Comparable<Item130126> {
int value;
Item130126(int value) { this.value = value; }

public int compareTo(Item130126 other) {
return Integer.compare(this.value, other.value);
}
}
public static void main(String[] args) {
TreeSet<Item130126> set = new TreeSet<>();
Item130126 item1 = new Item130126(1);
Item130126 item2 = new Item130126(2);
set.add(item1);
set.add(item2);
item1.value = 3;
System.out.println(set.contains(item1));
System.out.println(set.contains(item2));
set.remove(item1);
System.out.println(set.size());
}
}


#Tasks
👍1
Варианты ответа:
Anonymous Quiz
43%
true true 1
36%
false true 2
14%
true false 1
7%
false false 2
👍1
Почему @Transactional не работает при self-invocation? 🤓

Ответ:

Spring использует прокси.

Вызов метода внутри того же класса минует прокси, поэтому транзакция не создаётся.

Это частая ошибка, которую проверяют на собеседованиях.


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


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

Никого не нашел(


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

2005 — космический зонд «Гюйгенс» совершил посадку на Титан.

2020 — прекращение расширенной технической поддержки Windows 7 и Windows 10 Mobile.


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

Глава 2: Линейные алгоритмы и один проход

Линейный поиск и его ниша

Линейный поиск — фундаментальный алгоритм, основанный на последовательной проверке элементов коллекции. Его детерминированность гарантирует, что при наличии целевого элемента в коллекции он будет обнаружен за конечное число шагов. Алгоритм не требует специальной организации данных и работает с массивами, списками и другими структурами с последовательным доступом.

public class LinearSearch {
public static int find(int[] array, int target) {
for (int i = 0; i < array.length; i++) {
if (array[i] == target) {
return i; // Элемент найден
}
}
return -1; // Элемент отсутствует
}

// Универсальная версия для любых Comparable объектов
public static <T extends Comparable<T>> int findGeneric(T[] array, T target) {
for (int i = 0; i < array.length; i++) {
if (array[i].compareTo(target) == 0) {
return i;
}
}
return -1;
}
}



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

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

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


Оптимальные сценарии применения

Маленькие объемы данных (n < 100)

Для коллекций до 100 элементов преимущества алгоритмов с лучшей асимптотикой нивелируются их константными издержками. Рассмотрим практический пример: поиск в массиве из 50 целых чисел.

Линейный поиск выполнит в среднем 25 сравнений. При скорости современных процессоров (миллиарды операций в секунду) это занимает наносекунды. Бинарный поиск потребует предварительной сортировки массива — операция O(n log n), которая для 50 элементов означает примерно 300 операций сравнений и перестановок.

Неотсортированные данные для разовых операций

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

Ситуации с ограниченными ресурсами разработки

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


#Java #для_новичков #beginner #algorithm #line_search
👍3
Количественное сравнение производительности

Рассмотрим конкретные измерения для массива из 50 элементов на процессоре с тактовой частотой 3 ГГц:
Линейный поиск: 25 сравнений × 0.3 наносекунды ≈ 7.5 наносекунд
Бинарный поиск: 6 сравнений × 0.3 нс + накладные расходы на вычисление середины ≈ 5 наносекунд
Подготовка к бинарному поиску: сортировка 50 элементов ≈ 300 операций × 0.3 нс ≈ 90 наносекунд
Разница в 2.5 наносекунды между линейным и бинарным поиском теряется на фоне 90 наносекунд, затраченных на сортировку. Для HashMap ситуация аналогична — вычисление хеша, обработка коллизий и аллокация памяти создают значительные накладные расходы.


Практические реализации в Java

В стандартной библиотеке Java линейный поиск представлен несколькими методами:
// Для List интерфейса
List<String> list = Arrays.asList("A", "B", "C", "D");
int index = list.indexOf("C"); // Линейный поиск

// Для массивов через Collections
String[] array = {"A", "B", "C", "D"};
int pos = Collections.indexOf(Arrays.asList(array), "C");

// Использование Stream API (также линейный поиск)
Optional<String> result = Stream.of(array)
.filter(s -> s.equals("C"))
.findFirst();



Для примитивных типов рекомендуется ручная реализация, избегающая автоупаковки:

public static int findInt(int[] array, int target) {
// Развернутый цикл для потенциальной оптимизации JIT-компилятором
int len = array.length;
for (int i = 0; i < len; i++) {
if (array[i] == target) {
return i;
}
}
return -1;
}



Когда переходить к сложным алгоритмам

Критерии для выбора более сложных алгоритмов поиска:
Объем данных превышает 1000 элементов — асимптотические преимущества становятся значимыми
Многократный поиск в одной коллекции — инвестиции в подготовку данных окупаются
Критическая производительность — когда наносекунды имеют значение в высоконагруженных системах
Специфические требования — поиск по диапазону, префиксу или сложному ключу

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

import java.util.*;

public class Task140126 {
public static void main(String[] args) {
List<Double> list = Arrays.asList(1.0, 2.0, 3.0, Double.NaN, 4.0);

int index1 = list.indexOf(Double.NaN);
int index2 = list.lastIndexOf(Double.NaN);
int index3 = list.indexOf(Double.valueOf(Double.NaN));
boolean contains = list.contains(Double.NaN);

System.out.println(index1 + " " + index2 + " " + index3 + " " + contains);
}
}


#Tasks
👍1
Варианты ответа:
Anonymous Quiz
56%
3 3 3 true
13%
3 3 -1 true
31%
-1 -1 -1 false
0%
-1 -1 3 false
👍2
Чем prototype-бин опасен в Spring? 🤓

Ответ:

Spring не управляет жизненным циклом prototype после создания.

Он не уничтожается автоматически. Это может привести к утечкам ресурсов, если бин содержит соединения или файловые дескрипторы.


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


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

Со́фья Васи́льевна Ковале́вская (урождённая Корвин-Круковская; 3 [15] января 1850, Москва — 29 января [10 февраля] 1891, Стокгольм)русский математик и механик, с 1889 года — иностранный член-корреспондент Петербургской академии наук. Первая в мире женщина — профессор математики. Её работы в области дифференциальных уравнений и аналитической механики лежат в основе алгоритмов численного моделирования, которые используются в современном софте для физических и инженерных расчетов.

Дави́д Пи́нхусович Ми́льман (15 января 1912,[3] Чечельник, Ольгопольский уезд, Подольская губерния — 12 июля 1982, Тель-Авив)советский и израильский математик, известный работами в области функционального анализа, в частности теории операторов. Кандидат физико-математических наук (1939). Его теоремы и методы используются в теории оптимизации и машинном обучении (Gradient Descent и другие методы опираются на этот раздел математики).


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

2001 — начал работу англоязычный сайт «Википедии», вики-энциклопедии со свободно распространяемым содержимым.


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

Глава 2: Линейные алгоритмы и один проход

Агрегация за один проход: максимум эффективности

Однопроходные алгоритмы (One-Pass Algorithms) обрабатывают входные данные строго последовательно, без возвратов и повторных чтений. Каждый элемент просматривается ровно один раз, а результаты агрегации обновляются инкрементально. Этот подход идеален для потоковых данных, больших наборов, не помещающихся в память, и систем реального времени.

Основной принцип: поддерживать минимальное необходимое состояние (state) и обновлять его при получении каждого нового элемента. Такой подход требует O(1) дополнительной памяти (кроме самих данных) и выполняется за O(n) времени.


Базовые операции: минимум, максимум, сумма

Поиск минимума и максимума

Простейший однопроходный алгоритм ищет минимальный и максимальный элементы, поддерживая двух кандидатов:
public class ExtremumFinder {
public static MinMax findMinMax(int[] arr) {
int min = arr[0], max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] < min) min = arr[i];
if (arr[i] > max) max = arr[i];
}
return new MinMax(min, max);
}
}

Оптимизация: обработка элементов парами уменьшает количество сравнений с 2n до 3n/2. Вместо сравнения каждого элемента с обоими экстремумами, сначала сравниваем пару элементов между собой, затем меньший — с минимумом, больший — с максимумом.


Сумма и количество

Вычисление суммы — канонический пример свертки (fold):
public long sum(int[] arr) {
long total = 0; // long для избежания переполнения
for (int value : arr) {
total += value;
}
return total;
}


Для подсчета элементов, удовлетворяющих условию, добавляем фильтрацию:
public long countPositive(int[] arr) {
long count = 0;
for (int value : arr) {
if (value > 0) count++;
}
return count;
}



Усложненные задачи агрегации

Поиск двух максимальных значений

Задача: найти две самые старые книги (два минимальных года издания).

Алгоритм поддерживает двух кандидатов:
public static int[] findTwoOldest(int[] years) {
if (years.length < 2) throw new IllegalArgumentException();

// Инициализируем упорядоченно
int oldest = Math.min(years[0], years[1]);
int secondOldest = Math.max(years[0], years[1]);

for (int i = 2; i < years.length; i++) {
if (years[i] < oldest) {
secondOldest = oldest;
oldest = years[i];
} else if (years[i] < secondOldest) {
secondOldest = years[i];
}
}
return new int[]{oldest, secondOldest};
}

Особенность: алгоритм корректно обрабатывает дубликаты и все возможные порядки следования элементов. Сложность — O(n) с ровно n-1 сравнениями в худшем случае.



Вычисление среднего и дисперсии за один проход

Традиционно дисперсию вычисляют в два прохода, но алгоритм Уэлфорда (Welford) решает задачу за один проход, обеспечивая численную стабильность:
public class Statistics {
public static Stats compute(int[] data) {
double mean = 0, m2 = 0; // m2 - сумма квадратов отклонений
int n = 0;

for (int x : data) {
n++;
double delta = x - mean;
mean += delta / n;
m2 += delta * (x - mean);
}

double variance = (n > 1) ? m2 / (n - 1) : 0;
return new Stats(mean, variance);
}
}

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



#Java #для_новичков #beginner #algorithm #line_search
👍4
Поиск наиболее частого элемента

Для поиска моды (наиболее частого элемента) используем хеш-таблицу для подсчета частот:

public static String mostFrequentAuthor(String[] authors) {
Map<String, Integer> freq = new HashMap<>();
String mostFreq = null;
int maxCount = 0;

for (String author : authors) {
int count = freq.getOrDefault(author, 0) + 1;
freq.put(author, count);

if (count > maxCount) {
maxCount = count;
mostFreq = author;
}
}
return mostFreq;
}


Пространственная сложность — O(k), где k — количество уникальных авторов. Если элемент составляет строгое большинство (> n/2), эффективнее алгоритм Бойера-Мура, который находит кандидата за O(n) времени и O(1) памяти:
public static String majorityElement(String[] arr) {
String candidate = null;
int count = 0;

for (String s : arr) {
if (count == 0) {
candidate = s;
count = 1;
} else if (s.equals(candidate)) {
count++;
} else {
count--;
}
}

// Второй проход для проверки (тоже O(n))
return candidate; // если гарантировано наличие большинства
}



Принцип: один сложный проход против нескольких простых

Константные факторы имеют значение

Хотя с точки зрения асимптотической нотации 2*O(n) = O(n), на практике константные множители существенны.

Рассмотрим обработку массива из 10 миллионов элементов:
Два последовательных прохода: 20 миллионов операций чтения, двойные накладные расходы на управление циклом, потенциально двойное количество кэш-промахов.
Один сложный проход: 10 миллионов операций чтения, но больше вычислений на элемент.
Если один проход в 1.3 раза "тяжелее" на элемент, но избавляет от второго прохода, общее время: 1.3T против 2T — выигрыш ~35%.


Использование кэша процессора

Современные процессоры имеют иерархию памяти: кэш L1 (1-2 нс), L2 (3-10 нс), L3 (10-20 нс), основная память (80-100 нс). Однопроходные алгоритмы лучше используют пространственную локальность: данные, прочитанные в кэш, сразу используются для всех необходимых вычислений.

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


Комплексная агрегация за один проход

Многие задачи можно решить за один проход, комбинируя несколько агрегаций:
public class CombinedStats {
int min, max, count;
long sum;

public void process(int value) {
if (value < min) min = value;
if (value > max) max = value;
sum += value;
count++;
}

public double average() {
return (double) sum / count;
}
}
Такой подход особенно ценен при обработке потоковых данных, где невозможно хранить историю.



Практическое применение

Потоковая обработка данных
Однопроходные алгоритмы — основа систем обработки потоков (Apache Kafka, Apache Flink, Apache Storm). Они позволяют вычислять скользящие средние, агрегировать метрики в реальном времени, обнаруживать аномалии без хранения всего потока.

Анализ больших данных
В MapReduce и подобных фреймворках комбинаторы (combiners) используют однопроходные агрегации для уменьшения объема передаваемых данных. Вместо передачи всех значений, каждый узел предварительно агрегирует свою порцию.

Мониторинг и телеметрия
Системы мониторинга (Prometheus, InfluxDB) вычисляют метрики на лету, используя однопроходные алгоритмы для экономии памяти и процессорного времени.


Ограничения и предостережения

Не все задачи решаемы за один проход.

Например:
Поиск медианы требует хранения хотя бы части данных
Вычисление корреляции между двумя потоками требует их синхронизации
Некоторые статистики (квантили) требуют случайного доступа к данным
Однопроходные алгоритмы часто являются приближенными или требуют компромиссов между точностью и эффективностью.

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

import java.util.List;

public class Task150126 {
public static void main(String[] args) {
List<String> data = List.of("A", "B", "C");

long count = data.stream()
.peek(s -> System.out.print(s))
.map(s -> s + s)
.count();

System.out.print(count);
}
}


#Tasks
👍1