Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
192: Implementing a Binary Search Tree in Java
I've seen a lot of developers struggle with their first Binary Search Tree (BST) implementation. Usually, it's not the logic of "left is smaller, right is larger" that trips them up—it's the way Java handles object references during recursion. Let's look at a piece of code that looks perfectly logical at first glance, but fails miserably in practice.
public class BinarySearchTree {
class Node {
int value;
Node left, right;
Node(int value) { this.value = value; }
}
Node root;
public void insert(int value) {
root = insertRecursive(root, value);
}
private Node insertRecursive(Node current, int value) {
if (current == null) {
return new Node(value);
}
if (value < current.value) {
insertRecursive(current.left, value); // Look closely here
} else if (value > current.value) {
insertRecursive(current.right, value); // And here
}
return current;
}
}
Why Your Tree Only Ever Has One Node
If you ran this code and tried to insert 10, 5, and 15, you'd find that your tree only contains the number 10. You'd be scratching your head because the insertRecursive method is clearly being called and it's clearly returning a new Node(value). So where is it going?
Here is the problem: in Java, when you pass an object reference (like current.left) into a method, you are passing a copy of that reference. When the recursive call reaches the base case and returns a new Node, that new Node is just floating in space. It isn't actually attached to the parent node because you never assigned the result of the method call to the parent's child pointer.
Wiring the Tree Together
To fix this, we have to ensure that the return value of the recursive call is actually assigned back to the node's child. I personally find this pattern much cleaner than trying to track a "parent" pointer manually, which often leads to messy null checks.
private Node insertRecursive(Node current, int value) {
if (current == null) {
return new Node(value);
}
if (value < current.value) {
// We assign the result back to current.left
current.left = insertRecursive(current.left, value);
} else if (value > current.value) {
// We assign the result back to current.right
current.right = insertRecursive(current.right, value);
}
return current; // Return the (possibly updated) current node
}
Now, when the recursion hits the bottom, the new Node is returned to the previous caller, which then assigns it to current.left or current.right. This "bubbles up" the updated structure back to the root. It's a elegant way to handle tree mutations.
Navigating the Left and Right Branches
Once you've got the insertion working, searching is where the BST really shines. Since we've maintained the property that everything to the left is smaller and everything to the right is larger, we don't have to look at every node—we can discard half the tree with every single comparison. I always recommend writing the search method recursively first; it mirrors the structure of the tree itself, making the code much easier to reason about.
public boolean contains(int value) {
return containsRecursive(root, value);
}
private boolean containsRecursive(Node current, int value) {
if (current == null) {
return false; // We hit a leaf and didn't find it
}
if (value == current.value) {
return true; // Found it!
}
return value < current.value
? containsRecursive(current.left, value)
: containsRecursive(current.right, value);
}
Maintaining the BST Invariant
The most important thing to remember as you build more complex operations (like deletion, which is a whole other beast) is the "BST Invariant." This is the rule that for any given node, all nodes in its left subtree must be smaller, and all nodes in its right subtree must be larger. If you ever write a method that moves nodes around and you accidentally break this rule, your contains method will stop working, even though the nodes are still technically in the tree. Always test your BST by performing an in-order traversal; if the resulting list isn't sorted, your invariant is broken.
📋 Practical Task
Implementing a Minimum Value Finder for the BST
In a Binary Search Tree, the smallest element is always located at the leftmost node of the tree. Your task is to extend the BinarySearchTree class by implementing a method called findMin().
Requirements:
- Create a public method
public Integer findMin(). - If the tree is empty (root is null), return
null. - Otherwise, traverse the tree to the leftmost leaf and return that node's value.
- Implement this using a
whileloop rather than recursion to practice iterative tree traversal.
Starter Code:
public class BinarySearchTree {
class Node {
int value;
Node left, right;
Node(int value) { this.value = value; }
}
Node root;
// ... existing insert method here ...
public Integer findMin() {
// Your implementation here
}
}There are no comments for now.