尚硅谷图解Java数据结构和算法.pdf
图解.xlsx

1、二叉树

为什么需要树

  • 数组的查找效率高,但是插入效率低。
  • 链表的插入效率高,查找效率低。

需要一种数据结构来平衡查找与插入效率,使得查找速度和插入速度都能得到提升,因此有了树这种数据结构。
image.pngimage.png

树的基本概念

八、树结构 - 图3

二叉树的基本概念

每个节点最多只能有两个子节点的一种形式称为二叉树
八、树结构 - 图4
八、树结构 - 图5
八、树结构 - 图6

满二叉树

如果该二叉树的所有叶子节点都在最后一层,并且结点总数= 2^n -1 , n为层数,则我们称为满二叉树。
八、树结构 - 图72

完全二叉树

如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树
八、树结构 - 图8
八、树结构 - 图9
image.png

二叉树的遍历

前序遍历

先遍历父节点,再遍历左子节点,最后遍历右子节点

中序遍历

先遍历左子节点,再遍历父节点,最后遍历右子节点

后序遍历

先遍历左子节点,再遍历右子节点,最后遍历父节点
可以看出,前中后的区别在于父节点遍历的时机
image.pngimage.png
前序遍历结果:1、2、3、4
中序遍历结果:2、1、3、4
后序遍历结果:2、4、3、1

实际代码

此代码使用手动的方式创建二叉树,使用递归的方式遍历二叉树

  1. package Tree;
  2. /**
  3. * @author Voyager1
  4. * @create 2021-10-07 20:42
  5. */
  6. public class BinaryTreeDemo {
  7. public static void main(String[] args) {
  8. //创建二叉树
  9. BinaryTree binaryTree = new BinaryTree();
  10. HeroNode root = new HeroNode(1,"宋江");
  11. HeroNode node2 = new HeroNode(2, "吴用");
  12. HeroNode node3 = new HeroNode(3, "卢俊义");
  13. HeroNode node4 = new HeroNode(4, "林冲");
  14. HeroNode node5 = new HeroNode(5,"关胜");
  15. //说明,我们先手动创建该二叉树,后面我们学习递归的方式创建二叉树
  16. root.setLeft(node2);
  17. root.setRight(node3);
  18. node3.setLeft(node5);
  19. node3.setRight(node4);
  20. binaryTree.setRoot(root);
  21. //测试前序遍历
  22. System.out.println("前序遍历:" );//1,2,3,5,4
  23. binaryTree.preOrder();
  24. //测试中序遍历
  25. System.out.println("中序遍历:");//2,1,5,3,4
  26. binaryTree.infixOrder();
  27. //测试后序遍历
  28. System.out.println("后序遍历:");//2,5,4,3,1
  29. binaryTree.postOrder();
  30. }
  31. static class BinaryTree{
  32. private HeroNode root;
  33. public void setRoot(HeroNode root) {
  34. this.root = root;
  35. }
  36. //前序遍历
  37. public void preOrder(){
  38. if (this.root != null){
  39. this.root.preOrder();
  40. }else {
  41. System.out.println("二叉树为空,无法遍历");
  42. }
  43. }
  44. //中序遍历
  45. public void infixOrder(){
  46. if (this.root != null){
  47. this.root.infixOrder();
  48. }else{
  49. System.out.println("二叉树为空,无法遍历");
  50. }
  51. }
  52. //后序遍历
  53. public void postOrder(){
  54. if (this.root != null){
  55. this.root.postOrder();
  56. }else {
  57. System.out.println("二叉树为空,无法遍历");
  58. }
  59. }
  60. }
  61. //先创建HeroNode 节点
  62. static class HeroNode {
  63. private int no;
  64. private String name;
  65. private HeroNode left;
  66. private HeroNode right;
  67. public HeroNode(int no,String name){
  68. this.no = no;
  69. this.name = name;
  70. }
  71. public int getNo() {
  72. return no;
  73. }
  74. public void setNo(int no) {
  75. this.no = no;
  76. }
  77. public String getName() {
  78. return name;
  79. }
  80. public void setName(String name) {
  81. this.name = name;
  82. }
  83. public HeroNode getLeft() {
  84. return left;
  85. }
  86. public void setLeft(HeroNode left) {
  87. this.left = left;
  88. }
  89. public HeroNode getRight() {
  90. return right;
  91. }
  92. public void setRight(HeroNode right) {
  93. this.right = right;
  94. }
  95. @Override
  96. public String toString() {
  97. return "HeroNode{" +
  98. "no=" + no +
  99. ", name='" + name + '\'' +
  100. '}';
  101. }
  102. //前序遍历
  103. public void preOrder(){
  104. System.out.println(this);//先输出父结点
  105. if (this.left != null){
  106. this.left.preOrder();
  107. }
  108. if(this.right != null){
  109. this.right.preOrder();
  110. }
  111. }
  112. //中序遍历
  113. public void infixOrder(){
  114. if(this.left != null){
  115. this.left.infixOrder();
  116. }
  117. System.out.println(this);
  118. if (this.right != null){
  119. this.right.infixOrder();
  120. }
  121. }
  122. //后序遍历
  123. public void postOrder(){
  124. if (this.left != null){
  125. this.left.postOrder();
  126. }
  127. if (this.right != null){
  128. this.right.postOrder();
  129. }
  130. System.out.println(this);
  131. }
  132. }
  133. }

迭代实现

使用迭代对二叉树进行遍历与递归类似,不过需要自己维护一个用于存放节点

  1. public class Demo5 {
  2. public static void main(String[] args) {
  3. TreeNode node1 = new TreeNode(1);
  4. TreeNode node2 = new TreeNode(2);
  5. TreeNode node3 = new TreeNode(3);
  6. node1.left = node2;
  7. node1.right = node3;
  8. List<Integer> integers = preTraverse(node1);
  9. System.out.println("前序遍历结果");
  10. for (Integer integer : integers) {
  11. System.out.print(integer);
  12. System.out.print(" ");
  13. }
  14. System.out.println();
  15. List<Integer> integers2 = midTraverse(node1);
  16. System.out.println("中遍历结果");
  17. for (Integer integer : integers2) {
  18. System.out.print(integer);
  19. System.out.print(" ");
  20. }
  21. System.out.println();
  22. List<Integer> integers3 = lastTraverse(node1);
  23. System.out.println("后遍历结果");
  24. for (Integer integer : integers3) {
  25. System.out.print(integer);
  26. System.out.print(" ");
  27. }
  28. System.out.println();
  29. }
  30. /**
  31. * 使用迭代法对二叉树进行前序遍历
  32. * @param root 二叉树根节点
  33. * @return 遍历后的集合
  34. */
  35. public static List<Integer> preTraverse(TreeNode root) {
  36. // 用于存放结果的集合
  37. List<Integer> result = new ArrayList<>();
  38. if (root == null) {
  39. return result;
  40. }
  41. // 栈,用于存放遍历的节点
  42. Stack<TreeNode> stack = new Stack<>();
  43. stack.push(root);
  44. // 遍历二叉树
  45. while (!stack.isEmpty()) {
  46. // 栈顶元素出栈,并放入集合中
  47. root = stack.pop();
  48. result.add(root.val);
  49. // 先遍历右子树,将其压栈
  50. if (root.right != null) {
  51. stack.push(root.right);
  52. }
  53. // 遍历左子树,压栈
  54. if (root.left != null) {
  55. stack.push(root.left);
  56. }
  57. }
  58. return result;
  59. }
  60. /**
  61. * 使用迭代法对二叉树进行中序遍历
  62. * @param root 二叉树根节点
  63. * @return 中序遍历结果集合
  64. */
  65. public static List<Integer> midTraverse(TreeNode root) {
  66. List<Integer> result = new ArrayList<>();
  67. if (root == null) {
  68. return result;
  69. }
  70. Stack<TreeNode> stack = new Stack<>();
  71. while (root != null || !stack.isEmpty()) {
  72. // 节点压栈,并遍历其左子树
  73. while (root != null) {
  74. stack.push(root);
  75. root = root.left;
  76. }
  77. // 栈顶元素出栈,放入结果集合
  78. root = stack.pop();
  79. result.add(root.val);
  80. // 遍历该节点的右子树
  81. root = root.right;
  82. }
  83. return result;
  84. }
  85. /**
  86. * 使用迭代法的后序遍历
  87. * @param root 二叉树根节点
  88. * @return 后序遍历集合
  89. */
  90. public static List<Integer> lastTraverse(TreeNode root) {
  91. List<Integer> result = new ArrayList<>();
  92. if (root == null) {
  93. return result;
  94. }
  95. Stack<TreeNode> stack = new Stack<>();
  96. // 保存放入集合的右子树,避免重复放入
  97. TreeNode pre = null;
  98. while (root != null || !stack.isEmpty()) {
  99. while (root != null) {
  100. stack.push(root);
  101. root = root.left;
  102. }
  103. // 获取栈顶元素
  104. root = stack.pop();
  105. // 如果该元素没有右子树,或者右子树已近被遍历过了,就放入集合
  106. if (root.right == null || root.right == pre) {
  107. result.add(root.val);
  108. pre = root;
  109. root = null;
  110. } else {
  111. // 否则就继续遍历该节点的右子树
  112. stack.push(root);
  113. root = root.right;
  114. }
  115. }
  116. return result;
  117. }
  118. }
  119. 运行结果
  120. 前序遍历结果
  121. 1 2 3
  122. 中遍历结果
  123. 2 1 3
  124. 后遍历结果
  125. 2 3 1

