一. 为什么要研究树结构
- 树结构的本身是一种天然的组织结构
- 将数据使用树结构存储后,出奇的高效
二分搜索树(Binary Search Tree)
平衡二叉树:AVL; 红黑树
堆;并查集
线段树;Trie(字典树, 前缀树)
二. 二分搜索树基础
1.二叉树
2. 二分搜索树
代码框架搭建
新建项目BST
·
├── BST.iml
└── src
├── BST.java
└── Main.java
BST.java
public class BST<E extends Comparable> { // 对类型进行限制,并非可以存储所有类型, 要求必须是可以比较的类型
private class Node{
public E e;
public Node left, right;
public Node(E e){
this.e = e;
left = null;
right = null;
}
}
private Node root;
private int size;
public BST(){
root = null;
size = 0;
}
public int size(){
return size;
}
public boolean isEmpty(){
return size == 0;
}
}
三. 向二分搜索树添加元素
BST.java
public class BST<E extends Comparable> {
private class Node{
public E e;
public Node left, right;
public Node(E e){
this.e = e;
left = null;
right = null;
}
}
private Node root;
private int size;
public BST(){
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); // 左子树继续插入元素e
}
else{
add(node.right, e); // 右子树继续插入元素e
}
}
}
三. 改进添加操作: 深入理解递归终止条件
修改递归终止条件:
- null也为节点, 以给null节点赋值为终止条件
BST.java
public class BST<E extends Comparable> {
...
// 向二分搜索树中添加新元素e
public void add(E e) {
root = add(root, e);
}
// 向以node为根的二分搜索树中插入元素e, 递归算法
// 返回插入新节点后二分搜索树的根
private Node add(Node node, E e) {
if (node == null) {
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;
}
}
四. 二分搜索树的查询操作
新增方法: 查看二分搜索树是否包含元素e BST.java
public class BST<E extends Comparable> {
...
// 查看二分搜索树是否包含元素e
public boolean contains(E e){
return conatins(root, e);
}
// 查看以node为根的二分搜索树是否包含元素e, 递归算法
private boolean contains(Node node, E e){
if(node == null){
return false;
}
if(e.compareTo(node.e) == 0){
return true;
}
else if(e.compareTo(node.e) < 0){
return contains(node.left, e);
}
else{
return contains(node.right, e);
}
}
}
六. 二分搜索树的前序遍历
前序遍历:先访问该节点,再遍历该节点的子节点
代码实现
BST.java
public class BST<E extends Comparable> {
...
// 二分搜索树的前序遍历
public void preOder(){
preOrder(root);
}
//前序遍历以node为根的二分搜索树
private void preOrder(Node node){
if(node == null){
return;
}
System.out.println(node.e);
preOrder(node.left);
preOrder(node.right);
}
}
测试
Main.java
public class Main {
public static void main(String[] args) {
BST<Integer> bst = new BST<>();
int[] nums = {5,3,6,8,4,2};
for(int num: nums){
bst.add(num);
}
bst.preOder();
}
}
运行结果:
5
3
2
4
6
基于前序遍历,重写toString
BST.java
public class BST<E extends Comparable> {
...
@Override
public String toString() {
StringBuilder res = new StringBuilder();
generateBSTString(root, 0, res);
return res.toString();
}
// 基于前序遍历,生成以node为根结点,深度为depth的描述二叉树字符串
private void generateBSTString(Node node, int depth, StringBuilder res) {
if (node == null) {
res.append(generateDepthString(depth) + "null\n");
return;
}
res.append(generateDepthString(depth) + node.e + "\n");
generateBSTString(node.left, depth + 1, res);
generateBSTString(node.right, depth + 1, res);
}
// 打印节点深度
private String generateDepthString(int depth) {
StringBuilder res = new StringBuilder();
for (int i = 0; i < depth; i++) {
res.append("--");
}
return res.toString();
}
}
Main.java
public class Main {
public static void main(String[] args) {
BST<Integer> bst = new BST<>();
int[] nums = {5,3,6,8,4,2};
for(int num: nums){
bst.add(num);
}
//bst.preOder();
System.out.println(bst);
}
}
结果可以看到节点层次很清晰:
5
--3
----2
------null
------null
----4
------null
------null
--6
----null
----8
------null
------null
七. 二分搜索树的中序遍历和后序遍历
前序遍历是最自然的遍历方式, 也是最常用的。
但既然有前序遍历肯定也有中序遍历和后续遍历。
中序遍历的顺序:
1. 先访问 node.left
2. 再访问 node.e
3. 最后访问 node.right
中序遍历的应用: 按大小排序
BST.java
public class BST<E extends Comparable> {
...
// 中序遍历
public void inOrder(){
inOrder(root);
}
// 以node为根的中序遍历
private void inOrder(Node node){
if(node == null){
return;
}
inOrder(node.left);
System.out.println(node.e);
inOrder(node.right);
}
}
测试 MAin.java
public class Main {
public static void main(String[] args) {
BST<Integer> bst = new BST<>();
int[] nums = {5,3,6,8,4,2};
for(int num: nums){
bst.add(num);
}
//bst.preOder();
//System.out.println(bst);
bst.inOrder();
}
}
运行结果, 正好是排序结果
2
3
4
5
6
8
后序遍历的应用:
为二分搜索树释放内存
BST.java
public class BST<E extends Comparable> {
...
// 后序遍历
public void postOrder(){
postOrder(root);
}
// 以node为根的后序遍历
private void postOrder(Node node){
if(node == null){
return;
}
inOrder(node.left);
inOrder(node.right);
System.out.println(node.e);
}
八. 再看二分搜索树的遍历
我要做到给一个二分搜索树, 能够立马看出三种遍历得到的结果。
九. 二分搜索树前序遍历的非递归实现
流程:
1. node压入栈
2. 取出node
3. node.right压入栈
4. node.left压入栈
5. 取出node.left
6. node.left.right压入栈
7. node.left.left压入栈(假设node.left.left之后,node的左子树没有剩余元素了)
8. 取出node.left.left
9. 取出node.left.right
10.取出node.right
11.node.right.right压入栈
12.node.right.left压入栈(假设node.right.left之后,node的右子树没有剩余元素了)
13.取出node.right.left
14.取出node.right.right
BST.java
import java.util.Stack;
public class BST<E extends Comparable> {
...
// 非递归前序遍历
public void preOrderNR(){
Stack<Node> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()){
Node cur = stack.pop();
System.out.println(cur.e);
if(cur.right != null) {
stack.push(cur.right);
}
if(cur.left!=null){
stack.push(cur.left);
}
}
}
}
- 二分搜索树的非递归实现,比递归实现复杂很多
- 中序遍历和后序遍历的非递归实现更复杂
- 中序遍历和后序遍历的非递归实现,实际应用不广
十. 二分搜索树的层序遍历
BST.java
import java.util.Stack;
import java.util.Queue;
import java.util.LinkedList;
public class BST<E extends Comparable> {
...
// 层序遍历
public void levelOrder(){
Queue<Node> q = new LinkedList<>(); // java的Queue只是接口, 我们需要使用LinkedList来具体承载Queue
q.add(root);
while(!q.isEmpty()){
Node cur = q.remove();
System.out.println(cur.e);
if(cur.left != null){
q.add(cur.left);
}
if(cur.right != null){
q.add(cur.right);
}
}
}
}
十一. 删除二分搜索树的最大元素和最小元素
二分搜索树最左边的节点是最小节点, 最右边的节点是最大节点。
BST.java
import java.util.Stack;
import java.util.Queue;
import java.util.LinkedList;
public class BST<E extends Comparable> {
...
// 寻找二分搜索树的最小元素
public E minimum() {
if (size == 0) {
throw new IllegalArgumentException("BST is empty!");
}
return minimum(root).e;
}
// 返回以node为根的二分搜索树的最小值所在的节点
private Node minimum(Node node) {
if (node.left == null) {
return node;
}
return minimum(node.left);
}
// 寻找二分搜索树的最大元素
public E maximum() {
if (size == 0) {
throw new IllegalArgumentException("BST is empty!");
}
return maximum(root).e;
}
// 返回以node为根的二分搜索树的最大值所在的节点
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;
}
// 删除以node为根的最小节点, 返回删除节点后的根
private Node removeMin(Node node) {
if (node.left == null) {
Node rightNode = node.right;
node.right = null;
size--;
return rightNode;
}
node.left = removeMin(node.left);
return node;
}
// 删除最大值所在的节点, 返回最大值
public E removeMax() {
E ret = maximum();
root = removeMax(root);
return ret;
}
// 删除以node为根的最大节点, 返回删除节点后的根
private Node removeMax(Node node) {
if (node.right == null) {
Node leftNode = node.left;
node.left = null;
size--;
return leftNode;
}
node.right = removeMax(node.right);
return node;
}
}
测试Main.java
import java.util.ArrayList;
import java.util.Random;
public class Main {
public static void main(String[] args) {
BST<Integer> bst = new BST<>();
Random random = new Random();
int n = 1000;
for (int i = 0; i < n; i++) {
bst.add(random.nextInt(1000)); // 生成1000个随机数,放入二分搜索树
}
ArrayList<Integer> minnums = new ArrayList<>();
while (!bst.isEmpty())
minnums.add(bst.removeMin()); // 按从小到大的顺序从二分搜索树中删除, 装入minnums
System.out.println(minnums);
for (int i = 1; i < minnums.size(); i++) { // 如果不是从小到大排序,就报错
if (minnums.get(i - 1) > minnums.get(i))
throw new IllegalArgumentException("Error");
}
System.out.println("removeMin ok!");
for (int i = 0; i < n; i++) {
bst.add(random.nextInt(1000));
}
ArrayList<Integer> maxnums = new ArrayList<>();
while (!bst.isEmpty())
maxnums.add(bst.removeMax());
System.out.println(maxnums);
for (int i = 1; i < maxnums.size(); i++) { // 如果不是从大到小排序,就报错
if (maxnums.get(i - 1) < maxnums.get(i))
throw new IllegalArgumentException("Error");
}
System.out.println("removeMax ok!");
}
}
运行结果:
[0, 2, 3, 4, 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 25, 26, 28, 29, 31...
removeMin ok!
[999, 997, 996, 994, 993, 992, 989, 988, 983, 980, 978, 975, 974, 972, 971...
removeMax ok!
十二. 删除二分搜索树的任意元素
上面的图中流程中
是将 59放在被删节点d的位置上(后驱的方式),
我们也可以将53放在被删除元素d的位置上(前驱的方式)
BST.java
import java.util.Stack;
import java.util.Queue;
import java.util.LinkedList;
public class BST<E extends Comparable> {
...
// 从二分搜索树中删除元素为e的节点
public void remove(E e) {
root = remove(root, e);
}
// 删除以node为根的二分搜索树中 值为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);
} else if (e.compareTo(node.e) > 0) {
node.right = remove(node.right, e);
} else { // e 与node.e相等的情况, 即删除node
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 = minimum(node.right);
successor.right = removeMin(node.right); // removeMin中进行过size--
successor.left = node.left;
node.left = node.right = null;
return successor;
}
}
}
十三. 更多的二分搜索树话题
1. floor和ceil
给一个元素(比一定要在二分搜索树中), 找出树中刚好比它小(floor)和刚好比它大(ceil)的元素
2. rank和select
- rank: 给出一个树中的元素, 得出它的排名
- select:给出一个排名, 找出该排名对应的元素
实现rank和select比较好的方式 就是: 构建 维护size的二分搜索树