高级数据结构
Trie树(字典树)
定义
trie树,又称字典树或者前缀树,是一种有序的、用于统计、排序和存储字符串的数据结构,它与二叉查找树不同,关键字不是直接保存在节点中,而是由节点在树中的位置决定。
一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。
tried树的最大有点就是利用字符串的公共前缀来减少存储空间与查询时间,从而最大限度地减少无所谓的字符串比较,是非常高效的字符串查找数据结构。
其结构入下图所示:
应用
-
自动补全功能
-
拼写检查
-
ip路由(最长前缀匹配)
-
九宫格打字预测
还有其他的数据结构,如平衡树和哈希表,使我们能够在字符串数据集中搜索单词。为什么我们还需要 Trie 树呢?尽管哈希表可以在 O(1)O(1) 时间内寻找键值,却无法高效的完成以下操作:
找到具有同一前缀的全部键值。
按词典序枚举字符串的数据集。
Trie 树优于哈希表的另一个理由是,随着哈希表大小增加,会出现大量的冲突,时间复杂度可能增加到 O(n)O(n),其中 nn 是插入的键的数量。与哈希表相比,Trie 树在存储多个具有相同前缀的键时可以使用较少的空间。此时 Trie 树只需要 O(m)O(m) 的时间复杂度,其中 mm 为键长。而在平衡树中查找键值需要 O(m \log n)O(mlogn) 时间复杂度。
代码实现
public class TrieTree {
TrieNode root = new TrieNode();
private class TrieNode{
//字典树节点
boolean isEnd;
//保存节点到其它节点的边 也就是 上图a---TireNode()
HashMap<Character, TrieNode> child = new HashMap<>();
public TrieNode(boolean isEnd, HashMap<Character, TrieNode> child) {
this.isEnd = isEnd;
this.child = child;
}
public TrieNode(){
}
public TrieNode(boolean isEnd){
this.isEnd = isEnd;
}
}
//插入单词
void insert(String word){
TrieNode pre = root;
char[] chars = word.toCharArray();
for(int i =0; i<chars.length;i++){
if(!pre.child.containsKey(chars[i])){
pre.child.put(chars[i],new TrieNode());
}
pre = pre.child.get(chars[i]);
}
pre.isEnd = true;
}
// 搜索单词是否存在
boolean search(String word){
TrieNode pre = root;
char[] chars = word.toCharArray();
for (char aChar : chars) {
if(!pre.child.containsKey(aChar)){
return false;
}
pre = pre.child.get(aChar);
}
return pre.isEnd;
}
//搜索以单词为开头是否存在
boolean startWith(String word){
TrieNode pre = root;
char[] chars = word.toCharArray();
for (char aChar : chars) {
if(!pre.child.containsKey(aChar)){
return false;
}
pre = pre.child.get(aChar);
}
return true;
}
TrieNode root(){
return root;
}
}
题目
LeetCode 211 添加与搜索单词 - 数据结构设计
思路:
- 添加方法和字典树的实现没有差别,主要是差别在搜索上面。
- 搜索的时候多了一个‘.’的正则表达式,它代表匹配所有的字符。那么搜索的时候逻辑就分为下面几种,当前搜索字符是不是‘.’, 如果是需要遍历当前节点的所有孩子节点,因为‘.’匹配任意孩子; 这就是一个深度遍历过程,遍历的退出条件是当单词被遍历完而其当前节点的isEnd属性为true,就表明存在该单词。
- 如果当前字符不是‘.’,那么就先判断当前节点的孩子里面存不存在该字符,如果存在深度遍历该节点就可以。
代码:
class WordDictionary {
TrieTree tree = new TrieTree();
/** Initialize your data structure here. */
public WordDictionary() {
}
/** Adds a word into the data structure. */
public void addWord(String word) {
tree.insert(word);
}
/** Returns if the word is in the data structure. A word could contain the dot character '.' to represent any one letter. */
public boolean search(String word) {
return tree.search(tree.root,word,0);
}
private class TrieTree {
TrieNode root = new TrieNode();
private class TrieNode{
//字典树节点
boolean isEnd;
//保存节点到其它节点的边 也就是 上图a---TrieNode()
HashMap<Character, TrieNode> child = new HashMap<>();
public TrieNode(boolean isEnd, HashMap<Character, TrieNode> child) {
this.isEnd = isEnd;
this.child = child;
}
public TrieNode(){
}
public TrieNode(boolean isEnd){
this.isEnd = isEnd;
}
}
//插入单词
void insert(String word){
TrieNode pre = root;
char[] chars = word.toCharArray();
for(int i =0; i<chars.length;i++){
if(!pre.child.containsKey(chars[i])){
pre.child.put(chars[i],new TrieNode());
}
pre = pre.child.get(chars[i]);
}
pre.isEnd = true;
}
// 搜索单词是否存在
boolean search(TrieNode pre,String word,int point){
//遍历终止条件
if(point == word.length()) return pre.isEnd;
char val = word.charAt(point);
if('.'== val){
//遍历所有孩子
for (Map.Entry<Character, TrieNode> characterTrieNodeEntry : pre.child.entrySet()) {
TrieNode value = characterTrieNodeEntry.getValue();
if(search(value,word,point+1)){
return true;
}
}
}
else{
return (pre.child.containsKey(val)&&search(pre.child.get(val),word,point+1));
}
return false;
}
}
}
并查集
并查集(Union Find),又称不相交集合(Disjiont Set), 它应用于N个元素的集合求并与查询问题,在该应用场景中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。虽然该问题并不复杂,但面对极大的数据量时,普通的数据结构往往无法解决,并查集就是解决该种问题最为优秀的算法。
代码实现
public class DisjoinSet {
//代表并查集中集合的个数
int count;
//代表数字i集合是谁, 数组小标表示数字i,id[i]表示i所在的集合
int[] id;
//表示集合中成员数量 数组下标表示集合,size[i]表示集合中元素的个数
int[] size;
public DisjoinSet( int n){
id = new int[n];
size = new int[n];
//初始化,
for (int i = 0; i < n; i++) {
//每一个元素自己代表一个集合
id[i] = i;
//每个集合都有一个元素
size[i] =1;
}
count=n;
}
//查找元素p属于哪一个集合
Integer find(int p){
//如果元素p自己一个集合,那么它的下标和自己一定相等
while(id[p]!=p){
//如果不等,说明它挂在别的集合上面,它的id[p]表示它的下挂集合,但是不一定它的下挂集合是最终集合,因为可能下挂集合也别的集合下面,所以我们最终要一直找到p=id[p]的节点。
//这一步是优化,我们隔着一个元素来找提高速度
id[p] = id[id[p]];
p = id[p];
}
return p;
}
//合并两个元素到一个集合
void union(int p,int q){
//找到p所在的集合
int pid = find(p);
//找到q所在的集合
int qid = find(q);
//如果集合相等,返回
if(pid == qid) return;
//合并过程,我们把小集合的头指针插入到大集合的头指针后面
if(size[pid]<size[qid]){
id[pid] = qid;
size[qid]+=size[pid];
}else{
id[qid] = pid;
size[pid]+=size[qid];
}
//每合并一个集合,整体的集合数量减一
count--;
}
}
题目
LeetCode 574 省份数量
思路:
- 利用并查集来实现,每一个省份代表一个集合,如果两个省份相连 就把这两个集合union.
- 遍历所有省份,返回count就是省份数量
代码:
class Solution {
public int findCircleNum(int[][] isConnected) {
DisjoinSet d = new DisjoinSet(isConnected.length);
for(int i = 0; i<isConnected.length;i++){
for( int j = i+1 ; j<isConnected[i].length;j++){
if(isConnected[i][j] ==1){
d.union(i,j);
}
}
}
return d.count;
}
public class DisjoinSet {
int count;
int[] id;
int[] size;
DisjoinSet( int n){
id = new int[n];
size = new int[n];
for (int i = 0; i < n; i++) {
id[i] = i;
size[i] =1;
}
count=n;
}
Integer find(int p){
while(id[p]!=p){
id[p] = id[id[p]];
p = id[p];
}
return p;
}
void union(int p,int q){
int pid = find(p);
int qid = find(q);
if(pid == qid) return;
if(size[pid]<size[qid]){
id[pid] = qid;
size[qid]+=size[pid];
}else{
id[qid] = pid;
size[pid]+=size[qid];
}
count--;
}
}
}
线段树
定义
线段树是一种平衡二叉搜索树(完全二叉树),它将一个线段区间划分成一些单元区间。对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a.(a+b)/2],右儿子表示的区间是[(a+b)/2 +1,b],最后的叶子节点数目为N, 与数组下标对应。线段树的一般包括建立、查询、插入、更新等操作,建立规模为N的时间复杂度是O(NlogN),其他操作时间复杂度为O(logN)。
代码实现
public class SegmentTree {
//构建线段树
//value[] 代表线段区间内的和,默认vaule[0]标识nums整个区间的和,它用数组来表示树。
//那么它的左子树表示它数组集合mid左边那一半数据的和,右子树是mid右边的和
//涉及到树的一般都是递归处理,这个要记住
//nums待处理数字集合、pos表示value数组指针,left表示待遍历nums[]左区间,right表示右
void build(int[] value, int[] nums, int pos, int left, int right){
//如果left等于right 表示遍历到单一节点,而不是一个区间,这时候我们直接去nums[left] = value[pos]
if(left == right){
value[pos] = nums[left];
return;
}
//计算中间节点,将目标数组nums[]分段
int mid = (left+right) /2 ;
//递归左段和左子树
build(value,nums,2*pos+1,left,mid);
//递归右段和右子树
build(value,nums,2*pos+2,mid+1,right);
//子节点完成之后,父节点的值等于两个子节点的值相加
value[pos] = value[2*pos+1]+value[2*pos+2];
}
//求gleft,到gright这段区间的和
int sum(int[] value, int pos, int left, int right,int gleft, int gright){
if(left>gright || right<gleft){
return 0;
}
//如果区间再value的区间里面,直接返回value的值。
if(gleft<=left && gright>=right) return value[pos];
int mid = (left+right)/2;
//如果不在,就分区域查询。肯定会查到,最差就是查到子节点
return sum(value,pos*2+1,left,mid,gleft,gright)+sum(value,pos*2+2,mid+1,right,gleft,gright);
}
//更新某个节点的值
void update(int[] value,int pos,int left, int right, int index, int newValue){
//跳出条件,当left==right也就是子节点,等于nums节点,等于你想要修改的index节点时
if((left+right)/index==2){
//更新该index对应value[]的值
value[pos] = newValue;
return;
}
//还是左右段递归遍历
int mid = (left+right)/2;
if(left <= index && index <= mid){
update(value,pos*2+1,left,mid,index,newValue);
}else{
update(value,pos*2+2,mid+1,right,index,newValue);
}
//如果完成了修改,还要修改它父节点的值
value[pos]=value[pos*2+1]+value[pos*2+2];
}
}
LeetCode 307 区域和检索 - 数组可修改
思路:
- 典型利用线段树解决的问题,求i到j的区域总和,就代表values[]中存在的值。
代码:
class NumArray {
int []value ;
SegmentTree tree = new SegmentTree();
int rightM;
public NumArray(int[] nums) {
if(nums.length == 0) return;
//value值一般是nums值得4倍
value = new int[nums.length*4];
rightM = nums.length-1;
//构建线段树
tree.build(value,nums,0,0,nums.length-1);
}
public void update(int i, int val) {
tree.update(value,0,0,rightM,i,val);
}
public int sumRange(int i, int j) {
return tree.sum(value,0,0,rightM,i,j);
}
class SegmentTree {
void build(int[] value, int[] nums, int pos, int left, int right){
if(left == right){
value[pos] = nums[left];
return;
}
int mid = (left+right) /2 ;
build(value,nums,2*pos+1,left,mid);
build(value,nums,2*pos+2,mid+1,right);
value[pos] = value[2*pos+1]+value[2*pos+2];
}
int sum(int[] value, int pos, int left, int right,int gleft, int gright){
if(left>gright || right<gleft){
return 0;
}
if(gleft<=left && gright>=right) return value[pos];
int mid = (left+right)/2;
return sum(value,pos*2+1,left,mid,gleft,gright)+sum(value,pos*2+2,mid+1,right,gleft,gright);
}
void update(int[] value,int pos,int left, int right, int index, int newValue){
if(right == left && left == index){
value[pos] = newValue;
return;
}
int mid = (left+right)/2;
if(left<=index&&index<=mid){
update(value,pos*2+1,left,mid,index,newValue);
}else{
update(value,pos*2+2,mid+1,right,index,newValue);
}
value[pos]=value[pos*2+1]+value[pos*2+2];
}
}
}