二叉树的查找

前、中、后序查找的思路与遍历相似,当找到对应的元素时,直接返回即可。
image.png

实现代码

  1. package Tree;
  2. /**
  3. * @author Voyager1
  4. * @create 2021-10-07 20:42
  5. */
  6. public class BinaryTreeDemo {
  7. public static void main(String[] args) {
  8. //创建二叉树
  9. BinaryTree binaryTree = new BinaryTree();
  10. HeroNode root = new HeroNode(1,"宋江");
  11. HeroNode node2 = new HeroNode(2, "吴用");
  12. HeroNode node3 = new HeroNode(3, "卢俊义");
  13. HeroNode node4 = new HeroNode(4, "林冲");
  14. HeroNode node5 = new HeroNode(5,"关胜");
  15. //说明,我们先手动创建该二叉树,后面我们学习递归的方式创建二叉树
  16. root.setLeft(node2);
  17. root.setRight(node3);
  18. node3.setLeft(node5);
  19. node3.setRight(node4);
  20. binaryTree.setRoot(root);
  21. //测试前序遍历
  22. System.out.println("前序遍历:" );//1,2,3,5,4
  23. binaryTree.preOrder();
  24. //测试中序遍历
  25. System.out.println("中序遍历:");//2,1,5,3,4
  26. binaryTree.infixOrder();
  27. //测试后序遍历
  28. System.out.println("后序遍历:");//2,5,4,3,1
  29. binaryTree.postOrder();
  30. System.out.println("###################################");
  31. //测试前序查找
  32. binaryTree.preOrderSearch(5);//4次
  33. //中序查找
  34. binaryTree.infixOrderSearch(5);//3次
  35. //后序查找
  36. binaryTree.postOrderSearch(5);//2次
  37. }
  38. static class BinaryTree{
  39. private HeroNode root;
  40. public void setRoot(HeroNode root) {
  41. this.root = root;
  42. }
  43. //前序遍历
  44. public void preOrder(){
  45. if (this.root != null){
  46. this.root.preOrder();
  47. }else {
  48. System.out.println("二叉树为空,无法遍历");
  49. }
  50. }
  51. //中序遍历
  52. public void infixOrder(){
  53. if (this.root != null){
  54. this.root.infixOrder();
  55. }else{
  56. System.out.println("二叉树为空,无法遍历");
  57. }
  58. }
  59. //后序遍历
  60. public void postOrder(){
  61. if (this.root != null){
  62. this.root.postOrder();
  63. }else {
  64. System.out.println("二叉树为空,无法遍历");
  65. }
  66. }
  67. //前序查找
  68. public void preOrderSearch(int no){
  69. System.out.println("前序查找:");
  70. HeroNode resNode = null;
  71. if (root != null){
  72. resNode = root.preOrderSearch(no);
  73. }else {
  74. System.out.println("未找到该元素");
  75. return;
  76. }
  77. if(resNode != null){
  78. System.out.println(resNode);
  79. System.out.println();
  80. }else {
  81. System.out.println("未找到该元素");
  82. System.out.println();
  83. }
  84. }
  85. //中序查找
  86. public void infixOrderSearch(int no){
  87. System.out.println("中序查找:");
  88. HeroNode resNode = null;
  89. if (root != null){
  90. resNode = root.infixOrderSearch(no);
  91. }else {
  92. System.out.println("未找到该元素");
  93. return;
  94. }
  95. if(resNode != null){
  96. System.out.println(resNode);
  97. System.out.println();
  98. }else {
  99. System.out.println("未找到该元素");
  100. System.out.println();
  101. }
  102. }
  103. //后续查找
  104. public void postOrderSearch(int no){
  105. System.out.println("后序查找:");
  106. HeroNode resNode = null;
  107. if (root != null){
  108. resNode = root.postOrderSearch(no);
  109. }else {
  110. System.out.println("未找到该元素");
  111. return;
  112. }
  113. if(resNode != null){
  114. System.out.println(resNode);
  115. System.out.println();
  116. }else {
  117. System.out.println("未找到该元素");
  118. System.out.println();
  119. }
  120. }
  121. }
  122. //先创建HeroNode 节点
  123. static class HeroNode {
  124. private int no;
  125. private String name;
  126. private HeroNode left;
  127. private HeroNode right;
  128. public HeroNode(int no,String name){
  129. this.no = no;
  130. this.name = name;
  131. }
  132. public int getNo() {
  133. return no;
  134. }
  135. public void setNo(int no) {
  136. this.no = no;
  137. }
  138. public String getName() {
  139. return name;
  140. }
  141. public void setName(String name) {
  142. this.name = name;
  143. }
  144. public HeroNode getLeft() {
  145. return left;
  146. }
  147. public void setLeft(HeroNode left) {
  148. this.left = left;
  149. }
  150. public HeroNode getRight() {
  151. return right;
  152. }
  153. public void setRight(HeroNode right) {
  154. this.right = right;
  155. }
  156. @Override
  157. public String toString() {
  158. return "HeroNode{" +
  159. "no=" + no +
  160. ", name='" + name + '\'' +
  161. '}';
  162. }
  163. //前序遍历
  164. public void preOrder(){
  165. System.out.println(this);//先输出父结点
  166. if (this.left != null){
  167. this.left.preOrder();
  168. }
  169. if(this.right != null){
  170. this.right.preOrder();
  171. }
  172. }
  173. //中序遍历
  174. public void infixOrder(){
  175. if(this.left != null){
  176. this.left.infixOrder();
  177. }
  178. System.out.println(this);
  179. if (this.right != null){
  180. this.right.infixOrder();
  181. }
  182. }
  183. //后序遍历
  184. public void postOrder(){
  185. if (this.left != null){
  186. this.left.postOrder();
  187. }
  188. if (this.right != null){
  189. this.right.postOrder();
  190. }
  191. System.out.println(this);
  192. }
  193. //前序遍历查找
  194. public HeroNode preOrderSearch(int no){
  195. System.out.println("进入前序遍历");
  196. //比较当前结点是不是,如果找到了,就返回
  197. if (this.no == no){
  198. return this;
  199. }
  200. //在左子树中查找,如果找到了就返回
  201. HeroNode resNode = null;
  202. if (this.left != null){
  203. resNode = this.left.preOrderSearch(no);
  204. }
  205. if (resNode != null){
  206. return resNode;
  207. }
  208. //在右子树中查找,无论是否找到,都需要返回
  209. if (this.right != null){
  210. resNode = this.right.preOrderSearch(no);
  211. }
  212. return resNode;
  213. }
  214. //中序遍历查找
  215. public HeroNode infixOrderSearch(int no){
  216. HeroNode resNode = null;
  217. if (this.left != null){
  218. resNode = this.left.infixOrderSearch(no);
  219. }
  220. if (resNode != null){
  221. return resNode;
  222. }
  223. System.out.println("进入中序遍历");
  224. if (this.no == no){
  225. return this;
  226. }
  227. if (this.right != null){
  228. resNode = this.right.infixOrderSearch(no);
  229. }
  230. return resNode;
  231. }
  232. //后序遍历查找
  233. public HeroNode postOrderSearch(int no){
  234. HeroNode resNode = null;
  235. if (this.left != null){
  236. resNode = this.left.postOrderSearch(no);
  237. }
  238. if (resNode != null){
  239. return resNode;
  240. }
  241. if (this.right != null){
  242. resNode = this.right.postOrderSearch(no);
  243. }
  244. if(resNode != null) {
  245. return resNode;
  246. }
  247. System.out.println("进入后序遍历");
  248. if (this.no == no){
  249. return this;
  250. }
  251. return resNode;
  252. }
  253. }
  254. }

二叉树的删除

删除要求

  • 如果删除的是叶子节点,则直接删除即可
  • 如果删除的是非叶子节点,则删除该子树

    删除思路

  • 因为我们的二叉树是单向的,所以我们是判断当前结点的子结点是否需要删除结点,而不能去判断当前这个结点是不是需要删除结点

  • 如果当前结点的左子结点不为空,并且左子结点就是要删除结点,就将 this.left = null; 并且就返回 (结束递归删除)
  • 如果当前结点的右子结点不为空,并且右子结点就是要删除结点,就将 this.right= null ;并且就返回 (结束递归删除)
  • 如果第2和第3步没有删除结点,那么我们就需要向左子树进行递归删除
  • 如果 4步也没有删除结点,则应当向右子树进行递归删除

image.png

实现代码

  1. public class Demo3 {
  2. public static void main(String[] args) {
  3. //创建根节点
  4. BinaryDeleteTree deleteTree = new BinaryDeleteTree();
  5. //手动创建节点
  6. StudentNode student1 = new StudentNode(1, "A");
  7. StudentNode student2 = new StudentNode(2, "B");
  8. StudentNode student3 = new StudentNode(3, "C");
  9. StudentNode student4 = new StudentNode(4, "D");
  10. StudentNode student5 = new StudentNode(5, "E");
  11. student1.setLeft(student2);
  12. student1.setRight(student3);
  13. student2.setLeft(student4);
  14. student3.setRight(student5);
  15. //指定根节点
  16. deleteTree.setRoot(student1);
  17. //删除节点
  18. deleteTree.deleteNode(3);
  19. }
  20. }
  21. class BinaryDeleteTree {
  22. StudentNode root;
  23. public void setRoot(StudentNode root) {
  24. this.root = root;
  25. }
  26. /**
  27. * 删除节点
  28. * @param id 删除节点的id
  29. */
  30. public void deleteNode(int id) {
  31. System.out.println("删除节点");
  32. if(root.id == id) {
  33. root = null;
  34. System.out.println("根节点被删除");
  35. return;
  36. }
  37. //调用删除方法
  38. root.deleteNode(id);
  39. }
  40. }
  41. class StudentNode {
  42. int id;
  43. String name;
  44. StudentNode left;
  45. StudentNode right;
  46. public StudentNode(int id, String name) {
  47. this.id = id;
  48. this.name = name;
  49. }
  50. public int getId() {
  51. return id;
  52. }
  53. public void setId(int id) {
  54. this.id = id;
  55. }
  56. public String getName() {
  57. return name;
  58. }
  59. public void setName(String name) {
  60. this.name = name;
  61. }
  62. public StudentNode getLeft() {
  63. return left;
  64. }
  65. public void setLeft(StudentNode left) {
  66. this.left = left;
  67. }
  68. public StudentNode getRight() {
  69. return right;
  70. }
  71. public void setRight(StudentNode right) {
  72. this.right = right;
  73. }
  74. @Override
  75. public String toString() {
  76. return "StudentNode{" +
  77. "id=" + id +
  78. ", name='" + name + '\'' +
  79. '}';
  80. }
  81. /**
  82. * 删除节点
  83. * @param id 删除节点的id
  84. */
  85. public void deleteNode(int id) {
  86. //如果左子树不为空且是要查找的节点,就删除
  87. if(left != null && left.id == id) {
  88. left = null;
  89. System.out.println("删除成功");
  90. return;
  91. }
  92. //如果右子树不为空且是要查找的节点,就删除
  93. if(right != null && right.id == id) {
  94. right = null;
  95. System.out.println("删除成功");
  96. return;
  97. }
  98. //左递归,继续查找
  99. if(left != null) {
  100. left.deleteNode(id);
  101. }
  102. //右递归,继续查找
  103. if(right != null) {
  104. right.deleteNode(id);
  105. }
  106. }
  107. }

顺序存储二叉树

基本说明

从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树树也可以转换成数组
八、树结构 - 图15

特点

  • 顺序二叉树通常只考虑完全二叉树
  • 第 n个元素的子节点为 2 × n + 1
  • 第 n个元素的子节点为 2 × n + 2
  • 第 n个元素的节点为 (n-1) ÷2
    • 其中n 表示二叉树中的第几个元素(从0开始编号)

遍历过程和二叉树的遍历类似,只不过递归的条件有所不同
image.png

实现代码

package Tree;

/**
 * @author Voyager1
 * @create 2021-10-08 11:06
 */
public class ArrBinaryTreeDemo {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5, 6, 7};
        ArrBinaryTree arrBinaryTree = new ArrBinaryTree(arr);

        //前序遍历
        System.out.println("前序遍历");
        arrBinaryTree.preOrder();
        //中序遍历
        System.out.println("中序遍历");
        arrBinaryTree.infixOrder();
        //后序遍历
        System.out.println("后序遍历");
        arrBinaryTree.postOrder();
    }

    static class ArrBinaryTree{
        private int[] arr;

        public ArrBinaryTree(int[] arr) {
            this.arr = arr;
        }

        public void preOrder(){
            this.preOrder(0);
        }
        /**
         * 编写一个方法,完成顺序存储二叉树的前序遍历
         * @param index 数组的下标
         */
        public void preOrder(int index){
            if(arr == null || arr.length == 0){
                System.out.println("数组为空,不能按照二叉树的前序遍历");
            }
            //输出当前这个元素
            System.out.println(arr[index]);
            if ((2*index + 1) < arr.length){
                preOrder(2*index + 1);
            }
            if ((2*index + 2) < arr.length){
                preOrder(2*index + 2);
            }
        }
        public void infixOrder(){
            this.infixOrder(0);
        }
        /**
         * 中序遍历
         * @param index 数组下标
         */
        public void infixOrder(int index){
            if(arr == null || arr.length == 0){
                System.out.println("数组为空,不能按照二叉树的前序遍历");
            }

            if ((2*index + 1) < arr.length){
                infixOrder(2*index + 1);
            }
            //输出当前这个元素
            System.out.println(arr[index]);
            if ((2*index + 2) < arr.length){
                infixOrder(2*index + 2);
            }
        }
        public void postOrder(){
            this.postOrder(0);
        }
        /**
         * 后序遍历
         * @param index
         */
        public void postOrder(int index){
            if(arr == null || arr.length == 0){
                System.out.println("数组为空,不能按照二叉树的前序遍历");
            }
            if ((2*index + 1) < arr.length){
                postOrder(2*index + 1);
            }
            if ((2*index + 2) < arr.length){
                postOrder(2*index + 2);
            }
            //输出当前这个元素
            System.out.println(arr[index]);
        }
    }
}
运行结果
数组前序遍历
1 2 4 5 3 6 7 
数组中序遍历
4 2 5 1 6 3 7 
数组后序遍历
4 5 2 6 7 3 1

