Ежедневная проблема с кодированием – это веб-сайт, который каждый день будет отправлять вам задачу по программированию на ваш почтовый ящик. Я хочу показать новичкам, как решить некоторые из этих проблем с помощью Java, так что это будет продолжающаяся серия моих решений. Не стесняйтесь выбирать их отдельно в комментариях!
Проблема
Связанный список XOR – это более эффективный с точки зрения памяти двусвязный список. Вместо того, чтобы каждый узел, удерживающий следующий
и предыдущие
поля, он содержит поле с именем оба
, которое является XOR следующего узла и предыдущего узла. Реализуйте связанный список XOR; в нем есть add(element)
, который добавляет элемент в конец, и get(index)
, который возвращает узел по индексу.
Если вы используете язык, который не имеет указателей (например, Python), вы можете предположить, что у вас есть доступ к get_pointer получить указатель
и dereference_pointer
функции, которые преобразуют между узлами и адресами памяти.
Стратегия
Спойлеры! Не смотрите ниже, если не хотите увидеть мое решение!
Двусвязные списки
Итак, во-первых, мне нужно понять, что такое двусвязный список. Итак, я прочитал статью в Википедии и, как я понимаю, это так:
Каждый элемент односвязного списка (или просто связанный список ) содержит:
- некоторые данные
- указатель или ссылка на следующий элемент в списке
Каждый элемент двусвязного списка содержит:
- некоторые данные
- указатель или ссылка на следующий элемент в списке
- указатель или ссылка на предыдущий элемент в списке
Вы можете визуализировать различия между этими типами списков следующим образом:
Односвязные списки можно просматривать только в прямом направлении. Элемент списка понятия не имеет, какой элемент был перед ним (если таковой имелся), и не имеет возможности получить к нему доступ. Двусвязный список можно просматривать в любом направлении, как вперед, так и назад. Когда пользователь пытается выйти за пределы конца списка (или за пределы начала списка для списка с двойной связью), должна быть выдана какая-либо ошибка или должен быть возвращен null
.
Далее… что такое список XOR? Подсказка как бы объясняет концепцию, но я не уверен, что понимаю ее. Я вернусь к этому немного позже.
Строгая интерпретация подсказок
Я нашел этот ответ SO , который довольно хорошо объясняет плюсы и минусы двусвязных списков. Это также дает хорошее объяснение того, почему мы могли бы захотеть его использовать. Если позволите, я перефразирую…
В “истинном” односвязном или двусвязном списке мы храним два объекта:
- данные
- указатель или ссылка на другой элемент списка
Указатель – это просто еще одна переменная, которая содержит адрес памяти. В качестве примера предположим, что у нас есть двусвязный список C-language int
s, длина которых составляет 4 байта . Указатели в C на 64-разрядной машине будут иметь длину 8 байт , поэтому для каждого элемента нашего списка требуется 20 байт для int
и двух указателей.
Давайте предположим, что первый элемент списка находится по адресу 0x9A7D
…
Префикс 0x
указывает на то, что это шестнадцатеричное число. На 64-разрядной машине (где 64-разрядный
относится к размеру адресного пространства памяти) для представления любого адреса требуется 8 байт. Один байт (диапазон 0-255) может быть представлен двумя шестнадцатеричными цифрами. Таким образом, для 8 байт требуется 16 шестнадцатеричных цифр.
В нашем примере этот адрес ссылается на значение , сохраненное этим элементом списка. Поскольку это значение является int
, оно займет четыре байта пространства (восемь шестнадцатеричных цифр). Следующие 8 байт (шестнадцать шестнадцатеричных цифр) будут указателем на “следующий” элемент в списке, а последние 8 байт будут указателем на “предыдущий” элемент в списке, поэтому:
D0 C0 FF EE [ "next" address ] [ "previous" address ] ^^^^^^^^^^^ 4-byte integer value (hex "D0 C0 FF EE" = decimal "3 502 309 358")
Если первый элемент списка (этот) находится по адресу
0x9E7D6300BA679A7D
…затем следующий элемент будет смещен на 20 байт (при условии, что элементы списка хранятся в непрерывном блоке памяти, что может быть не так), в
0x9E7D6300BA679A91
0x7D + +
D0 C0 FF EE 9E 7D 63 00 BA 67 9A 91 [ "previous" address ] ^^^^^^^^^^^^^^^^^^^^^^^ "next" memory address
Аналогично, “предыдущий” адрес будет смещен на 20 байт в противоположном направлении для всех элементов списка, кроме этого первого. Поскольку у первого элемента нет “предыдущего” адреса, будет некоторый индикатор того, что там нет элемента (возможно, нулевой указатель на адрес памяти 0x0
):
D0 C0 FF EE 9E 7D 63 00 BA 67 9A 91 00 00 00 00 00 00 00 00 ^^^^^^^^^^^^^^^^^^^^^^^ "previous" memory address
В C мы, очевидно, не будем работать напрямую с этими байтами. Вместо этого у нас будет что-то вроде :
struct DLL { int data; // value struct DLL *next; struct DLL *previous; }
… но как, черт возьми, мы могли бы реализовать это на Java? Java заимствует много синтаксиса из C/C++, но в нем нет struct
s. Итак, в Java вместо этого мы могли бы иметь :
class DLL { int value; /* implement next */ /* implement previous */ }
Java также не имеет указателей, но помните, что в приглашении указано:
Если вы используете язык, который не имеет указателей (например, Python), вы можете предположить, что у вас есть доступ к get_pointer получить указатель и разыменование_поинтер_ функции, которые преобразуют между узлами и адресами памяти.
Java, разработанный как “безопасный” язык, не позволяет вам возиться с адресами памяти . Так что get_pointer
и методы dereference_pointer
являются чисто воображаемыми. Судя по описанию приглашения, они должны иметь подписи типов, такие как:
DLL* get_pointer (DLL link) DLL dereference_pointer (DLL* pointer)
…которые преобразуют объект DLL
в указатель, содержащий его адрес в памяти, и наоборот. Проблема предполагает выполнение чего-то вроде:
public class DLL { public int data; // value private DLL next; private DLL previous; public DLL* next() { return get_pointer(next) } public DLL* previous() { return get_pointer(previous) } }
Это уродливо. Это также недопустимый синтаксис Java. В реальной жизни это бесполезно. Я не думаю, что стоит продолжать это гипотетическое решение.
Переосмысление подсказки
Вместо того, чтобы следовать подсказке до буквы, давайте немного переосмыслим ее, чтобы мы действительно могли уловить дух связанных списков XOR в Java.
Мы могли бы попытаться использовать java.lang.instrument
для определения размеров (в байтах) объектов в Java , но такой подход дает неинтуитивные результаты ( все String
имеют одинаковый размер), которые не полезны для наших целей (зная фактический размер объекта в памяти).
Вместо этого давайте смоделируем адреса памяти. Java допускает шестнадцатеричные литералы и (по умолчанию) преобразует их в десятичные при печати:
jshell> 0x10 $3 ==> 16 jshell> 0x20 $4 ==> 32 jshell> 0x10 + 0x10 $5 ==> 32
Поскольку Java int
имеет диапазон [-2e31, 2e31-1]
( [-2147483648, 2147483647]
), его максимальные шестнадцатеричные значения равны:
jshell> 0x0 $31 ==> 0 jshell> 0x7FFFFFFF $32 ==> 2147483647 jshell> 0x80000000 $33 ==> -2147483648 jshell> 0xFFFFFFFF $34 ==> -1
Таким образом, мы можем создать метод, который “выделяет” адрес памяти для нового объекта, учитывая размер объекта. Мы можем отслеживать, какие “ячейки памяти” используются, чтобы случайно не “перезаписать” старые данные. Затем мы, наконец, сможем реализовать связанный с XOR список (который будет объяснен во время его реализации) на Java.
Код
Для начала давайте создадим простой класс Memory
в Java:
public class Memory { private static Random random = new Random(); // return any address except "0x00000000", the "NULL" address public static String randomAddress() { int value = random.ints(1L, Integer.MIN_VALUE, Integer.MAX_VALUE).toArray()[0] + 1; return intToAddress(value); } public static int addressToInt(String address) { return (int)(Long.parseLong(address.substring(2), 16) + Integer.MIN_VALUE); } public static String intToAddress(int value) { long longValue = (long)value - (long)Integer.MIN_VALUE; return String.format("0x%8s", Long.toString(longValue, 16).toUpperCase()).replaceAll(" ", "0"); } }
Этот класс имеет три функции: int To Address()
, который принимает любое значение
int в качестве аргумента и преобразует его в шестнадцатеричный адрес
Строка ,
адрес В Int() , который выполняет обратное, и
случайный адрес() , который генерирует случайный адрес
Строка . Репозиторий GitHub, в котором размещен этот код
jshell> Memory.randomAddress() $195 ==> "0xB821DAE5" jshell> Memory.addressToInt("0xB821DAE5") $196 ==> 941742821 jshell> Memory.intToAddress(941742821) $197 ==> "0xB821DAE5"
Максимальные значения для int Для Address()
равны Integer. МИНИМАЛЬНОЕ ЗНАЧЕНИЕ
и Целое число. MAX_VALUE
МАКСИМАЛЬНОЕ ЗНАЧЕНИЕ//:
jshell> Memory.intToAddress(Integer.MIN_VALUE) $198 ==> "0x00000000" jshell> Memory.intToAddress(Integer.MAX_VALUE) $199 ==> "0xFFFFFFFF"
… которые возвращают максимальные значения для адреса В Int()
:
jshell> Memory.addressToInt("0x00000000") $200 ==> -2147483648 jshell> Integer.MIN_VALUE $201 ==> -2147483648 jshell> Memory.addressToInt("0xFFFFFFFF") $202 ==> 2147483647 jshell> Integer.MAX_VALUE $203 ==> 2147483647
Обратите внимание, что моя реализация “максимальных” и “минимальных” шестнадцатеричных значений отличается от Java, которые переносятся в 0x7FFFFFFF
/ 0x80000000
. Я перестроил так, чтобы минимальное шестнадцатеричное значение соответствовало наименьшему адресу памяти, что, на мой взгляд, немного более интуитивно понятно.
Далее нам нужно “выделить память”, когда мы создаем новый объект (в данном случае наш объект с двойным списком). Когда мы делаем это, нам нужно указать необходимый объем памяти. Давайте создадим malloc
-подобная функция для памяти
:
public static String malloc(long size) { // if object has zero size, return NULL address if (size < 1) return "0x00000000"; // if object cannot fit in memory, throw error if (size > (long)Integer.MAX_VALUE - (long)Integer.MIN_VALUE ) throw new IllegalArgumentException("insufficient memory"); // if object can fit in memory, get largest possible address long first = Integer.MIN_VALUE; long last = (long)Integer.MAX_VALUE - size; // if only one possible memory address, return that one if (first == last) return "0x00000001"; // ...else, randomise over valid range int value = random.ints(1L, (int)first, (int)last).toArray()[0] + 1; // ...and return as address return intToAddress(value); }
Это, конечно, чрезвычайно простой и неэффективный способ выделения памяти. Есть лучшие решения , но они требуют немного больше работы. Давайте пока воспользуемся этим простым решением. Наконец, нам нужен способ “зарегистрировать” память, чтобы мы случайно не перезаписали объект другим.
// keep track of which int-indexed blocks are occupied by data private static HashSetoccupied = new HashSet<>(Arrays.asList(Integer.MIN_VALUE)); // free memory within a certain range public static void free(String iAddress, String fAddress) { int iAdd = addressToInt(iAddress); int fAdd = addressToInt(fAddress); // remove all addresses in range occupied.removeAll(IntStream.range(iAdd, fAdd).boxed().collect(Collectors.toList())); // check that "NULL" is still "occupied" occupied.add(Integer.MIN_VALUE); } // free all memory public static void free() { free("0x00000001", "0xFFFFFFFF"); } // list of objects in memory public static HashMap refTable = new HashMap<>(); static { refTable.put("0x00000000", null); } // dereference object public static Object dereference(String address) { return refTable.get(address); }
Вышесказанное, конечно, на самом деле не работает, так как мы не проверяем в malloc
, зарегистрирована память или нет. Чтобы иметь полностью работающее, надежное решение, нам понадобился бы гораздо более сложный механизм распределения памяти. Но в любом случае, этого, вероятно, достаточно, чтобы немного поиграть:
jshell> Memory.occupied $83 ==> [-2147483648] jshell> Memory.malloc(1) $84 ==> "0xB8B7087E" jshell> Memory.occupied $85 ==> [-2147483648, 951519358] jshell> Memory.intToAddress(951519358) $86 ==> "0xB8B7087E" jshell> Memory.malloc(2) $87 ==> "0x802AD3C8" jshell> Memory.occupied $88 ==> [-2147483648, 2806728, 2806729, 951519358]
Наконец мы можем начать говорить о списках, связанных с XOR.
Списки, связанные с XOR
Вспомните определение двусвязного списка, приведенное ранее:
Каждый элемент двусвязного списка содержит:
- некоторые данные
- указатель или ссылка на следующий элемент в списке
- указатель или ссылка на предыдущий элемент в списке
Итак, нам нужен класс Node
s для элементов списка и Двусвязный список
класс. Простой Node
класс может выглядеть следующим образом:
class Node { // number of "bytes" to allocate for a DLL static final int size = 20; U data; // data held by this DLL element String next; // address of next DLL element String prev; // address of previous DLL element String addr; // address of this DLL element // constructor with no "next" or "prev" elements public Node (U data) { this.data = data; this.next = "0x00000000"; // null this.prev = "0x00000000"; // null // allocate memory for this DLL element this.addr = Memory.malloc(size); } }
Мы можем добавить методы для получения и установки адресов следующий
и предыдущий
(предыдущие) узлы в списке:
// method to get a "pointer" to this object ("get_pointer") String ptr() { return this.addr; } // getters for next and prev String next() { return this.next; } String prev() { return this.prev; } // setters for next and prev void next(String addr) { this.next = addr; } void prev(String addr) { this.prev = addr; }
Наконец, нам нужен способ “выделения памяти” для этих объектов, присвоения им адреса памяти и “разыменования” этого адреса. Для нашего Двусвязного списка
и Node
classes чтобы получить доступ к нашему Memory
классу, нам нужно упаковать их в *.jar
. Я создам проект Maven со всем этим кодом. Затем мы можем развернуть объект Двусвязный список
…
public class DoublyLinkedList{ // List of Nodes private List > Nodes; // get number of Nodes in this List public int size() { return this.Nodes.size(); } // constructor public DoublyLinkedList() { this.Nodes = new ArrayList<>(); } // add a Node to the end of the List public DoublyLinkedList add(T t) { Node newNode = new Node<>(t); // if this List already has Nodes if (this.size() > 0) { // get Node which previously was last Node Node oldLastNode = this.Nodes.get(this.size()-1); // edit last Node in List to point to _new_ last Node oldLastNode.next = newNode.ptr(); // edit new last Node to point to _old_ last Node newNode.prev(oldLastNode.ptr()); } // add new last Node to end of List this.Nodes.add(newNode); // so add() can be chained return this; } /* Node inner class */ }
Теперь мы можем использовать Node.ptr()
для получения “адреса” элемента Node
и адресов ” dereference()
” для получения объектов, на которые они ссылаются (примечание что я также @Переопределить
методы по умолчанию toString()
для Узла
и Двусвязный список
):
$ jshell -cp target/006-1.0-SNAPSHOT.jar | Welcome to JShell -- Version 11.0.2 | For an introduction type: /help intro jshell> import DCP.* jshell> DoublyLinkedListdll = new DoublyLinkedList<>(); dll ==> jshell> dll.add(1) $3 ==> 0x00000000 <- 0xA0EAA2D0 -> 0x00000000 null <- 1 -> null jshell> dll.add(2) $4 ==> 0x00000000 <- 0xA0EAA2D0 -> 0x29728E8A null <- 1 -> 2 0xA0EAA2D0 <- 0x29728E8A -> 0x00000000 1 <- 2 -> null jshell> dll.add(3) $5 ==> 0x00000000 <- 0xA0EAA2D0 -> 0x29728E8A null <- 1 -> 2 0xA0EAA2D0 <- 0x29728E8A -> 0x5DBD6A3C 1 <- 2 -> 3 0x29728E8A <- 0x5DBD6A3C -> 0x00000000 2 <- 3 -> null
Переопределяя toString()
, у меня есть каждый Узел
укажите его адрес, а также адреса следующего
и предотвращать
Node
s в первой строке и их значения во второй строке:
0xA0EAA2D0 <- 0x29728E8A -> 0x5DBD6A3C 1 <- 2 -> 3
Со списком, связанным с XOR , вместо удержания обоих тот предыдущий
и следующие
адреса, мы сохраняем XOR этих адресов:
jshell> Memory.addressToInt("0xA0EAA2D0") $7 ==> 552248016 jshell> Memory.addressToInt("0x5DBD6A3C") $8 ==> -574789060 jshell> $7 ^ $8 $9 ==> -44578580 jshell> Memory.intToAddress($9) $10 ==> "0x7D57C8EC"
Списки, связанные с XOR, не могут разрешать произвольный доступ. Мы должны начать либо с начала, либо с конца списка и продвигаться вниз по списку. Если мы начнем с первого Узла
из нашего приведенного выше списка, например:
0x00000000 <- 0xA0EAA2D0 -> 0x29728E8A null <- 1 -> 2
jshell> int both = (Memory.addressToInt("0x00000000") ^ Memory.addressToInt("0x29728E8A")) both ==> 695373450 jshell> Memory.intToAddress(both) $12 ==> "0xA9728E8A"
“Чтобы начать просмотр списка в любом направлении с некоторой точки, требуется адрес двух последовательных элементов. Если адреса двух последовательных элементов поменять местами, обход списка будет происходить в противоположном направлении” — Вики
В приведенной выше цитате из Википедии отмечается, что нам нужен адрес предыдущего элемента, а также оба
для обхода списка:
jshell> Memory.intToAddress(both ^ Memory.addressToInt("0x00000000")) $18 ==> "0x29728E8A"
Для второго элемента в нашем списке выше:
jshell> int both = (Memory.addressToInt("0xA0EAA2D0") ^ Memory.addressToInt("0x5DBD6A3C")) both ==> -44578580 jshell> Memory.intToAddress(both) $20 ==> "0x7D57C8EC" jshell> Memory.intToAddress(both ^ Memory.addressToInt("0xA0EAA2D0")) $21 ==> "0x5DBD6A3C"
Мы видим, что нам нужен предыдущий адрес и XOR оба
, чтобы получить следующий адрес, или следующий адрес и XOR оба
, чтобы получить предыдущий адрес. Нам не нужно хранить два адреса в каждом Узле
, но нам нужно отслеживать адрес этого предыдущего Узла
где-то . В целом, это довольно неуклюжая структура данных с небольшой пользой . Возможно, это было более полезно в первые дни вычислительной техники, когда память была на высоте, но сейчас, вероятно, лучше избегать этого. (Не говоря уже о том, что на самом деле это невозможно реализовать на Java.)
Обсуждение
Это было интересное упражнение, но в основном по причинам, которые не имели ничего общего с фактическим приглашением. Изучение шестнадцатеричных литералов в Java, преобразование между ними и целыми числами, реализация поддельного хранилища объемом 4 ГБ с адресом NULL
, подделка указателей и разыменование… все это было действительно интересно и познавательно. Списки, связанные с XOR? Не так сильно.
В будущем я мог бы попытаться повторно реализовать свое поддельное пространство памяти с помощью лучшего malloc
. Я думаю, что это было бы действительно познавательным испытанием. Как ты думаешь?
Весь код для решения моих ежедневных проблем с кодированием доступен по адресу github.com/awwsmm/daily .
Предложения? Дайте мне знать в комментариях.
Оригинал: “https://dev.to/awwsmm/java-daily-coding-problem-006-2l98”