学习笔记
字典树,即Trie树,又称单词查找树或键树,是一种树形结构。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎用于文本词频统计。
它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。
- 结点本身不存完整单词;
- 从根结点到某一结点,路径上经过的字符串连接起来,为该结点对应的字符串;
- 每个结点的所有子结点路径代表的字符都不相同。
Trie树的核心思想是空间换时间。 利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
class Trie {
private boolean isEnd;
private Trie[] next;
/** Initialize your data structure here. */
public Trie() {
isEnd = false;
next = new Trie[26];
}
/** Inserts a word into the trie. */
public void insert(String word) {
if (word == null || word.length() == 0) return;
Trie curr = this;
char[] words = word.toCharArray();
for (int i = 0;i < words.length;i++) {
int n = words[i] - 'a';
if (curr.next[n] == null) curr.next[n] = new Trie();
curr = curr.next[n];
}
curr.isEnd = true;
}
/** Returns if the word is in the trie. */
public boolean search(String word) {
Trie node = searchPrefix(word);
return node != null && node.isEnd;
}
/** Returns if there is any word in the trie that starts with the given prefix. */
public boolean startsWith(String prefix) {
Trie node = searchPrefix(prefix);
return node != null;
}
private Trie searchPrefix(String word) {
Trie node = this;
char[] words = word.toCharArray();
for (int i = 0;i < words.length;i++) {
node = node.next[words[i] - 'a'];
if (node == null) return null;
}
return node;
}
}- 组团、配对问题
- Group or not?
- makeSet(s):建立一个新的并查集,其中包含s个单元素集
- unionSet(x, y):把元素x和元素y所在的集合合并,要求x和y所在的集合不相交,如果相交则不合并
- find(x):找到元素x所在的集合的代表,该操作也可以用于判断两个元素是否位于同一个集合,只要将它们各自的代表比较一下就可以了。
基本操作有初始化、查询、合并、路径压缩
class UnionFind {
private int count = 0;
private int[] parent;
public UnionFind(int n) {
count = n;
parent = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
public int find(int p) {
while (p != parent[p]) {
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
}
public void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ) return;
parent[rootP] = rootQ;
count--;
}
}def init(p):
# for i = 0 .. n: p[i] = i;
p = [i for i in range(n)]
def union(self, p, i, j):
p1 = self.parent(p, i)
p2 = self.parent(p, j)
p[p1] = p2
def parent(self, p, i):
root = i
while p[root] != root:
root = p[root]
while p[i] != i: # 路径压缩 ?
x = i; i = p[i]; p[x] = root
return root- 朴素搜索
- 优化方向:不重复(fibonacci)、剪枝(生成括号问题)
- 搜索方向:
- DFS:depth first search 深度优先搜索
- BFS:breath first search 广度优先搜索
优化:双向搜索、启发式搜索
课后作业:总结双向 BFS 代码模版,请同学们提交在学习总结中。
public class AStar
{
public final static int BAR = 1; // 障碍值
public final static int PATH = 2; // 路径
public final static int DIRECT_VALUE = 10; // 横竖移动代价
public final static int OBLIQUE_VALUE = 14; // 斜移动代价
Queue<Node> openList = new PriorityQueue<Node>(); // 优先队列(升序)
List<Node> closeList = new ArrayList<Node>();
// 开始算法
public void start(MapInfo mapInfo)
{
if(mapInfo==null) return;
// clean
openList.clear();
closeList.clear();
// 开始搜索
openList.add(mapInfo.start);
moveNodes(mapInfo);
}
// 移动当前结点
private void moveNodes(MapInfo mapInfo)
{
while (!openList.isEmpty())
{
Node current = openList.poll();
closeList.add(current);
addNeighborNodeInOpen(mapInfo,current);
if (isCoordInClose(mapInfo.end.coord))
{
drawPath(mapInfo.maps, mapInfo.end);
break;
}
}
}
// 在二维数组中绘制路径
private void drawPath(int[][] maps, Node end)
{
if(end==null||maps==null) return;
System.out.println("总代价:" + end.G);
while (end != null)
{
Coord c = end.coord;
maps[c.y][c.x] = PATH;
end = end.parent;
}
}
// 添加所有邻结点到open表
private void addNeighborNodeInOpen(MapInfo mapInfo,Node current)
{
int x = current.coord.x;
int y = current.coord.y;
// 左
addNeighborNodeInOpen(mapInfo,current, x - 1, y, DIRECT_VALUE);
// 上
addNeighborNodeInOpen(mapInfo,current, x, y - 1, DIRECT_VALUE);
// 右
addNeighborNodeInOpen(mapInfo,current, x + 1, y, DIRECT_VALUE);
// 下
addNeighborNodeInOpen(mapInfo,current, x, y + 1, DIRECT_VALUE);
// 左上
addNeighborNodeInOpen(mapInfo,current, x - 1, y - 1, OBLIQUE_VALUE);
// 右上
addNeighborNodeInOpen(mapInfo,current, x + 1, y - 1, OBLIQUE_VALUE);
// 右下
addNeighborNodeInOpen(mapInfo,current, x + 1, y + 1, OBLIQUE_VALUE);
// 左下
addNeighborNodeInOpen(mapInfo,current, x - 1, y + 1, OBLIQUE_VALUE);
}
// 添加一个邻结点到open表
private void addNeighborNodeInOpen(MapInfo mapInfo,Node current, int x, int y, int value)
{
if (canAddNodeToOpen(mapInfo,x, y))
{
Node end=mapInfo.end;
Coord coord = new Coord(x, y);
int G = current.G + value; // 计算邻结点的G值
Node child = findNodeInOpen(coord);
if (child == null)
{
int H=calcH(end.coord,coord); // 计算H值
if(isEndNode(end.coord,coord))
{
child=end;
child.parent=current;
child.G=G;
child.H=H;
}
else
{
child = new Node(coord, current, G, H);
}
openList.add(child);
}
else if (child.G > G)
{
child.G = G;
child.parent = current;
openList.add(child);
}
}
}
// 从Open列表中查找结点
private Node findNodeInOpen(Coord coord)
{
if (coord == null || openList.isEmpty()) return null;
for (Node node : openList)
{
if (node.coord.equals(coord))
{
return node;
}
}
return null;
}
// 计算H的估值:“曼哈顿”法,坐标分别取差值相加
private int calcH(Coord end,Coord coord)
{
return Math.abs(end.x - coord.x)
+ Math.abs(end.y - coord.y);
}
// 判断结点是否是最终结点
private boolean isEndNode(Coord end,Coord coord)
{
return coord != null && end.equals(coord);
}
// 判断结点能否放入Open列表
private boolean canAddNodeToOpen(MapInfo mapInfo,int x, int y)
{
// 是否在地图中
if (x < 0 || x >= mapInfo.width || y < 0 || y >= mapInfo.hight) return false;
// 判断是否是不可通过的结点
if (mapInfo.maps[y][x] == BAR) return false;
// 判断结点是否存在close表
if (isCoordInClose(x, y)) return false;
return true;
}
// 判断坐标是否在close表中
private boolean isCoordInClose(Coord coord)
{
return coord!=null&&isCoordInClose(coord.x, coord.y);
}
// 判断坐标是否在close表中
private boolean isCoordInClose(int x, int y)
{
if (closeList.isEmpty()) return false;
for (Node node : closeList)
{
if (node.coord.x == x && node.coord.y == y)
{
return true;
}
}
return false;
}
}- 前序(Pre-order):根-左-右
- 中序(In-order):左-根-右
- 后序(Post-order):左-右-根
二叉搜索树,是指一棵空树或者具有下列性质的二叉树:
- 左子树上所有结点的值均小于它的根结点的值;
- 右子树上所有结点的值均大于它的根结点的值;
- 以此类推:左、右子树也分别为二叉查找树。(这就是重复性!) 中序遍历:升序排列
查找结点:最优时间复杂度O(log2n),极端情况树退化成链变成O(n)
- 保证二维维度!-> 左右子树结点平衡(recursively)
- Balanced
- https://en.wikipedia.org/wiki/Self-balancing_binary_search_tree
维基百科-平衡树 其中需要掌握的有:
- 2-3 Tree
- AVL Tree
- B-Tree
- Red-Black Tree
- Splay Tree
- Treap
- 发明者G. M. Adelson-Velsky和Evgenii Landis,树的名字是发明者名称的缩写
- Balance Factor(平衡因子):
- 是它的左子树的高度减去它的右子树的高度(有时相反)
- balance factor = {-1,0,1}
- 通过旋转操作来进行平衡(四种)
- https://en.wikipedia.org/wiki/Self-balancing_binary_search_tree
- 平衡二叉搜索树
- 每个结点存在 balance factor = {-1, 0, 1}
- 四种旋转操作 不足:结点需要存储格外信息(平衡因子,需要一个int的空间)、且调整次数频繁
红黑树是一种近似平衡的二叉搜索树(Binary Search Tree),它能够确保任何一个结点的左右子树的高度差小于两倍。具体来说,红黑树是满足如下条件的二叉搜索树:
- 每个结点要么是红色,要么是黑丝(通过1个bit标记,0或1)
- 根结点是黑色
- 每个叶结点(NIL结点、空结点)是黑色的
- 不能有相邻接的两个红色结点
- 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点

