树是一种(可能非线性的)数据结构,由节点或顶点和边组成,不具有任何循环。
树是算法中最重要的数据结构之一
像我们目前的文件系统
树有许多不同的类型
Tree
B Tree
Trie Tree
Binary Tree
BST
RB Tree
AVL
A
B
C
D
E
F
G
J
I
H
public class BinaryTree<T> {
private Node<T> root;
public Tree(T rootData) {
root = new Node<T>();
root.data = rootData;
}
public static class Node<T> {
private T data;
private Node<T> leftNode;
private Node<T> rightNode;
}
}A
B
D
E
H
I
J
G
F
C
先历顺序: A B D E H I C F G J
中序遍历: D B H E I A F C G J
后序遍历: D H I E B F J G C A
我们需要使用递归来遍历一棵树
public void preorder(TreeNode root) {
if(root != null) {
//Visit the node by Printing the node data
System.out.printf("%c ",root.data);
preorder(root.left);
preorder(root.right);
}
} public void inorder(TreeNode root) {
if(root != null) {
inorder(root.left);
System.out.printf("%c ",root.data);
inorder(root.right);
}
} public void postorder(TreeNode root) {
if(root != null) {
postorder(root.left);
postorder(root.right);
System.out.printf("%c ",root.data);
}
} 我们能否使用栈来遍历树而不使用递归?
前序遍历:访问当前节点,将右子节点压入栈中,再将左子节点压入栈中
因为需要先访问左子节点
public static void PreOrder(Node root) {
Stack<Node> nodeStack = new Stack<Node>();
nodeStack.push(root);
while(!nodeStack.empty()) {
Node node = nodeStack.pop();
System.out.printf("%c ", node.data);
if(node.rightNode != null) {
nodeStack.push(node.rightNode);
}
if(node.leftNode != null) {
nodeStack.push(node.leftNode);
}
}
}我们有更好的方法吗?
我们能否在堆栈上节省一些空间?
我们需要同时将两个子节点推入栈中吗?
public static void PreOrder2(Node root) {
Stack<Node> nodeStack = new Stack<Node>();
nodeStack.push(root);
Node node = root;
while(!nodeStack.empty()) {
System.out.printf("%c ", node.data);
if(node.rightNode != null) {
nodeStack.push(node.rightNode);
}
if(node.leftNode != null) {
node = node.leftNode;
}
else {
node = nodeStack.pop();
}
}
}中序遍历:我们应该怎么做?
中序遍历: 我们将当前节点压入栈中,然后一直向左走,如果没有左节点了,就弹出当前节点,输出它的值,并将指针指向右节点
public static void InOrder(Node root) {
Stack<Node> nodeStack = new Stack<Node>();
Node node = root;
while(!nodeStack.empty() || node != null) {
if(node != null) {
nodeStack.push(node);
node = node.leftNode;
}
else {
node = nodeStack.pop();
System.out.printf("%c ", node.data);
node = node.rightNode;
}
}
}后序遍历是最困难的一种遍历方式
后序遍历:先往左走,如果没有左孩子,则访问它。然后对于栈顶的节点,我们现在访问它吗?
不,因为那是中序遍历
我们需要接着往右边走,直到不能再走,然后访问并弹出
二叉树的后序遍历是最难的一种遍历方式
可以通过对当前数据结构的操作来实现后序遍历吗?
不行。我们不知道栈顶元素的状态。右子树是否已经被遍历过了呢?
我们需要一个额外的标志来存储节点的状态
static class NodeWithFlag {
Node node;
boolean flag;
public NodeWithFlag(Node n, boolean value) {
node = n;
flag = value;
}
}在我们访问完右子节点后,我们会更新标志;
public static void PostOrder(Node root) {
Stack<NodeWithFlag> nodeStack = new Stack<NodeWithFlag>();
Node curNode = root;
NodeWithFlag newNode;
while(!nodeStack.empty() || curNode != null) {
while(curNode != null) {
newNode = new NodeWithFlag(curNode, false);
nodeStack.push(newNode);
curNode = curNode.leftNode;
}
newNode = nodeStack.pop();
curNode = newNode.node;
if(!newNode.flag) {
newNode.flag = true;
nodeStack.push(newNode);
curNode = curNode.rightNode;
}
else {
System.out.printf("%c ", curNode.data);
curNode = null;
}
}
}另一种方法是使用 HashSet 存储已访问的节点
第一次访问:HashSet.contains() == false -> 加入到集合中
第二次访问:HashSet.contains() == true -> 输出该节点的值
如果我们有一个树的遍历结果,我们能构造出这个树吗?
前序遍历: A B C D
A
B
D
C
A
B
D
C
无论是给定先序遍历、中序遍历还是后序遍历,都可以构建不同的二叉树
如果我们给出两个遍历结果呢?
先序遍历(PreOrder): A B C D
A
B
D
C
A
B
D
C
中序遍历(InOrder): B A D C
先序遍历(PreOrder): A B C D
A
B
D
C
后序遍历(PostOrder): B D C A
A
B
D
C
结论:你需要两次遍历才能构造一棵树,其中之一必须是中序遍历
请给出一个先序遍历和中序遍历,如何构建一棵二叉树?
A
B
D
E
H
I
J
G
F
C
先序遍历(PreOrder): A B D E H I C F G J
中序遍历(InOrder): D B H E I A F C G J
A是根节点,所以A是先序遍历中的第一个节点,而中序遍历可以使用A来分离左子树和右子树
然后我们可以进行递归
public TreeNode buildTree(int[] preorder, int[] inorder) {
int preStart = 0;
int preEnd = preorder.length-1;
int inStart = 0;
int inEnd = inorder.length-1;
return construct(preorder, preStart, preEnd, inorder, inStart, inEnd);
}
public TreeNode construct(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd){
if(preStart>preEnd||inStart>inEnd){
return null;
}
int val = preorder[preStart];
TreeNode p = new TreeNode(val);
int k=0;
for(int i=inStart; i<=inEnd; i++){
if(val == inorder[i]){
k=i;
break;
}
}
p.left = construct(preorder, preStart+1, preStart+(k-inStart), inorder, inStart, k-1);
p.right= construct(preorder, preStart+(k-inStart)+1, preEnd, inorder, k+1 , inEnd);
return p;
}A
B
D
E
H
I
J
G
F
C
后序遍历(PostOrder): D H I E B F J G C A
中序遍历(InOrder): D B H E I A F C G J
这种构建二叉树的方法和先序遍历+中序遍历的方法类似,唯一的不同在于根节点现在是后序遍历中的最后一个元素
public TreeNode buildTree(int[] inorder, int[] postorder) {
int inStart = 0;
int inEnd = inorder.length - 1;
int postStart = 0;
int postEnd = postorder.length - 1;
return build(inorder, inStart, inEnd, postorder, postStart, postEnd);
}
public TreeNode build(int[] inorder, int inStart, int inEnd, int[] postorder, int postStart, int postEnd) {
if (inStart > inEnd || postStart > postEnd)
return null;
int rootValue = postorder[postEnd];
TreeNode root = new TreeNode(rootValue);
int k = 0;
for (int i = inStart; i <=inEnd; i++) {
if (inorder[i] == rootValue) {
k = i;
break;
}
}
root.left = build(inorder, inStart, k - 1, postorder, postStart,
postStart + k - (inStart + 1));
root.right = build(inorder, k + 1, inEnd, postorder, postStart + k- inStart, postEnd - 1);
return root;
}30
18
13
24
22
27
47
40
31
34
删除
30
18
13
24
22
27
47
40
31
34
查找 24
查找42
public static boolean find(int value, Node root) {
Node node = root;
while(node != null) {
if(node.data > value) {
node = node.leftNode;
}
else if(node.data < value) {
node = node.rightNode;
}
else return true;
}
return false;
}30
18
13
24
22
27
47
40
31
34
添加 42
30
18
13
24
22
27
47
40
31
34
30
18
13
24
22
27
47
40
31
34
42
public static boolean add(int value, Node root) {
if(root == null) {
root = new Node(value);
return true;
}
Node node = root;
while(node != null) {
if(node.data > value) {
if(node.leftNode != null) {
node = node.leftNode;
}
else {
node.leftNode = new Node(value);
return true;
}
}
else if(node.data < value) {
if(node.rightNode != null) {
node = node.rightNode;
}
else {
node.rightNode = new Node(value);
return true;
}
}
else return false;
}
return false;
}查找和添加元素到树中的时间复杂度是多少?
30
18
13
24
22
27
47
40
31
34
移除 27
移除30
P
Q
R
P
Q
R
P
Q
R
P
Q
R
P<R<Q
P<Q<R
R<Q<P
Q<R<P
只要用R来代替Q
P
Q
R1
R2
如果我们移除 Q,哪个元素最适合替代它呢?
P
Q
R1
R2
考虑中序遍历,在Q之前和之后的节点是最佳替代者,因为如果它们替代Q,再进行中序遍历,输出结果仍然是一个排序的数组,这意味着树仍然是BST。
那么,这两个节点在哪里?
P
Q
R1
R2
因此,位于 Q 前面的元素是子树 R1 中的最大节点,而位于 Q 后面的元素是子树 R2 中的最小节点。
这个节点有没有子节点?我们能直接移除它们吗?
也许有,但最多只有一个,所以我们回到条件2。
public static boolean remove(int value, Node root) {
if(root == null) return false;
if(root.data == value) {
root = removeNode(root);
return true;
}
Node node = root;
while(node != null) {
if(node.data > value) {
if(node.leftNode != null && node.leftNode.data != value) {
node = node.leftNode;
}
else if(node.leftNode == null) return false;
else {
node.leftNode = removeNode(node.leftNode);
return true;
}
}
else if(node.data < value) {
if(node.rightNode != null && node.rightNode.data != value) {
node = node.rightNode;
}
else if(node.rightNode == null) return false;
else {
node.rightNode = removeNode(node.rightNode);
return true;
}
}
else return false;
}
return false;
}public static Node removeNode(Node node) {
if(node.leftNode == null && node.rightNode == null) {
return null;
}
else if(node.leftNode == null) {
return node.rightNode;
}
else if(node.rightNode == null) {
return node.leftNode;
}
else {
node.data = findAndRemove(node);
return node;
}
}
public static int findAndRemove(Node node) {
int result;
if(node.leftNode.rightNode == null) {
result = node.leftNode.data;
node.leftNode = node.leftNode.leftNode;
return result;
}
node = node.leftNode;
while(node.rightNode.rightNode != null) {
node = node.rightNode;
}
result = node.rightNode.data;
node.rightNode = node.rightNode.leftNode;
return result;
}30
18
13
24
22
27
47
40
31
34
42
43
30
18
13
24
22
27
47
43
34
40
42
31
我们知道搜索时间与树的高度密切相关。
如果我们不断添加和删除元素,树就会变得不平衡。
因此,我们有红黑树和AVL树,它们可以使用旋转和重构来使树保持平衡。
在计算机科学中,Trie树也被称为数字树,有时也称为基数树或前缀树(因为可以按前缀搜索),它是一种用于存储动态集合或关联数组的有序树数据结构,其中键通常是字符串。
在插入和查找中,需要注意的是,在树中下降一个级别可以在常数时间内完成,每次程序下降一个级别,一个字符就会从字符串中删除。
我们可以得出结论,每个函数将在树中下降L级,每次函数在树中下降一级,时间复杂度为常数,因此在Trie中插入和查找单词的时间复杂度为O(L)。
Trie树所使用的内存取决于存储边缘的方法以及有多少个单词具有相同的前缀。
实现一个 trie,包含插入 (insert),查找 (search),以及按前缀查找 (startsWith) 方法。
我们只使用小写字母'a'到'z'
一个节点有多少个子节点?
class TrieNode {
// Initialize your data structure here.
boolean isWord;
TrieNode[] children;
public TrieNode() {
children = new TrieNode[26];
isWord = false;
}
}public class Trie {
private TrieNode root;
public Trie() {
root = new TrieNode();
}
// Inserts a word into the trie.
public void insert(String word) {
if(word == null || word.length() == 0) return;
TrieNode pNode = root;
for(int i = 0; i < word.length(); i ++) {
char c = word.charAt(i);
int index = c - 'a';
if(pNode.children[index] == null) {
TrieNode newNode = new TrieNode();
pNode.children[index] = newNode;
}
pNode = pNode.children[index];
}
pNode.isWord = true;
}
}// Returns if the word is in the trie.
public boolean search(String word) {
TrieNode pNode = root;
if(word == null || word.length() == 0) return true;
for(int i = 0; i < word.length(); i ++) {
int index = word.charAt(i) - 'a';
pNode = pNode.children[index];
if(pNode == null) return false;
}
return pNode.isWord;
}
// Returns if there is any word in the trie that starts with the given prefix.
public boolean startsWith(String prefix) {
TrieNode pNode = root;
if(prefix == null || prefix.length() == 0) return true;
for(int i = 0; i < prefix.length(); i ++) {
int index = prefix.charAt(i) - 'a';
pNode = pNode.children[index];
if(pNode == null) return false;
}
return true;
}设计一个内存文件系统,模拟以下功能:
ls:给定一个字符串格式的路径。如果它是一个文件路径,则返回只包含该文件名的列表。如果它是一个目录路径,则返回该目录中文件和目录名称的列表。输出(文件和目录名称一起)应按字典顺序排列。
mkdir:给定一个不存在的目录路径,应根据路径创建一个新的目录。如果路径中间的目录也不存在,则需要创建它们。此函数返回void类型。
addContentToFile:给定一个文件路径和字符串格式的文件内容。如果文件不存在,则需要创建包含给定内容的文件。如果文件已经存在,则需要将给定内容追加到原始内容。此函数返回void类型。
readContentFromFile:给定一个文件路径,返回其字符串格式的内容。
我们提到文件系统是一棵树。
文件系统可以在每个节点下拥有很多子节点。因此,使用 Trie 来表示它是一个不错的选择。
class TrieNode {
// Initialize your data structure here.
boolean isFile;
String content;
Map<String, TrieNode> children;
public TrieNode() {
isFile = false;
children = new HashMap<>();
}
}class FileSystem {
TrieNode root;
public FileSystem() {
root = new TrieNode();
}
public List<String> ls(String path) {
List<String> results = new ArrayList<>();
TrieNode cur = root;
String[] routes = path.split("/");
for (int i = 1; i < routes.length; i ++) {
cur = cur.children.get(routes[i]);
}
if (cur.isFile) {
results.add(routes[routes.length - 1]);
} else {
results.addAll(cur.children.keySet());
Collections.sort(results);
}
return results;
}
public void mkdir(String path) {
getCurrentNode(path);
}
}class FileSystem {
TrieNode root;
public FileSystem() {
root = new TrieNode();
}
public void addContentToFile(String filePath, String content) {
TrieNode cur = getCurrentNode(filePath);
cur.isFile = true;
if (cur.content == null) {
cur.content = new String(content);
} else {
cur.content = cur.content.concat(content);
}
}
public String readContentFromFile(String filePath) {
TrieNode cur = getCurrentNode(filePath);
return cur.content;
}
private TrieNode getCurrentNode(String path) {
TrieNode cur = root;
String[] routes = path.split("/");
for (int i = 1; i < routes.length; i ++) {
if (!cur.children.containsKey(routes[i])) {
cur.children.put(routes[i], new TrieNode());
}
cur = cur.children.get(routes[i]);
}
return cur;
}
}我们使用 Trie 来存储只含有小写字母的单词,但是 Trie 也可以用来存储许多其他的数据。我们可以使用位或字节代替小写字母,而且 Trie 还能够存储各种数据类型,而不仅仅是字符串。