二叉搜索树、线段树、Trie字典树

二叉树常被用于实现二叉查找树和二叉堆。
树型结构常被用于大量数据的运行操作,处理效率大大高于线性结构的数据结构,
所以在数据结构中占据着极其重要的地位

二叉树


满二叉树

根节点:树结构的起始点

叶子节点:当树结构左右节点孩子都为空时,称为叶子节点

二叉树每个节点最多有两个孩子

二叉树每个节点最多有一个父亲

二叉树同链表一样,属于动态数据结构

静态链表和动态链表

1、静态链表是用类似于数组方法实现的,是顺序的存储结构,在物理地址上是连续的,而且需要预先分配地址空间大小。所以静态链表的长度是固定的,在做插入和删除操作时不需要移动元素,仅需修改指针。

2、动态链表是用内存申请函数动态申请内存的,所以在链表的长度上没有限制。动态链表因为是动态申请内存的,所以每个节点的物理地址不连续,要通过节点创建next指针指向下一个节点

二分搜索树(排序树)


二分搜索树是二叉树(存储的元素具有可比较性,如:数字)
二分搜索树的每个节点的值
[图片上传中...(image.png-ddd5c4-1564728631358-0)]

·大于它左子树所有节点的值


·小于它右子树所有节点的值

·每一颗子树也是二分搜索树(树型结构的天然递归特征)

二分搜索树实现
二分搜索树创建逻辑
public class BST>{
   //节点内部类
   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;
   }
}
添加新元素

这里不设计重复元素包含
若想实现包含需定义
左子树<=节点,右子树>=节点

public void add(E e){
   /*if(root == null){
      root = new Node(e);
      size ++;
   }
   else{
      add(root,e);
   }*/
   root = add(root ,e)
}
//向以node为根的二分搜索树插入元素E,递归调用
//返回插入新节点后二分搜索树的根
private Node 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;
   }*/

   //把null也当成树的一个节点
   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
public boolean contains(E e){
   return contains(root,e)
}
private boolean contains(Node node,E e){
   if(node.e == null){
      return false;
   }
   if(e.compareTo(node.e) == 0){
      return true;
   }
   else if(e.compareTo(node.e) < 0){
      contain(node.left,e);
   }
   else if(e.compareTo(node.e) > 0){
      contain(node.right,e);
   }

   return false;
}

二分搜索树的遍历


遍历就是把所有节点都访问一遍

前序遍历:根结点 ---> 左子树 ---> 右子树
中序遍历:左子树 ---> 根结点 ---> 右子树
后序遍历:左子树 ---> 右子树 ---> 根结点
前序遍历(也可称深度优先遍历)

public void preOrder(){
   preOrder(root)
}
//以node为根进行前序遍历
private void preOrder(Node node){
   if(node == null){
      return ;
   }
   System.out.println(node.e);
   preOrder(node.left);
   preOrder(node.right);
}
中序遍历(二分搜索树中序遍历结果是顺序的)

public void inOrder(){
   inOrder(root);
}
private void inOrder(Node node){
   if(node == null){
       return;
   }
   inOrder(node.left);
   System.out.println(node.e);
   inOrder(node.right);
}
后序遍历

public void tailOrder(){
   tailOrder(root);
}
private void tailOrder(Node node){
   if(node == null){
       return;
   }
   tailOrder(node.left);
   tailOrder(node.right);
   System.out.println(node.e);
}
应用:为二分搜索树释放资源

层序遍历(广度优先遍历)

利用队列实现

public void levelOrder(){
   Queue q = new LinkedList<>();
   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)
   }
}

广度优先遍历的搜索效率更高效常用于算法设计中的最短路径

删除二分搜索树最大以及最小值

//查询最小值
public E minimum(){
  return minimum(root).e;
}
//以node为根进行前序遍历
private E minimum(Node node){
   if(node == null){
      return node;
   }
   minimum(node.left);
}


//查询最大值
public E max(){
   return max(root).e;
}
//以node为根进行前序遍历
private E max(Node node){
   if(node == null){
      return node;
   }
   minimum(node.right);
}
//删除最小值
public E removeMin(){
   E ret = minimum();
   root = removeMin(root);
   return ret;
}

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 = max();
   root = removeMax(root);
   return ret;
}
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;
}
删除二分搜索树任意元素

public void remove(E e){
   root = 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);
   }

   else if(e.compareTo(node.e) > 0){
      node.right = remove(node.right ,e);
   }

   else{ // e.compareTo(node.e) == 0
      //待删除节点左子树为空
      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节点后的树
     successor.right = removeMin(node.right);
     successor.left = node.left;

     node.left = node.right = null;

     return  successor;
   }
}