线索化二叉树

基本概念

因为一般的二叉树,叶子节点的左右指针都为空,这样就会造成空间的浪费。为了减少浪费,便有了线索化二叉树

  • n个结点的二叉链表中含有 n+1 【公式 2n-(n-1)=n+1】个空指针域。利用二叉链表中的空指针域,存放指向该结点在某种遍历次序下的前驱和后继结点的指针
  • 这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树
  • 根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种
  • 如果一个节点已经有了左右孩子,那么该节点就不能被线索化了,所以线索化二叉树后,节点的left和right有如下两种情况
    • left可能指向的是左孩子,也可能指向的是前驱节点
    • right可能指向的是右孩子,也可能指向的是后继节点

image.png
image.png

图解

中序线索化前
八、树结构 - 图19
中序线索化后
八、树结构 - 图20

实现代码

线索化思路

  • 每个节点需要用两个变量来表示左右指针的类型(保存左右子树,还是前驱后继)
  • 需要用两个变量来表示当前节点和当前节点的前驱节点
  • 通过将当前节点的左指针指向前驱节点,来实现前驱节点的绑定
  • 通过将前驱节点的右指针指向当前节点,来实现后继节点的绑定

遍历方式

  • 各个节点可以通过线性的方式遍历,无需使用递归的方式遍历
  • 首先有一个外循环,代替递归操作,循环条件的暂存节点不为空
  • 第一个内循环用于找到第一个元素,然后打印
  • 第二个循环用于找到节点的后继元素
  • 最后将暂存节点令为右孩子

image.png
image.png

public class Demo1 {
    public static void main(String[] args) {
        //初始化节点
        Student student1 = new Student(1, "1");
        Student student2 = new Student(2, "3");
        Student student3 = new Student(3, "6");
        Student student4 = new Student(4, "8");
        Student student5 = new Student(5, "10");
        Student student6 = new Student(6, "14");

        //手动创建二叉树
        ThreadedBinaryTree tree = new ThreadedBinaryTree();
        student1.setLeft(student2);
        student1.setRight(student3);
        student2.setLeft(student4);
        student2.setRight(student5);
        student3.setLeft(student6);

        tree.setRoot(student1);

        tree.midThreaded();

        //得到第五个节点的前驱节点和后继节点
        System.out.println("第五个节点的前驱节点和后继节点");
        System.out.println(student5.getLeft());
        System.out.println(student5.getRight());

        //遍历线索化二叉树
        System.out.println("遍历线索化二叉树");
        tree.midThreadedTraverse();

    }
}

class ThreadedBinaryTree {
    private Student root;
    /**
     * 指向当前节点的前一个节点
     */
    private Student pre;

    public void setRoot(Student root) {
        this.root = root;
    }

    /**
     * 中序线索化
     * @param node 当前节点
     */
    private void midThreaded(Student node) {
        if(node == null) {
            return;
        }
        //左线索化
        midThreaded(node.getLeft());

        //线索化当前节点
        //如果当前节点的左指针为空,就指向前驱节点,并改变左指针类型
        if(node.getLeft() == null) {
            node.setLeft(pre);
            node.setLeftType(1);
        }
        //通过前驱节点来将右指针的值令为后继节点
        if(pre != null && pre.getRight() == null) {
            pre.setRight(node);
            pre.setRightType(1);
        }

        //处理一个节点后,让当前节点变为下一个节点的前驱节点
        pre = node;

        //右线索化
        midThreaded(node.getRight());
    }

    public void midThreaded() {
        midThreaded(root);
    }

    /**
     * 遍历线索化后的二叉树
     */
    public void midThreadedTraverse() {
        //暂存遍历到的节点
        Student tempNode = root;
        //非递归的方法遍历,如果tempNode不为空就一直循环
        while(tempNode != null) {
            //一直访问二叉树的左子树,直到某个节点的左子树指向前驱节点
            while(tempNode.getLeftType() != 1) {
                tempNode = tempNode.getLeft();
            }
            //找到了第一个节点
            System.out.println(tempNode);
            //再访问该节点的右子树,看是否保存了后继节点
            //如果是,则打印该节点的后继节点信息
            while(tempNode.getRightType() == 1) {
                tempNode = tempNode.getRight();
                System.out.println(tempNode);
            }

            tempNode = tempNode.getRight();
        }

    }
}

class Student {
    private int id;
    private String name;
    private Student left;
    private Student right;
    /**
     * 左、右指针的类型,0-->指向的是左右孩子,1-->指向的是前驱、后续节点
     */
    private int leftType = 0;
    private int rightType = 0;

    public Student(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Student getLeft() {
        return left;
    }

    public void setLeft(Student left) {
        this.left = left;
    }

    public Student getRight() {
        return right;
    }

    public void setRight(Student right) {
        this.right = right;
    }

    public int getLeftType() {
        return leftType;
    }

    public void setLeftType(int leftType) {
        this.leftType = leftType;
    }

    public int getRightType() {
        return rightType;
    }

    public void setRightType(int rightType) {
        this.rightType = rightType;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}
运行结果

第五个节点的前驱节点和后继节点
Student{id=2, name='3'}
Student{id=1, name='1'}
遍历线索化二叉树
Student{id=4, name='8'}
Student{id=2, name='3'}
Student{id=5, name='10'}
Student{id=1, name='1'}
Student{id=6, name='14'}
Student{id=3, name='6'}

2、树的应用

堆排序

详见排序算法——堆排序
image.pngimage.png
image.pngimage.png
image.png
image.png
image.png

package Tree;
import java.util.Arrays;
/**
 * @author Voyager1
 * @create 2021-10-11 16:39
 */
public class HeapSort {
    public static void main(String[] args) {
        int[] arr = {4, 6, 8, 5, 9};
        heapSort(arr);
        System.out.println("排序后=" + Arrays.toString(arr));
    }
    //堆排序方法
    public static void heapSort(int arr[]){
        int temp = 0;
        System.out.println("堆排序");
        //将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆
        for (int i = arr.length/2 -1;i >= 0;i--){
            adjustHeap(arr,i, arr.length);
        }
        /*
         * 2).将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
           3).重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,
         *    反复执行调整+交换步骤,直到整个序列有序。
         */
        for (int j = arr.length - 1; j > 0; j--){
            //交换
            temp = arr[j];
            arr[j] = arr[0];
            arr[0] = temp;
            adjustHeap(arr,0,j);
        }
    }
    /**
     *  功能: 完成 将 以 i 对应的非叶子结点的树调整成大顶堆
     * @param arr 待调整的数组
     * @param i   表示非叶子结点在数组中索引
     * @param length 表示对多少个元素继续调整, length 是在逐渐的减少
     */
    public static void adjustHeap(int arr[],int i,int length){
        int temp = arr[i];
        //开始调整
        //说明
        //1. k = i * 2 + 1 k 是 i结点的左子结点
        for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
            if (k + 1 <length && arr[k] < arr[k + 1]){//说明左子结点的值小于右子结点的值
                k++;
            }
            if (arr[k] > temp){//如果子结点大于父结点
                arr[i] = arr[k];//把较大的值赋给当前结点
                i = k;//!!! i 指向 k,继续循环比较
            }else {
                break;
            }
        }
        //当for 循环结束后,我们已经将以i 为父结点的树的最大值,放在了 最顶(局部)
        arr[i] = temp;//将temp值放到调整后的位置
    }
}

堆排序的时间复杂度为O(nlogn) 8w 随机数的排序 秒杀 80w依旧秒杀 800w 4秒

哈夫曼树

八、树结构 - 图30

基本介绍

