Adblocker

数据结构-核心篇之树 - 图3
数据结构-核心篇之树

数据结构-核心篇之树

一 树

树是由n(n>=1)个有限结点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
image.png
树具有以下特点:
1.每个结点有零个或多个子结点;
2.没有父结点的结点为根结点;
3.每一个非根结点只有一个父结点;
4.每个结点及其后代结点整体上可以看做是一棵树,称为当前结点的父结点的一个子树;

1.1 树的相关概念

结点的度:
一个结点含有的子树的个数称为该结点的度;
叶结点:
度为0的结点称为叶结点,也可以叫做终端结点
分支结点:
度不为0的结点称为分支结点,也可以叫做非终端结点
结点的层次:
从根结点开始,根结点的层次为1,根的直接后继层次为2,以此类推
结点的层序编号:
将树中的结点,按照从上层到下层,同层从左到右的次序排成一个线性序列,把他们编成连续的自然数。
树的度:
树中所有结点的度的最大值
树的高度(深度):
树中结点的最大层次
森林:
m(m>=0)个互不相交的树的集合,将一颗非空树的根结点删去,树就变成一个森林;给森林增加一个统一的根
结点,森林就变成一棵树
image.png
孩子结点:
一个结点的直接后继结点称为该结点的孩子结点
双亲结点(父结点):
一个结点的直接前驱称为该结点的双亲结点
兄弟结点:
同一双亲结点的孩子结点间互称兄弟结点

1.2 二叉树的种类

二叉树
就是度不超过2的树(每个结点最多有两个子结点)
image.png
满二叉树
如果每一个层的结点树都达到最大值,则这个二叉树就是满二叉树
image.png
完全二叉树
叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树
image.png

二 二叉搜索树

2.1 代码实现

  1. package com.ycc.data.structure.tree;
  2. import cn.itcast.algorithm.linear.Queue;
  3. /**
  4. * 二叉搜索树
  5. * 二叉树 拥有两个孩子 ,二叉搜索树 左孩子小于根节点,右孩子大于根节点
  6. *
  7. * @author liaozx
  8. * @description
  9. * @create 2020-11-27 10:29
  10. */
  11. public class BinarySearchTree<Key extends Comparable<Key>, Value> {
  12. private Node root;
  13. private int current;
  14. public class Node {
  15. //存储键
  16. public Key key;
  17. //存储值
  18. private Value value;
  19. Node left;//左孩子
  20. Node right;//右孩子
  21. public Node(Key key, Value value, Node left, Node right) {
  22. this.key = key;
  23. this.value = value;
  24. this.left = left;
  25. this.right = right;
  26. }
  27. }
  28. /**
  29. * 向树中添加元素key-value
  30. *
  31. * @param key
  32. * @param value
  33. */
  34. public void put(Key key, Value value) {
  35. root = put(root, key, value);
  36. }
  37. /**
  38. * 向指定的树node中添加key-value,并返回添加元素后新的树
  39. * 其逻辑是,新节点的key比较root的key,大于在右边,小于在左边,等于则替换
  40. *
  41. * @param node
  42. * @param key
  43. * @param value
  44. * @return
  45. */
  46. private Node put(Node node, Key key, Value value) {
  47. //如果node子树为空
  48. if (null == node) {
  49. current++;
  50. return new Node(key, value, null, null);
  51. }
  52. //如果node子树不为空,比较node结点的键和key的大小
  53. int cmp = key.compareTo(node.key);
  54. if (cmp > 0) {
  55. //如果key大于node结点的键,则继续找node结点的右子树
  56. node.right = put(node.right, key, value);
  57. } else if (cmp < 0) {
  58. //如果key小于node结点的键,则继续找node结点的左子树
  59. node.left = put(node.left, key, value);
  60. } else {
  61. //如果key等于node结点的键,则替换node结点的值为value即可
  62. node.value = value;
  63. }
  64. return node;
  65. }
  66. /**
  67. * 查询树中指定key对应的value
  68. *
  69. * @param key
  70. * @return
  71. */
  72. public Value get(Key key) {
  73. return get(root, key);
  74. }
  75. /**
  76. * 从指定的树node中,查找key对应的值
  77. *
  78. * @param node
  79. * @param key
  80. * @return
  81. */
  82. public Value get(Node node, Key key) {
  83. //node树为null
  84. if (node == null) {
  85. return null;
  86. }
  87. //node树不为null,比较key和node结点的键的大小
  88. int cmp = key.compareTo(node.key);
  89. if (cmp > 0) {
  90. //如果key大于node结点的键,则继续找node结点的右子树
  91. return get(node.right, key);
  92. } else if (cmp < 0) {
  93. //如果key小于node结点的键,则继续找node结点的左子树
  94. return get(node.left, key);
  95. } else {
  96. //如果key等于node结点的键,就找到了键为key的结点,只需要返回node结点的值即可
  97. return node.value;
  98. }
  99. }
  100. /**
  101. * 删除树中key对应的value
  102. *
  103. * @param key
  104. */
  105. public void delete(Key key) {
  106. delete(root, key);
  107. }
  108. //删除指定树node中的key对应的value,并返回删除后的新树
  109. public Node delete(Node node, Key key) {
  110. //node树为null
  111. if (node == null) {
  112. return null;
  113. }
  114. //node树不为null
  115. int cmp = key.compareTo(node.key);
  116. if (cmp > 0) {
  117. //如果key大于node结点的键,则继续找node结点的右子树
  118. node.right = delete(node.right, key);
  119. } else if (cmp < 0) {
  120. //如果key小于node结点的键,则继续找node结点的左子树
  121. node.left = delete(node.left, key);
  122. } else {
  123. //如果key等于node结点的键,完成真正的删除结点动作,要删除的结点就是node,让元素个数-1
  124. current--;
  125. //得找到右子树中最小的结点
  126. if (node.right == null) {
  127. return node.left;
  128. }
  129. if (node.left == null) {
  130. return node.right;
  131. }
  132. Node minNode = node.right;
  133. while (minNode.left != null) {
  134. minNode = minNode.left;
  135. }
  136. //删除右子树中最小的结点
  137. Node n = node.right;
  138. while (n.left != null) {
  139. if (n.left.left == null) {
  140. n.left = null;
  141. } else {
  142. //变换n结点即可
  143. n = n.left;
  144. }
  145. }
  146. //让node结点的左子树成为minNode的左子树
  147. minNode.left = node.left;
  148. //让node结点的右子树成为minNode的右子树
  149. minNode.right = node.right;
  150. //让node结点的父结点指向minNode
  151. node = minNode;
  152. }
  153. return node;
  154. }
  155. /**
  156. * 查找整个树中最小的键
  157. *
  158. * @return
  159. */
  160. public Key min() {
  161. return min(root).key;
  162. }
  163. /**
  164. * 在指定树node中找出最小键所在的结点
  165. *
  166. * @param node
  167. * @return
  168. */
  169. private Node min(Node node) {
  170. //需要判断node还有没有左子结点,如果有,则继续向左找,如果没有,则node就是最小键所在的结点
  171. if (node.left != null) {
  172. return min(node.left);
  173. } else {
  174. return node;
  175. }
  176. }
  177. /**
  178. * 在整个树中找到最大的键
  179. *
  180. * @return
  181. */
  182. public Key max() {
  183. return max(root).key;
  184. }
  185. /**
  186. * 在指定的树node中,找到最大的键所在的结点
  187. *
  188. * @param node
  189. * @return
  190. */
  191. public Node max(Node node) {
  192. //判断node还有没有右子结点,如果有,则继续向右查找,如果没有,则node就是最大键所在的结点
  193. if (node.right != null) {
  194. return max(node.right);
  195. } else {
  196. return node;
  197. }
  198. }
  199. /**
  200. * 前序遍历 根左右
  201. * 获取整个树中所有的键
  202. *
  203. * @return
  204. */
  205. public Queue<Key> preErgodic() {
  206. Queue<Key> keys = new Queue<>();
  207. preErgodic(root, keys);
  208. return keys;
  209. }
  210. /**
  211. * 前序遍历 根左右
  212. * 获取指定树node的所有键,并放到keys队列中
  213. *
  214. * @param node
  215. * @param keys
  216. */
  217. private void preErgodic(Node node, Queue<Key> keys) {
  218. if (node == null) {
  219. return;
  220. }
  221. //把node结点的key放入到keys中
  222. keys.enqueue(node.key);
  223. //递归遍历node结点的左子树
  224. if (node.left != null) {
  225. preErgodic(node.left, keys);
  226. }
  227. //递归遍历node结点的右子树
  228. if (node.right != null) {
  229. preErgodic(node.right, keys);
  230. }
  231. }
  232. /**
  233. * 中序遍历 左根右
  234. * 使用中序遍历获取树中所有的键
  235. *
  236. * @return
  237. */
  238. public Queue<Key> midErgodic() {
  239. Queue<Key> keys = new Queue<>();
  240. midErgodic(root, keys);
  241. return keys;
  242. }
  243. /**
  244. * 中序遍历 左根右
  245. * 使用中序遍历,获取指定树node中所有的键,并存放到key中
  246. *
  247. * @param node
  248. * @param keys
  249. */
  250. private void midErgodic(Node node, Queue<Key> keys) {
  251. if (node == null) {
  252. return;
  253. }
  254. //先递归,把左子树中的键放到keys中
  255. if (node.left != null) {
  256. midErgodic(node.left, keys);
  257. }
  258. //把当前结点node的键放到keys中
  259. keys.enqueue(node.key);
  260. //在递归,把右子树中的键放到keys中
  261. if (node.right != null) {
  262. midErgodic(node.right, keys);
  263. }
  264. }
  265. /**
  266. * 后序遍历 左右根
  267. * 使用后序遍历,把整个树中所有的键返回
  268. *
  269. * @return
  270. */
  271. public Queue<Key> afterErgodic() {
  272. Queue<Key> keys = new Queue<>();
  273. afterErgodic(root, keys);
  274. return keys;
  275. }
  276. /**
  277. * 后序遍历 左右根
  278. * 使用后序遍历,把指定树node中所有的键放入到keys中
  279. *
  280. * @param node
  281. * @param keys
  282. */
  283. private void afterErgodic(Node node, Queue<Key> keys) {
  284. if (node == null) {
  285. return;
  286. }
  287. //通过递归把左子树中所有的键放入到keys中
  288. if (node.left != null) {
  289. afterErgodic(node.left, keys);
  290. }
  291. //通过递归把右子树中所有的键放入到keys中
  292. if (node.right != null) {
  293. afterErgodic(node.right, keys);
  294. }
  295. //把node结点的键放入到keys中
  296. keys.enqueue(node.key);
  297. }
  298. /**
  299. * 使用层序遍历,获取整个树中所有的键
  300. *
  301. * @return
  302. */
  303. public Queue<Key> layerErgodic() {
  304. //定义两个队列,分别存储树中的键和树中的结点
  305. Queue<Key> keys = new Queue<>();
  306. Queue<Node> nodes = new Queue<>();
  307. //默认,往队列中放入根结点
  308. nodes.enqueue(root);
  309. while (!nodes.isEmpty()) {
  310. //从队列中弹出一个结点,把key放入到keys中
  311. Node n = nodes.dequeue();
  312. keys.enqueue(n.key);
  313. //判断当前结点还有没有左子结点,如果有,则放入到nodes中
  314. if (n.left != null) {
  315. nodes.enqueue(n.left);
  316. }
  317. //判断当前结点还有没有右子结点,如果有,则放入到nodes中
  318. if (n.right != null) {
  319. nodes.enqueue(n.right);
  320. }
  321. }
  322. return keys;
  323. }
  324. //获取整个树的最大深度
  325. public int maxDepth() {
  326. return maxDepth(root);
  327. }
  328. //获取指定树node的最大深度
  329. private int maxDepth(Node node) {
  330. if (node == null) {
  331. return 0;
  332. }
  333. //node的最大深度
  334. int max = 0;
  335. //左子树的最大深度
  336. int maxL = 0;
  337. //右子树的最大深度
  338. int maxR = 0;
  339. //计算node结点左子树的最大深度
  340. if (node.left != null) {
  341. maxL = maxDepth(node.left);
  342. }
  343. //计算node结点右子树的最大深度
  344. if (node.right != null) {
  345. maxR = maxDepth(node.right);
  346. }
  347. //比较左子树最大深度和右子树最大深度,取较大值+1即可
  348. max = maxL > maxR ? maxL + 1 : maxR + 1;
  349. return max;
  350. }
  351. //获取树中元素的个数
  352. public int size() {
  353. return current;
  354. }
  355. }

2.2 折纸代码实现

  1. package com.ycc.data.structure.tree.test;
  2. import com.ycc.data.structure.line.MyLinedQueue;
  3. /**
  4. * @author liaozx
  5. * @date 2020-11-27
  6. */
  7. public class PaperFolding {
  8. public static void main(String[] args) {
  9. //构建折痕树
  10. Node tree = createTree(3);
  11. //遍历折痕树,并打印
  12. printTree(tree);
  13. }
  14. //3.使用中序遍历,打印出树中所有结点的内容;
  15. private static void printTree(Node tree) {
  16. if (tree == null) {
  17. return;
  18. }
  19. printTree(tree.left);
  20. System.out.print(tree.item + ",");
  21. printTree(tree.right);
  22. }
  23. //2.构建深度为N的折痕树;
  24. private static Node createTree(int N) {
  25. Node root = null;
  26. for (int i = 0; i < N; i++) {
  27. if (i == 0) {
  28. //1.第一次对折,只有一条折痕,创建根结点;
  29. root = new Node("down", null, null);
  30. } else {
  31. //2.如果不是第一次对折,则使用队列保存根结点;
  32. MyLinedQueue<Node> queue = new MyLinedQueue<>();
  33. queue.enQueue(root);
  34. //3.循环遍历队列:
  35. while (!queue.isEmpty()) {
  36. //3.1从队列中拿出一个结点;
  37. Node tmp = queue.deQueue();
  38. //3.2如果这个结点的左子结点不为空,则把这个左子结点添加到队列中;
  39. if (tmp.left != null) {
  40. queue.enQueue(tmp.left);
  41. }
  42. //3.3如果这个结点的右子结点不为空,则把这个右子结点添加到队列中;
  43. if (tmp.right != null) {
  44. queue.enQueue(tmp.right);
  45. }
  46. //3.4判断当前结点的左子结点和右子结点都不为空,如果是,则需要为当前结点创建一个 值为down的左子结点,一个值为up的右子结点。
  47. if (tmp.left == null && tmp.right == null) {
  48. tmp.left = new Node("down", null, null);
  49. tmp.right = new Node("up", null, null);
  50. }
  51. }
  52. }
  53. }
  54. return root;
  55. }
  56. //1.定义结点类
  57. private static class Node {
  58. //存储结点元素
  59. String item;
  60. //左子结点
  61. Node left;
  62. //右子结点
  63. Node right;
  64. public Node(String item, Node left, Node right) {
  65. this.item = item;
  66. this.left = left;
  67. this.right = right;
  68. }
  69. }
  70. }

三 堆

堆是计算机科学中一类特殊的数据结构的统称,堆通常可以被看做是一棵完全二叉树的数组对象。它对应的数组是按层存储,如图
image.png
特点:
父节点索引:i,左孩子索引:2i ,右孩子索引:2i+1,即8号球(索引:4)、7号球(索引:5)、14号球(索引:2)。
原理是他是完全二叉树。
image.png
大顶堆:父节点大于子节点,二叉树的根(堆顶)最大。
小顶堆:父节点小于子节点,二叉树的根(堆顶)最小。

3.1 大顶堆

3.1.1 实现原理

主要实现功能有:

  1. 新增元素:insert
  2. 删除最大元素:delMax

要实现上面两个功能需要理解两个堆化步骤:
上浮:
下面我们在该堆中插入一个新的元素26:
image.png
比较父节点的新节点的大小,如果新元素更大则交换两个元素的位置,这个操作就相当于把该元素上浮了一下
image.png
递归该操作直到26到了一个满足堆条件的位置,此时就完成了插入的操作:
image.png
下沉:
取出堆中的最大元素,最后一个元素替换掉栈顶元素,然后把最后一个元素删除掉.取出62元素。
image.png
用最后元素替换掉栈顶元素,然后删除最后一个元素:
image.png
然后比较其孩子结点的大小:
image.png
如果不满足堆的条件,那么就跟孩子结点中较大的一个交换位置:
image.png

递归该步骤,直到16到达合适的位置:
image.png

3.1.2代码实现

大顶堆的实现,

  1. package com.ycc.data.structure.heap;
  2. /**
  3. * 大顶堆
  4. *
  5. * @author liaozx
  6. * @date 2020-11-27
  7. */
  8. public class MaxHeap<T extends Comparable<T>> {
  9. //存储堆中的元素
  10. private final T[] items;
  11. //记录堆中元素的个数
  12. private int current;
  13. public MaxHeap(int capacity) {
  14. this.items = (T[]) new Comparable[capacity + 1];
  15. this.current = 0;
  16. }
  17. /**
  18. * 判断堆中索引i处的元素是否小于索引j处的元素
  19. * i<j
  20. *
  21. * @param i
  22. * @param j
  23. * @return
  24. */
  25. private boolean less(int i, int j) {
  26. return items[i].compareTo(items[j]) < 0;
  27. }
  28. /**
  29. * 交换堆中i索引和j索引处的值
  30. *
  31. * @param i
  32. * @param j
  33. */
  34. private void swap(int i, int j) {
  35. T temp = items[i];
  36. items[i] = items[j];
  37. items[j] = temp;
  38. }
  39. /**
  40. * 往堆中插入一个元素
  41. *
  42. * @param t
  43. */
  44. public void insert(T t) {
  45. items[++current] = t;
  46. siftUp(current);
  47. }
  48. /**
  49. * 使用上浮算法,使索引k处的元素能在堆中处于一个正确的位置
  50. *
  51. * @param k
  52. */
  53. private void siftUp(int k) {
  54. //通过循环,不断的比较当前结点的值和其父结点的值,如果发现父结点的值比当前结点的值小,则交换位置
  55. while (k > 1) {
  56. //比较当前结点和其父结点
  57. if (less(k / 2, k)) {
  58. swap(k / 2, k);
  59. }
  60. //上级节点
  61. k = k / 2;
  62. }
  63. }
  64. /**
  65. * 删除堆中最大的元素,并返回这个最大元素
  66. *
  67. * @return
  68. */
  69. public T delMax() {
  70. T max = items[1];
  71. //交换索引1处的元素和最大索引处的元素,让完全二叉树中最右侧的元素变为临时根结点
  72. swap(1, current);
  73. //最大索引处的元素删除掉
  74. items[current] = null;
  75. //元素个数-1
  76. current--;
  77. //通过下沉调整堆,让堆重新有序
  78. siftDown(1);
  79. return max;
  80. }
  81. /**
  82. * 使用下沉算法,使索引k处的元素能在堆中处于一个正确的位置
  83. *
  84. * @param k
  85. */
  86. private void siftDown(int k) {
  87. //通过循环不断的对比当前k结点和其左子结点2*k以及右子结点2k+1处中的较大值的元素大小,如果当前结点小,则需要交换位置
  88. while (2 * k <= current) {
  89. //获取当前结点的子结点中的较大结点
  90. int max;//记录较大结点所在的索引
  91. if (2 * k + 1 <= current) {
  92. if (less(2 * k, 2 * k + 1)) {
  93. max = 2 * k + 1;
  94. } else {
  95. max = 2 * k;
  96. }
  97. } else {
  98. max = 2 * k;
  99. }
  100. //比较当前结点和较大结点的值
  101. if (!less(k, max)) {
  102. break;
  103. }
  104. //交换k索引处的值和max索引处的值
  105. swap(k, max);
  106. //变换k的值
  107. k = max;
  108. }
  109. }
  110. }

测试代码

  1. package com.ycc.data.structure.heap.test;
  2. import com.ycc.data.structure.heap.MaxHeap;
  3. /**
  4. * @author liaozx
  5. * @date 2020-11-27
  6. */
  7. public class MaxHeapTest {
  8. public static void main(String[] args) {
  9. MaxHeap<String> heap = new MaxHeap<String>(20);
  10. heap.insert("1");
  11. heap.insert("2");
  12. heap.insert("3");
  13. heap.insert("4");
  14. heap.insert("5");
  15. heap.insert("6");
  16. heap.insert("7");
  17. String del;
  18. while ((del = heap.delMax()) != null) {
  19. System.out.print(del + ",");
  20. }
  21. }
  22. }

3.2 小顶堆

3.2.1 实现原理

主要实现功能有:

  1. 新增元素:insert
  2. 删除最小元素:delMin

要实现上面两个功能需要理解两个堆化步骤:
上浮:
参考大顶堆
下沉:
参考大顶堆

3.2.2 代码实现

  1. package com.ycc.data.structure.heap;
  2. /**
  3. * 小顶堆
  4. *
  5. * @author liaozx
  6. * @date 2020-11-27
  7. */
  8. public class MinHeap<T extends Comparable<T>> {
  9. //存储堆中的元素
  10. private final T[] items;
  11. //记录堆中元素的个数
  12. private int current;
  13. /**
  14. * 从1开始
  15. *
  16. * @param capacity
  17. */
  18. public MinHeap(int capacity) {
  19. this.items = (T[]) new Comparable[capacity + 1];
  20. this.current = 0;
  21. }
  22. //判断堆中索引i处的元素是否小于索引j处的元素
  23. private boolean less(int i, int j) {
  24. return items[i].compareTo(items[j]) < 0;
  25. }
  26. //交换堆中i索引和j索引处的值
  27. private void swap(int i, int j) {
  28. T tmp = items[i];
  29. items[i] = items[j];
  30. items[j] = tmp;
  31. }
  32. //往堆中插入一个元素
  33. public void insert(T t) {
  34. items[++current] = t;
  35. siftUp(current);
  36. }
  37. //使用上浮算法,使索引k处的元素能在堆中处于一个正确的位置
  38. private void siftUp(int k) {
  39. //通过循环比较当前结点和其父结点的大小
  40. while (k > 1) {
  41. if (less(k, k / 2)) {
  42. swap(k, k / 2);
  43. }
  44. k = k / 2;
  45. }
  46. }
  47. //删除堆中最小的元素,并返回这个最小元素
  48. public T delMin() {
  49. T min = items[1];
  50. swap(1, current);
  51. //最小索引处的元素删除掉
  52. items[current] = null;
  53. current--;
  54. siftDown(1);
  55. return min;
  56. }
  57. //使用下沉算法,使索引k处的元素能在堆中处于一个正确的位置
  58. private void siftDown(int k) {
  59. //通过循环比较当前结点和其子结点中的较小值
  60. while (2 * k <= current) {
  61. //1.找到子结点中的较小值
  62. int min;
  63. if (2 * k + 1 <= current) {
  64. if (less(2 * k, 2 * k + 1)) {
  65. min = 2 * k;
  66. } else {
  67. min = 2 * k + 1;
  68. }
  69. } else {
  70. min = 2 * k;
  71. }
  72. //2.判断当前结点和较小值的大小
  73. if (less(k, min)) {
  74. break;
  75. }
  76. swap(k, min);
  77. k = min;
  78. }
  79. }
  80. }

测试代码

  1. package com.ycc.data.structure.heap.test;
  2. import com.ycc.data.structure.heap.MinHeap;
  3. /**
  4. * @author liaozx
  5. * @date 2020-11-27
  6. */
  7. public class MinHeapTest {
  8. public static void main(String[] args) {
  9. MinHeap<String> heap = new MinHeap<String>(20);
  10. heap.insert("7");
  11. heap.insert("4");
  12. heap.insert("2");
  13. heap.insert("1");
  14. heap.insert("3");
  15. heap.insert("5");
  16. heap.insert("6");
  17. String del;
  18. while ((del = heap.delMin()) != null) {
  19. System.out.print(del + ",");
  20. }
  21. }
  22. }

四 优先队列

优先队列也是一种队列,只不过不同的是,优先队列的出队顺序是按照优先级来的,底层数据结构就是基于二叉堆的。

4.1 最大优先队列

可以获取并删除队列中最大的值

  1. package com.ycc.data.structure.heap;
  2. /**
  3. * 最大优先队列
  4. *
  5. * @author liaozx
  6. * @date 2020-11-27
  7. */
  8. public class MaxPriorityQueue<E extends Comparable<E>> {
  9. private final MaxHeap<E> maxHeap;
  10. public MaxPriorityQueue() {
  11. maxHeap = new MaxHeap<E>(20);
  12. }
  13. public void enqueue(E e) {
  14. maxHeap.insert(e);
  15. }
  16. public E dequeue() {
  17. return maxHeap.delMax();
  18. }
  19. }

4.2 最小优先队列

可以获取并删除队列中最小的值

  1. package com.ycc.data.structure.heap;
  2. /**
  3. * 最小优先队列
  4. *
  5. * @author liaozx
  6. * @date 2020-11-27
  7. */
  8. public class MinPriorityQueue<E extends Comparable<E>> {
  9. private final MinHeap<E> minHeap;
  10. public MinPriorityQueue() {
  11. minHeap = new MinHeap<E>(20);
  12. }
  13. public void enqueue(E e) {
  14. minHeap.insert(e);
  15. }
  16. public E dequeue() {
  17. return minHeap.delMin();
  18. }
  19. }

4.3 索引优先队列

最大优先队列和最小优先队列,他们可以分别快速访问到队列中最大元素和最小元素,但是他们有一个缺点,就是没有办法通过索引访问已存在于优先队列中的对象。但是我们可以给元素关联一个key,然后通过这个key查询这个value。

