分类

  • 二分搜索树(Binary Search Tree)
  • 红黑树、AVL
  • 字典树、前缀树

构建二叉树

  1. private class Node{
  2. public E e;
  3. public Node left;
  4. public Node right;
  5. public Node(E e){
  6. this.e = e;
  7. left = null;
  8. right = null;
  9. }
  10. }

二分搜索树

二分搜索树是二叉树,二分搜索树的每个节点的值满足

  1. 大于其左子树的所有结点的值
  2. 小于其右子树的所有结点的值
  3. 每个子树也是一个二分搜索树

特点:每个节点的key值大于左节点 小于右节点

结论:

  • 二叉搜索树的最小节点是根节点左边子树的最左子树
  • 二叉搜索树的最大节点是根节点右边子树的最右子树

插入操作

二分搜索树 - 图1

  1. public void add(E e){
  2. if(root == null){
  3. root = new Node(e);
  4. size++;
  5. }else
  6. add(root, e);
  7. }
  8. //向以node为根的二分搜索树中插入元素E
  9. private void add(Node node, E e){
  10. if(e.equals(node.e)){ //终止条件
  11. return;
  12. }else if(e.compareTo(node.e) <0 && node.left == null){
  13. node.left = new Node(e);
  14. size++;
  15. return ;
  16. }else if(e.compareTo(node.e) >0 && node.right == null){
  17. node.right = new Node(e);
  18. size++;
  19. return;
  20. }
  21. if(e.compareTo(node.e) < 0)
  22. add(node.left, e);
  23. else
  24. add(node.right, e);
  25. }

简化版

  1. //添加元素
  2. public void add(E e){
  3. root = add(root, e);
  4. }
  5. //向以node为根的二分搜索树中插入元素E
  6. private Node add(Node node, E e){
  7. if(node == null){
  8. size++;
  9. return new Node(e);
  10. }
  11. //比较插入的元素与node节点的大小
  12. if(e.compareTo(node.e) < 0){
  13. node.left = add(node.left,e);//
  14. }else if(e.compareTo(node.e)>0){
  15. node.right = add(node.right,e);
  16. }
  17. return node;
  18. }

查询元素

  1. /*查询元素*/
  2. public boolean contains(E e){
  3. return contains(root, e);
  4. }
  5. private boolean contains(Node node, E e){
  6. if(node == null)//终止条件
  7. return false;
  8. if(e.compareTo(node.e) == 0)
  9. return true; //找到了
  10. else if(e.compareTo(node.e) <0)
  11. return contains(node.left, e);
  12. else
  13. return contains(node.right, e);
  14. }

遍历操作

  • 前序遍历(根、左、右)
  • 中序遍历(左、根、右)
  • 后序遍历(左、右、根)
  1. /*前序遍历*/
  2. public void preOrder(){
  3. preOrder(root);
  4. }
  5. private void preOrder(Node node){
  6. if(node == null)
  7. return;
  8. System.out.println(node.e);
  9. preOrder(node.left);//访问左
  10. preOrder(node.right);//访问右
  11. }
  1. /*中序遍历*/
  2. public void midOrder(){
  3. midOrder(root);
  4. }
  5. public void midOrder(Node node){
  6. if(node == null)
  7. return;
  8. midOrder(node.left);
  9. System.out.println(node.e);
  10. midOrder(node.right);
  11. }

惊奇的是这样的树经过中序遍历后元素是经过排过序的

  1. /*后序遍历*/
  2. public void postOrder(){
  3. postOrder(root);
  4. }
  5. private void postOrder(Node node){
  6. if(node == null)
  7. return;
  8. postOrder(node.left);
  9. postOrder(node.right);
  10. System.out.println(node.e);
  11. }

二分搜索树的层序遍历

也是对树的bfs

  1. public void bfs(){
  2. //借助队列的数据结构
  3. Queue<Node> q = new ArrayDeque<>();
  4. //首先添加root结点
  5. q.add(root);
  6. while(!q.isEmpty()){
  7. Node cur = q.remove();
  8. System.out.print(cur.e);
  9. if(cur.left != null) //添加左孩子与右孩子
  10. q.add(cur.left);
  11. if(cur.right != null)
  12. q.add(cur.right);
  13. }
  14. }

删除操作

首先确立二分搜索树的最大值与最小值

最小值一定会在根元素左的最左边。如果最小值拥有右子树,删除本节点右子树代替当前位置

最大值一定会在根元素的右的最右边。如果是一个叶子节点直接删除如果拥有左子树,则删除本节点 本节点的左子树代替当前位置。

先看一下如何查找元素

