数据结构与算法——高级数据结构:字典树/Trie树+线段树+树状数组

1. 字典树/Trie Tree

1.1 场景

  • 在海量字符串中做查询操作(但不局限于字符串问题)
  • 节约空间:10万个只包含小写字母的字符串,采用字典树可以减少内存消耗
  • 节约时间:检索效率高
  • 扩展:
    • 对数字的二进制01串构造字典树可以快速按位查询数字是否存在
    • 利用KMP的利用失配信息思想快速继续匹配,可以实现AC自动机算法
    • 对构造好的字典树进行BFS预处理并增加直达叶子节点的指针时,可以解决输入最小操作数的问题

1.2 思路

在这里插入图片描述

  • 根节点不包含字符,每条边代表一个字符

  • 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串

  • 每个节点的所有子节点包含的字符都不相同

参考链接:https://blog.csdn.net/kuronekonano/article/details/100063157

1.3 代码

class TrieNode {
	char val; // 当前节点存储的字符
    TrieNode[] next = new TrieNode[26]; // 节点下的孩子数组,通常为26个字母
    boolean isEnd = false; // 标记是否为待存储的最后一个字符,下面没有子节点
    public TrieNode() {
    	
    }
    public TrieNode(char val) {
    	this.val = val;
    }
}
class Trie {
	TrieNode root;

	public Trie() {
		root = new TrieNode();
		root.val = ' ';
	}
	
	// 1.插入一个单词到Trie树中
	public void insert(String word) {
		TrieNode cur_node = root;
		char[] chars = word.toCharArray();
		int len = chars.length;
		for (int i = 0; i < len; i ++) {
			char c = chars[i];
			if (cur_node.next[c - 'a'] == null) {
				cur_node.next[c - 'a'] = new TrieNode(c);
			}
			cur_node = cur_node.next[c - 'a'];
		}
		cur_node.isEnd = true;
	}
	
	// 2.判断某个单词是否在Trie树之中 
	public boolean get(String word) {
		TrieNode cur_node = root;
		char[] chars = word.toCharArray();
		int len = chars.length;
		for (int i = 0; i < len; i ++) {
			char c = chars[i];
			// 在单词还未走完的时候发现已经不匹配了
			if (cur_node.next[c - 'a'] == null) {
				return false;
			}
			cur_node = cur_node.next[c - 'a'];
		}
		// 如果后面还有,说明未储存word
		return cur_node.isEnd;
	}
	
	// 3.判断当前的单词是否为Trie树某个单词的前缀
	public boolean isPrefix(String word) {
		TrieNode cur_node = root;
		char[] chars = word.toCharArray();
		int len = chars.length;
		for (int i = 0; i < len; i ++) {
			char c = chars[i];
			// 在单词还未走完的时候发现已经不匹配了
			if (cur_node.next[c - 'a'] == null) {
				return false;
			}
			cur_node = cur_node.next[c - 'a'];
		}
		return true;
	}	
}
public class Test {
	public static void main(String[] args) {
		Trie trie = new Trie();
        trie.insert("abcdefg");
        trie.insert("abomn");
        trie.insert("acg");
        trie.insert("dkz");        
        
        System.out.println(trie.get("abcdefg")); // true
        System.out.println(trie.isPrefix("abc")); // true
	}
}

2. 线段树/Segment Tree

2.1 场景

给定一个数组,随机截取一段,计算区间和;随机修改数组中的元素。假设数组很大,截取或者修改的操作很频繁

2.2 思路

1.通过遍历对区间内数字进行累加:O(N),修改:O(1)
2.使用前缀和,先计算出前缀和,利用前缀和计算区间和:O(1),修改:O(N)

如何平衡计算区间和、修改的时间复杂度?使用线段树!

构造线段树:根节点包含整个区间,然后依此对区间二分,构造新节点

在这里插入图片描述
依据填入的叶子节点数据构造完整的线段树:
在这里插入图片描述
计算[2,5]区间和:在根节点处分割区间为[2],[3,5],从左子树找到区间[2]的元素,右子树根节点即[3,5],最后得出区间和:32=5+27

在这里插入图片描述
修改索引为4的元素为6:

在这里插入图片描述
将线段树转换成满二叉树,对中间的叶子节点建立虚节点:

