Одинаковый результат после применения хэш-функции называется коллизией.
Коллизия возникает, когда две разные входные величины (ключи) при обработке одной и той же хэш-функцией дают одинаковое хэш-значение. Это проблема, так как хэш-функции обычно используются для быстрого доступа к данным (например, в хэш-таблицах), и одинаковые хэши для разных данных нарушают уникальность ключей.
Хэш-функция генерирует значения фиксированной длины (например, 32 или 64 бита), поэтому существует конечное количество возможных хэшей. Однако количество входных данных, которые можно подать на вход, теоретически бесконечно.
Некоторые хэш-функции менее устойчивы к коллизиям из-за их структуры.
Есть несколько способов обработки коллизий в хэш-таблицах:
Каждое значение хэша хранит список элементов, которые получили одинаковый хэш. При добавлении нового ключа он просто добавляется в связанный список.
#include <iostream>
#include <list>
#include <vector>
using namespace std;
class HashTable {
int size;
vector<list<int>> table;
public:
HashTable(int s) : size(s), table(s) {}
int hashFunction(int key) {
return key % size;
}
void insert(int key) {
int index = hashFunction(key);
table[index].push_back(key);
}
void display() {
for (int i = 0; i < size; i++) {
cout << i << ": ";
for (int key : table[i]) {
cout << key << " -> ";
}
cout << "NULL" << endl;
}
}
};
int main() {
HashTable ht(5);
ht.insert(10);
ht.insert(15);
ht.insert(20);
ht.insert(7);
ht.insert(2);
ht.display();
return 0;
}
При коллизии новое значение записывается в следующую свободную ячейку в таблице. Используются разные стратегии, такие как линейное пробирование, квадратичное пробирование или двойное хэширование.
Устойчивые хэш-функции (например, SHA-256) уменьшают вероятность коллизий, но они медленнее и чаще используются в безопасности.
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1🔥1
Контейнеры set и unordered_set представляют собой различные структуры данных, каждая из которых имеет свои особенности по скорости выполнения основных операций, включая поиск. Вот как работают эти контейнеры и какова сложность их операций поиска:
Реализуется как сбалансированное двоичное дерево поиска, обычно как красно-черное дерево. Он хранит элементы в отсортированном порядке, что позволяет выполнять двоичный поиск. Сложность поиска: Поиск в нем выполняется за логарифмическое время, \(O(\log n)\), где \(n\) — количество элементов в
set. Эта эффективность достигается за счёт использования структуры сбалансированного дерева, которое позволяет быстро делить данные на меньшие сегменты.Реализуется с использованием хеш-таблицы. Это позволяет, при идеальных условиях, выполнять поиск за константное время. Сложность поиска: В среднем, поиск в нем занимает константное время \(O(1)\). Однако в худшем случае, например, при неудачной работе хеш-функции или при большом количестве коллизий, поиск может деградировать до \(O(n)\). В таких ситуациях все ключи могут оказаться в одной "корзине" или "ведре" (bucket), и для нахождения правильного элемента потребуется просмотреть все элементы в этом ведре.
Для
set#include <iostream>
#include <set>
int main() {
std::set<int> mySet = {5, 3, 9, 1};
auto search = mySet.find(3);
if (search != mySet.end()) {
std::cout << "Found " << *search << std::endl;
} else {
std::cout << "Not found" << std::endl;
}
return 0;
}
Для
unordered_set#include <iostream>
#include <unordered_set>
int main() {
std::unordered_set<int> mySet = {5, 3, 9, 1};
auto search = mySet.find(3);
if (search != mySet.end()) {
std::cout << "Found " << *search << std::endl;
} else {
std::cout << "Not found" << std::endl;
}
return 0;
}
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
💊1
Стек: Память в стеке управляется автоматически. Когда вызывается функция, память для её локальных переменных выделяется одним блоком при входе в функцию и освобождается при выходе из неё. Эта операция выполняется за постоянное время (O(1)).
Куча: Память в куче управляется вручную (программистом) или через автоматическое управление памятью (например, сборщик мусора). Выделение и освобождение памяти в куче требуют поиска подходящего блока памяти, что может занимать больше времени (O(log n) или даже O(n)).
Стек: Данные в стеке расположены компактно и последовательно. Это означает, что доступ к данным будет быстрее из-за лучшего использования кэш-памяти процессора.
Куча: Данные в куче могут быть фрагментированы, что приводит к меньшей эффективности кэширования и увеличению времени доступа.
Стек: Память в стеке выделяется и освобождается в строго определённом порядке (LIFO - Last In, First Out). Это делает операции со стеком предсказуемыми и упрощает управление памятью.
Куча: Память в куче может выделяться и освобождаться в произвольном порядке, что приводит к фрагментации и усложняет управление памятью.
Стек: Операции выделения и освобождения памяти на стеке имеют минимальные накладные расходы, так как это просто смещение указателя стека.
Куча: Операции выделения и освобождения памяти в куче требуют более сложных алгоритмов и могут включать в себя дополнительные накладные расходы, такие как управление списками свободных блоков и слияние фрагментов.
#include <iostream>
void stackFunction() {
int stackArray[1000]; // Массив на стеке
// Работа с массивом
}
void heapFunction() {
int* heapArray = new int[1000]; // Массив в куче
// Работа с массивом
delete[] heapArray; // Освобождение памяти
}
int main() {
stackFunction(); // Быстрая работа со стеком
heapFunction(); // Медленная работа с кучей
return 0;
}
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Реальное перемещение выполняется методами, поддерживающими rvalue-ссылки, например, конструктором перемещения или оператором присваивания.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
При перегрузке оператора
= в C++ принято возвращать ссылку на текущий объект (*this). Это позволяет цепочечное присваивание (a = b = c), а также улучшает производительность и удобство использования. class MyClass {
public:
MyClass& operator=(const MyClass& other) {
if (this != &other) { // Защита от самоприсваивания
// Копируем данные
}
return *this; // Возвращаем ссылку на текущий объект
}
};Поддержка цепочки присваивания (
a = b = c)a = b = c; // Интерпретируется как a = (b = c);
Пример
#include <iostream>
class MyClass {
public:
int value;
MyClass(int v) : value(v) {}
MyClass& operator=(const MyClass& other) {
if (this != &other) { // Защита от самоприсваивания
value = other.value;
}
return *this;
}
};
int main() {
MyClass a(1), b(2), c(3);
a = b = c; // Работает, потому что `b = c` возвращает ссылку на `b`
std::cout << "a: " << a.value << ", b: " << b.value << ", c: " << c.value << std::endl;
}
Вывод
a: 3, b: 3, c: 3
Если оператор
= вернёт не ссылку, а объект, то произойдёт лишнее копирование. Плохо (возвращаем объект, а не ссылку)
MyClass operator=(const MyClass& other) { // ⚠️ Ошибка: возвращаем объект
value = other.value;
return *this; // Здесь создаётся временный объект (лишняя копия!)
}Хорошо (возвращаем
T&) MyClass& operator=(const MyClass& other) { // ✅ Возвращаем ссылку
value = other.value;
return *this;
}Если оператор
= вернёт ссылку, мы можем использовать его в условии: if ((a = b).value == 42) { // ОК
std::cout << "Присваивание выполнено, a == 42";
}Если оператор
= вернёт void, цепочка a = b = c; не будет работать. class MyClass {
public:
void operator=(const MyClass& other) { // ⚠️ Ошибка!
value = other.value;
}
};
int main() {
MyClass a, b, c;
a = b = c; // ❌ Ошибка: `b = c` возвращает void, а `a = void` не работает!
}Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
💊1
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
std::map реализован как самобалансирующееся красно-чёрное дерево (Red-Black Tree). Вставка (
insert)Поиск (
find) Удаление (
erase)выполняются за O(log n) в худшем и среднем случае.
Однако
std::map не имеет амортизированной O(1) сложности, как std::unordered_map. Амортизированная сложность O(1) появляется в
std::unordered_map, но не в std::map! Каждая операция (
insert, erase) перестраивает дерево так, чтобы его высота оставалась O(log n). Это достигается с помощью поворотов (rotate left, rotate right) и перекраски узлов. Вставка нового элемента иногда требует перестройки дерева, но в среднем это занимает O(log n).
10
/ \
5 20
В
std::unordered_map используется хеш-таблица, где: Операции
insert, find, erase – O(1) в среднем (поиск по хешу). Но если возникает коллизия (несколько ключей в одном бакете), сложность ухудшается до O(n).
Чтобы избежать O(n),
std::unordered_map использует rehash (перехеширование). При увеличении элементов контейнер расширяет бакеты.
Операции разделяются по разным вызовам → за счёт этого амортизированная сложность O(1).
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
💊3
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4🤔1
Виртуальное наследование применяется для решения проблемы "ромбовидного наследования" (или "diamond problem"), которая возникает в иерархиях классов с множественным наследованием. Это проблема возникает, когда класс наследуется от двух классов, которые в свою очередь наследуются от одного общего базового класса. В результате создаются две копии базового класса в наследнике, что приводит к неоднозначностям и увеличению использования памяти.
#include <iostream>
class A {
public:
void show() {
std::cout << "Class A" << std::endl;
}
};
class B : public A {};
class C : public A {};
class D : public B, public C {};
int main() {
D obj;
// obj.show(); // Ошибка: неоднозначность, show() есть и в B, и в C
return 0;
}
Виртуальное наследование решает эту проблему, гарантируя, что в иерархии будет только одна копия базового класса. Это достигается с помощью ключевого слова
virtual при наследовании.#include <iostream>
class A {
public:
void show() {
std::cout << "Class A" << std::endl;
}
};
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};
int main() {
D obj;
obj.show(); // Корректно: вызов метода show() из A
return 0;
}
При виртуальном наследовании в наследуемом классе создается только одна копия базового класса, независимо от того, сколько раз он виртуально наследуется.
Виртуальное наследование предотвращает создание множества копий базового класса, что экономит память.
Виртуальное наследование устраняет неоднозначности, связанные с вызовом методов и доступом к членам базового класса.
При виртуальном наследовании конструкторы базовых классов вызываются инициализирующим классом.
#include <iostream>
class A {
public:
A() {
std::cout << "Constructor of A" << std::endl;
}
};
class B : virtual public A {
public:
B() {
std::cout << "Constructor of B" << std::endl;
}
};
class C : virtual public A {
public:
C() {
std::cout << "Constructor of C" << std::endl;
}
};
class D : public B, public C {
public:
D() {
std::cout << "Constructor of D" << std::endl;
}
};
int main() {
D obj; // Вывод: Constructor of A, Constructor of B, Constructor of C, Constructor of D
return 0;
}
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Значение переменной перейдёт в максимальное значение типа (например, UINT_MAX для unsigned int).
Это связано с переполнением, так как беззнаковые типы используют арифметику по модулю.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Существует множество алгоритмов сортировки, каждый из которых имеет свои преимущества и недостатки в зависимости от условий использования. Рассмотрим основные из них.
Простейший алгоритм, который многократно проходит по массиву, сравнивая соседние элементы и меняя их местами, если они стоят в неправильном порядке.
Сложность: O(n²) в худшем и среднем случаях, O(n) в лучшем случае (если массив уже отсортирован).
Когда использовать: Почти никогда, так как слишком медленный.
void bubbleSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
std::swap(arr[j], arr[j + 1]);
}
}
}
}На каждом шаге ищется минимальный элемент и ставится в начало неотсортированной части массива.
Сложность: O(n²) всегда.
Когда использовать: Если важна простота реализации, но нужна немного лучшая производительность, чем у пузырьковой сортировки.
void selectionSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
int minIdx = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIdx]) {
minIdx = j;
}
}
std::swap(arr[i], arr[minIdx]);
}
}Берём один элемент и вставляем его в правильное место среди уже отсортированных элементов.
Сложность: O(n²) в худшем случае, O(n) в лучшем (если массив почти отсортирован).
Когда использовать: Для небольших массивов или почти отсортированных данных.
void insertionSort(int arr[], int n) {
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}Разделяем массив на две части, рекурсивно сортируем их и затем сливаем.
Сложность: O(n log n) всегда.
Когда использовать: Когда нужна стабильность и предсказуемая скорость работы.
void merge(int arr[], int l, int m, int r) {
int n1 = m - l + 1, n2 = r - m;
int L[n1], R[n2];
for (int i = 0; i < n1; i++) L[i] = arr[l + i];
for (int i = 0; i < n2; i++) R[i] = arr[m + 1 + i];
int i = 0, j = 0, k = l;
while (i < n1 && j < n2) arr[k++] = (L[i] < R[j]) ? L[i++] : R[j++];
while (i < n1) arr[k++] = L[i++];
while (j < n2) arr[k++] = R[j++];
}
void mergeSort(int arr[], int l, int r) {
if (l < r) {
int m = l + (r - l) / 2;
mergeSort(arr, l, m);
mergeSort(arr, m + 1, r);
merge(arr, l, m, r);
}
}Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
Это контейнеры из стандартной библиотеки, но у них есть важные различия в управлении памятью, гибкости и производительности.
std::vector использует динамическую память, выделяемую в куче (heap). Его размер может изменяться во время выполнения.std::array использует статическую память, выделяемую в стеке (stack) или в статической области памяти, и его размер фиксирован на этапе компиляции.#include <vector>
#include <array>
#include <iostream>
int main() {
std::vector<int> vec = {1, 2, 3}; // Размер может изменяться динамически
vec.push_back(4); // Добавляем новый элемент
std::array<int, 3> arr = {1, 2, 3}; // Размер фиксирован, нельзя добавить новый элемент
std::cout << "Vector size: " << vec.size() << std::endl; // Выведет 4
std::cout << "Array size: " << arr.size() << std::endl; // Выведет 3
return 0;
}
std::vector позволяет изменять размер в процессе работы, автоматически выделяя новую память при необходимости.std::array имеет фиксированный размер, который нельзя изменить после создания.std::vector<int> v = {1, 2, 3};
v.push_back(4); // Увеличиваем размер
std::array<int, 3> a = {1, 2, 3};
// a.push_back(4); // Ошибка! У std::array нет метода push_backstd::array работает быстрее, так как все данные хранятся в непрерывном участке памяти и нет затрат на динамическое выделение.std::vector может требовать дополнительное время при изменении размера, так как может потребоваться новое выделение памяти и копирование элементов.#include <vector>
#include <array>
#include <chrono>
#include <iostream>
int main() {
constexpr int N = 1'000'000;
std::vector<int> vec(N, 1); // Динамический массив
std::array<int, N> arr{}; // Статический массив
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < N; ++i) vec[i] += 1;
auto end = std::chrono::high_resolution_clock::now();
std::cout << "Vector time: "
<< std::chrono::duration_cast<std::chrono::microseconds>(end - start).count()
<< " us" << std::endl;
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < N; ++i) arr[i] += 1;
end = std::chrono::high_resolution_clock::now();
std::cout << "Array time: "
<< std::chrono::duration_cast<std::chrono::microseconds>(end - start).count()
<< " us" << std::endl;
return 0;
}
std::array хранит данные как обычный C-массив, поэтому можно легко передавать его в функции, ожидающие int*.std::vector использует динамическую память, но можно получить указатель на внутренний буфер с помощью data().void processArray(int* arr, size_t size) {
for (size_t i = 0; i < size; ++i) {
std::cout << arr[i] << " ";
}
}
int main() {
std::array<int, 3> arr = {1, 2, 3};
std::vector<int> vec = {4, 5, 6};
processArray(arr.data(), arr.size()); // std::array можно передавать в C-функции
processArray(vec.data(), vec.size()); // std::vector тоже можно передавать
return 0;
}Оба контейнера поддерживают итераторы и совместимы со стандартными алгоритмами из
#include <algorithm>#include <iostream>
#include <vector>
#include <array>
#include <algorithm>
int main() {
std::vector<int> vec = {3, 1, 4, 1, 5};
std::array<int, 5> arr = {3, 1, 4, 1, 5};
std::sort(vec.begin(), vec.end());
std::sort(arr.begin(), arr.end());
for (int n : vec) std::cout << n << " "; // 1 1 3 4 5
std::cout << std::endl;
for (int n : arr) std::cout << n << " "; // 1 1 3 4 5
return 0;
}
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Деструктор должен быть виртуальным, если класс предназначен для использования в качестве базового, и предполагается полиморфное удаление через указатель (Base* ptr = new Derived; delete ptr;). Без виртуального деструктора деструкторы производных классов не будут вызваны, что приведет к утечке памяти.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3🔥1
Делиторы – это специальные функции, которые определяют, как должен уничтожаться объект при освобождении памяти умным указателем.
Обычно
std::unique_ptr и std::shared_ptr по умолчанию вызывают delete, но иногда это поведение нужно изменить. Например, если объект создан через
malloc(), fopen(), CreateFile(), то delete не подходит! Можно добавить
std::cout, логику очистки, сброс ресурсов. delete не удалит массив корректно, нужно delete[]. Например, в
std::shared_ptr можно передать free(), fclose() или CloseHandle(). Пример: освобождение памяти от
malloc() через std::free()#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int, void(*)(void*)> ptr(malloc(sizeof(int)), free);
*reinterpret_cast<int*>(ptr.get()) = 42;
std::cout << "Значение: " << *reinterpret_cast<int*>(ptr.get()) << "\n";
}
Здесь
free используется вместо delete, потому что память выделена malloc(). Пример: закрытие файла через
std::fclose()#include <iostream>
#include <memory>
#include <cstdio>
int main() {
std::unique_ptr<FILE, decltype(&std::fclose)> file(fopen("test.txt", "w"), &std::fclose);
if (file) {
std::fprintf(file.get(), "Hello, world!\n");
}
} // `fclose(file)` вызовется автоматически!
В
std::shared_ptr можно передавать делитор прямо в конструкторе#include <iostream>
#include <memory>
void customDeleter(int* ptr) {
std::cout << "Удаляю объект: " << *ptr << "\n";
delete ptr;
}
int main() {
std::shared_ptr<int> ptr(new int(100), customDeleter);
} // В конце `customDeleter(ptr)` вызовется автоматически!
По умолчанию
std::unique_ptr<int[]> сам вызывает delete[], но если std::unique_ptr<int> используется неправильно, то delete удалит только первый элемент массива! Ошибка:
delete вместо delete[]std::unique_ptr<int> arr(new int[10]); // ОШИБКА! `delete` вызовет утечку памяти!
Правильный вариант
std::unique_ptr<int[], std::default_delete<int[]>> arr(new int[10]);
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
Равенство двух float проверяется с учётом допустимой разницы (эпсилон), чтобы избежать ошибок из-за неточности представления:
∣a−b∣<ϵ, где ϵ — небольшое значение, например 10−610^{-6}.
Такой подход помогает корректно сравнивать близкие числа, которые могут отличаться в пределах допустимой погрешности.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
В C++ можно запретить наследование от класса несколькими способами.
Ключевое слово
final запрещает наследование от класса.class Base final {
public:
void show() { std::cout << "Base class\n"; }
};
// Ошибка! Наследование запрещено
class Derived : public Base {
};Можно сделать так, чтобы класс нельзя было создать или скопировать в унаследованных классах.
class Base {
private:
Base() = default; // Приватный конструктор
};Можно сделать конструктор
protected, если хотим создать объекты внутри класса, но не разрешать наследование снаружи. class Base {
protected:
~Base() = default; // Деструктор защищённый
};Если сделать деструктор
private, то класс нельзя будет корректно удалить через указатель. class Base {
private:
~Base() = default; // Закрытый деструктор
};Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Это класс, который содержит хотя бы одну чисто виртуальную функцию. Он не может быть создан как объект и предназначен для использования в качестве базового класса. Такие классы служат для определения интерфейсов и полиморфного поведения.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM