Interview fun article: Start with a picture and spend ten minutes understanding the tree logic of HashMap

I. Introduction

I have nothing to do, so I took a deep dive into the more important nodes of HashMap, and then tried to output them in the most understandable way.

The old rule is to serve food to friends who save time:

image.png

2. Tree formation of HashMap

Basic entry

When interviewing others before, I often asked this question. One of the more frequently asked questions is the tree formation of HashMap. The standard answer to this question is also very simple:

  • Before Java 1.8 , HashMap handled hash conflicts through the data structure of array + linked list . When multiple keys have the same hash code, they are stored in a hash bucket to form a linked list structure.
  • After Java 1.8 , it is also an array + linked list, but when the length of the linked list exceeds a certain threshold, it will transform into a red-black tree structure . This behavior is called treeing.

After knowing these two points, it is generally considered that you have a certain understanding of HashMap. Even if you have passed the elementary level, but if you are an intermediate level, you will ask a little more questions.

Intermediate talk:

  • How to implement linked list?

Each object written to HashMap will be encapsulated into a Node object. This object has four attributes, one of which is also a Node object, used to point to the next Node in the linked list.

java

复制代码

static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; // next 即为链表中下一个 Node Node<K,V> next; }
  • How to implement red-black tree?

Implemented based on TreeNode object, which contains TreeNode-Left, TreeNode-Right, and TreeNode-Parent objects, used to implement tree structure

java

复制代码

// 实际每个 TreeNode 包含了数据信息 TreeNode extends Node { // 父节点 TreeNode parent; // 左子节点 TreeNode left; // 右子节点 TreeNode right; // 上一节点用于生成双向链表,便于树形操作 TreeNode prev; boolean red; }
  • Why are there Hash conflicts?

Because the bottom layer of HashMap is an array, and which array cell an element is placed in is realized by Hash & array length . This means that even if the Hash algorithm is very powerful, the possibility of getting the same subscript after taking the modulo is very high. .

  • Why choose red-black trees?

The red-black tree can ensure the balance of elements on both sides. Based on this feature, the red-black tree can ensure the efficiency of the query. No matter how many elements there are, the query can always be at the logarithmic level.

3. Advanced principles

Of course, brothers with higher technical requirements may continue to ask questions. At this time, it is necessary to string together all the concepts before and after. Without further explanation, let’s go directly to the code:

3.1 Handwritten logic

The logical linear expression of HashMap is not very clear. Here is a piece of handwritten logic to make the logic clearer:

  • 2 storage objects: convert between treeing and detreeting.
    • Node is used to store data in the Map and points to the next node through next reference to form a linked list.
    • TreeNode is an extension of Node and is used to form a tree structure
  • 4 processes:
    • If the position corresponding to the array subscript obtained by Hash does not have an object, put it directly into the Node object.
    • If the object exists but is less than the tree threshold, it is placed at the end of the linked list.
    • If there is an object and it is greater than the treeization threshold (8) , it is first converted into a tree linked list , and then the tree linked list is treeized.
    • If insertion or deletion triggers the release threshold(6) , the tree will be released

Two storage objects:


java

复制代码

