Realizing Map based on binary tree is so good!

Preface

In this article, we will learn about binary trees in the future and implement the Map structure defined in the previous article through binary trees.

Introduction to Binary Trees

Although everyone knows what a binary tree is, in order to ensure the integrity of the article, here is a brief talk about what a binary tree is

Each node in the binary tree contains two pointers to its left subtree and right subtree.

Each node of the binary tree contains a Key, and the Key of each node is greater than any node in its left subtree and smaller than any node in the right subtree.

The data structure definition of the node:

class Node {
    private K key;
    private V value;
    private Node left;
    private Node right;
    private int size = 1; 

    public Node(K key, V value) {
        this.key = key;
        this.value = value;
    }
}

size Record the number of nodes in the subtree where the current node is located, calculation method:size=左子树的个数 + 1 + 右子树的个数

Realize Map based on binary tree

In the previous article "Implementing Map Based on Array or Linked List" , we defined the Map interface. In this article, we will continue to use the map interface.

public interface Map<K, V> {
    void put(K key, V value);

    V get(K key);

    void delete(K key);

    int size();

    Iterable<K> keys();

    Iterable<TreeNode> nodes();

    default boolean contains(K key) {
        return get(key) != null;
    }

    default boolean isEmpty() {
        return size() == 0;
    }
}

public interface SortedMap<K extends Comparable<K>, V> extends Map<K, V> {
    int rank(K key);

    void deleteMin();

    void deleteMax();
    
    K min();

    K max();
}

Inquire

The simplest and most direct way to find a key in a binary tree is to use recursion. Compare the key to be found with the key of the node. If it is smaller, go to the left subtree to continue the recursive search, and if it is larger, search in the right subtree. If it is equal, it means that it has been found and return value directly, if the recursion has not been found, it will return null.

Code:

@Override
public V get(K key) {
    if (Objects.isNull(key)) {
        throw new IllegalArgumentException("key can not null");
    }

    Node node = get(root, key);
    return Objects.isNull(node) ? null : node.value;
}

private Node get(Node node, K key) {
    if (Objects.isNull(node)) {
        return null;
    }
    int compare = key.compareTo(node.key);
    if (compare > 0) {
        return get(node.right, key);
    } else if (compare < 0) {
        return get(node.left, key);
    } else {
        return node;
    }
}

Query the maximum and minimum values

In the binary tree, we may often use the maximum and minimum values ​​in the query tree, including our delete operations later, so here we need to implement these two methods;

The realization of the maximum value: starting from the root node and recursively along the right subtree, until the right subtree is encountered when it is null, it ends, the node at this time is the realization of the maximum value and the minimum value: starting from the root node along the left subtree Recursion, knowing that it ends when the left subtree is null, and the node at this time is the minimum value

@Override
public K max() {
    Node max = max(root);
    return max.key;
}

protected Node min(Node node) {
    if (Objects.isNull(node.left)) {
        return node;
    }
    return min(node.left);
}

protected Node max(Node node) {
    if (Objects.isNull(node.right)) {
        return node;
    }
    return max(node.right);
}

insert

From the above implementation, we can see that the query method of the binary tree is as simple and efficient as the array binary search method in the previous article. This is an important feature of the binary tree, and the insertion of the binary tree is as simple as the query operation. Ideally, insert and query Operation time complexity is log(N)

The implementation idea of ​​the insert operation: Similar to the query operation, it is still recursive. If the key value of put is greater than the current node, it needs to go to the right subtree recursively, if it is smaller, go to the left subtree recursively, and if it is equal, update the value of the node directly . If the value is not found after the recursion, create a new node and return

private Node put(Node node, K key, V value) {
    if (Objects.isNull(node)) {
        return new Node(key, value);
    }

    int compare = key.compareTo(node.key);
    if (compare > 0) {
        node.right = put(node.right, key, value);
    } else if (compare < 0) {
        node.left = put(node.left, key, value);
    } else {
        node.value = value;
    }

    node.size = size(node.left) + 1 + size(node.right);
    return node;
}

private int size(Node node) {
    if (Objects.isNull(node)) {
        return 0;
    }
    return node.size;
}

The calculation of size has been mentioned before, the current nodesize = 左子树.size + 1 + 右子树.size

Delete maximum and minimum

The relatively troublesome operation in the binary tree is the delete operation, so let's first understand how to delete the maximum and minimum values.

Delete the minimum value: Similar to the previous implementation of finding the minimum value, follow the path on the left until the left subtree of a node is null, then the current node is the minimum value, and the right subtree of the current node is recursively Just return;

The maximum realization idea is similar

code show as below:

@Override
public void deleteMin() {
    root = deleteMin(root);
}

public Node deleteMin(Node node) {
    if (Objects.isNull(node.left)) {
        return node.right;
    }
    node.left = deleteMin(node.left);
    node.size = size(node.left) + 1 + size(node.right);
    return node;
}

@Override
public void deleteMax() {
    root = deleteMax(root);
}

public Node deleteMax(Node node) {
    if (Objects.isNull(node.right)) {
        return node.left;
    }
    node.right = deleteMax(node.right);
    node.size = size(node.left) + 1 + size(node.right);
    return node;
}

delete

We can delete a node with only one child node or no child node in a similar way; but what should we do if we need to delete a node with two nodes?

Two ideas: replace the node to be deleted with the maximum value of the left subtree, and then delete the maximum value of the left subtree; or replace the node to be deleted with the minimum value in the right subtree, and then delete the minimum value in the right subtree

step:

  1. Take the maximum value from the left subtree of the node or take the minimum value from the right subtree
  2. Replace the current node with the maximum or minimum value
  3. Call to delete the maximum value or delete the minimum value

Code

@Override
public void delete(K key) {
    root = delete(root, key);
}

private Node delete(Node node, K key) {
    if (Objects.isNull(node)) {
        return null;
    }
    int compare = key.compareTo(node.key);
    if (compare > 0) {
        node.right = delete(node.right, key);
    } else if (compare < 0) {
        node.left = delete(node.left, key);
    } else {
        if (Objects.isNull(node.left)) {
            return node.right;
        }
        if (Objects.isNull(node.right)) {
            return node.left;
        }

        Node max = max(node.left);
        node.key = max.key;
        node.value = max.value;

        node.left = deleteMax(node.left);
    }
    node.size = size(node.left) + 1 + size(node.right);
    return node;
}

analysis

The efficiency of Map running using binary trees depends on the shape of the tree, and the shape of the tree depends on the order of data input; in the best case, the binary tree is balanced, so the time complexity of get and put are both log(N); But if the inserted data is ordered, then the binary tree will evolve into a linked list, then the performance of get and put will be greatly reduced;

Based on this problem, we will continue to improve the Map we implemented. In the next article, we will learn to use red-black trees to implement our Map operations. Regardless of the order of data insertion, we can ensure that the binary tree is approximately balanced.

Reference article:

https://www.douban.com/note/797590013/

https://blog.51cto.com/11436461/2591583

https://www.jianshu.com/p/cb04acd2c881

Guess you like

Origin blog.csdn.net/qq_38082146/article/details/115086727