Написать метод, который принимает основание и показатель степени, реализует алгоритм быстрого возведения в степень и возвращает результат. Ниже приведены примеры.
double x = 3;
int n = 3;
double result = FastPow(x, n);
double x = 3;
int n = 1;
double result = FastPow(x, n);
double result = FastPow(2, 18);
FastPow(2, 51);
FastPow(3, 32);
FastPow(9, 13);
FastPow(5, 16);
Пишите варианты в комментариях. Решение будет сегодня вечером новым постом в канале.
#Задача #Lvl1
Please open Telegram to view this post
VIEW IN TELEGRAM
Представим, что мы крупный сайт — какой-нибудь маркетплейс — и нам нужно определять количество уникальных посетителей. Как мы это сделаем? Вполне логичным решением будет сохранение нового IP-адреса из потока сетевого трафика в список, а общее количество элементов такого списка и будет сообщать нам количество уникальных посетителей.
Вопреки такому решению, хранение всех этих данных (IP`шки) для нас может быть лишним. Мы не будем их использовать в дальнейшем, но каждый хранимый объект в памяти будет непременно "раздувать" его общее заполнение. В оптимизации такой задачи нам и поможет фильтр Блума, который мы сегодня рассмотрим на примере поставленной задачи.
Фильтр Блума проверяет принадлежность конкретного элемента к множеству данных без необходимости его хранения. Он использует массив битов (0 — false, 1 — true), а также несколько хеш-функций. Когда новый объект попадает в базу, его хеш-коды вычисляются, заполняя соответствующие биты массива в значении 1. Например, если хеш-функция возвращает число 50, то 50 бит (он же индекс) массива устанавливается в 1. Так происходит добавление IP-адреса в фильтр.
Если при получении нового IP-адреса все нужные биты уже установлены в значение 1, то это значит, что он уже встречался ранее. Но важно помнить, что фильтр Блума не даёт 100% гарантии, что IP-адрес был уже замечен. Всегда есть небольшой риск ложноположительного результата. Эта вероятность зависит от размера массива битов, количества хеш-функций и количества уникальных IP-адресов.
Фильтр Блума экономит память системы, особенно при работе с большими множествами, ускоряя операции добавления/проверки. Он подходит для задач проверки принадлежности, особенно когда требуется скорость при работе системы в условиях ограниченной памяти. Несмотря на небольшой процент ложноположительных результатов, он широко используется в разных приложениях.
Коллизии возникают, когда несколько различных элементов имеют одинаковые хеш-коды, что может привести к ложноположительным результатам при проверке.
Стратегии снижения вероятности коллизий:
public class BloomFilter<T>
{
public BloomFilter(int capacity, int hashFunctionCount)
{
if (capacity < 1) throw new ArgumentOutOfRangeException(nameof(capacity), "Размер должен быть положительным");
if (hashFunctionCount < 1) throw new ArgumentOutOfRangeException(nameof(hashFunctionCount), "Количество хеш-функций должно быть положительным");
bitArray = new(capacity);
hashCount = hashFunctionCount;
}
private readonly BitArray bitArray;
private readonly int hashCount;
public void Add(T item)
{
foreach (int hash in GetHashValues(item))
{
bitArray[hash] = true;
}
}
public bool Contains(T item)
{
foreach (int hash in GetHashValues(item))
{
if (!bitArray[hash]) return false;
}
return true;
}
public void Clear() => bitArray.SetAll(false);
private IEnumerable<int> GetHashValues(T item)
{
for (int i = 0; i < hashCount; i++)
{
int seed = i * 31;
int hash = (item?.GetHashCode() ?? 0) ^ seed;
yield return Math.Abs(hash) % bitArray.Length;
}
}
}
Проверим:
var filter = new BloomFilter<string>(1000, 5);
var apple = "apple";
var banana = "banana";
filter.Add(apple);
filter.Add(banana);
Check(apple);
Check(banana);
Check("date");
Check("grape");
filter.Clear();
Check(apple);
Check(banana);
void Check(string item) => Console.WriteLine(filter.Contains(item));
Вывод:
True
True
False
False
False
False
Отмечу, что реализация сильно простая. Рекомендую заменять GetHashCode на хеш-функцию MurmurHash2, MD5 или др. Ставьте реакции на пост, если хотите посмотреть на лучшую реализацию — напишу в комментариях.
#Полезно #Хеширование #GetHashCode #Array
Please open Telegram to view this post
VIEW IN TELEGRAM
Нередко возникает необходимость узнать некоторые аспекты производительности программы: скорость выполнения, количество потребляемой памяти, сравнить производительность ряда альтернативных решений и выбрать из них наилучшее, и так далее. Для этих целей и принято использовать данную библиотеку.
Рассмотрим базовое использование библиотеки. Для этого создадим новый проект консольного приложения и через Nuget добавим пакет BenchmarkDotNet.
Рассмотрим примитивную программу, которая склеивает строки:
using System.Text;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
BenchmarkRunner.Run<StringTest>();
public class StringTest
{
public StringTest() { }
private string[] numbers = { "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten" };
[Benchmark]
public string StringBuilder()
{
StringBuilder stringBuilder = new StringBuilder();
foreach (string str in numbers)
{
stringBuilder.Append(str);
stringBuilder.Append(' ');
}
return stringBuilder.ToString();
}
[Benchmark]
public string Concatenation()
{
string result = "";
foreach (string str in numbers) result = result + str + " ";
return result;
}
[Benchmark]
public string Interpolation()
{
string result = "";
foreach (string str in numbers) result = $"{result}{str} ";
return result;
}
}
Мы определили класс StringTest, который определяет массив numbers из 10 строк и три метода. Методы используют разные техники, склеивая все эти строки в одну и возвращая её в качестве результата.
Для тестирования все методы должны иметь модификатор public и к ним должны применяться атрибуты [Benchmark]. Сам класс, который содержит эти методы, тоже должен иметь модификатор public.
Для запуска теста вызывается метод BenchmarkRunner.Run, который типизируется нашим классом:
BenchmarkRunner.Run<StringTest>();
Особенности запуска такого проекта:
По завершению теста консоль отобразит нам результаты. Нас интересует следующая таблица:
| Method | Mean | Error | StdDev |
|-------------- |---------:|--------:|---------:|
| StringBuilder | 313.5 ns | 6.31 ns | 17.49 ns |
| Concatenation | 427.0 ns | 8.51 ns | 7.96 ns |
| Interpolation | 428.7 ns | 5.10 ns | 4.26 ns |
Здесь столбец "Mean" представляет среднее время выполнения в наносекундах. Так, мы видим, что выполнение метода StringBuilder завершилось немногим быстрее, чем у других методов.
Инструмент имеет множество различных настроек и возможностей конфигурации. Полная документация библиотеки.
#Полезно #Benchmark
Please open Telegram to view this post
VIEW IN TELEGRAM
Написать метод, который принимает массив целочисленного типа и сортирует его алгоритмом гномьей сортировки. Ниже приведены примеры.
int[] arr = { 21, -53, 75, -49, -80, 100, -48, 27, 38, -47, -42, -72 };
GnomeSort(arr);
int[] arr = { 175, 136, 138, 300, 147, 252, 204, 91 };
GnomeSort(arr);
int[] arr = { 61, 45, 25, 31, 110, 48, 90, 114, 70, 60, 18, 112, 7, 82, 41, 39, 115, 84, 26, 29, 21, 57, 116, 65, 45 };
GnomeSort(arr);
int[] arr = { 6, 18, -7, -9, -12, -16, 0, 8, 7, 3, -2, -10, 1, -9, 9, -6, 11, 4 };
GnomeSort(arr);
int[] arr = { 8, 7, 6, 4, 3, 1 };
GnomeSort(arr);
Пишите варианты в комментариях. Решение будет сегодня вечером новым постом в канале.
#Задача #Lvl1
Please open Telegram to view this post
VIEW IN TELEGRAM
Ранее я уже немного писал о stackalloc, однако сейчас хочу разобрать более детально этот вопрос.
Ключевое слово позволяет выделить память под массив в стеке. Смысл выделения памяти в стеке — это повышение быстродействия кода. Выделенный стеком блок памяти автоматически удаляется при выходе из этого метода, поэтому он не подлежит сборке мусора.
Конструкторы ссылочных типов, используя оператор new, выделяют память в куче, а значит мы можем убедиться в том, что stackalloc не занимает память в куче.
Напишем класс:
[MemoryDiagnoser]
public class MemoryBenchmark
{
public MemoryBenchmark() { }
[Benchmark]
public void HeapArray()
{
decimal[] arr = new decimal[100];
for (int i = 0; i < 100; i++) arr[i] = 123;
}
[Benchmark]
public unsafe void StackArray()
{
decimal* arr = stackalloc decimal[100];
for (int i = 0; i < 100; i++) arr[i] = 123;
}
}
Метод HeapArray создаёт массив в привычном нам виде, а метод StackArray создаёт массив через аллокацию памяти в стеке. Для бенчмарка мы помечаем класс и методы соответствующими атрибутами.
Специально для примера берём самый объёмный тип decimal, который занимает 16 байт и 100 его элементов. Из этого следует, что мы ожидаем ~1600 занятых байтов в памяти (16 * 100). Запустим тест:
BenchmarkRunner.Run<MemoryBenchmark>();
Дождавшись результатов, я получил следующий отчёт:
| Method | Mean | Error | StdDev | Gen0 | Allocated |
|----------- |---------:|--------:|---------:|-------:|----------:|
| HeapArray | 322.1 ns | 6.39 ns | 12.00 ns | 0.7763 | 1624 B |
| StackArray | 228.7 ns | 3.15 ns | 2.95 ns | - | - |
Столбец "Allocated" сообщает, что метод HeapArray занял 1624 байта, что мы и ожидали. Метод же StackArray ни аллоцировал и байта, что свидетельствует поставленной задаче.
А теперь посмотрим на столбец "Mean": 322.1 наносекунд против 228.7 — разница в скорости заметная.
При выделении слишком большого объёма памяти в стеке возникает исключение StackOverflowException.
Рекомендуется ограничить объём памяти, выделяемый под стек, например, как в следующем коде:
int length = 1000;
Span<byte> buffer = length <= 1024 ? stackalloc byte[length] : new byte[length];
Поскольку объём доступной памяти на стеке зависит от среды, в которой выполняется код, при определении фактического предельного значения следует использовать консервативное значение.
Хоть это и ни к чему, но давайте убедимся в очистке стека после выхода из метода. Напишем и запустим следующий код:
var t = new Test();
t.Print();
public unsafe class Test
{
public Test()
{
int* ptr = stackalloc int[length];
pointer = ptr;
for (int i = 0; i < length; i++) ptr[i] = i + 1;
Print();
}
private const int length = 5;
private int* pointer;
public void Print()
{
for (int i = 0; i < length; i++) Console.WriteLine(pointer[i]);
Console.WriteLine("------------");
}
}
Вывод:
1
2
3
4
5
------------
1
0
2100389455
2046
5
------------
При первом выводе массива на экран всё работает как и ожидается, ибо вызов метода Print происходит внутри конструктора, где была выделена память.
После выхода из конструктора и вызова метода Print во второй раз блок памяти уже заполнен иначе, а значит стек уже не хранит наш массив и дальнейшая попытка работы с ним небезопасна.
#Полезно #Array #Unsafe #Pointers #Benchmark
Please open Telegram to view this post
VIEW IN TELEGRAM
🖥 Из примера выше, что более вероятно будет выведено на экран?
Anonymous Quiz
33%
[0] = 0; [1] = 10; [2] = 20;
38%
[0] = *мусорное_значение*; [1] = *мусорное_значение*; [2] = *мусорное_значение*;
11%
[3] = 0; [3] = 0; [3] = 0;
17%
Ошибка доступа к памяти
Валидация — это процесс проверки данных различных типов по критериям корректности и полезности для конкретного применения.
Смысл валидации в том, что мы совершаем проверку типа до его использования в дальнейшем. Она позволяет исключить поступление на вход заведомо ошибочных, неполных или неточных данных, которые могут привести к ошибочным результатам работы, утрате данных и сбоям в работе систем.
Фреймворк .NET предлагает удобный функционал в виде атрибутов из пространства имён System.ComponentModel.DataAnnotations. Я же люблю ручную самовалидацию, что мы и разберём ниже.
Рассмотрим крайне популярный процесс валидации на сегодняшний день — это корректность создаваемого пароля.
Сперва напишем интерфейс для валидации объектов. Я предпочитаю использовать сразу две версии, классическую и с обобщением, что расширит его гибкость:
public interface IValidatable
{
bool IsValid(object context);
}
public interface IValidatable<Tin, TOut> : IValidatable
{
TOut Validate(Tin context);
}
Теперь вспомним о современных требованиях к паролю. По классике — это 8+ символов, минимум 1 цифра, минимум 1 заглавная буква и минимум 1 специальный символ. В связи с этим мы можем реализовать структуру с обязательными требованиями к паролю:
public struct PasswordRequirements
{
public int MinLength { get; }
public int MinUppercase { get; }
public int MinDigits { get; }
public int MinSpecialChars { get; }
public PasswordRequirements() : this(8, 1, 1, 1) { }
public PasswordRequirements(int minLength, int minUppercase, int minDigits, int minSpecialChars)
{
MinLength = minLength;
MinUppercase = minUppercase;
MinDigits = minDigits;
MinSpecialChars = minSpecialChars;
}
}
Теперь нам нужен класс, который и реализует валидацию нашего пароля. Я предпочитаю извлекать методы валидации в отдельные объекты, что позволяет более гибко тестировать фактические типы данных. Дополнительно класс-валидации мы можем использовать как временный объект с некоторой доп. инфой — оценка сложности пароля (слабый/средний/хороший) и т. п. —, без нужды вечного хранения такой информации. Напишем же такой класс:
public class PasswordValidation : IValidatable<string, bool>
{
public static PasswordRequirements Requirements { get; }
// Здесь можно добавить свойство
// оценки сложности пароля
static PasswordValidation() => Requirements = new();
public PasswordValidation() { }
public bool IsValid(object password) => Validate(password?.ToString());
public bool Validate(string password)
{
if (string.IsNullOrWhiteSpace(password) || password.Length < Requirements.MinLength) return false;
int uppercases = 0, digits = 0, specialChars = 0;
foreach (char c in password)
{
if (char.IsUpper(c)) uppercases++;
else if (char.IsDigit(c)) digits++;
else if (!char.IsLetterOrDigit(c)) specialChars++;
}
return uppercases >= Requirements.MinUppercase && digits >= Requirements.MinDigits && specialChars >= Requirements.MinSpecialChars;
}
}
Поскольку требования к паролю у нас для всех юзеров одинаковы свойство Requirements у меня статическое.
Финалим наш пример фактическим типом пароля:
public class Password
{
public string Hash { get; }
static Password() => validator = new();
public Password(string value)
{
if (!validator.Validate(value)) throw new FormatException();
// Hash = ComputeMd5Hash(value);
// ...
}
private static readonly PasswordValidation validator;
}
Таким образом пароль будет проверяться сразу при инициализации и выбрасывать исключение при несоответствии требований:
Password p1 = new("qwerty"); // Ошибка
Password p2 = new("123qwerty"); // Ошибка
Password p3 = new("Qwerty123@"); // Успех
Валидацию пароля можно проверять и до инициализации фактического класса, ибо классы независимы друг от друга — это одна из гибкостей, о которой я писал выше.
#Полезно #Пароль #Generics
Please open Telegram to view this post
VIEW IN TELEGRAM
Написать метод, который принимает число, реализует алгоритм нахождения постоянной Капрекара и выводит итерации на экран. Метод должен уметь работать с трёхзначными и четырёхзначными числами. Ниже приведены примеры.
KaprekarConstant(5621);
KaprekarConstant(796);
KaprekarConstant(3412);
KaprekarConstant(6389);
KaprekarConstant(123);
Пишите варианты в комментариях. Решение будет сегодня вечером новым постом в канале.
#Задача #Lvl2
Please open Telegram to view this post
VIEW IN TELEGRAM
Решение задачи к посту.
#Задача #Решение #Полезно
void KaprekarConstant(int number)
{
if (number < 100 || number > 9999) throw new ArgumentOutOfRangeException(nameof(number));
int count = number < 1000 ? 3 : 4;
int constant = count == 3 ? 495 : 6174;
int[] digits = new int[count];
while (number != constant)
{
ExtractDigits(number, digits);
int ascending = AscendingValue(digits);
int descending = DescendingValue(digits);
number = descending - ascending;
Console.WriteLine($"{descending} - {ascending} = {number}");
}
}
void ExtractDigits(int from, int[] to)
{
for (int i = 0; i < to.Length; i++)
{
from = Math.DivRem(from, 10, out int rem);
to[i] = rem;
}
for (int i = 1; i < to.Length; i++) if (to[i - 1] != to[i]) return;
throw new ArgumentException();
}
int AscendingValue(int[] from) => from.OrderBy(x => x).Aggregate((x, y) => x * 10 + y);
int DescendingValue(int[] from) => from.OrderByDescending(x => x).Aggregate((x, y) => x * 10 + y);
#Задача #Решение #Полезно
Please open Telegram to view this post
VIEW IN TELEGRAM