删除节点可以选择自己的前驱(左子树的最大值)后继(右子树的最小值)

线段树


常用于动态变化更新的数据段:
1、区间染色


2、区间查询(如:在某个注册用户群体里统计查询消费最高最低,或者消费总和)
这一类的问题常常可以用数组来实现,但不能以更好的效率确定数量体的变化,
所以时间复杂度为O(n)
而使用线段树则可以将时间复杂度达到O(logn)以达到更好的实现效率


线段树可以通过按区间范围查询,达到高效查找执行的目的

线段树是平衡二叉树

什么是平衡二叉树?


一颗树最大深度和最小深度之间的差最多为1
平衡二叉树也是一颗完全二叉树


若把线段树多余的节点看为满二叉树,
则根据树的结构,
若二叉树有h层,则应该有2h-1个节点,大约为2h
而h-1层则有2h-1个节点,
最后一层的节点数大约等于前面所有层的节点之和

基于数组的线段树的实现


public class SegmentTree {

    private E[] tree;//把线段树看成满二叉树
    private E[] data;
    private Merger merger;

    public SegmentTree(E[] arr, Merger merger){
        //传入用户自己定义的融合器
        this.merger = merger;

        data = (E[])new Object[arr.length];

        for(int i = 0; i < arr.length; i ++){
            data[i] = arr[i];
        }

        //线段树设为满二叉树,最坏情况需要4n的节点空间构建成一棵树
        tree = (E[])new Object[4 * arr.length];
        buildSegmentTree(0,0,data.length - 1);
    }

    //三个参数分别是根节点treeIndex,根节点所对应的左区间l,以及右区间r
    //如节点为sum值,则sum(l,r)
    private void buildSegmentTree(int treeIndex, int l, int r){
        //已经遍历到最后节点,区间两端点值相等
        if(l==r){
            tree[treeIndex] = data[l];
            return;
        }

        int leftTreeIndex = leftChild(treeIndex);
        int rightTreeIndex = rightChild(treeIndex);

        //左边界+左右边界距离除2
        //可以用(l + r) / 2,但为了防止数据溢出采用以下方法
        int mid = l + (r - l) / 2;

        buildSegmentTree(leftTreeIndex,l,mid);
        buildSegmentTree(rightTreeIndex,mid+1,r);

        //若线段树业务为sum求和,则
        //这里使用融合接口,但未定义用户实现,抽象地进行了计算(重点)
        tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
    }

    public int getSize(){
        return data.length;
    }

    public E get(int index){
        return data[index];
    }

    //树充当成数组时左孩子添加的索引
    private int leftChild(int index){
        return 2*index + 1;
    }
    //树充当成数组时右孩子添加的索引
    private int rightChild(int index){
        return 2*index + 2;
    }
}

再定义一个融合接口merge
用于对区间进行操作,类似Comparator

public interface Merger {
    E merge(E a ,E b);
}

这样就实现了一颗线段树的结构

线段树的查询


//线段树的区间查询
    public E query(int queryL, int queryR){
        if (queryL < 0 || queryL > data.length ||
                queryR < 0 || queryR > data.length || queryL >queryR){
            throw new IllegalArgumentException("Index is illegal" );
        }

        return query(0,0,data.length - 1,queryL,queryR);
    }

    private E query(int treeIndex, int l, int r, int queryL, int queryR){
      //若返回的左右区间点与用户所需要的一致则返回该区间
        if(l == queryL && r = queryR){
            return tree[treeIndex];
        }
        int mid = l + (r - l) / 2;
        int leftTreeIndex = leftChild(treeIndex);
        int rightTreeIndex = rightChild(treeIndex);
      //若mid+1值小于用户查询左区间,说明l值开始的子树查询不到所需的值,则直接从右子树查询
        if(queryL >= mid + 1){
            return query(rightTreeIndex, mid + 1 , r,queryL,queryR);
        }
      //若mid值大用户查询右区间,说明l值开始的子树查询不到所需的值,则直接从右子树查询
        else if(queryR <= mid){
            return query(leftTreeIndex,l,mid,queryL,queryR);
        }
      //这部分是对于值不完全包含在区间内的递归
        E leftResult = query(leftTreeIndex,l,mid,queryL,mid);
        E rightResult = query(rightTreeIndex,mid+1,r,mid+1,queryR);

        return merger.merge(leftResult,rightResult);
    }

线段树的更新

