简述
树是由边连接的节点而构成的。
每个节点最多有两个子节点的称为二叉树,而多于两个子节点的称为多路树。
由于树通常结合了有序数组和链表两种数据结构,所以在查找数据项项时和有序数据一样快,而插入、删除数据项则和链表一样快。
二叉树
二叉树每个节点的两个子节点称为“左子节点“和”右子节点“。二叉树中的节点不是必须有两个子节点,它可以只有一个左或右子节点,也可以没有任何子节点。
二叉搜索树
二叉搜索树(如下图所示,图片来源为参看书籍)其特征为:
- 节点的左子节点的关键字值小于这个节点
- 节点的右子节点的关键字值大于或等于这个节点
树的不平衡性
树的大部分节点在根的某一边,个别子树也可能出现同样的情况,如下图所示。
数据项插入的顺序会影响树的平衡性。如果待插入的数据项是有序(升序或降序)的,则所有的值都是右子节点或都是左子节点。如果顺序是随机的,则不平衡性相对会好一些,但是还是会出现局部不平衡。要避免这种不平衡,需要进行一些特殊的处理。
效率
树的查找、插入、删除的时间复杂度为O(logN)。
Java实现二叉树
Java实现二叉树有两种方式。一种是吧节点存在无关联的存储器中,通过每个节点指向自己子节点的应用来连接。另一种是在内存中用数据来表示,通过数组中相对的位置来表示节点在树中的位置。其中第一种最常用。
以下是实现类中一些方法的讲解。
查找节点
从根开始,将当前节点设为跟节点。如果要找的节点的关键值小于当前节点,则说明要找的节点在当前节点的左侧子树中,将当前节点重新指向其左子节点;如果大于当前节点的关键值,则说明要找的节点在右侧子树中,将当前节点重新指向其右子节点;如果相等,即找到该节点。
插入节点
首先从根开始找到节点要插入的地方,如果当前树为空,则待插入节点为根节点;如果找到其父节点,则根据该节点的关键值与父节点关键值的大小关系来决定是连接到父节点的左子节点还是右子节点。
删除节点
删除节点最为复杂,有三种情况需要考虑:
1、该节点是叶节点(没有子节点);
2、该节点有一个子节点;
3、该节点有两个字节点。
其中第三种情况最为复杂。
删除没有子节点的节点
要删除叶节点,只需要改变该节点的父节点对应子节点的值,由指向该节点改为null即可。
删除有一个子节点的节点
该节点有两个连接:连向父节点的和指向其唯一子节点的。删除该节点,需要将其子节点连接到其父节点上,即将其父节点指向它的引用改为指向其唯一子节点。
删除有两个子节点的节点
二叉搜索树中,节点是按照关键字值升序排列的。对每一个节点,比该节点关键字值次高的节点是它的中序后继,简称为该节点的后继。
删除有两个子节点的节点,用它的中序后继来代替该节点。
寻找后继节点:找到待删除节点的右子节点,它的关键字值一定大于待删除节点。然后顺着该节点的左子节点一路向下找,最后一个左子节点就是待删除节点的后继。如果待删除节点的右子节点没有左子节点,则该右子节点就是待删除节点的后继。
后继节点是不存在左子节点的。
如果后继节点没有子节点,则直接用后继节点代替待删除节点。
如果后继节点是待删除节点的右子节点,只需要把后继为根的子树已到删除的节点位置。把待删除节点的父节点指向待删除节点改为后继节点,后继节点的左子节点指向待删除节点的左子节点。
最为复杂的是,如果后继节点是待删除节点的右子节点的左后代,则需要执行以下步骤:
1、把后继父节点的左子节点改为后继节点的右子节点;
2、把后继节点的右子节点改为要删除节点的右子节点;
3、把要删除节点父节点指向要删除节点的子节点改为后继节点;
4、把后继节点的左子节点改为要删除节点的左子节点。
遍历树
根据某种特定顺序访问树的每一个节点。有三种遍历方法:前序、中序、后序。其中中序最为常用。
遍历树的最简单方法是使用递归。从根节点开始。
前序遍历
1、访问这个节点;
2、调用自身来遍历节点的左子树;
3、调用自身来遍历节点的右子树。
中序遍历
1、调用自身来遍历节点的左子树;
2、访问这个节点;
3、调用自身来遍历节点的右子树。
后序遍历
1、调用自身来遍历节点的左子树;
2、调用自身来遍历节点的右子树;
3、访问这个节点。
查找最大值和最小值
在二叉搜索树中得到最大值和最小值非常简单。
最小值即树的没有左子节点的左子节点。
最大值即树的没有右子节点的右子节点。
完整Java实现
package com.jikefriend.binarytree;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Stack;
/**
* 二叉树
*/
public class Tree {
private Node root;
public Tree()
{
root = null;
}
public Node find(int key)
{
Node current = root;
while (current.iData != key)
{
if (key < current.dData)
current = current.leftChild;
else
current = current.rightChild;
if (current == null)
return null;
}
return current;
}
public void insert(int i, double d)
{
Node newNode = new Node(i, d);
if (root == null)
root = newNode;
else
{
Node current = root;
Node parent;
while (true)
{
parent = current;
if (i < current.iData)
{
current = current.leftChild;
if (current == null)
{
parent.leftChild = newNode;
return;
}
}
else
{
current = current.rightChild;
if (current == null)
{
parent.rightChild = newNode;
return;
}
}
}
}
}
public boolean delete(int key)
{
Node current = root;
Node parent = root;
boolean isLeftChild = true;
while (current.iData != key)
{
parent = current;
if (key < current.iData)
{
isLeftChild = true;
current = current.leftChild;
}
else
{
isLeftChild = false;
current = current.rightChild;
}
if (current == null)
return false;
}
if (current.leftChild == null && current.rightChild == null)
{
if (current == root)
root = null;
else if (isLeftChild)
parent.leftChild = null;
else
parent.rightChild = null;
}
else if (current.rightChild == null)
{
if (current == root)
root = current.leftChild;
else if (isLeftChild)
parent.leftChild = current.leftChild;
else
parent.rightChild = current.leftChild;
}
else if (current.leftChild == null)
{
if (current == root)
root = current.rightChild;
else if (isLeftChild)
parent.leftChild = current.rightChild;
else
parent.rightChild = current.rightChild;
}
else
{
Node successor = getSuccessor(current);
if (current == root)
root = successor;
else if (isLeftChild)
parent.leftChild = successor;
else
parent.rightChild = successor;
successor.leftChild = current.leftChild;
}
return true;
}
private Node getSuccessor(Node delNode)
{
Node successorParent = delNode;
Node successor = delNode;
Node current = delNode.rightChild;
while (current != null)
{
successorParent = successor;
successor = current;
current = current.leftChild;
}
if (successor != delNode.rightChild)
{
successorParent.leftChild = successor.rightChild;
successor.rightChild = delNode.rightChild;
}
return successor;
}
public void traverse(int traverseType)
{
switch (traverseType)
{
case 1:
System.out.print("\nPreorder traversal:");
preOrder(root);
break;
case 2:
System.out.print("\nInorder traversal:");
inOrder(root);
break;
case 3:
System.out.print("\nPostorder traversal:");
postOrder(root);
break;
}
}
private void preOrder(Node localRoot)
{
if (localRoot != null)
{
System.out.print(localRoot.iData + " ");
preOrder(localRoot.leftChild);
preOrder(localRoot.rightChild);
}
}
private void inOrder(Node localRoot)
{
if (localRoot != null)
{
inOrder(localRoot.leftChild);
System.out.print(localRoot.iData + " ");
inOrder(localRoot.rightChild);
}
}
private void postOrder(Node localRoot)
{
if (localRoot != null)
{
postOrder(localRoot.leftChild);
postOrder(localRoot.rightChild);
System.out.print(localRoot.iData + " ");
}
}
public void display()
{
Stack globalStack = new Stack();
globalStack.push(root);
int nBlanks = 32;
boolean isRowEmpty = false;
System.out.println(".............................................");
while (isRowEmpty == false)
{
Stack localStack = new Stack();
isRowEmpty = true;
for (int i = 0; i < nBlanks; i++)
System.out.print(" ");
while (globalStack.isEmpty() == false)
{
Node temp = (Node) globalStack.pop();
if (temp != null)
{
System.out.print(temp.iData);
localStack.push(temp.leftChild);
localStack.push(temp.rightChild);
if (temp.leftChild != null || temp.rightChild != null)
isRowEmpty = false;
}
else
{
System.out.print("--");
localStack.push(null);
localStack.push(null);
}
for (int i = 0; i < nBlanks * 2 - 2; i++)
System.out.print(" ");
}
System.out.println();
nBlanks /= 2;
while (localStack.isEmpty() == false)
globalStack.push(localStack.pop());
}
System.out.println(".............................................");
}
static class Node
{
public int iData;
public double dData;
public Node leftChild;
public Node rightChild;
public Node(int i, double d)
{
iData = i;
dData = d;
}
public void display()
{
System.out.print("{" + iData + ", " + dData + "}");
}
}
public static void main(String[] args) throws IOException{
int value;
Tree theTree = new Tree();
theTree.insert(50, 1.5);
theTree.insert(25, 1.2);
theTree.insert(75, 1.7);
theTree.insert(12, 1.5);
theTree.insert(37, 1.2);
theTree.insert(43, 1.7);
theTree.insert(30, 1.5);
theTree.insert(33, 1.2);
theTree.insert(87, 1.7);
theTree.insert(93, 1.5);
theTree.insert(97, 1.5);
while (true)
{
System.out.print("Enter first letter of show, ");
System.out.print("insert , find, delete, or traverse: ");
int choice = getChar();
switch (choice)
{
case 's':
theTree.display();
break;
case 'i':
System.out.print("Enter value to insert: ");
value = getInt();
theTree.insert(value, value + 0.9);
break;
case 'f':
System.out.print("Enter value to find: ");
value = getInt();
Node found = theTree.find(value);
if (found != null)
{
System.out.print("Found:");
found.display();
System.out.println();
}
else
{
System.out.println("Can't find " + value);
}
break;
case 'd':
System.out.print("Enter value to delete: ");
value = getInt();
boolean didDeleted = theTree.delete(value);
if (didDeleted)
System.out.println("Deleted " + value);
else
System.out.println("Could not delete " + value);
break;
case 't':
System.out.print("Enter type 1, 2 or 3: ");
value = getInt();
theTree.traverse(value);
break;
default:
System.out.println("Invalid entry");
}
}
}
public static String getString() throws IOException
{
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(isr);
return br.readLine();
}
public static char getChar() throws IOException
{
return getString().charAt(0);
}
public static int getInt() throws IOException
{
return Integer.parseInt(getString());
}
}
其他平衡二叉树
二叉树在不平衡的条件下,搜索的时间介于O(N)和O(logN)之间,其取决于树的不平衡程度。
为了保证能以O(logN)的时间来搜索一棵树,需要保证树总是平衡的,即左子树和右子树应大致对称。
红黑树
红黑树特征。
- 节点都有颜色。
- 在插入和删除的过程中,要遵循保持这些颜色的不同排列的规则。
红黑树规则,如果遵循这些规则,则树就是平衡的。
- 每一个节点不是红色的就是黑色的。
- 根总是黑色的。
- 如果节点是红色的,则它的字节点必须是黑色的(反之不一定必须为真)。
- 从根到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点。
更详细的原理及分析可以参看v_JULY_v的教你初步了解红黑树
而具体的java实现可以参看jdk中的TreeMap源码。
2-3-4树
名字中的2、3和4的含义是指一个节点可能含有的字节点的个数。对非叶子节点有三种可能的情况
有一个数据项的节点总是有两个字节点。
有两个数据项的节点总是有三个字节点。
有三个数据项的节点总是有四个字节点。
也就是说,非叶节点的字节点树总是比它含有的数据项多1。
红黑树是在2-3-4树的基础上发展而来的。