4.3.1 最小索引优先队列原理

  1. 索引关联 | 元数据
    T[]items | 数组的索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 将来需堆化存储 | | :—-: | :—-: | :—-: | :—-: | :—-: | :—-: | :—-: | :—-: | :—-: | :—-: | :—-: | :—-: | :—-: | :—-: | | | value | S | O | R | T | E | X | A | M | P | L | E | 真实的数据无序的 | | 因为上面的数据,不能堆化,如果堆化,则会上浮或者下沉操作,即数据与索引的对应关系发生change,因此我们要堆化索引即可,如下图所示(最小的数据的索引在最上面): | | | | | | | | | | | | | | | tt.png
    将上面的索引保存到下面的数组中
    ⏬ | | | | | | | | | | | | | | | pq存数据索引 | 数组的索引 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 堆从1开始,不是0 | | | 存储items索引 | 6 | 10 | 4 | 9 | 7 | 1 | 8 | 2 | 0 | 3 | 5 | 堆化后的索引 | | 如果我们查询S元素,即遍历pq,判断value=0,即获取了s元素,但是这个效率很低,需要我们去遍历。
    因此我们还有一个辅助数组,存pq的索引。 | | | | | | | | | | | | | | | qp存pq的索引 | 索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | items的索引 | | | 存pq的索引 | 9 | 6 | 8 | 10 | 3 | 11 | 1 | 5 | 7 | 4 | 2 | 存储pq的索引 | | 既然qp数组存储了pq的索引,那么我们很容易定位元素。比如我们需要更新s的值,那么元素S对应的索引是0,我们根据0从qp中获取pq的索引,即9。 | | | | | | | | | | | | | |

4.3.1 最小索引优先队列代码实现

  1. package com.ycc.data.structure.heap;
  2. public class IndexMinPriorityQueue<T extends Comparable<T>> {
  3. /**
  4. * 普通数组 存储堆中的元素
  5. */
  6. private final T[] items;
  7. /**
  8. * 二叉堆,保存每个元素在items数组中的索引,pq数组需要堆有序
  9. */
  10. private final int[] pq;
  11. /**
  12. * 普通数组 保存qp的逆序,pq的值作为索引,pq的索引作为值
  13. */
  14. private final int[] qp;
  15. //记录堆中元素的个数
  16. private int current;
  17. public IndexMinPriorityQueue(int capacity) {
  18. this.items = (T[]) new Comparable[capacity + 1];
  19. this.pq = new int[capacity + 1];
  20. this.qp = new int[capacity + 1];
  21. this.current = 0;
  22. //默认情况下,队列中没有存储任何数据,让qp中的元素都为-1;
  23. for (int i = 0; i < qp.length; i++) {
  24. qp[i] = -1;
  25. }
  26. }
  27. //判断堆中索引i处的元素是否小于索引j处的元素
  28. private boolean less(int i, int j) {
  29. return items[pq[i]].compareTo(items[pq[j]]) < 0;
  30. }
  31. //交换堆中i索引和j索引处的值
  32. private void swap(int i, int j) {
  33. //交换pq中的数据
  34. int tmp = pq[i];
  35. pq[i] = pq[j];
  36. pq[j] = tmp;
  37. //更新qp中的数据,pq[i]即为items元素的索引,即qp[元素索引]存储pq的索引
  38. qp[pq[i]] = i;
  39. qp[pq[j]] = j;
  40. }
  41. //判断k对应的元素是否存在
  42. public boolean contains(int k) {
  43. return qp[k] != -1;
  44. }
  45. //往队列中插入一个元素,并关联索引i
  46. public void insert(int i, T t) {
  47. //判断i是否已经被关联,如果已经被关联,则不让插入
  48. if (contains(i)) {
  49. return;
  50. }
  51. //元素个数+1
  52. current++;
  53. //把数据存储到items对应的i位置处
  54. items[i] = t;
  55. //把i对应的索引,存储到pq中,即在末尾存储元数据的索引
  56. pq[current] = i;
  57. //通过qp来记录,pq存储元数据(元数据对应的索引)的索引
  58. qp[i] = current;
  59. //通过堆上浮完成堆的调整
  60. siftUp(current);
  61. }
  62. //删除队列中最小的元素,并返回该元素关联的索引
  63. public int delMin() {
  64. //获取最小元素关联的索引
  65. int minIndex = pq[1];
  66. //交换pq中索引1处和最大索引处的元素
  67. swap(1, current);
  68. //删除qp中对应的内容
  69. qp[pq[current]] = -1;
  70. //删除pq最大索引处的内容
  71. pq[current] = -1;
  72. //删除items中对应的内容
  73. items[minIndex] = null;
  74. //元素个数-1
  75. current--;
  76. //下沉调整
  77. siftDown(1);
  78. return minIndex;
  79. }
  80. //删除索引i关联的元素
  81. public void delete(int i) {
  82. //找到i在pq中的索引
  83. int k = qp[i];
  84. //交换pq中索引k处的值和索引N处的值
  85. swap(k, current);
  86. //删除qp中的内容
  87. qp[pq[current]] = -1;
  88. //删除pq中的内容
  89. pq[current] = -1;
  90. //删除items中的内容
  91. items[k] = null;
  92. //元素的数量-1
  93. current--;
  94. //堆的调整
  95. siftDown(k);
  96. siftUp(k);
  97. }
  98. //把与索引i关联的元素修改为为t
  99. public void changeItem(int i, T t) {
  100. //修改items数组中i位置的元素为t
  101. items[i] = t;
  102. //找到i在pq中出现的位置
  103. int k = qp[i];
  104. //堆调整
  105. siftDown(k);
  106. siftUp(k);
  107. }
  108. /**
  109. * 使用上浮算法,使索引k处的元素能在堆中处于一个正确的位置
  110. *
  111. * @param k
  112. */
  113. private void siftUp(int k) {
  114. while (k > 1) {
  115. if (less(k, k / 2)) {
  116. swap(k, k / 2);
  117. }
  118. k = k / 2;
  119. }
  120. }
  121. //使用下沉算法,使索引k处的元素能在堆中处于一个正确的位置
  122. private void siftDown(int k) {
  123. while (2 * k <= current) {
  124. //找到子结点中的较小值
  125. int min;
  126. if (2 * k + 1 <= current) {
  127. if (less(2 * k, 2 * k + 1)) {
  128. min = 2 * k;
  129. } else {
  130. min = 2 * k + 1;
  131. }
  132. } else {
  133. min = 2 * k;
  134. }
  135. //比较当前结点和较小值
  136. if (less(k, min)) {
  137. break;
  138. }
  139. swap(k, min);
  140. k = min;
  141. }
  142. }
  143. }

五 AVL树

AVL树是高度平衡的而二叉树。它的特点是:AVL树中任何节点的两个子树的高度最大差别为1。
image.png
右图:7的左子树(即2为根的树)高度为:3 ,右子树(即8为根的树)的高度为:1,则高度差2>1即不是平衡树。

5.1 实现原理

5.1.1 失衡的情况分析

image.png
image.png
总的来说,AVL树失去平衡时的情况一定是LL、LR、RL、RR这4种之一,它们都由各自的定义:

LL:LeftLeft,也称为”左左”。插入或删除一个节点后,根节点的左子树的左子树还有非空子节点,导致”根的左子树的高度”比”根的右子树的高度”大2,导致AVL树失去了平衡。
例如,在上面LL情况中,由于”根节点(8)的左子树(4)的左子树(2)还有非空子节点”,而”根节点(8)的右子树(12)没有子节点”;导致”根节点(8)的左子树(4)高度”比”根节点(8)的右子树(12)”高2。

LR:LeftRight,也称为”左右”。插入或删除一个节点后,根节点的左子树的右子树还有非空子节点,导致”根的左子树的高度”比”根的右子树的高度”大2,导致AVL树失去了平衡。
例如,在上面LR情况中,由于”根节点(8)的左子树(4)的左子树(6)还有非空子节点”,而”根节点(8)的右子树(12)没有子节点”;导致”根节点(8)的左子树(4)高度”比”根节点(8)的右子树(12)”高2。

RL:RightLeft,称为”右左”。插入或删除一个节点后,根节点的右子树的左子树还有非空子节点,导致”根的右子树的高度”比”根的左子树的高度”大2,导致AVL树失去了平衡。
例如,在上面RL情况中,由于”根节点(8)的右子树(12)的左子树(10)还有非空子节点”,而”根节点(8)的左子树(4)没有子节点”;导致”根节点(8)的右子树(12)高度”比”根节点(8)的左子树(4)”高2。

RR:RightRight,称为”右右”。插入或删除一个节点后,根节点的右子树的右子树还有非空子节点,导致”根的右子树的高度”比”根的左子树的高度”大2,导致AVL树失去了平衡。
例如,在上面RR情况中,由于”根节点(8)的右子树(12)的右子树(14)还有非空子节点”,而”根节点(8)的左子树(4)没有子节点”;导致”根节点(8)的右子树(12)高度”比”根节点(8)的左子树(4)”高2。
_
如果在AVL树中进行插入或删除节点后,可能导致AVL树失去平衡。AVL失去平衡之后,可以通过旋转使其恢复平衡,下面分别介绍”LL(左左),LR(左右),RR(右右)和RL(右左)”这4种情况对应的旋转方法。

5.1.2 选择平衡分析

LL的旋转:
LL失去平衡的情况,可以通过一次旋转让AVL树恢复平衡。如下图:
image.png
RR的旋转:
理解了LL之后,RR就相当容易理解了。RR是与LL对称的情况!RR恢复平衡的旋转方法如下:
image.png
LR的旋转:
LR失去平衡的情况,需要经过两次旋转才能让AVL树恢复平衡。如下图:
image.png
RL的旋转:
RL是与LR的对称情况!RL恢复平衡的旋转方法如下:
image.png