  • 给定 n个权值作为 n个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)
  • 哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近

    重要概念

  • 路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为 L-1

  • 结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权
    • 结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积(W×L)
  • 树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和(W1×L1+W2×L2…),记为WPL(weighted pathlength) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。
  • WPL最小的就是哈夫曼树

    创建思路及图解

    创建思路

  • 从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树

  • 取出根节点权值最小的两颗二叉树
  • 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
  • 再将这颗新的二叉树,以根节点的权值大小 再次排序,不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树

图解
以{3, 6, 7, 1, 8, 29, 13}为例
首先排序:{1, 3, 6, 7, 8, 13, 29}
八、树结构 - 图31
八、树结构 - 图32
八、树结构 - 图33
八、树结构 - 图34
八、树结构 - 图35
八、树结构 - 图36

实现代码

public class Demo1 {
   public static void main(String[] args) {
      int[] arr = {3, 6, 7, 1, 8, 29, 13};
      HuffmanTree huffmanTree = new HuffmanTree();
      Node root = huffmanTree.createHuffmanTree(arr);
      root.preTraverse();
   }
}

class HuffmanTree {

   public Node createHuffmanTree(int[] arr) {
      //创建数组用于存放Node
      ArrayList<Node> nodes = new ArrayList<>(arr.length);
      for(int value : arr) {
         nodes.add(new Node(value));
      }
      //对集合中的元素进行排序
      Collections.sort(nodes);

      while(nodes.size() > 1) {
         //左右子树在集合中对应的下标
         int leftIndex = 0;
         int rightIndex = 1;

         //取出最小的两个节点
         Node leftNode = nodes.get(leftIndex);
         Node rightNode = nodes.get(rightIndex);
         //创建父节点,并创建左右子树
         Node parent = new Node(leftNode.value + rightNode.value);
         parent.left = leftNode;
         parent.right = rightNode;
         //从集合中移除两个最小的节点,并将父节点放入集合中
         nodes.add(parent);
         nodes.remove(leftNode);
         nodes.remove(rightNode);
         //再次比较
         Collections.sort(nodes);
      }
      //返回根节点
      return nodes.get(0);
   }
}

class Node implements Comparable<Node> {
   int value;
   Node left;
   Node right;

   public Node(int value) {
      this.value = value;
   }

   @Override
   public String toString() {
      return "Node{" +
            "value=" + value +
            '}';
   }

   /**
    * 重写比较函数,用于排序
    */
   @Override
   public int compareTo(Node o) {
      return value - o.value;
   }

   public void preTraverse() {
      System.out.print(this.value + " ");
      if (this.left != null) {
         this.left.preTraverse();
      }
      if (this.right != null) {
         this.right.preTraverse();
      }
   }
}
运行结果
前序遍历哈夫曼树
67 29 38 15 7 8 23 10 4 1 3 6 13

哈夫曼编码

原理及图解

前缀编码:任何一个字符的编码,都不会是其它的字符的前缀

  • 统计需编码的字符串中,各个字符出现的次数:如helloworld
    • h:1 e:1 w:1 r:1 d:1 o:2 l:3
  • 将字符出现的次数作为权值,构建哈弗曼树。如下图

八、树结构 - 图37

  • 根据哈弗曼树进行编码,向左的路径为0,向右的路径为1
    • 字符编码结果为:h:000 e:001 w:100 d:1010 r:1011 l:11 o:01
    • 字符串编码结果为:000001111101100011011111010

      实现代码

      此处代码只实现了哈弗曼树的创建与每个字符的编码 ```java public class Demo2 { public static void main(String[] args) { String str = “helloworld”; //哈夫曼编码 HuffmanCode huffmanCode = new HuffmanCode(); ArrayList list = huffmanCode.getList(str); //构建哈弗曼树 Code root = huffmanCode.createHuffmanTree(list); System.out.println(“前序遍历哈弗曼树”); root.preTraverse(); //进行哈弗曼编码 Map codeMap = root.getCodeMap(); System.out.println(“哈弗曼编码”); System.out.println(codeMap); } }

class HuffmanCode { public ArrayList getList(String codes) { //得到字符串对应的字节数组 byte[] byteCodes = codes.getBytes();

  //创建哈希表,用于存放数据及其权值(出现次数)
  Map<Byte, Integer> dataAndWight = new HashMap<>();
  for(byte b : byteCodes) {
     Integer wight = dataAndWight.get(b);
     //如果还没有该数据,就创建并让其权值为1
     if(dataAndWight.get(b) == null) {
        dataAndWight.put(b, 1);
     }else {
        //如果已经有了该数据,就让其权值加一
        dataAndWight.put(b, wight+1);
     }
  }

  //创建List,用于返回
  ArrayList<Code> list = new ArrayList<>();
  //遍历哈希表,放入List集合中
  for(Map.Entry<Byte, Integer> entry : dataAndWight.entrySet()) {
     list.add(new Code(entry.getKey(), entry.getValue()));
  }
  return list;

}

public Code createHuffmanTree(ArrayList lists) { int leftIndex = 0; int rightIndex = 1; //根据权值进行排序 Collections.sort(lists);

  while (lists.size() > 1) {
     Code leftCode = lists.get(leftIndex);
     Code rightCode = lists.get(rightIndex);
     Code parent = new Code(null,leftCode.weight + rightCode.weight);
     parent.left = leftCode;
     parent.right = rightCode;
     lists.add(parent);
     lists.remove(leftCode);
     lists.remove(rightCode);
     //再次排序
     Collections.sort(lists);
  }
  return lists.get(0);

} }

class Code implements Comparable { Byte data; int weight; Code left; Code right; private Map codeMap = new HashMap<>();

public Code(Byte data, int weight) { this.data = data; this.weight = weight; }

@Override public String toString() { return “Code{“ + “data=” + data + “, weight=” + weight + ‘}’; }

public void preTraverse() { System.out.println(this); if(left != null) { left.preTraverse(); } if(right != null) { right.preTraverse(); } }

public Map getCodeMap() { return getCode(this, “”, new StringBuilder()); }

/**

* 对哈弗曼树中的叶子节点进行编码
* @param node 根节点
* @param code 左子树为0,右子树为1
* @param stringBuilder 用于拼接的字符串
* @return
*/

private Map getCode(Code node, String code, StringBuilder stringBuilder) { //新建拼接路径 StringBuilder appendCode = new StringBuilder(stringBuilder); appendCode.append(code); if(node != null) { //如果是非叶子结点,就继续向下遍历 if(node.data == null) { getCode(node.left, “0”, appendCode); getCode(node.right, “1”, appendCode); }else { //如果是叶子节点,就将哈弗曼编码放入哈希表中 codeMap.put(node.data, appendCode.toString()); } } return codeMap; }

@Override public int compareTo(Code o) { return weight - o.weight; } } 运行结果 前序遍历哈弗曼树 Code{data=null, weight=10} Code{data=null, weight=4} Code{data=null, weight=2} Code{data=114, weight=1} Code{data=100, weight=1} Code{data=null, weight=2} Code{data=101, weight=1} Code{data=119, weight=1} Code{data=null, weight=6} Code{data=108, weight=3} Code{data=null, weight=3} Code{data=104, weight=1} Code{data=111, weight=2} 每个字符对应的哈弗曼编码 {114=000, 100=001, 101=010, 119=011, 104=110, 108=10, 111=111}

<a name="h0pk3"></a>
### 二叉排序树
<a name="n1mif"></a>
#### 基本介绍
**二叉排序树**:BST: (Binary Sort(Search) Tree), 对于二叉排序树的**任何一个非叶子节点**,要求**左子节点的值比当前节点的值小,右子节点的值比当前节点的值大**

- **特别说明**:如果有相同的值,可以将该节点放在左子节点或右子节点

比如针对前面的数据 (7, 3, 10, 12, 5, 1, 9) ,对应的二叉排序树为:<br />[![](https://cdn.nlark.com/yuque/0/2021/png/12831985/1629115134409-fa340406-017c-46e5-8669-24420cd83e01.png#clientId=u9c439b5d-0bee-4&from=paste&id=u62c37ce8&margin=%5Bobject%20Object%5D&originHeight=481&originWidth=586&originalType=url&ratio=1&status=done&style=none&taskId=u5739f510-1d4c-424f-91eb-a971dcf1681)](https://nyimapicture.oss-cn-beijing.aliyuncs.com/img/20200729161543.png)
<a name="C9Rlk"></a>
#### 操作思路
**添加**

- 根据插入节点的值来寻找其应该插入的位置
- 新插入的节点都是**叶子节点**

**删除**<br />删除**叶子节点**(如删除值为2节点)

- 找到待删除的节点
- 找到待删除节点的父节点
- 判断待删除节点是其**父节点**的左孩子还是右孩子,然后让其令为空

删除**只有一颗子树的节点**(如删除值为1的节点)

- 找到待删除的节点
- 找到待删除的节点的父节点
- 判断待删除节点是其**父节点**的左孩子还是右孩子
- 判断待删除节点的**子树**是其左孩子还是右孩子
- 让父节点指向待删除节点的子树指向待删除节点的子树

删除有**两颗子树的节点**(如删除值为3的节点)

- 找到待删除的节点
- 找到待删除的节点的父节点
- 判断待删除节点是其**父节点**的左孩子还是右孩子
- 顺着待删除节点的**右子树**,找到一个值最小的节点(该值的大小最接近待删除节点的值)
- 让父节点指向待删除节点的子树指向上一步找到的最小的节点

删除**根节点**(如删除值为7的节点)

- 删除根结点的方法和删除有两个子树的方法相似
- 找到一个接近根节点值的节点
- 删除该节点,将该节点的值赋值给根节点即可
<a name="ZZWbf"></a>
#### 二叉排序树的创建和遍历![image.png](https://cdn.nlark.com/yuque/0/2021/png/12831985/1634026543045-61eaccb5-4e84-4f22-8ee4-0727db98ba6a.png#clientId=u4e076597-780b-4&from=paste&id=uf9c60599&margin=%5Bobject%20Object%5D&name=image.png&originHeight=347&originWidth=761&originalType=url&ratio=1&size=153049&status=done&style=none&taskId=uee4c41a6-f433-4218-9e86-0afd9ce4b5c)
代码实现
```java
package Tree;

