线段树,Trie和并查集
线段树(区间树)Segment Tree
为什么要使用线段树
问题引出:区间染色问题
颜色可被覆盖
操作划分:
- 染色操作(更新区间)
- 查询操作(查询区间)
使用数组实现,染色和查询都是遍历数组的操作,时间复杂度都是O(n),而线段树可以更高效。
另一经典问题:区间查询
对于给定区间:
- 更新:更新区间中一个元素或者一个区间的值
- 查询:查询一个区间[i,j]的最大值,最小值,或者区间数字和
什么是线段树
以求和为例,每一个节点存储的是线段和。
线段树不是完全二叉树,但线段树是平衡二叉树。
平衡二叉树:对于整棵树,最大深度和最小深度之间的差最多为1
可以将它看作是满二叉树,将那些无叶子节点的根的叶子节点看作是空,故可以用数组表示。
数组表示的节点个数
我们的线段树不考虑添加元素,即区间固定,使用4n的静态空间即可。
线段树的创建
/**
* @Auther Ycj
* @Date 2021-01-21 9:33
*/
public class SegmentTree<E> {
private E[] data;
private E[] tree;
private Merger<E> merger;
public SegmentTree(E[] arr, Merger<E> merger){
this.merger = merger;
data = (E[])new Object[arr.length];
for (int i = 0; i < data.length ; i++) {
data[i] = arr[i];
}
tree = (E[])new Object[4 * arr.length];
buildSegmentTree(0, 0, data.length - 1);
}
//在treeIndex的位置创建表示区间[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);
int mid = l + (r - l) / 2;
buildSegmentTree(leftTreeIndex, l, mid);
buildSegmentTree(rightTreeIndex, mid + 1, r);
//业务逻辑部分
tree[treeIndex] = merger.merge(tree[leftTreeIndex] , tree[rightTreeIndex]);
}
public E get(int index){
if(index < 0 || index >= data.length)
throw new IllegalArgumentException("Index is illegal");
return data[index];
}
public int getSize(){
return data.length;
}
//返回完全二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引
private int leftChild(int index){
return index * 2 + 1;
}
//返回完全二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引
private int rightChild(int index){
return index * 2 + 2;
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
res.append('[');
for (int i = 0; i < tree.length; i++) {
if(tree[i] != null)
res.append(tree[i]);
else
res.append("null");
if(i != tree.length - 1)
res.append(", ");
}
res.append(']');
return res.toString();
}
}
public interface Merger<E> {
E merge(E a, E b);
}
/**
* @Auther Ycj
* @Date 2021-01-21 11:06
*/
public class Main {
public static void main(String[] args) {
Integer[] nums = {-2, 0, 3, -5, 2, -1};
SegmentTree<Integer> segmentTree = new SegmentTree<>(nums, (a,b) -> a + b );
System.out.println(segmentTree);
}
}
线段树的查询
//返回区间[querL, querR]的值
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);
}
//在以treeIndex为根的线段树中[l....r]的范围里,搜索区间[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);
if(queryL >= mid + 1)
return query(rightTreeIndex,mid + 1, r, queryL,queryR);
else if(queryR <= mid)
return query(leftTreeIndex, l, mid, queryL, queryR);
//区间有一部分在左节点,一部分在右节点
E leftRes = query(leftTreeIndex, l, mid, queryL, mid);
E rightRes = query(rightTreeIndex, mid + 1, r, mid + 1, queryR);
return merger.merge(leftRes,rightRes);
}
测试:
线段树的更新
//将index位置的值,更新为e
public void set(int index, E e){
if(index < 0 || index >= data.length)
throw new IllegalArgumentException("Index is illegal");
data[index] = e;
set(0, 0, data.length - 1, index, e);
}
//在以treeIndex为根的线段树中更新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);
if(index >= mid + 1)
set(rightTreeIndex, mid + 1, r, index, e);
else
set(leftTreeIndex, l, mid, index, e);
tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
}
字典树(前缀树)Trie
什么是Trie
常用于对单词(字符串)进行操作
例如查询单词:cat,dog,deer,panda
-
每个节点有若干个指向下个节点的指针
-
考虑不同的语言,不同的情景,比如对于区分大小写的情况,要包含52个指针
-
class Node{ char c;//可以省略 boolean isWord;//当前节点是否代表为一个单词的结尾 Map<char, Node> next; }
代码实现
import java.util.TreeMap;
/**
* @Auther Ycj
* @Date 2021-01-23 9:20
*/
public class Trie {
private class Node{
public boolean isWord;
public TreeMap<Character, Node> 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;
}
//获取Trie中存储的单词数量
public int getSize() {
return size;
}
//向Trie中新添加一个单词word
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是否在Trie中
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;
}
}
并查集
可以高效解决节点之间的连接问题
连接问题
并查集Union Find
对于一组数据,主要支持两个动作:
- union(p , q):在并查集内部将两个数据以及它们所在的集合进行合并
- isConnected(p , q):对于给定的两个数据是否属于同一个集合
public interface UF {
//不考虑添加和删除元素
boolean isConnected(int p, int q);
void unionElements(int p, int q);
int getSize();
}
Quick Find
isConnected(p, q) ----> find§ == find(q)
Quick Find 时间复杂度为O(1)
Quick Find 下的Union
例如union(1, 4)
会将两个集合的所有元素都相连,意味着属于同一个集合
/**
* @Auther Ycj
* @Date 2021-01-24 9:02
*/
//Quick Find
public class UnionFind1 implements UF {
private int[] id;
public UnionFind1(int size){
id = new int[size];
for (int i = 0; i < id.length; i++) {
id[i] = i;
}
}
//查找元素p所对应的集合编号
private int find(int p){
if(p < 0 && p >= id.length)
throw new IllegalArgumentException("p is out of bound");
return id[p];
}
//查看p和q是否属于同一个集合
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
//合并p和q所属集合
@Override
public void unionElements(int p, int q) {
int pId = find(p);
int qId = find(q);
if(pId == qId)
return;
for (int i = 0; i < id.length; i++) {
if(id[i] == pId)
id[i] = qId;
}
}
@Override
public int getSize() {
return id.length;
}
}
Quick Union
将每一个元素,看做是一个节点
树的结构为:孩子指向父亲
/**
* @Auther Ycj
* @Date 2021-01-24 9:43
*/
//Quick Union
public class UnionFind2 implements UF{
private int[] parent;
public UnionFind2(int size){
parent = new int[size];
//初始化时,每个节点都指向自己
for (int i = 0; i < size; i++) {
parent[i] = i;
}
}
//查找过程,查找元素p所对应的集合编号
//O(h)复杂度,h为树的高度
private int find(int p){
if(p < 0 && p >= parent.length)
throw new IllegalArgumentException("p is out of bound");
while (p != parent[p])
p = parent[p];
return p;
}
//O(h)复杂度,h为树的高度
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
//O(h)复杂度,h为树的高度
@Override
public void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot)
return;
parent[pRoot] = qRoot;
}
@Override
public int getSize() {
return parent.length;
}
}
基于size的优化
/**
* @Auther Ycj
* @Date 2021-01-24 10:02
*/
//基于size优化
public class UnionFind3 implements UF{
private int[] parent;
private int[] sz; //sz[i]表示以i为根的集合中元素的个数
public UnionFind3(int size){
parent = new int[size];
//初始化时,每个节点都指向自己
for (int i = 0; i < size; i++) {
parent[i] = i;
sz[i] = 1;
}
}
//查找过程,查找元素p所对应的集合编号
//O(h)复杂度,h为树的高度
private int find(int p){
if(p < 0 && p >= parent.length)
throw new IllegalArgumentException("p is out of bound");
while (p != parent[p])
p = parent[p];
return p;
}
//O(h)复杂度,h为树的高度
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
//O(h)复杂度,h为树的高度
@Override
public void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot)
return;
//根据两个元素所在树的元素个数不同判断合并方向
//将元素个数少的集合合并到元素个数多的集合上
if(sz[pRoot] < sz[qRoot]){
parent[pRoot] = qRoot;
sz[qRoot] += sz[qRoot];
}else{
parent[qRoot] = pRoot;
sz[pRoot] += sz[qRoot];
}
}
@Override
public int getSize() {
return parent.length;
}
}
基于rank的优化
rank[i] 表示根节点为i的树的高度
/**
* @Auther Ycj
* @Date 2021-01-24 10:28
*/
//基于rank优化
public class UnionFind4 implements UF{
private int[] parent;
private int[] rank; //rank[i] 表示根节点为i的集合所表示的树的层数
public UnionFind4(int size){
parent = new int[size];
//初始化时,每个节点都指向自己
for (int i = 0; i < size; i++) {
parent[i] = i;
rank[i] = 1;
}
}
//查找过程,查找元素p所对应的集合编号
//O(h)复杂度,h为树的高度
private int find(int p){
if(p < 0 && p >= parent.length)
throw new IllegalArgumentException("p is out of bound");
while (p != parent[p])
p = parent[p];
return p;
}
//O(h)复杂度,h为树的高度
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
//O(h)复杂度,h为树的高度
@Override
public void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot)
return;
//根据两个元素所在树的rank不同判断合并方向
//将rank低的集合合并到rank高的集合上
if(rank[pRoot] < rank[qRoot]){
parent[pRoot] = qRoot;
}else if(rank[pRoot] > rank[qRoot])
parent[qRoot] = pRoot;
else{
parent[qRoot] = pRoot;
rank[pRoot] += 1;
}
}
@Override
public int getSize() {
return parent.length;
}
}
路径压缩
修改find方法
//查找过程,查找元素p所对应的集合编号
//O(h)复杂度,h为树的高度
private int find(int p){
if(p < 0 && p >= parent.length)
throw new IllegalArgumentException("p is out of bound");
while (p != parent[p]){
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
}