/* * esodata - data structures and other things, of varying utility * Copyright 2022, Ben Culkin * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package bjc.funcdata.bst; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.function.Predicate; import bjc.funcdata.FunctionalList; import bjc.funcdata.ListEx; /** * A binary search tree, with some mild support for functional traversal. * * @author ben * * @param * The data type stored in the node. */ public class BinarySearchTree { /* The comparator for use in ordering items */ private final Comparator comparator; /* The current count of elements in the tree */ private int elementCount; /* The root element of the tree */ private TreePart root; /** * Create a new tree using the specified way to compare elements. * * @param cmp * The thing to use for comparing elements */ public BinarySearchTree(final Comparator cmp) { if (cmp == null) throw new NullPointerException("Comparator must not be null"); elementCount = 0; comparator = cmp; } /** * Add a node to the binary search tree. * * @param element * The data to add to the binary search tree. */ public void addNode(final T element) { elementCount++; if (root == null) root = new BinarySearchTreeNode<>(element, null, null); else root.add(element, comparator); } /** * Check if an adjusted pivot falls with the bounds of a list. * * @param elements * The list to get bounds from. * * @param pivot * The pivot. * * @param pivotAdjustment * The distance from the pivot. * * @return Whether the adjusted pivot is with the list. */ private boolean adjustedPivotInBounds(final ListEx elements, final int pivot, final int pivotAdjustment) { return ((pivot - pivotAdjustment) >= 0) && ((pivot + pivotAdjustment) < elements.getSize()); } /** * Balance the tree, and remove soft-deleted nodes for free. * * Takes O(N) time, but also O(N) space. */ public void balance() { final ListEx elements = new FunctionalList<>(); /* Add each element to the list in sorted order. */ root.forEach(TreeLinearizationMethod.INORDER, element -> elements.add(element)); /* Clear the tree. */ root = null; /* Set up the pivot and adjustment for readding elements. */ final int pivot = elements.getSize() / 2; int pivotAdjustment = 0; /* Add elements until there aren't any left. */ while (adjustedPivotInBounds(elements, pivot, pivotAdjustment)) { if (root == null) { /* Create a new root element. */ root = new BinarySearchTreeNode<>(elements.getByIndex(pivot), null, null); } else { /* * Add the left and right elements in a balanced manner. */ root.add(elements.getByIndex(pivot + pivotAdjustment), comparator); root.add(elements.getByIndex(pivot - pivotAdjustment), comparator); } /* Increase the distance from the pivot. */ pivotAdjustment++; } /* Add any trailing unbalanced elements. */ if (pivot - pivotAdjustment >= 0) { root.add(elements.getByIndex(pivot - pivotAdjustment), comparator); } else if (pivot + pivotAdjustment < elements.getSize()) { root.add(elements.getByIndex(pivot + pivotAdjustment), comparator); } } /** * Soft-delete a node from the tree. * * Soft-deleted nodes stay in the tree until trim()/balance() is invoked, and * are not included in traversals/finds. * * @param element * The node to delete */ public void deleteNode(final T element) { elementCount--; root.delete(element, comparator); } /** * Get the root of the tree. * * @return The root of the tree. */ public TreePart getRoot() { return root; } /** * Check if a node is in the tree. * * @param element * The node to check the presence of for the tree.. * * @return Whether or not the node is in the tree. */ public boolean isInTree(final T element) { return root.contains(element, comparator); } /** * Traverse the tree in a specified way until the function fails. * * @param linearizationMethod * The way to linearize the tree for traversal. * * @param traversalPredicate * The function to use until it fails. */ public void traverse(final TreeLinearizationMethod linearizationMethod, final Predicate traversalPredicate) { if (linearizationMethod == null) { throw new NullPointerException("Linearization method must not be null"); } else if (traversalPredicate == null) { throw new NullPointerException("Predicate must not be nulls"); } root.forEach(linearizationMethod, traversalPredicate); } /** Remove all soft-deleted nodes from the tree. */ public void trim() { final List nodes = new ArrayList<>(elementCount); /* * Add all non-soft deleted nodes to the tree in insertion order. */ traverse(TreeLinearizationMethod.PREORDER, node -> { nodes.add(node); return true; }); /* Clear the tree. */ root = null; /* Add the nodes to the tree in the order they were inserted. */ nodes.forEach(node -> addNode(node)); } @Override public String toString() { return String.format("BinarySearchTree [elementCount=%s, root='%s']", elementCount, root); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + elementCount; result = prime * result + (root == null ? 0 : root.hashCode()); return result; } @Override public boolean equals(final Object obj) { if (this == obj) return true; if (obj == null) return false; if (!(obj instanceof BinarySearchTree)) return false; final BinarySearchTree other = (BinarySearchTree) obj; if (elementCount != other.elementCount) return false; if (root == null) { if (other.root != null) return false; } else if (!root.equals(other.root)) return false; return true; } }