ARTS是什么?
Algorithm:每周至少做一个leetcode的算法题;
Review:阅读并点评至少一篇英文技术文章;
Tip:学习至少一个技术技巧;
Share:分享一篇有观点和思考的技术文章。
Algorithm
LC 647. Palindromic Substrings
题目解析:
给定一个字符串,需要求出这个字符串的回文子串的个数。这是一道非常经典的题目,通过这道题可以衍生出来很多类似的题目。
一般说来,和回文相关的题目可以试着用动态规划去解,这里也有着相类似的解题思路。那么我们就通过这道题来看看用动态规划具体怎么解这一类的题目。首先,我们需要找到状态和子问题。其实题目暗示已经很明显了,起始下标和终止下标确定一个区间,也就是确定一个子串。那么我们就可以定义 dp[i][j]
表示的是区间 [i, j]
上的子串是否回文。然后,我们需要找到递归方程,也就是子问题之间的递进关系。换句话说,当前状态怎么推导到下一个状态的?我们需要找到相邻状态之间的逻辑关系。我们通过一个例子来看看:
比如,输入的字符串是 abcba
,我们可以画出表格
i\j a b c b a
a T F F F T
b F T F T F
c F F T F F
b F F F T F
a F F F F T
复制代码
通过上面的表格可以清晰地看到有 7 个 T
,也就是 abcba
有 7 个回文子串。同时,通过表格,我们也可以发现一些规律:
- 矩阵的对角线都是
T
。因为对角线表示的区间是单个字符,单个字符肯定回文 - 对角线将矩阵划分成上下两半,其中有一半肯定全是
F
。因为区间的起始下标必须要小于或等于终止下标,否则构不成区间,因此为F
。 - 状态
dp[i][j]
可以由状态dp[i+1][j-1]
推导得知。比如上面的dp[0][4]
是由dp[1][3]
推导得出的。
由此可知,我们需要找到状态 dp[i][j]
和 dp[i+1][j-1]
之间的递进关系。另外,我们还需要想出一个迭代/遍历方式,使得状态 dp[i+1]j-1]
的值在 dp[i][j]
更新之前被计算到。
首先回答第一个问题,从回文的定义以及区间的包含关系可以得出,如果 [i+1,j-1]
不是回文,那么 [i,j]
也肯定不是。但如果 [i+1,j-1]
是回文,我们还需要比较 i
,j
位置上的字符来确定 [i,j]
的状态,由此可得出递推方程:
dp[i][j] = dp[i+1][j-1] && s.charAt(i) == s.charAt(j)
复制代码
针对第二个问题,也就是如何遍历。正常情况下,我们都是采用逐行逐列的遍历方式。但对于这种问题,我们可以换一种思维方式。我们可以基于左右指针来遍历,这么说可能有点模糊,我们来看看下面这个模版:
// 这里 len 表示的是左右指针的间隔
// l, r 分别表示的是左指针和右指针
for (int len = 0; len < n; ++len) {
for (int l = 0, r = l + i; r < n; ++l, ++r) {
...
}
}
复制代码
这里我就不过多解释为什么这种遍历方式可以解决当前问题。你对照着之前的表格,试着在纸上画画就理解了。它其实是从对角线开始,斜着向矩阵的某一个角去遍历,区别于传统的从上到下,从左到右。
对于这个问题,还有另一个解法。这个解法利用了回文字串从中间向两边延伸的对称性质。因为这种解法并不通用,我就不过多展开了。
时间方面,两种解法的时间都是 O(n^2)
。空间上,动态规划使用了二维数组,空间复杂度是 O(n^2)
。
参考代码(动态规划):
public int countSubstrings(String s) {
if (s == null || s.length() == 0) {
return 0;
}
int n = s.length();
boolean[][] dp = new boolean[n][n];
int count = 0;
for (int i = 0; i < n; ++i) {
for (int l = 0, r = l + i; r < n; ++l, ++r) {
if (s.charAt(l) == s.charAt(r)) {
// 这里增加一个 i + 1 >= r - 1 的判断是为了防止越界
// 针对 l == r,或者 l+1 == r 的情况(这里只存在这两种特殊情况)
dp[l][r] = l + 1 >= r - 1 ? true : dp[l + 1][r - 1];
}
if (dp[l][r]) {
count++;
}
}
}
return count;
}
复制代码
参考代码:
public int countSubstrings(String s) {
if (s == null || s.length() == 0) {
return 0;
}
int count = 1;
for (int i = 1; i < s.length(); ++i) {
count += countByGivenMiddle(i - 1, i, s);
count += countByGivenMiddle(i, i, s);
}
return count;
}
public int countByGivenMiddle(int a, int b, String s) {
int count = 0;
int i = a, j = b;
while (i >= 0 && j < s.length() && s.charAt(i--) == s.charAt(j++)) {
count++;
}
return count;
}
复制代码
Review
Pro Git 的第 10 章,也是全书的最后一章。这一章主要讲述的是 Git 的一些底层实现机制。其实,Git 的所有秘密都存在在 .git
目录下,我们就先从这个目录入手,看看每个子目录具体表示的是什么:
.git/
config # 当前项目下的 git 配置
description # 主要用于 gitweb 项目,一般情况下用不到
HEAD # 存放当前 HEAD 指针的指向位置
hooks/ # 客户端/服务器 端的脚本文件
info/ # 里面有一个 exclude 文件,用于存放 ignore 的信息(区别于 .gitignore)
objects/ # 可以看作是 git 的数据库,用于存储 Git 主要的内容
refs/ # 存放一些指针(如 commit,tags,branch,remote)
index # 存放暂存区的信息
复制代码
Git 的核心是在 objects
目录下,这里存放的是版本控制所有的实际数据。研究 Git 的机制其实就是研究这个目录下的数据是如何存储的。
Git 是一个内容寻址文件系统。你可以把它想象成一个哈希表,每当你把一个 value 放进来,比如你的一个 commit 信息,git 会产生一个 key。也就是我们熟悉的长长的 SHA-1 字符串:
d5b95be9bc839aba25a6a2322aacc14e1db69fff
复制代码
往后,我们可以通过这个 SHA-1 字符串来对应到我们需要的内容。SHA-1 字符串,以及其对应的内容都是存在 objects
目录下的。它们是如何存储的呢,举个例子就明白了:
objects/
00/
...
d5/
b95be9bc839aba25a6a2322aacc14e1db69fff
...
...
ff/
复制代码
上面我们展示的 d5b95be9bc839aba25a6a2322aacc14e1db69fff
的内容其实存在于 objects/d5
目录下的 b95be9bc839aba25a6a2322aacc14e1db69fff
的文件中。这里你或许发现了,Git 用这个 SHA-1 字符串的头 2 个字符作为文件夹名,后 38 个字符作为文件名。个人觉得这样的分区设计的目的还是为了提升寻找时的效率。
说了这些,你可能好奇这个文件里面到底存了什么。这里我们先提一下 Git 中的几种基本的数据类型:blob
,tree
,commit
。下面的这张图可以很好地解释这 3 种数据结构的意义以及它们之间的相互关系:
首先我们来看看 commit 文件,对应于图中的 SHA-1 是 1a410e
,cac0ca
以及 fd4fc
(为了方便表示,这里只是缩写/前缀)。这些文件中存储的是某个 commit 的基本信息,比如作者,commit message 等。除了这些外,最关键的是它还会存放一些 SHA-1,对应的是当前 commit 的父 commit,还有就是指向当前 commit 的内容的 tree。这里就不得不说到第二种数据结构 tree
。你可以把它当作是一个目录,其实它实际存储时也是一个以 SHA-1 值命名的文件,区别在于文件中的内容。文件里面存储的是内容的元信息。比如一个 commit 改动了两个文件,那这个 commit 对应的 tree 中就会存有两个 SHA-1,分别表示两个 blob。比如图中的 0155eb
。最后说一下 blob
这个东西,这个可以说是 git 中最小的一个结构了。和前面一样,它也是一个以 SHA-1 后 38 位命名的文件,里面存放的就是某个文件的变动情况。
通过上面的解释以及描述,可以很清楚的看到 Git 中的数据存储其实很类似数据结构中的树。每一个节点表示的是一个文件,每个文件中存放了其子节点的 SHA-1(这里看作是子节点的地址)。根据节点的所在位置的不同,我们给了它们不一样的命名。根节点叫做 commit,叶子节点叫做 blob,中间的节点叫做 tree(tree 也可以作为叶子节点,比如一个空文件夹)。
Tip
记录一下近期学到的一些计算机基础知识:
-
每台计算机都有一个 字长(word size),也就是我们平时经常说的 “32位/64位的系统”。它决定的是虚拟地址空间的最大大小。32 位字长限制的地址范围为
0 ~ 2^32 - 1
位,差不多就是 4GB 的空间。当然,我们知道这对于现代的很多计算机是不够的,因此很早就出现了从 32 位字长机器到 64 位字长机器的迁移。扩展到 64 位字长使得虚拟地址空间变为 16EB,这就远远满足了大多数计算机的需求。 -
不管是程序还是指令,到了机器底层都会成为 0,1 序列。但是不同机器的字节顺序是不一样的,主要有两种规则 - 大端法,小端法。最低有效字节在最前面的方式是小端法,最高有效字节在最前面的方式是大端法。
-
ASCII 码是字符编码的基础。在计算机中,我们可以运行
man ascii
来生成一张 ASCII 字符码表。
Share
新年刚过,到了定计划的时候了