daily Coding Problem is a website which will send you a programming challenge to your inbox every day. I want to show beginners how to solve some of these problems using Java, so this will be an ongoing series of my solutions. Feel free to pick them apart in the comments!
Problem
XOR链表是内存效率更高的双链表。 而不是每个节点都持有下一个和上一个字段,它包含一个名为都, which is an XOR of the 下一个 node和the 上一个ious node. Implement an XOR linked list; it has an 加(元素) which adds the element to the end,和a 获取(索引)返回索引处的节点。如果使用没有指针的语言(例如Python),则可以假定您有权访问get_pointer和dereference_pointer functions that converts between nodes和memory addresses.
Strategy
剧透!除非您想查看我的解决方案,否则请不要在下面查看!
Doubly-Linked Lists
So, first, I need to understand what a doubly-linked list is. So I read the Wikipedia article and the way I understand it is this:
一个的每个元素单链表(或只是一个链表)包含:
- 一些数据指向列表中下一个元素的指针或引用
一个的每个元素双链表包含:
- 一些数据指向列表中下一个元素的指针或引用指向列表中前一个元素的指针或引用
您可以像这样可视化这些类型的列表之间的差异:
单链列表只能向前移动。 列表的元素不知道哪个元素在它之前(如果有),也无法访问它。 双向链表可以在任一方向上遍历,也可以向前或向后遍历。 当用户尝试到达列表的末尾(或双向链接的列表的末尾)时,应该抛出某种错误,或者空值应该退货。
接下来...什么是XOR列表? 迅速的解释了这个概念,但是我不确定我是否理解。 我会再谈一谈。
Strict Interpretation of Prompt
I found this SO answer which explains the pros and cons of doubly-linked lists fairly well. It also gives a good explanation of why we might want to use one. If I may paraphrase...
在“真”单链接或双链接列表中,我们拥有两个对象:
- 数据指向列表中另一个元素的指针或引用
一种 pointer is simply another variable which holds a memory address. As an example, let's say we have a doubly-linked list of C-language int
s, which are 4 bytes in length. Pointers in C on a 64-bit machine will be 8 bytes long, so each element of our list requires 20 bytes, for the int
and the two pointers.
假设列表的第一个元素位于地址0x9A7D...
前缀0x表示这是一个十六进制数。 在64位计算机上(其中64位是指内存地址空间的大小),代表任何地址都需要8个字节。 一个字节(范围为0-255)可以由两个十六进制数字表示。 因此8个字节需要16个十六进制数字。
在我们的示例中,该地址引用了值由列表的该元素存储。 因为值是一个整型, it will take up four bytes of space (eight hex digits). The next 8 bytes (sixteen hex digits) will be a po整型er to the "next" element in the list, and the final 8 bytes will be a po整型er to the "previous" element in the list, so:
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 + 20 = 0x7D + 0x14 = 0x91
D0 C0 FF EE 9E 7D 63 00 BA 67 9A 91 [ "previous" address ]
^^^^^^^^^^^^^^^^^^^^^^^
"next" memory address
Similarly, the "previous" address will be offset by 20 bytes in the opposite direction for all elements of the list except this first one. Since the first element has no "previous" address, there will be some indicator that there is no element there (probably a null pointer to memory address 0x0
):
D0 C0 FF EE 9E 7D 63 00 BA 67 9A 91 00 00 00 00 00 00 00 00
^^^^^^^^^^^^^^^^^^^^^^^
"previous" memory address
In C, we obviously won't work directly with these bytes. Instead, we'll have something like:
struct DLL {
int data; // value
struct DLL *next;
struct DLL *previous;
}
...but how on earth could we implement this in Java? Java borrows a lot of syntax from C/C++, but it has no struct
s. So in Java, we might instead have:
class DLL {
int value;
/* implement next */
/* implement previous */
}
Java也没有指针,但是请记住提示指出:
如果使用没有指针的语言(例如Python),则可以假定您有权访问get_pointer和dereference_pointer functions that converts between nodes和memory addresses.
Java, designed to be a "safe" language, does not allow you to mess around with memory addresses. So the get_pointer
and dereference_pointer
methods are purely imaginary. From the prompt description, they should have type signatures like:
DLL* get_pointer (DLL link)
DLL dereference_pointer (DLL* pointer)
...将动态链接库对象变成一个指针,该指针包含其在内存中的地址,反之亦然。 该问题建议采取以下措施:
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语法。 在现实生活中毫无用处。 我认为继续这种假设的解决方案不值得。
Re-Interpretation of Prompt
让我们重新解释一下该提示,而不是按照字母的提示进行操作,以便我们实际上可以掌握Java中XOR链接列表的精神。
We could try to use java.lang.instrument
to determine the sizes (in bytes) of objects in Java, but this approach yields unintuitive results (all String
s are the same size) which aren't useful for our purposes (knowing the actual size in memory of an object).
Instead, let's simulate memory addresses. Java allows for hexadecimal literals, and (by default) converts them to decimal when they're printed:
jshell> 0x10
$3 ==> 16
jshell> 0x20
$4 ==> 32
jshell> 0x10 + 0x10
$5 ==> 32
自从Java整型有一个范围[-2e31,2e31-1]([-2147483648,2147483647]),其最大十六进制值为:
jshell> 0x0
$31 ==> 0
jshell> 0x7FFFFFFF
$32 ==> 2147483647
jshell> 0x80000000
$33 ==> -2147483648
jshell> 0xFFFFFFFF
$34 ==> -1
因此,我们可以创建一个给定对象大小为新对象“分配”内存地址的方法。 我们可以跟踪正在使用的“内存位置”,因此我们不会意外“覆盖”旧数据。 然后,我们最终可以用Java实现XOR链接列表(将在其实现期间进行说明)。
Code
首先,让我们创建一个简单的记忆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");
}
}
This class has three functions: intŤoAddress()
, which takes any int
value as an argument and converts it to a hex address String
, addressŤoInt()
, which performs the inverse, and randomAddress()
, which generates a random address String
. Ťhe GitHub repository which hosts this code adds some bounds checking and comments.
jshell> Memory.randomAddress()
$195 ==> "0xB821DAE5"
jshell> Memory.addressToInt("0xB821DAE5")
$196 ==> 941742821
jshell> Memory.intToAddress(941742821)
$197 ==> "0xB821DAE5"
的最大值intToAddress()是整数MIN_VALUE和整数MAX_VALUE:
jshell> Memory.intToAddress(Integer.MIN_VALUE)
$198 ==> "0x00000000"
jshell> Memory.intToAddress(Integer.MAX_VALUE)
$199 ==> "0xFFFFFFFF"
...返回的最大值addressToInt():
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。 我重新排列,以便最小的十六进制值对应于最低的内存地址,我认为这更直观。
Next, we need to "allocate memory" when we create a new object (in this case, our doubly-linked list object). When we do this, we need to specify the amount of memory we need. Let's create a malloc
-like function for Memory
:
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);
}
This, of course, is an extremely simple and inefficient way of allocating memory. Ťhere are better solutions, but they require a bit more work. Let's use this simple solution for now. Finally, we need a way to "register" memory so we don't accidentally overwrite an object with another one.
// keep track of which int-indexed blocks are occupied by data
private static HashSet<Integer> occupied = 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<String, Object> refTable = new HashMap<>();
static { refTable.put("0x00000000", null); }
// dereference object
public static Object dereference(String address) {
return refTable.get(address);
}
当然,以上内容实际上并不起作用,因为我们没有签入分配该内存是否已注册。 要拥有一个完整的,强大的解决方案,我们将需要一个更加复杂的内存分配引擎。 但是无论如何,这可能足以发挥作用:
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-Linked Lists
回想一下先前的双向链接列表的定义:
一个的每个元素双链表包含:
- 一些数据指向列表中下一个元素的指针或引用指向列表中前一个元素的指针或引用
所以我们需要一类节点代表列表中的元素,以及双链表类。 一个简单的节点类可能看起来像:
class Node<U> {
// 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);
}
}
我们可以添加方法来获取和设置地址下一个和上一个 (上一个ious) nodes in the list:
// 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; }
最后,我们需要一种为这些对象“分配内存”,为它们分配内存地址并“取消引用”该地址的方法。 为了我们双链表和节点访问我们的课程记忆类,我们需要将它们打包到一个*。罐。 我将使用所有这些代码创建一个Maven项目。 然后,我们可以扩展双链表宾语...
public class DoublyLinkedList<T> {
// List of Nodes
private List<Node<T>> 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<T> add(T t) {
Node<T> newNode = new Node<>(t);
// if this List already has Nodes
if (this.size() > 0) {
// get Node which previously was last Node
Node<T> 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 */
}
现在,我们可以使用节点.ptr()得到一个的“地址”节点元素和“解引用()以获取它们所引用的对象的地址(请注意,我也@Override默认值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> DoublyLinkedList<Integer> dll = 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(),我有节点给出其地址,以及下一个和上一个节点sonthefirstline,和theirvaluesonthesecondline:
0xA0EAA2D0 <- 0x29728E8A -> 0x5DBD6A3C
1 <- 2 -> 3
与异或链接的列表,而不是持有都的上一个和下一个 addresses, we hold的XOR of those addresses:
jshell> Memory.addressToInt("0xA0EAA2D0")
$7 ==> 552248016
jshell> Memory.addressToInt("0x5DBD6A3C")
$8 ==> -574789060
jshell> $7 ^ $8
$9 ==> -44578580
jshell> Memory.intToAddress($9)
$10 ==> "0x7D57C8EC"
异或链接列表不能允许随机访问。 我们必须从列表的开头或结尾开始,然后沿列表的下方进行操作。 如果我们从第一个开始节点例如,在我们上面的列表中:
0x00000000 <- 0xA0EAA2D0 -> 0x29728E8A
null <- 1 -> 2
jshell> int both = (Memory.addressToInt("0x00000000") ^ Memory.addressToInt("0x29728E8A"))
both ==> 695373450
jshell> Memory.intToAddress(both)
$12 ==> "0xA9728E8A"
"To start traversing the list in either direction from some point, the address of two consecutive items is required. If the addresses of the two consecutive items are reversed, list traversal will occur in the opposite direction"
-- Wiki
上面的Wikipedia引用指出,我们需要上一个元素的地址以及都遍历列表:
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"
We can see that we need the previous address and the XOR both
to get the next address, or the next address and the XOR both
to get the previous address. We don't have to store two addresses in each Node
, but we do need to keep track of the address of that previous Node
somewhere. All in all, this is quite a clunky data structure with little benefit. It may have been more useful in the early days of computing, when memory was at a premium, but it's probably best to avoid it now. (Not to mention that it's actually impossible to implement in Java.)
Discussion
这是一个有趣的练习,但是主要是由于与实际提示无关的原因。 了解Java中的十六进制文字,在它们和整数之间进行转换,并使用空值地址,伪造的指针和取消引用……这一切都非常有趣且有益。 异或链接列表? 没有那么多。
将来,我可能会尝试以更好的方式重新实现我的假内存空间分配。 我认为这将是一个真正有益的挑战。 你怎么看?
All the code for my Daily Coding Problems solutions is available at github.com/awwsmm/daily.
有什么建议吗? 在评论中让我知道。