Страницы

Поиск по вопросам

Показаны сообщения с ярлыком структуры-данных. Показать все сообщения
Показаны сообщения с ярлыком структуры-данных. Показать все сообщения

четверг, 13 февраля 2020 г.

Структура данных в задаче “Волчий остров”

#c #структуры_данных


Как бы Вы организовали данные в такой задаче? Программирую я на C, если это вдруг важно.
Волчий остров размером 20х20 заселен дикими кроликами, волками и волчицами. Имеется
по нескольку представителей каждого вида. Кролики довольно глупы: в каждый момент времени
они с одинаковой вероятностью 1/9 передвигаются в один из восьми соседних квадратов
(за исключением участков, ограниченных береговой линией) или просто сидят неподвижно.
Каждый кролик с вероятностью 0,2 превращается в двух кроликов. Каждая волчица передвигается
случайным образом, пока в одном из соседних восьми квадратов не окажется кролик, за
которым она охотится. Если волчица и кролик оказываются в одном квадрате, волчица съедает
кролика и получает одно очко. В противном случае она теряет 0,1 очка. Волки и волчицы
с нулевым количеством очков умирают.
В начальный момент времени все волки и волчицы имеют 1 очко. Волк ведет себя подобно
волчице до тех пор, пока в соседних квадратах не исчезнут все кролики; тогда, если
волчица находится в одном из восьми близлежащих квадратов, волк гонится за ней. Если
волк и волчица окажутся в одном квадрате и там нет кролика, которого нужно съесть,
они производят потомство случайного пола.
Запрограммировать предполагаемую экологическую модель и понаблюдать за изменением
популяции в течение некоторого периода времени.
Я рассматривал несколько вариантов: список зверей, при этом элемент "зверь" хранит
собственные координаты. Но тогда поиск "соседей" осложняется. Можно сделать массив
списков. В каждом таком списке только те звери, которые "живут" в соответствующем месте
карты. Уже лучше. Была даже мысль организовать данные так: в каждой ячейке массива
структура (одна, не список), а внутри число (количество зайцев) и два массива из 10
элементов каждый. В каждой ячейке массива - количество волков/волчиц, имеющих соответствующее
здоровье (0.1 очка, 0.2 очка и т.д.) Это идеальный вариант, но он предполагает, что
больше 1 очка у волка не может быть, а в условии задачи об этом прямо не говорится.
Хотя это логично, ведь нельзя же съесть тонну еды, чтобы потом не есть год.    


Ответы

Ответ 1



С точки зрения ООП (Java) в этой игре есть 5 классов: Игровая карта класс Environment - класс или статический или в виде Singleton Животное - абстрактный класс Animal Волк - класс Wolf extends Animal Волчица - класс Wolfess extends Wolf Кролик - класс Rabbit extends Animal При этом класс Animal содержит текущие координаты и имеет 3 абстрактных метода: doStep() - шаг, логика которого отличается у волков и кроликов doBirth() - рождение, пустой у волков, непустой у волчиц и кроликов doEat() - поедание - непустой у волков и волчиц и пустой у кроликов Класс Environment кроме игрового поля содержит в себе список животных - типа List, который добавляется/убавляется при отработке методов doBirth() и doEat() Собсно игровой ход: получаем объект Environment берем список животных и по каждому из них делаем doStep() Как то так.

Ответ 2



Я писал на С++, но общий принцип следующий. Объект Region - содержит в себе перечень животных. Объект Map - содержит в себе перечень регионов и их координаты. Объект MapControl - по-сути, объект, управляющий игрой. Имеет набор методов, каждый из которых на вход ждет ссылку на карту. Методы: вывести состояние карты и животных на экран, вывести статистику (в моем случае по мутациям и по количеству живых на карте), задать стартовое состояние карты (случайная расстановка зайцев и волков), MoveAll (это сдвиг всех монстров на карте) и ActAll (это выполнение всех действий всех монстров на карте). Объект Action с одним абстрактным методом Do, принимающим на вход координаты, животное и карту. Объект, который имеет право изменять состояние животных и карты. Объекты, наследники от Action, это 3 варианта движения для волков (с разной логикой), питание для волков, размножение для зайцев, размножение для волков, смерть для волков. Иерархия классов животных, как у Barmaley: Animal с двумя абстрактными методами, один возвозвращает Action движения животного, другой возвращает массив указателей на все Action's животного, которые должны быть выполнены на этапе ActAll. Объект Rabbit : public Animal с реализацией методов, возвращающих относящиеся к нему Actions, и аналогично два объекта Wolf и Wolfess. Поскольку я ввел понятие инициативы, и выполнение Move происходит в очередности ее роста (чем меньше инициатива, тем раньше сдвиг), при этом при размножении заяц имеет инициативу родителя +/-2, волк = (инициативу папы * 3 + инициативу мамы * 1)/4 +/- 4, волчица просто (инициативу папы * 3 + инициативу мамы * 1)/4. Таким образом, волчицы не мутируют, но наследуют мутацию отца. Кроме того, выживаемость волков сильно возросла, когда я добавил условие, что если волк сыт более чем на 6, то сначала он ищет волчицу, а потом еду. При стартовых 170ти зайцах и 20-20 волков, есть несколько этапов развития: популяция зайцев медленно растет к 600-700, популяция волков до 150-200 каждого пола. популяция зайцев начинает резко падать до 20-40, волки тоже переходят свой максимум и их в районе 100-150 каждого пола. популяция зайцев начинает потихоньку расти, до 70-80, волки к этому моменту вымирают. Так вот, ожидаемо, что инициатива зайцев будет все время увеличиваться. И к третьему этапу вместо стартовых средних 5,5, зайцы имеют ~9,5 в среднем. Но что удивительно - инициатива волков падает. И когда их остается несколько десятков, их средняя инициатива колеблется в районе 3-4. Я предполагаю, что высокая инициатива, хоть и хорошо сказывается на охоте, плохо сказывается на спаривании (инициатива самок должна быть маленькой), поэтому в итоге инициатива лучшего волка бывает ниже инициативы худшего зайца. Кроме того, тот, кто последним пришел в регион (с более высокой инициативой), оказывается в конце списка животных, и если там уже несколько волков, ему может не хватить зайцев. Такие вот интересные результаты :)

пятница, 31 января 2020 г.

Быстрое клонирование

#java #алгоритм #память #структуры_данных