5.2 代码实现

  1. package com.ycc.data.structure.tree;
  2. /**
  3. * https://www.cnblogs.com/skywang12345/p/3577479.html
  4. *
  5. * @author liaozx
  6. * @description 平衡二叉树
  7. * @create 2020-11-27 11:02
  8. */
  9. public class AVLTree<T extends Comparable<T>> {
  10. private Node<T> root; // 根结点
  11. // AVL树的节点(内部类)
  12. private class Node<T extends Comparable<T>> {
  13. // 关键字(键值)
  14. private T key;
  15. // 高度
  16. private int height;
  17. // 左孩子
  18. private Node<T> left;
  19. // 右孩子
  20. private Node<T> right;
  21. public Node(T key) {
  22. this.key = key;
  23. }
  24. }
  25. // 构造函数
  26. public AVLTree() {
  27. root = null;
  28. }
  29. /*
  30. * 获取树的高度
  31. */
  32. private int height(Node<T> tree) {
  33. if (tree != null)
  34. return tree.height;
  35. return 0;
  36. }
  37. public int height() {
  38. return height(root);
  39. }
  40. /*
  41. * 比较两个值的大小
  42. */
  43. private int max(int a, int b) {
  44. return a > b ? a : b;
  45. }
  46. public Node<T> search(T key) {
  47. return search(root, key);
  48. }
  49. /*
  50. * (递归实现)查找"AVL树x"中键值为key的节点
  51. */
  52. private Node<T> search(Node<T> x, T key) {
  53. if (x == null)
  54. return x;
  55. int cmp = key.compareTo(x.key);
  56. if (cmp < 0)
  57. return search(x.left, key);
  58. else if (cmp > 0)
  59. return search(x.right, key);
  60. else
  61. return x;
  62. }
  63. public Node<T> iterativeSearch(T key) {
  64. return iterativeSearch(root, key);
  65. }
  66. /*
  67. * (非递归实现)查找"AVL树x"中键值为key的节点
  68. */
  69. private Node<T> iterativeSearch(Node<T> x, T key) {
  70. while (x != null) {
  71. int cmp = key.compareTo(x.key);
  72. if (cmp < 0)
  73. x = x.left;
  74. else if (cmp > 0)
  75. x = x.right;
  76. else
  77. return x;
  78. }
  79. return x;
  80. }
  81. /*
  82. * 查找最小结点:返回tree为根结点的AVL树的最小结点。
  83. */
  84. private Node<T> minimum(Node<T> tree) {
  85. if (tree == null)
  86. return null;
  87. while (tree.left != null)
  88. tree = tree.left;
  89. return tree;
  90. }
  91. public T minimum() {
  92. Node<T> p = minimum(root);
  93. if (p != null)
  94. return p.key;
  95. return null;
  96. }
  97. /*
  98. * 查找最大结点:返回tree为根结点的AVL树的最大结点。
  99. */
  100. private Node<T> maximum(Node<T> tree) {
  101. if (tree == null)
  102. return null;
  103. while (tree.right != null)
  104. tree = tree.right;
  105. return tree;
  106. }
  107. public T maximum() {
  108. Node<T> p = maximum(root);
  109. if (p != null)
  110. return p.key;
  111. return null;
  112. }
  113. /*
  114. * LL:左左对应的情况(右单旋转)。
  115. *
  116. * 10
  117. * * *
  118. * * *
  119. * 6 15
  120. * *
  121. * *
  122. * 4
  123. * *
  124. * *
  125. * 2
  126. *
  127. *
  128. * 10
  129. * * *
  130. * 4 15
  131. * * *
  132. * 2 6
  133. * *
  134. * 1
  135. *
  136. *
  137. * 返回值:旋转后的根节点
  138. */
  139. private Node<T> lLRotation(Node<T> k2) {
  140. //获取左节点,然后k1即将升为根节点
  141. Node<T> k1 = k2.left;
  142. //k1的右节点,变成k2的左子节点
  143. k2.left = k1.right;
  144. //k1升级为根节点,即上级节点
  145. k1.right = k2;
  146. //k2的树高度
  147. k2.height = max(height(k2.left), height(k2.right)) + 1;
  148. //k1的树高度
  149. k1.height = max(height(k1.left), k2.height) + 1;
  150. return k1;
  151. }
  152. /*
  153. * RR:右右对应的情况(左单旋转)。
  154. *
  155. * 返回值:旋转后的根节点
  156. */
  157. private Node<T> rRRotation(Node<T> k1) {
  158. //获取右节点,然后k2即将升为根节点
  159. Node<T> k2 = k1.right;
  160. //k1即将变成左子节点,且k1的有孩子,即k2的左孩子
  161. k1.right = k2.left;
  162. //k1 变成更节点
  163. k2.left = k1;
  164. k1.height = max(height(k1.left), height(k1.right)) + 1;
  165. k2.height = max(height(k2.right), k1.height) + 1;
  166. return k2;
  167. }
  168. /*
  169. * LR:左右对应的情况(左双旋转)。
  170. *
  171. *
  172. * 10
  173. * *
  174. * *
  175. * 6
  176. * *
  177. * *
  178. * 8
  179. * 返回值:旋转后的根节点
  180. *
  181. * 左右先掰直,即先左旋变成LL,然后在在右旋
  182. */
  183. private Node<T> leftRightRotation(Node<T> k3) {
  184. k3.left = rRRotation(k3.left); //先左旋,掰直成左斜杠
  185. return lLRotation(k3);//再右旋,掰弯成倒三角平衡了
  186. }
  187. /*
  188. * RL:右左对应的情况(右双旋转)。
  189. *
  190. * 返回值:旋转后的根节点
  191. */
  192. private Node<T> rightLeftRotation(Node<T> k1) {
  193. k1.right = lLRotation(k1.right);//先右旋,掰直成右斜杠
  194. return rRRotation(k1);//再左旋,掰弯成倒三角平衡了
  195. }
  196. public void insert(T key) {
  197. root = insert(root, key);
  198. }
  199. /*
  200. * 将结点插入到AVL树中,并返回根节点
  201. *
  202. * 参数说明:
  203. * tree AVL树的根结点
  204. * key 插入的结点的键值
  205. * 返回值:
  206. * 根节点
  207. *
  208. * 16
  209. * * *
  210. * * *
  211. * 10 20
  212. * * *
  213. * * *
  214. * 6 14
  215. * *
  216. * *
  217. * 4
  218. */
  219. private Node<T> insert(Node<T> tree, T key) {
  220. if (tree == null) {
  221. // 新建节点
  222. tree = new Node<T>(key);
  223. } else {
  224. int cmp = key.compareTo(tree.key);
  225. if (cmp < 0) {
  226. // 应该将key插入到"tree的左子树"的情况
  227. tree.left = insert(tree.left, key);
  228. // 插入节点后,若AVL树失去平衡,则进行相应的调节。
  229. if (height(tree.left) - height(tree.right) == 2) {
  230. if (key.compareTo(tree.left.key) < 0) // 比左孩子小(如上图 4<6) 则是左左,即右旋
  231. tree = lLRotation(tree);//右旋
  232. else //左右 即8>6
  233. tree = leftRightRotation(tree);//左双旋
  234. }
  235. } else if (cmp > 0) { // 应该将key插入到"tree的右子树"的情况
  236. tree.right = insert(tree.right, key);
  237. // 插入节点后,若AVL树失去平衡,则进行相应的调节。
  238. if (height(tree.right) - height(tree.left) == 2) {
  239. if (key.compareTo(tree.right.key) > 0) //比右边孩子大 则是右右,即左旋
  240. tree = rRRotation(tree);//左旋
  241. else
  242. tree = rightLeftRotation(tree);//右双旋
  243. }
  244. } else { // cmp==0
  245. System.out.println("添加失败:不允许添加相同的节点!");
  246. }
  247. }
  248. tree.height = max(height(tree.left), height(tree.right)) + 1;
  249. return tree;
  250. }
  251. public void remove(T key) {
  252. Node<T> z;
  253. if ((z = search(root, key)) != null)
  254. root = remove(root, z);
  255. }
  256. /*
  257. * 删除结点(z),返回根节点
  258. *
  259. * 参数说明:
  260. * tree AVL树的根结点
  261. * z 待删除的结点
  262. * 返回值:
  263. * 根节点
  264. */
  265. private Node<T> remove(Node<T> tree, Node<T> z) {
  266. // 根为空 或者 没有要删除的节点,直接返回null。
  267. if (tree == null || z == null)
  268. return null;
  269. int cmp = z.key.compareTo(tree.key);
  270. if (cmp < 0) {
  271. // 待删除的节点在"tree的左子树"中
  272. tree.left = remove(tree.left, z);
  273. // 删除节点后,若AVL树失去平衡,则进行相应的调节。
  274. if (height(tree.right) - height(tree.left) == 2) {
  275. Node<T> r = tree.right;
  276. if (height(r.left) > height(r.right))
  277. tree = rightLeftRotation(tree);
  278. else
  279. tree = rRRotation(tree);
  280. }
  281. } else if (cmp > 0) {
  282. // 待删除的节点在"tree的右子树"中
  283. tree.right = remove(tree.right, z);
  284. // 删除节点后,若AVL树失去平衡,则进行相应的调节。
  285. if (height(tree.left) - height(tree.right) == 2) {
  286. Node<T> l = tree.left;
  287. if (height(l.right) > height(l.left))
  288. tree = leftRightRotation(tree);
  289. else
  290. tree = lLRotation(tree);
  291. }
  292. } else { // tree是对应要删除的节点。
  293. // tree的左右孩子都非空
  294. if ((tree.left != null) && (tree.right != null)) {
  295. if (height(tree.left) > height(tree.right)) {
  296. // 如果tree的左子树比右子树高;
  297. // 则(01)找出tree的左子树中的最大节点
  298. // (02)将该最大节点的值赋值给tree。
  299. // (03)删除该最大节点。
  300. // 这类似于用"tree的左子树中最大节点"做"tree"的替身;
  301. // 采用这种方式的好处是:删除"tree的左子树中最大节点"之后,AVL树仍然是平衡的。
  302. Node<T> max = maximum(tree.left);
  303. tree.key = max.key;
  304. tree.left = remove(tree.left, max);
  305. } else {
  306. // 如果tree的左子树不比右子树高(即它们相等,或右子树比左子树高1)
  307. // 则(01)找出tree的右子树中的最小节点
  308. // (02)将该最小节点的值赋值给tree。
  309. // (03)删除该最小节点。
  310. // 这类似于用"tree的右子树中最小节点"做"tree"的替身;
  311. // 采用这种方式的好处是:删除"tree的右子树中最小节点"之后,AVL树仍然是平衡的。
  312. Node<T> min = maximum(tree.right);
  313. tree.key = min.key;
  314. tree.right = remove(tree.right, min);
  315. }
  316. } else {
  317. Node<T> tmp = tree;
  318. tree = (tree.left != null) ? tree.left : tree.right;
  319. tmp = null;
  320. }
  321. }
  322. return tree;
  323. }
  324. /*
  325. * 销毁AVL树
  326. */
  327. private void destroy(Node<T> tree) {
  328. if (tree == null)
  329. return;
  330. if (tree.left != null)
  331. destroy(tree.left);
  332. if (tree.right != null)
  333. destroy(tree.right);
  334. tree = null;
  335. }
  336. public void destroy() {
  337. destroy(root);
  338. }
  339. /*
  340. * 打印"二叉查找树"
  341. *
  342. * key -- 节点的键值
  343. * direction -- 0,表示该节点是根节点;
  344. * -1,表示该节点是它的父结点的左孩子;
  345. * 1,表示该节点是它的父结点的右孩子。
  346. */
  347. private void print(Node<T> tree, T key, int direction) {
  348. if (tree != null) {
  349. if (direction == 0) // tree是根节点
  350. System.out.printf("%2d is root\n", tree.key, key);
  351. else // tree是分支节点
  352. System.out.printf("%2d is %2d's %6s child\n", tree.key, key, direction == 1 ? "right" : "left");
  353. print(tree.left, tree.key, -1);
  354. print(tree.right, tree.key, 1);
  355. }
  356. }
  357. public void print() {
  358. if (root != null)
  359. print(root, root.key, 0);
  360. }
  361. }

5.3总结分析

AVL更平衡,结构上更加直观,时间效能针对读取而言更高;维护稍慢,空间开销较大。

六 2-3树

一棵2-3查找树要么为空,要么满足满足下面两个要求。
2-结点:
含有一个键(及其对应的值)和两条腿,左腿接指向2-3树中的键都小于该结点,右腿接指向的2-3树中的键都大于该结点。
3-结点:
含有两个键(及其对应的值)和三条腿,左腿接指向的2-3树中的键都小于该结点,中腿接指向的2-3树中的键都
位于该结点的两个键之间,右腿接指向的2-3树中的键都大于该结点。
image.png

6.1 树实现原理

6.1.2 插入原理

  1. 向2-结点中插入新键,则变成3-结点

image.png

  1. 向一棵只含有一个3-结点的树中插入新键,即满四进二

image.png

  1. 向一个父结点为2-结点,子节点为3-结点中插入新键

image.png
4.分解根结点
image.png

6.2 代码实现

略,后期有时间补充

6.3 总结分析

  1. 任意空链接到根结点的路径长度都是相等的。
  2. 4-结点变换为3-结点时,树的高度不会发生变化,只有当根结点是临时的4-结点,分解根结点时,树高+1。
  3. 2-3树与普通二叉查找树最大的区别在于,普通的二叉查找树是自顶向下生长,而2-3树是自底向上生长。

    七 红黑树

    红黑树主要是对2-3树进行编码,红黑树背后的基本思想是用标准的二叉查找树(完全由2-结点构成)和一些额外的信息(替换3-结点)来表示2-3树。
    我们将树中的链接分为两种类型:红链接:将两个2-结点连接起来构成一个3-结点; 黑链接:则是2-3树中的普通链接。

红黑树是含有红黑链接并满足下列条件的二叉查找树:
1. 红链接均为左链接;
2. 没有任何一个结点同时和两条红链接相连;
3. 该树是完美黑色平衡的,即任意空链接到根结点的路径上的黑链接数量相同;
image.png

7.1 实现原理

7.1.1 插入原理

  1. 向单个2-结点中插入新键

    1. 如果新键小于当前结点的键,插入左红子节点,新的红黑树和单个3-结点完全等价。(正常)

      image.png

    2. 如果新键大于当前结点的键,插入右红子节点,(1.右红失衡)此时我们需要左旋,把右红变成左红,插入操作才算完成。

image.png

  1. 颜色反转

当一个结点的左右子结点的color都为RED时,也就是出现了临时的4-结点,此时只需要把左右子结点的颜色变为BLACK,同时让当前结点的颜色变为RED即可。
image.png

  1. 向一棵双键树(即一个3-结点)中插入新键
    1. 新键大于原树中的两个键,则插入右红子节点(左右红失衡,需要颜色反转)

image.png

  1. 新键小于原树中的两个键,(左左红,失衡)

image.png

  1. 新键介于原数中两个键之间(左右红失衡)

image.png

7.1.2 左旋平衡

右红是失衡的,必须调整即:
当某个结点的左子结点为黑色,右子结点为红色,此时需要左旋。
前提:当前结点为h,它的右子结点为x;
左旋过程:

  1. 让x的左子结点变为h的右子结点:h.right=x.left;
  2. 让h成为x的左子结点:x.left=h;
  3. 让h的color属性变为x的color属性值:x.color=h.color;
  4. 让h的color属性变为RED:h.color=true;

image.png

7.1.3 右旋平衡

当某个结点的左子结点是红色,且左子结点的左子结点也是红色,需要右旋
前提:当前结点为h,它的左子结点为x;
右旋过程:

  1. 让x的右子结点成为h的左子结点:h.left = x.right;
  2. 让h成为x的右子结点:x.right=h;
  3. 让x的color变为h的color属性值:x.color = h.color;
  4. 让h的color为RED;