在这里插入图片描述
将线段树转成数组:(注意满二叉树节点编号的性值)

在这里插入图片描述

2.3 代码

import java.util.Arrays;

public class SegmentTree {
	
	private static final int MAX_LEN=20;
	
	public static void main(String[] args) {
		int[] arr=new int[] {1,3,5,7,9,11};
		int[] tree=new int[MAX_LEN];
		
		build_tree(arr,tree,0,0,arr.length-1);//node=0,start=0,end=5		
		//[36, 9, 27, 4, 5, 16, 11, 1, 3, 0, 0, 7, 9, 0, 0, 0, 0, 0, 0, 0]
		System.out.println(Arrays.toString(tree));

		//[33, 9, 24, 4, 5, 13, 11, 1, 3, 0, 0, 7, 6, 0, 0, 0, 0, 0, 0, 0]
		update_tree(arr, tree, 0, 0, arr.length-1, 4, 6);
		System.out.println(Arrays.toString(tree));

		//29
		int res=query_tree(arr, tree, 0, 0, arr.length-1, 2, 5);
		System.out.println(res);

	}
	
	/**
	 * 1.对初始数组arr[]的start-end的区间构造tree[]
	 * @param node tree[]根节点
	 * @param start arr[]左边界 
	 * @param end arr[]右边界
	 */
	public static void build_tree(int[] arr,int[] tree,int node,int start,int end) {
		//设置递归出口
		if(start==end) {
			tree[node]=arr[start];
			return;
		}
		
		int mid=(start+end)/2;//mid=(0+5)/2=2
		int left_node=node*2+1;//left=1
		int right_node=node*2+2;//right=2
		
		build_tree(arr,tree,left_node,start,mid);
		build_tree(arr,tree,right_node,mid+1,end);
		
		tree[node]=tree[left_node]+tree[right_node];
		
	}
	
	/**
	 * 2.修改初始数组arr[]的idx位置的元素为val
	 */
	public static void update_tree(int[] arr,int[] tree,int node,int start,int end,int idx,int val) {
		//设置递归出口
		if(start==end) {
			arr[idx]=val;
			tree[node]=val;
			return;
		}
		
		int mid=(start+end)/2;
		int left_node=node*2+1;
		int right_node=node*2+2;
		
		//改左子树
		if(idx>=start && idx<=mid) {
			update_tree(arr, tree, left_node, start, mid, idx, val);
		}
		//改右子树
		else {
			update_tree(arr, tree, right_node, mid+1, end, idx, val);
		}
		
		tree[node]=tree[left_node]+tree[right_node];
		
	}

	/**
	 * 3.计算初始数组arr[]的[L,R]的区间和
	 */
	public static int query_tree(int[] arr,int[] tree,int node,int start,int end,int L,int R) {
		//设置递归出口
		if(R<start || end<L) {
			return 0;
		}
		if(start==end) {
			return tree[node];
		}
		if(L<=start && end<=R) {
			return tree[node];
		}
		
		int mid=(start+end)/2;
		int left_node=node*2+1;
		int right_node=node*2+2;
		
		int sum_left=query_tree(arr, tree, left_node, start, mid, L, R);
		int sum_right=query_tree(arr, tree, right_node, mid+1, end, L, R);
		
		return sum_left+sum_right;
	}

}

完整教程:

bilibili - 正月点灯笼 - 【数据结构】线段树(Segment Tree)

3. 树状数组

高效计算数组的前缀和、区间和以及解决单点更新问题
不能解决数组有增加或修改的情况
它可以以O(logn) 的时间得到任意前缀和,并同时支持在O(logn) 时间内支持动态单点值的修改
空间复杂度O(n)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

例题:

LeetCode 315. 计算右侧小于当前元素的个数

给定一个整数数组 nums,按要求返回一个新数组 counts。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。

输入:nums = [5,2,6,1]
输出:[2,1,1,0] 
解释:
5 的右侧有 2 个更小的元素 (2 和 1)
2 的右侧仅有 1 个更小的元素 (1)
6 的右侧有 1 个更小的元素 (1)
1 的右侧有 0 个更小的元素

完整教程:

bilibili - liweiwei1419 -「树状数组」知识入门(「力扣」第 315 题)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值