/**
 * @author Voyager1
 * @create 2021-10-12 15:45
 */
public class BinarySortTreeDemo {
    public static void main(String[] args) {
        int[] arr = {7, 3, 10, 12, 5, 1, 9, 2};
        //创建二叉排序树
        BinarySortTree binarySortTree = new BinarySortTree();
        //循环的添加结点到二叉排序树
        for (int i = 0; i < arr.length; i++) {
            binarySortTree.add(new Node(arr[i]));
        }
        //中序遍历二叉排序树
        System.out.println("中序遍历二叉排序树:");
        binarySortTree.infixOrder(); // 1, 3, 5, 7, 9, 10, 12
    }
}
//创建二叉排序树
class BinarySortTree{
    private Node root;

    public Node getRoot() {
        return root;
    }

    //添加结点
    public void add(Node node){
        if (root == null){
            root = node;
        }else {
            root.add(node);
        }
    }

    //中序遍历
    public void infixOrder(){
        if (root == null){
            System.out.println("二叉排序树为空,不能遍历");
        }else {
            root.infixOrder();
        }
    }
}


//创建Node结点
class Node{
     int value;
     Node left;
     Node right;

    public Node(int value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                '}';
    }

    //添加结点的方法
    // 递归的形式添加结点,注意需要满足二叉排序树的要求
    public void add(Node node){
        if (node == null){
            return;
        }
        if (node.value < this.value){
            if (this.left == null){
                this.left = node;
            }else {
                this.left.add(node);
            }
        }else {
            if (this.right == null){
                this.right = node;
            }else {
                this.right.add(node);
            }
        }
    }
    //中序遍历
    public void infixOrder(){
        if (this.left != null){
            this.left.infixOrder();
        }
        System.out.println(this);
        if (this.right != null){
            this.right.infixOrder();
        }
    }

}

二叉排序树的删除

image.png
image.pngimage.png
image.png

package Tree;

/**
 * @author Voyager1
 * @create 2021-10-12 15:45
 */
public class BinarySortTreeDemo {
    public static void main(String[] args) {
        int[] arr = {7, 3, 10, 12, 5, 1, 9, 2};
        //创建二叉排序树
        BinarySortTree binarySortTree = new BinarySortTree();
        //循环的添加结点到二叉排序树
        for (int i = 0; i < arr.length; i++) {
            binarySortTree.add(new Node(arr[i]));
        }
        //中序遍历二叉排序树
        System.out.println("中序遍历二叉排序树:");
        binarySortTree.infixOrder(); // 1, 3, 5, 7, 9, 10, 12

        //测试删除节点
        binarySortTree.delNode(2);
        binarySortTree.delNode(5);
        binarySortTree.delNode(3);
        binarySortTree.delNode(10);
        binarySortTree.delNode(1);
        binarySortTree.delNode(7);
        binarySortTree.delNode(9);
        System.out.println("删除节点后中序遍历:");
        binarySortTree.infixOrder();
    }
}
//创建二叉排序树
class BinarySortTree{
    private Node root;

    public Node getRoot() {
        return root;
    }

    //添加结点
    public void add(Node node){
        if (root == null){
            root = node;
        }else {
            root.add(node);
        }
    }

    //中序遍历
    public void infixOrder(){
        if (root == null){
            System.out.println("二叉排序树为空,不能遍历");
        }else {
            root.infixOrder();
        }
    }

    //查找要删除的节点
    public Node search(int value){
        if (root == null){
            return null;
        }else{
            return root.search(value);
        }
    }
    //查找要删除节点的父节点
    public Node searchParent(int value){
        if (root == null){
            return null;
        }else {
            return root.searchParent(value);
        }
    }
    //编写方法:
    //1. 返回的 以node 为根结点的二叉排序树的最小结点的值
    //2. 删除node 为根结点的二叉排序树的最小结点
    public int delRightTreeMin(Node node){
        Node target = node;
        while (target.left != null){
            target = target.left;
        }
        //这时 target就指向了最小结点
        //删除最小结点
        delNode(target.value);
        return target.value;
    }

    //删除节点
    public void delNode(int value){
        if (root == null){
            return;
        }else {
            //1.需求先去找到要删除的结点  targetNode
            Node targetNode = search(value);
            //如果没有找到要删除的结点
            if (targetNode == null){
                return;
            }
            //如果我们发现当前这颗二叉排序树只有一个结点
            if (root.left == null && root.right == null){
                return;
            }
            //找到targetNode的父结点
            Node parent = searchParent(value);
            //如果要删除的结点是叶子结点
            if (targetNode.left == null && targetNode.right == null){
                //判断targetNode 是父结点的左子结点,还是右子结点
                if(parent != null) {
                    if (parent.left != null && parent.left.value == value) {
                        parent.left = null;
                    } else if (parent.right != null && parent.right.value == value) {
                        parent.right = null;
                    }
                }
            }else if (targetNode.left != null && targetNode.right != null){//删除有两颗子树的节点
                int minVal = delRightTreeMin(targetNode);
                targetNode.value = minVal;
            }else {// 删除只有一颗子树的结点
                //如果要删除的结点有左子结点
                if (targetNode.left != null){
                   if (parent != null){
                       //如果 targetNode 是 parent 的左子结点
                       if (parent.left.value == value){
                           parent.left = targetNode.left;
                       }else {//targetNode 是 parent 的右子结点
                           parent.right = targetNode.left;
                       }
                   }else {
                       root = targetNode.left;
                   }
                }else {//如果要删除的结点有右子结点
                    if (parent != null){
                        //如果 targetNode 是 parent 的左子结点
                        if (parent.left.value == value){
                            parent.left = targetNode.right;
                        }else {//targetNode 是 parent 的右子结点
                            parent.right = targetNode.right;
                        }
                    }else {
                        root = targetNode.right;
                    }
                }
            }
        }
    }
}