Задача, в целом, фундаментальная, но вопрос мой вопрос произрастает из истоков олимпиадного
программирования, где скорость - ключевой ресурс.
Чтобы не было проблем в четкости формулировки, прикреплю ссылку. Вот [1] (http://acm.timus.ru/problem.aspx?space=1&num=1992)!


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


Конечно, самым простым решением является просто полное клонирование (.clone(), в
java) с реализацией интерфейса Cloneable или схожего. После этого у нас будут две независимые
сущности, с которыми можно работать; критический недостаток - количество потребляемой
памяти и время, конечно же. Есть идеи как улучшить и оптимизировать обычную идею двух
стеков; или же что альтернативное?

Спасибо!  
    


Ответы

Ответ 1



Ещё одно популярное решение — иммутабельные структуры данных. При этом вам не нужно никогда глубокое копирование, вы можете повторно использовать общие части, т. к. они не изменяются. При создании нового состояния вы копируете ссылки на все неизменившиеся части старого в объект, представляющий собой новое состояние (это быстро и не требует много памяти). Изменившиеся части создаёте заново (или если у вас есть эта информация, используете её, раз она иммутабельна). Новое состояние храните в списке «версий» состояния. Дополнительное чтение по теме: Immutability in C# Part Two: A Simple Immutable Stack (да, это на C#, но принципы те же).

Ответ 2



Обращаю внимание, что в запросах на вывод надо назвать только последнюю технологию. Там нужен указатель на предка в дереве. Даже само дерево не нужно. Каждый элемент имеет значение и ссылку на такой же. Массив клонов содержит такие элементы. Массив деков содержит последовательность откатов для клона. Вроде на этом всё. Любая операция выполняется за O(1).

Ответ 3



Вариант 1. (Copy-on-Write) Создаём копию в виде ссылки. Когда изменится оригинал либо копия, выполняем глубокое копирование. Если ничего не меняется, память почти не расходуется, максимум что нам надо - несколько байт для счётчика. Вариант 2 А вот теперь решение посложнее. История изменений объекта фактически есть набор состояний объекта. Эти состояния можно упорядочить. Создаём массив, в который помещаем состояние объекта после изменения. С каждым объектом будет связан вектор ссылок на состояния. Вектор ссылок в общем случае требует меньше места, чем набор состояний. Вектор копируем при копировании объекта. Ещё можно гибридизировать оба подхода, копируя вектор ссылок только при изменении объектов.

среда, 29 января 2020 г.

задача: возможно ли продолжить фрагмент в обе стороны?

#алгоритм #структуры_данных


Задача  из области  "алгоритмы и структуры данных". 

Дан фрагмент последовательности скобок, состоящей из символов 

 (){}[]


Требуется определить, возможно ли продолжить фрагмент в обе стороны, получив корректную
последовательность.
Если возможно - выведите минимальную корректную последовательность, иначе - напечатайте
"IMPOSSIBLE".
Максимальная длина строки 10^6 символов.

Sample Input 1:

}[[([{[]}


Sample Output 1:

{}[[([{[]}])]]


Sample Input 2:

{][[[[{}[] 


IMPOSSIBLE 

Идеи: 

Есть стандартный алгоритм проверки корректности расстановки скобок, реализуемый через
стэк. Суть в следудующем:


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


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

Для этого пытался заводить счетчики на каждую скобку и ее направление и держать 
противоположную скобку в деке и в нужный момент извлекать из дека. Но пока все покрывается
ифами и алгоритм все равно правильно не работает
    


Ответы

Ответ 1



Привет со Степика! Держи код, прошедший все тесты в том уроке: Pastebin. В кратце об алгоритме: открывающие скобки заносим в стек встречая закрывающие скобки проверяем на соответствие последней в стеке соответствует -> убираем из стека, иначе -> см.пункт 3 стек пуст? -> выводим слева, иначе -> IMPOSIBLE если после полного прохода последовательности стек не пуст, дополняем справа class Stack: def __init__(self): self.items = [] def isEmpty(self): return self.items == [] def push(self, item): self.items.append(item) def pop(self): return self.items.pop() posible = True cin = input() a=Stack() s=Stack() for i in cin: if ( i == '(' or i == '[' or i == '{' ): s.push( i ) if ( i == ')' or i == ']' or i == '}'): if s.isEmpty(): a.push(i) else: temp = s.pop() if (i == ')' and temp != '(') or (i == ']' and temp != '[') or (i == '}' and temp != '{'): posible = False break if posible: while not a.isEmpty(): temp = a.pop() if temp == ']': temp = '[' if temp == ')': temp = '(' if temp == '}': temp = '{' print(temp, end = '') print(cin, end = '') while not s.isEmpty(): temp = s.pop() if temp == '[': temp = ']' if temp == '(': temp = ')' if temp == '{': temp = '}' print(temp, end = '') else: print('IMPOSSIBLE')

Ответ 2



При реализации алгоритма проверки корректности строк можно встретиться со следующими ситуациями (кроме варианта когда каждой твари по паре): Прошли всю строку - в стеке остались открывающие скобки; Дошли до закрывающей скобки - стек пустой; Дошли до закрывающей скобки - на вершине стека другая скобка. В первом случае можно дополнить вашу строку справа чтобы принудительно закрыть скобки. Во втором случае можно дополнить скобками слева чтобы принудительно открыть их. Чтобы исправить третий случай, придется вставлять скобки в середину строки. Дополнено Допустим A, B, C и D пустые или сбалансированные выражения. Если мы имеем выражение A(B в стеке останется открывающая скобка и ее можно закрыть, дополнив строку закрывающей скобкой. Если мы имеем выражение A(B[C, то мы можем аналогично сделать сбалансированным выражение B[C и привести выражение A(B[C к A(B. Таким образом можно сбалансировать любое аналогичное выражение; Если мы имеем выражение A)B в ходе проверки будет попытка закрытия неоткрытой скобки (в стеке открытых скобок не будет). Исправить выражение можно дополнив выражение слева открывающей скобкой. Аналогично балансируется выражение A)B]C и имеющие больше закрытых; Если имеет место выражение вида A(B[C)D или A(B]C)D то при получении закрывающей скобки на вершине стека будет обнаружена скобка, которая не соответствует открывающей. Исправить эту ситуацию можно только в пределах внешних скобок, соответственно невозможно исправить такое выражение дополнением скобок слева или справа.

Ответ 3



А в чём проблема-то? Идём слева направо. Если после открывающей скобки следует чужая закрывающая, то IMPOSSIBLE. И всё. Особенности программы: Для удобства обработки вместо символьных строк работаем с числовыми массивами. Открывающие скобки имеют номера 0-2. закрывающие 3-5. Открывающей скобке $i соответствует закрывающая скобка $i+3. Перевод в обе стороны обеспечивает функция str_arr_exchange(). Выходной массив инициализируется входным. Открывающие скобки сразу записываются в стек. Закрывающей скобке должна соответствовать последняя скобка в стеке либо пустой стек (иначе IMPOSSIBLE). Во втором случае выходной массив дополняется парной скобкой. В обоих случаях скобки удаляются из входного массива и стека. Если входной массив закончился, а в стеке остались открывающие скобки, то в выходной массив следует дописать парные им скобки. Программа на PHP использует рекурсивный вызов функции bracket_close() : $input1 = "}[[([{[]}"; $input2 = "{][[[[{}[]"; function str_arr_exchange($input){ $arr_brack = ["(","{","[",")","}","]"]; if(is_string($input)){ for ($output = [], $i = strlen($input)-1; $i>=0; $i--){ array_push($output, array_search($input[$i], $arr_brack)); } } else { $output = ""; while(!is_null($brack = array_pop($input))){ $output .= $arr_brack[$brack]; } } return $output; } function bracket_close(&$arr_input, &$arr_output, &$stack = []){ if($arr_output === FALSE) return; if(empty($arr_input)){ while(count($stack)){ // в стеке только открывающие скобки array_unshift($arr_output, array_pop($stack)+3); // конец строки - в начале массива } return; } if(empty($stack)){ // стек пуст $brack = array_pop($arr_input); if($brack>2){ // скобка закрывающая array_push($arr_output, $brack-3); // начало строки - в конце массива } else { array_push($stack, $brack); } } else { // стек непуст $old_brack = array_pop($stack); $new_brack = array_pop($arr_input); if($new_brack>2){ // скобка закрывающая if($old_brack == $new_brack - 3){ // скобки сомкнулись } else { // скобки конфликтуют $arr_output = FALSE; } } else { // скобка открывающая array_push($stack, $old_brack, $new_brack); } } bracket_close($arr_input, $arr_output, $stack); } function brackets_close($input, $num){ print("
Input$num = $input"); $arr_input = str_arr_exchange($input); $arr_output = $arr_input; bracket_close($arr_input, $arr_output); if ($arr_output === FALSE) { print("
IMPOSSIBLE
"); } else { $output = str_arr_exchange($arr_output); print("
Output$num = $output
"); } } brackets_close($input1, 1); brackets_close($input2, 2); Результаты: Input1 = }[[([{[]} Output1 = {}[[([{[]}])]] Input2 = {][[[[{}[] IMPOSSIBLE Уверенность в корректности алгоритма обеспечивается следующими соображениями: 1. Закрывающие скобки обрабатываются на месте, анализ ситуаций исчерпывающий. 2. Открывающие скобки всегда упорядочены.

среда, 22 января 2020 г.

Как выстроить структуру БД

#база_данных #структуры_данных


Добрый день, есть задача разработать БД таким образом, чтобы можно было определить
расстояние между двумя станциями. Но проблема в том, что все тарифное расстояние РЖД
находится на бумаге так сказать, либо в электронном виде, вот пример(http://lawrussia.ru/2001_legislation_of_russia_documents/russian_laws_1547.htm).
В принципе, я немного имею идею, как сделать алгоритм расчета от станции А до станции
Б, ноя  не могу никак придумать, как лучше организовать структуру БД. Есть Страна,
Есть дорога, есть участок, на это участке набор остановочных пунктов, вот как можно
лучше всего сделать это, ведь не буду я для каждого узла делать отдельные таблицы,
может кто понимает в этом, подскажите плиз. Пока идея, все станции запихнуть в одну
таблицу, и сделать ссылки на узлы, узлы на дороги, а дороги на страны, но это как-то
не очень получается.

Я в этом не особо силен, поэтому мог некорректно вопрос составить. 
    


Ответы

Ответ 1



Для ответа на этот вопрос необходимо вспомнить немого теории. Для поиска Вам идеально подходит структура данных - матрица растояний. Фактически, это квадартная матрица, в которой по осях находятся идексы станций, а значения по этому индексу и есть искомое растояние. Это именно та структура к которой нужно стремится при работе в программном коде. Но у неё есть небостатки. Она обладает высокой избыточностью. Фактически вы дублируете растояние из узла А в узел Б и наоборот т.е. Вам это придётся дублировать. Кроме того, добавление новых станций будет требовать изменения структуры БД, для расширения на добавляемые станции. Если посмотреть внимательно - то в этой задаче данные о растояниях будут сильно "прорежены". Т.к. почти у всех станций растояний м.б. только два - к предыдущей и последующей станции. И только у узлов их будет больше. Наиболее подходящим в этом случае будет хранить информацию о растояних отдельно от станций. CREATE TABLE STATION ( STATION_ID INT PRIMARY KEY ); CREATE TABLE STATION_DISTANCE ( STATION1_ID INT, STATION2_ID INT, DISTANCE NUMERIC NOT NULL, PRIMARY KEY (STATION1_ID, DISTANCE, STATION2_ID), FOREIGN KEY (STATION1_ID) REFERENCES STATION (STATION_ID), FOREIGN KEY (STATION2_ID) REFERENCES STATION (STATION_ID) ); Такая структура БД предоставит достаточно удобное хранение записи о растоянии, но совершить поиск оптимального пути на уровне базы данных при указанных условиях, не предоставляется возможным. Поиск в этом случае можно будет совершить двумя способами: Способ с предварительной подготовкой и подниманием всей информации о путях из БД. Способ требующий уточняющих запросов. Первый - потребует подготовки которая заключается в постороении из этой таблицы матрицы растояний к примеру при помощи алгоритма Флойда - Уоршелла. И хотя этот алгоритм достаточно тяжёлый его результат, можно сохранить сериализовав матрицу растояний в файл и далее работать с ним, при этом его модификацию потребуется выполнять только при изменении структуры ЖД, что не так и часто. При таком подходе можно быстро находить нужное растояние по матрице растояний. Второй - базируется на алгоритме Дейкстры для разреженных графов. У Вас есть возможность оптимизировать решение отталкиваясь от информации является ли станция узловой, и относится ли она к одной и той-же линии. При этом критерием сложности я бы назвал количество обращений к базе данных. Надеюсь здесь, в нескольких строках, мне удалось немного приблизить Вас к нужному варианту ответа по этой достаточно сложной задаче.

Ответ 2



Возможно, я не совсем правильно понял задачу, но по идее, должна быть БД примерно такого вида: В таблице "Дистанция" - расстояние между 2-мя соседними узлами. В таблице "дороги" будет информации о движении по узлам. Так как от станции А до станции Б можно проложить путь несколькими способами, выходит, что дорога должна включать все узлы, даже те, где нет остановки. Там, где нет остановки, поле "код_станции" будет пустым. Примерно такая получится заполненная таблица с 2-мя дорогами: Теперь, при выводе дороги можно получить все необходимые данные: Порядок движения. 1. Узлы (не для отображения) 2. Станции (то есть остановки - для отображения) 3. Расстояние (либо вычислять уже при выводе дороги в программу, для каждой строки выполнив запрос к табл. "дистанция", либо добавить в табл. "дороги" поле "код_дистанции" и заполнять его при добавлении данных в таблицу "дороги", если есть такая возможность. Также можно добавить триггеры, чтобы автоматизировать этот процесс). 4. При необходимости дополнительным запросом получить страны (из таблицы "Узлы"). Возможно, подойдет такое решение. Наверное, что-то можно пересмотреть и улучшить, писал быстро. Например, в таблице "дороги" вроде как глупо указывать узел, если указан код_станции, дублирование получается. Можно убрать вообще поле код_узла, оставить только код_станции и добавить флаг "остановка". UPD: обратил внимание, что все-таки неверно понял насчет узла. Если узел - это некий объект для группировки удаленных станций, тогда в схеме нужно "узлы" и "станции" поменять местами. Таблица "дистанция" будет хранить расстояния между станциями. А в таблице дороги убрать поле "узел" и добавить флаг "остановка".

Ответ 3



Можно сделать всё связанными иерархическими таблицами: страна содержит список таблиц дорог, дорога - список таблиц узлов и т.п. Но это, на мой взгляд, неудобно. Я бы свёл всё в одну таблицу (возможно, две, если будет нужна доп. информация), где будут и страны, и дороги, и узлы, и станции. Введите идентификатор, однозначно определяющий тип (пример: 1 - страна, 2 - дорога и т.п.), создайте референсы, указывающие на родителя (станция Толмачёвка имеет родителя за номером 81, а это - Измайловский узел и т.п.). Расстояния тоже можно вводить по-разному. Логичнее, на мой взгляд, в километрах от начала узла. Так и рассчитывать расстояния между станциями будет проще. Например, так: CREATE TABLE `new_table` ( `id` INT NOT NULL COMMENT 'уникальный ID', `ob_type` INT NOT NULL COMMENT 'тип объекта: 1 - страна, 2 -узел, 3 -дорога, 4 - станция', `parent_id1` INT NULL COMMENT 'ссылка на первого родителя - номер его ID', `parent_id2` INT NULL COMMENT 'ссылка на второго родителя - номер его ID', `parent_id3` INT NULL COMMENT 'ссылка на третьего родителя - номер его ID', `name` VARCHAR(90) NULL COMMENT 'Имя объекта', `distance` FLOAT NULL COMMENT 'расстояние от начала узла (только для станций)', `description` TEXT(2000) NULL COMMENT 'Описание', PRIMARY KEY (`id`) COMMENT '', UNIQUE INDEX `id_UNIQUE` (`id` ASC) COMMENT 'Комментарий');

Ответ 4



Надо отталкиваться от сущностей. Имеются: Станции Граф описывающий как эти станции соединены друг с другом (то есть станция по сути вершина графа) Есть маршрут который идет по этому графу от узла к узлу (от вершины к вершине) - длина маршрута и есть искомая величина. Итого получаются 3 таблицы, 1 и 3 самоочевидны. Есть сложность со 2-й таблицей - как описать граф. Ладно бы граф был в виде дерева, но в реальности все немного сложнее, поскольку это граф с кольцами, что-то типа такого: Я не совсем глубокий специалист в деле DDL/SQL, но я бы реализовал это через отношение один-ко многим - всегда есть 1 вершина, которая соединена с несколькими вершинами. Как то так.

пятница, 10 января 2020 г.

Структура таблицы в БД (денормализация)

#структуры_данных #база_данных


В рамках базы данных (в данном случае - PostgreSQL, но это не суть) есть несколько
небольших таблиц, которые содержат практически неизменяемые, но очень часто используемые
данные:

группы страниц (pagegroups), 
страницы (pages), 
шаблоны (templates),
позиции на страницах (positions),
модули (modules), 
группы пользователей (usergroups).

В каждой из таблиц есть поля ID, название, алиас и порядок показа. Между данными
есть связи: 

группы пользователей имеют разные права доступа, 
модули могут встречаются на разных позициях разных страниц,
каждой странице может быть задан свой шаблон,
шаблон состоит из позиций (header, sidebar_left, canvas, sidebar_right, footer),
которые могут отображаться или нет и т.п. (список связей должет остаться открытым)

Хочу свести все данные в одну таблицу, где представить данные в виде "ключ/значение".
Сломал всю голову. Пример базы (PostgreSQL):
CREATE TABLE core (
  id serial primary key,
  level int,
  group int,
  parent int references core,
  key text,
  value text
);

Фрагмент данных:
INSERT INTO core (id, level, group, parent, key, value) VALUES
(1, 0, 1, 0, 'type', 'coreelements'),
(2, 0, 1, 0, 'name', 'Компоненты системы'),
(3, 1, 2, 1, 'type', 'page'),
(4, 1, 2, 1, 'name', 'Страницы'),
(5, 1, 2, 1, 'order', ''),
(6, 1, 3, 1, 'type', 'template'),
(7, 1, 3, 1, 'name', 'Шаблоны'),
(8, 1, 3, 1, 'order', ''),
(9, 1, 4, 1, 'type', 'position'),
(10, 1, 4, 1, 'name', 'Позиции'),
(11, 1, 4, 1, 'order', '18,19,20,21,22'),
(12, 1, 5, 1, 'type', 'module'),
(13, 1, 5, 1, 'name', 'Модули'),
(14, 1, 5, 1, 'order', ''),
(15, 2, 18, 4, 'name', 'Шапка'),
(16, 2, 18, 4, 'alias', 'header'),
(17, 2, 19, 4, 'name', 'Рабочая зона'),
(18, 2, 19, 4, 'alias', 'canvas'),
(19, 2, 20, 4, 'name', 'Левый сайдбар'),
(20, 2, 20, 4, 'alias', 'sidebar_left'),
(21, 2, 21, 4, 'name', 'Правый сайдбар'),
(22, 2, 21, 4, 'alias', 'sidebar_right'),
(23, 2, 22, 4, 'name', 'Подвал'),
(24, 2, 22, 4, 'alias', 'footer');

Помимо общего вопроса о структуре таблицы смущает меня вот что:

В необходимости поля level не уверен, убрать?
Поскольку родитель и зависимые записи имеют по несколько пар "ключ/значение", как
организовать связь? по ID, по group или третьим способом? Иными словами, что должно
быть в поле parent?
Как связать, скажем, страницы, модули и шаблоны?

Пример к последнему вопросу:
Страница 'settings', в header имеет модуль 6, в canvas - модули 1, 3 и 2, в остальных
- ничего.

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


Ответы

Ответ 1



Нафиг такую структуру. Разным сущностям - разные таблицы! Что за извращенное стремление впихнуть все в одно? Даже если сейчас Вы чудом это все слепите так, чтобы работало, в будущем будут большие проблемы с масштабированием и изменением каждой сущности.

Ответ 2



Базовый элемент таблицы - узел, к которому привязаны как его свойства, так и узлы-потомки. CREATE TABLE core ( id serial primary key, parent int references core, node bool, key text, value text ); Два ключа (id, parent) используются для связи данных между собой; Булево поле (node) используется для отличия типов данных (собственно узлов и свойств). TRUE, если узел, FALSE, если свойства. key и value - собственно свойства. Пример данных: INSERT INTO core (id, parent, node, key, value) VALUES (3, NULL, TRUE, NULL, NULL), -- узел-родитель (4, 3, FALSE, 'alias', 'page'), -- свойства узла-родителя (4-6) (5, 3, FALSE, 'name', 'Страницы'), (6, 3, FALSE, 'order', '90,96,101,106'), ------ (90, 3, TRUE, NULL, NULL), -- узел-потомок (91, 90, FALSE, 'name', 'Настройки системы'), -- свойства узла-потомка (91-94) (92, 90, FALSE, 'alias', 'settings'), (93, 90, FALSE, 'template', '29'), -- связь с шаблоном (94, 90, FALSE, 'module', '54,62,58'); -- очередность модулей на странице Поле node можно убрать. Дело в том, что key/value для узлов равны NULL (что соответствует TRUE в поле node), тогда как NOT NULL в этих полях == FALSE.

четверг, 9 января 2020 г.

Хеши целых чисел в Python

#python #криптография #хеширование #структуры_данных


Добрый день.

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

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

В чём же дело? Я что-то неправильно понимаю о концепции хорошей хеш-функции? Или
в питоновских словарях и множествах не используется напрямую результат функции hash(),
а как-то дополнительно обрабатывается?
    


Ответы

Ответ 1



Встроенная хеш-функция имеет совсем другие задачи, не связанные с криптографией. Она используется для быстрого и удобного сравнения ключей словарей. Hash values are integers. They are used to quickly compare dictionary keys during a dictionary lookup. Numeric values that compare equal have the same hash value (even if they are of different types, as is the case for 1 and 1.0). По поводу идущих подряд ключей: In [37]: hash('aaaa') Out[37]: 5927745366728125705 In [38]: hash('aaab') Out[38]: 3762861188151674483 In [39]: hash('aaac') Out[39]: -5197229166136799781 Для "криптографических" целей стоит обратить внимание на модуль hashlib: In [35]: hashlib.sha512(b'aaa').hexdigest() Out[35]: 'd6f644b19812e97b5d871658d6d3400ecd4787faeb9b8990c1e7608288664be77257104a58d033bcf1a0e0945ff06468ebe53e2dff36e248424c7273117dac09' In [36]: hashlib.sha512(b'123').hexdigest() Out[36]: '3c9909afec25354d551dae21590bb26e38d53f2173b8d3dc3eee4c047e7ab1c1eb8b85103e3be7ba613b31bb5c9c36214dc9f14a42fd7a2fdb84856bca5c44c2' Пример из доки с использованием "соли": >>> import os >>> from hashlib import blake2b >>> msg = b'some message' >>> # Calculate the first hash with a random salt. >>> salt1 = os.urandom(blake2b.SALT_SIZE) >>> h1 = blake2b(salt=salt1) >>> h1.update(msg) >>> # Calculate the second hash with a different random salt. >>> salt2 = os.urandom(blake2b.SALT_SIZE) >>> h2 = blake2b(salt=salt2) >>> h2.update(msg) >>> # The digests are different. >>> h1.digest() != h2.digest() True

“Структура данных «Массив» имеет фиксированный размер”. Это всегда так?

#массивы #структуры_данных #динамические_массивы


В статье про структуру данных связанный список, автор пишет про недостатки массива:


  1) The size of the arrays is fixed: So we must know the upper limit on
  the number of elements in advance...


и тут же во втором пункте, как мне кажется, он это опровергает


  2) Inserting a new element in an array of elements is expensive...


Т.е. сначала говорится, что массив – это фиксированный набор данных, а затем говорится
о добавлении. Как это понимать или автор имеет ввиду уже разновидность массива – динамический
массив?
    


Ответы

Ответ 1



Важно различать элемент и место под элемент. Массив не всегда весь заполнен, но даже незаполненная часть занимает память1. Фиксировано в массиве число мест. Элементов в массиве ненулевой длины может не быть. Из-за особенностей хранимого типа может быть невозможно иметь в массиве ячейку без значения, но это лишь техническая деталь — если значение не имеет смысла, для алгоритма его всё равно что нет. И следить за такой ситуацией можно, храня где-то неподалёку число "полезных элементов", если вы заполняете массив последовательно от начала (что бывает не всегда). Во втором пункте говорится о дороговизне вставки элемента в произвольное место с сохранением существующих элементов массива и порядка их следования2. Чтобы её осуществить когда место занято, нужно все элементы, начиная с запрашиваемого места и вверх по индексам до конца заполненной части массива, переместить на одно место вперёд. Худший случай — вставка в начало, когда необходимо сдвинуть вперёд всю заполненную часть массива. Для сравнения, для вставки элемента в связный список (с тем же требованием сохранить порядок) необходимо лишь создать запись списка с элементом и прописать её в указывающего на неё соседа (или двух, если список двусвязаный). Такая богатая на переходы по ссылкам структура заметно замедляет обход, но в некоторых узких нишах она удобна. Нет, динамические массивы тут ни при чём. 1 если не используется трюков вроде разрежения, но с ними это уже не тот примитивный массив, о котором речь 2 в пределах двух частей, на которые вставляемный элемент этот массив разделяет

понедельник, 6 января 2020 г.

в Java hashTable.hashCode() всегда возвращает 0

#java #коллекции #структуры_данных #hashcode


Есть структура(класс).

    public class Node {
        private static String separator = "\\";
        private Map fileMap = new HashMap<>();
        private Node parent;
        private String absolutePath;
        private Integer hash;
        public void addFile(File file) {

        fileMap.put(file.getAbsolutePath(), new Node(file.getAbsolutePath(), this));
            hash = hashCode();
        }

        public Node(String absolutePath, Node parent) {
            this.absolutePath = absolutePath;
            this.parent = parent;
        }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Node)) return false;

        Node node = (Node) o;

        if (getFileMap() != null ? !getFileMap().equals(node.getFileMap()) : node.getFileMap()
!= null) return false;
        return getAbsolutePath().equals(node.getAbsolutePath());
    }

    @Override
    public int hashCode() {
        boolean eq = getFileMap() != null;
        int result;
        if (eq){
            result = getFileMap().hashCode();
            result += 0;
        } else
            result = 0;
        result = 31 * result + getAbsolutePath().hashCode();
        return result;
    }


Данная структура используется для представление иерархии файловой системы. То есть
node может содержать сам файл, так и hashTable (fileMap) с вложенными файлами, если
это директория, также файлы в hashTable (fileMap) могут быть файлами или директориями.

Специально переопределил метод hashCode() через if для наглядности.
Строка result = getFileMap().hashCode(); всегда возвращает 0, сколько бы разных сущностей
в ней не было.
В итоге hash считается только от параметра "absolutePath". Что меня не устраивает,
ведь мне нужно, чтобы хэш был разным в зависимости от вложенных файлов. 
Иначе получается ситуация в которой, два дерева представляющих файловую систему,
например:

testdir/innerdir/1/2

testdir/outdir

и содержащие одинаковую папку в root - "testdir" равны, т.к. хэш вложенных структур
в hashTable не учитывается.
Может кто-нибудь сказать Почему так происходит?
    


Ответы

Ответ 1



Если посмотреть в исходный код HashMap (точнее его родителя AbstractMap),то можно увидеть как реализовано метод hashCode().Он суммирует хеш коды всех элементов: public int hashCode(){ int h=0; Iterator>i=entrySet().iterator(); while(i.hasNext()) h+=i.next().hashCode(); return h; } Далее смотрим как реализуют этот метод элементы HashMap - HashMapEntry: public final int hashCode() { return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); } Здесь используется исключающее ИЛИ для хешкодов ключа и значения. То есть, если хешкоды ключа и значение будут равны, то метод вернёт 0. В итоге получаем, что когда getFileMap() == null, то для этого Node хешкод берётся от getAbsolutePath(). Этот же getAbsolutePath() является ключём в мапе, получается, что хешкоды ключа и значения равны, отсюда и возвращается 0.

вторник, 31 декабря 2019 г.

Теория по односвязному списку

#алгоритм #структуры_данных


Здравствуйте. 

Довольно часто на собеседованиях или же в тестовых заданиях (для прохождения на стажировку,
например) задается вопрос об односвязных списках.

Например, дают задачу, в которой необходимо развернуть односвязный список за О(n)
времени. Так вот, как это сделать? Можно ли просто переопределить ссылки? Т.е. сделать
за 1 итерацию так, чтобы не A->B, а A<-B ? 

Можете, пожалуйста, привести пример? 
Заранее благодарю
    


Ответы

Ответ 1



Первая ссылка из гугла. void reverse(OneWayList list){ OneWayList.Node node = list.head; OneWayList.Node previous = null; while(node != null){ //next item OneWayList.Node tmp = node.next; //swap items node.next = previous; previous = node; list.head = node; //next item node = tmp; } } Отсюда

Ответ 2



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

Можно ли это назвать хеш-таблицей. Если нет то почему ?

#data_structures #структуры_данных #алгоритм #хеширование #cpp


Почитал кормена и написал хеш-таблицу на основе сцепления элементов. Можно ли это
назвать хеш-таблицей и если нет то почему, какие ошибки есть логические ?
node.hpp
#ifndef NODE_HPP
#define NODE_HPP

template 
class list;

template 
class node
{
    private:
        friend class list;

    private:
        node* m_next;
        node* m_prev;
        T m_data;

    public:
        node() :  m_next(0)
                , m_prev(0)
                , m_data(0) {}

        explicit node(T d) :  m_next(0)
                            , m_prev(0)
                            , m_data(d) {}
        T get_data()
        {
            return m_data;
        }
};

#endif // NODE_HPP

list.hpp
#ifndef LIST_HPP
#define LIST_HPP

#include 
#include 

#include "node.hpp"

template 
class list
{
    private:
        node* m_head;
        node* m_tail;
        unsigned m_size;

    public:
        list() :   m_size(0) 
                 , m_head(0)
                 , m_tail(0){}

        node* get_new_node(T d) const;
        node* find(T data) const;
        void insert_at_front(T data);
        void insert_at_back(T data);
        void delete_at_front();
        void delete_at_back();
        bool is_empty() const;
        void print() const;
        node* get_begin() const
        {
            return m_head;
        }
        node* get_end() const
        {
            return m_tail;
        }
        unsigned get_size() const
        {
            return m_size;
        }
};

template 
node* list::get_new_node(T data) const 
{
    node* n = new node(data);
    assert(n != 0);
    return n;
}

template 
bool list::is_empty() const
{
    if(m_head == 0)
    {
        return true;
    }
    return false;
}

template 
void list::print() const 
{
    if(is_empty())
    {
        return;
    }
    node* t = m_head;
    while(t != 0)
    {
        std::cout << t->m_data << " ";
        t = t->m_next;
    }
}

template 
node* list::find(T data) const
{
    if(is_empty())
    {
        return 0;
    }
    node* t = m_head;
    while(t != 0 && t->m_data != data)
    {
        t = t->m_next;
    }
    return t;
}

template 
void list::insert_at_front(T data)
{
    node* n = get_new_node(data);
    if(m_head == 0)
    {
        m_head = m_tail = n;
        n->m_next = n->m_prev = 0;
        ++m_size;
    }
    else
    {
        n->m_next = m_head;
        if(m_head != 0)
        {
            m_head->m_prev = n;
        }
        m_head = n;
        n->m_prev = 0;
        ++m_size;
    }
}

template 
void list::insert_at_back(T data)
{
    node* n = get_new_node(data);
    if(m_tail == 0)
    {
        m_head = m_tail = n;
        n->m_next = n->m_prev = 0;
        ++m_size;
    }
    else
    {
        m_tail->m_next = n;
        n->m_prev = m_tail;
        m_tail = n;
        ++m_size;
    }

}

template 
void list::delete_at_front()
{
    if(is_empty())
    {
        return;
    }
    else
    {
        node* t = m_head->m_next;
        t->m_prev = 0;
        delete m_head;
        m_head = t;
        --m_size;
    }
}

template 
void list::delete_at_back()
{
    if(is_empty())
    {
        return;
    }
    else
    {
        node* t = m_tail->m_prev;
        t->m_next = 0;
        delete m_tail;
        m_tail = t;
        --m_size;
    }
}

#endif // LIST_HPP

hash_table.hpp
#ifndef HASH_TABLE_HPP
#define HASH_TABLE_HPP

#include "list.hpp"

template 
class hash_table
{
    private:
        list** m_table;
        unsigned m_size;

    private:
        int get_hash(int key);

    public:
        explicit hash_table(unsigned);
        T find(const T&, const T&); 
        void insert(const T&, unsigned);
        void remove(const T&);
};

template 
hash_table::hash_table(unsigned size)
{
    m_size = size;
    m_table = new list*[m_size];
    for(int i = 0; i < m_size; ++i)
    {
        m_table[i] = new list();
    }
}

template 
int hash_table::get_hash(int key)
{
    return (key % m_size);
}

template 
T hash_table::find(const T& d, const T& i)
{
    int h = get_hash(i);
    node* n = m_table[h]->find(d);
    assert(n != 0);
    return n->get_data();
}

template 
void hash_table::insert(const T& d, unsigned key)
{
    unsigned h = get_hash(key);
    m_table[h]->insert_at_front(d);
}

#endif // HASH_TABLE_HPP

Поправил код find(...)
template 
T hash_table::find(const T& i)
{
    int h = get_hash(i);
    if(m_table[h]->get_begin() != 0)
    {
        return m_table[h]->get_begin()->get_data();
    }
    return 0;
}

но так получается что функция всегда возвращает только голову списка а если есть
коллизия то этот случай не учитывается ... ?    


Ответы

Ответ 1



Есть несколько замечаний по реализации: Непонятна сигнатура find(T, T). Почему не find(Key)? Непонятно ваше разделение для элементов хэш-таблицы. То есть, сигнатуры методов должны выглядеть как insert(Key, Value) / find(Key) / remove(Key), либо как insert(Value) / find(Value) / remove(Value) для случая, когда в хэш-таблице хранятся не пары ключ-значение, а сами значения. Других вариантов нет. Не предложена имплементация remove(T). Вместо траты времени на реализацию своего std::list, лучше бы уж написали метод удаления элементов из хэш-таблицы. Крайне странное решение, в котором вызов функции find() coredump'ится в случае отсутствующего в хэш-таблице значения. Вообще, предложенный код, за исключением последних пятнадцати строчек, не имеет к хэш-таблицам никакого отношения, а просто предлагает какой-то неочевидный способ реализации аналога std::list. Решение с наследованием node ← list, кстати, кажется очень странным. А так, ну да, обычная хэш-таблица с chaining'ом для резолвинга коллизий.

Ответ 2



Похоже, что да, это может быть хэш таблицей. Для начала читаем определение с википедии: Хеш-табли́ца — это структура данных, реализующая интерфейс ассоциативного массива, а именно, она позволяет хранить пары (ключ, значение) и выполнять три операции: операцию добавления новой пары, операцию поиска и операцию удаления пары по ключу. Все эти три операции я вижу. Правда было бы не плохо перегрузить operator [] (что бы реализовать интерфейс "массива"). Меня только смущает порядок параметров в функции вставки. Я думаю, ключ должен идти первым. Обязательно напишите тестовые примеры и проверьте, что бы Ваша таблица работала как нужно.

суббота, 21 декабря 2019 г.

Реальный пример использования LinkedList

#java #структуры_данных


В каких случаях можно использовать LinkedList, на собеседованиях спрашивают где можно
использовать, когда привожу примеру типа как очередь или стек, они говорят что для
таких случаев есть свои Queue и Stack
    


Ответы

Ответ 1



Каноническим ответом на такой вопрос является следующий: LinkedList используется если необходимо производить много операций вставки/удаления элементов в середине списка и мало операций доступа к элементу по индексу. Структура связного списка в таких операциях будет эффективнее массива, на котором основана реализация ArrayList, потому что при вставке элементов в середину ArrayList физически сдвигаются все последующие элементы. Но у LinkedList есть недостатки по сравнению с ArrayList: Потребление памяти на один элемент у LinkedList больше, чем у ArrayList (для каждого элемента хранятся ссылки на предыдущий и следующий элементы) Доступ к элементу по индексу медленнее (O(n) в худшем случае) А со следующими задачами обе реализации справляются одинаково: Поиск элемента по значению (в обеих реализациях O(n) в худшем случае) Итерация по элементам (сложность получения следующего элемента O(1) в обоих случаях) С учётом того что ArrayList при вставке сдвигает элементы массива достаточно быстрым нативным методом System.arraycopy() (документация утверждает, что сложность вставки n элементов в ArrayList будет O(n)), я бы рекомендовал в общем случае использовать только ArrayList, а к LinkedList обращаться разве что в академических целях (например, в бенчмарках).

пятница, 20 декабря 2019 г.

Redis, как кэш для SQL запросов в веб-приложении

#проектирование #веб_программирование #кэширование #структуры_данных #redis


Я впервые использую редис в веб-приложении. Изначально, цель была - кэшировать результаты
сложных запросов, которых было не очень много, это не так сложно, и я подумал, почему
бы не кэшировать всё? Ну или почти всё.

И так, я написал конструктор mysql-запросов, который всегда перед тем как выполнить
запрос к бд, принимает необязательный параметр - ключ, по которому хранятся данные
в redis. В итоге если такой ключ был передан, конструктор сначала посмотрит, есть ли
данные по ключу в редисе, и если есть - то он вернет данные из редиса и к мускулу обращаться
не будет, а если нет - то выполнит sql-запрос, вернет данные в контроллер и в соседнем
потоке запишет их в редис (используя тот самый, переданный ключ). С запросами на выборку
понятно, а вот если мы делаем insert/update, то мы удаляем из редиса все данные которые
попали под маску переданного ключа, и тогда при новой выборке кэш обновится.

Все что попадает в редис, в основном хранится в виде JSON. И, вот настал тот ужасный
момент, когда количество хранимых данных стало большим (а на проде, будет еще в over
9999 раз больше), и самое важное - много дублирующихся данных. Приведу простой пример.
Мы заходим в раздел articles, загружаются посты которые находятся на первой странице
(пусть их по умолчанию отображается по 50). В этот момент, контроллер сформировал ключ
articles:list:page_1 по которому будет искать данные в редисе. Дальше походили по страницам,
сформировали кэш для articles:list:page_2 articles:list:page_5 articles:list:page_19
articles:list:page_28 .... После чего, нажали на кнопку "Показать все статьи", тем
обратились по ключу articles:list:all - понятно, что тут хранятся все записи (и я думаю
- это ужасно!).  Потом мы открыли несколько статей articles:post:id1234 articles:post:id567
articles:post:id890. Вы представьте сколько уже есть дублирующих данных в редисе, а
если мы дадим пользователю выбирать кол-во выводимых записей, то тогда появятся такие
ключи: articles:list:page_1_55 articles:list:page_3_55 и тд (_55 записей на странице).
Это приведет к большой беде. Теперь дальше, если  в админке мы отредактируем какой-то
пост, а заголовок этого поста выводится в листинге, тогда мы удалим из кэша старые
данные так articles:post:id890 и очистим весь листинг articles:list:* - что есть тоже
очень плохо.

А что касается сложных запросов к бд, есть еще один случай, когда сам пост хранится
в одной таблице, комменты к нему в другой, лайки в третьей и еще 5 таких зависимостей.
Сейчас (без редиса) это вытягивается с помощью сложного запроса с JOIN'ами (допустим
их 7 шт.). Это все можно сохранить в редис, но тогда если кто-то лайкнет пост, нужно
инвалидировать кэш, и заново придется выполнять сложный запрос. Есть вариант разбить
это на 7 простых запросов и сохранить каждый в редисе, и тогда если кто-то оставит
коммент или лайкнет, то нужно будет только один простой запрос выполнить. Вот, как
в такой ситуации было бы правильно поступить? (не будем говорить о тестах, будем рассуждать
примерно, берем какие-то средние значения и среднестатичные случаи). 

Что в этом случае вы посоветуете делать? Как правильно реализовать хранение данных
в кэше? Возможно стоит пересмотреть способ хранения, может не хранить коллекции :list:
целиком, а извлекать всегда какие-то части (элементы)? Я вот не могу в этом разобраться.

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


Ответы

Ответ 1



Вы столкнулись с некоторыми вещами, которые не очень хорошо помещаются в тот мир, в котором вы работали раньше. В какой-то степени можно это назвать просто NoSQL, но это не совсем так, в любом случае могу поздравить с важным шагом в карьере. Дублирование данных Первое, о чем хотелось бы сказать: самое важное - много дублирующихся данных Дублирующиеся данные сами по себе нормальны. Нормализация базы данных SQL невольно учит нас тому, чтобы данные были в единичном экземпляре, но это, на самом деле, не аксиома. Дублирующиеся данные сложнее поддерживать, но само их наличие в целях улучшения работы приложения - это абсолютно нормально. Кроме этого, стоит вставить мою любимую ремарку про модель работы приложения: если сейчас у вас это де-факто pull-on-change - сформировать данные по запросу, то есть гораздо более интересная модель push-on-change - когда данные для запросов подготавливаются при добавлении, и в этом дублирование данных подразумевается само по себе. Представьте себе некую социальную сеть с лентой новостей. При стандартном подходе (и SQL-бэкенде) придется формировать гигантский SQL-запрос, который будет проверять необходимость показа каждой записи, наличие доступа к ней (мы же не хотим показывать приватные записи, верно?) и прочие атрибуты. Push-on-change же предлагает в этом случае в момент обновления записи в ленте вычислять, кому она должна быть показана, и формировать таблицу вида 'id пользователя | id записи | datetime', чтобы просто доставать из нее N первых записей, по которым уже формировать запрос по новостям. Данные в этом случае де-факто будут продублированы, но это необходимо, чтобы ускорить многократные read-операции за счет однократной write-операции. С переходом в хайлоад это становится особенно актуально, потому что один сервер не в состоянии хранить все данные и/или выдержать нагрузку, а поэтому необходимо разделять ответственность между серверами (что может поставить запрет на джойны, например). Однако, Отказ от подготовки ответов на все запросы геометрическая прогрессия, конечно, не позволяет подготовить заранее готовые ответы на все возможные запросы: Вы представьте сколько уже есть дублирующих данных в редисе, а если мы дадим пользователю выбирать кол-во выводимых записей, то тогда появятся такие ключи: articles:list:page_1_55 поэтому от вышеприведенного примера действительно стоит отказаться - у Redis нет столько оперативки (как и от :all - тут проблема не в оперативке, а в количестве данных, которые надо будет передать по сети и затем разобрать). Конкретно в вашем случае есть две вещи, которые я считаю необходимым отметить Текущий подход работает, де-факто, на кэширование конкретных запросов Redis используется как KeyValue-кэш Первое подразумевает тот самый рост данных в геометрической прогрессии со степенью, зависящей от количества параметров, по которым может проводиться выборка. Вам не обязательно получать из Redis уже готовый ответ - вы можете получать его либо по частям, либо бОльшим куском. В случае со страницами по 55 записей вы можете просто хранить "верхушку" таблицы в редисе: # пусть верхушка состоит из 500 записей from = 63 to = 63 + 55 if to < 500: top_entries = redis.get 'articles:top' # в редисе может не оказаться ничего или оказаться устаревший и слишком короткий список if top_entries && top_entries.length > to: return top_entries.slice from, to return article_repository.get_slice from, to В этом случае приложение знает, что у него (возможно) есть кусман на 500 записей в редисе, и запрос, скорее всего, проще обслужить оттуда, поэтому пытается сделать именно это. В случае со сложной иерархической структурой проще наоборот, разбить сущность на составляющие: article = redis.get 'article:' + id if not article: article = article_repository.get id if not article: throw new ResourceNotFoundException comments = redis.get 'comments:by-article:' + id if not comments: comments = comment_repository.get_by_article id likes = redis.get ' В этом случае вы делаете из сущностей некоторое подобие строительного материала, который не слишком сильно дублируется и позволяет собирать различные результаты на основе одних и тех же источников (например, поиск по статьям в любом случае пойдет через БД, но лайки для них можно вытащить из Redis без особых затрат по времени). Это не самый богатый арсенал, но он поможет избежать избыточного дублирования, которое может возникнуть, если иерархические сущности кэшируются целиком, и, заодно, уменьшит развесистость операции, необходимой для обновления одной атомарной сущности (т.е. не придется сбрасывать половину кэша из-за одного проставленного лайка). Закешировать всё Кроме всего вышеописанного, я бы относился немного более практично к идее закешировать всё. Сейчас вы пытаетесь уменьшить нагрузку на сервис за счет кэширования, но переносите в Redis буквально вообще все, но это вам не нужно. До десятой страницы новостей доберется один читатель из сотни - вам действительно нужно оптимизировать ее быстродействие? В том случае, если она срендерится за 100 мс, а не за 10, пользователь это не почувствует, и сервер тоже не почувствует, потому что основная нагрузка у него идет на получение первых записей. LazyLoad Все вышеописанное только вскользь говорило о том, как данные попадают в Redis - что в случае добавления записи необходимо пересчитать специально подготовленную выборку. Однако мы знаем, что записи в любом in-memory кэш-сервисе (будь то Redis, Memcache, Aerospike) вечны максимум вплоть до первой перезагрузки машины, и даже подготовленные выборки в этом случае умрут. В этом случае поможет механизм ленивой загрузки - если грубо, то он пытается получить данные из некоего источника данных А, и, если не находит, загружает их из источника Б, кладет в А, и возвращает. В программировании этот подход часто применяется для инициализации тяжелых объектов и вызовов из БД, которые могут быть не нужны: private heavy_object private settings get_heavy_object(): if not heavy_object: heavy_object = new HeavyObject return heavy_object get_settings(): if not settings: settings = read_settings_from_database() return settings В случае с кэшем хранилищем А является не переменная, а кэш, а хранилищем Б - БД: get_article(id): redis_id = 'article:' + id article = redis.get redis_id if not article: # не обнаружен в Redis - ищем в БД article = repository.get id if not article: # значит, он вообще не существует return null redis.put id, article # кладем в Redis для последующих вызовов return article Если вы будете применять эту парадигму везде, то приложению можно будет хоть живьем подменить Redis, оно все равно будет работать. Кроме того - для меня это самый важный пункт - в этом случае можно спокойно инвалидировать существующий кэш, не боясь за само приложение (но, конечно, стоит иметь в виду резко возрастающую нагрузку на БД). Инвалидация (и немного про CAP) Следующий вопрос, который у вас так же возникает - это инвалидация данных, т.е., когда они должны исчезнуть из кэша или обновиться. Самый напроломный вариант - это обновление данных in-place, т.е., как только обновилась сущность, обновились и все связи в кэше. Однако, это плохой вариант - точнее, он отличный, но его невероятно сложно реализовать, не забыв про что-то и не раздув код до невероятных размеров. В случае, если кэш условно-бесконечен, и какой-то тип записей не обновляется, то все пользователи будут видеть устаревшие данных и спрашивать у вас, почему на разных страницах у одной и той же статьи разные комментарии. Тут на помощь приходят две стратегии инвалидации, которые есть в Redis: LRU (Least Recently Used). При переполнении указанного в параметре max-memory размера Redis начнет удалять записи, которые не запрашивались дольше всех. Это гарантирует некоторую ротацию записей (при добавлении новых записей некоторые из старых будут удаляться, чтобы, при повторном запросе, быть обновленными из БД) TTL (Time To Live). Запись с указанным TTL может быть удалена по истечении этого TTL (может быть - в связи с проблемами в выполнении задачи не могу сказать, насколько вовремя это сделает Redis, но вряд ли задержка составит больше трех секунд). Именно это я и хочу предложить в качестве серебряной пули - делайте всем записям TTL в пределах 1-60 секунд, и устаревшие данные у вас гарантированно не продержатся дольше минуты, а благодаря lazy load воскреснут вновь. Резюмируя, проще всего задавать небольшое время жизни записям, и сильно не беспокоиться об инвалидации, пока продукт не вышел на поддержку. В этом случае у вас страдает целостность данных (consistency), но в большинстве случаев она на самом деле не является критичной, более того, существует применимая к распределенным системам теорема CAP (NB: обсуждаемое в этом ответе приложение - это не распределенная система), которая (если очень грубо) говорит о том, что невозможно одновременно поддерживать доступность и согласованность данных - это просто некоторое свойство нашей вселенной, и небольшая задержка в обновлении данных чаще всего не только остается незамеченной, но и не всегда может быть замеченной (если у вас есть задержка в обновлении выдачи статей, то пользователь не ожидает получить новую в тот же момент, когда она была написана - он не знает момента написания). Dogpile effect В связи с параграфом про TTL и lazyload нельзя пропустить так называемый эффект собачьей стаи. С момента запроса обнаружения пустоты в кэше первым клиентом до момента его обновления проходит некоторое время X, за которое могут прийти еще N клиентов. Приложение в этом случае добросовестно попытается реконструировать кэш еще N раз. Единого решения у этой проблемы нет, но вы можете либо ставить лок на время обновления кэша (но если у вас приложение стоит более, чем на одном сервере, то придется использовать сервис, поддерживающий распределенные блокировки - например, consul, etcd или даже реализация с помощью самого Redis - см. Redlock), либо просто считать, что в SLA приложения добавлена возможность работать без кэша вообще (если за кэшем у вас скрывается один запрос для одной записи в БД, то почему бы и нет). Поиск И, наконец, самое веселое. Как объединить сам поиск и подготовку данных для него? В общем случае - никак, ищите по базе, вы не сможете запихнуть все необходимое для запроса в KV-хранилище. Но если вас интересует, как это решается в серьезных случаях (имеется в виду поиск по базе данных, а не уровня яндекса/гугла, конечно), то есть специализированные решения, например, Lucene (и построенный на его основе ElasticSearch), Solr, Sphinx, YoctoDB. В общем случае поиск сводится к построению индексов из полей документа, поиску по этим индексам и агрегированию результата. Микросервисная архитектура Предложенные мной решения по разбиению на building blocks так или иначе приведут к появлению в приложении менеджеров каждой сущности, каждый из которых заведует отдельной сущностью. Хочется сказать, что это разбиение может продолжиться и вне приложения - приложение можно разбить на отдельные приложения, каждое из которых будет заниматься своим доменом. Это так же убьет всякую возможность джойнов, но, на самом деле, она и не нужна. Если пользователи у вас лежат в одном приложении на одном кластере, а статьи, в которых автор указан в виде идентификатор - в другом приложении на другом кластере, то вряд ли вам потребуется искать все статьи, в имени автора которого стоит "Андрей". Как обо всем позаботиться? Скорее всего, через весь ответ так и сквозят вопросы вроде "какой TTL ставить", "нужно ли кэшировать Х" и "какйо прирост производительности я получу". Ответ простой - это вообще никак не узнать, это выясняется только на практике. Деплойте приложение, анализируйте статистику, и через некоторое время вы просто поймете, где оно подтормаживает. Если оно не подтормаживает в месте, где вы забыли поставить кэш - возможно, там и нет смертельной необходимости в нем, и к этому месту нужно будет вернуться, если вообще не останется других задач? Просто пара слов про Redis Во-первых просто хотелось сказать, что кроме Redis есть еще сервисы, выполняющие те же функции (мой любимый - Aerospike). Redis довольно прост в использовании, но не умеет, например, шардиться, у него были проблемы с фрагментацией памяти (порог в 100 мб теоретически имел право забрать до 200 мб оперативки), он однопоточный и в мире NoSQL довольно похож на MySQL в мире баз данных. Тем не менее, с небольшим приложением по меркам интернет-гигантов он наверняка справится без особых проблем.

Ответ 2



MySQL предлагает собственный похожий механизм кэширования: query cache. Только тестирование объективно покажет, что эффективнее именно для вашего веб-приложения: работа без кэша, query cache или ваш велосипед с Redis.

Ответ 3



Может быть, перед тем как пытаться совместить SQL c не-SQL стоит сначала хорошо разобраться хотя бы в чём-то одном? Ваш случай не уникален и в SQL уже есть всё, что может понадобиться для кэширования сложных запросов. В MySQL для этого есть Materialized Views. В других базах данных это называется чуть по другому, но тоже ничего изобретать не приходится.

вторник, 17 декабря 2019 г.

Упорядоченный обход префиксного дерева

#алгоритм #структуры #дерево #структуры_данных


Существует префиксное дерево (нагруженное дерево, trie).


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

Обновление

Дерево никак не отсортировано. У каждого узла может быть бесконечно потомков. Думаю
хранить в каждом узле максимальное значение на этой ветви. И пытаться делать прицельный
обход.
    


Ответы

Ответ 1



Если вам нужно обходить дерево с максимального значения к минимальному я бы посоветовал использовать двоичную кучу (двоичное дерево), где значение в любой вершине всегда больше потомков. При этом построение кучи O(2n*log n), добавление элемента O(log n)

воскресенье, 8 декабря 2019 г.

Мертвы ли списки? C

#c #память #структуры_данных #data_structures


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

struct Node{
    void *data;
    struct Node *next;
}


Появились следующие вопросы:


Как затратней, realloc или моя реализация?
Почему создатель связного списка его не использует?
Как работает класс vector в C++?
Почему в C++ есть аналог malloc, calloc и free (new, new [], delete), но нет аналога
realloc?


Пожалуйста приведите как реализовываете вы, как лучше, почему.

UPD


Что затратней выделять память или копировать один участок в другой (уже выделенный),
на сколько затратней?

    


Ответы

Ответ 1



Архитектура современных компьютеров такова, что массивы (и любые структуры данных, основанные на них) оказываются в большинстве случаев быстрее, чем связные списки. Кеш Все дело в кеше. В одной линии кеша располагается сразу несколько элементов массива. Следовательно, при доступе к ним процессор не тратит много времени. А элементы связного списка с высокой долей вероятности не попадут в одну линию кеша. Следовательно, при обращении к очередному элементу будет промах и процессору придётся обращаться к основной памяти. Память Английское название - RAM (random access memory) - память со случайным доступом - давным-давно стало неправильным обозначением этого типа памяти. На самом деле она давно является блочной. Кстати, русское ОЗУ (оперативное запоминающее устройство), не несёт в своём определении такого недостатка. Современная память устроена так, что пишет/читает данные большими порциями. Подготовка к чтению и записи (латентность) занимает много времени. Зато потом данные выстреливаются очень быстро. Нетрудно понять, что последовательно размещённый массив будет читаться и писаться намного быстрее, чем связный список, элементы которого размещены в разных банках памяти.

Ответ 2



Как затратней, realloc или моя реализация? Для чего? С точки зрения памяти, локальности, поиска, сортировки...? Как будете выделять память - каждый раз по одному элементу или сразу раза в 2 больший буфер и поддерживать его емкость/заполненность? Вопрос поставлен некорректно... Почему создатель связного списка его не использует? А "создатель" - это кто? без этого непонятно, использует ли он его или нет... Update С тем же успехом, что считать Блоха создателем связанного списка, можно считать собравшего из разного железа в гараже велосипед слесаря дядю Васю - создателем велосипеда. Но дяде Васе просто некуда и незачем на нем ездить. Как и у Блоха, вероятно, нет задач, для которых связанный список более подходящ, чем массив... Как работает класс vector в C++? Поддерживая буфер, увеличиваемый по заполнении в некоторое количество раз - например, в два, так что обычно в нем есть достаточно пустого места для новых элементов. Когда заполняется - выделяется новый буфер удвоенного размера, куда перекопируется содержимое старого. Так что в результате амортизированное количество копирований - O(1), а все данные хранятся в одном блоке памяти. Почему в C++ есть аналог malloc, calloc и free (new, new[], delete), но нет аналога realloc? Ну, я бы не говорил, что это аналоги, уж тем более что new[] - аналог calloc. Эти "аналоги" вызывают конструкторы и деструкторы, а при realloc это, скажем так, задача, которую непросто решить для нетривиальных типов. Это совсем не так просто, как в С - перебросить память с одного места в другое...

Ответ 3



Как затратней, realloc или моя реализация? Выделение памяти- это дорогостоящий процесс. Именно по этому память любят выделять с запасом. Более того, происходит копирования всего старого блока памяти на новый участок памяти нового размера(если размер увеличивается). Поэтому реализация без постоянного дерганья realloc более производительна. Как работает класс vector в C++? Типичная реализация вектора — это указатель на динамический массив. Размер вектора — это фактическое число элементов, а объём — количество используемой им памяти. Если при вставке в вектор новых элементов, его размер становится больше его объёма, происходит перераспределение памяти. Как правило, это приводит к тому, что вектор выделяет новую область хранения, перемещая элементы и свободные старые области в новый участок памяти. Wiki

четверг, 5 декабря 2019 г.

что такое барьер? (структуры данных)

#алгоритм #структуры_данных #терминология


По предмету "Алгоритмы и структуры данных" дали задание создать:


  Структуру данных для организации линейного односвязного списка
  
  Структура данных: очередь (FIFO)  с барьером. 
  
  Представление памяти - динам. память.


Все, в принципе понятно, кроме барьера.

Кто то может простым языком объяснить, что это, и с чем его едят?
    


Ответы

Ответ 1



У вас имеется линейный неупорядоченный список элементов, а именно, в данном случае - FIFO (не будем заморачиваться с тем, что такая структура хранения данных - это очередь, будем называть списком). Однажды вам понадобится выполнить поиск в этом списке элемента по значению, но так как элементы в нем не упорядочены, то и вариант поиска всего один: полный обход списка поэлементно (если без непосредственного применения сортировки перед поиском, конечно же). На каждом шаге поиска вам придется проверять не выходите ли вы за границы вашего списка, а лишь затем сравнивать текущий элемент списка с искомым. Существует несколько способов проверки этого факта: один из них - установить в конец списка уникальный элемент, натыкаясь на который вы будете уверены, что достигли конца списка. При этом необходимо обеспечить уникальность данного элемента. Вот именно этот элемент и будет называться барьером (т.е. дальше него искать не стоит - вы обошли весь список). В вашем же случае с FIFO (First In, First Out) придется добавлять барьерный элемент в список самым последним в связи с порядком обхода данного списка, а затем при обходе списка проверять не является ли текущий элемент барьером, т.е. вы должны заранее знать значение барьера.

воскресенье, 1 декабря 2019 г.

Реляционная БД — где хранить счётчики (кол-во комментариев, кол-во лайков)?

#sql #база_данных #sql_server #архитектура #структуры_данных


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



Собственно, как это лучше сделать? Я вижу три варианта (не считая вариантов использования
кэша, вроде Redis):


Запрашиваем список Записей и для каждой записи запрашиваем COUNT Лайков и Комментариев.
Но это как-то слишком затрано, по-моему. Будет много запросов к базе.
Сохраняем кол-во Лайков и Комментариев в таблице Записей. Придётся добавить дополнительную
логику, но мне такой вариант нравится больше всего.
Создаём ещё одну таблицу PostInfo, в которой будем хранить кол-во Лайков и Комментариев
для конкретной Записи. При запросе Записей делаем JOIN к этой таблице. Так, наверное,
правильнее всего будет, так как Записям не нужно знать о кол-во Лайков и Комментариев.

    


Ответы

Ответ 1



В SqlServer вариант 1 можно реализовать с помощью materialized view, в этом случае по затратности он будет приближаться к третьему варианту. Создаём представление: create view dbo.LikeCount with schemabinding as select post_id, Cnt = count_big(*) from dbo.Like group by post_id GO Добавляем индекс, материализующий представление: create unique clustered index IX_LikeCount on dbo.LikeCount (post_id) GO Аналогичное представление можно создать для таблицы Comment. После чего можно запрашивать данные следующим образом: select p.id, p.title, isnull(lc.Cnt, 0) as LikeCnt, isnull(cc.Cnt, 0) as CommentCnt from Post p left join LikeCount lc with (noexpand) on lc.post_id = p.id left join CommentCount cc with (noexpand) on cc.post_id = p.id Удобство такого варианта в том, что при изменении данных в таблицах Comment и Like количества будут автоматически пересчитываться (в вариантах 2 и 3 эту логику придётся реализовывать специально - в триггерах, или процедурах/запросах, которые работают с добавлением/удалением лайков и комментариев). Минус в том, что для лайков и комментариев представления раздельные, в следствие чего дополнительных соединений в запросе два (в варианте 3 оно будет одно, а в варианте 2 его вообще не будет). Также, если есть вероятность, что в будущем может потребоваться удалять пользователей, оставляя лайки, то этот вариант не подойдёт, и лучше смотреть в сторону вариантов 2 и 3. Вариант 2 выгоднее для select (в запросе не потребуется дополнительное соединение для вытаскивания количеств лайков и комментариев). Вариант 3 лучше с точки зрения независимости постов от сопутствующих им количеств (если происходит изменение записи, соответствующей какому-то посту, и в это же время кому-то вздумалось лайкнуть эту запись, то эти два действия не будут блокировать друг-друга).

Ответ 2



По-моему, надо исходить из использования: хранить лучше в нормализованной схеме, т.е. 1-й вариант; читать быстрее из единого места: чтобы пост + счетчики (2-й вариант). Минус второго варианта в том, что вычисляемые данные хранятся для всех записей, в то время, как реально требуется только вершина айсберга – свежие записи, которые читают/запрашивают. Поэтому решение – кэш, тот же Redis, в котором держать записи+счетчики, а в БД – только данные по 1-му варианту. В кэше ограничить время жизни записи. При чтении искать в кэше, если нет, то дергать БД и писать в кэш. При новом лайке/комментарии обновлять БД и кэш, если запись в кэше ещё присутствует.

Ответ 3



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

четверг, 28 ноября 2019 г.

Оптимизация операций для двоичного индексированного дерева (дерево Фенвика)

#алгоритм #любой_язык #дерево #структуры_данных #сложность


Я занимаюсь оптимизацией в одном проекте, который сильно не укладывается в требования
по производительности. Тут широко используются деревья Фенвика для быстрого получения
префиксных сумм (AKA промежуточные суммы) для  массива с постоянно изменяющимся содержимым.
Сама по себе структура очень полезная и сильно выручает, но здесь кроме стандартных
операций инкремента произвольных элементов очень часто используются две дополнительные
нестандартные операции (я не нашел аналогов в литературе). И на эти операции приходится
значительная часть вычислительной нагрузки.


Получение значений (выгрузка во внешний массив) префиксных сумм для
элементов из непрерывного диапазона от i до i + k.
Обнуление (или, скажем, заполнение константой) элементов из непрерывного
диапазона от i до i + k с корректным пересчетом всех затрагиваемых сумм.


Важно: Для обоих операций среднее значение k много меньше n (количества элементов
в дереве).

На данный момент реализованы эти операции тупо и просто:


В цикле (отдельно!) для каждого элемента из диапазона выполняется нахождение его
значения (стандартная операция для дерева Фенвика). Итого: сложность операции составляет
O(k*log(n)) для k элементов.
В цикле (отдельно!) для каждого элемента выполняется нахождение его значения, после
чего выполняется его инкремент на число обратное его текущему значению (для сброса
в ноль). Сложность, опять же, равна O(k*log(n)).


Я задумался над структурой деревьев и этими операциями. И мне кажется, обе они могут
быть реализованы с меньшей сложностью. Ведь log(n) это глубина прохода от корня до
произвольного элемента, а в этих операциях выполняется обход группы последовательно
расположенных элементов. Из этого факта возможно извлечь пользу, так как ближайший
общий узел дерева в лучшем случае лежит на глубине log(k) от любого из конечных узлов
в непрерывном диапазоне длинной k. Значит для обхода всего диапазона можно спуститься
от корня за log(n), после чего подниматься и спускаться от одного соседнего элемента
к другому за log(k). Итого: сложность алгоритма должна составить O(log(n) + k*log(k))
(что намного лучше текущего варианта). Это для лучшего случая. Но и средняя оценка
должна быть близка к этому, так как худший случай O(k*log(n)) маловероятен при k ≪ n.

Сейчас я залип над реализаций алгоритмов для этих двух операций. Пока что мне не
совсем понятно как перейти от элемента i к элементу i+1, не возвращаясь к корню. По
идее надо подниматься до ближайшего общего узла и спускаться обратно к следующему элементу,
по дороге подсчитывая суммы, необходимые для получения значения следующего элемента.
А для операции (2), видимо, следует на этом же проходе одновременно корректировать
значения промежуточных узлов как при выполнении стандартного инкремента (если это вообще
возможно сделать одновременно).

Это все общие соображения для бинарных деревьев. Как это должно реализовываться конкретно
для дерева Фенвика я пока не представляю.

Буду благодарен за помощь с конкретными алгоритмами на псевдокоде или любом императивном
языке.

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


Ответы

Ответ 1



Да, можно попробовать ускорить эти операции. Приведу для начала свою реализацию: int next2n(int x) { // only 32bit x = x - 1; x = x | (x >> 1); x = x | (x >> 2); x = x | (x >> 4); x = x | (x >> 8); x = x | (x >> 16); return x+1; } class BIT_plus { std::vector in; std::vector num; int n; public: BIT_plus(int n) { init(n); } BIT_plus(const std::vector& list) { init(list); } void init(const std::vector& list) { init(list.size()); for(auto i=0u; i < list.size(); ++i) { inc(i, list[i]); } } std::vector sum_dip(int start, int len) { int summ = sum(start); std::vector res; for(int i=start; i < start + len; i++) { res.push_back(summ); summ += num[i+1]; } return res; } void init(int n) { in.assign(n, 0); num.assign(n, 0); this->n = n; } int size() { return n; } void inc (int i, int delta) { num[i] += delta; for (; i < n; i = (i | (i+1))) in[i] += delta; } int sum (int r) const { int result = 0; for (; r >= 0; r = (r & (r+1)) - 1) result += in[r]; return result; } int sum (int l, int r) const{ return sum (r) - sum (l-1); } void fill_const(int start, int len, int val) { int n2n = next2n(start + len + 1)-1; int sumdelta = 0; for(int i = start; i < start + len; i++) { int delta = val - num[i]; sumdelta += delta; for(int j = i; j < n2n; j = (j|(j+1))) { in[j] += delta; } num[i] = val; } for(int i = n2n; i < n; i = (i|(i+1))) { in[i] += sumdelta; } } auto get_num() const { return num; } auto get_in() const { return in; } }; Она немножко раздута лишними методами, но всё самое важное в ней есть. (За некоторую основу взял реализацию отсюда). В ней по сравнению с обычной реализацией есть два новых метода: fill_const (заполняет определенным числом диапазон исходной последовательности) и sum_range (возвращает префиксные суммы элементов из диапазона). Основная идея реализации довольно проста. Она заключается в том, что помимо непосредственно массива для дерева Фенвика(in) мы храним и сами исходные элементы (num). 1. Тогда нахождение всех префиксных сумм из диапазона [i, i+k] происходит так: сначала мы находим префиксную сумму для i-го элемента. Это происходит как и в обычной реализации. Сложность вычисления будет O(log(n)). А дальше, чтобы найти sum(i+1) прибавить число num[i+1]. И действительно если sum(i) -- сумма первых i элементов, то sum(i) + num[i+1] -- сумма первых i+1 элемента. 2. Вторая часть с заполнением константой чуть сложнее. Сначала поймём, какие элементы нам нужно изменить в исходном массиве in. Если мы меняем элемент с индексом i, то сначала мы должны сменить элемент с индексом i, потом f(i), f(f(i)), где f(i) = i|(i+1) ( | -- побитовое или). Заметим, что если в какой-то момент мы попали в 2k-1, то дальше мы также будем попадать в 2i-1. Более того, если мы начинаем из некого числа i, то в следующее за ним число вида 2k-1 мы2 не пропустим (иначе как мы получим единицу в следующем разряде, а функция у нас монотонно возрастающая). Воспользуемся этим так: от числа j из [i, i+k) будем изменять ячейки (по стандартным правилам. Прибавляя к ним delta) пока номер ячейки, которую мы меняем будет меньше, чем следующее за i+k число вида 2k-1 (next2n(i+k)). Дальше же индексы массива, который мы будем менять для всех j совпадут. И менять мы их будем на сумарную величину изменений всех ячеек. Вероятно, второй алгоритм можно несколько оптимизировать (сейчас он работает, кажется, за O(k*log(i)+log(N)), что асимптотически не быстрее (i может быть порядка n), но быстрее практически особенно при маленькиx i), но уже в такой его реализации он будет работать быстрее, чем независимое изменение для каждой переменной.

Ответ 2



Операция выгрузки ответов во внешний массив делается за O(k + log n) следующим образом: Отдельно храним исходный массив Выгружаем за O(log n) сумму до первого элемента (i). За O(1) прибавляем к ней элемент i+1, получаем сумму до элемента i+1. Аналогично считаем суммы до всех элементов до i+k, итого O(k) операций. С изменением сложнее - дерево Фенвика такую операцию так просто не поддерживает. Однако вам может помочь дерево отрезков с групповым изменением - оно весьма популярно в спортивном программировании. По этому поводу есть, например, руководство на e-maxx, но никаким разумным стандартам читаемости и поддерживаемости кода оно не удовлетворит, поэтому надо вникать и писать свою реализацию (возможно, есть библиотечная, но я не изучал). Дерево отрезков позволит сделать подсчёт суммы и массовое присваивание на любом отрезке за O(log n) (независимо от k), а выгрузку операций можно либо сделать за O(k * log n) "в лоб", либо за O(k + log n) примерно той же оптимизацией, что и с Фенвиком - сначала считаем сумму до элемента i, а далее по одному находим значения следующих элементов. Это можно делать массово, и там получится O(k) операций (придётся полностью обойти несколько поддеревьев суммарного размера O(k)).

Ответ 3



Алгоритмы с использованием массива исходных данных (из которых строилось дерево) будут, конечно, работать быстрее. Так что, если проект позволяет хранить копию исходных данных параллельно с деревом Фенвика, рекомендую обратиться в первую очередь к ответам других участников. Мне же было интересно повозиться только с самим деревом Фенвика. Поэтому мои решения не столь быстры, зато позволяют не хранить копию исходных данных. Пункт 1 - получение значений (выгрузка во внешний массив) префиксных сумм для элементов из непрерывного диапазона от i до (i+k) Перейти от префиксной суммы i-го элемента к префиксной сумме (i+1)-го на самом деле несложно. У меня получился следующий алгоритм: 1) собираем частичные суммы, составляющие i-й элемент, кладём их в структуру данных "стек", в порядке их индексов в дереве от наименьших к наибольшим; в целях оптимизации можно хранить в стеке не сами частичные суммы а нарастающий итог по ним; вершина стека при этом будет хранить значение i-й префиксной суммы (возвращаем её в ответ); например, при i = 10, стек будет таким: stack[0] = tree_item[7] stack[1] = tree_item[7] + tree_item[9] stack[2] = tree_item[7] + tree_item[9] + tree_item[10] // вершина стека, равная 10-му префиксу кстати, максимальный размер этого стека совсем невелик - log2(max_size), т.е. когда количество исходных элементов описывается 32-разрядным числом, размер данного стека будет всего 32 элемента; 2) рассматриваем двоичную запись числа (i+1), записывая биты слева направо от старших к младшим; если двоичная запись заканчивается нулём, переходим к следующему шагу; если двоичная запись заканчивается некоторой непрерывной последовательностью единиц, то подсчитываем количество этих единиц, и удаляем с вершины стека столько элементов, сколько обнаружили завершающих единиц; например, при i = 10, имеем: (i+1) = 11 (десятичная запись) = 0001011 (двоичная запись); двоичная запись числа заканчивается группой из 2-х единиц; следовательно, снимаем со стека 2 элемента, и содержимое стека становится таким: stack[0] = tree_item[7] 3) добавляем в стек (i+1)-й элемент дерева; при этом вершина стека станет содержать значение (i+1)-й префиксной суммы (возвращаем её в ответ); например, при i = 10, имеем: stack[0] = tree_item[7] stack[1] = tree_item[7] + tree_item[11] // вершина стека, равная 11-му префиксу 4) повторяем шаги 2-3 для индексов (i+2),(i+3),...(i+k). для большей ясности выпишу ещё несколько шагов: (i+2), двоичная запись 0001100; из стека ничего не удаляется; stack[0] = tree_item[7] stack[1] = tree_item[7] + tree_item[11] stack[2] = tree_item[7] + tree_item[11] + tree_item[12] // 12-й префикс (i+3), двоичная запись 0001101; из стека удаляется только 1 элемент; stack[0] = tree_item[7] stack[1] = tree_item[7] + tree_item[11] stack[2] = tree_item[7] + tree_item[11] + tree_item[13] // 13-й префикс (i+4), двоичная запись 0001110; из стека ничего не удаляется; stack[0] = tree_item[7] stack[1] = tree_item[7] + tree_item[11] stack[2] = tree_item[7] + tree_item[11] + tree_item[13] stack[3] = tree_item[7] + tree_item[11] + tree_item[13] + tree_item[14] // 14-й префикс ... все эти операции со стеком очень легко понять если смотреть на картинку, изображающую дерево Фенвика в 2-мерном виде, примерно как здесь По части владения Big-O-notation у меня не очень, поэтому не могу точно определить сложность данного алгоритма. Могу только сказать, что это точно будет намного лучше, чем O(k*log(i)), когда i достаточно велико. Пункт 2 - заполнение константным значением элементов из непрерывного диапазона от i до (i+k) с корректным пересчетом всех затрагиваемых сумм Здесь у меня получилось очень похоже на то, что описано в ответе участника retorta, поэтому не буду зря дублировать. Упомяну только о том, что тут приходилось дополнительно вычислять исходные значения. value[i] = prefix(i) - prefix(i-1) Тривиальная реализация требует 2*k обращений к функции вычисления префиксной суммы, и это легко оптимизировать до (k+1) обращений. Однако, можно зайти и с другой стороны: неплохо оптимизируется функция, вычисляющая i-е значение исходного массива, если основываться на том, что каждый чётный элемент дерева есть значение исходное, а из нечётных каждый второй - равен сумме i-го и (i-1)-го исходных значений, и т.д... В общем же случае - опять "выстреливает" подсчет количества единичных битов в младших разрядах числа, как и по пункту 1 :) Реализация Текста получилось уже и так довольно много, поэтом код я решил вынести на github, надеюсь это не запрещается :) Закодил в итоге на C++ в MS Visual Studio Community 2015. В коде отсутствуют некоторые проверки, зато есть простенькие тесты на валидность и производительность кода. Отдельно хочу напомнить, что результаты замеров сильно различаются в Debug- и Release-конфигурациях, в последней выигрыш обычно меньше. Ссылка на код

вторник, 26 ноября 2019 г.

Редкие, экзотические структуры данных и их применение? [закрыт]


Все знают про массивы, связные списки, стэки, очереди, хэш таблицы, простые двоичны
деревья. Как насчет того чтобы привести хороший, годный пример чего-нибудь более продвинутого вроде фильтра Блума или B-дерева?    


Ответы

Ответ 1



Динамический массив размером до 2^32 элементов с временем вставки O(1). По сути структура MMU с динамическим выделением сегментов данных и блоков оглавления нижнего уровня. Оглавление всегда 2 уровня. Года полтора назад здесь был вопрос Как реализовать динамический массив? в ходе ответ (скорее спора о возможности операций с такой структурой за время O(1)) появилась такая структура данных. Там и программка есть. Применение неизвестно.

Ответ 2



Красно-черное дерево, применяется в Java-коллекциях (TreeMap, TreeSet)

Ответ 3



самое экзотическое, что я видел - Дерево ван Эмде Боаса

Ответ 4



Коллега самостоятельно делал buddy allocator - вот это было прикольно, хотя это и не совсем структура данных. Насколько я помню, чтобы сэкономить память, он на незаняты кусках памяти хранил информацию о соседних занятых. И линковал их в списки. А для фрагментов по 4 и 8 байт он делал ещё как-то по-особенному. Потому что первым способом на них потребовалось бы слишком много накладных расходов :)

суббота, 22 июня 2019 г.

Как рекурсивно создать двунаправленный список зная только количество элементов, Java

Есть DDS DoubleNode, которая является двунаправленным списком:
public class DoubleNode { public int value; public DoubleNode prev; public DoubleNode next;
public DoubleNode(int value, DoubleNode prev, DoubleNode next) { this.value = value; this.prev = prev; this.next = next; }
а вот что я пытаюсь сделать, так это построить такую Node, зная только количество элементов в ней (значения элементов выбираются случайно). При всем при этом построить хочу рекурсивно, сваял вот такой метод:
public static DoubleNode doubleNodeGenRec (int length) { return (length == 0) ? null : new DoubleNode((int)(Math.random()*10), null, doubleNodeGenRnd(--length)); }
как новому элементу передать ссылку на DoubleNode prev;?
выходит, что строится Node в одну сторону, т.к. в методе туда передается просто null, и в результате выходит просто связанный список. чем заменить этот null?


Ответ

public DoubleNode(int value) { this.value = value; }
public static DoubleNode doubleNodeGen(int length) { return doubleNodeGenRec(length, null); }
private static DoubleNode doubleNodeGenRec(int length, DoubleNode prev) { if (length == 0) { return null; }
int value = (int) (Math.random() * 10); DoubleNode node = new DoubleNode(value); node.prev = prev; node.next = doubleNodeGenRec(length - 1, node); return node; }

четверг, 11 апреля 2019 г.

Теория по односвязному списку

Здравствуйте.
Довольно часто на собеседованиях или же в тестовых заданиях (для прохождения на стажировку, например) задается вопрос об односвязных списках
Например, дают задачу, в которой необходимо развернуть односвязный список за О(n) времени. Так вот, как это сделать? Можно ли просто переопределить ссылки? Т.е. сделать за 1 итерацию так, чтобы не A->B, а A<-B ?
Можете, пожалуйста, привести пример? Заранее благодарю


Ответ

Первая ссылка из гугла.
void reverse(OneWayList list){ OneWayList.Node node = list.head; OneWayList.Node previous = null; while(node != null){ //next item OneWayList.Node tmp = node.next; //swap items node.next = previous; previous = node; list.head = node; //next item node = tmp; } }
Отсюда