从根到叶子的最长的可能路径不多余最短的可能路径的两倍长
- AVL trees provide faster lookups than Red Black Trees because they are more strictly balanced.
- Red Black Trees provide faster insertion and removal operations than AVL trees as fewer rotations are done due to relatively relaxed balancing.
- AVL trees store balance factors or heights with each node, thus requires storage for an integer per node whereas Red Black Tree requires only 1 bit of information per node.
- Red Black Trees are used in most of the language libraries like map, multimap, multisetin C++ whereas AVL trees are used in databases where faster retrievals are required.
| 题目编号 | 题目名称 | 难度 | 类型 | #1 | #2 | #3 | #4 | #5 |
|---|---|---|---|---|---|---|---|---|
| 208 | implement-trie-prefix-tree | Medium | Trie | 0921/0921 | 0921/0921 | 0922/0922 | 0929/0929 | |
| 212 | word-search-ii | Hard | Trie | 0921/0922 | 0921/0922 | 0923/0923 | 0929/0929 | |
| 547 | friend-circles | Medium | Disjoint Set | 0921/0922 | 0921/0922 | 0923/0923 | 0929/0929 | |
| X | 分析单词搜索2用Tire树方式实现的时间复杂度 | Medium | Trie | 0921/ | X | X | X | X |
分析单词搜索2用Tire树方式实现的时间复杂度,请同学们提交在学习总结中。 Ref
- 时间复杂度:O(M(4⋅3^(L−1))O(M(4⋅3L−1)),其中M 是二维网格中的单元格数,L 是单词的最大长度。
- 计算回溯算法将执行的确切步数是一个棘手的问题。我们为这个问题的最坏情况提供了该步骤的上限。该算法循环遍历二维网格中的所有单元,因此在复杂度公式中我们有M作为因子。然后将其归结为每个启动单元所需的最大步骤数(即 4⋅3^(L−1))。
- 假设单词的最大长度是 L,从一个单元格开始,最初我们最多可以探索 4 个方向。假设每个方向都是有效的(即最坏情况),在接下来的探索中,我们最多有3个相邻的单元(不包括我们来的单元)要探索。因此,在回溯探索期间,我们最多遍历 4⋅3^(L−1) 个单元格。
- 你可能会想最坏的情况是什么样子。这里有一个例子。想象一下,二维网格中的每个单元都包含字母 a,单词词典包含一个单词 ['aaaa']。这是算法将遇到的最坏的情况之一。
注意,上述时间复杂性是在Trie数据结构一旦构建就不会改变的假设下估计的。如果采用优化策略逐步删除Trie中的节点,则可以大大提高时间复杂度,因为一旦匹配词典中的所有单词,即 Trie 变为空,回溯的成本就会降低到零。
- 空间复杂度:O(N),其中 N 是字典中的字母总数。
- 算法消耗的主要空间是我们构建的 Trie数据结构。在最坏的情况下,如果单词之间没有前缀重叠,则Trie将拥有与所有单词的字母一样多的节点。也可以选择在 Trie 中保留单词的副本。因此,我们可能需要 2N 的空间用于 Trie。
| 题目编号 | 题目名称 | 难度 | 类型 | #1 | #2 | #3 | #4 | #5 |
|---|---|---|---|---|---|---|---|---|
| 200 | number-of-islands | Medium | Disjoint Set | 0922/0922 | 0922/0922 | 0923/0923 | 0930/0930 | |
| 130 | surrounded-regions | Medium | Disjoint Set | 0922/0922 | 0922/0922 | 0923/0923 | 0930/0930 | |
| 70 | climbing-stairs | Medium | Pruning | 0922/0922 | 0922/0922 | 0923/0923 | 0930/0930 | |
| 22 | generate-parentheses | Medium | Pruning | 0922/0922 | 0922/0922 | 0923/0923 | 0930/0930 |
| 题目编号 | 题目名称 | 难度 | 类型 | #1 | #2 | #3 | #4 | #5 |
|---|---|---|---|---|---|---|---|---|
| 51 | n-queens | Hard | Pruning | 0923/0923 | 0923/0923 | 0924/0924 | 1001/1002 | |
| 36 | valid-sudoku | Medium | Pruning | 0923/0923 | 0923/0923 | 0924/0924 | 1001/1002 | |
| 37 | sudoku-solver | Hard | Pruning | 0923/0923 | 0923/0923 | 0924/0924 | 1001/1002 |
| 题目编号 | 题目名称 | 难度 | 类型 | #1 | #2 | #3 | #4 | #5 |
|---|---|---|---|---|---|---|---|---|
| 127 | word-ladder | Medium | Two-End BFS | 0924/0924 | 0924/0924 | 0925/0925 | 1002/ | |
| 433 | minimum-genetic-mutation | Medium | Two-End BFS | 0924/0924 | 0924/0924 | 0925/0925 | 1002/ | |
| 1091 | shortest-path-in-binary-matrix | Medium | A* | 0924/0924 | 0924/0924 | 0925/0925 | 1002/ | |
| 37 | sudoku-solver | Hard | A* | 0924/ |
https://leetcode.com/problems/shortest-path-in-binary-matrix/discuss/313347/A*-search-in-Python https://zxi.mytechroad.com/blog/searching/8-puzzles-bidirectional-astar-vs-bidirectional-bfs/
| 题目编号 | 题目名称 | 难度 | 类型 | #1 | #2 | #3 | #4 | #5 |
|---|---|---|---|---|---|---|---|---|
| 773 | sliding-puzzle | Hard | BFS | 0925/0927 | 0925/0927 | 0928/1005 | 1003/1006 | |
| 146 | lru-cache | Medium | LRU Cache | 0925/0927 | 0925/0927 | 0928/1005 | 1003/1006 | |
| 242 | valid-anagram | Easy | Sort | 0925/0927 | 0925/0927 | 0928/1003 | 1003/1005 | |
| X | 总结双向 BFS 代码模版 | Medium | 双向BFS | 0925/0924 | X | X | X | X |
总结双向 BFS 代码模版,请同学们提交在学习总结中。
public int twoEndBFS(String beginWord, String endWord, List<String> wordList) {
if (beginWord == null || beginWord.length() == 0 || endWord == null
|| endWord.length() == 0 || beginWord.length() != endWord.length()) return 0;
Set<String> wordSet = new HashSet<>(wordList);
if (!wordSet.contains(endWord)) return 0;
Set<String> qs = new HashSet<>(), qe = new HashSet<>(), visited = new HashSet<>(), qn;
qs.add(beginWord);
visited.add(beginWord);
qe.add(endWord);
visited.add(endWord);
for (int len = 2; !qs.isEmpty(); len++) {
qn = new HashSet<>();
for (String word : qs) {
Set<String> nexts = getNextWords(word, wordSet);
for (String next : nexts) {
if (qe.contains(next)) return len;
if (visited.contains(next)) continue;
qn.add(next);
visited.add(next);
}
}
qs = (qn.size() < qe.size()) ? qn : qe;
qe = (qs == qe) ? qn : qe;
}
return 0;
}本周的A的部分还需要再深入理解,其他部分觉得还不错,目前代码测试时不知道是不是leetcode上数量级的问题,A*的算法比普通的BFS的使用时间更慢。推测是小数量级中,A中计算距离所消耗的时间拉平了优化的时间。
| 题目编号 | 题目名称 | 难度 | 类型 | #1 | #2 | #3 | #4 | #5 |
|---|---|---|---|---|---|---|---|---|
| 773 | sliding-puzzle | Hard | A* |