//创建Node结点
class Node{
     int value;
     Node left;
     Node right;

    public Node(int value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                '}';
    }

    //添加结点的方法
    // 递归的形式添加结点,注意需要满足二叉排序树的要求
    public void add(Node node){
        if (node == null){
            return;
        }
        if (node.value < this.value){
            if (this.left == null){
                this.left = node;
            }else {
                this.left.add(node);
            }
        }else {
            if (this.right == null){
                this.right = node;
            }else {
                this.right.add(node);
            }
        }
    }
    //中序遍历
    public void infixOrder(){
        if (this.left != null){
            this.left.infixOrder();
        }
        System.out.println(this);
        if (this.right != null){
            this.right.infixOrder();
        }
    }

    /**
     *查找要删除的节点
     * @param value 希望删除的结点的值
     * @return 如果找到返回该结点,否则返回null
     */
    public Node search(int value){
      if (this.value == value){
          return this;
      } else if (value < this.value){
          if (this.left == null){
              return null;
          }
          return this.left.search(value);
      }else {//如果查找的值不小于当前结点,向右子树递归查找
          if (this.right == null){
              return null;
          }
          return this.right.search(value);
      }
    }

    public Node searchParent(int value){
        if ((this.left != null && this.left.value == value)||(this.right != null && this.right.value ==value)){
            return this;
        }else {
            if ( value < this.value && this.left != null ){
                return this.left.searchParent(value);
            }else if (value >= this.value  && this.right != null){
                return this.right.searchParent(value);
            }else{
                return null;
            }
        }
    }
}

二叉排序树的注意事

如果删除的数10这个根节点 走到这一步时 会报空指针异常 所以需要加一个判断
image.png
修改方案如果
parent 判空image.png

public void delNode(int value) {
        if (root ==  null){
            return;
        }else {
            // 1 先去找到要删除的节点 targetNode
            Node targetNode = search(value);
            if (targetNode == null){ //没有找到
                return;
            }
            // 如果这颗二叉树只有一个 targetNode 节点
            if (root.left == null && root.right == null){
                root = null;
                return;
            }

            // 去找到targetNode的父节点
            Node parent = searchParent(value);
            // 如果要删除的节点是叶子节点  左节点? 右节点
            if (targetNode.left == null && targetNode.right == null){
                if (parent.value > value){
                    parent.left = null;
                }else {
                    parent.right = null;
                }

            }else if(targetNode.right != null && targetNode.left != null){ // 要删除的节点左右都有子节点

                int min = delRightTreeMin(targetNode);
                targetNode.value = min;


            }else { // 删除只有一颗子树的接单
                // 如果要删除的这个节点有左子节点
                if (targetNode.left != null){
                    if (parent != null) {
                        if (parent.left.value == targetNode.value) {// 如果targetNode是parent的左子节点
                            parent.left = targetNode.left;
                        } else {//如果targetNode是parent的右子节点
                            parent.right = targetNode.left;
                        }
                    }else {
                        root = targetNode.left;
                    }
                }else {
                    if (parent != null) {
                        if (parent.left.value == targetNode.value) {// 如果targetNode是parent的左子节点
                            parent.left = targetNode.right;
                        } else { //如果targetNode是parent的右子节点
                            parent.right = targetNode.right;
                        }
                    }else {
                        root = targetNode.right;
                    }
                }
            }
        }
    }

平衡二叉树

基本介绍

为什么需要平衡二叉树

  • 如果由数组{1, 2, 3, 4}来构建一颗二叉排序树,得到的二叉树不仅没有体现其特点,反而还退化成了链表

八、树结构 - 图44
简介

  • 平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树,可以保证查询效率较高
  • 具有以下特点:
    • 它是一棵空树它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树
    • 平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等
  • 下图所示的二叉树就是平衡二叉树

八、树结构 - 图45

应用案例——左旋转

将由数列 {4,3,6,5,7,8}构建的二叉排序树,修改为一颗平衡二叉树
此处右子树的高度高于左子树,且差值大于1,所以需要进行左旋转,来降低右子树的高度
八、树结构 - 图46
步骤

  • 创建一个新节点,值为当前节点的值(4)

八、树结构 - 图47

  • 让新节点的左子树指向当前节点的左子树

八、树结构 - 图48

  • 让新节点的右子树指向当前节点的右子树的左子树

八、树结构 - 图49

  • 将当前节点的值改为其右子树的值八、树结构 - 图50
  • 将当前节点的右子树变为其右子树的右子树

八、树结构 - 图51

  • 让当前节点的左子树指向新节点

八、树结构 - 图52
整理后结果
八、树结构 - 图53

应用案例——右旋转

当一颗二叉排序树的左子树高度大于右子树高度,且差值大于1时,需要进行右旋转,来降低左子树的高度
步骤

  • 创建一个新节点,其值为当前节点的值
  • 将新节点的右子树指向当前节点的右子树
  • 将新节点的左子树指向当前节点的左子树的右子树
  • 将当前节点的值改为其左子树的值
  • 将当前节点的左子树指向其左子树的左子树
  • 将当前节点的右子树指向新节点

    应用案例——双旋转

    某些时候,只进行左旋转或者右旋转,并不能将二叉排序树变为平衡二叉树。这时就需要进行双旋转,即同时进行左旋转和右旋转

  • 进行左旋转时,如果当前节点右子树的左子树高于其右子树,需要先进行左旋转

八、树结构 - 图54

  • 进行右旋转时,如果当前节点左子树的右子树高于其左子树,需要先进性右旋转

八、树结构 - 图55image.png

实现代码

package Tree;
/**
 * @author Voyager1
 * @create 2021-10-14 15:15
 */
public class AVLTreeDemo {
    public static void main(String[] args) {
        //int[] arr = {4,3,6,5,7,8};
        //int[] arr = { 10, 12, 8, 9, 7, 6 };
        int[] arr = { 10, 11, 7, 6, 8, 9 };
        //创建一个 AVLTree对象
        AVLTree avlTree = new AVLTree();
        //添加结点
        for(int i=0; i < arr.length; i++) {
            avlTree.add(new AVLNode(arr[i]));
        }
        //遍历
        System.out.println("中序遍历");
        avlTree.infixOrder();

        System.out.println("在平衡处理~");
        System.out.println("树的高度=" + avlTree.getRoot().height());//4-->3
        System.out.println("树的左子树高度=" + avlTree.getRoot().leftHeight());//1-->2
        System.out.println("树的右子树高度=" + avlTree.getRoot().rightHeight());//3-->2
        System.out.println("根节点=" + avlTree.getRoot().right.right);
    }
}

class AVLTree{
    private AVLNode root;