/** * 基础节点 */ public static class Node { protected int hash; protected String key; protected String value; // 基于 next 指向下一个节点从而形成一个链表结构 protected Node next; } /** * 树节点 ,实际上继承了 Node ,意味着每个 TreeNode 实际也包含数据对象 */ public static class TreeNode extends Node { // 父节点 TreeNode parent; // 左子节点 TreeNode left; // 右子节点 TreeNode right; // 上一节点用于生成双向链表,便于树形操作 TreeNode prev; boolean red; public TreeNode(Node node) { this.key = node.key; this.value = node.value; this.next = node.next; this.hash = node.hash; } }

Core process operations:


java

复制代码

public static void main(String[] args) { // 插入一个数据 HashMapDemo mapDemo = new HashMapDemo(); mapDemo.putVal("test", "1"); } /** * HashMap 底层数组结构 , 每一个 Node 对应一个实际的对象 */ Node[] table = null; /** * 树化条件,超过该条件则进行红黑树转化 */ static final int TREEIFY_THRESHOLD = 8; /** * 解除树化条件,小于该条件则解除树化 */ static final int UNTREEIFY_THRESHOLD = 6; /** * 红黑树的操作一般都是基于插入操作来实现的 */ public void putVal(String key, String value) { int hash = getHash(key); // S0 : 先获取 Hash Node newNode = new Node(getHash(key), key, value, null); // S1 : 判断桶中是否存在元素, 如果不存在元素 ,直接添加 if (table[hash] == null) { table[hash] = newNode; return; } // S2 : 先添加到链表中,判断添加后的长度是否超过最大长度 Node currentNode = table[hash]; boolean isAdd = addNode(currentNode, newNode); // S3 : 超过最大长度,进行树化操作 if (isAdd) { treeifyBin(currentNode); } } // 模拟Hash冲突 , 只有 Hash 冲突时才会触发 private static int hash(String key) { return 10; } private static boolean addNode(Node head, Node newNode) { int length = 0; Node lastNode = head; // S2-1 :循环整个链表, 把数据加入链表最后一位 for (; lastNode != null; lastNode = lastNode.next) { length++; } lastNode.next = newNode; // S2-2 :假代码,模拟当前链表长度超过树化阈值 return length > TREEIFY_THRESHOLD; } private static void treeifyBin(Node head) { TreeNode treeNode = null; // S3-1 : 执行树华,先将节点转换为树节点 , 本质上形成的还是一个树形链表 for (Node currentNode = head; currentNode != null; currentNode = currentNode.next) { if (treeNode == null) { treeNode = new TreeNode(currentNode); } else { treeNode.next = new TreeNode(currentNode); } } // S3-2 : 执行树华逻辑 treeify(treeNode); } private static void treeify(TreeNode treeNode) { // S3-3 : 树形转换,此处略过 }

3.2 Collection of advanced questions

When will red-black trees de-tree?

  • When the number of elements in the bucket drops below a certain threshold (UNTREEIFY_THRESHOLD=6), if the node deletion is triggered again , the tree will be released.
  • When the overall capacity shrinks , the Hash modulus will change, and ( resize ) may also trigger the de-escalation at this time.

3.3 Red-black tree conversion essence

The code of the red-black tree is very interesting, but it is not easy to remember it all. Mastering the most important points is enough:

Convert tree linked list to red-black tree: treeify(Node<K,V>[] tab)

  • S1: First default the Root node to null , and then start traversing all nodes
  • S2: Extract each tree node from the tree linked list and initialize its left and right child nodes (null)
  • S3: If it is the first node, it is directly set to root. If it is not the first node, it is placed on the corresponding node side based on size comparison.
  • S4: Call the balanceInsertion method to adjust the structure of the tree
  • S5: Return the adjusted root node and move it to the array as the first entry point

image.png

The principle of balance:

This balanced piece of code is the essence of the entire red-black tree. It is enough to write an article on its own. The space is limited, so I will only talk about a few key points:

  • S1: First define the inserted node as red, and then insert it at the specified position according to the size
  • S2: Starting from the current node and returning to the root node, the node is displaced and colored in this process, usually including 2 positions and 3 situations.
    • 2 positions: Whether the parent node of the current node is a left child node or a right child node (used to determine whether it is left-handed or right-handed)
    • 3 cases: Color and position of tree nodes (siblings of the current parent node)
      • The uncle node is red (modify the color and move the current node up)
      • The uncle node is black, and x is the right child node (rotation)
      • The uncle node is black, and x is the left child node (modify the color and rotate)

If you know this process and the specific rotation method, then the code will be easy to see clearly, just like the following method

I won’t go into the details any more. I’ll wait until a more targeted article comes out later. It would be almost meaningless if I didn’t write about this myself.

java

复制代码

x.parent.red = false; // 将父节点设为黑色 y.red = false; // 将叔节点设为黑色 x.parent.parent.red = true; // 将祖父节点设为红色 x = x.parent.parent; // 将 x 上移到祖父节点 rotateRight(root, x.parent.parent); // 右旋转 rotateLeft(root, x.parent.parent); // 左旋转

Untreeing:

Reason: To save space , as can be seen above, TreeNode contains pre, next, parent and other information, which will occupy a lot of memory

Untreeing is mainly performed in the resize and removeNode logic. Resize is mainly used to control capacity. The Resize operation may usually be triggered in the following scenarios:

  • When inserting elements: When inserting data, if the threshold is triggered, resize will be triggered, including but not limited to insertion, merging, etc.
  • Direct manual call or Map initialization

To perform untreeing operation in untreeify, the processing logic is very simple:

  • S1: for loops the next attribute of the node and converts the attribute into a Node object
  • S2: Generate a linked list by connecting Node according to the Next attribute

PS: Even if it is converted to TreeNode, because TreeNode inherits Node, the next attribute is retained, and all variables can be looped through next.

Summarize

If you still get asked questions after reading this article, it’s not your fault, that’s the interviewer’s question.

Guess you like

Origin blog.csdn.net/m0_71777195/article/details/132975180