数据结构 - 二叉查询树的Java实现
关于树及二叉树的概念和性质,请参考我的这篇文章:https://blog.csdn.net/funnyrand/article/details/81662602
本节将介绍二叉查询树(又称二叉排序树,二叉搜索树,二叉查找树)的基本原理和相关操作,相关的分析和算法都基于Java实现。
一、代码
先定义两个接口,IBinaryTreeNode 和 IBinaryTree,分别表示二叉树的结点类型和二叉树的类型。
package com.my.study.algorithm.tree.bstree;
/**
* Binary tree node interface.
*
* @param <E>
* Node type
*/
public interface IBinaryTreeNode<E extends Comparable<E>> {
/**
* Get left node.
*
* @return left node
*/
IBinaryTreeNode<E> getLeftNode();
/**
* Get right node.
*
* @return right node
*/
IBinaryTreeNode<E> getRightNode();
/**
* Get parent node.
*
* @return parent node
*/
IBinaryTreeNode<E> getParentNode();
/**
* Set left node.
*
* @param leftNode
* left node
*/
void setLeftNode(IBinaryTreeNode<E> leftNode);
/**
* Set right node.
*
* @param rightNode
* right node
*/
void setRightNode(IBinaryTreeNode<E> rightNode);
/**
* Set parent node.
*
* @param parentNode
* parent node
*/
void setParentNode(IBinaryTreeNode<E> parentNode);
/**
* Get value.
*
* @return node value
*/
E getValue();
/**
* Set node value.
*
* @param value
* node value
*/
void setValue(E value);
/**
* Check if current node is left node.
*
* @return true: left node, false: not left node
*/
boolean isLeft();
/**
* Check if current node is right node.
*
* @return true: right node, false: not right node
*/
boolean isRight();
/**
* Set current node is the left node.
*/
void setLeft();
/**
* set current node is the right node.
*/
void setRight();
/**
* Get current node's height.
*
* @return height
*/
int getHeight();
/**
* Set current node's height.
*
* @param height height
*/
void setHeight(int height);
}
package com.my.study.algorithm.tree.bstree;
import java.util.List;
/**
* Binary tree interface.
*
* @param <E>
* Node type
*/
public interface IBinaryTree<E extends Comparable<E>> {
/**
* Get root node
*
* @return root node
*/
IBinaryTreeNode<E> getRoot();
/**
* Find element from root node.
*
* @param e
* element
* @return null if cannot find specified element or object IBinaryTreeNode<E> if
* find it
*/
IBinaryTreeNode<E> find(E e);
/**
* Find element from a specified node.
*
* @param e
* element
* @return null if cannot find specified element or object IBinaryTreeNode<E> if
* find it
*/
IBinaryTreeNode<E> find(IBinaryTreeNode<E> startNode, E e);
/**
* Insert an element under root node.
*
* @param e
* element
*
* @return IBinaryTreeNode<E> object
*/
IBinaryTreeNode<E> insert(E e);
/**
* Remove an node under root node.
*
* @param e
* element
* @return null if cannot find e, or IBinaryTreeNode<E> if remove successfully
*/
IBinaryTreeNode<E> remove(E e);
/**
* Find previous node by a specified node.
*
* @param startNode
* start node
* @return null if cannot find previous node or an IBinaryTreeNode<E> object
*/
IBinaryTreeNode<E> findPre(IBinaryTreeNode<E> startNode);
/**
* Find next node by a specified node.
*
* @param startNode
* start node
* @return null if cannot find next node or an IBinaryTreeNode<E> object
*/
IBinaryTreeNode<E> findNext(IBinaryTreeNode<E> startNode);
/**
* Preorder traversal from root node.
*
* @return List<E> object.
*/
List<E> preorderTraversal();
/**
* Preorder traversal from a specified node.
*
* @param startNode
* start node
* @return List<E> object
*/
List<E> prerderTraversal(IBinaryTreeNode<E> startNode);
/**
* Inorder traversal from root node.
*
* @return List<E> object.
*/
List<E> inorderTraversal();
/**
* Inorder traversal from a specified node.
*
* @param startNode
* start node
* @return List<E> object
*/
List<E> inorderTraversal(IBinaryTreeNode<E> startNode);
/**
* Postorder traversal from root node.
*
* @return List<E> object
*/
List<E> postorderTraversal();
/**
* Postorder traversal from a specified node.
*
* @param startNode
* start node
* @return List<E> object
*/
List<E> postorderTraversal(IBinaryTreeNode<E> startNode);
/**
* Get size of a specified node.
*
* @param startNode
* start node
* @return node size
*/
int getSize(IBinaryTreeNode<E> startNode);
/**
* Get tree size.
*
* @return tree size
*/
int getSize();
}
接着编写二叉查询树结点的实现类和二叉查询树的实现类,BinaryTreeNode 和 BinarySearchTree。
package com.my.study.algorithm.tree.bstree;
/**
* Binary tree node.
*
* @param <E>
* Element type
*/
public class BinaryTreeNode<E extends Comparable<E>> implements IBinaryTreeNode<E> {
// Value
private E value;
// Parent node reference
private IBinaryTreeNode<E> parent;
// Left node reference
private IBinaryTreeNode<E> left;
// Right node reference
private IBinaryTreeNode<E> right;
// Height
private int height;
// Node location, NODE_LOCATION_RIGHT: left node, 0, unknown(root node),
// NODE_LOCATION_RIGHT: right node
private int nodeLocation;
private static final int NODE_LOCATION_LEFT = -1;
private static final int NODE_LOCATION_RIGHT = 0;
public BinaryTreeNode(E value) {
this.value = value;
}
@Override
public IBinaryTreeNode<E> getLeftNode() {
return left;
}
@Override
public IBinaryTreeNode<E> getRightNode() {
return right;
}
@Override
public IBinaryTreeNode<E> getParentNode() {
return parent;
}
@Override
public void setLeftNode(IBinaryTreeNode<E> leftNode) {
this.left = leftNode;
if (this.left != null) {
this.left.setParentNode(this);
leftNode.setLeft();
}
}
@Override
public void setRightNode(IBinaryTreeNode<E> rightNode) {
this.right = rightNode;
if (this.right != null) {
this.right.setParentNode(this);
rightNode.setRight();
}
}
@Override
public void setParentNode(IBinaryTreeNode<E> parentNode) {
this.parent = parentNode;
}
@Override
public E getValue() {
return value;
}
@Override
public void setValue(E value) {
this.value = value;
}
@Override
public boolean isLeft() {
return nodeLocation == NODE_LOCATION_LEFT;
}
@Override
public boolean isRight() {
return nodeLocation == NODE_LOCATION_RIGHT;
}
@Override
public void setLeft() {
nodeLocation = NODE_LOCATION_LEFT;
}
@Override
public void setRight() {
nodeLocation = NODE_LOCATION_RIGHT;
}
@Override
public int getHeight() {
return height;
}
@Override
public void setHeight(int height) {
this.height = height;
}
}
package com.my.study.algorithm.tree.bstree;
import java.util.ArrayList;
import java.util.List;
/**
* This class demonstrates how to generate a binary search tree, and its related
* operations, such as insert, remove, search, traversal, height, size, etc.
*
* @param <E>
* element type
*/
public class BinarySearchTree<E extends Comparable<E>> implements IBinaryTree<E> {
// Root node
protected IBinaryTreeNode<E> root;
@Override
public IBinaryTreeNode<E> getRoot() {
return root;
}
/**
* Empty constructor, will generate a empty tree.
*/
public BinarySearchTree() {
}
/**
* Generate binary search tree by root node.
*
* @param root
* root node.
*/
public BinarySearchTree(IBinaryTreeNode<E> root) {
root.setParentNode(null);
this.root = root;
}
/**
* Constructor method, generate tree by array.
*
* @param data
* array data
*/
public BinarySearchTree(E[] data) {
if (data == null || data.length == 0) {
throw new IllegalArgumentException("Parameter data cannot be null or empty.");
}
generateBSTree(data);
}
@Override
public IBinaryTreeNode<E> find(E e) {
IBinaryTreeNode<E> node = search(root, e);
if (node != null && node.getValue().compareTo(e) == 0) {
return node;
}
return null;
}
@Override
public IBinaryTreeNode<E> find(IBinaryTreeNode<E> startNode, E e) {
if (startNode == null) {
return null;
}
IBinaryTreeNode<E> node = search(startNode, e);
if (node != null && node.getValue().compareTo(e) == 0) {
return node;
}
return null;
}
@Override
public IBinaryTreeNode<E> insert(E e) {
IBinaryTreeNode<E> newNode = new BinaryTreeNode<E>(e);
// Empty tree, then create root node.
if (root == null) {
root = newNode;
} else {
insert(root, newNode);
}
reHeight(newNode);
return newNode;
}
@Override
public IBinaryTreeNode<E> remove(E e) {
// Just return null if cannot find e
IBinaryTreeNode<E> node = find(root, e);
if (node == null) {
return null;
}
IBinaryTreeNode<E> returnedNode = remove(root, node);
reHeight(returnedNode);
return returnedNode;
}
@Override
public IBinaryTreeNode<E> findPre(IBinaryTreeNode<E> startNode) {
if (startNode == null) {
return null;
}
// Previous node exists on the right side in the left sub tree of specified node
IBinaryTreeNode<E> selectedNode = startNode.getLeftNode();
if (selectedNode == null) {
return null;
}
while (selectedNode.getRightNode() != null) {
selectedNode = selectedNode.getRightNode();
}
return selectedNode;
}
@Override
public IBinaryTreeNode<E> findNext(IBinaryTreeNode<E> startNode) {
if (startNode == null) {
return null;
}
// Next node exists on the left side in the right sub tree of specified node
IBinaryTreeNode<E> selectedNode = startNode.getRightNode();
if (selectedNode == null) {
return null;
}
while (selectedNode.getLeftNode() != null) {
selectedNode = selectedNode.getLeftNode();
}
return selectedNode;
}
@Override
public List<E> preorderTraversal() {
return prerderTraversal(root);
}
@Override
public List<E> prerderTraversal(IBinaryTreeNode<E> startNode) {
List<E> resultList = new ArrayList<>();
if (startNode == null) {
return resultList;
}
// Parent->Left->Right
resultList.add(startNode.getValue());
resultList.addAll(prerderTraversal(startNode.getLeftNode()));
resultList.addAll(prerderTraversal(startNode.getRightNode()));
return resultList;
}
@Override
public List<E> inorderTraversal() {
return inorderTraversal(root);
}
@Override
public List<E> inorderTraversal(IBinaryTreeNode<E> startNode) {
List<E> resultList = new ArrayList<>();
if (startNode == null) {
return resultList;
}
// Left->Parent->Right
resultList.addAll(inorderTraversal(startNode.getLeftNode()));
resultList.add(startNode.getValue());
resultList.addAll(inorderTraversal(startNode.getRightNode()));
return resultList;
}
@Override
public List<E> postorderTraversal() {
return postorderTraversal(root);
}
@Override
public List<E> postorderTraversal(IBinaryTreeNode<E> startNode) {
List<E> resultList = new ArrayList<>();
if (startNode == null) {
return resultList;
}
// Left->Right->Parent
resultList.addAll(postorderTraversal(startNode.getLeftNode()));
resultList.addAll(postorderTraversal(startNode.getRightNode()));
resultList.add(startNode.getValue());
return resultList;
}
@Override
public int getSize(IBinaryTreeNode<E> startNode) {
int size = 0;
if (startNode == null) {
return 0;
}
// Size equals left_size + 1(self) + right_size
size = getSize(startNode.getLeftNode()) + 1 + getSize(startNode.getRightNode());
return size;
}
@Override
public int getSize() {
return getSize(root);
}
/**
* Generate binary search tree by array.
*
* @param data
* Array data
*/
protected void generateBSTree(E[] data) {
for (int i = 0; i < data.length; i++) {
insert(data[i]);
}
}
/**
* Search node by specified element. This is an very import method of binary
* search tree, this method will return a node which value equals to e or the
* value is the most similar to e. This method provide a way to find a position
* to insert element e.
*
* @param startNode
* start node
*
* @param e
* element
*
* @return null if tree is empty or a node object which value equals to e or the
* value is the most similar to e.
*/
protected IBinaryTreeNode<E> search(IBinaryTreeNode<E> startNode, E e) {
if (root == null) {
return null;
}
IBinaryTreeNode<E> returnedNode = startNode;
// Equals then return
if (startNode.getValue().compareTo(e) == 0) {
return returnedNode;
// Search from left node
} else if (startNode.getValue().compareTo(e) > 0) {
if (startNode.getLeftNode() != null) {
returnedNode = search(startNode.getLeftNode(), e);
}
// Search from right node
} else {
if (startNode.getRightNode() != null) {
returnedNode = search(startNode.getRightNode(), e);
}
}
return returnedNode;
}
/**
* Insert an element under a specified node.
*
* @param startNode
* Start node
*
* @param newNode
* new node to insert
*/
protected void insert(IBinaryTreeNode<E> startNode, IBinaryTreeNode<E> newNode) {
if (startNode == null) {
return;
}
IBinaryTreeNode<E> seledtedNode = startNode;
while (true) {
seledtedNode = search(seledtedNode, newNode.getValue());
// e is lesser than searched node, so need to create a new left leaf node
if (seledtedNode.getValue().compareTo(newNode.getValue()) > 0) {
seledtedNode.setLeftNode(newNode);
break;
// e is greater than searched node, so need to create a new right leaf node
} else if (seledtedNode.getValue().compareTo(newNode.getValue()) < 0) {
seledtedNode.setRightNode(newNode);
break;
}
// e equals to selected node, there are 3 scenarios
if (seledtedNode.getLeftNode() == null) {
seledtedNode.setLeftNode(newNode);
break;
} else if (seledtedNode.getRightNode() == null) {
seledtedNode.setRightNode(newNode);
break;
} else {
// continue to search a node which value is e
seledtedNode = seledtedNode.getLeftNode();
}
}
}
/**
* Remove an element under a specified node.
*
* @param startNode
* start node.
*
* @param e
* element
*
* @return null if cannot find e, or IBinaryTreeNode<E> if remove successfully
*/
protected IBinaryTreeNode<E> remove(IBinaryTreeNode<E> startNode, IBinaryTreeNode<E> removedNode) {
// When left node is null, remove this node and use its right node to instead
if (removedNode.getLeftNode() == null) {
if (root == removedNode) {
root = removedNode.getRightNode();
// For last node, root is null
if (root != null) {
root.setParentNode(null);
}
removedNode.setRightNode(null);
} else {
if (removedNode.isLeft()) {
removedNode.getParentNode().setLeftNode(removedNode.getRightNode());
} else if (removedNode.isRight()) {
removedNode.getParentNode().setRightNode(removedNode.getRightNode());
}
}
IBinaryTreeNode<E> parentNode = removedNode.getParentNode();
removedNode.setParentNode(null);
removedNode.setRightNode(null);
return parentNode;
}
// When left node is not null, find its previous node
IBinaryTreeNode<E> preNode = findPre(removedNode);
// Change node value
removedNode.setValue(preNode.getValue());
// Remove previous node and use its left node to instead.
// At this time, preNode has no right node, so no need to check right node
if (preNode.isLeft()) {
preNode.getParentNode().setLeftNode(preNode.getLeftNode());
} else if (preNode.isRight()) {
preNode.getParentNode().setRightNode(preNode.getLeftNode());
}
IBinaryTreeNode<E> parentNode = preNode.getParentNode();
preNode.setParentNode(null);
preNode.setLeftNode(null);
return parentNode;
}
/**
* Get height.
*
* @param startNode
* IBinaryTreeNode<E>
*
* @return 0 if startNode is null, else return height
*/
protected int getHeight(IBinaryTreeNode<E> startNode) {
if (startNode == null) {
return 0;
}
return startNode.getHeight();
}
/**
* Re-height of a specified node.
*
* @param startNode
* IBinaryTreeNode<E>
*/
protected void reHeight(IBinaryTreeNode<E> startNode) {
if (startNode != null) {
int height = Math.max(getHeight(startNode.getLeftNode()), getHeight(startNode.getRightNode())) + 1;
startNode.setHeight(height);
reHeight(startNode.getParentNode());
}
}
}
为了打印出二叉树的结构,我们编写了一个工具类,BinaryTreePrinter。
package com.my.study.algorithm.tree.bstree;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Print structure of binary tree.
*/
public class BinaryTreePrinter {
/**
* Print tree structure.
*
* @param tree IBinaryTree<E>
*/
public static <E extends Comparable<E>> void printTree(IBinaryTree<E> tree) {
IBinaryTreeNode<E> root = tree.getRoot();
int maxLevel = BinaryTreePrinter.maxLevel(root);
printNodeInternal(Collections.singletonList(root), 1, maxLevel);
}
private static <E extends Comparable<E>> void printNodeInternal(List<IBinaryTreeNode<E>> nodes, int level,
int maxLevel) {
if (nodes.isEmpty() || BinaryTreePrinter.isAllElementsNull(nodes))
return;
int floor = maxLevel - level;
int endgeLines = (int) Math.pow(2, (Math.max(floor - 1, 0)));
int firstSpaces = (int) Math.pow(2, (floor)) - 1;
int betweenSpaces = (int) Math.pow(2, (floor + 1));
BinaryTreePrinter.printWhitespaces(firstSpaces);
List<IBinaryTreeNode<E>> newNodes = new ArrayList<IBinaryTreeNode<E>>();
for (IBinaryTreeNode<E> node : nodes) {
if (node != null) {
System.out.print(node.getValue());
newNodes.add(node.getLeftNode());
newNodes.add(node.getRightNode());
} else {
newNodes.add(null);
newNodes.add(null);
System.out.print("");
}
int valueLength = (node == null) ? 0 : node.getValue().toString().length();
BinaryTreePrinter.printWhitespaces(betweenSpaces - valueLength);
}
System.out.println("");
for (int i = 1; i <= endgeLines; i++) {
for (int j = 0; j < nodes.size(); j++) {
BinaryTreePrinter.printWhitespaces(firstSpaces - i);
if (nodes.get(j) == null) {
BinaryTreePrinter.printWhitespaces(endgeLines + endgeLines + i + 1);
continue;
}
if (nodes.get(j).getLeftNode() != null) {
System.out.print("/");
} else {
BinaryTreePrinter.printWhitespaces(1);
}
BinaryTreePrinter.printWhitespaces(i + i - 1);
if (nodes.get(j).getRightNode() != null) {
System.out.print("\\");
} else {
BinaryTreePrinter.printWhitespaces(1);
}
BinaryTreePrinter.printWhitespaces(endgeLines + endgeLines - i);
}
System.out.println("");
}
printNodeInternal(newNodes, level + 1, maxLevel);
}
private static void printWhitespaces(int count) {
for (int i = 0; i < count; i++)
System.out.print(" ");
}
private static <E extends Comparable<E>> int maxLevel(IBinaryTreeNode<E> node) {
if (node == null) {
return 0;
}
int level = Math.max(BinaryTreePrinter.maxLevel(node.getLeftNode()),
BinaryTreePrinter.maxLevel(node.getRightNode())) + 1;
return level;
}
private static <E> boolean isAllElementsNull(List<E> list) {
for (Object object : list) {
if (object != null) {
return false;
}
}
return true;
}
}
最后是一个测试类,BinarySearchTreeTest,用于测试二叉查询树的各种操作。
package com.my.study.algorithm.tree.bstree;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
public class BinarySearchTreeTest {
private static final Integer[] ARRAY = { 5, 10, 13, 66, -3, -7, 9, 1, 9, -8, 23 };
public static void main(String[] args) {
testGenerateBSTreeByArray();
// testGenerateBSTreeByInserting();
// testRemoveElementFromTree();
// testSearchData();
// testPerformance();
// testLinkedStyleTree();
// testLinkedStyleTreeByDeleting();
}
private static void testGenerateBSTreeByArray() {
IBinaryTree<Integer> tree = new BinarySearchTree<>(ARRAY);
printTreeInfo(tree);
}
private static void testGenerateBSTreeByInserting() {
IBinaryTree<Integer> tree = new BinarySearchTree<>();
for (int data : ARRAY) {
tree.insert(data);
System.out.println("Inserted data: " + data);
printTreeInfo(tree);
}
}
private static void testRemoveElementFromTree() {
IBinaryTree<Integer> tree = new BinarySearchTree<>(ARRAY);
printTreeInfo(tree);
for (int data : ARRAY) {
tree.remove(data);
System.out.println("Removed data: " + data);
printTreeInfo(tree);
}
}
private static void testSearchData() {
System.out.println("Array: " + Arrays.toString(ARRAY));
IBinaryTree<Integer> tree = new BinarySearchTree<>(ARRAY);
Integer[] findData = { 10, -7, 100, 8, -8 };
for (int data : findData) {
IBinaryTreeNode<Integer> node = tree.find(data);
System.out.println("Data: " + data + ", exist? " + (node != null));
}
}
private static void testPerformance() {
// Original data size
int originalDataSize = 1000000;
int minNum = 0;
int maxNum = originalDataSize;
Integer[] data = generateRandomArrays(minNum, maxNum, originalDataSize);
// Generate tree
long time1 = new Date().getTime();
IBinaryTree<Integer> tree = new BinarySearchTree<>(data);
long time2 = new Date().getTime();
System.out.println("Generate tree takes: " + (time2 - time1) / 1000 + " seconds, tree size: " + tree.getSize());
// Selected data size
int selectedDataSize = originalDataSize / 100;
Integer[] selectedData = generateRandomArrays(minNum, maxNum, selectedDataSize);
// Find data by tree
long time3 = new Date().getTime();
long findDataCount = 0;
for (int v : selectedData) {
if (tree.find(v) != null) {
findDataCount++;
}
}
long time4 = new Date().getTime();
System.out.println(
"Find data by tree takes: " + (time4 - time3) / 1000 + " seconds, data size: " + selectedDataSize);
System.out.println("Find data percentage: " + (findDataCount * 1.0 / selectedDataSize));
// Find data by array, traditional way
long time5 = new Date().getTime();
long findDataCount2 = 0;
for (int i = 0; i < selectedData.length; i++) {
for (int k : data) {
if (selectedData[i] == k) {
findDataCount2++;
break;
}
}
}
long time6 = new Date().getTime();
System.out.println(
"Find data by array takes: " + (time6 - time5) / 1000 + " seconds, data size: " + selectedDataSize);
System.out.println("Find data percentage: " + (findDataCount2 * 1.0 / selectedDataSize));
/*
* Result analyze:
*
* When tree size is 1,000,000 and selected data size is 10,000.
*
* Generate tree takes: 1 seconds, tree size: 1000000
*
* Find data by tree takes: 0 seconds, data size: 10000
*
* Find data percentage: 0.6349
*
* Find data by array takes: 49 seconds, data size: 10000
*
* Find data percentage: 0.6349
*
*
* When tree size 1s 10,000,000, selected data size is 10,000,000
*
* Generate tree takes: 19 seconds, tree size: 10000000
*
* Find data by tree takes: 22 seconds, data size: 10000000
*
* Find data percentage: 0.6321235
*
* Find data by array takes: 151971 seconds, data size: 10000000
*
* Find data percentage: 0.6321235
*
*/
}
public static void testLinkedStyleTree() {
Integer[] array = { 1, 2, 3, 4, 5 };
IBinaryTree<Integer> tree = new BinarySearchTree<>(array);
printTreeInfo(tree);
Integer[] array2 = { 5, 4, 3, 2, 1 };
tree = new BinarySearchTree<>(array2);
printTreeInfo(tree);
}
public static void testLinkedStyleTreeByDeleting() {
Integer[] data = generateRandomArrays(0, 100, 100);
IBinaryTree<Integer> tree = new BinarySearchTree<>(data);
while (tree.getSize() > 6) {
List<Integer> treeList = tree.inorderTraversal();
int randomIndex = (int) (Math.random() * (treeList.size()));
tree.remove(treeList.get(randomIndex));
}
printTreeInfo(tree);
}
private static Integer[] generateRandomArrays(int min, int max, int size) {
Integer[] data = new Integer[size];
for (int i = 0; i < size; i++) {
int num = (int) (Math.random() * (max - min + 1)) + min;
data[i] = num;
}
return data;
}
private static <E extends Comparable<E>> void printTreeInfo(IBinaryTree<E> tree) {
System.out.println("Tree height: " + (tree.getRoot() == null ? 0 : tree.getRoot().getHeight()));
System.out.println("Tree size: " + tree.getSize());
System.out.println("Pre order traversal: " + tree.preorderTraversal());
System.out.println("In order traversal: " + tree.inorderTraversal());
System.out.println("Post order traversal: " + tree.postorderTraversal());
System.out.println("Tree structure:");
BinaryTreePrinter.printTree(tree);
System.out.println("-----------------------------------------------");
}
}
二、二叉查询树的重要操作
1. 查询
参考方法:private IBinaryTreeNode<E> search(IBinaryTreeNode<E> startNode, E e) {...}。
该操作是最基本的一个操作,用于查询给定结点下面值和e相等或者最接近的那个结点。值相等的结点很好理解,直接比较数据大小即可,如果存在多个值相等的结点,则返回第一个。最接近的结点表示如果将e插入到二叉查询树里面,该结点可以作为新结点的父结点。查询的基本思路:从给定结点开始遍历,如果值相等则返回当前结点,如果e小于当前结点则从当前结点的左子树递归搜索,如果e大于当前结点则从当前结点的右子树递归搜索。这类似于二分查找,只不过二分查找在查找不到的情况下返回null,而此时需要返回最接近的那个结点。search操作只有当root为null时(空树)返回null,其它任何时候都不能返回null,因为总存在一个最接近的结点。
2. 插入
参考方法:private IBinaryTreeNode<E> insert(IBinaryTreeNode<E> startNode, E e) {...}。
该操作用于在以startNode为根节点的子树下面插入新的结点e,使得插入完毕后以startNode为根结点的子树还是二叉查询树。插入的基本思路:首先调用search方法,找到插入点。如果e小于插入点,那么在插入点下面创建一个新的左子结点,如果大于则创建一个新的右子结点。如果e等于插入点,有三种场景,如果插入点没有左结点,则创建新的左子结点,如果插入点没有右结点,则创建新的右子节点,如果左右结点都存在,那么从左结点开始继续搜索(当然也可以从右结点开始搜索)。
3. 删除
参考方法:private E remove(IBinaryTreeNode<E> startNode, E e) {...}。
该操作用于在以startNode为根节点的的子树下面删除某个值为e的结点,使得删除完成后以startNode为根结点的子树还是二叉查询树。删除的基本思路:首先调用search方法,找到删除点。如果删除点的值不等于e,那么说明不存在值为e的结点,返回null。如果删除点的值等于e,那么分两种情况,判断其左结点是否存在。如果左结点不存在,那么移除删除点,以删除点的右结点取而代之。如果左结点存在,则找到删除点的直接前驱结点,交换删除点和前驱结点的值,并移除原前驱结点,以原前驱结点的左结点取而代之。注:此时原前驱结点不存在右结点,故不需要判断。
4. 几种遍历操作
前序遍历:public List<E> preorderTraversal() {...}。
中序遍历:public List<E> inorderTraversal() {...}。
后序遍历:public List<E> postorderTraversal() {...}。
5. 其它操作
IBinaryTree里面还定义了其它操作,例如:find, getRoot, getSize, getHeight等,算法比较简单,就不一一说明,参考源码即可。
三、二叉查询树的测试
参考测试类代码:BinarySearchTreeTest,并逐一执行main方法里面的方法:
方法名 | 说明 |
testGenerateBSTreeByArray | 用于演示通过给定一个数组来生成二叉查询树。 |
testGenerateBSTreeByInserting | 用于演示二叉查询树的插入过程。 |
testRemoveElementFromTree | 用于演示二叉查询树的删除过程。 |
testSearchData | 用于演示二叉查询树的查询结果。 |
testPerformance | 用于演示二叉查询树的查询性能。 |
testLinkedStyleTree | 用于演示二叉查询树蜕变成链表的场景。 |
testLinkedStyleTreeByDeleting | 用于演示二叉查询树在经过多次删除操作后蜕变成近似链表的场景。 |
1. testGenerateBSTreeByArray
通过数组生成二叉查询树,本质上是将给定数组的元素逐一插入到二叉查询树里面。
输出结果:
Tree height: 5
Tree size: 11
Pre order traversal: [5, -3, -7, -8, 1, 10, 9, 9, 13, 66, 23]
In order traversal: [-8, -7, -3, 1, 5, 9, 9, 10, 13, 23, 66]
Post order traversal: [-8, -7, 1, -3, 9, 9, 23, 66, 13, 10, 5]
Tree structure:
5
/ \
/ \
/ \
/ \
/ \
/ \
/ \
/ \
-3 10
/ \ / \
/ \ / \
/ \ / \
/ \ / \
-7 1 9 13
/ / \
/ / \
-8 9 66
/
23
-----------------------------------------------
2. testGenerateBSTreeByInserting
演示了每次插入元素后二叉查询树的结构变化过程,需要结合上面的插入算法来分析。
输出结果:
Inserted data: 5
Tree height: 1
Tree size: 1
Pre order traversal: [5]
In order traversal: [5]
Post order traversal: [5]
Tree structure:
5
-----------------------------------------------
Inserted data: 10
Tree height: 2
Tree size: 2
Pre order traversal: [5, 10]
In order traversal: [5, 10]
Post order traversal: [10, 5]
Tree structure:
5
\
10
-----------------------------------------------
Inserted data: 13
Tree height: 3
Tree size: 3
Pre order traversal: [5, 10, 13]
In order traversal: [5, 10, 13]
Post order traversal: [13, 10, 5]
Tree structure:
5
\
\
10
\
13
-----------------------------------------------
Inserted data: 66
Tree height: 4
Tree size: 4
Pre order traversal: [5, 10, 13, 66]
In order traversal: [5, 10, 13, 66]
Post order traversal: [66, 13, 10, 5]
Tree structure:
5
\
\
\
\
10
\
\
13
\
66
-----------------------------------------------
Inserted data: -3
Tree height: 4
Tree size: 5
Pre order traversal: [5, -3, 10, 13, 66]
In order traversal: [-3, 5, 10, 13, 66]
Post order traversal: [-3, 66, 13, 10, 5]
Tree structure:
5
/ \
/ \
/ \
/ \
-3 10
\
\
13
\
66
-----------------------------------------------
Inserted data: -7
Tree height: 4
Tree size: 6
Pre order traversal: [5, -3, -7, 10, 13, 66]
In order traversal: [-7, -3, 5, 10, 13, 66]
Post order traversal: [-7, -3, 66, 13, 10, 5]
Tree structure:
5
/ \
/ \
/ \
/ \
-3 10
/ \
/ \
-7 13
\
66
-----------------------------------------------
Inserted data: 9
Tree height: 4
Tree size: 7
Pre order traversal: [5, -3, -7, 10, 9, 13, 66]
In order traversal: [-7, -3, 5, 9, 10, 13, 66]
Post order traversal: [-7, -3, 9, 66, 13, 10, 5]
Tree structure:
5
/ \
/ \
/ \
/ \
-3 10
/ / \
/ / \
-7 9 13
\
66
-----------------------------------------------
Inserted data: 1
Tree height: 4
Tree size: 8
Pre order traversal: [5, -3, -7, 1, 10, 9, 13, 66]
In order traversal: [-7, -3, 1, 5, 9, 10, 13, 66]
Post order traversal: [-7, 1, -3, 9, 66, 13, 10, 5]
Tree structure:
5
/ \
/ \
/ \
/ \
-3 10
/ \ / \
/ \ / \
-7 1 9 13
\
66
-----------------------------------------------
Inserted data: 9
Tree height: 4
Tree size: 9
Pre order traversal: [5, -3, -7, 1, 10, 9, 9, 13, 66]
In order traversal: [-7, -3, 1, 5, 9, 9, 10, 13, 66]
Post order traversal: [-7, 1, -3, 9, 9, 66, 13, 10, 5]
Tree structure:
5
/ \
/ \
/ \
/ \
-3 10
/ \ / \
/ \ / \
-7 1 9 13
/ \
9 66
-----------------------------------------------
Inserted data: -8
Tree height: 4
Tree size: 10
Pre order traversal: [5, -3, -7, -8, 1, 10, 9, 9, 13, 66]
In order traversal: [-8, -7, -3, 1, 5, 9, 9, 10, 13, 66]
Post order traversal: [-8, -7, 1, -3, 9, 9, 66, 13, 10, 5]
Tree structure:
5
/ \
/ \
/ \
/ \
-3 10
/ \ / \
/ \ / \
-7 1 9 13
/ / \
-8 9 66
-----------------------------------------------
Inserted data: 23
Tree height: 5
Tree size: 11
Pre order traversal: [5, -3, -7, -8, 1, 10, 9, 9, 13, 66, 23]
In order traversal: [-8, -7, -3, 1, 5, 9, 9, 10, 13, 23, 66]
Post order traversal: [-8, -7, 1, -3, 9, 9, 23, 66, 13, 10, 5]
Tree structure:
5
/ \
/ \
/ \
/ \
/ \
/ \
/ \
/ \
-3 10
/ \ / \
/ \ / \
/ \ / \
/ \ / \
-7 1 9 13
/ / \
/ / \
-8 9 66
/
23
-----------------------------------------------
3. testRemoveElementFromTree
演示了每次删除元素后,二叉查询树的结构变化过程,需要结合上面的删除算法来分析。
输出结果:
Tree height: 5
Tree size: 11
Pre order traversal: [5, -3, -7, -8, 1, 10, 9, 9, 13, 66, 23]
In order traversal: [-8, -7, -3, 1, 5, 9, 9, 10, 13, 23, 66]
Post order traversal: [-8, -7, 1, -3, 9, 9, 23, 66, 13, 10, 5]
Tree structure:
5
/ \
/ \
/ \
/ \
/ \
/ \
/ \
/ \
-3 10
/ \ / \
/ \ / \
/ \ / \
/ \ / \
-7 1 9 13
/ / \
/ / \
-8 9 66
/
23
-----------------------------------------------
Removed data: 5
Tree height: 5
Tree size: 10
Pre order traversal: [1, -3, -7, -8, 10, 9, 9, 13, 66, 23]
In order traversal: [-8, -7, -3, 1, 9, 9, 10, 13, 23, 66]
Post order traversal: [-8, -7, -3, 9, 9, 23, 66, 13, 10, 1]
Tree structure:
1
/ \
/ \
/ \
/ \
/ \
/ \
/ \
/ \
-3 10
/ / \
/ / \
/ / \
/ / \
-7 9 13
/ / \
/ / \
-8 9 66
/
23
-----------------------------------------------
Removed data: 10
Tree height: 5
Tree size: 9
Pre order traversal: [1, -3, -7, -8, 9, 9, 13, 66, 23]
In order traversal: [-8, -7, -3, 1, 9, 9, 13, 23, 66]
Post order traversal: [-8, -7, -3, 9, 23, 66, 13, 9, 1]
Tree structure:
1
/ \
/ \
/ \
/ \
/ \
/ \
/ \
/ \
-3 9
/ / \
/ / \
/ / \
/ / \
-7 9 13
/ \
/ \
-8 66
/
23
-----------------------------------------------
Removed data: 13
Tree height: 4
Tree size: 8
Pre order traversal: [1, -3, -7, -8, 9, 9, 66, 23]
In order traversal: [-8, -7, -3, 1, 9, 9, 23, 66]
Post order traversal: [-8, -7, -3, 9, 23, 66, 9, 1]
Tree structure:
1
/ \
/ \
/ \
/ \
-3 9
/ / \
/ / \
-7 9 66
/ /
-8 23
-----------------------------------------------
Removed data: 66
Tree height: 4
Tree size: 7
Pre order traversal: [1, -3, -7, -8, 9, 9, 23]
In order traversal: [-8, -7, -3, 1, 9, 9, 23]
Post order traversal: [-8, -7, -3, 9, 23, 9, 1]
Tree structure:
1
/ \
/ \
/ \
/ \
-3 9
/ / \
/ / \
-7 9 23
/
-8
-----------------------------------------------
Removed data: -3
Tree height: 3
Tree size: 6
Pre order traversal: [1, -7, -8, 9, 9, 23]
In order traversal: [-8, -7, 1, 9, 9, 23]
Post order traversal: [-8, -7, 9, 23, 9, 1]
Tree structure:
1
/ \
/ \
-7 9
/ / \
-8 9 23
-----------------------------------------------
Removed data: -7
Tree height: 3
Tree size: 5
Pre order traversal: [1, -8, 9, 9, 23]
In order traversal: [-8, 1, 9, 9, 23]
Post order traversal: [-8, 9, 23, 9, 1]
Tree structure:
1
/ \
/ \
-8 9
/ \
9 23
-----------------------------------------------
Removed data: 9
Tree height: 3
Tree size: 4
Pre order traversal: [1, -8, 9, 23]
In order traversal: [-8, 1, 9, 23]
Post order traversal: [-8, 23, 9, 1]
Tree structure:
1
/ \
/ \
-8 9
\
23
-----------------------------------------------
Removed data: 1
Tree height: 3
Tree size: 3
Pre order traversal: [-8, 9, 23]
In order traversal: [-8, 9, 23]
Post order traversal: [23, 9, -8]
Tree structure:
-8
\
\
9
\
23
-----------------------------------------------
Removed data: 9
Tree height: 2
Tree size: 2
Pre order traversal: [-8, 23]
In order traversal: [-8, 23]
Post order traversal: [23, -8]
Tree structure:
-8
\
23
-----------------------------------------------
Removed data: -8
Tree height: 1
Tree size: 1
Pre order traversal: [23]
In order traversal: [23]
Post order traversal: [23]
Tree structure:
23
-----------------------------------------------
Removed data: 23
Tree height: 0
Tree size: 0
Pre order traversal: []
In order traversal: []
Post order traversal: []
Tree structure:
-----------------------------------------------
4. testSearchData
判断二叉查询树里面是否包含给定的数据。
输出结果:
Array: [5, 10, 13, 66, -3, -7, 9, 1, 9, -8, 23]
Data: 10, exist? true
Data: -7, exist? true
Data: 100, exist? false
Data: 8, exist? false
Data: -8, exist? true
5. testPerformance
通过与数组遍历查询进行比较,演示了二叉查询树的平均算法复杂度是。
当originalDataSize=1,000,000,selectedDataSize=10,000时,
输出结果:
Generate tree takes: 1 seconds, tree size: 1000000
Find data by tree takes: 0 seconds, data size: 10000
Find data percentage: 0.6349
Find data by array takes: 49 seconds, data size: 10000
Find data percentage: 0.6349
当originalDataSize=10,000,000,selectedDataSize=10,000,000时,
输出结果:
Generate tree takes: 19 seconds, tree size: 10000000
Find data by tree takes: 22 seconds, data size: 10000000
Find data percentage: 0.6321235
Find data by array takes: 151971 seconds, data size: 10000000
Find data percentage: 0.6321235
可以看出,在平均情况下,二叉查询树的性能在数据量比较大时要远远大于传统数组遍历查询时的性能。特别是第二种场景,性能快了近 3706 倍,完全不是一个数量级。
6. testLinkedStyleTree
当输入的数据已经排好序,此时的二叉查询树会蜕变成链表,所有操作的算法复杂度会变为,与链表的算法复杂度相同。造成该现象的原因是二叉查询树在生成过程中没有考虑平衡性,平衡二叉查询树会解决此问题。
输出结果:
Tree height: 5
Tree size: 5
Pre order traversal: [1, 2, 3, 4, 5]
In order traversal: [1, 2, 3, 4, 5]
Post order traversal: [5, 4, 3, 2, 1]
Tree structure:
1
\
\
\
\
\
\
\
\
2
\
\
\
\
3
\
\
4
\
5
-----------------------------------------------
Tree height: 5
Tree size: 5
Pre order traversal: [5, 4, 3, 2, 1]
In order traversal: [1, 2, 3, 4, 5]
Post order traversal: [1, 2, 3, 4, 5]
Tree structure:
5
/
/
/
/
/
/
/
/
4
/
/
/
/
3
/
/
2
/
1
-----------------------------------------------
7. testLinkedStyleTreeByDeleting
可能二叉查询树在刚生成的时候是比较平衡的(可通过随机方式插入数据实现),但如果进行了频繁的删除操作,其将变得不再平衡,因为删除操作都是将左子树的前驱节点上移,使得左侧结点变少,右侧结点变多。
输出结果(每次运行结果不一定相同):
Tree height: 5
Tree size: 6
Pre order traversal: [4, 46, 36, 82, 97, 93]
In order traversal: [4, 36, 46, 82, 93, 97]
Post order traversal: [36, 93, 97, 82, 46, 4]
Tree structure:
4
\
\
\
\
\
\
\
\
46
/ \
/ \
/ \
/ \
36 82
\
\
97
/
93
-----------------------------------------------
四、二叉查询树操作的算法复杂度
操作 | 平均复杂度 | 最坏复杂度 |
---|---|---|
空间 | ||
查询 | ||
插入 | ||
删除 |