    public AVLNode getRoot() {
        return root;
    }
    //添加结点
    public void add(AVLNode node){
        if (root == null){
            root = node;
        }else {
            root.add(node);
        }
    }
    //中序遍历
    public void infixOrder(){
        if (root == null){
            System.out.println("二叉排序树为空,不能遍历");
        }else {
            root.infixOrder();
        }
    }
    //查找要删除的节点
    public AVLNode search(int value){
        if (root == null){
            return null;
        }else{
            return root.search(value);
        }
    }
    //查找要删除节点的父节点
    public AVLNode searchParent(int value){
        if (root == null){
            return null;
        }else {
            return root.searchParent(value);
        }
    }
    //编写方法:
    //1. 返回的 以node 为根结点的二叉排序树的最小结点的值
    //2. 删除node 为根结点的二叉排序树的最小结点
    public int delRightTreeMin(AVLNode node){
        AVLNode target = node;
        while (target.left != null){
            target = target.left;
        }
        //这时 target就指向了最小结点
        //删除最小结点
        delNode(target.value);
        return target.value;
    }
    //删除节点
    public void delNode(int value){
        if (root == null){
            return;
        }else {
            //1.需求先去找到要删除的结点  targetNode
            AVLNode targetNode = search(value);
            //如果没有找到要删除的结点
            if (targetNode == null){
                return;
            }
            //如果我们发现当前这颗二叉排序树只有一个结点
            if (root.left == null && root.right == null){
                return;
            }
            //找到targetNode的父结点
            AVLNode parent = searchParent(value);
            //如果要删除的结点是叶子结点
            if (targetNode.left == null && targetNode.right == null){
                //判断targetNode 是父结点的左子结点,还是右子结点
                if(parent != null) {
                    if (parent.left != null && parent.left.value == value) {
                        parent.left = null;
                    } else if (parent.right != null && parent.right.value == value) {
                        parent.right = null;
                    }
                }
            }else if (targetNode.left != null && targetNode.right != null){//删除有两颗子树的节点
                int minVal = delRightTreeMin(targetNode);
                targetNode.value = minVal;
            }else {// 删除只有一颗子树的结点
                //如果要删除的结点有左子结点
                if (targetNode.left != null){
                    if (parent != null){
                        //如果 targetNode 是 parent 的左子结点
                        if (parent.left.value == value){
                            parent.left = targetNode.left;
                        }else {//targetNode 是 parent 的右子结点
                            parent.right = targetNode.left;
                        }
                    }else {
                        root = targetNode.left;
                    }
                }else {//如果要删除的结点有右子结点
                    if (parent != null){
                        //如果 targetNode 是 parent 的左子结点
                        if (parent.left.value == value){
                            parent.left = targetNode.right;
                        }else {//targetNode 是 parent 的右子结点
                            parent.right = targetNode.right;
                        }
                    }else {
                        root = targetNode.right;
                    }
                }
            }
        }
    }
}

class AVLNode {
    int value;
    AVLNode left;
    AVLNode right;

    public AVLNode(int value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "AVLNode{" +
                "value=" + value +
                '}';
    }

    //添加结点的方法
    // 递归的形式添加结点,注意需要满足二叉排序树的要求
    public void add(AVLNode node){
        if (node == null){
            return;
        }
        if (node.value < this.value){
            if (this.left == null){
                this.left = node;
            }else {
                this.left.add(node);
            }
        }else {
            if (this.right == null){
                this.right = node;
            }else {
                this.right.add(node);
            }
        }
        //当添加完一个结点后,如果: (右子树的高度-左子树的高度) > 1 , 左旋转
        if ((rightHeight() - leftHeight()) > 1){
            //如果它的右子树的左子树的高度大于它的右子树的右子树的高度
            if (right != null && right.leftHeight() > right.rightHeight()){
                //先对右子结点进行右旋转
                right.rightRotate();
                //然后在对当前结点进行左旋转
                leftRotate();
            }else {
                //直接进行左旋转即可
                leftRotate();
            }
        }
        //当添加完一个结点后,如果 (左子树的高度 - 右子树的高度) > 1, 右旋转
        if ((leftHeight() - rightHeight()) > 1){
            //如果它的左子树的右子树高度大于它的左子树的高度
            if (left != null && left.rightHeight() > left.leftHeight()){
                //先对当前结点的左结点(左子树)->左旋转
                left.leftRotate();
                //再对当前结点进行右旋转
                rightRotate();
            }else {
                //直接进行右旋转即可
                rightRotate();
            }
        }
    }
    //中序遍历
    public void infixOrder(){
        if (this.left != null){
            this.left.infixOrder();
        }
        System.out.println(this);
        if (this.right != null){
            this.right.infixOrder();
        }
    }

    /**
     *查找要删除的节点
     * @param value 希望删除的结点的值
     * @return 如果找到返回该结点,否则返回null
     */
    public AVLNode search(int value){
        if (this.value == value){
            return this;
        } else if (value < this.value){
            if (this.left == null){
                return null;
            }
            return this.left.search(value);
        }else {//如果查找的值不小于当前结点,向右子树递归查找
            if (this.right == null){
                return null;
            }
            return this.right.search(value);
        }
    }

    /**
     * 查找父节点
     * @param value
     * @return
     */
    public AVLNode searchParent(int value){
        if ((this.left != null && this.left.value == value)||(this.right != null && this.right.value ==value)){
            return this;
        }else {
            if ( value < this.value && this.left != null ){
                return this.left.searchParent(value);
            }else if (value >= this.value  && this.right != null){
                return this.right.searchParent(value);
            }else{
                return null;
            }
        }
    }
    // 返回左子树的高度
    public int leftHeight(){
        if (left == null){
            return 0;
        }
        return left.height();
    }
    // 返回右子树的高度
    public int rightHeight(){
        if(right == null){
            return 0;
        }
        return right.height();
    }

    // 返回 以该结点为根结点的树的高度
    public int height(){
        return Math.max(left == null ? 0:left.height(),right == null ? 0:right.height()) + 1;
    }

    //左旋转方法
    public void leftRotate(){
        //创建新的结点,以当前根结点的值
        AVLNode newNode = new AVLNode(value);
        //把新的结点的左子树设置成当前结点的左子树
        newNode.left = left;
        //把新的结点的右子树设置成带你过去结点的右子树的左子树
        newNode.right = right.left;
        //把当前结点的值替换成右子结点的值
        value = right.value;
        //把当前结点的右子树设置成当前结点右子树的右子树
        right = right.right;
        //把当前结点的左子树(左子结点)设置成新的结点
        left = newNode;
    }
    //右旋转方法
    public void rightRotate(){
        AVLNode newNode = new AVLNode(value);
        newNode.right = right;
        newNode.left = left.right;
        value = left.value;
        left = left.left;
        right = newNode;
    }
}

3、多叉树

基本介绍

在二叉树中,每个节点最多有一个数据项和两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树(multiway tree)
多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化

2-3树

八、树结构 - 图57
2-3树是最简单的B树结构,具有以下特点

  • 2-3树的所有叶子节点都在同一层(只要是 B树都满足这个条件)
  • 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点
  • 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点
  • 2-3树是由二节点和三节点构成的树(2和3)

2-3树插入规则

  • 2-3树的所有叶子节点都在同一层.(只要是 B树都满足这个条件)
  • 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点
  • 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点
  • 当按照规则插入一个数到某个节点时,不能满足上面三个要求,就需要拆,先向上拆,如果上层满,则拆本层,拆后仍然需要满足上面 3个条件
  • 对于三节点的子树的值大小仍然遵守(BST 二叉排序树)的规则

    • 左边的子树值小于父节点的值
    • 中间的子树值在父节点的值之间
    • 右边子树的值大于父节点的值

      B树、B+和B*树

      B树

      八、树结构 - 图58
  • B树的阶:节点的最多子节点个数。比如 2-3树的阶是3,2-3-4树的阶是4

  • B树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点
  • 关键字集合分布在整颗树中, 即叶子节点和非叶子节点都存放数据
  • 搜索有可能在非叶子结点结束
  • 其搜索性能等价于在关键字全集内做一次二分查找

    B+树

    八、树结构 - 图59

  • B+树是B树的变体,也是一种多路搜索树

  • B+树的搜索与 B树也基本相同,区别是 B+树只有达到叶子结点才命中(B树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找
  • 所有关键字都出现在叶子结点的链表中(即数据只能在叶子节点【也叫稠密索引】),且链表中的关键字(数据)恰好是有序的
  • 不可能在非叶子结点命中
  • 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层
  • 更适合文件索引系统
  • B树和 B+树各有自己的应用场景,不能说 B+树完全比 B树好,反之亦然

    B*树

    八、树结构 - 图60

  • B*树是 B+树的变体,在 B+树的非根和非叶子结点再增加指向兄弟的指针

  • B*树定义了非叶子结点关键字个数至少为(2/3)M,即块的最低使用率为 2/3,而B+树的块的最低使用率为的1/2
  • 从第 1个特点我们可以看出,B*树分配新结点的概率比B+树要低,空间使用率更高