image.png

7.1.4 失衡归纳分析

我们前面研究平衡树叶做了类似的归纳 左左,左右 右右,右左,这里也有类似总结:

  1. 右红

即红黑树,不允许有右红链,所以,它会及时的左旋,变成左链红,即也就避免了,右右的出现,也避免了右左的出现

  1. 左左红

左子树红色,左孙子树也是红色,所以它需要先右旋,即先掰弯成一个倒三角,在颜色反转

  1. 左右红

左子树为红色,右孙子树为红色,则需要先调整右红(1的情况),左旋,变成左左(2的情况),总结起来,是先掰直,再掰弯,再颜色反转。

7.1.5 总结

通过上面的分析,不难看出,红黑树的失衡情况要好于AVL树,因此在做插入删除操作,其红黑树的性能高些,当然查询当然AVL树的效率高些,因为我比较平衡。即成也平衡,败也平衡,凡是过犹不及(追求平衡导致频繁旋转,影响删除,插入)。

7.2 代码实现

  1. package com.ycc.data.structure.tree;
  2. /**
  3. * 红黑二叉树
  4. *
  5. * @param <Key>
  6. * @param <Value>
  7. */
  8. public class RedBlackTree<Key extends Comparable<Key>, Value> {
  9. /**
  10. * 红色链接
  11. */
  12. private static final boolean RED = true;
  13. /**
  14. * 黑色链接
  15. */
  16. private static final boolean BLACK = false;
  17. /**
  18. * 根节点
  19. */
  20. private Node root;
  21. /**
  22. * 记录树中元素的个数
  23. */
  24. private int current;
  25. //获取树中元素的个数
  26. public int size() {
  27. return current;
  28. }
  29. /**
  30. * 判断当前节点的父指向链接是否为红色
  31. *
  32. * @param x
  33. * @return
  34. */
  35. private boolean isRed(Node x) {
  36. if (x == null) {
  37. return false;
  38. }
  39. return x.color == RED;
  40. }
  41. /**
  42. * 右红链需左旋转
  43. *
  44. * @param h
  45. * @return
  46. */
  47. private Node rotateLeft(Node h) {
  48. //找到h结点的右子结点x
  49. Node x = h.right;
  50. //找到x结点的左子结点,让x结点的左子结点称为h结点的右子结点
  51. h.right = x.left;
  52. //让h结点称为x结点的左子结点
  53. x.left = h;
  54. //让x结点的color属性变为h结点的color属性
  55. x.color = h.color;
  56. //让h结点的color属性变为RED
  57. h.color = RED;
  58. return x;
  59. }
  60. /**
  61. * 右旋
  62. *
  63. * @param h
  64. * @return
  65. */
  66. private Node rotateRight(Node h) {
  67. //找到h结点的左子结点 x
  68. Node x = h.left;
  69. //让x结点的右子结点成为h结点的左子结点
  70. h.left = x.right;
  71. //让h结点成为x结点的右子结点
  72. x.right = h;
  73. //让x结点的color属性变为h结点的color属性
  74. x.color = h.color;
  75. //让h结点的color属性为RED
  76. h.color = RED;
  77. return x;
  78. }
  79. /**
  80. * 颜色反转,相当于完成拆分4-节点
  81. *
  82. * @param h
  83. */
  84. private void flipColors(Node h) {
  85. //当前结点变为红色
  86. h.color = RED;
  87. //左子结点和右子结点变为黑色
  88. h.left.color = BLACK;
  89. h.right.color = BLACK;
  90. }
  91. /**
  92. * 在整个树上完成插入操作
  93. *
  94. * @param key
  95. * @param val
  96. */
  97. public void put(Key key, Value val) {
  98. root = put(root, key, val);
  99. //根结点的颜色总是黑色
  100. root.color = RED;
  101. }
  102. /**
  103. * 在指定树中,完成插入操作,并返回添加元素后新的树
  104. *
  105. * @param h
  106. * @param key
  107. * @param val
  108. */
  109. private Node put(Node h, Key key, Value val) {
  110. //判断h是否为空,如果为空则直接返回一个红色的结点就可以了
  111. if (h == null) {
  112. //数量+1
  113. current++;
  114. return new Node(key, val, null, null, RED);
  115. }
  116. //比较h结点的键和key的大小
  117. int cmp = key.compareTo(h.key);
  118. if (cmp < 0) {
  119. //继续往左
  120. h.left = put(h.left, key, val);
  121. } else if (cmp > 0) {
  122. //继续往右
  123. h.right = put(h.right, key, val);
  124. } else {
  125. //发生值的替换
  126. h.value = val;
  127. }
  128. //进行左旋:当当前结点h的左子结点为黑色,右子结点为红色,需要左旋
  129. if (isRed(h.right) && !isRed(h.left)) {
  130. h = rotateLeft(h);
  131. }
  132. //进行右旋:当当前结点h的左子结点和左子结点的左子结点都为红色,需要右旋
  133. if (isRed(h.left) && isRed(h.left.left)) {
  134. rotateRight(h);
  135. }
  136. //颜色反转:当前结点的左子结点和右子结点都为红色时,需要颜色反转
  137. if (isRed(h.left) && isRed(h.right)) {
  138. flipColors(h);
  139. }
  140. return h;
  141. }
  142. //根据key,从树中找出对应的值
  143. public Value get(Key key) {
  144. return get(root, key);
  145. }
  146. //从指定的树x中,查找key对应的值
  147. public Value get(Node x, Key key) {
  148. if (x == null) {
  149. return null;
  150. }
  151. //比较x结点的键和key的大小
  152. int cmp = key.compareTo(x.key);
  153. if (cmp < 0) {
  154. return get(x.left, key);
  155. } else if (cmp > 0) {
  156. return get(x.right, key);
  157. } else {
  158. return x.value;
  159. }
  160. }
  161. //结点类
  162. private class Node {
  163. //存储键
  164. public Key key;
  165. //记录左子结点
  166. public Node left;
  167. //记录右子结点
  168. public Node right;
  169. //由其父结点指向它的链接的颜色
  170. public boolean color;
  171. //存储值
  172. private Value value;
  173. public Node(Key key, Value value, Node left, Node right, boolean color) {
  174. this.key = key;
  175. this.value = value;
  176. this.left = left;
  177. this.right = right;
  178. this.color = color;
  179. }
  180. }
  181. }

八 B-树

B树中允许一个结点中包含多个key,可以是3个、4个、5个甚至更多,并不确定,需要看具体的实现。现在我们选择一个参数M,来构造一个B树,我们可以把它称作是M阶的B树,那么该树会具有如下特点:

  1. 每个结点最多有M-1个key,并且以升序排列;
  2. 每个结点最多能有M个子结点;
  3. 根结点至少有两个子结点;

image.png

8.1 实现原理

若参数M选择为5,那么每个结点最多包含4个键值对,我们以5阶B树为例,看看B树的数据存储。
image.png

8.2 代码实现

