JAVA实现数据结构:字典/哈夫曼/线段树


在这里插入图片描述

字典树(Trie树)

在这里插入图片描述

  • 定义
    又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
  • 性质
    它有3个基本性质:
    根节点不包含字符,除根节点外每一个节点都只包含一个字符;
    从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串;
    每个节点的所有子节点包含的字符都不相同。
  • 实现方法
    搜索字典项目的方法为:
    (1) 从根结点开始一次搜索;
    (2) 取得要查找关键词的第一个字母,并根据该字母选择对应的子树并转到该子树继续进行检索;
    (3) 在相应的子树上,取得要查找关键词的第二个字母,并进一步选择对应的子树进行检索。
    (4) 迭代过程……
    (5) 在某个结点处,关键词的所有字母已被取出,则读取附在该结点上的信息,即完成查找。
    其他操作类似处理
  • 应用
    串的快速检索:
    给出N个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。
    在这道题中,我们可以用数组枚举,用哈希,用字典树,先把熟词建一棵树,然后读入文章进行比较,这种方法效率是比较高的。
    “串”排序:
    给定N个互不相同的仅由一个单词构成的英文名,让你将他们按字典序从小到大输出
    用字典树进行排序,采用数组的方式创建字典树,这棵树的每个结点的所有儿子很显然地按照其字母大小排序。对这棵树进行先序遍历即可。
    最长公共前缀:
    对所有串建立字典树,对于两个串的最长公共前缀的长度即他们所在的结点的公共祖先个数,于是,问题就转化为当时公共祖先问题。
  • 实现
package 数据结构;

public class Trie {
	private int SIZE = 26;// 每层26个单词
	private TrieNode root;// 字典树的根

	Trie() // 初始化字典树
	{
		root = new TrieNode();
	}

	private class TrieNode // 字典树结点
	{
		private int num;// 有多少单词通过这个结点,即由根至该结点组成的字符串模式出现的次数
		private TrieNode[] son;// 所有的儿子结点
		private boolean isEnd;// 是不是最后一个结点
		private char val;// 结点的值
		private boolean haveSon;// 有无子结点

		TrieNode() {// 初始化结点
			num = 1;
			son = new TrieNode[SIZE];
			isEnd = false;
			haveSon = false;
		}
	}

//建立字典树
	public void insert(String str) // 在字典树中插入一个单词
	{
		if (str == null || str.length() == 0) {
			return;
		}
		TrieNode node = root;
		char[] letters = str.toCharArray();
		for (int i = 0; i < str.length(); i++) {
			int pos = letters[i] - 'a';
			if (node.son[pos] == null) {
				node.haveSon = true;
				node.son[pos] = new TrieNode();
				node.son[pos].val = letters[i];
			} else {
				node.son[pos].num++;
			}
			node = node.son[pos];
		}
		node.isEnd = true;
	}

//计算单词前缀的数量
	public int countPrefix(String prefix) {
		if (prefix == null || prefix.length() == 0) {
			return -1;
		}
		TrieNode node = root;
		char[] letters = prefix.toCharArray();
		for (int i = 0, len = prefix.length(); i < len; i++) {
			int pos = letters[i] - 'a';
			if (node.son[pos] == null) {
				return 0;
			} else {
				node = node.son[pos];
			}
		}
		return node.num;
	}

//打印指定前缀的单词
	public String hasPrefix(String prefix) {
		if (prefix == null || prefix.length() == 0) {
			return null;
		}
		TrieNode node = root;
		char[] letters = prefix.toCharArray();
		for (int i = 0, len = prefix.length(); i < len; i++) {
			int pos = letters[i] - 'a';
			if (node.son[pos] == null) {
				return null;
			} else {
				node = node.son[pos];
			}
		}
		preTraverse(node, prefix);
		return null;
	}

// 遍历经过此结点的单词.
	public void preTraverse(TrieNode node, String prefix) {
		if (node.haveSon) {
			for (TrieNode child : node.son) {
				if (child != null) {
					preTraverse(child, prefix + child.val);
				}
			}
			return;
		}
		System.out.println(prefix);
	}

//在字典树中查找一个完全匹配的单词.
	public boolean has(String str) {
		if (str == null || str.length() == 0) {
			return false;
		}
		TrieNode node = root;
		char[] letters = str.toCharArray();
		for (int i = 0, len = str.length(); i < len; i++) {
			int pos = letters[i] - 'a';
			if (node.son[pos] != null) {
				node = node.son[pos];
			} else {
				return false;
			}
		}
		return node.isEnd;
	}

//打印字典树中所有单词
	public void printAllWords() {
		TrieNode node = root;
		if (node.haveSon) {
			for (TrieNode child : node.son) {
				if (child != null) {
					hasPrefix(String.valueOf(child.val));
				}
			}
		}
	}

	// 前序遍历字典树.
	public void preTraverse(TrieNode node) {
		if (node != null) {
			System.out.print(node.val + "-");
			for (TrieNode child : node.son) {
				preTraverse(child);
			}
		}
	}