二分搜索树 - 图2

  1. /*寻找最大值与最小值*/
  2. public E findMax(){
  3. if(size==0)
  4. throw new IllegalArgumentException("Is Empty!");
  5. return findMax(root).e;
  6. }
  7. private Node findMax(Node node){
  8. if(node.right == null)
  9. return node;
  10. return findMax(node.right);
  11. }
  12. public E findMin(){
  13. if(size==0)
  14. throw new IllegalArgumentException("Is Empty!");
  15. return findMin(root).e;
  16. }
  17. private Node findMin(Node node){
  18. if(node.left == null)
  19. return node;
  20. return findMin(node.left);
  21. }
  22. // 删除最大值与最小值
  23. //删除以node为根的二分搜索树中的最大节点 返回删除节点后新的二分搜索树的根
  24. public E removeMax(){
  25. E res = findMax();
  26. root = removeMax(root);
  27. return res;
  28. }
  29. private Node removeMax(Node node){ //传入结点
  30. if(node.right == null){//已经递归到底
  31. Node leftNode = node.left; //接住结点的左树
  32. node.left = null;
  33. size--;
  34. return leftNode;
  35. }
  36. node.right = removeMax(node.right);
  37. return node;
  38. }

删除单个节点的分类

  1. 拥有一个左子树
  2. 拥有一个右子树
  3. 是一个叶子节点
  4. 拥有左右两个子树(难点)

删除左右都有孩子的节点d。找到s = min(d->right) s是d的后继

s->right = delMin(d->right) 删除节点的右子树

s->left = d->left 删除节点的左子树

删除d,s是新的子树的根

二分搜索树 - 图3

  1. /*删除元素节点*/
  2. public void remove(E e){
  3. root = remove(root,e);
  4. }
  5. private Node remove(Node node,E e){
  6. if(node == null)
  7. return null; //没找着
  8. if(e.compareTo(node.e) <0){
  9. node.left = remove(node.left , e);
  10. return node;
  11. }else if(e.compareTo(node.e)>0){
  12. node.right = remove(node.right,e);
  13. return node;
  14. }else{
  15. // e == node.e
  16. if(node.left == null){ //如果删除节点左子树为空
  17. Node rightNode = node.right;
  18. node.right = null;
  19. size--;
  20. return rightNode;
  21. }
  22. if(node.right == null){//如果删除节点右子树为空
  23. Node leftNode = node.left;
  24. node.left = null;
  25. size--;
  26. return leftNode;
  27. }
  28. //如果删除节点左右均不为空
  29. Node successor = findMin(node.right);
  30. successor.right = removeMin(node.right);
  31. successor.left = node.left;
  32. node.left = node.right = null;
  33. return successor;
  34. }
  35. }

