数据结构思维 第十二章 `TreeMap`
第十二章 TreeMap
原文:Chapter 12 TreeMap
译者:飞龙
协议:CC BY-NC-SA 4.0
自豪地采用谷歌翻译
这一章展示了二叉搜索树,它是个Map
接口的高效实现。如果我们想让元素有序,它非常实用。
12.1 哈希哪里不对?
此时,你应该熟悉 Java 提供的Map
接口和HashMap
实现。通过使用哈希表来制作你自己的Map
,你应该了解HashMap
的工作原理,以及为什么我们预计其核心方法是常数时间的。
由于这种表现,HashMap
被广泛使用,但并不是唯一的Map
实现。有几个原因可能需要另一个实现:
哈希可能很慢,所以即使HashMap
操作是常数时间,“常数”可能很大。
如果哈希函数将键均匀分配给子映射,效果很好。但设计良好的散列函数并不容易,如果太多的键在相同的子映射上,那么HashMap
的性能可能会很差。
哈希表中的键不以任何特定顺序存储;实际上,当表增长并且键被重新排列时,顺序可能会改变。对于某些应用程序,必须或至少保持键的顺序,这很有用。
很难同时解决所有这些问题,但是 Java 提供了一个称为TreeMap
的实现:
- 它不使用哈希函数,所以它避免了哈希的开销和选择哈希函数的困难。
- 在
TreeMap
之中,键被存储在二叉搜索树中,这使我们可以以线性时间顺序遍历键。 - 核心方法的运行时间与
log(n)
成正比,并不像常数时间那样好,但仍然非常好。
在下一节中,我将解释二进制搜索树如何工作,然后你将使用它来实现Map
。另外,使用树实现时,我们将分析映射的核心方法的性能。
12.2 二叉搜索树
二叉搜索树(BST)是一个树,其中每个node
(节点)包含一个键,并且每个都具有“BST 属性”:
- 如果
node
有一个左子树,左子树中的所有键都必须小于node
的键。 - 如果
node
有一个右子树,右子树中的所有键都必须大于node
的键。
图 12.1:二叉搜索树示例
图 12.1 展示了一个具有此属性的整数的树。这个图片来自二叉搜索树的维基百科页面,位于 http://thinkdast.com/bst,当你做这个练习时,你会发现它很实用。
根节点中的键为8
,你可以确认根节点左边的所有键小于8
,右边的所有键都更大。你还可以检查其他节点是否具有此属性。
在二叉搜索树中查找一个键是很快的,因为我们不必搜索整个树。从根节点开始,我们可以使用以下算法:
- 将你要查找的键
target
,与当前节点的键进行比较。如果他们相等,你就完成了。 - 如果
target
小于当前键,搜索左子树。如果没有,target
不在树上。 - 如果
target
大于当前键,搜索右子树。如果没有,target
不在树上。
在树的每一层,你只需要搜索一个子树。例如,如果你在上图中查找target = 4
,则从根节点开始,它包含键8
。因为target
小于8
,你走了左边。因为target
大于3
,你走了右边。因为target
小于6
,你走了左边。然后你找到你要找的键。
在这个例子中,即使树包含九个键,它需要四次比较来找到目标。一般来说,比较的数量与树的高度成正比,而不是树中的键的数量。
因此,我们可以计算树的高度h
和节点个数n
的关系。从小的数值开始,逐渐增加:
如果h=1
,树只包含一个节点,那么n=1
。
如果h=2
,我们可以添加两个节点,总共n=3
。
如果h=3
,我们可以添加多达四个节点,总共n=7
。
如果h=4
,我们可以添加多达八个节点,总共n=15
。
现在你可能会看到这个规律。如果我们将树的层数从1
数到n
,第i
层可以拥有多达2^(n-1)
个节点。h
层的树共有2^h-1
个节点。如果我们有:
n = 2^h - 1
我们可以对两边取以2
为底的对数:
log2(n) ≈ h
意思是树的高度正比于logn
,如果它是满的。也就是说,如果每一层包含最大数量的节点。
所以我们预计,我们可以以正比于logn
的时间,在二叉搜索树中查找节点。如果树是慢的,即使是部分满的,这是对的。但是并不总是对的,我们将会看到。
时间正比于logn
的算法是对数时间的,并且属于O(logn)
的增长级别。
12.3 练习 10
对于这个练习,你将要使用二叉搜索树编写Map
接口的一个实现。
这里是实现的开头,叫做MyTreeMap
:
public class MyTreeMap<K, V> implements Map<K, V> {private int size = 0;private Node root = null;
实例变量是size
,它跟踪了键的数量,以及root
,它是树中根节点的引用。树为空的时候,root
是null
,size
是0
。
这里是Node
的定义,它在MyTreeMap
之中定义。
protected class Node {public K key;public V value;public Node left = null;public Node right = null;public Node(K key, V value) {this.key = key;this.value = value;}}
每个节点包含一个键值对,以及两个子节点的引用,left
和right
。任意子节点都可以为null
。
一些Map
方法易于实现,比如size
和clear
:
public int size() {return size;}public void clear() {size = 0;root = null;}
size
显然是常数时间的。
clear
也是常数时间的,但是考虑这个:当root
赋为null
时,垃圾收集器回收了树中的节点,这是线性时间的。这个工作是否应该由垃圾收集器的计数来完成呢?我认为是的。
下一节中,你会填充一些其它方法,包括最重要的get
和set
。
12.4 实现TreeMap
这本书的仓库中,你将找到这些源文件:
MyTreeMap.java
包含上一节的代码,其中包含缺失方法的大纲。MyTreeMapTest.java
包含单元MyTreeMap
的测试。
运行ant build
来编译源文件。然后运行ant MyTreeMapTest
。几个测试应该失败,因为你有一些工作要做!
我已经提供了get
和containsKey
的大纲。他们都使用findNode
,这是我定义的私有方法;它不是Map
接口的一部分。以下是它的起始:
private Node findNode(Object target) {if (target == null) {throw new IllegalArgumentException();}@SuppressWarnings("unchecked")Comparable<? super K> k = (Comparable<? super K>) target;// TODO: FILL THIS IN!return null;}
参数target
是我们要查找的键。如果target
是null
,findNode
抛出异常。一些Map
实现可以将null
处理为一个键,但是在二叉搜索树中,我们需要能够比较键,所以处理null
是有问题的。为了保持简单,这个实现不将null
视为键。
下一行显示如何将target
与树中的键进行比较。按照get
和containsKey
的签名(名称和参数),编译器认为target
是一个Object
。但是,我们需要能够对键进行比较,所以我们将target
强制转换为Comparable<? super K>
,这意味着它可以与类型K
(或任何超类)的示例比较。如果你不熟悉“类型通配符”的用法,可以在 http://thinkdast.com/gentut 上内容。
幸运的是,Java 的类型系统的处理不是这个练习的重点。你的工作是填写剩下的findNode
。如果它发现一个包含target
键的节点,它应该返回该节点。否则应该返回null
。当你使其工作,get
和containsKey
的测试应该通过。
请注意,你的解决方案应该只搜索通过树的一条路径,因此它应该与树的高度成正比。你不应该搜索整棵树!
你的下一个任务是填充containsValue
。为了让你起步,我提供了一个辅助方法equals
,比较target
和给定的键。请注意,树中的值(与键相反)不一定是可比较的,所以我们不能使用compareTo
;我们必须在target
上调用equals
。
不像你以前的findNode
解决方案,你的containsValue
解决方案应该搜索整个树,所以它的运行时间正比于键的数量n
,而不是树的高度h
。
译者注:这里你可能想使用之前讲过的 DFS 迭代器。
你应该填充的下一个方法是put
。我提供了处理简单情况的起始代码:
public V put(K key, V value) {if (key == null) {throw new IllegalArgumentException();}if (root == null) {root = new Node(key, value);size++;return null;}return putHelper(root, key, value);}private V putHelper(Node node, K key, V value) {// TODO: Fill this in.}
如果你尝试将null
作为关键字,put
则会抛出异常。
如果树为空,则put
创建一个新节点并初始化实例变量root
。
否则,它调用putHelper
,这是我定义的私有方法;它不是Map
接口的一部分。
填写putHelper
,让它搜索树,以及:
- 如果
key
已经在树中,它将使用新值替换旧值,并返回旧值。 - 如果
key
不在树中,它将创建一个新节点,找到正确的添加位置,并返回null
。
你的put
实现的是时间应该与树的高度h
成正比,而不是元素的数量n
。理想情况下,你只需搜索一次树,但如果你发现两次更容易搜索,可以这样做:它会慢一些,但不会改变增长级别。
最后,你应该填充keySet
。根据 http://thinkdast.com/mapkeyset 的文档,该方法应该返回一个Set
,可以按顺序迭代键;也就是说,按照compareTo
方法,升序迭代。我们在 8.3 节中使用的HashSet
实现不会维护键的顺序,但LinkedHashSet
实现可以。你可以阅读 http://thinkdast.com/linkedhashset。
我提供了一个keySet
的大纲,创建并返回LinkedHashSet
:
public Set<K> keySet() {Set<K> set = new LinkedHashSet<K>();return set;}
你应该完成此方法,使其以升序向set
添加树中的键。提示:你可能想编写一个辅助程序;你可能想让它递归;你也可能想要阅读 http://thinkdast.com/inorder 上的树的中序遍历。
当你完成时,所有测试都应该通过。下一章中,我会讲解我的解法,并测试核心方法的性能。
数据结构思维 第十二章 `TreeMap`相关推荐
- 数据结构思维 第十五章 爬取维基百科
第十五章 爬取维基百科 原文:Chapter 15 Crawling Wikipedia 译者:飞龙 协议:CC BY-NC-SA 4.0 自豪地采用谷歌翻译 在本章中,我展示了上一个练习的解决方案, ...
- 数据结构思维 第十四章 持久化
第十四章 持久化 原文:Chapter 14 Persistence 译者:飞龙 协议:CC BY-NC-SA 4.0 自豪地采用谷歌翻译 在接下来的几个练习中,我们将返回到网页搜索引擎的构建.为了回 ...
- 卜若的代码笔记-数据结构系列-第十二章:栈三.链栈
1.太简单了,不介绍了,直接贴代码,有问题请看10,11,章 //测试代码public static void main(String[] args) throws IOException {Link ...
- Android版数据结构与算法汇总十二章
Android版数据结构与算法(一):基础简介 https://www.cnblogs.com/leipDao/p/9140726.html Android版数据结构与算法(二):基于数组的实现Arr ...
- 【Java数据结构与算法】第十二章 哈夫曼树和哈夫曼编码
第十二章 哈夫曼树和哈夫曼编码 文章目录 第十二章 哈夫曼树和哈夫曼编码 一.哈夫曼树 1.基本术语 2.构建思路 3.代码实现 三.哈夫曼编码 1.引入 2.介绍 3.代码实现哈夫曼编码综合案例 一 ...
- DayDayUp:《机器崛起前传》第二十二章【蹒跚而来】读后感(文章源自网络)—听课笔记
DayDayUp:<机器崛起前传>第二十二章[蹒跚而来]读后感(文章源自网络)-听课笔记 <机器崛起前传> 1.机器岂不能有自我? 计算机的发展历程可谓蹒跚.从 ...
- 《构建之法》第十一、十二章学习总结
第十一章的内容是软件设计与实现. 在第一节中,讲的是关于分析和设计方法,向我们介绍在"需求分析"."设计与实现"阶段."测试""发 ...
- 第十二章_网络搭建及训练
文章目录 第十二章 网络搭建及训练 CNN训练注意事项 第十二章 TensorFlow.pytorch和caffe介绍 12.1 TensorFlow 12.1.1 TensorFlow是什么? 12 ...
- 《神经质人格》摘录(第十二章)
第十二章.拯救之路 文章目录 第十二章.拯救之路 一.自我认同 二.对待他人 三.勇气 四.情绪的精神化 五.高自尊 六.人格改变和心灵成长 本书的主要目的是使读者了解神经质人格的运作机制和原理,以及 ...
最新文章
- 解决windows找不到D:launcher\launcher.exe的方法
- python处理行情数据_请教 Python 如何解析 DBF 文件, SJSHQ.dbf 上交所行情文件,数据来源于巨灵数据。...
- JavaScript实现TwoQueues缓存模型
- python怎么重复输出_如何根据输出在Python中重复函数?
- dns迭代查询配置_dns解析?瞅瞅这篇文章
- 怎么把matlab 训练的model 保存下来 然后在opencv 中调用
- 对象序列化Java中的序列化
- sqlite 迁移 oracle,Oracle 数据导入 Sqlite
- 进程调度算法-先来先服务、最短作业优先调度算法和高响应比优先调度算法
- MySQL创建外键出现 ERROR 1005: Can't create table (errno: 150)解决办法
- Idea标记(或书签)功能
- Golang 实现定时任务
- vi/vim的一些干货命令及快捷键(跳转最后一行,跳转行末等)~舒服!!!
- 数组、集合、map的遍历方法
- 驱动方腔流SIMPLE方法
- 从头开始敲代码之《从BaseApplication/Activity开始(五)》(自定义控件,实现点击/滑动翻页)
- Python——二进制16位加法器(采用手算二进制加法的过程实现)(tkinter实现)【2021-07-08】
- Java 基础实验 找出1000以内的完数
- Nwafu-Oj-1444 Problem l C语言实习题七——2.结构体数组的定义与引用
- 传智播客支持中国制造2025人才培养工程
热门文章
- (38)FPGA面试题Verilog设计计数器
- c语言中index函数,MATCH+INDEX函数详解
- linux清除硬盘,linux下清除硬盘的几种方法
- python忠告_学习Python一段时间,老司机给上路新手的3点忠告!
- JsonCpp测试代码使用新API
- 东方通 no suitable default request_【官】海宁鸿翔东方郡璀璨来袭,不容错过!【营销官网】...
- linux鼠标键盘被禁用了,debian squeeze下鼠标、键盘突然被系统禁用
- 【Java数据结构与算法】第十二章 哈夫曼树和哈夫曼编码
- 力扣1103.分糖果
- java并发AtomicIntegerArray