代码较为复杂,了解即可

  1. package com.ycc.data.structure.tree;
  2. import java.io.BufferedReader;
  3. import java.io.IOException;
  4. import java.io.InputStreamReader;
  5. import java.util.ArrayList;
  6. import java.util.List;
  7. import java.util.NoSuchElementException;
  8. /**
  9. * 该程序参考文章:http://blog.csdn.net/v_JULY_v/article/details/6530142/
  10. * <p>
  11. * B-tree
  12. *
  13. * @author liaozx
  14. * @description
  15. * @create 2020-11-27 15:31
  16. */
  17. public class BSubTree<T extends Comparable<T>> {
  18. private static final BSubTree<Integer> tree = new BSubTree<Integer>();
  19. private static final BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
  20. public BTNode root;
  21. int m = 4; //此B-树的阶数。关键字数等于阶数-1。m至少为2,m必须大于等于2。
  22. int n; //n是关键字最小个数
  23. public BSubTree() {
  24. root = new BTNode(null, null);
  25. if (m >= 2) {
  26. if (m % 2 == 0) {
  27. n = m / 2 - 1;
  28. } else {
  29. n = m / 2;
  30. }
  31. } else {
  32. System.out.println("error");
  33. System.exit(0);
  34. }
  35. }
  36. public BSubTree(BTNode root) {
  37. this.root = root;
  38. if (m % 2 == 0) {
  39. n = m / 2;
  40. } else {
  41. n = m / 2 + 1;
  42. }
  43. }
  44. private static String stringInput(String inputRequest) throws IOException {
  45. System.out.println(inputRequest);
  46. return reader.readLine();
  47. }
  48. public static void main(String[] args) throws IOException {
  49. System.out.println("test B - balanced tree operations");
  50. System.out.println("*****************************");
  51. String input;
  52. Integer value;
  53. do {
  54. input = stringInput("please select: [i]nsert, [d]elete, [s]how, [e]xit");
  55. switch (input.charAt(0)) {
  56. case 'i':
  57. value = Integer.parseInt(stringInput("insert: "), 10);
  58. tree.insert(value);
  59. break;
  60. case 'd':
  61. value = Integer.parseInt(stringInput("delete: "), 10);
  62. tree.delete(value);
  63. break;
  64. case 's':
  65. System.out.println(tree.perOrder(tree.root));
  66. break;
  67. // case 'h':
  68. // System.out.println(tree.getHeight());
  69. }
  70. } while ((input.charAt(0) != 'e'));
  71. }
  72. public BTNode findNode(T information) {
  73. return findNode(information, root);
  74. } //isMember应该返回插入点
  75. private BTNode findNode(T info, BTNode node) { //不论是否找到都返回一个node。
  76. BTNode member = null;
  77. if (node.informations.size() == 0) {//这种情况存在,只有root可能
  78. member = node;
  79. //System.out.println("error");
  80. } else {
  81. if (info.compareTo(node.informations.get(node.informations.size() - 1)) > 0) { //info比节点最大的还大,则直接进入最右分支
  82. if (node.ptr.size() > 0) {//有孩子的情况,进入范围中的子节点
  83. member = findNode(info, node.ptr.get(node.informations.size()));
  84. } else {//没有子节点,直接返回node
  85. member = node;
  86. }
  87. } else {//没有判断没有子节点的情况,上一个if中判断了,这一个else中就忘了,怒
  88. if (node.ptr.size() > 0) {//有子节点
  89. if (info.compareTo(node.informations.get(0)) < 0) {
  90. member = findNode(info, node.ptr.get(0));
  91. } else {
  92. for (int i = 0; i < node.informations.size(); ++i) {
  93. if (info.compareTo(node.informations.get(i)) == 0) {
  94. member = node;
  95. break;
  96. } else if (info.compareTo(node.informations.get(i)) > 0 && info.compareTo(node.informations.get(i + 1)) < 0) { //只要不是最右,info比之大的,进入它的孩子节点
  97. member = findNode(info, node.ptr.get(i + 1));
  98. break;
  99. }
  100. }
  101. }
  102. } else {//没有子节点
  103. member = node;
  104. }
  105. }
  106. }
  107. return member;
  108. }
  109. public void insert(T info) {
  110. BTNode temp = findNode(info);
  111. if (temp.informations.size() != 0) {
  112. for (T i : temp.informations) {
  113. if (i.compareTo(info) == 0) {
  114. System.out.println("已存在所插入的值。");
  115. return;
  116. }
  117. }
  118. }
  119. insert(info, temp, temp.parent);
  120. return;
  121. }
  122. private void insert(T info, BTNode node, BTNode parent) {//插入一定是在叶子节点
  123. if (node == null) {//insert中的node为空应该只有一种情况,node=root
  124. if (parent == null) {
  125. root = new BTNode(info, parent);
  126. } else {
  127. System.out.println("不应该出现的情况,请检查。");
  128. //node = new BTNode(info,parent);
  129. }
  130. } else {
  131. if (node.informations.size() == 0) {
  132. //System.out.println("这种情况应该不存在,请检查代码");//现在存在这种情况啦
  133. node.informations.add(info);
  134. } else if (node.informations.size() > 0 && node.informations.size() < m - 1) {
  135. if (info.compareTo(node.informations.get(node.informations.size() - 1)) > 0) {//info比node最右边最大的值还大,则直接插入
  136. node.informations.add(info);
  137. } else {
  138. for (int i = 0; i < node.informations.size(); ++i) {
  139. if (info.compareTo(node.informations.get(i)) < 0) {
  140. node.informations.add(i, info);
  141. break;
  142. }
  143. }
  144. }
  145. } else if (node.informations.size() == m - 1) {//需要分裂
  146. if (info.compareTo(node.informations.get(node.informations.size() - 1)) > 0) {//info比node最右边最大的值还大,则直接插入
  147. node.informations.add(info);
  148. } else {
  149. for (int i = 0; i < node.informations.size(); ++i) {
  150. if (info.compareTo(node.informations.get(i)) < 0) {
  151. node.informations.add(i, info);
  152. break;
  153. }
  154. }
  155. }
  156. split(node);
  157. } else {
  158. System.out.println("node 的size大于等于m-1,不应该出现,请检查代码");
  159. }
  160. }
  161. }
  162. public void delete(T info) {
  163. BTNode temp = findNode(info, root);
  164. if (temp.informations.size() == 0) {
  165. System.out.println("根节点为空!");
  166. return;
  167. }
  168. for (T i : temp.informations) {
  169. if (info.compareTo(i) == 0) {
  170. delete(info, temp);
  171. break;
  172. } else if (temp.informations.indexOf(i) == temp.informations.size() - 1) {//循环到最后一个值了,仍到这里,说明不存在要删除的值!
  173. System.out.println("不存在要删除的值!");
  174. }
  175. }
  176. }
  177. private void delete(T info, BTNode node) throws NoSuchElementException {
  178. if (node == null) { //其实到这里,就一定存在要删除的值了。
  179. throw new NoSuchElementException();
  180. } else {
  181. int i;
  182. for (i = 0; i < node.informations.size(); i++) {
  183. if (info.compareTo(node.informations.get(i)) == 0) {
  184. node.informations.remove(i); //删除关键字,其实要是索引向文件,也应该删除文件。
  185. break;
  186. }
  187. }
  188. if (node.ptr.size() > 0) {//删除一个非叶子节点的关键字后,如果有孩子,则判断孩子的孩子,如果孩子有孩子,则将右孩子的孩子最深左孩子的第一个值赋给删除关键字的节点
  189. //每一个关键字,一定有两个孩子
  190. if (node.ptr.get(i + 1).ptr.size() == 0) {//孩子没有孩子的时候,只将孩子的最左关键字上升。
  191. node.informations.add(i, node.ptr.get(i + 1).informations.get(0));
  192. node.ptr.get(i + 1).informations.remove(0);
  193. if (node.ptr.get(i + 1).informations.size() < n) {
  194. dManageNode(node.ptr.get(i + 1));
  195. }
  196. } else {//孩子有孩子的时候,则将右孩子的孩子最深左孩子的第一个值赋给删除关键字的节点
  197. pullRLeftNode(node, i, node.ptr.get(i + 1), i);
  198. }
  199. } else {//如果没有孩子,要判断该节点关键字数量是否大于最小值
  200. if (node.informations.size() >= n) {//大于等于就没事,不用动
  201. return;
  202. } else {//叶子节点中关键字数小于n,需要继续判断兄弟节点是否饱满
  203. dManageNode(node);
  204. }
  205. }
  206. }
  207. }
  208. public String perOrder(BTNode node) {
  209. String result = "";
  210. if (node.ptr.size() > 0) {
  211. int i = 0;
  212. for (BTNode n : node.ptr) {
  213. result += perOrder(n);
  214. if (i < node.informations.size()) {
  215. result += node.informations.get(i).toString() + ",";
  216. ++i;
  217. }
  218. }
  219. } else {//叶子节点
  220. if (node.informations.size() > 0) {
  221. for (T t : node.informations) {
  222. result += t.toString() + ",";
  223. }
  224. } else {//叶子节点没有空值的时候,除非是根节点,根节点为空值的时候,说句话意思意思
  225. result += "B-树为空!";
  226. }
  227. }
  228. return result;
  229. }
  230. public void split(BTNode node) {//进到这里的node都是m个关键字,需要提出m/2
  231. if (node == null) {
  232. System.out.println("error");
  233. } else {
  234. if (node.informations.size() != m) {
  235. System.out.println("error");
  236. } else {
  237. if (node.parent == null) {//node是root时
  238. T temp = node.informations.get(n);//这里正好
  239. root = new BTNode(temp, null);
  240. node.informations.remove(n);//加进去了就要删掉!
  241. root.ptr.add(node);
  242. node.parent = root;
  243. splitNewNode(node, n, root);
  244. } else {//一个非根节点
  245. T temp = node.informations.get(n);
  246. node.parent.informations.add(node.parent.ptr.indexOf(node), temp);
  247. node.informations.remove(n);
  248. splitNewNode(node, n, node.parent);
  249. if (node.parent.informations.size() >= m) {
  250. split(node.parent);
  251. }
  252. }
  253. }
  254. }
  255. }
  256. public void splitNewNode(BTNode node, int n, BTNode parent) {
  257. BTNode newnode = new BTNode(node.informations.get(n), node.parent);
  258. newnode.informations.addAll(node.informations.subList(n + 1, node.informations.size()));
  259. node.informations.removeAll(node.informations.subList(n, node.informations.size()));
  260. //newnode.parent=node.parent;//新增节点的父节点
  261. node.parent.ptr.add(node.parent.ptr.indexOf(node) + 1, newnode); //新增节点加到父节点上
  262. if (node.ptr.size() > 0) { //处理新增节点的孩子
  263. newnode.ptr.addAll(node.ptr.subList(n + 1, node.ptr.size()));
  264. node.ptr.removeAll(node.ptr.subList(n + 1, node.ptr.size()));
  265. for (BTNode bn : newnode.ptr) { //子节点移到了新节点上,但是子节点的父节点没有处理!!!T_T
  266. bn.parent = newnode;
  267. }
  268. }
  269. }
  270. public void combine(BTNode lnode, BTNode rnode) {
  271. if (lnode.informations.size() < n) {
  272. lnode.informations.add(lnode.parent.informations.get(lnode.parent.ptr.indexOf(lnode)));
  273. lnode.parent.informations.remove(lnode.parent.ptr.indexOf(lnode));
  274. } else if (rnode.informations.size() < n) {
  275. rnode.informations.add(0, rnode.parent.informations.get(lnode.parent.ptr.indexOf(lnode)));
  276. rnode.parent.informations.remove(lnode.parent.ptr.indexOf(lnode));
  277. } else {
  278. System.out.println("error");
  279. }
  280. lnode.informations.addAll(rnode.informations);
  281. lnode.ptr.addAll(rnode.ptr);
  282. for (BTNode n : rnode.ptr) {
  283. n.parent = lnode;
  284. }
  285. lnode.parent.ptr.remove(lnode.parent.ptr.indexOf(lnode) + 1);
  286. if (lnode.parent.parent == null && lnode.parent.informations.size() == 0) {//父节点是根节点
  287. lnode.parent = null; //lnode为新的根节点
  288. root = lnode;
  289. return;
  290. }
  291. if (lnode.parent.informations.size() < n) {
  292. dManageNode(lnode.parent);
  293. }
  294. }
  295. public void lrotate(BTNode lnode, BTNode rnode) {
  296. lnode.informations.add(lnode.parent.informations.get(lnode.parent.ptr.indexOf(lnode)));
  297. lnode.parent.informations.remove(lnode.parent.ptr.indexOf(lnode));
  298. lnode.parent.informations.add(lnode.parent.ptr.indexOf(lnode), rnode.informations.get(0));
  299. rnode.informations.remove(0);
  300. if (rnode.ptr.size() > 0) {//要判断叶子节点没有孩子!
  301. lnode.ptr.add(rnode.ptr.get(0));
  302. rnode.ptr.remove(0);
  303. lnode.ptr.get(lnode.ptr.size() - 1).parent = lnode;
  304. }
  305. }
  306. public void rrotate(BTNode lnode, BTNode rnode) {
  307. rnode.informations.add(rnode.parent.informations.get(lnode.parent.ptr.indexOf(lnode)));
  308. rnode.parent.informations.remove(lnode.parent.ptr.indexOf(lnode));
  309. rnode.parent.informations.add(lnode.parent.ptr.indexOf(lnode), lnode.informations.get(lnode.informations.size() - 1));
  310. lnode.informations.remove(lnode.informations.size() - 1);
  311. if (lnode.ptr.size() > 0) {
  312. rnode.ptr.add(0, lnode.ptr.get(lnode.ptr.size() - 1));
  313. lnode.ptr.remove(lnode.ptr.size() - 1);
  314. rnode.ptr.get(0).parent = rnode;
  315. }
  316. }
  317. public void dManageNode(BTNode node) {//叶子节点中关键字数小于n,需要继续判断兄弟节点是否饱满,是旋转还是合并
  318. if (node.parent == null) {
  319. return;
  320. } else {
  321. int x = node.parent.ptr.indexOf(node);
  322. if (x == 0) {//被删除关键字所在节点,是父节点最左边的节点时,判断右兄弟,而且肯定有右兄弟
  323. if (node.parent.ptr.get(x + 1).informations.size() == n) {//刚脱贫,需要合并
  324. combine(node, node.parent.ptr.get(x + 1));
  325. } else if (node.parent.ptr.get(x + 1).informations.size() > n) {//关键字数大于最小值,丰满
  326. lrotate(node, node.parent.ptr.get(x + 1));
  327. } else {
  328. System.out.println("error");
  329. }
  330. } else if (x == node.parent.ptr.size() - 1) {//是父节点最右边的节点时,判断左兄弟
  331. if (node.parent.ptr.get(x - 1).informations.size() == n) {//左兄弟刚脱贫,需要合并
  332. combine(node.parent.ptr.get(x - 1), node);
  333. } else if (node.parent.ptr.get(x - 1).informations.size() > n) {//关键字数大于最小值,丰满
  334. rrotate(node.parent.ptr.get(x - 1), node);
  335. } else {
  336. System.out.println("error");
  337. }
  338. } else {//node在父节点的子节点的中间,需要先判断左兄弟,再判断右兄弟。靠,感觉判断兄弟是否饱满,还是应该写一个函数,也许可以传递两个值
  339. //先跟饱满的借,除非两个兄弟都刚脱贫。
  340. if (node.parent.ptr.get(x - 1).informations.size() > n) {//左兄弟丰满
  341. rrotate(node.parent.ptr.get(x - 1), node);
  342. } else if (node.parent.ptr.get(x + 1).informations.size() > n) {//右兄弟丰满
  343. lrotate(node, node.parent.ptr.get(x + 1));
  344. } else {//左右兄弟都刚脱贫,需要合并
  345. combine(node.parent.ptr.get(x - 1), node);
  346. }
  347. }
  348. }
  349. }
  350. public void pullRLeftNode(BTNode donode, int j, BTNode node, int i) {//节点删除关键字后,如果该节点有孩子,则孩子需要贡献关键字,由于孩子减少了关键字还需要向下借,一直递归到叶子。
  351. if (node.ptr.get(0).ptr.size() > 0) {
  352. pullRLeftNode(donode, j, node.ptr.get(0), 0);
  353. } else {
  354. donode.informations.add(j, node.ptr.get(0).informations.get(0));
  355. node.ptr.get(0).informations.remove(0);
  356. if (node.ptr.get(0).informations.size() < n) {
  357. dManageNode(node.ptr.get(0));
  358. }
  359. }
  360. }
  361. class BTNode {
  362. BTNode parent; //父节点
  363. List<T> informations = new ArrayList<>(); //关键字的信息
  364. List<BTNode> ptr = new ArrayList<BTNode>(); //分支
  365. public BTNode(T information, BTNode parent) {
  366. if (information != null) {
  367. informations.add(information);
  368. this.parent = parent;
  369. } else {
  370. this.parent = null;
  371. }
  372. }
  373. boolean isLeaf() {
  374. return (ptr.size() == 0);
  375. }
  376. boolean isNode() {
  377. return (ptr.size() != 0);
  378. }
  379. int infoLength() {
  380. return informations.size();
  381. }
  382. int ptrLength() {
  383. return ptr.size();
  384. }
  385. }
  386. }

8.3 总结分析

磁盘IO文件系统的设计者利用了磁盘预读原理,将一个结点的大小设为等于一个页(1024个字节或其整数倍),这样每个结点只需要一次I/O就可以完全载入。那么3层的B树可以容纳102410241024差不多10亿个数据,如果换成二叉查找树,则需要30层!假定操作系统一次读取一个节点,并且根节点保留在内存中,那么B树在10亿个数据中查找目标值,只需要小于3次硬盘读取就可以找到目标值,但红黑树需要小于30次,因此B树大大提高了IO的操作效率。

九 B+树

B+树是对B树的一种变形树,它与B树的差异在于:
1. 非叶结点仅具有索引作用,也就是说,非叶子结点只存储key,不存储value;
2. 树的所有叶结点构成一个有序链表,可以按照key排序的次序遍历全部数据。

9.1 实现原理

若参数M选择为5,那么每个结点最多包含4个键值对,我们以5阶B+树为例,看看B+树的数据存储。
image.png
B+树的优点在于:
1.由于B+树在非叶子结点上不包含真正的数据,只当做索引使用,因此在内存相同的情况下,能够存放更多的key。 2.B+树的叶子结点都是相连的,因此对整棵树的遍历只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。
B树的优点在于:
由于B树的每一个节点都包含key和value,因此我们根据key查找value时,只需要找到key所在的位置,就能找到value,但B+树只有叶子结点存储数据,索引每一次查找,都必须一次一次,一直找到树的最大深度处,也就是叶子结点的深度,才能找到value。

