Java每日编码问题#006

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...

在“真”单链接或双链接列表中,我们拥有两个对象:

  1. 数据指向列表中另一个元素的指针或引用

一种 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 ints, 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 structs. 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 Strings 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.

有什么建议吗? 在评论中让我知道。

from: https://dev.to//awwsmm/java-daily-coding-problem-006-2l98

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值