patternjavaMinor
Are AVL trees equal?
Viewed 0 times
areavlequaltrees
Problem
I was inspired by this answer and decided to implement an AVL tree with the methods
This is my code on GitHub.
In addition to the solution itself, I wrote tests for all the possible cases. If I missed something, or if my solution has any bugs, please let me know.
AvlTree
```
package com.bst;
import java.util.LinkedList;
public class AvlTree {
Node mRoot;
public AvlTree() {
}
public AvlTree(int root) {
mRoot = new Node(root);
}
private Node insert(Node root, int key) {
if (root == null) {
return new Node(key);
}
if (key 0) {
p.mLeft = rotateLeft(p.mLeft);
}
return rotateRight(p);
}
return p;
}
private Node rotateRight(Node p) {
Node q = p.mLeft;
p.mLeft = q.mRight;
q.mRight = p;
fixHeight(p);
fixHeight(q);
return q;
}
private Node rotateLeft(Node q) {
Node p = q.mRight;
q.mRight = p.mLeft;
p.mLeft = q;
fixHeight(q);
fixHeight(p);
return p;
}
private int height(Node p) {
return p == null ? 0 : p.mHeight;
}
private int bfactor(Node p) {
return height(p.mRight) - height(p.mLeft);
}
private void fixHeight(Node p) {
int hl = height(p.mLeft);
int hr = height(p.mRight);
p.mHeight = (hl > hr ? hl : hr) + 1;
}
public void insert(int... keys) {
for (int value : keys) {
mRoot = insert(mRoot, value);
}
}
@Override
public boolean equals(Object arg0) {
if (this == arg0) {
return true;
}
if (!(arg0 instanceof AvlTree)) {
return false;
}
AvlTree another = (AvlTree) arg0;
return areTreesEqual(this.mRoot, another.mRoot);
}
private boolean areTreesEqual(Node root1, Node root
equals and hashCode as if I was asked to do that at a job interview.This is my code on GitHub.
In addition to the solution itself, I wrote tests for all the possible cases. If I missed something, or if my solution has any bugs, please let me know.
AvlTree
```
package com.bst;
import java.util.LinkedList;
public class AvlTree {
Node mRoot;
public AvlTree() {
}
public AvlTree(int root) {
mRoot = new Node(root);
}
private Node insert(Node root, int key) {
if (root == null) {
return new Node(key);
}
if (key 0) {
p.mLeft = rotateLeft(p.mLeft);
}
return rotateRight(p);
}
return p;
}
private Node rotateRight(Node p) {
Node q = p.mLeft;
p.mLeft = q.mRight;
q.mRight = p;
fixHeight(p);
fixHeight(q);
return q;
}
private Node rotateLeft(Node q) {
Node p = q.mRight;
q.mRight = p.mLeft;
p.mLeft = q;
fixHeight(q);
fixHeight(p);
return p;
}
private int height(Node p) {
return p == null ? 0 : p.mHeight;
}
private int bfactor(Node p) {
return height(p.mRight) - height(p.mLeft);
}
private void fixHeight(Node p) {
int hl = height(p.mLeft);
int hr = height(p.mRight);
p.mHeight = (hl > hr ? hl : hr) + 1;
}
public void insert(int... keys) {
for (int value : keys) {
mRoot = insert(mRoot, value);
}
}
@Override
public boolean equals(Object arg0) {
if (this == arg0) {
return true;
}
if (!(arg0 instanceof AvlTree)) {
return false;
}
AvlTree another = (AvlTree) arg0;
return areTreesEqual(this.mRoot, another.mRoot);
}
private boolean areTreesEqual(Node root1, Node root
Solution
Testing all possible cases
In addition to the solution itself, I wrote tests for all the possible cases.
It seems you have verified all execution paths are covered.
That's great,
but it doesn't mean you have tested "all the possible cases",
far from it.
Code coverage (also known as structured basis testing) is but one of the dimensions of testing.
Another dimension is data-flow testing,
which is just as important, and can be very tricky.
Testing all possible cases is in general not feasible:
you'd have to enumerate all conceivable inputs,
which is impossible.
There are a couple of things you can do:
Consider this, for example:
What will happen:
From this we gain some interesting insights:
I suggest to add more test cases with larger test trees.
Think of node sequences that will trigger interesting rotations.
Testing hashCode
you call the other node "another".
I suggest using "other", which is just more common.
In addition to the solution itself, I wrote tests for all the possible cases.
It seems you have verified all execution paths are covered.
That's great,
but it doesn't mean you have tested "all the possible cases",
far from it.
Code coverage (also known as structured basis testing) is but one of the dimensions of testing.
Another dimension is data-flow testing,
which is just as important, and can be very tricky.
Testing all possible cases is in general not feasible:
you'd have to enumerate all conceivable inputs,
which is impossible.
There are a couple of things you can do:
- Test with "interesting" data sets
- Test boundaries, corner cases
- Test combinations of the above
Consider this, for example:
AvlTree tree = new AvlTree();
tree.insert(1, 1, 1);What will happen:
- The first 1 is set as root
- The second 1 is set as root -> right
- The second 1 is set as root -> right -> right
- The tree is rebalanced, rotating to the left, with 1 as root, and 1 as left node and 1 as right node
From this we gain some interesting insights:
- The tree implementation allows duplicates: an unusual, and potentially troublesome feature
- The outcome of the rotation is smelly: the implementation of
insertuses strictly
I suggest to add more test cases with larger test trees.
Think of node sequences that will trigger interesting rotations.
Testing hashCode
with empty tree and trees with 2 nodes is hardly sufficient.
For more details about data-flow testing,
check out Chapter 22 in Code Complete.
To allow duplicate values or not
Allowing duplicate nodes in a binary search tree is unusual,
and invites all kinds of problems.
In addition to the code smell I highlighted earlier,
you'll probably have some headaches when implementing deletion.
For example tree.insert(3, 3, 3, 3, 1, 1, 1) results in this tree:
3
/ \
1 3
/ \ \
1 3 3
/
1
Given such tree, what should happen on delete(3) ? Remove the first?
How to delete all nodes with 3? Keep removing until nothing is removed?
You could save yourself a lot of headaches by simply not allowing duplicates.
Adopting Android coding practices in plain Java code
I do understand your sentiment for linking the "m" prefix convention used in Android development.
If you want to stick with it, that's up to you,
but let me remind you of some concerns:
-
In expressions like root.mLeft, you don't need the "m" to clarify it's a member variable: root.left is perfectly clear too
-
In the few places where it might not be obvious, as in mRoot = insert(mRoot, value), you can always use this.root = ... to clarify
Looking over the code, most uses cases fall in the first category, and the second is so rare than despite "this." being longer than "m". By using the this. prefix where ambiguous and dropping the m prefix, you will get shorter, perfectly unambiguous code that won't put off any Java developers.
Food for thought ;-)
Lastly,
it seems that the "m"-prefix is required for code to be contributed to the Android Open Source Project,
and not really a style guide for the code of individual Android apps.
LinkedList as a Queue
In the implementation of hashCode,
you're using LinkedList nodes as a queue.
So I suggest to declare it as a Queue,
name the variable "queue",
and instead of the remove() method,
use the more natural poll().
Naming
I find it a bit confusing that in insert(Node root, int key) { ... } the parameter variable is named "root":
it may mislead that the function manipulates the tree root,
when in fact it's most often an intermediary node.
I suggest to rename the variable to "node" instead.
In other methods you name a Node parameter p.
I'd suggest the more intuitive node everywhere.
And where you use q I'd suggest other.
In the implementation of equals`,you call the other node "another".
I suggest using "other", which is just more common.
Code Snippets
AvlTree tree = new AvlTree();
tree.insert(1, 1, 1);3
/ \
1 3
/ \ \
1 3 3
/
1Context
StackExchange Code Review Q#101308, answer score: 5
Revisions (0)
No revisions yet.