	public TrieNode getRoot() {
		return this.root;
	}

	public static void main(String[] args) {
		Trie tree = new Trie();
		String[] strs = { "banana", "band", "bee", "absolute", "acm", };
		String[] prefix = { "ba", "b", "band", "abc", };
		for (String str : strs) {
			tree.insert(str);
		}
		System.out.println(tree.has("abc"));
		tree.preTraverse(tree.getRoot());
		System.out.println();
		tree.printAllWords();
		for (String pre : prefix) {
			int num = tree.countPrefix(pre);
			System.out.println(pre + ":" + num);
		}
	}
}

在这里插入图片描述

哈夫曼树(最优二叉树)

在这里插入图片描述

  • 定义
    给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
  • 基本术语
    (1)路径和路径长度
    在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1。
    (2)结点的权及带权路径长度
    若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。
    (3)树的带权路径长度
    树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。
  • 构造
    假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则哈夫曼树的构造规则为:
    (1) 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点);
    (2) 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
    (3)从森林中删除选取的两棵树,并将新树加入森林;
    (4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。
  • 实现
package 数据结构;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;

public class HaffmanTree {
//静态内部结点类
	public static class Node<E> {
		private E data;
		private double weight;
		private Node<E> leftChild;
		private Node<E> rightChild;

		public Node(E data, double weight) {
			this.data = data;
			this.weight = weight;
		}

		public String toString() {//重写toString方法,输出时将自动调用
			return this.data+":"+  this.weight ;
		}
	}

// 实现快速排序,对节点进行排序
	public <E> void quickSort(List<Node<E>> nodes) {
		subSort(nodes, 0, nodes.size() - 1);
	}

	private <E> void subSort(List<Node<E>> nodes, int start, int end) {
		int pivot;
		if (start < end) {
			pivot = Parttion(nodes, start, end);
			subSort(nodes, start, pivot - 1);
			subSort(nodes, pivot + 1, end);
		}
	}

	private <E> int Parttion(List<Node<E>> nodes, int start, int end) {
		double pivokey = nodes.get(start).weight;
		while (start < end) {
			while (start < end && nodes.get(end).weight >= pivokey)
				end--;
			swap(nodes, start, end);
			while (start < end && nodes.get(start).weight <= pivokey)
				start++;
			swap(nodes, start, end);
		}
		return start;
	}

	private <E> void swap(List<Node<E>> nodes, int i, int j) {
		Node<E> temp;
		temp = nodes.get(i);
		nodes.set(i, nodes.get(j));

		nodes.set(j, temp);
	}

//层序遍历树
	public List<Node> BFS(Node root) {
		Queue<Node> queue = new ArrayDeque<>();
		List<Node> list = new ArrayList<Node>();
		if (root != null) {
			queue.offer(root);
		}
		while (!queue.isEmpty()) {
			Node p = queue.poll();
			list.add(p);
			if (p.leftChild != null) {
				queue.offer(p.leftChild);
			}
			if (p.rightChild != null) {
				queue.offer(p.rightChild);
			}
		}
		return list;
	}

//生成树
	public <E> Node<E> createTree(List<Node<E>> nodes) {
		while (nodes.size() > 1) {
			quickSort(nodes);
			Node<E> left = nodes.get(0);
			Node<E> right = nodes.get(1);

			Node<E> parent = new Node<E>(null, left.weight + right.weight);
			parent.leftChild = left;
			parent.rightChild = right;

			nodes.remove(nodes.get(0));
			nodes.remove(nodes.get(0));//删处原0处,原1处变为现0处

			nodes.add(parent);

		}

		return nodes.get(0);
	}

	public static void main(String[] args) {
		List<Node<String>> nodes = new ArrayList<Node<String>>();
		HaffmanTree tree = new HaffmanTree();
		nodes.add(new Node<String>("A", 5));
		nodes.add(new Node<String>("B", 15));
		nodes.add(new Node<String>("C", 40));
		nodes.add(new Node<String>("D", 30));
		nodes.add(new Node<String>("E", 10));
		Node<String> root = tree.createTree(nodes);
		System.out.println(tree.BFS(root));
	}
}

在这里插入图片描述

线段树

在这里插入图片描述
(我在实现时去掉了[1,1][2,2]这种点)

  • 定义
    线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
    对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。
    使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,因此有时需要离散化让空间压缩。

  • 基本结构
    线段树是建立在线段的基础上,每个结点都代表了一条线段[a,b]。长度为1的线段称为元线段。非元线段都有两个子结点,左结点代表的线段为[a,(a + b) / 2],右结点代表的线段为[((a + b) / 2)+1,b]。
    长度范围为[1,L] 的一棵线段树的深度为log (L) + 1。这个显然,而且存储一棵线段树的空间复杂度为O(L)。
    线段树支持最基本的操作为插入和删除一条线段。下面以插入为例,详细叙述,删除类似。
    将一条线段[a,b] 插入到代表线段[l,r]的结点p中,如果p不是元线段,那么令mid=(l+r)/2。如果b<mid,那么将线段[a,b] 也插入到p的左儿子结点中,如果a>mid,那么将线段[a,b] 也插入到p的右儿子结点中。
    插入(删除)操作的时间复杂度为O(logn)。

  • 实际应用
    最简单的应用就是记录线段是否被覆盖,随时查询当前被覆盖线段的总长度。那么此时可以在结点结构中加入一个变量int count;代表当前结点代表的子树中被覆盖的线段长度和。这样就要在插入(删除)当中维护这个count值,于是当前的覆盖总值就是根节点的count值了。另外也可以将count换成bool cover;支持查找一个结点或线段是否被覆盖。

  • 实现

package 数据结构;

public class SegmentsTree {
	Node root;

	// 线段树的树结点
	class Node {
		int left, right;// 左右区间的值
		boolean cover;// 表示是否被覆盖
		int count;// 表示此结点表示的线段区间出现的次数(被覆盖的次数),默认为0
		Node leftChild;
		Node rightChild;

		Node(int left, int right) {
			this.left = left;
			this.right = right;
			count = 0;
			cover = false;
		}

		public void setLeftChild(Node leftChild) {
			this.leftChild = leftChild;
		}

		public void setRightChild(Node rightChild) {
			this.rightChild = rightChild;
		}

		public String toString() {
			return "[" + this.left + "," + this.right + "]:" + this.count;
		}
	}

	public SegmentsTree(int left, int right) {
		root = new Node(left, right);
		build(root);
	}
//生成树
	private void build(Node node) {
		int left = node.left;
		int right = node.right;
		// root结点为叶子结点
		if (right == left) {
			node=null;
			return;
		}
		if (right - left >= 1) {
			int mid = left + (right - left) / 2;//// 将左右区间平分
			Node leftNode = new Node(left, mid);
			Node rightNode = new Node(mid + 1, right);
			node.setLeftChild(leftNode);
			node.setRightChild(rightNode);
			// 递归的创建左右子树
			build(leftNode);
			build(rightNode);
		}
	}

	// 插入一条线段[c,d]的外部接口
	public void insert(int c, int d) {
		insert(c, d, root);
	}

	// 插入一条线段[c,d]的内部实现
	private void insert(int c, int d, Node node) {
		if (node == null || c < node.left || d > node.right) {
			System.out.println("超出范围!");
			return;
		}
		if (node.left == c && node.right == d) {
			node.count++;
			node.cover = true;
			return;
		}
		int mid = (node.left + node.right) >> 1;
		if (d <= mid) {
			insert(c, d, node.leftChild);
		}

		else if (c > mid)
			insert(c, d, node.rightChild);
		else {
			insert(c, mid, node.leftChild);
			insert(mid + 1, d, node.rightChild);
		}
	}

	// 删除一条线段[c,d]的外部接口
	public void delete(int c, int d) {
		delete(c, d, root);
	}

	// 删除一条线段[c,d]的内部实现
	private void delete(int c, int d, Node node) {
		if (node == null || c < node.left || d > node.right) {
			System.out.println("输入的参数不合法!");
			return;
		}
		if (c == node.left && d == node.right) {
			node.count--;
			if (node.count == 0)
				node.cover = false;
			return;
		}
		int mid = (node.left + node.right) >> 1;
		if (d <= mid)
			delete(c, d, node.leftChild);
		else if (c > mid)
			delete(c, d, node.rightChild);
		else {
			delete(c, mid, node.leftChild);
			delete(mid + 1, d, node.rightChild);
		}
	}

	// 前序遍历 外部接口
	public void preOrder() {
		preOrder(root);
	}

	// 前序遍历 内部实现
	private void preOrder(Node node) {
		if (node.right == node.left) {
			return;
		} else {
			System.out.println(node);
			preOrder(node.leftChild);
			preOrder(node.rightChild);
		}
	}

	// 统计线段树中cover为true的线段的总长度外部接口
	public int Count() {
		return Count(root);
	}

	// 统计线段树中cover为true的线段的总长度内部实现
	private int Count(Node node) {
		if (node.cover == true)// 不继续往下查找,否则会重复
			return node.right - node.left;
		else {
			if (node.right - node.left == 1)
				return 0;
			else
				return Count(node.leftChild) + Count(node.rightChild);
		}
	}

	public static void main(String[] args) {
		SegmentsTree tree = new SegmentsTree(1, 10);
		tree.insert(2, 8);
		tree.insert(3, 6);
		tree.insert(4, 7);
		tree.delete(1, 10);
		System.out.println(tree.Count());
		tree.preOrder();
	}
}

在这里插入图片描述

发布了41 篇原创文章 · 获赞 1 · 访问量 1446

猜你喜欢

转载自blog.csdn.net/qq_44467578/article/details/104278102