//线段树的元素更新
    public void set(int index, E e){
        data[index] = e;
        set(0,0,data.length - 1, index, e);
    }

    private void set(int treeIndex, int l, int r, int index, E e){
        //找到需要更新的索引
        if(l == r){
            tree[treeIndex] = e;
            return;
        }

        int mid = l + (r - l) / 2;

        int leftTreeIndex = leftChild(treeIndex);
        int rightTreeIndex = rightChild(treeIndex);
c
        if(index >= mid + 1){
            set(rightTreeIndex,mid+1,r,index,e);
        }
        else if(index <= mid){
            set(leftTreeIndex,l,mid,index,e);
        }

        tree[treeIndex] = merger.merge(tree[leftTreeIndex],tree[rightTreeIndex]);
    }

线段树利用其索引性质来判断查询以及更新等操作

Trie字典树(前缀树)



Trie不是一颗二叉树,一颗多叉

这棵树的每一个节点都有若干个指向下个节点的指针

适用于快速查询,但也有占用空间问题的劣势

构建Tire字典树


字典树指向节点利用映射TreeMap来构建
来表示每个字母对应的下一个节点

import java.util.TreeMap;

public class Trie {

    private class Node{

        public boolean isWord;//当访问到当前节点是是否已经拼接成一个单词
        public TreeMap next;

        public Node(boolean isWord){
             this.isWord = isWord;
            next = new TreeMap<>();
        }

        public Node(){
            this(false);
        }
    }

    private Node root;
    private int size;

    public Trie(){
        root = new Node();
        size = 0;
    }

    public int getSize(){
        return size;
    }

    //向Trie添加一个新的单词
    public void add(String word){

        Node cur = root;
        for(int i = 0; i < word.length(); i++){
            char c = word.charAt(i);
            if(cur.next.get(c) == null)
                cur.next.put(c, new Node());
            cur = cur.next.get(c);
        }
        if(!cur.isWord){
            cur.isWord = true;
            size ++;
        }
    }

    //查询单词word是否存在
    public boolean contains(String word){
        Node cur = root;
        for (int i = 0;i < word.length(); i++){
            char c = word.charAt(i);
            if(cur.next.get(c) == null){
                return false;
            }
            cur = cur.next.get(c);
        }
        return cur.isWord;
    }
}

Trie的前缀查询


//前缀查询,prefix为前缀
    public boolean isPrefix(String prefix){
        Node cur = root;
        for (int i = 0;i < prefix.length(); i++){
            char c = prefix.charAt(i);
            if(cur.next.get(c) == null){
                return false;
            }
            cur = cur.next.get(c);
        }
        return true;
    }

Trie实现简单的模式匹配

条件:当搜索一个单词时,.可以当作是匹配任意字母

public boolean search(String word){
    return match(root, word, 0);
}

//index代表当前索引的字母
private boolean match(Node node, String word, int index){
    if(index == word.length()){
        return  node.isWord;
    }
    char c = word.charAt(index);
    //当匹配到的字符不是.的时候
    if(c != ' . '){
        if(node.next.get(c) == null){
            return false;
        }
        return match(node.next.get(c), word, index + 1);
    }
    else{
        //这里使用TreeMap的方法取出下一个节点所有的键
        for(char nextChar: node.next.keySet){
            if(match(node.next.get(nextChar), word, index + 1)){
                return true;
            }
            return false;
        }
    }
}

Trie与字符串映射的应用


实现一个 MapSum 类里的两个方法,insertsum

对于方法 insert,你将得到一对(字符串,整数)的键值对。字符串表示键,整数表示值。如果键已经存在,那么原来的键值对将被替代成新的键值对。

对于方法 sum,你将得到一个表示前缀的字符串,你需要返回所有以该前缀开头的键的值的总和。

import java.util.TreeMap;

class MapSum {

    private class Node{

        public int value;
        public TreeMap next;

        public Node(int value){
            this.value = value;
            next = new TreeMap<>();
        }

        public Node(){
            this(0);
        }
    }

    private Node root;

    public MapSum() {
        root = new Node();
    }

    public void insert(String word, int val) {
        Node cur = root;
        for(int i = 0; i < word.length(); i++){
            char c = word.charAt(i);
            if(cur.next.get(c) == null)
                cur.next.put(c, new Node());
            cur = cur.next.get(c);
        }
        cur.value = val;
    }

    public int sum(String prefix) {

        Node cur = root;
        for(int i = 0; i < prefix.length(); i++){
            char c = prefix.charAt(i);
            if(cur.next.get(c) == null){
                return 0;
            }
            cur = cur.next.get(c);
        }
        return sum(cur);
    }

    private int sum(Node node){

        //已经递归到了叶子节点
        if(node.next.size() == 0){
            return node.value;
        }

        int res = node.value;
        //判断是否存在孩子节点,若有继续循环
        for (char c:node.next.keySet()){
            res += sum(node.next.get(c));
        }
        return res;
    }
}

你可能感兴趣的:(二叉搜索树、线段树、Trie字典树)