9.2 代码实现

代码比较复杂,了解即可。

  1. package com.ycc.data.structure.tree;
  2. /**
  3. * 实现B+树
  4. *
  5. * @author liaozx
  6. * @description
  7. * @create 2020-11-27 15:53
  8. * * @param <T> 指定值类型
  9. * * @param <V> 使用泛型,指定索引类型,并且指定必须继承Comparable
  10. */
  11. public class BPlusTree<T, V extends Comparable<V>> {
  12. //B+树的阶
  13. private final Integer bTreeOrder;
  14. //B+树的非叶子节点最小拥有的子节点数量(同时也是键的最小数量)
  15. //private Integer minNUmber;
  16. //B+树的非叶子节点最大拥有的节点数量(同时也是键的最大数量)
  17. private final Integer maxNumber;
  18. private Node<T, V> root;
  19. private LeafNode<T, V> left;
  20. //无参构造方法,默认阶为3
  21. public BPlusTree() {
  22. this(3);
  23. }
  24. //有参构造方法,可以设定B+树的阶
  25. public BPlusTree(Integer bTreeOrder) {
  26. this.bTreeOrder = bTreeOrder;
  27. //this.minNUmber = (int) Math.ceil(1.0 * bTreeOrder / 2.0);
  28. //因为插入节点过程中可能出现超过上限的情况,所以这里要加1
  29. this.maxNumber = bTreeOrder + 1;
  30. this.root = new LeafNode<T, V>();
  31. this.left = null;
  32. }
  33. //查询
  34. public T find(V key) {
  35. T t = this.root.find(key);
  36. if (t == null) {
  37. System.out.println("不存在");
  38. }
  39. return t;
  40. }
  41. //插入
  42. public void insert(T value, V key) {
  43. if (key == null)
  44. return;
  45. Node<T, V> t = this.root.insert(value, key);
  46. if (t != null)
  47. this.root = t;
  48. this.left = this.root.refreshLeft();
  49. // System.out.println("插入完成,当前根节点为:");
  50. // for(int j = 0; j < this.root.number; j++) {
  51. // System.out.print((V) this.root.keys[j] + " ");
  52. // }
  53. // System.out.println();
  54. }
  55. /**
  56. * 节点父类,因为在B+树中,非叶子节点不用存储具体的数据,只需要把索引作为键就可以了
  57. * 所以叶子节点和非叶子节点的类不太一样,但是又会公用一些方法,所以用Node类作为父类,
  58. * 而且因为要互相调用一些公有方法,所以使用抽象类
  59. *
  60. * @param <T> 同BPlusTree
  61. * @param <V>
  62. */
  63. abstract class Node<T, V extends Comparable<V>> {
  64. //父节点
  65. protected Node<T, V> parent;
  66. //子节点
  67. protected Node<T, V>[] childs;
  68. //键(子节点)数量
  69. protected Integer number;
  70. //键
  71. protected Object[] keys;
  72. //构造方法
  73. public Node() {
  74. this.keys = new Object[maxNumber];
  75. this.childs = new Node[maxNumber];
  76. this.number = 0;
  77. this.parent = null;
  78. }
  79. //查找
  80. abstract T find(V key);
  81. //插入
  82. abstract Node<T, V> insert(T value, V key);
  83. abstract LeafNode<T, V> refreshLeft();
  84. }
  85. /**
  86. * 非叶节点类
  87. *
  88. * @param <T>
  89. * @param <V>
  90. */
  91. class BPlusNode<T, V extends Comparable<V>> extends Node<T, V> {
  92. public BPlusNode() {
  93. super();
  94. }
  95. /**
  96. * 递归查找,这里只是为了确定值究竟在哪一块,真正的查找到叶子节点才会查
  97. *
  98. * @param key
  99. * @return
  100. */
  101. @Override
  102. T find(V key) {
  103. int i = 0;
  104. while (i < this.number) {
  105. if (key.compareTo((V) this.keys[i]) <= 0)
  106. break;
  107. i++;
  108. }
  109. if (this.number == i)
  110. return null;
  111. return this.childs[i].find(key);
  112. }
  113. /**
  114. * 递归插入,先把值插入到对应的叶子节点,最终讲调用叶子节点的插入类
  115. *
  116. * @param value
  117. * @param key
  118. */
  119. @Override
  120. Node<T, V> insert(T value, V key) {
  121. int i = 0;
  122. while (i < this.number) {
  123. if (key.compareTo((V) this.keys[i]) < 0)
  124. break;
  125. i++;
  126. }
  127. if (key.compareTo((V) this.keys[this.number - 1]) >= 0) {
  128. i--;
  129. // if(this.childs[i].number + 1 <= bTreeOrder) {
  130. // this.keys[this.number - 1] = key;
  131. // }
  132. }
  133. // System.out.println("非叶子节点查找key: " + this.keys[i]);
  134. return this.childs[i].insert(value, key);
  135. }
  136. @Override
  137. LeafNode<T, V> refreshLeft() {
  138. return this.childs[0].refreshLeft();
  139. }
  140. /**
  141. * 当叶子节点插入成功完成分解时,递归地向父节点插入新的节点以保持平衡
  142. *
  143. * @param node1
  144. * @param node2
  145. * @param key
  146. */
  147. Node<T, V> insertNode(Node<T, V> node1, Node<T, V> node2, V key) {
  148. // System.out.println("非叶子节点,插入key: " + node1.keys[node1.number - 1] + " " + node2.keys[node2.number - 1]);
  149. V oldKey = null;
  150. if (this.number > 0)
  151. oldKey = (V) this.keys[this.number - 1];
  152. //如果原有key为null,说明这个非节点是空的,直接放入两个节点即可
  153. if (key == null || this.number <= 0) {
  154. // System.out.println("非叶子节点,插入key: " + node1.keys[node1.number - 1] + " " + node2.keys[node2.number - 1] + "直接插入");
  155. this.keys[0] = node1.keys[node1.number - 1];
  156. this.keys[1] = node2.keys[node2.number - 1];
  157. this.childs[0] = node1;
  158. this.childs[1] = node2;
  159. this.number += 2;
  160. return this;
  161. }
  162. //原有节点不为空,则应该先寻找原有节点的位置,然后将新的节点插入到原有节点中
  163. int i = 0;
  164. while (key.compareTo((V) this.keys[i]) != 0) {
  165. i++;
  166. }
  167. //左边节点的最大值可以直接插入,右边的要挪一挪再进行插入
  168. this.keys[i] = node1.keys[node1.number - 1];
  169. this.childs[i] = node1;
  170. Object[] tempKeys = new Object[maxNumber];
  171. Object[] tempChilds = new Node[maxNumber];
  172. System.arraycopy(this.keys, 0, tempKeys, 0, i + 1);
  173. System.arraycopy(this.childs, 0, tempChilds, 0, i + 1);
  174. System.arraycopy(this.keys, i + 1, tempKeys, 0, this.number - i - 1);
  175. System.arraycopy(this.childs, i + 1, tempChilds, 0, this.number - i - 1);
  176. tempKeys[i + 1] = node2.keys[node2.number - 1];
  177. tempChilds[i + 1] = node2;
  178. this.number++;
  179. //判断是否需要拆分
  180. //如果不需要拆分,把数组复制回去,直接返回
  181. if (this.number <= bTreeOrder) {
  182. System.arraycopy(tempKeys, 0, this.keys, 0, this.number);
  183. System.arraycopy(tempChilds, 0, this.childs, 0, this.number);
  184. // System.out.println("非叶子节点,插入key: " + node1.keys[node1.number - 1] + " " + node2.keys[node2.number - 1] + ", 不需要拆分");
  185. return null;
  186. }
  187. // System.out.println("非叶子节点,插入key: " + node1.keys[node1.number - 1] + " " + node2.keys[node2.number - 1] + ",需要拆分");
  188. //如果需要拆分,和拆叶子节点时类似,从中间拆开
  189. Integer middle = this.number / 2;
  190. //新建非叶子节点,作为拆分的右半部分
  191. BPlusNode<T, V> tempNode = new BPlusNode<T, V>();
  192. //非叶节点拆分后应该将其子节点的父节点指针更新为正确的指针
  193. tempNode.number = this.number - middle;
  194. tempNode.parent = this.parent;
  195. //如果父节点为空,则新建一个非叶子节点作为父节点,并且让拆分成功的两个非叶子节点的指针指向父节点
  196. if (this.parent == null) {
  197. // System.out.println("非叶子节点,插入key: " + node1.keys[node1.number - 1] + " " + node2.keys[node2.number - 1] + ",新建父节点");
  198. BPlusNode<T, V> tempBPlusNode = new BPlusNode<>();
  199. tempNode.parent = tempBPlusNode;
  200. this.parent = tempBPlusNode;
  201. oldKey = null;
  202. }
  203. System.arraycopy(tempKeys, middle, tempNode.keys, 0, tempNode.number);
  204. System.arraycopy(tempChilds, middle, tempNode.childs, 0, tempNode.number);
  205. for (int j = 0; j < tempNode.number; j++) {
  206. tempNode.childs[j].parent = tempNode;
  207. }
  208. //让原有非叶子节点作为左边节点
  209. this.number = middle;
  210. this.keys = new Object[maxNumber];
  211. this.childs = new Node[maxNumber];
  212. System.arraycopy(tempKeys, 0, this.keys, 0, middle);
  213. System.arraycopy(tempChilds, 0, this.childs, 0, middle);
  214. //叶子节点拆分成功后,需要把新生成的节点插入父节点
  215. BPlusNode<T, V> parentNode = (BPlusNode<T, V>) this.parent;
  216. return parentNode.insertNode(this, tempNode, oldKey);
  217. }
  218. }
  219. /**
  220. * 叶节点类
  221. *
  222. * @param <T>
  223. * @param <V>
  224. */
  225. class LeafNode<T, V extends Comparable<V>> extends Node<T, V> {
  226. protected Object[] values;
  227. protected LeafNode left;
  228. protected LeafNode right;
  229. public LeafNode() {
  230. super();
  231. this.values = new Object[maxNumber];
  232. this.left = null;
  233. this.right = null;
  234. }
  235. /**
  236. * 进行查找,经典二分查找,不多加注释
  237. *
  238. * @param key
  239. * @return
  240. */
  241. @Override
  242. T find(V key) {
  243. if (this.number <= 0)
  244. return null;
  245. // System.out.println("叶子节点查找");
  246. Integer left = 0;
  247. Integer right = this.number;
  248. Integer middle = (left + right) / 2;
  249. while (left < right) {
  250. V middleKey = (V) this.keys[middle];
  251. if (key.compareTo(middleKey) == 0)
  252. return (T) this.values[middle];
  253. else if (key.compareTo(middleKey) < 0)
  254. right = middle;
  255. else
  256. left = middle;
  257. middle = (left + right) / 2;
  258. }
  259. return null;
  260. }
  261. /**
  262. * @param value
  263. * @param key
  264. */
  265. @Override
  266. Node<T, V> insert(T value, V key) {
  267. // System.out.println("叶子节点,插入key: " + key);
  268. //保存原始存在父节点的key值
  269. V oldKey = null;
  270. if (this.number > 0)
  271. oldKey = (V) this.keys[this.number - 1];
  272. //先插入数据
  273. int i = 0;
  274. while (i < this.number) {
  275. if (key.compareTo((V) this.keys[i]) < 0)
  276. break;
  277. i++;
  278. }
  279. //复制数组,完成添加
  280. Object[] tempKeys = new Object[maxNumber];
  281. Object[] tempValues = new Object[maxNumber];
  282. System.arraycopy(this.keys, 0, tempKeys, 0, i);
  283. System.arraycopy(this.values, 0, tempValues, 0, i);
  284. System.arraycopy(this.keys, i, tempKeys, i + 1, this.number - i);
  285. System.arraycopy(this.values, i, tempValues, i + 1, this.number - i);
  286. tempKeys[i] = key;
  287. tempValues[i] = value;
  288. this.number++;
  289. // System.out.println("插入完成,当前节点key为:");
  290. // for(int j = 0; j < this.number; j++)
  291. // System.out.print(tempKeys[j] + " ");
  292. // System.out.println();
  293. //判断是否需要拆分
  294. //如果不需要拆分完成复制后直接返回
  295. if (this.number <= bTreeOrder) {
  296. System.arraycopy(tempKeys, 0, this.keys, 0, this.number);
  297. System.arraycopy(tempValues, 0, this.values, 0, this.number);
  298. //有可能虽然没有节点分裂,但是实际上插入的值大于了原来的最大值,所以所有父节点的边界值都要进行更新
  299. Node node = this;
  300. while (node.parent != null) {
  301. V tempkey = (V) node.keys[node.number - 1];
  302. if (tempkey.compareTo((V) node.parent.keys[node.parent.number - 1]) > 0) {
  303. node.parent.keys[node.parent.number - 1] = tempkey;
  304. node = node.parent;
  305. }
  306. }
  307. // System.out.println("叶子节点,插入key: " + key + ",不需要拆分");
  308. return null;
  309. }
  310. // System.out.println("叶子节点,插入key: " + key + ",需要拆分");
  311. //如果需要拆分,则从中间把节点拆分差不多的两部分
  312. Integer middle = this.number / 2;
  313. //新建叶子节点,作为拆分的右半部分
  314. LeafNode<T, V> tempNode = new LeafNode<T, V>();
  315. tempNode.number = this.number - middle;
  316. tempNode.parent = this.parent;
  317. //如果父节点为空,则新建一个非叶子节点作为父节点,并且让拆分成功的两个叶子节点的指针指向父节点
  318. if (this.parent == null) {
  319. // System.out.println("叶子节点,插入key: " + key + ",父节点为空 新建父节点");
  320. BPlusNode<T, V> tempBPlusNode = new BPlusNode<>();
  321. tempNode.parent = tempBPlusNode;
  322. this.parent = tempBPlusNode;
  323. oldKey = null;
  324. }
  325. System.arraycopy(tempKeys, middle, tempNode.keys, 0, tempNode.number);
  326. System.arraycopy(tempValues, middle, tempNode.values, 0, tempNode.number);
  327. //让原有叶子节点作为拆分的左半部分
  328. this.number = middle;
  329. this.keys = new Object[maxNumber];
  330. this.values = new Object[maxNumber];
  331. System.arraycopy(tempKeys, 0, this.keys, 0, middle);
  332. System.arraycopy(tempValues, 0, this.values, 0, middle);
  333. this.right = tempNode;
  334. tempNode.left = this;
  335. //叶子节点拆分成功后,需要把新生成的节点插入父节点
  336. BPlusNode<T, V> parentNode = (BPlusNode<T, V>) this.parent;
  337. return parentNode.insertNode(this, tempNode, oldKey);
  338. }
  339. @Override
  340. LeafNode<T, V> refreshLeft() {
  341. if (this.number <= 0)
  342. return null;
  343. return this;
  344. }
  345. }
  346. }

