一.简介
由于之前博主尝试Java重构redis,在redis中的的字典数据结构底层也是采用数组实现,字典中存在两个hash表,一个是用于存储数据,另一个被用于rehash扩容为前者两倍。但是我注意到了在redis的数据结构中,并没有像Java集合类中的hashmap一样的树化机制,即当链表数大于8,且通过线性搜索法查找后续hash槽,发现占用了64个了,就会将拉链上的链表转化为红黑树,涉及到自平衡等等操作。是比较麻烦的,于是博主尝试使用二叉搜索树来实现一个基础的树的存储数据的数据结构。
二.准备代码
先定义两个接口,里面是我们准备实现的一些必须的方法,
IMember 接口
这个接口的实现类是该存储数据结构中的每个对象。
package partA;
/**
*
* an objects of a class implementing this interface holds a
* database of member information
* DO NOT CHANGE THIS INTERFACE
* You must create a class that implements this interface
*
*/
public interface IMemberDB {
/**
* Empties the database.
* @pre true
*/
public void clearDB();
/**
* Determines whether a member's name exists as a key inside the database
* @pre name is not null and not empty string
* @param name the member name (key) to locate
* @return true if the name exists as a key in the database
*/
public boolean containsName(String name);
/**
* Returns a Member object mapped to the supplied name.
* @pre name not null and not empty string
* @param name The Member name (key) to locate
* @return the Member object mapped to the key name if the name
exists as key in the database, otherwise null
*/
public Member get(String name);
/**
* Returns the number of members in the database
* @pre true
* @return number of members in the database.
*/
public int size();
/**
* Determines if the database is empty or not.
* @pre true
* @return true iff the database is empty
*/
public boolean isEmpty();
/**
* Inserts a Member object into the database, with the key of the supplied
* member's name.
* Note: If the name already exists as a key, then the original entry
* is overwritten.
* This method must return the previous associated value
* if one exists, otherwise null
*
* @pre member not null and member name not empty string
*/
public Member put(Member member);
/**
* Removes and returns a member from the database, with the key
* the supplied name.
* @param name The name (key) to remove.
* @pre name not null and name not empty string
* @return the removed member object mapped to the name, or null if
* the name does not exist.
*/
public Member remove(String name);
/**
* Prints the names and affiliations of all the members in the database in
* alphabetic order.
* @pre true
*/
public void displayDB();
}
IMemberDB 接口
该接口就是定义一些 crud方法
package partA;
/**
*
* an objects of a class implementing this interface holds a
* database of member information
* DO NOT CHANGE THIS INTERFACE
* You must create a class that implements this interface
*
*/
public interface IMemberDB {
/**
* Empties the database.
* @pre true
*/
public void clearDB();
/**
* Determines whether a member's name exists as a key inside the database
* @pre name is not null and not empty string
* @param name the member name (key) to locate
* @return true if the name exists as a key in the database
*/
public boolean containsName(String name);
/**
* Returns a Member object mapped to the supplied name.
* @pre name not null and not empty string
* @param name The Member name (key) to locate
* @return the Member object mapped to the key name if the name
exists as key in the database, otherwise null
*/
public Member get(String name);
/**
* Returns the number of members in the database
* @pre true
* @return number of members in the database.
*/
public int size();
/**
* Determines if the database is empty or not.
* @pre true
* @return true iff the database is empty
*/
public boolean isEmpty();
/**
* Inserts a Member object into the database, with the key of the supplied
* member's name.
* Note: If the name already exists as a key, then the original entry
* is overwritten.
* This method must return the previous associated value
* if one exists, otherwise null
*
* @pre member not null and member name not empty string
*/
public Member put(Member member);
/**
* Removes and returns a member from the database, with the key
* the supplied name.
* @param name The name (key) to remove.
* @pre name not null and name not empty string
* @return the removed member object mapped to the name, or null if
* the name does not exist.
*/
public Member remove(String name);
/**
* Prints the names and affiliations of all the members in the database in
* alphabetic order.
* @pre true
*/
public void displayDB();
}
三.实现类
package partA;
import java.util.Objects;
public class Member implements IMember{
String fullName;
String affiliation;
public Member(String name, String affiliation){
this.fullName = name;
this.affiliation = affiliation;
}
public String getName() {
return fullName;
}
public String getAffiliation() {
return affiliation;
}
public void setAffiliation(String affiliation) {
this.affiliation = affiliation;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Member member = (Member) o;
return Objects.equals(fullName, member.fullName) && Objects.equals(affiliation, member.affiliation);
}
@Override
public int hashCode() {
return Objects.hash(fullName, affiliation);
}
}
package partA;
import java.util.ArrayList;
import java.util.List;
public class MemberBST implements IMemberDB{
private Node root;
private static class Node {
Member member;
String name;
Node left, right;
Node(Member member) {
this.member = member;
this.name = member.getName();
left = right = null;
}
}
// constructor
public MemberBST() {
System.out.println("Binary Search Tree");
this.root = null;
}
@Override
public void clearDB() {
root = null;
}
@Override
public boolean containsName(String name) {
return recursionTree(root, name);
}
// if two string is same,compare() method will return 0
private boolean recursionTree(Node node, String name) {
if (node == null) return false;
if (node.name.compareTo(name) == 0) return true;
return recursionTree(node.left, name) || recursionTree(node.right, name);
}
@Override
public Member get(String name) {
List<String> sequence = new ArrayList<>();
Member result = recursionGetter(root, name, sequence);
if (result != null) {
System.out.println("the sequence of nodes of the tree visited: " + sequence);
}
return result;
}
private Member recursionGetter(Node node, String name, List<String> sequence) {
if (node == null){
return null;
}
sequence.add(node.member.getName()); // Add the node to the sequence
if (node.name.compareTo(name) == 0) {
System.out.println("the Member name: " + node.member.getName());
return node.member;
}
// recursion
Member leftResult = recursionGetter(node.left, name, sequence);
if (leftResult != null) {
return leftResult;
}
return recursionGetter(node.right, name, sequence);
}
@Override
public int size() {
int cnt = 0;
return calculator(root, cnt);
}
private int calculator(Node node, int cnt) {
if (node == null) return cnt;
cnt++;
cnt = calculator(node.left, cnt);
cnt = calculator(node.right, cnt);
return cnt;
}
@Override
public boolean isEmpty() {
return root == null;
}
@Override
public Member put(Member member) {
if ((member == null) || member.getName().isEmpty()) {
throw new IllegalArgumentException("member and " +
"member's name can not be null");
}
// exist?
Member meb = get(member.getName());
if (meb != null) {
meb.setAffiliation(member.affiliation);
System.out.println("the Member name: " + meb.getName());
return meb;
}
if (isEmpty()) {
root = new Node(member);
System.out.println("Visited node: " + root.member.getName());
} else {
recursionAdder(root, member);
}
return member;
}
private void recursionAdder(Node node, Member member) {
int gap = member.getName().compareTo(node.name);
if (gap < 0) {
// The new member's name comes before the current node's name, go left
if (node.left == null) {
node.left = new Node(member);
System.out.println("Visited node: " + node.member.getName());
} else {
recursionAdder(node.left, member);
}
} else if (gap > 0) {
// The new member's name comes after the current node's name, go right
if (node.right == null) {
node.right = new Node(member);
System.out.println("Visited node: " + node.member.getName());
} else {
recursionAdder(node.right, member);
}
}
}
@Override
public Member remove(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Name cannot be null or an empty string.");
}
MemberWrapper removedMemberWrapper = new MemberWrapper();
root = removeRecursive(root, name, removedMemberWrapper);
System.out.println("Removed member: " + removedMemberWrapper.member.getName());
return removedMemberWrapper.member;
}
private Node removeRecursive(Node node, String name, MemberWrapper removedMemberWrapper) {
if (node == null) {
return null;
}
int cmp = name.compareTo(node.name);
if (cmp < 0) {
System.out.println("Visited node: " + node.member.getName());
node.left = removeRecursive(node.left, name, removedMemberWrapper);
} else if (cmp > 0) {
System.out.println("Visited node: " + node.member.getName());
node.right = removeRecursive(node.right, name, removedMemberWrapper);
} else {
// Found the node to remove
System.out.println("Removing node: " + node.member.getName());
removedMemberWrapper.member = node.member;
if (node.left == null) {
return node.right;
} else if (node.right == null) {
return node.left;
}
// Node has two children, find the inorder successor
node.member = findMin(node.right).member;
node.name = findMin(node.right).name;
node.right = removeRecursive(node.right, node.name, null);
}
return node;
}
private Node findMin(Node node) {
while (node.left != null) {
System.out.println("Visited node: " + node.member.getName());
node = node.left;
}
return node;
}
@Override
public void displayDB() {
disRecDB(root);
}
private void disRecDB(Node node) {
if (node == null) return;
String name = node.name;
String affiliation = node.member.affiliation;
System.out.println("fullName:\t" + name + "\t" + affiliation);
disRecDB(node.left);
disRecDB(node.right);
}
}
包装类:用于值传递
package partA;
public class MemberWrapper {
Member member;
}
四.解释及测试类
APIs
First we need to build the inner class of the node and define the root node.
private Node root;
private static class Node {
Member member;
String name;
Node left, right;
Node(Member member) {
this.member = member;
this.name = member.getName();
left = right = null;
}
}
And then in the constructor, it says "Binary Search Tree"
// constructor
public MemberBST() {
System.out.println("Binary Search Tree");
this.root = null;
}
Next we will implement all the apis defined in the MemberBST class inheritance interface:
- clearDB()
public void clearDB() {
root = null;
}
Emptying a tree simply requires setting its root node to 0, and the jvm's gc automatically frees up memory.
- containsName()
public boolean containsName(String name) {
return recursionTree(root, name);
}
// if two string is same,compare() method will return 0
private boolean recursionTree(Node node, String name) {
if (node == null) return false;
if (node.name.compareTo(name) == 0) return true;
return recursionTree(node.left, name) || recursionTree(node.right, name);
}
This is done using a simple depth-first traversal in the middle order traversal.
- get()
@Override
public Member get(String name) {
List<String> sequence = new ArrayList<>();
Member result = recursionGetter(root, name, sequence);
if (result != null) {
System.out.println("the sequence of nodes of the tree visited: " + sequence);
}
return result;
}
private Member recursionGetter(Node node, String name, List<String> sequence) {
if (node == null){
return null;
}
sequence.add(node.member.getName()); // Add the node to the sequence
if (node.name.compareTo(name) == 0) {
System.out.println("the Member name: " + node.member.getName());
return node.member;
}
// recursion
Member leftResult = recursionGetter(node.left, name, sequence);
if (leftResult != null) {
return leftResult;
}
return recursionGetter(node.right, name, sequence);
}
Since I need to return the order of access in task3, I create a variable-length array to store the locations of the accessed nodes and add them to the dynamic array whenever a node is accessed.
We also use recursion to implement the depth-first algorithm's mid-order pass to get the member of the specified name. Comparing two strings we use the compareTo() method, which converts the string to a char array and then determines whether the two strings are equal by adding the asc code. If 0 is equal, 1 means that the former is greater, and 2 means that the latter is greater.
- size()
public int size() {
int cnt = 0;
return calculator(root, cnt);
}
private int calculator(Node node, int cnt) {
if (node == null) return cnt;
cnt++;
cnt = calculator(node.left, cnt);
cnt = calculator(node.right, cnt);
return cnt;
}
dfs is also implemented using recursion. By defining a cnt counter to record the number of nodes and objects.
isEmpty()
@Override
public boolean isEmpty() {
return root == null;
}
Very simple no introduction.
put()
public Member put(Member member) {
if ((member == null) || member.getName().isEmpty()) {
throw new IllegalArgumentException("member and " +
"member's name can not be null");
}
// exist?
Member meb = get(member.getName());
if (meb != null) {
meb.setAffiliation(member.affiliation);
System.out.println("the Member name: " + meb.getName());
return meb;
}
if (isEmpty()) {
root = new Node(member);
System.out.println("Visited node: " + root.member.getName());
} else {
recursionAdder(root, member);
}
return member;
}
private void recursionAdder(Node node, Member member) {
int gap = member.getName().compareTo(node.name);
if (gap < 0) {
// The new member's name comes before the current node's name, go left
if (node.left == null) {
node.left = new Node(member);
System.out.println("Visited node: " + node.member.getName());
} else {
recursionAdder(node.left, member);
}
} else if (gap > 0) {
// The new member's name comes after the current node's name, go right
if (node.right == null) {
node.right = new Node(member);
System.out.println("Visited node: " + node.member.getName());
} else {
recursionAdder(node.right, member);
}
}
}
First we need to determine if a member of that name exists in the tree. Now add setAffiliation() to the Member class. Because we need to modify the operation, we need to get the member object so we can use the GET method here. If it exists, call setAffiliation() and modify the Affiliation corresponding to name.
If it is empty, create a new node to store the new member.
If it does not exist, we call the auxiliary method recursionAdder() to determine whether a member is placed in the left or right subtree by compartTo's gap.
desplyDB()
public void displayDB() {
disRecDB(root);
}
private void disRecDB(Node node) {
if (node == null) return;
String name = node.name;
String affiliation = node.member.affiliation;
System.out.println("fullName:\t" + name + "\t" + affiliation);
disRecDB(node.left);
disRecDB(node.right);
}
Recursive depth traversal, just print.
remove()
public Member remove(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Name cannot be null or an empty string.");
}
MemberWrapper removedMemberWrapper = new MemberWrapper();
root = removeRecursive(root, name, removedMemberWrapper);
System.out.println("Removed member: " + removedMemberWrapper.member.getName());
return removedMemberWrapper.member;
}
private Node removeRecursive(Node node, String name, MemberWrapper removedMemberWrapper) {
if (node == null) {
return null;
}
int cmp = name.compareTo(node.name);
if (cmp < 0) {
System.out.println("Visited node: " + node.member.getName());
node.left = removeRecursive(node.left, name, removedMemberWrapper);
} else if (cmp > 0) {
System.out.println("Visited node: " + node.member.getName());
node.right = removeRecursive(node.right, name, removedMemberWrapper);
} else {
// Found the node to remove
System.out.println("Removing node: " + node.member.getName());
removedMemberWrapper.member = node.member;
if (node.left == null) {
return node.right;
} else if (node.right == null) {
return node.left;
}
// Node has two children, find the inorder successor
node.member = findMin(node.right).member;
node.name = findMin(node.right).name;
node.right = removeRecursive(node.right, node.name, null);
}
return node;
}
private Node findMin(Node node) {
while (node.left != null) {
System.out.println("Visited node: " + node.member.getName());
node = node.left;
}
return node;
}
To implement value passing, we first create a wrapper class to hold the deleted member:
public class MemberWrapper {
Member member;
}
The member with the specified name is then recursively found and deleted. If the current node is empty, it indicates that the subtree has been traversed and the target member is not found, and null is returned. If the node has no right subtree, simply return its left subtree as the new subtree (or null if there is no left subtree). Compare the name of the current node member with the name of the target, and decide whether the left subtree or the right subtree recursion based on the comparison results. If the node has no left subtree, simply return its right subtree as the new subtree (or null if there is no right subtree), for a node with two children, find the smallest node in the right subtree (via the findMin method), replace the current node with this node, and then recursively remove the smallest node.
Testcode
Download Junit for unit testing using Maven.
(In Java, we generally do not use assertion types directly in our code, we use more if conditions, and assertion types are required to be used in test classes as much as possible.)
/*
Insertion structure:
Alice
/ \
Adam Bob
/ \
Barbara Charlie
/ \
null null
*/
private MemberBST memberBST;
private Member member1;
private Member member2;
private Member member3;
private Member member4;
@Before
public void setUp() {
memberBST = new MemberBST();
member1 = new Member("Alice", "Company A");
member2 = new Member("Bob", "Company B");
member3 = new Member("Charlie", "Company C");
member4 = new Member("Adam", "Company D");
memberBST.put(member1);
memberBST.put(member2);
memberBST.put(member3);
memberBST.put(member4);
}
First, insert the test data. See comments for the structure.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Member member = (Member) o;
return Objects.equals(fullName, member.fullName) && Objects.equals(affiliation, member.affiliation);
}
@Override
public int hashCode() {
return Objects.hash(fullName, affiliation);
}
Note that we need to override the equals and hashcode methods in the Member class because we need a time value comparison rather than a memory address comparison.
The unit test code is as follows:
Then we can allow the main use case to be given in actual code:
Display:
Apparently it's allowed.
Analyse
- put
- Average case: O(log n), because each insertion attempts to place a new node in the proper place in the tree so that the tree is balanced.
- Best case: O(1), when the tree is empty or a new node is always inserted as the last node (this can happen with ordered inserts, but is uncommon).
- Worst case: O(n), if the tree is extremely unbalanced (degenerates into a linked list), each insertion may require traversing the entire tree.
- get & containsName
- Average case: O(log n), similar to insertion because the search follows a path from root to leaf.
- Best case: O(1), if the target node happens to be the root node.
- Worst case: O(n), when the tree degenerates into a linked list.
- Remove
- Average case: O(log n), the node needs to be found, and then three cases (no child, one child, two children) are processed, most of the operations are concentrated in a small part of the tree.
- Best case: O(1), delete the leaf node.
- Worst case: O(n), also when the tree is reduced to a linked list, it takes linear time to find the node.
- size & displayDB
The recursive implementation results in a time complexity of O(n) because each node needs to be traversed