完整代码

  1. import java.util.ArrayDeque;
  2. import java.util.LinkedList;
  3. import java.util.Queue;
  4. public class BST<E extends Comparable<E>> {
  5. /**
  6. * 结点
  7. */
  8. private class Node{
  9. public E e;
  10. public Node left;
  11. public Node right;
  12. public Node(E e){
  13. this.e = e;
  14. left = null;
  15. right = null;
  16. }
  17. }
  18. private Node root;
  19. private int size;
  20. public BST(){
  21. root = null;
  22. size=0;
  23. }
  24. public int size(){
  25. return size;
  26. }
  27. public boolean isEmppty(){
  28. return size==0;
  29. }
  30. /*添加元素*/
  31. public void add(E e){
  32. root = add(root, e);
  33. }
  34. //向以node为根的二分搜索树中插入元素E
  35. private Node add(Node node, E e){
  36. if(node == null){
  37. size++;
  38. return new Node(e);
  39. }
  40. if(e.compareTo(node.e) < 0){
  41. node.left = add(node.left,e);//
  42. }else if(e.compareTo(node.e)>0){
  43. node.right = add(node.right,e);
  44. }
  45. return node;
  46. }
  47. /*查询元素*/
  48. public boolean contains(E e){
  49. return contains(root, e);
  50. }
  51. private boolean contains(Node node, E e){
  52. if(node == null)//终止条件
  53. return false;
  54. if(e.compareTo(node.e) == 0)
  55. return true; //找到了
  56. else if(e.compareTo(node.e) <0)
  57. return contains(node.left, e);
  58. else
  59. return contains(node.right, e);
  60. }
  61. /*前序遍历*/
  62. public void preOrder(){
  63. preOrder(root);
  64. }
  65. private void preOrder(Node node){
  66. if(node == null)
  67. return;
  68. System.out.println(node.e);
  69. preOrder(node.left);//访问左
  70. preOrder(node.right);//访问右
  71. }
  72. /*中序遍历*/
  73. public void midOrder(){
  74. midOrder(root);
  75. }
  76. public void midOrder(Node node){
  77. if(node == null)
  78. return;
  79. midOrder(node.left);
  80. System.out.println(node.e);
  81. midOrder(node.right);
  82. }
  83. /*后序遍历*/
  84. public void postOrder(){
  85. postOrder(root);
  86. }
  87. private void postOrder(Node node){
  88. if(node == null)
  89. return;
  90. postOrder(node.left);
  91. postOrder(node.right);
  92. System.out.println(node.e);
  93. }
  94. /*寻找最大值与最小值*/
  95. public E findMax(){
  96. if(size==0)
  97. throw new IllegalArgumentException("Is Empty!");
  98. return findMax(root).e;
  99. }
  100. private Node findMax(Node node){
  101. if(node.right == null)
  102. return node;
  103. return findMax(node.right);
  104. }
  105. public E findMin(){
  106. if(size==0)
  107. throw new IllegalArgumentException("Is Empty!");
  108. return findMin(root).e;
  109. }
  110. private Node findMin(Node node){
  111. if(node.left == null)
  112. return node;
  113. return findMin(node.left);
  114. }
  115. // 删除最大值与最小值
  116. //删除以node为根的二分搜索树中的最大节点 返回删除节点后新的二分搜索树的根
  117. public E removeMax(){
  118. E res = findMax();
  119. root = removeMax(root);
  120. return res;
  121. }
  122. private Node removeMax(Node node){ //传入结点
  123. if(node.right == null){//已经递归到底
  124. Node leftNode = node.left; //接住结点的左树
  125. node.left = null;
  126. size--;
  127. return leftNode;
  128. }
  129. node.right = removeMax(node.right);
  130. return node;
  131. }
  132. private Node removeMin(Node node){ //传入结点
  133. if(node.left == null){//已经递归到底
  134. Node rightNode = node.right; //接住结点的左树
  135. node.right = null;
  136. size--;
  137. return rightNode;
  138. }
  139. node.left = removeMin(node.left);
  140. return node;
  141. }
  142. /*删除元素*/
  143. public void remove(E e){
  144. root = remove(root,e);
  145. }
  146. private Node remove(Node node,E e){
  147. if(node == null)
  148. return null; //没找着
  149. if(e.compareTo(node.e) <0){
  150. node.left = remove(node.left , e);
  151. return node;
  152. }else if(e.compareTo(node.e)>0){
  153. node.right = remove(node.right,e);
  154. return node;
  155. }else{
  156. // e == node.e
  157. if(node.left == null){ //如果删除节点左子树为空
  158. Node rightNode = node.right;
  159. node.right = null;
  160. size--;
  161. return rightNode;
  162. }
  163. if(node.right == null){//如果删除节点右子树为空
  164. Node leftNode = node.left;
  165. node.left = null;
  166. size--;
  167. return leftNode;
  168. }
  169. //如果删除节点左右均不为空
  170. Node successor = findMin(node.right);
  171. successor.right = removeMin(node.right);
  172. successor.left = node.left;
  173. node.left = node.right = null;
  174. return successor;
  175. }
  176. }
  177. @Override
  178. public String toString() {
  179. StringBuilder res = new StringBuilder();
  180. BSTString(root,0,res);
  181. return res.toString();
  182. }
  183. // 生成以node为根结点 深度为depth的二叉树的字符串
  184. private void BSTString(Node node, int depth, StringBuilder res){
  185. if(node == null){
  186. res.append(BSTString(depth)+"null\n");
  187. return;
  188. }
  189. res.append(BSTString(depth) + node.e + "\n");
  190. BSTString(node.left, depth+1, res);
  191. BSTString(node.right, depth+1, res);
  192. }
  193. private String BSTString(int depth){
  194. StringBuilder res = new StringBuilder();
  195. for(int i=0; i<depth; i++)
  196. res.append("--");
  197. return res.toString();
  198. }
  199. }

Tree 前缀树

Trie是一个树形结构。是一种专门处理字符串匹配的数据结构,用来解决字符串集合中快速查找字符串的问题。

Trie也成为前缀树

Trie查询每条条目的事件复杂度与字典中一共多少条数目无关。

时间复杂度O(w),w为查询单词的长度。

核心思想:通过最大限度地减少字符串比较,用空间换时间,利用共同前缀提高查询效率

Trie特点

  1. 根节点没有任何字符,除根节点外每一个节点都只包含一个字符
  2. 根节点开始到某一结点路径上经过的字符连接起来就是搜索的字符串

构造

每个节点有若干指向下个节点的指针

  1. class Node{
  2. char c;
  3. Map<char,Node> next;
  4. }

插入元素

二分搜索树 - 图4