9.3 总结分析

在数据库的操作中,查询操作可以说是最频繁的一种操作,因此在设计数据库时,必须要考虑到查询的效率问题,在很多数据库中,都是用到了B+树来提高查询的效率。

9.3.1 未建立索引

image.png
执行select * from user where id=18 ,需要从第一条数据开始,一直查询到第6条,发现id=18,此时才能查询出
目标结果,共需要比较6次;

9.3.2 建立主键索引查询

image.png
执行select * from user where id>=12 and id<=18 ,如果有了索引,由于B+树的叶子结点形成了一个有序链表,所以我们只需要找到id为12的叶子结点,按照遍历链表的方式顺序往后查即可,效率非常高。

十 并差集

并查集是一种数据结构,形象地来说,有点像“朋友圈”:A和B是朋友,A和B就构成了一个朋友圈,C和D是朋友,C和D也构成了一个朋友圈,那么这时,如果B和C在成为了朋友,A、B、C、D就构成了一个大的朋友圈。并查集就是可以描述这种情形的数据结构。
并查集是一种树型的数据结构 ,并查集可以高效地进行如下操作:
查询元素p和元素q是否属于同一组
合并元素p和元素q所在的组

image.png

10.1 简单并查集

并查集也是一种树型结构,但这棵树跟我们之前讲的二叉树、红黑树、B树等都不一样,这种树的要求比较简单:
1. 每个元素都唯一的对应一个结点;
2. 每一组数据中的多个元素都在同一颗树中;
3. 一个组中的数据对应的树和另外一个组中的数据对应的树之间没有任何联系;
4. 元素在树中并没有子父级关系的硬性要求;
image.png

10.1.1 实现原理

  1. 如果p和q已经在同一个分组中,则无需合并
    2. 如果p和q不在同一个分组,则只需要将p元素所在组的所有的元素的组标识符修改为q元素所在组的标识符即可
    3. 分组数量-1
    image.png

    10.1.2 代码实现

    ```java package cn.itcast.algorithm.uf; /**

    • @author liaozx
    • @date 2020-11-27 */ public class UF { //记录结点元素和该元素所在分组的标识 private int[] eleAndGroup; //记录并查集中数据的分组个数、即朋友圈的的数量 private int count;

      //初始化并查集 public UF(int current) { //初始化分组的数量,默认情况下,有N个分组 this.count = current; //初始化eleAndGroup数组 this.eleAndGroup = new int[current]; //初始化eleAndGroup中的元素及其所在的组的标识符,让eleAndGroup数组的索引作为并查集的每个结点的元素,并且让每个索引处的值(该元素所在的组的标识符)就是该索引 for (int i = 0; i < eleAndGroup.length; i++) {

      1. //表示每一个元素对应的一个组,每个人的圈子只有自己一个人
      2. eleAndGroup[i] = i;

      } }

      //获取当前并查集中的数据有多少个分组 public int count() { return count; }

      //元素p所在分组的标识符 private int find(int p) { return eleAndGroup[p]; }

      //判断并查集中元素p和元素q是否在同一分组中 public boolean connected(int p, int q) { return find(p) == find(q); }

      //把p元素所在分组和q元素所在分组合并 public void union(int p, int q) { //判断元素q和p是否已经在同一分组中,如果已经在同一分组中,则结束方法就可以了 if (connected(p, q)) {

      1. return;

      } //找到p所在分组的标识符 int pGroup = find(p); //找到q所在分组的标识符 int qGroup = find(q);

      //合并组:让p所在组的所有元素的组标识符变为q所在分组的标识符 for (int i = 0; i < eleAndGroup.length; i++) {

      1. if (eleAndGroup[i] == pGroup) {
      2. eleAndGroup[i] = qGroup;
      3. }

      } //分组个数-1 //朋友圈的数量减少1 this.count—;

      }

}

  1. 缺点:**Union合并朋友圈的操作复杂度为O(n),实在是太慢了**
  2. <a name="BZSop"></a>
  3. ## 10.2 树结构并查集
  4. 为了提升union算法的性能,我们需要重新设计find方法和union方法的实现,此时我们先需要对我们的之前数据结<br />构中的eleAndGourp数组的含义进行重新设定:<br />1.我们仍然让eleAndGroup数组的索引作为某个结点的元素;<br />2.eleAndGroup[i]的值不再是当前结点所在的分组标识,而是该结点的父结点;<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1502688/1600440636239-df75b881-fbff-44ce-9816-ac978f3e1ff2.png#align=left&display=inline&height=105&margin=%5Bobject%20Object%5D&name=image.png&originHeight=209&originWidth=459&size=15031&status=done&style=none&width=229.5)
  5. <a name="h5VVI"></a>
  6. ### 10.2.1 实现原理
  7. 1. 找到p元素所在树的根结点<br />2. 找到q元素所在树的根结点<br />3. 如果pq已经在同一个树中,则无需合并;<br />4. 如果pq不在同一个分组,则只需要将p元素所在树根结点的父结点设置为q元素的根结点即可;<br />5. 分组数量-1<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1502688/1600440680094-16c41188-ddde-4629-8588-3221dcc5c62e.png#align=left&display=inline&height=371&margin=%5Bobject%20Object%5D&name=image.png&originHeight=741&originWidth=469&size=40369&status=done&style=none&width=234.5)
  8. <a name="05Znb"></a>
  9. ### 10.2.2 代码实现
  10. ```java
  11. package com.ycc.data.structure.uf;
  12. /**
  13. * @author liaozx
  14. * @date 2020-11-27
  15. */
  16. public class UFTree {
  17. //记录结点元素和该元素所在分组的标识
  18. private final int[] eleAndGroup;
  19. //记录并查集中数据的分组个数
  20. private int count;
  21. //初始化并查集
  22. public UFTree(int N) {
  23. //初始化分组的数量,默认情况下,有N个分组
  24. this.count = N;
  25. //初始化eleAndGroup数组
  26. this.eleAndGroup = new int[N];
  27. //初始化eleAndGroup中的元素及其所在的组的标识符,让eleAndGroup数组的索引作为并查集的每个结点的元素,并且让每个索引处的值(该元素所在的组的标识符)就是该索引
  28. for (int i = 0; i < eleAndGroup.length; i++) {
  29. eleAndGroup[i] = i;
  30. }
  31. }
  32. //获取当前并查集中的数据有多少个分组
  33. public int count() {
  34. return count;
  35. }
  36. //判断并查集中元素p和元素q是否在同一分组中
  37. public boolean connected(int p, int q) {
  38. return find(p) == find(q);
  39. }
  40. //元素p所在分组的标识符
  41. public int find(int p) {
  42. while (true) {
  43. if (p == eleAndGroup[p]) {
  44. return p;
  45. }
  46. p = eleAndGroup[p];
  47. }
  48. }
  49. //把p元素所在分组和q元素所在分组合并
  50. public void union(int p, int q) {
  51. //找到p元素和q元素所在组对应的树的根结点
  52. int pRoot = find(p);
  53. int qRoot = find(q);
  54. //如果p和q已经在同一分组,则不需要合并了
  55. if (pRoot == qRoot) {
  56. return;
  57. }
  58. //让p所在的树的根结点的父结点为q所在树的根结点即可
  59. eleAndGroup[pRoot] = qRoot;
  60. //组的数量-1
  61. this.count--;
  62. }
  63. }

10.3 路径压缩

我们优化后的算法union,如果要把并查集中所有的数据连通,仍然至少要调用N-1次union方法,但是,我们发现union方法中已经没有了for循环,所以union算法的时间复杂度由O(N^2)变为了O(N)。但是这个算法仍然有问题,因为我们之前不仅修改了union算法,还修改了find算法。我们修改前的find算法的时间复杂度在任何情况下都为O(1),但修改后的find算法在最坏情况下是O(N):
image.png

10.3.1 实现原理

UF_Tree中最坏情况下union算法的时间复杂度为O(N^2),其最主要的问题在于最坏情况下,树的深度和数组的大小一样,如果我们能够通过一些算法让合并时,生成的树的深度尽可能的小,就可以优化find方法。之前我们在union算法中,合并树的时候将任意的一棵树连接到了另外一棵树,这种合并方法是比较暴力的,如果我们把并查集中每一棵树的大小记录下来,然后在每次合并树的时候,把较小的树连接到较大的树上,就可以减小树的深度。
image.png

10.3.2 代码实现

  1. package com.ycc.data.structure.uf;
  2. public class UFTreeWeighted {
  3. //记录结点元素和该元素所在分组的标识
  4. private final int[] eleAndGroup;
  5. //记录并查集中数据的分组个数
  6. private int count;
  7. //用来存储每一个根结点对应的树中保存的结点的个数
  8. private final int[] sz;
  9. //初始化并查集
  10. public UFTreeWeighted(int N) {
  11. //初始化分组的数量,默认情况下,有N个分组
  12. this.count = N;
  13. //初始化eleAndGroup数组
  14. this.eleAndGroup = new int[N];
  15. //初始化eleAndGroup中的元素及其所在的组的标识符,让eleAndGroup数组的索引作为并查集的每个结点的元素,并且让每个索引处的值(该元素所在的组的标识符)就是该索引
  16. for (int i = 0; i < eleAndGroup.length; i++) {
  17. eleAndGroup[i] = i;
  18. }
  19. this.sz = new int[N];
  20. //默认情况下,sz中每个索引处的值都是1
  21. for (int i = 0; i < sz.length; i++) {
  22. sz[i] = 1;
  23. }
  24. }
  25. //获取当前并查集中的数据有多少个分组
  26. public int count() {
  27. return count;
  28. }
  29. //判断并查集中元素p和元素q是否在同一分组中
  30. public boolean connected(int p, int q) {
  31. return find(p) == find(q);
  32. }
  33. //元素p所在分组的标识符
  34. public int find(int p) {
  35. while (true) {
  36. if (p == eleAndGroup[p]) {
  37. return p;
  38. }
  39. p = eleAndGroup[p];
  40. }
  41. }
  42. //把p元素所在分组和q元素所在分组合并
  43. public void union(int p, int q) {
  44. //找到p元素和q元素所在组对应的树的根结点
  45. int pRoot = find(p);
  46. int qRoot = find(q);
  47. //如果p和q已经在同一分组,则不需要合并了
  48. if (pRoot == qRoot) {
  49. return;
  50. }
  51. //判断proot对应的树大还是qroot对应的树大,最终需要把较小的树合并到较大的树中
  52. if (sz[pRoot] < sz[qRoot]) {
  53. eleAndGroup[pRoot] = qRoot;
  54. sz[qRoot] += sz[pRoot];
  55. } else {
  56. eleAndGroup[qRoot] = pRoot;
  57. sz[pRoot] += sz[qRoot];
  58. }
  59. //组的数量-1
  60. this.count--;
  61. }
  62. }