常用树结构
- 二分搜索树
- 平衡二叉树:AVL、红黑树
- 堆;并查集
- 线段树;Trie(字典树、前缀树)
二叉树基础
和链表一样,二叉树是一种动态数据结构,数据存储在“节点”(Node)中,left指向左孩子,right指向右孩子
二叉树具有唯一根节点,每个节点最多有两个孩子,没有孩子的节点称为叶子节点
二叉树具有天然的递归结构,每个节点的左右子树也是二叉树
二分搜索树基础
二分搜索树是二叉树,其每个子树也是二分搜索树
二分搜索树的根节点的值满足:大于左子树的所有节点值,小于右子树的所有节点值,不能重复
也就是根节点相当于二分查找法中的mid,只需比较target和根节点的大小,就可以快速的判断出其在左子树还是右子树,然后用递归的方式直到查找到目标
递归实现二分搜索树
增、删、改、查,前序、中序、后序遍历
public class Algorithm {
public static void main(String[] args) {
BinarySearchTree<Integer> bst = new BinarySearchTree<>();
int[] nums = {5,3,6,8,4,2};
for (int i : nums) {
bst.add(i);
}
bst.preOrder();
System.out.println();
bst.inOrder();
System.out.println();
bst.postOrder();
System.out.println();
System.out.println(bst.removeMin());
System.out.println();
System.out.println(bst.removeMax());
System.out.println();
bst.remove(5);
System.out.println();
bst.preOrder();
}
}
class BinarySearchTree<E extends Comparable<E>> {
/**
* 节点类Node
*/
private class Node{
public E e;
public Node leftNext;
public Node rightNext;
private Node(E e){
this.e = e;
leftNext = null;
rightNext = null;
}
}
private Node root;
private int size;
public BinarySearchTree(){
root = null;
size = 0;
}
public int size(){
return size;
}
public boolean isEmpty(){
return size == 0;
}
/**
* 添加节点
* 将根节点和要添加的节点值传入可递归的私有add()方法
* 从根节点挨个进行递归比较,值相同则抛弃,左右孩子为空则添加
* 最后返回添加节点后新的根节点
*/
public void add(E e) {
root = add(root, e);
}
private Node add(Node root, E e){
if (root == null){
size++;
return new Node(e);
}
if (root.e.compareTo(e) < 0){
root.rightNext = add(root.rightNext, e);
}
else if (root.e.compareTo(e) > 0) {
root.leftNext = add(root.leftNext, e);
}
return root;
}
/**
* 删除最小的节点
* Min()方法在树为空时已经做出判断了,故无需再次判断
* 传入根节点进行递归查找,如果左孩子为空,说明该节点就是最小节点
* 删除最小节点以后新的根节点就是右孩子,最后返回新的根节点
*/
public E removeMin(){
E value = Min();
root = removeMin(root);
return value;
}
private Node removeMin(Node root){
if (root.leftNext == null){
Node newRoot = root.rightNext;
root.rightNext = null;
size--;
return newRoot;
}
root.leftNext = removeMin(root.leftNext);
return root;
}
/**
* 删除最大的节点
*/
public E removeMax(){
E value = Max();
root = removeMax(root);
return value;
}
private Node removeMax(Node root){
if (root.rightNext == null){
Node newRoot = root.leftNext;
root.leftNext = null;
size--;
return newRoot;
}
root.rightNext = removeMax(root.rightNext);
return root;
}
/**
* 删除任意节点(Hubbard Deletion)
* 先找到待删除节点,如果该节点有左右孩子,那就让右子树的最小节点或者左子树的最大节点顶替该节点的位置
* 先获取右子树的最小节点,其就是新的根节点,让其指向待删除节点的左右孩子,注意右孩子是删除最小节点后新的右孩子,最后安全删除节点,返回新的根节点
* 注意不能直接把最大/小节点值直接赋值给该节点,因为其可能也有左右子树
*/
public void remove(E e){
root = remove(root, e);
}
private Node remove(Node root, E e){
if (root == null){
return null;
}
if (root.e.compareTo(e) < 0){
root.rightNext = remove(root.rightNext, e);
return root;
}
else if (root.e.compareTo(e) > 0){
root.leftNext = remove(root.leftNext, e);
return root;
}
else {
if (root.leftNext == null) {
Node newRoot = root.rightNext;
root.rightNext = null;
size--;
return newRoot;
} else if (root.rightNext == null) {
Node newRoot = root.leftNext;
root.leftNext = null;
size--;
return newRoot;
} else {
/**
* removeMin()方法中已经进行过size--
*/
Node newRoot = Min(root.rightNext);
newRoot.rightNext = removeMin(root.rightNext);
newRoot.leftNext = root.leftNext;
root.leftNext = null;
root.rightNext = null;
return newRoot;
}
}
}
/**
* 修改节点
* 前序遍历,先访问根节点,再递归访问左子树和右子树
*/
public void preOrder(){
preOrder(root);
}
private void preOrder(Node root){
if (root == null) {
return;
}
System.out.println(root.e);
preOrder(root.leftNext);
preOrder(root.rightNext);
}
/**
* 中序遍历,先递归访问左子树,再访问根节点,最后递归访问右子树
* 该结果就是所有节点的升序排列
*/
public void inOrder(){
inOrder(root);
}
private void inOrder(Node root){
if (root == null){
return;
}
inOrder(root.leftNext);
System.out.println(root.e);
inOrder(root.rightNext);
}
/**
* 后序遍历,先递归访问左子树,再递归访问右子树,最后访问根节点
*/
public void postOrder(){
postOrder(root);
}
private void postOrder(Node root){
if (root == null){
return;
}
postOrder(root.leftNext);
postOrder(root.rightNext);
System.out.println(root.e);
}
/**
* 查询节点
*/
public boolean contains(E e){
return contains(root, e);
}
private boolean contains(Node root, E e){
if (root == null) {
return false;
}
if (root.e.compareTo(e) == 0){
return true;
}
else if (root.e.compareTo(e) > 0){
return contains(root.leftNext, e);
}
else {
return contains(root.rightNext, e);
}
}
/**
* 查找最小的节点
* 如果左孩子为空,则当前节点就是最小节点
*/
public E Min(){
return Min(root).e;
}
private Node Min(Node root){
if (root == null){
return null;
}
if (root.leftNext == null){
return root;
}
return Min(root.leftNext);
}
/**
* 查找最大的节点
*/
public E Max(){
return Max(root).e;
}
private Node Max(Node root){
if (root == null){
return null;
}
if (root.rightNext == null){
return root;
}
return Max(root.rightNext);
}
@Override
public String toString(){
StringBuilder str = new StringBuilder();
generateString(root, 0, str);
return str.toString();
}
public void generateString(Node root, int depth, StringBuilder str){
if (root == null) {
str.append(generateDepth(depth) + "null\n");
return;
}
str.append(generateDepth(depth) + root.e + "\n");
generateString(root.leftNext, depth + 1, str);
generateString(root.rightNext, depth + 1, str);
}
public String generateDepth(int depth){
StringBuilder str = new StringBuilder();
for (int i = 0; i < depth; i++) {
str.append("--");
}
return str.toString();
}
}
深入理解前中后序遍历(深度优先遍历DFS)
前中后指的是访问根节点的顺序,前序遍历第一次遇到该节点就打印,中序遍历第二次遇见该节点才打印,后序遍历则是第三次遇见该节点才打印
中序遍历相当于排序,可以打印出按顺序排列的所有节点
后序遍历的应用:为二分搜索树释放内存,只有先释放了子树,才能释放根节点
拓展:使用栈实现前序遍历的非递归写法
public void preOrder(){
preOrder(root);
}
public void preOrder(Node root) {
/**
* 前序遍历,在每次弹出节点后,先后压入自己的右子节点和左子节点
* 然后对栈顶节点,也就是左子节点执行相同的操作,直到所有的左子树都遍历完了,再遍历右子树
* 栈空了说明所有节点都遍历并且弹出了
*/
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);
}
}
}
中序遍历和后序遍历的非递归实现更复杂,实际应用也不广
层序遍历(广度优先遍历BFS)
根节点为第0层
使用队列实现广度优先遍历,采用非递归方式
public void levelOrder(){
levelOrder(root);
}
public void levelOrder(Node root) {
/**
* 层序遍历,每次节点出队后,先后将自己的左右子节点入队
* 然后对队首节点,也就是左子节点执行相同的操作,再让左子节点的左右子节点入队
* 队列空了说明所有节点都遍历并且出队了
*/
Queue<Node> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
Node cur = queue.remove();
System.out.println(cur.e);
if (cur.left != null) {
queue.add(cur.left);
}
if (cur.right != null) {
queue.add(cur.right);
}
}
}
常用于算法设计中——最短路径