原文:普林斯顿大学算法课程 译者:飞龙 协议:CC BY-NC-SA 4.0
原文:
algs4.cs.princeton.edu/42digraph
译者:飞龙 协议:CC BY-NC-SA 4.0
一个有向图(或有向图)是一组顶点和一组有向边,每条边连接一个有序对的顶点。我们说一条有向边从该对中的第一个顶点指向该对中的第二个顶点。对于 V 个顶点的图,我们使用名称 0 到 V-1 来表示顶点。
这里是我们使用的一些定义。
w
是从顶点 v
可达的,如果存在一条从 v
到 w
的有向路径。
v
和 w
是强连通的,那么它们是相互可达的:从 v
到 w
有一条有向路径,从 w
到 v
也有一条有向路径。
我们实现了以下有向图 API。
关键方法 adj()
允许客户端代码遍历从给定顶点邻接的顶点。
我们使用以下输入文件格式准备测试数据 tinyDG.txt。
我们使用邻接表表示法,其中我们维护一个以顶点为索引的列表数组,其中包含与每个顶点通过边连接的顶点。
Digraph.java 使用邻接表表示法实现了有向图 API。AdjMatrixDigraph.java 使用邻接矩阵表示法实现了相同的 API。
深度优先搜索和广度优先搜索是基本的有向图处理算法。
s
,是否存在一条从 s 到 v 的有向路径?如果是,找到这样的路径。DirectedDFS.java 使用深度优先搜索来解决这个问题。
s
,是否存在一条从 s 到 v 的有向路径?如果是,找到这样的路径。DepthFirstDirectedPaths.java 使用深度优先搜索来解决这个问题。
s
,是否存在从 s 到 v 的有向路径?如果有,找到一条最短的这样的路径。BreadthFirstDirectedPaths.java 使用广度优先搜索来解决这个问题。
在涉及处理有向图的应用中,有向循环尤为重要。输入文件 tinyDAG.txt 对应于以下 DAG:
DepthFirstOrder.java 计算这些顺序。
有向图具有拓扑顺序当且仅当它是 DAG。
DAG 中的逆后序是拓扑排序。
使用深度优先搜索,我们可以在时间上将 DAG 进行拓扑排序,时间复杂度为 V + E。
强连通性是顶点集合上的等价关系:
强连通性将顶点划分为等价类,我们简称为强连通分量。我们试图实现以下 API:
令人惊讶的是,KosarajuSharirSCC.java 仅通过在 CC.java 中添加几行代码就实现了该 API,如下所示:
dfs()
的调用到达的所有顶点都在一个强连通分量中(!),因此像在 CC 中一样识别它们。
Kosaraju-Sharir 算法使用预处理时间和空间与 V + E 成比例,以支持有向图中的常数时间强连通性查询。
有向图 G 的传递闭包是另一个有向图,具有相同的顶点集,但如果且仅当在 G 中从 v 到 w 可达时,有一条从 v 到 w 的边。
TransitiveClosure.java 通过从每个顶点运行深度优先搜索并存储结果来计算有向图的传递闭包。这种解决方案非常适合小型或密集的有向图,但不适用于我们在实践中可能遇到的大型有向图,因为构造函数使用的空间与 V² 成比例,时间与 V (V + E) 成比例。
Digraph
的内存使用情况,根据第 1.4 节的内存成本模型。
解决方案. 56 + 40V + 64E。MemoryOfDigraph.java 根据经验计算,假设没有缓存Integer
值—Java 通常会缓存-128 到 127 之间的整数。
提示:创建一个有向图,如果箱子 i 嵌套在箱子 j 内部,则从箱子 i 到箱子 j 添加一条边。然后运行拓扑排序。
tinyDG.txt
上。
带权重的图 是一种我们为每条边关联权重或成本的图。带权重图的*最小生成树(MST)*是其边权重之和不大于任何其他生成树的生成树。
为了简化演示,我们采用以下约定:
我们回顾树的两个定义性质:
图的切割是将其顶点划分为两个不相交集合。跨越边是连接一个集合中的顶点与另一个集合中的顶点的边。我们假设为简单起见,所有边的权重都是不同的。在此假设下,MST 是唯一的。定义切割和循环。以下性质导致多种 MST 算法。
在带权重图中的任何切割中(所有边权重不同),最小权重的跨越边在图的 MST 中。
切割性质是��们考虑 MST 问题的算法的基础。具体来说,它们是贪心算法的特例。
以下方法将所有连接的带权重图的 MST 中的所有边涂黑:从所有边都涂灰色开始,找到没有黑色边的切割,将其最小权重的边涂黑,继续直到涂黑 V-1 条边。
最小生成树问题](…/Images/fce4a44e5b52cd8391fb6ea99f7fa182.png)
我们使用以下 API 表示带权重的边:
either()
和 other()
方法用于访问边的顶点;compareTo()
方法通过权重比较边。Edge.java 是一个直接的实现。
我们使用以下 API 表示带权重的图:
我们允许平行边和自环。EdgeWeightedGraph.java 使用邻接表表示法实现 API。
我们使用以下 API 计算带权重图的最小生成树:
我们准备了一些测试数据:
Prim 算法通过在每一步将新边附加到单个增长树上来工作:从任何顶点开始作为单个顶点树;然后向其添加 V-1 条边,始终取下一个(着色为黑色)连接树上顶点与尚未在树上的顶点的最小权重边(对于由树顶点定义的切割的跨越边)。
Prim 算法的一句描述留下了一个关键问题:我们如何(高效地)找到最小权重的跨越边?
PrimMST.java 是这种急切方法的实现。它依赖于 IndexMinPQ.java 索引优先队列来执行减少键操作。
Prim 算法计算任何连通的边权重图的最小生成树。Prim 算法的懒惰版本使用空间与 E 成比例,时间与 E log E 成比例(在最坏情况下)来计算具有 E 条边和 V 个顶点的连通边权重图的最小生成树;急切版本使用空间与 V 成比例,时间与 E log V 成比例(在最坏情况下)。
Kruskal 算法按照它们的权重值(从小到大)的顺序处理边,每次添加不与先前添加的边形成循环的边作为 MST(着色为黑色),在添加 V-1 条边后停止。黑色边形成逐渐演变为单一树 MST 的树林。
要实现 Kruskal 算法,我们使用优先队列按权重顺序考虑边,使用并查集数据结构标识导致循环的边,使用队列收集最小生成树边。程序 KruskalMST.java 按照这些方式实现了 Kruskal 算法。它使用了辅助的 MinPQ.java、UF.java 和 Queue.java 数据类型。
Kruskal 算法使用额外空间与 E 成正比,时间与 E log E 成正比(在最坏情况下)来计算具有 E 条边和 V 个顶点的任何连通边权图的最小生成树。
compareTo()
方法访问边权重。给每个权重添加一个正常数(或乘以一个正常数)不会改变 compareTo()
方法的结果。
compareTo()
方法中反转比较的意义)。
Integer
值来进行经验计算—Java 通常会缓存 -128 到 127 的整数。
toString()
。
distTo[]
数组中的所有 V
个条目,找到具有最小值的非树顶点。在具有 V 个顶点和 E 条边的图上,Prim 算法的急切版本的最坏情况运行时间的增长顺序是多少?如果有的话,什么时候这种方法是合适的?为什么?请解释你的答案。
解决方案. Prim 算法的运行时间将与 V² 成正比,这对于稠密图是最佳的。
edges()
。
check()
的方法,使用以下割优化条件来验证提议的边集是否实际上是最小生成树(MST):如果一组边是一棵生成树,并且每条边都是通过从树中移除该边定义的割的最小权重边,则这组边就是 MST。你的方法的运行时间增长率是多少?
解决方案。 KruskalMST.java。
原文:
algs4.cs.princeton.edu/44sp
译者:飞龙 协议:CC BY-NC-SA 4.0
加权有向图是一个有向图,其中我们为每条边关联权重或成本。从顶点 s 到顶点 t 的最短路径是从 s 到 t 的有向路径,具有没有更低权重的其他路径的属性。
我们总结了几个重要的属性和假设。
我们使用以下 API 表示加权边:
from()
和to()
方法对于访问边的顶点很有用。DirectedEdge.java 实现了这个 API。
我们使用以下 API 表示加权有向图:
EdgeWeightedDigraph.java 使用邻接表表示实现了该 API。
我们使用以下 API 计算加权有向图的最短路径:
我们准备了一些测试数据:
给定一个加权有向图和一个指定的顶点 s,最短路径树(SPT)是一个子图,包含 s 和所有从 s 可达的顶点,形成以 s 为根的有向树,使得每条树路径都是图中的最短路径。
我们用两个顶点索引数组表示最短路径:
edgeTo[v]
是从 s 到 v 的最短路径上的最后一条边。
distTo[v]
是从 s 到 v 的最短路径的长度。
我们的最短路径实现基于一种称为松弛的操作。我们将distTo[s]
初始化为 0,对于所有其他顶点 v,将distTo[v]
初始化为无穷大。
边松弛。 对边 v->w 进行松弛意味着测试从 s 到 w 的已知最佳路径是否是从 s 到 v,然后沿着从 v 到 w 的边,如果是,则更新我们的数据结构。
private void relax(DirectedEdge e) {
int v = e.from(), w = e.to();
if (distTo[w] > distTo[v] + e.weight()) {
distTo[w] = distTo[v] + e.weight();
edgeTo[w] = e;
}
}
顶点松弛。 我们所有的实现实际上都会松弛从给定顶点指向的所有边。
private void relax(EdgeWeightedDigraph G, int v) {
for (DirectedEdge e : G.adj(v)) {
int w = e.to();
if (distTo[w] > distTo[v] + e.weight()) {
distTo[w] = distTo[v] + e.weight();
edgeTo[w] = e;
}
}
}
戴克斯特拉算法将dist[s]
初始化为 0,将所有其他distTo[]
条目初始化为正无穷。然后,它重复地放松并将具有最低distTo[]
值的非树顶点添加到树中,继续直到所有顶点都在树上或没有非树顶点具有有限的distTo[]
值。
DijkstraSP.java 是戴克斯特拉算法的高效实现。它使用 IndexMinPQ.java 作为优先队列。
戴克斯特拉算法使用额外空间与 V 成正比,时间与 E log V 成正比(在最坏情况下)解决了带非负权重的带权有向图中的单源最短路径问题。
我们使用术语带权有向无环图来指代无环带权有向图。
该算法将顶点放松与拓扑排序结合起来。我们将distTo[s]
初始化为 0,将所有其他distTo[]
值初始化为无穷大,然后按照拓扑顺序放松顶点。AcyclicSP.java 是这种方法的实现。它依赖于这个版本的 Topological.java,扩展以支持带权有向图。
distTo[]
值初始化为负无穷大并在relax()
中改变不等式的意义来解决带权有向无环图中的单源最长路径问题。AcyclicLP.java 实现了这种方法。
通过将问题制定为带权有向无环图中的最长路径问题,可以解决此问题:创建一个带权有向无环图,其中包含一个源 s,一个汇 t,以及每个作业的两个顶点(一个起始顶点和一个结束顶点)。对于每个作业,从其起始顶点到其结束顶点添加一条权重等于其持续时间的边。对于每个前置约束 v->w,从对应于 v 的结束顶点到对应于 w 的开始顶点添加一条零权重边。还从源到每个作业的起始顶点和从每个作业的结束顶点到汇添加零权重边。
现在,根据从源到达的最长路径的长度安排每个作业的时间。
CPM.java 是关键路径法的实现。
通过按拓扑顺序放松顶点,我们可以在时间复杂度为 E + V 的情况下解决带权有向无环图的单源最短路径和最长路径问题。
如果(i)所有权重为非负或(ii)没有循环,则可以解决最短路径问题。
负循环。负循环是一个总权重为负的有向循环。如果存在负循环,则最短路径的概念是没有意义的。
因此,我们考虑没有负循环的加权有向图。
贝尔曼-福特算法。将distTo[s]
初始化为 0,将所有其他distTo[]
值初始化为无穷大。然后,以任意顺序考虑有向图的边,并放松所有边。进行 V 次这样的遍历。
for (int pass = 0; pass < G.V(); pass++)
for (int v = 0; v < G.V(); v++)
for (DirectedEdge e : G.adj(v))
relax(e);
我们不详细考虑这个版本,因为它总是放松 V E 条边。
基于队列的贝尔曼-福特算法。可能导致distTo[]
变化的唯一边是那些离开上一轮中distTo[]
值发生变化的顶点的边。为了跟踪这样的顶点,我们使用一个 FIFO 队列。BellmanFordSP.java 通过维护两个额外的数据结构来实现这种方法:
onQ[]
,指示哪些顶点在队列上,以避免重复
负循环检测。在许多应用中,我们的目标是检查并提取负循环。因此,我们向 API 添加以下方法:
当且仅当在所有边的第 V 次遍历后队列非空时,从源可达负循环。此外,我们edgeTo[]
数组中的边子图必须包含一个负循环。因此,为了实现negativeCycle()
,BellmanFordSP.java 从edgeTo[]
中的边构建一个加权有向图,并在该图中查找循环。为了找到循环,它使用 EdgeWeightedDirectedCycle.java,这是第 4.3 节中 DirectedCycle.java 的一个版本,适用于加权有向图。我们通过仅在每次第 V 次边放松后执行此检查来分摊此检查的成本。
套汇检测。考虑一个基于商品交易的金融交易市场。rates.txt 中的表显示了货币之间的转换率。文件的第一行是货币 V 的数量;然后文件每行给出货币的名称,然后是转换为其他货币的汇率。套汇机会是一个有向循环,使得交换率的乘积大于 1。例如,我们的表格显示,1000 美元可以购买 1000.00 × .741 = 741 欧元,然后我们可以用我们的欧元购买 741 × 1.366 = 1,012.206 加拿大元,最后,我们可以用我们的加拿大元购买 1,012.206 × .995 = 1,007.14497 美元,获得 7.14497 美元的利润!
为了将套汇问题制定为负循环检测问题,将每个权重替换为其对数的负值。通过这种改变,在原问题中通过乘以边权重来计算路径权重对应于在转换后的问题中将它们相加。Arbitrage.java 通过解决相应的负循环检测问题来识别货币兑换网络中的套汇机会。
在加权有向图中,从 s 到 v 存在最短路径当且仅当从 s 到 v 存在至少一条有向路径,并且从 s 到 v 的任何有向路径上的顶点都不在负循环上。
贝尔曼-福特算法解决了给定源 s 的单源最短路径问题(或找到从 s 可达的负循环)对于具有 V 个顶点和 E 条边的任意加权有向图,在最坏情况下,时间复杂度为 E V,额外空间复杂度为 V。
Q. Dijkstra 算法能处理负权重吗?
A. 是和否。有两种已知的最短路径算法称为Dijkstra 算法,取决于一个顶点是否可以多次入队到优先队列。当权重为非负时,这两个版本是相同的(因为没有顶点会多次入队)。DijkstraSP.java 中实现的版本(允许一个顶点多次入队)在存在负边权(但没有负环)时是正确的,但其最坏情况下的运行时间是指数级的。(我们注意到 DijkstraSP.java 如果边权重为负数,则会抛出异常,以便程序员不会对这种指数级行为感到惊讶。)如果我们修改 DijkstraSP.java 以使一个顶点不能多次入队(例如,使用marked[]
数组标记那些已经被松弛的顶点),那么算法保证在E log V时间内运行,但当存在负权边时可能产生错误结果。
toString()
的实现。
Integer
值 - Java 通常缓存-128 到 127 的整数。
DirectedCycle
和Topological
类中使用本节的EdgeweightedDigraph
和DirectedEdge
API,从而实现 EdgeWeightedDirectedCycle.java 和 Topological.java。
EdgeWeightedGraph
中的每个Edge
创建两个DirectedEdge
对象(分别在每个方向上)来将EdgeWeightedGraph
转换为EdgeWeightedDigraph
,然后使用贝尔曼-福特算法。解释为什么这种方法会失败得惊人。
解决方案: 即使带权图不包含负权重环,这可能会引入负成本循环。
dist[v]
。顶点 v 和 w 之间的最短路径是|dist[v] - dist[w]
|。
distTo[w] <= distTo[v] + length(v, w)
。对循环上的所有边进行这个不等式求和意味着循环的长度是非负的。
edgeTo[]
数组中就有一个有向循环,并且任何这样的循环都是负循环。
解决方案:待定。
distTo[w] <= distTo[v] + e.weight()
。将这个不等式对沿循环的所有边相加。
edgeTo[]
数组总是会回到 s 的路径。对 Dijkstra 算法重复这个问题。
distTo[]
值的顶点开始并且仅使用 A 中的边的任何路径都会得到正确的distTo[]
值;B 也是如此。所需的遍历次数是路径上 A-B 交替的最大次数,最多为(V+1)/2。因此,所需的遍历次数最多为(V+1)/2,而不是 V。
原文:
algs4.cs.princeton.edu/50strings
译者:飞龙 协议:CC BY-NC-SA 4.0
我们通过交换字符串来进行通信。我们考虑经典算法来解决围绕以下应用程序的基本计算挑战:
为了清晰和高效,我们的实现是基于 Java String 类表达的。我们简要回顾它们最重要的特性。
String
是字符序列。字符的类型是 char
,可以有 2¹⁶ 种可能的值。几十年来,程序员们一直关注编码为 7 位 ASCII 或 8 位扩展 ASCII 的字符,但许多现代应用程序需要 16 位 Unicode。
String
对象是不可变的,因此我们可以在赋值语句中使用它们,并且作为方法的参数和返回值,而不必担心它们的值会改变。
charAt()
方法以常数时间从字符串中提取指定字符。
length()
方法以常数时间返回字符串的长度。
substring()
方法通常以常数时间提取指定的子字符串。
警告:从 Oracle 和 OpenJDK Java 7,更新 6 开始,substring()
方法在提取的子字符串大小上需要线性时间和空间。由于我们没有预料到这种 drastical 变化,我们的一些字符串处理代码将受到影响。String API 对其任何方法,包括 substring()
和 charAt()
,都不提供性能保证。教训是自行承担风险。
查看这篇文章获取更多细节。
+
运算符执行字符串连接。我们避免逐个字符附加形成字符串,因为在 Java 中这是一个 二次时间 的过程。(Java 有一个 StringBuilder 类用于这种用途。)
String
不是原始类型。标准实现提供了上述操作,以便于客户端编程。相比之下,我们考虑的许多算法可以使用低级表示,比如一个 char
值数组,许多客户端可能更喜欢这种表示,因为它占用更少的空间并且耗时更少。
一些应用程序涉及从受限字母表中获取的字符串。在这种应用程序中,使用具有以下 API 的 Alphabet.java 类通常是有意义的:
构造函数以 R 个字符的字符串作为参数,该字符串指定了字母表;toChar()
和toIndex()
方法在常数时间内在字符串字符和介于 0 和 R-1 之间的int
值之间进行转换。R()
方法返回字母表或基数中的字符数。包括一些预定义的字母表:
Count.java 是一个客户端程序,它在命令行上指定一个字母表,读取该字母表上的一系列字符(忽略不在字母表中的字符),计算每个字符出现的频率,
以下是本章中的 Java 程序列表。单击程序名称以访问 Java 代码;单击参考号以获取简要描述;阅读教科书以获取全面讨论。
REF程序描述 / JAVADOC-Alphabet.java字母表-Count.java字母表客户端5.1LSD.javaLSD 基数排序5.2MSD.javaMSD 基数排序-InplaceMSD.java原地 MSD 基数排序¹5.3Quick3string.java三向字符串快速排序-AmericanFlag.java美国国旗排序¹-AmericanFlagX.java非递归美国国旗排序¹5.4TrieST.java多向字典树符号表-TrieSET.java多向字典树集合5.5TST.java三向单词查找树5.6KMP.java子字符串查找(Knuth–Morris–Pratt)5.7BoyerMoore.java子字符串查找(Boyer–Moore)5.8RabinKarp.java子字符串查找(Rabin–Karp)5.9NFA.java正则表达式的 NFA-GREP.javagrep-BinaryDump.java二进制转储-HexDump.java十六进制转储-PictureDump.java图片转储-Genome.java基因组编码-RunLength.java数据压缩(行程长度编码)5.10Huffman.java数据压缩(赫夫曼)5.11LZW.java数据压缩(Lempel–Ziv–Welch)
Q. 什么是 Unicode。
A. Unicode(通用字符编码)= 复杂的 21 位代码,用于表示国际符号和其他字符。
Q. 什么是 UTF-16。
A. UTF-16(Unicode 转换格式)= 复杂的 16 位可变宽度代码,用于表示 Unicode 字符。大多数常见字符使用 16 位(一个char
)表示,但代理对使用一对char
值表示。如果第一个char
值在D800
和DFFF
之间,则与下一个char
(在相同范围内)组合形成代理对。没有 Unicode 字符对应于D800
到DFFF
。例如,007A
表示小写字母 Z,6C34
表示中文水的符号,D834 DD1E
表示音乐的 G 大调。
Unicode 参考。
Q. 什么是子字符串陷阱?
A. 字符串方法调用s.substring(i, j)
返回 s 从索引 i 开始到 j-1 结束的子字符串(而不是在 j 结束,正如你可能会怀疑的那样)。
Q. 如何更改字符串的值?
A. 在 Java 中无法修改字符串,因为字符串是不可变的。如果你想要一个新的字符串,那么你必须使用字符串连接或返回新字符串的字符串方法之一,如toLowerCase()
或substring()
来创建一个新的字符串。
**挤压空格。**编写一个程序 Squeeze.java,该程序接受一个字符串作为输入,并删除相邻的空格,最多保留一个空格。
**删除重复项。**给定一个字符串,创建一个新字符串,其中删除所有连续的重复项。例如,ABBCCCCCBBAB
变为ABCBAB
。
**N 个 x 的字符串。**描述以下函数返回的字符串,给定一个正整数N
?
public static String mystery(int N) {
String s = "";
while(N > 0) {
if (N % 2 == 1) s = s + s + "x";
else s = s + s;
N = N / 2;
}
return s;
}
**回文检查。**编写一个函数,该函数以字符串作为输入,并在字符串是回文时返回true
,否则返回false
。回文是指字符串从前往后读和从后往前读是相同的。
**Watson-Crick 互补回文检查。**编写一个函数,该函数以字符串作为输入,并在字符串是 Watson-Crick 互补回文时返回true
,否则返回false
。Watson-Crick 互补回文是指 DNA 字符串等于其反向的互补(A-T,C-G)。
**Watson-Crick 互补。**编写一个函数,该函数以 A、C、G 和 T 字符的 DNA 字符串作为输入,并返回以其互补替换所有字符的反向字符串。例如,如果输入是 ACGGAT,则返回 ATCCGT。
**完美洗牌。**给定长度相同的两个字符串s
和t
,以下递归函数返回什么?
public static String mystery(String s, String t) {
int N = s.length();
if (N <= 1) return s + t;
String a = mystery(s.substring(0, N/2), t.substring(0, N/2));
String b = mystery(s.substring(N/2, N), t.substring(N/2, N));
return a + b;
}
**二叉树表示。**编写一个名为TreeString.java
的数据类型,使用二叉树表示不可变字符串。它应该支持在常数时间内进行连接,并在与字符数成比例的时间内打印出字符串。
**反转字符串。**编写一个递归函数来反转一个字符串。不要使用任何循环。提示:使用 String 方法substring()
。
public static String reverse(String s) {
int N = s.length();
if (N <= 1) return s;
String a = s.substring(0, N/2);
String b = s.substring(N/2, N);
return reverse(b) + reverse(a);
}
你的方法效率如何?我们的方法具有线性对数运行时间。
**随机字符串。**编写一个递归函数,创建一个由字符’A’和’Z’之间的随机字符组成的字符串。
public static String random(int N) {
if (N == 0) return "";
if (N == 1) return 'A' + StdRandom.uniform(26);
return random(N/2) + random(N - N/2);
}
**子序列。**给定两个字符串s
和t
,编写一个程序 Subsequence.java,确定s
是否是t
的子序列。也就是说,s
的字母应该按照相同的顺序出现在t
中,但不一定是连续的。例如accag
是taagcccaaccgg
的子序列。
最长互补回文。 在 DNA 序列分析中,互补回文是一个等于其反向互补的字符串。腺嘌呤(A)和胸腺嘧啶(T)是互补的,胞嘧啶(C)和鸟嘌呤(G)也是互补的。例如,ACGGT 是一个互补回文。这样的序列作为转录结合位点,并与基因扩增和遗传不稳定性相关。给定一个长度为 N 的文本输入,找到文本的最长互补回文子串。例如,如果文本是 GACACGGTTTTA
,那么最长的互补回文是 ACGGT
。提示:将每个字母视为奇数长度可能回文的中心,然后将每对字母视为偶数长度可能回文的中心。
DNA 转 RNA。 编写一个函数,该函数接受一个 DNA 字符串(A、C、G、T)并返回相应的 RNA 字符串(A、C、G、U)。
DNA 互补。 编写一个函数,该函数以 DNA 字符串(A、C、G、T)作为输入,并返回互补的碱基对(T、G、C、A)。DNA 通常以双螺旋结构存在。两条互补的 DNA 链以螺旋结构连接在一起。
从十六进制转换为十进制。 Hex2Decimal.java 包含一个函数,该函数接受一个十六进制字符串(使用 A-F 表示数字 11-15)并返回相应的十进制整数。它使用了一些字符串库方法和霍纳方法。
public static int hex2decimal(String s) {
String digits = "0123456789ABCDEF";
s = s.toUpperCase();
int val = 0;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
int d = digits.indexOf(c);
val = 16*val + d;
}
return val;
}
替代方案:Integer.parseInt(String s, int radix)
。更加健壮,并且适用于负整数。
本节正在大规模施工中。
程序 LSD.java 实现了用于固定长度字符串的 LSD 基数排序。它包括一种用于对待每个整数作为 4 字节字符串处理的 32 位整数进行排序的方法。当 N 很大时,这种算法比系统排序快 2-3 倍。
程序 MSD.java 实现了 MSD 基数排序。
程序 Quick3string.java 实现了三向字符串快速排序。
count[]
数组,告诉你键需要放置的位置。扫描输入数组。取第一个键,找到它应该属于的桶,并将其交换到相应的位置(更新相应的 count[]
条目)。重复第二个键,但要小心跳过已知属于其位置的键。
a[]
和一个目标值 T,确定是否存在两个不同的整数 i 和 j,使得 a[i]
+ a[j]
等于 T。你的算法应该在最坏情况下线性时间运行。
解决方案。在线性时间内对数组进行基数排序。从左到右扫描指针 i 和从右到左扫描指针 j:考虑 a[i] + a[j]。如果它大于 T,则推进 j 指针;如果它小于 T,则推进 i 指针;如果它等于 T,则我们找到了所需的索引。
注意,整数数组可以使用 Franceschini、Muthukrishnan 和 Patrascu 的高级基数排序算法在线性时间和常数额外空间内进行基数排序。
compareTo()
的版本的性能。(compareTo()
方法的优点是它不需要调用 charAt()
,因为它是作为 String
数据类型的实例方法实现的。)
本节正在大规模建设中。
可以使用标准符号表实现。而是利用字符串键的附加结构。为字符串(以及其他以数字表示的键)定制搜索算法。目标:像哈希一样快速,比二叉搜索树更灵活。可以有效地支持额外的操作,包括前缀和通配符匹配,例如,IP 路由表希望转发到 128.112.136.12,而实际上转发到 128.112 是它已知的最长匹配前缀。附带好处:快速且占用��间少的字符串搜索。
R 向查找树。 程序 TrieST.java 使用多向查找树实现了一个字符串符号表。
三向查找树。 程序 TST.java 使用三向查找树实现了一个字符串符号表。
参考:快速排序和搜索的算法 作者 Bentley 和 Sedgewick。
属性 A.(Bentley-Sedgewick)给定一个输入集,无论字符串插入的顺序如何,其 TST 中的节点数都是相同的。
证明。在集合中,TST 中每个不同字符串前缀都有一个唯一的节点。节点在 TST 中的相对位置可能会根据插入顺序而改变,但节点数是不变的。
通配符搜索,前缀匹配。R 向查找树和 TST 实现包括用于通配符匹配和前缀匹配的代码。
惰性删除 = 更改单词边界位。急切删除 = 清理任何死亡父链接。
应用:T9 手机文本输入。用户使用手机键盘键入;系统显示所有对应的单词(并在唯一时自动完成)。如果用户键入 0,系统会显示所有可能的自动完成。
编写 R 向查找树字符串集和 TST 的非递归版本。
长度为 L 的唯一子字符串。 编写一个程序,从标准输入中读取文本并计算其包含的长度为 L 的唯一子字符串的数量。例如,如果输入是cgcgggcgcg
,那么长度为 3 的唯一子字符串有 5 个:cgc
、cgg
、gcg
、ggc
和ggg
。应用于数据压缩。提示:使用字符串方法substring(i, i + L)
提取第 i 个子字符串并插入符号表。另一种解决方案:使用第 i 个子字符串的哈希值计算第 i+1 个子字符串的哈希值。在第一千万位数的π或者第一千万位数的π上测试它。
唯一子字符串。 编写一个程序,从标准输入中读取文本并计算任意长度的不同子字符串的数量。(可以使用后缀树非常高效地完成。)
文档相似性。 要确定两个文档的相似性,计算每个三字母组(3 个连续字母)的出现次数。如果两个文档的三字母组频率向量的欧几里德距离很小,则它们相似。
拼写检查。 编写一个程序 SpellChecker.java,它接受一个包含英语词汇的字典文件的名称,然后从标准输入读取字符串并打印出不在字典中的任何单词。使用一个字符串集。
垃圾邮件黑名单。 将已知的垃圾邮件地址插入到存在表中,并用于阻止垃圾邮件。
按国家查找 IP。 使用数据文件ip-to-country.csv来确定给定 IP 地址来自哪个国家。数据文件有五个字段(IP 地址范围的开始,IP 地址范围的结束,两个字符的国家代码,三个字符的国家代码和国家名称。请参阅IP-to-country 网站。IP 地址不重叠。这样的数据库工具可用于:信用卡欺诈检测,垃圾邮件过滤,网站上语言的自动选择以及 Web 服务器日志分析。
Web 的倒排索引。 给定一个网页列表,创建包含网页中包含的单词的符号表。将每个单词与出现该单词的网页列表关联起来。编写一个程序,读取一个网页列表,创建符号表,并通过返回包含该查询单词的网页列表来支持单词查询。
Web 的倒排索引。 扩展上一个练习,使其支持多词查询。在这种情况下,输出包含每个查询词至少出现一次的网页列表。
带有重复项的符号表。
密码检查器。 编写一个程序,从命令行读取一个字符串和从标准输入读取一个单词字典,并检查它是否是一个“好”密码。在这里,假设“好”意味着(i)至少有 8 个字符长,(ii)不是字典中的单词,(iii)不是字典中的单词后跟一个数字 0-9(例如,hello5),(iv)不是由一个数字分隔的两个单词(例如,hello2world)。
反向密��检查器。 修改上一个问题,使得(ii)-(v)也适用于字典中单词的反向形式(例如,olleh 和 olleh2world)。巧妙的解决方案:将每个单词及其反向形式插入符号表中。
随机电话号码。 编写一个程序,接受一个命令行输入 N,并打印 N 个形式为(xxx)xxx-xxxx 的随机电话号码。使用符号表避免多次选择相同的号码。使用这个区号列表来避免打印虚假的区号。使用 R 向 Trie。
包含前缀。 向StringSET
添加一个方法containsPrefix()
,接受字符串 s 作为输入,并在集合中存在包含 s 作为前缀的字符串时返回 true。
子字符串匹配。 给定一个(短)字符串列表,您的目标是支持查询,其中用户查找字符串 s,您的任务是报告列表中包含 s 的所有字符串。提示:如果您只想要前缀匹配(字符串必须以 s 开头),请使用文本中描述的 TST。要支持子字符串匹配,请将每个单词的后缀(例如,string,tring,ring,ing,ng,g)插入 TST 中。
Zipf 定律。 哈佛语言学家乔治·齐普夫观察到,包含 N 个单词的英文文本中第 i 个最常见单词的频率大致与 1/i 成比例,其中比例常数为 1 + 1/2 + 1/3 + … + 1/N。通过从标准输入读取一系列单词,制表它们的频率,并与预测的频率进行比较来测试“齐普夫定律”。
打字猴和幂律。(Micahel Mitzenmacher)假设一个打字猴通过将每个 26 个可能的字母以概率 p 附加到当前单词来创建随机单词,并以概率 1 - 26p 完成单词。编写一个程序来估计生成的单词长度的频率分布。如果“abc”被生成多次,则只计算一次。
打字猴和幂律。 重复上一个练习,但假设字母 a-z 出现的概率与以下概率成比例,这是英文文本的典型概率。
CHAR | FREQ | CHAR | FREQ | CHAR | FREQ | CHAR | FREQ | CHAR | FREQ | ||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
A | 8.04 | G | 1.96 | L | 4.14 | Q | 0.11 | V | 0.99 | ||||
B | 1.54 | H | 5.49 | M | 2.53 | R | 6.12 | W | 1.92 | ||||
C | 3.06 | I | 7.26 | N | 7.09 | S | 6.54 | X | 0.19 | ||||
D | 3.99 | J | 0.16 | O | 7.60 | T | 9.25 | Y | 1.73 | ||||
E | 12.51 | K | 0.67 | P | 2.00 | U | 2.71 | Z | 0.09 | ||||
F | 2.30 |
书的索引。 编写一个程序,从标准输入中读取一个文本文件,并编制一个按字母顺序排列的索引,显示哪些单词出现在哪些行,如下所示的输入。忽略大小写和标点符号。
It was the best of times,
it was the worst of times,
it was the age of wisdom,
it was the age of foolishness,
age 3-4
best 1
foolishness 4
it 1-4
of 1-4
the 1-4
times 1-2
was 1-4
wisdom 4
worst 2
熵。 我们定义一个包含 N 个单词的文本语料库的相对熵为 E = 1 / (N log N) * sum (p[i] log(k) - log(p[i]), i = 1…k),其中 p_i 是单词 i 出现的次数的比例。编写一个程序,读取一个文本语料库并打印出相对熵。将所有字母转换为小写,并将标点符号视为空格。
最长前缀。 真或假。二进制字符串 x 在符号表中的最长前缀要么是 x 的下取整,要么是 x 的上取整(如果 x 在集合中则两者都是)。
错误。在 { 1, 10, 1011, 1111 } 中,1100 的最长前缀是 1,而不是 1011 或 1111。
原文:
algs4.cs.princeton.edu/53substring
译者:飞龙 协议:CC BY-NC-SA 4.0
本节正在大规模施工中。在长字符串中搜索 - 在线。
这个网站是一个关于精确字符串搜索算法的重要资源。
Java 中的高性能模式匹配用于一般字符串搜索,带通配符的搜索和带字符类的搜索。
程序 Brute.java 是暴力字符串搜索。基本上等同于 SystemSearch.java。
程序 RabinKarp.java 实现了拉宾卡普随机指纹算法。
程序 KMP.java 是 Knuth-Morris-Pratt 算法。KMPplus.java 是一个改进版本,时间和空间复杂度与 M + N 成正比(与字母表大小 R 无关)。
程序 BoyerMoore.java 实现了 Boyer-Moore 算法的坏字符规则部分。它不实现强好后缀规则。
需要非常快速的字符串搜索,因为这些部署在网络的瓶颈处。应用
本节正在大力整理中。
NFA.java, DFS.java, Digraph.java, 和 GREP.java.
M = 表达式长度,N = 输入长度。正则表达式匹配算法可以在 O(M)时间内创建 NFA,并在 O(MN)时间内模拟输入。
Validate.java。
大多数正则表达式库实现使用回溯算法,在某些输入上可能需要指数级的时间。这样的输入可能非常简单。例如,确定长度为 N 的字符串是否与正则表达式(a|aa)*b
匹配,如果选择字符串得当,可能需要指数级的时间。下表展示了 Java 1.4.2 正则表达式的失败情况。
java Validate "(a|aa)*b" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaac 1.6 seconds
java Validate "(a|aa)*b" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac 3.7 seconds
java Validate "(a|aa)*b" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac 9.7 seconds
java Validate "(a|aa)*b" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac 23.2 seconds
java Validate "(a|aa)*b" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac 62.2 seconds
java Validate "(a|aa)*b" aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac 161.6 seconds
java Validate "(a*)*|b*" aaaaaaaaaaaaaaaaaaaaac 1.28
java Validate "(a*)*|b*" aaaaaaaaaaaaaaaaaaaaaac 2.45
java Validate "(a*)*|b*" aaaaaaaaaaaaaaaaaaaaaaac 4.54
java Validate "(a*)*|b*" aaaaaaaaaaaaaaaaaaaaaaaac 8.84
java Validate "(a*)*|b*" aaaaaaaaaaaaaaaaaaaaaaaaac 17.74
java Validate "(a*)*|b*" aaaaaaaaaaaaaaaaaaaaaaaaaac 33.77
java Validate "(a*)*|b*" aaaaaaaaaaaaaaaaaaaaaaaaaaac 67.72
java Validate "(a*)*|b*" aaaaaaaaaaaaaaaaaaaaaaaaaaaac 134.73
上述示例是人为的,但它们展示了大多数正则表达式库中的一个令人担忧��缺陷。在实践中确实会出现不良输入。根据Crosby 和 Wallach的说法,以下正则表达式出现在 SpamAssassin 的一个版本中,这是一个功能强大的垃圾邮件过滤程序。
[a-z]+@[a-z]+([a-z\.]+\.)+[a-z]+
它试图匹配某些电子邮件地址,但在许多正则表达式库中,包括 Sun 的 Java 1.4.2 中,匹配某些字符串需要指数级的时间。
java Validate "[a-z]+@[a-z]+([a-z\.]+\.)+[a-z]+" spammer@x......................
这尤其重要,因为垃圾邮件发送者可以使用一种病态的返回电子邮件地址来拒绝服务攻击一个运行 SpamAssassin 的邮件服务器。这个特定的模式现在已经修复,因为 Perl 5 正则表达式使用内部缓存来在回溯过程中在相同位置短路重复匹配。
这些缺陷不仅限于 Java 的实现。例如,GNU regex-0.12 对于匹配形式为aaaaaaaaaaaaaac
的字符串与正则表达式(a*)*|b*
需要指数级的时间。Sun 的 Java 1.4.2 同样容易受到这个问题的影响。此外,Java 和 Perl 正则表达式支持反向引用 - 对于这些扩展正则表达式的正则表达式模式匹配问题是NP 难的,因此在某些输入上这种指数级的增长似乎是固有的。
这是我实际写的一个,用来找到字符串NYSE
之前的最后一个单词:regexp = “([\w\s]+).*NYSE”;
参考:正则表达式匹配可以简单快速(但在 Java、Perl、PHP、Python、Ruby 等中很慢)。比较了 Thompson NFA 和回溯方法。包含了一些针对 Thompson NFA 的性能优化。还有一些历史注释和参考资料。
Q. Java 正则表达式库的文档?
A. 这里是 Oracle 关于使用正则表达式的指南。它包括更多操作,我们不会探索。还请参阅String
方法matches()
、split()
和replaceAll()
。这些是使用Pattern
和Matcher
类的简写。这里有一些常见的正则表达式模式。
Q. 用于电子邮件地址、Java 标识符、整数、小数等的工业级别正则表达式?
A. 这里有一个有用的正则表达式库,提供了工业级别的模式,用于匹配电子邮件地址、URL、数字、日期和时间。试试这个正则表达式工具。
Q. 我困惑为什么(a | b)*
匹配所有的 a 和 b 的字符串,而不仅仅是所有 a 的字符串或所有 b 的字符串?
A. *操作符复制正则表达式(而不是匹配正则表达式的固定字符串)。因此,上述等同于ε | (a|b) | (a|b)(a|b) | (a|b)(a|b)(a|b) | …
Q. 历史?
A. 在 1940 年代,沃伦·麦卡洛克和沃尔特·皮茨将神经元建模为有限自动机来描述神经系统。1956 年,史蒂夫·克利纳发明了一种数学抽象称为正则集来描述这些模型。神经网络和有限自动机中事件的表示,《自动机研究》,3-42 页,普林斯顿大学出版社,新泽西州普林斯顿,1956 年。
Q. 有哪些可视化正则表达式的工具?
A. 尝试Debuggerx。
为以下每组二进制字符串编写正则表达式。只使用基本操作。
答案:0 | 11 | 101, 0*
为以下每组二进制字符串编写正则表达式。只使用基本操作。
答案:(0|1), (0|1)(0|1), 1 | 1(0|1)*1, (0|1)*00, (0|1)1(0|1)1(0|1)1(0|1)或 010101(0|1)。
编写一个正则表达式描述字母表{a, b, c}上按排序顺序的输入。答案:abc*。
为以下每组二进制字符串编写正则表达式。只使用基本操作。
答案:(0|1)111(0|1), (0|1)110(0|1), (0|1)1101100(0|1), (0|10)1。最后一个是最棘手的。
为至少有两个 0 但不连续的 0 的二进制字符串编写正则表达式。
为以下每组二进制字符串编写正则表达式。只使用基本操作。
答案:(0|1)(0|1)0(0|1), 1 | (1010101), 1(0|1)1 | 0(0|1)0 | 0 | 1, (0|1)((0|1)(0|1)), 0((0|1)(0|1)) | 1(0|1)((0|1)(0|1)), (0|1) | (0|1)(0|1) | (0|1)(0|1)(0|1)。
对于以下每个问题,指出有多少长度为 1000 的位字符串与正则表达式匹配:0(0 | 1)*1
,0*101*
,(1 | 01)*
。
编写一个正则表达式,匹配字母表{a, b, c}中包含的所有字符串:
找出字母按字母顺序排列的长单词,例如,almost
和beefily
。答案:使用正则表达式’^abcdefghijklmnopqrstuvwxyz$'。
编写一个 Java 正则表达式,匹配电话号码,带有或不带有区号。区号应为(609) 555-1234 或 555-1234 的形式。
找出所有以nym
结尾的英语单词。
找出所有包含三连字母bze
的英语单词。答案:subzero。
找出所有以 g 开头,包含三连字母pev
且以 e 结尾的英语单词。答案:grapevine。
找出所有包含三个 r 且至少有两个 r 的英语单词。
找出可以用标准键盘顶行写出的最长英语单词。答案:proprietorier。
找出所有包含字母 a、s、d 和 f 的单词,不一定按照顺序。解决方案:cat words.txt | grep a | grep s | grep d | grep f
。
给定一个由 A、C、T 和 G 以及 X 组成的字符串,找到一个字符串,其中 X 匹配任何单个字符,例如,CATGG 包含在 ACTGGGXXAXGGTTT 中。
编写一个 Java 正则表达式,用于 Validate.java,验证形式为 123-45-6789 的社会安全号码。提示:使用\d
表示任何数字。答案:[0-9]{3}-[0-9]{2}-[0-9]{4}
。
修改上一个练习,使-
成为可选项,这样 123456789 就被视为合法输入。
编写一个 Java 正则表达式,匹配包含恰好五个元音字母且元音字母按字母顺序排列的所有字符串。 答案: [^aeiou]*a[^aeiou]*e[^aeiou]*i[^aeiou]*o[^aeiou]*u[^aeiou]*
编写一个 Java 正则表达式,匹配有效的 Windows XP 文件名。这样的文件名由除了冒号以外的任意字符序列组成。
/ \ : * ? " < > |
此外,它不能以空格或句号开头。
编写一个 Java 正则表达式,描述有效的 OS X 文件名。这样的文件名由除冒号以外的任意字符序列组成。此外,它不能以句点开头。
给定一个代表 IP 地址的名称为s
的字符串,采用dotted quad表示法,将其分解为其组成部分,例如,255.125.33.222。确保四个字段都是数字。
编写一个 Java 正则表达式,描述形式为Month DD, YYYY的所有日期,其中Month由任意大写或小写字母字符串组成,日期是 1 或 2 位数字,年份正好是 4 位数字。逗号和空格是必需的。
编写一个 Java 正则表达式,描述形式为 a.b.c.d 的有效 IP 地址,其中每个字母可以表示 1、2 或 3 位数字,句点是必需的。是:196.26.155.241。
编写一个 Java 正则表达式,匹配以 4 位数字开头并以两个大写字母结尾的车牌。
编写一个正则表达式,从 DNA 字符串中提取编码序列。它以 ATG 密码子开头,以停止密码子(TAA、TAG 或 TGA)结尾。参考
编写一个正则表达式来检查序列 rGATCy:即,它是否以 A 或 G 开头,然后是 GATC,最后是 T 或 C。
编写一个正则表达式来检查一个序列是否包含两个或更多次重复的 GATA 四核苷酸。
修改 Validate.java 使搜索不区分大小写。 提示: 使用(?i)
嵌入式标志。
编写一个 Java 正则表达式,匹配利比亚独裁者穆阿迈尔·卡扎菲姓氏的各种拼写,使用以下模板:(i)以 K、G、Q 开头,(ii)可选地跟随 H,(iii)后跟 AD,(iv)可选地跟随 D,(v)可选地跟随 H,(vi)可选地跟随 AF,(vii)可选地跟随 F,(vii)以 I 结尾。
编写一个 Java 程序,读取类似(K|G|Q)[H]AD[D][H]AF[F]I
的表达式,并打印出所有匹配的字符串。这里的符号[x]
表示字母x
的 0 或 1 个副本。
为什么s.replaceAll("A", "B");
不会替换字符串s
中所有出现的字母 A 为 B?
答案:使用s = s.replaceAll("A", "B");
代替。replaceAll
方法返回结果字符串,但不会改变s
本身。字符串是不可变的。
编写一个程序 Clean.java,从标准输入中读取文本并将其打印出来,在一行上去除任何尾随空格,并用 4 个空格替换所有制表符。
提示: 使用replaceAll()
和正则表达式\s
匹配空格。
编写一个正则表达式,匹配在文本a href ="
和下一个"
之间的所有文本。 答案: href=\"(.*?)\"
。?
使.*
变得不贪婪而是懒惰。在 Java 中,使用Pattern.compile("href=\\\"(.*?)\\\"", Pattern.CASE_INSENSITIVE)
来转义反斜杠字符。
使用正则表达式提取在<title>
和<\title>
标签之间的所有文本。(?i)
是另一种使匹配不区分大小写的方法。$2
指的是第二个捕获的子序列,即title
标签之间的内容。
String pattern = "(?i)(<title.*?>)(.+?)(</title>)";
String updated = s.replaceAll(pattern, "$2");
编写一个正则表达式来匹配在<TD …>和标签之间的所有文本。 答案:<TD[^>]*>([^<]*)</TD>
FMR-1 三联重复区域。 “人类 FMR-1 基因序列包含一个三联重复区域,在该区域中序列 CGG 或 AGG 重复多次。三联体的数量在个体之间高度变化,增加的拷贝数与脆性 X 综合征相关,这是一种导致 2000 名儿童中的一名智力残疾和其他症状的遗传疾病。”(参考:Durbin 等人的《生物序列分析》)。该模式由 GCG 和 CTG 括起来,因此我们得到正则表达式 GCG (CGG | AGG)* CTG。
广告拦截。 Adblock 使用正则表达式来阻止 Mozilla 和 Firebird 浏览器下的横幅广告。
解析文本文件。 一个更高级的例子,我们想要提取匹配输入的特定部分。这个程序代表了解析科学输入数据的过程。
PROSITE 到 Java 正则表达式。 编写一个程序,读取 PROSITE 模式并打印出相应的 Java 正则表达式。PROSITE 是蛋白质家族和结构域的“第一个和最著名”的数据库。其主要用途是确定从基因组序列翻译而来的未知功能蛋白质的功能。生物学家使用PROSITE 模式语法规则在生物数据中搜索模式。这是CBD FUNGAL(访问代码 PS00562)的原始数据。每行包含各种信息。也许最有趣的一行是以 PA 开头的行 - 它包含描述蛋白质基序的模式。这些模式很有用,因为它们通常对应于功能或结构特征。
PA C-G-G-x(4,7)-G-x(3)-C-x(5)-C-x(3,5)-[NHG]-x-[FYWM]-x(2)-Q-C.
每个大写字母对应一个氨基酸残基。字母表由对应于 2x 氨基酸的大写字母组成。连字符-
表示连接。例如,上面的模式以 CGG(Cys-Gly-Gly)开头。符号x
扮演通配符的角色 - 它匹配任何氨基酸。这对应于我们符号中的.
。括号用于指定重复:x(2)
表示恰好两个氨基酸,x(4,7)
表示 4 到 7 个氨基酸。这对应于 Java 符号中的.{2}
和.{4,7}
。花括号用于指定禁止的残基:{CG}表示除 C 或 G 之外的任何残基。星号具有其通常的含义。
文本转语音合成。 grep 的原始动机。“例如,如何处理发音多种不同的二连音 ui:fruit, guile, guilty, anguish, intuit, beguine?”
具有挑战性的正则表达式。 为以下每组二进制字符串编写一个正则表达式。只使用基本操作。
二进制可被整除。 为以下每组二进制字符串编写一个正则表达式。只使用基本操作。
波士顿口音。 编写一个程序,将所有的 r 替换为 h,将句子翻译成波士顿版本,例如将“Park the car in Harvard yard”翻译为波士顿版本的“Pahk the cah in Hahvahd yahd”。
文件扩展名。 编写一个程序,以文件名作为命令行参数,并打印出其文件类型扩展名。扩展名是跟在最后一个.
后面的字符序列。例如,文件sun.gif
的扩展名是gif
。提示:使用split("\\.")
;请记住.
是一个正则表达式元字符,因此您需要转义它。
反向子域。 为了进行网络日志分析,方便地根据子域(如wayne.faculty.cs.princeton.edu
)组织网络流量。编写一个程序来读取域名并以反向顺序打印出来,如edu.princeton.cs.faculty.wayne
。
银行抢劫。 你刚刚目睹了一起银行抢劫案,并且得到了逃跑车辆的部分车牌号。它以ZD
开头,中间有一个3
,以V
结尾。帮助警官写出这个车牌的正则表达式。
排列的正则表达式。 找到 N 个元素的所有排列集合的最短正则表达式(仅使用基本操作),其中 N = 5 或 10。例如,如果 N = 3,则语言是 abc,acb,bac,bca,cab,cba。*答案:*困难。解决方案的长度与 N 呈指数关系。
解析带引号的字符串。 读取一个文本文件并打印出所有带引号的字符串。使用类似"[^"]*"
的正则表达式,但需要担心转义引号。
解析 HTML。 一个>,可选地跟随空格,后跟a
,后跟空格,后跟href
,可选地跟随空格,后跟=,可选地跟随空格,后跟
"http://,后跟字符直到
",可选地跟随空格,然后是一个<。
< \s* a \s+ href \s* = \s* \\"http://[^\\"]* \\" \s* >
子序列。 给定一个字符串s
,确定它是否是另一个字符串t
的子序列。例如,abc 是 achfdbaabgabcaabg 的一个子序列。使用正则表达式。现在不使用正则表达式重复这个过程。答案:(a) a.*b.c.,(b) 使用贪婪算法。
亨廷顿病诊断。 导致亨廷顿病的基因位于染色体 4 上,并且具有可变数量的 CAG 三核苷酸重复。编写一个程序来确定重复次数并打印不会患 HD
,如果重复次数少于 26,则打印后代有风险
,如果数字为 37-35,则打印有风险
,如果数字在 36 和 39 之间,则打印将患 HD
。这就是遗传测试中识别亨廷顿病的方式。
基因查找器。 基因是基因组的一个子字符串,以起始密码子(ATG)开始,以终止密码子(TAG,TAA,TAG 或 TGA)结束,并由除起始或终止密码子之外的密码子序列(核苷酸三联体)组成。基因是起始和终止密码子之间的子字符串。
重复查找器。 编写一个程序Repeat.java
,它接受两个命令行参数,并查找指定由第二个命令行参数指定的文件中第一个命令行参数的最大重复次数。
字符过滤器。 给定一个包含坏字符的字符串t
,例如t = "!@#$%^&*()-_=+"
,编写一个函数来读取另一个字符串s
并返回删除所有坏字符后的结果。
String pattern = "[" + t + "]";
String result = s.replaceAll(pattern, "");
通配符模式匹配器。 不使用 Java 内置的正则表达式,编写一个程序 Wildcard.java 来查找与给定模式匹配的字典中的所有单词。特殊符号匹配任意零个或多个字符。因此,例如模式"ward"匹配单词"ward"和"wildcard"。特殊符号.匹配任何一个字符。您的程序应将模式作为命令行参数读取,并从标准输入读取单词列表(由空格分隔)。
通配符模式匹配器。 重复上一个练习,但这次使用 Java 内置的正则表达式。*警告:*在通配符的上下文中,*的含义与正则表达式不同。
搜索和替换。 文字处理器允许您搜索给定查询字符串的所有出现并用另一个替换字符串替换每个出现。编写一个程序 SearchAndReplace.java,它接受两个字符串作为命令行输入,从标准输入读取数据,并用第一个字符串替换所有出现的第一个字符串,并将结果发送到标准输出。*提示:*使用方法String.replaceAll
。
密码验证器。 假设出于安全原因,您要求所有密码至少包含以下字符之一
~ ! @ # $ % ^ & * |
为String.matches
编写一个正则表达式,如果密码包含所需字符之一,则返回true
。答案:“[~!@#
”
字母数字过滤器。 编写一个程序 Filter.java,从标准输入中读取文本,并消除所有不是空格或字母数字的字符。答案 这是关键行。
String output = input.replaceAll("[^\\s0-9a-zA-Z]", "");
将制表符转换为空格。 编写一个程序,将 Java 源文件中的所有制表符转换为 4 个空格。
解析分隔文本文件。 存储数据库的一种流行方式是将其存储在一个文本文件中,每行一个记录,每个字段由称为分隔符的特殊字符分隔。
19072/Narberth/PA/Pennsylvania
08540/Princeton/NJ/New Jersey
编写一个程序 Tokenizer.java,它读取两个命令行参数,一个是分隔符字符,另一个是文件名,并创建一个标记数组。
解析分隔文本文件。 重复上一个练习,但使用String
库方法split()
。
检查文件格式。
拼写错误。 编写一个 Java 程序,验证这个常见拼写错误列表中只包含形式为的行
misdemenors (misdemeanors)
mispelling (misspelling)
tennisplayer (tennis player)
第一个单词是拼写错误,括号中的字符串是可能的替换。
有趣的英语单词
DFA 的大小与 RE 的大小呈指数关系。 给出一个 RE,用于表示所有最后一个字符为 1 的比特串集合。RE 的大小应该与 k 成线性关系。现在,给出同一组比特串的 DFA。它使用了多少个状态?
提示:对于这组比特串,每个确定有限自动机(DFA)至少需要有 2^k 个状态。
原文:
algs4.cs.princeton.edu/55compression
译者:飞龙 协议:CC BY-NC-SA 4.0
本节正在大规模施工中。
数据压缩:将文件大小缩小以节省空间存储和在传输时节省时间。摩尔定律:芯片上的晶体管数量每 18-24 个月翻一番。帕金森定律:数据会扩张以填满可用空间。文本、图像、声音、视频等。维基百科提供公共转储以供学术研究和再发布。使用 bzip 和 SevenZip 的 LZMA。对 300GB 数据进行压缩可能需要一周的时间。
摩尔斯电码,十进制数系统,自然语言,旋转电话(较低的号码拨号速度更快,所以纽约是 212,芝加哥是 312)。
我们使用 BinaryStdIn.java、BinaryStdOut.java、BinaryDump.java、HexDump.java 和 PictureDump.java。
需要 ceil(lg R) 位来指定 R 个符号中的一个。Genome.java。使用 Alphabet.java。
RunLength.java。
希望有唯一可解码的编码。实现这一目标的一种方法是向每个码字附加一个特殊的停止符号。更好的方法是前缀无码:没有字符串是另一个字符串的前缀。例如,{ 01, 10, 0010, 1111 } 是前缀无码,但 { 01, 10, 0010, 1010 } 不是,因为 10 是 1010 的前缀。
给出传真机的例子。
构建最佳前缀无码的特定方式。由 David Huffman 在 1950 年在 MIT 时发明。Huffman.java 实现了 Huffman 算法。
属性 A. 没有前缀无码使用更少的比特。
使用 TST.java 中的前缀匹配代码,LZW.java 实现了 LZW 压缩。
现实世界:Pkzip = LZW + Shannon-Fano,GIF,TIFF,V.42bis 调制解调器,Unix 压缩。实际问题:
实际问题:限制符号表中元素的数量。
Huffman:固定长度符号的变长编码。LZW:变长字符串的固定长度编码。
不可能压缩所有文件(通过简单计数论证)。直观论证:压缩莎士比亚的生平作品,然后压缩结果,再次压缩结果。如果每个文件都严格缩小,最终将只剩下一个比特。
卡内基梅隆大学的 Guy Blelloch 在 数据压缩 方面有一章非常出色。
假设用于发送信息的信道存在噪声,每个比特以概率 p 翻转。发送每个比特 3 次;解码时取 3 个比特的大多数。解码比特的正确概率为 3p² - 2p³。这小于 p(如果 p < 1/2)。可以通过多次发送每个比特来减少解码比特错误的概率,但这在传输速率方面是浪费的。
参考资料。用于大容量存储系统(CD 和 DVD)和卫星传输(旅行者号探测器,火星探路者)当错误是突发性的时候。将要发送的数据视为一个度为 d 的多项式。只需要 d+1 个点来唯一指定多项式。发送更多点以实现纠错/检测错误。如果我们要发送的编码是 a0,a1,…,am-1(每个元素在有限域 K 上),将其视为多项式 p(x) = a0 + a1x + … + am-1 x^m-1。发送 p(0),p(b),p(b²),…,其中 b 是 K 上的乘法循环群的生成元。
大致来说,如果信道容量为 C,则我们可以以略低于 C 的速率发送比特,使用编码方案将解码错误的概率降低到任意所需水平。证明是非构造性的。
以下哪些编码是前缀自由的?唯一可解码的?对于那些唯一可解码的编码,给出编码为 1000000000000 的编码。
code 1 code 2 code 3 code 4
A 0 0 1 1
B 100 1 01 01
C 10 00 001 001
D 11 11 0001 000
给出一个不是前缀自由的唯一可解码编码的例子。
解决方案。 任何无后缀编码都是唯一可解码的,例如,{ 0, 01 }。
给出一个不是前缀自由或无后缀的唯一可解码编码的例子。
解决方案。 { 0011, 011, 11, 1110 }或{ 01, 10, 011, 110 }。
{ 1, 100000, 00 },{ 01, 1001, 1011, 111, 1110 }和{ 1, 011, 01110, 1110, 10011 }是唯一可解码的吗?如果不是,找到一个具有两个编码的字符串。解决方案。 第一组编码是唯一可解码的。第二组编码不是唯一可解码的,因为 111-01-1110-01 和 1110-111-1001 是 11101111001 的两种解码方式。第三组编码不是唯一可解码的,因为 01110-1110-011 和 011-1-011-10011 是 011101110011 的两种解码方式。
唯一可解码性测试。 实现 Sardinas-Patterson 算法,用于测试一组编码词是否是唯一可解码的:将所有编码词添加到一个集合中。检查所有编码词对,看看是否有一个是另一个的前缀;如果是,提取悬挂后缀(即,长字符串中不是短字符串前缀的部分)。如果悬挂后缀是一个编码词,则编码不是唯一可解码的;否则,将悬挂后缀添加到列表中(前提是它尚未存在)。重复此过程直到没有剩余的新悬挂后缀为止。
该算法是有限的,因为添加到列表中的所有悬挂后缀都是有限一组编码词的后缀,并且悬挂后缀最多只能添加一次。
Kraft-McMillan 不等式。 考虑一个具有长度为 n1, n2, …, nN 的 N 个编码词的编码 C。证明如果编码是唯一可解码的,则 K© = sum_i = 1 to N 2^(-ni) ≤ 1。
Kraft-McMillan 构造。 假设我们有一组满足不等式 sum_i = 1 to N 2^(-ni) ≤ 1 的整数 n1, n2, …, nN。证明总是可以找到一个编码长度为 n1, n2, …, nN 的前缀自由编码。因此,通过将注意力限制在前缀自由编码上(而不是唯一可解码编码),我们不会失去太多。
Kraft-McMillan 最优前缀自由编码等式。 证明如果 C 是一个最优前缀自由编码,那么 Kraft-McMillan 不等式是一个等式:K© = sum_i = 1 to N 2^(-ni) = 1。
假设所有符号概率都是 2 的负幂次方。描述哈夫曼编码。
假设所有符号频率相等。描述哈夫曼编码。
找到一个哈夫曼编码,其中概率为 pi 的符号的长度大于 ceil(-lg pi)。
解决方案. .01 (000), .30 (001), .34 (01), .35 (1)。码字 001 的长度大于 ceil(-lg .30)。
真或假。任何最优前缀自由编码都可以通过哈夫曼算法获得。
解决方案. 错误。考虑以下符号和频率集合(A 26, B 24, C 14, D 13, E 12, F 11)。
C1 C2 C3
A 26 01 10 00
B 24 10 01 01
C 14 000 111 100
D 13 001 110 101
E 12 110 001 110
F 11 111 000 111
在任何哈夫曼编码中,字符 A 和 B 的编码必须以不同的位开始,但是代码 C3 没有这个属性(尽管它是一个最优前缀自由编码)。
以下输入的 LZW 编码是什么?
描述 LZW 编码中的棘手情况。
解决方案. 每当遇到 cScSc,其中 c 是一个符号,S 是一个字符串,cS 在字典中但 cSc 不在字典中。
作为 N 的函数,编码 N 个符号 A 需要多少位?N 个序列 ABC 需要多少位?
让 F(i) 为第 i 个斐波那契数。考虑 N 个符号,其中第 i 个符号的频率为 F(i)。注意 F(1) + F(2) + … + F(N) = F(N+2) - 1。描述哈夫曼编码。
解决方案. 最长的码字长度为 N-1。
显示对于给定的 N 个符号集合,至少有 2^(N-1) 种不同的哈夫曼编码。
解决方案. 有 N-1 个内部节点,每个节点都可以任意选择其左右子节点。
给出一个哈夫曼编码,其中输出中 0 的频率远远高于 1 的频率。
解决方案. 如果字符 ‘A’ 出现一百万次,字符 ‘B’ 出现一次,那么 ‘A’ 的码字将是 0,‘B’ 的码字将是 1。
证明有关哈夫曼树的以下事实。
描述如何在一组符号 { 0, 1, …, N-1 } 上传输哈夫曼编码(或最优前缀自由编码),使用 2N - 1 + N ceil(lg N) 位。
提示:使用 2N-1 位来指定相应 trie 的结构。
假设在一个扩展 ASCII 文件(8 位字符)中,最大字符频率最多是最小字符频率的两倍。证明固定长度的 8 位扩展 ASCII 码是最优的。
香农-范诺编码。 证明哈夫曼算法的以下自顶向下版本不是最优的。将码字集合 C 分成两个子集 C1 和 C2,其频率(几乎)相等。递归地为 C1 和 C2 构建树,从 0 开始为 C1 的所有码字,从 1 开始为 C2 的所有码字。为了实现第一步,香农和范诺建议按频率对码字进行排序,并尽可能地将集合分成两个子数组。
解决方案. S 32, H 25, A 20, N 18, O 5。
LZMW 编码(米勒-韦格曼 1985)。 LZ 变种:在字典中搜索最长的已经存在的字符串(当前匹配);将前一个匹配与当前匹配的连接添加到字典中。字典条目增长更快。当字典填满时,也可以删除低频率条目。难以实现。
LZAP 编码。 类似于 LZMW:不仅添加前一个匹配与当前匹配的连接,还添加前一个匹配与当前匹配的 所有前缀 的连接。比 LZMW 更容易实现,但字典条目更多。
确定一个不是前缀自由的最优编码。
提示:只需要 3 个具有相等频率的符号。
确定对于相同输入的两个最优前缀自由编码,其码字长度分布不同。
提示:只需要 4 个符号。
最小方差 Huffman 编码。 由于与打破平局相关的不确定性,Huffman 算法可能生成具有不同码字长度分布的编码。在生成压缩流时传输,希望以(近)恒定速率传输比特。找到最小化 sum_i (p_i (l_i - l_average(T)) ²) 的 Huffman 编码。
解决方案。 在组合 tries 时,通过选择具有最小概率的最早生成的 trie 来打破平局。
用于 Huffman 编码的双队列算法。 证明以下算法计算出 Huffman 编码(如果输入符号已按频率排序,则在线性时间内运行)。维护两个 FIFO 队列:第一个队列包含输入符号,按频率升序排列,第二个队列包含组合权重的内部节点。只要两个队列中有超过一个节点,就通过检查两个队列的前端出队两个权重最小的节点。创建一个新的内部节点(左右子节点 = 两个节点,权重 = 两个节点的权重之和)并将其加入第二个队列。
要获得最小方差的 Huffman 编码,通过从第一个队列中选择节点来打破平局。
提示:证明第二个队列按频率升序排列。
兄弟属性。 如果(i)每个节点(除了根节点)都有一个兄弟节点,且(ii)二叉树可以按概率的非递增顺序列出,使得在列表中所有兄弟节点都相邻,则二叉树具有 兄弟属性。证明二叉树表示 Huffman 树当且仅当它具有兄弟属性。
相对编码。 不是压缩图像中的每个像素,而是考虑像素与前一个像素之间的差异并对差异进行编码。直觉:通常像素变化不大。与颜色表字母上的 LZW 一起使用。
可变宽度 LZW 编码。 在第 2^p 个码字插入表后,将表的宽度从 p 增加到 p+1。与颜色表字母一起使用。
自适应 Huffman 编码。 一次通过算法,不需要发送前缀自由码。根据迄今为止读入的字符的频率构建 Huffman 树。在读入每个字符后更新树。编码器和解码器需要协调处理平局的约定。
香农熵。 具有可能值 x1, …, xN 且以概率 p1, …, pN 出现的离散随机变量 X 的熵 H 定义为 H(X) = -p1 lg p1 - p2 lg p2 - … - pN lg pN,其中 0 lg 0 = 0 与极限一致。
经验熵。 经验熵 是通过计算每个符号出现频率并将其用作离散随机变量的概率来获得的一段文本的熵。计算你最喜欢小说的经验熵。将其与 Huffman 编码实现的数据压缩率进行比较。
香农实验。 进行以下实验。给一个主体一段文本(或 Leipzig 语料库)中的 k 个字母序列,并要求他们预测下一个字母。估计主体在 k = 1, 2, 5, 100 时答对的比例。
真或假。固定长度编码是���一可解码的。
解决方案。 真,它们是前缀自由的。
给出两棵不同高度的 Huffman 树字符串 ABCCDD。
前缀自由编码。 设计一个高效的算法来确定一组二进制码字是否是前缀自由的。提示:使用二进制 trie 或排序。
唯一可解码编码。 设计一个唯一可解码的编码,它不是前缀自由编码。提示:后缀自由编码 = 前缀自由编码的反向。后缀自由编码的反向是前缀自由编码 -> 可以通过以相反顺序读取压缩消息来解码。不太方便。
哈夫曼树。 修改 Huffman.java,使得编码器打印查找表而不是先序遍历,并修改解码器以通过读取查找表构建树。
真或假。在最佳前缀自由三进制编码中,出现频率最低的三个符号具有相同的长度。
解答。 False.
三进制哈夫曼编码。 将哈夫曼算法推广到三进制字母表(0, 1 和 2)上的码字,而不是二进制字母表。也就是说,给定一个字节流,找到一个使用尽可能少的三进制位(0、1 和 2)的前缀自由三进制编码。证明它产生最佳前缀自由三进制编码。
解答。 在每一步中合并最小的 3 个概率(而不是最小的 2 个)。当有 3 + 2k 个符号时,这种方法有效。为了将其减少到这种情况,添加概率为 0 的 1 或 2 个虚拟符号。(或者,如果符号数量不是 3 + 2k,则在第一步中合并少于 3 个符号。)例如:{ 0.1, 0.2, 0.2, 0.5 }。
非二进制哈夫曼编码。 将哈夫曼算法扩展到 m 进制字母表(0, 1, 2, …, m-1)上的码字,而不是二进制字母表。
考虑以下由 3 个 a、7 个 c、6 个 t 和 5 个 g 组成的 21 个字符消息。
a a c c c c a c t t g g g t t t t c c g g
以下的 43 位是否是上述消息的可能哈夫曼编码?
0000001111000101010010010010101010111001001
尽可能简洁而严谨地证明你的答案。
解答。 对于一条消息的哈夫曼编码会产生使用最少位数的编码,其中 2 位二进制码 a = 00, c = 01, g = 10, t = 11 是一个使用 21 * 2 = 42 位的前缀自由编码。因此,哈夫曼编码将使用少于 43 位。
如果一个二叉树是满的,则除了叶子节点外的每个节点都有两个子节点。证明与最佳前缀自由编码对应的任何二叉树都是满的。
提示:如果内部节点只有一个子节点,请用其唯一子节点替换该内部节点。
Move-to-front 编码(Bentley, Sleator, Tarjan 和 Wei 1986)。 编写一个名为 MoveToFront
的程序,实现 move-to-front 编码和解码。维护符号字母表的列表,其中频繁出现的符号位于前面。一个符号被编码为列表中在它之前的符号数。编码一个符号后,将其移动到列表的前面。参考
Move-ahead-k 编码。 与 move-to-front 编码相同,但将符号向前移动 k 个位置。
等待-c-并移动。 与 move-to-front 编码相同,但只有在符号在上次移动到前面后遇到 c 次后才将其移动到前面。
双哈夫曼压缩。 找到一个输入,对该输入应用 Huffman.java 中的 compress()
方法两次比仅应用 compress()
一次导致输出严格较小。
合并 k 个排序数组。 你有 k 个已排序的列表,长度分别为 n1、n2、…、nk。假设你可以执行的唯一操作是 2 路合并:给定长度为 n1 的一个已排序数组和长度为 n2 的另一个已排序数组,用长度为 n = n1 + n2 的已排序数组替换它们。此外,2 路合并操作需要 n 个单位的时间。合并 k 个已排序数组的最佳方法是什么?
解决方案. 将列表长度排序,使得 n1 < n2 < … < nk。重复地取最小的两个列表并应用 2 路合并操作。最优性的证明与哈夫曼编码的最优性证明相同:重复应用 2 路合并操作会产生一棵二叉树,其中每个叶节点对应于原始排序列表中的一个,每个内部节点对应于一个 2 路合并操作。任何原始列表对总体成本的贡献是列表长度乘以其树深度(因为这是其元素参与 2 路合并的次数)。
原文:
algs4.cs.princeton.edu/60context
译者:飞龙 协议:CC BY-NC-SA 4.0
本章节正在大规模施工中。
以下是本章节中的 Java 程序列表。点击程序名称以访问 Java 代码;点击参考编号以获取简要描述;阅读教材以获取详细讨论。
REF程序描述 / JAVADOC6.1CollisionSystem.java碰撞系统-Particle.java粒子6.2BTree.javaB 树6.3SuffixArray.java后缀数组(后缀排序)-SuffixArrayX.java后缀数组(优化)-LongestRepeatedSubstring.java最长重复子串-KWIK.java上下文关键词-LongestCommonSubstring.java最长公共子串6.4FordFulkerson.java最大流-最小割-FlowNetwork.java带容量网络-FlowEdge.java带流量的容量边-GlobalMincut.java全局最小割(Stoer-Wagner)⁵-BipartiteMatching.java二分图匹配(交替路径)-HopcroftKarp.java二分图匹配(Hopcroft-Karp)-AssignmentProblem.java加权二分图匹配-LinearProgramming.java线性规划(单纯形法)-TwoPersonZeroSumGame.java双人零和博弈