从跟节点开始,将单词的每个字母逐一插入 Trie树。插入前先看字母对应的节点是否存在,存在则共享该节点,不存在则创建对应的节点。

  1. //添加一个单词word
  2. public void add(String word){
  3. Node cur = root;
  4. for(int i=0; i<word.length(); i++){//使用循环的方式添加映射
  5. char c =word.charAt(i);
  6. if(cur.next.get(c)==null){
  7. cur.next.put(c,new Node());
  8. }
  9. cur = cur.next.get(c);
  10. }
  11. if(!cur.isWord){//如果单词没有存在
  12. cur.isWord = true;
  13. size++;
  14. }
  15. }

查询操作

二分搜索树 - 图5

从根节点开始遍历,如果下一个节点不包含直接返回false,需要注意的是最后的返回值不是直接返回true而是cur.isWord,因为比如pan、panda但是只添加了panda如果返回值为true那么pan也会搜索成功。所以应该返回Node的isWord属性

  1. //查询单词word
  2. public boolean contains(String word){
  3. Node cur = root;
  4. for(int i=0; i<word.length(); i++){
  5. char c = word.charAt(i);
  6. if(cur.next.get(c)==null)
  7. return false;
  8. cur = cur.next.get(c);
  9. }
  10. return cur.isWord;//Important
  11. }

简单模式匹配

p.. = pan .可以代表任何一个字母

  1. //匹配单词word
  2. public boolean search(String word){
  3. return match(root,word,0);
  4. }
  5. /*三个参数节点|单词|单词指针*/
  6. private boolean match(Node node, String word, int index){
  7. if(index==word.length())
  8. return node.isWord;
  9. char c = word.charAt(index); //获取当前指针
  10. if(c!='.'){
  11. if(node.next.get(c) == null) //如果不是.看下一个是不是空 不是空接着下一个节点
  12. return false;
  13. return match(node.next.get(c),word,index+1);
  14. }else{
  15. for(char nextChar:node.next.keySet())
  16. if(match(node.next.get(nextChar),word,index+1))
  17. return true;
  18. return false;
  19. }
  20. }
  1. import java.util.TreeMap;
  2. public class Trie {
  3. private class Node{
  4. public boolean isWord; //是否找到了
  5. public TreeMap<Character,Node> next;
  6. public Node(boolean isWord){
  7. this.isWord = isWord;
  8. next = new TreeMap<>();
  9. }
  10. public Node(){
  11. this(false);
  12. }
  13. }
  14. private Node root;
  15. private int size;
  16. public Trie(){ //构造函数
  17. root = new Node();
  18. size=0;
  19. }
  20. public int getSize(){
  21. return this.size;
  22. }
  23. //添加一个单词word
  24. public void add(String word){
  25. Node cur = root;
  26. for(int i=0; i<word.length(); i++){//使用循环的方式添加映射
  27. char c =word.charAt(i);
  28. if(cur.next.get(c)==null){
  29. cur.next.put(c,new Node());
  30. }
  31. cur = cur.next.get(c);
  32. }
  33. if(!cur.isWord){//如果单词没有存在
  34. cur.isWord = true;
  35. size++;
  36. }
  37. }
  38. //查询单词word
  39. public boolean contains(String word){
  40. Node cur = root;
  41. for(int i=0; i<word.length(); i++){
  42. char c = word.charAt(i);
  43. if(cur.next.get(c)==null)
  44. return false;
  45. cur = cur.next.get(c);
  46. }
  47. return cur.isWord;//Important
  48. }
  49. //匹配单词word
  50. public boolean search(String word){
  51. return match(root,word,0);
  52. }
  53. /*三个参数节点|单词|单词指针*/
  54. private boolean match(Node node, String word, int index){
  55. if(index==word.length())
  56. return node.isWord;
  57. char c = word.charAt(index); //获取当前指针
  58. if(c!='.'){
  59. if(node.next.get(c) == null) //如果不是.看下一个是不是空 不是空接着下一个节点
  60. return false;
  61. return match(node.next.get(c),word,index+1);
  62. }else{
  63. for(char nextChar:node.next.keySet())
  64. if(match(node.next.get(nextChar),word,index+1))
  65. return true;
  66. return false;
  67. }
  68. }
  69. }

删除操作

二分搜索树 - 图6

应用

前缀匹配

image.png

字符串检索


给出 N 个单词组成的熟词表,以及一篇全用小写英文书写的文章,按最早出现的顺序写出所有不在熟词表中的生词。
检索/查询功能是Trie树最原始的功能。给定一组字符串,查找某个字符串是否出现过,思路就是从根节点开始一个一个字符进行比较:

  • 如果沿路比较,发现不同的字符,则表示该字符串在集合中不存在。
  • 如果所有的字符全部比较完并且全部相同,还需判断最后一个节点的标志位(标记该节点是否代表一个关键字)。