《数据结构与算法》
本文来源于liuyubobobo的“玩转数据结构 从入门到进阶”视频教程
上图是一个Binary Search Tree,因汉语博大精深,就有了很多种翻译,如:二分搜索树、二叉搜索树、……………
二分搜索树以二叉树为基础,但多了两个特点:
1、二分搜索树节点的值具有可比较性。
2、每个节点都比它左子树的任意元素大,而且比右子树的任意元素小(此文不讨论有重复元素的树)。
下面使用java代码实现Binary Search Tree的增加、查找、删除操作。
定义节点Node类
private class Node {
// 节点值
public E e;
// 每个节点都可能有左节点、右节点。最后一层的节点没有左右节点可以视为left、right都为null
public Node left, right;
public Node(E e) {
this.e = e;
left = null;
right = null;
}
}
树的代码实现
//二分搜索树
public static class BinarySearchTree<E extends Comparable<E>> {
private class Node {
// 节点值,二分搜索树的值是有比较性的,所以泛型E必须是Comparable的子类
public E e;
// 每个节点都可能有左节点、右节点。最后一层的节点没有左右节点可以视为left、right都为null
public Node left, right;
public Node(E e) {
this.e = e;
left = null;
right = null;
}
}
// 树使用root节点作为索引,增查删都从root开始操作
private Node root;
// 树中节点的数量,即树的大小
private int size;
public BinarySearchTree() {
root = null;
size = 0;
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
// 在此添加增查删方法
}
先说下递归,递归实际上是一种把大问题、大数据拆成相同类型的小问题、小数据的算法。
而树具有天然的递归性。看上图,蓝色框内的树就是整棵树的缩小版,所以说树具有天然的递归性。
树新增节点代码
// 向二分搜索树中添加新的元素e
public void add(E e){
// 第一次添加的是根节点
if(root == null){
root = new Node(e);
size ++;
}else{
add(root, e); //递归添加元素
}
}
// 向以node为根的二分搜索树中插入元素e,递归算法
private void add(Node node, E e){
// 不考虑相同元素的情况
if(e.equals(node.e)){
return;
}else if(e.compareTo(node.e) < 0 && node.left == null){
// 新增元素比当前节点小,且当前节点左边无节点,新元素插入左边
node.left = new Node(e);
size ++;
return;
}else if(e.compareTo(node.e) > 0 && node.right == null){
// 新增元素比当前节点大,且当前节点右边无节点,新元素插入右边
node.right = new Node(e);
size ++;
return;
}
// 新增元素比当前节点小,且当前节点左边有节点,递归处理当前元素左边的节点
if(e.compareTo(node.e) < 0)
add(node.left, e);
else{
//e.compareTo(node.e) > 0 的情况
// 新增元素比当前节点大,且当前节点右边有节点,递归处理当前元素右边的节点
add(node.right, e);
}
}
上面新增节点的代码容易理解,但代码量较多,可以精简,如下:
public void add(E e){
// 使用root为索引
root = add(root, e);
}
public Node add(Node node, E e){
if (node == null){
// 第一次添加元素,root就为new Node(e)
size++;
return new Node(e);
}
if (e.compareTo(node.e) < 0){
// 在节点左边添加节点
node.left = add(node.left, e);
}else if (e.compareTo(node.e) > 0){
// 在节点右边添加节点
node.right = add(node.right, e);
}
return node;
}
遍历节点
上图的方式遍历方式,称为前序遍历
// 前序遍历
public void preOrder(){
preOrder(root);
}
public void preOrder(Node node){
if (node == null){
return;
}
// 打印当前元素值
System.out.println(node.e);
preOrder(node.left);
preOrder(node.right);
}
中序遍历、后序遍历
// 中序遍历结果从小到大排序
public void inOrder(){
inOrder(root);
}
public void inOrder(Node node){
if (node == null){
return;
}
inOrder(node.left);
System.out.println(node.e);
inOrder(node.right);
}
// 后续遍历,先遍历父节点的子节点,再遍历父节点
public void postOrder(){
postOrder(root);
}
public void postOrder(Node node){
if (node == null){
return;
}
postOrder(node.left);
postOrder(node.right);
System.out.println(node.e);
}
写个main函数测试下
public static void main(String[] args) {
BinarySearchTree<Integer> bts = new BinarySearchTree<>();
int[] nums = {28, 16, 30, 22, 13, 42, 29};
for (int num:nums){
bts.add(num);
}
System.out.println("-------前序遍历------");
bts.preOrder();
System.out.println("------中序遍历-------");
bts.inOrder();
System.out.println("------后序遍历-------");
bts.postOrde
}
前序、后续、中序都属于深度优先,下面实现广度优先遍历
先遍历28,再遍历16、30,最后遍历13、22、29、42。可以通过多加一个队列来实现广度优先遍历。
流程如上图所示
1、28入队
2、28出队,并且若左、右节点不为空,把28左、右节点(16、30)入队
3、16出队,并且若左、右节点不为空,把16左、右节点(13、22)入队
4、30出队,并且若左、右节点不为空,把30左、右节点(29、42)入队
代码实现如下:
// 广度优先遍历
public void levelOrder(){
if (root == null){
return;
}
Queue<Node> q = new LinkedBlockingQueue<>();
q.add(root);
while (!q.isEmpty()){
Node node = q.remove();
System.out.println(node.e);
if (node.left != null)
q.add(node.left);
if (node.right != null)
q.add(node.right);
}
}
查找树的最小值、最大值。当树最左边节点的node.left == null,则node值最小。当树最右边节点的node.right== null,则node值最大。
查找最小值的方式如上图。查找最大值方式也类似,就是在最右边线上查找
// 最小值元素
public E minimum(){
if(size == 0)
throw new IllegalArgumentException("空树");
Node minNode = minimum(root);
return minNode.e;
}
// 最小值节点
private Node minimum(Node node){
if( node.left == null )
return node;
return minimum(node.left);
}
// 最大值元素
public E maximum(){
if(size == 0)
throw new IllegalArgumentException("空树");
return maximum(root).e;
}
// 最大值节点
private Node maximum(Node node){
if( node.right == null )
return node;
return maximum(node.right);
}
删除最小值
public E removeMin(){
// 查找最小值
E ret = minimum();
// 删除最小节点
root = removeMin(root);
// 返回最小值
return ret;
}
public Node removeMin(Node node){
// node.left == null,node必然是最小节点
if (node.left == null){
// 先保存最小节点的right
Node nodeRight = node.right;
// 再把最小节点的right指向null,相当于把节点从树中删除
node.right = null;
size--;
return nodeRight;
}
// node.left != null,递归node.left
node.left = removeMin(node.left);
return node;
}
删除最大值,只给出代码,不画图了。请参考删除最小值的逻辑
public E removeMax(){
E ret = maximum();
root = removeMax(root);
return ret;
}
private Node removeMax(Node node){
// node.right == null,节点必然是最大节点
if(node.right == null){
// 保存最大节点的right
Node leftNode = node.left;
node.left = null;
size --;
return leftNode;
}
// node.right != null,递归node.right
node.right = removeMax(node.right);
return node;
}
删除任意值,情况比较多,如下:
1、节点左孩子为null,就是删除最小值。
2、节点右孩子为null,就是删除最大值。
3、节点左右孩子都有值,使用Hibbard Deletion算法删除值。
Hibbard Deletion在1962年被一个叫做 Hibbard 的计算机科学家提出,这算法简单来说就是这样:假设这个被删除的节点是 d,d 既有左孩子,又有右孩子,可以使用d右子树中的最小值替换d。
public void remove(E e){
remove(root, e);
}
private Node remove(Node node, E e){
if (node == null)
return null;
if (e.compareTo(node.e) < 0){
// 递归左侧节点
node.left = remove(node.left, e);
return node;
}else if (e.compareTo(node.e) > 0){
// 递归右侧节点
node.right = remove(node.right, e);
return node;
}else {
// 删除最小值
if (node.left == null){
Node rightNode = node.right;
node.right = null;
size--;
return rightNode;
}
// 删除最大值
if (node.right == null){
Node leftNode = node.left;
node.left=null;
size--;
return leftNode;
}
//当前node有左右孩子,找出右树中的最小节点successor
Node successor = minimum(node.right);
// successor.right = 删除当前node最小值后的树
successor.right = removeMin(node.right);
// successor.left = 当前node节点的left
successor.left = node.left;
node.left = node.right = null;
// 返回successor
return successor;
}
}
所有代码
//二分搜索树
public class BinarySearchTree<E extends Comparable<E>> {
private class Node {
// 节点值
public E e;
// 每个节点都可能有左节点、右节点。最后一层的节点没有左右节点可以视为left、right都为null
public Node left, right;
public Node(E e) {
this.e = e;
left = null;
right = null;
}
}
// 树使用root节点作为索引,增查删都从root开始操作
private Node root;
// 树中节点的数量,即树的大小
private int size;
public BinarySearchTree() {
root = null;
size = 0;
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public void add(E e){
// 使用root为索引
root = add(root, e);
}
public Node add(Node node, E e){
if (node == null){
// 第一次添加元素,root就为new Node(e)
size++;
return new Node(e);
}
if (e.compareTo(node.e) < 0){
// 在节点左边添加节点
node.left = add(node.left, e);
}else if (e.compareTo(node.e) > 0){
// 在节点右边添加节点
node.right = add(node.right, e);
}
return node;
}
// 前序遍历
public void preOrder(){
preOrder(root);
}
public void preOrder(Node node){
if (node == null){
return;
}
// 打印当前元素值
System.out.println(node.e);
preOrder(node.left);
preOrder(node.right);
}
// 中序遍历,结果从小到大排序
public void inOrder(){
inOrder(root);
}
public void inOrder(Node node){
if (node == null){
return;
}
inOrder(node.left);
System.out.println(node.e);
inOrder(node.right);
}
// 后序遍历,先遍历父节点的子节点,在遍历父节点
public void postOrder(){
postOrder(root);
}
public void postOrder(Node node){
if (node == null){
return;
}
postOrder(node.left);
postOrder(node.right);
System.out.println(node.e);
}
// 广度优先遍历
public void levelOrder(){
if (root == null){
return;
}
Queue<Node> q = new LinkedBlockingQueue<>();
q.add(root);
while (!q.isEmpty()){
Node node = q.remove();
System.out.println(node.e);
if (node.left != null)
q.add(node.left);
if (node.right != null)
q.add(node.right);
}
}
// 最小值元素
public E minimum(){
if(size == 0)
throw new IllegalArgumentException("空树");
Node minNode = minimum(root);
return minNode.e;
}
// 最小值节点
private Node minimum(Node node){
if( node.left == null )
return node;
return minimum(node.left);
}
// 最大值元素
public E maximum(){
if(size == 0)
throw new IllegalArgumentException("空树");
return maximum(root).e;
}
// 最大值节点
private Node maximum(Node node){
if( node.right == null )
return node;
return maximum(node.right);
}
public E removeMin(){
// 查找最小值
E ret = minimum();
// 删除最小节点
root = removeMin(root);
// 返回最小值
return ret;
}
public Node removeMin(Node node){
// node.left == null,node必然是最小节点
if (node.left == null){
// 先保存最小节点的right
Node nodeRight = node.right;
// 再把最小节点的right指向null,以便java虚拟机回收最小节点内存
node.right = null;
size--;
return nodeRight;
}
// node.left != null,递归node.left
node.left = removeMin(node.left);
return node;
}
public E removeMax(){
E ret = maximum();
root = removeMax(root);
return ret;
}
private Node removeMax(Node node){
// node.right == null,节点必然是最大节点
if(node.right == null){
// 保存最大节点的right
Node leftNode = node.left;
node.left = null;
size --;
return leftNode;
}
// node.right != null,递归node.right
node.right = removeMax(node.right);
return node;
}
public void remove(E e){
remove(root, e);
}
private Node remove(Node node, E e){
if (node == null)
return null;
if (e.compareTo(node.e) < 0){
// 递归左侧节点
node.left = remove(node.left, e);
return node;
}else if (e.compareTo(node.e) > 0){
// 递归右侧节点
node.right = remove(node.right, e);
return node;
}else {
// 删除最小值
if (node.left == null){
Node rightNode = node.right;
node.right = null;
size--;
return rightNode;
}
// 删除最大值
if (node.right == null){
Node leftNode = node.left;
node.left=null;
size--;
return leftNode;
}
//当前node有左右孩子,找出右树中的最小节点successor
Node successor = minimum(node.right);
// successor.right = 删除当前node最小值后的树
successor.right = removeMin(node.right);
// successor.left = 当前node节点的left
successor.left = node.left;
node.left = node.right = null;
// 返回successor
return successor;
}
}
public static void main(String[] args) {
_5_BinarySearchTree.BinarySearchTree<Integer> bst = new _5_BinarySearchTree.BinarySearchTree<>();
int[] nums = {28, 16, 30, 22, 13, 42, 29};
for (int num:nums){
bst.add(num);
}
System.out.println("-------前序遍历------");
bst.preOrder();
System.out.println("------中序遍历-------");
bst.inOrder();
System.out.println("------后序遍历-------");
bst.postOrder();
System.out.println("------广度优先遍历-------");
bst.levelOrder();
ArrayList<Integer> nums2 = new ArrayList<>();
//while(!bst.isEmpty())
// nums2.add(bst.removeMin());
//System.out.println(nums2);
//while(!bst.isEmpty())
// nums2.add(bst.removeMax());
//System.out.println(nums2);
//bst.remove(16);
//bst.preOrder();
}
}