二叉树的起源
说起二叉树,我们不能直接从 “树” 开始,而是要回到数据结构的两大 “原始部落”:数组和链表。二叉树(尤其是二叉搜索树 BST)的出现,本质上是一场为了终结 “性能偏科” 而发起的架构革命。
原始部落的“偏科”困境
在二叉树诞生前,程序员只能在两种极端的存储方式中挣扎:

数组的痛:如果你在 100 万长度数组开头插入一个数,你得把后面 99.9 万个数往后挪一位。这实在 是不可接受的。
链表的痛:即便链表是排好序的,你也无法跳跃。想找中间的数?对不起,请从头开始数。
二叉树的起源
二叉搜索树(BST)的灵感来源极其天才:如果我们将二分查找的 “逻辑过程” 直接变成数据的 “存储物理结构” 呢?
- 二分查找的逻辑:先看中间的数,比它小往左看,比它大往右看。
- 二叉树的结构:把中间那个数作为根节点,左边连小的,右边连大的。
二叉树 = 具有二分查找能力的链表:它通过增加一个维度的指针(从 next 变成 left / right),成功实现了到二维的跨越。
解决的问题
二叉树存在的终极任务只有四个字:动态平衡,即在数据不断变动(动态插入/删除)的情况下,依然保持极高的查找速度。
- 它既像链表,节点分散存储,插入时只需改动几个指针,不需要挪动整块内存。
- 它又像数组:通过父子节点的逻辑关系,每次比较都能砍掉一半的搜索范围。
从理想到现实的修补
虽然二叉树(BST)理论上解决了数组和链表的矛盾,但它在现实中遇到了一个致命伤:退化。如果数据是有序存入的(比如 1, 2, 3, 4, 5),二叉树会 “长歪”,变成一根长长的棍子(链表),这时它所有的优势都荡然无存。于是从二叉树在20世纪60年代被正式提出,到后续几十年的算法史,其实都是在为二叉树做 “整形手术”:
AVL 树:为了解决退化,发明了“旋转”,强迫树必须长得左右对称。
红黑树:觉得 AVL 旋转太频繁浪费性能,于是发明了 “颜色规则”,在平衡和效率间找平衡(用于 Java HashMap)。
B/B+ 树:觉得内存太小,要把树存到硬盘里,于是把 “二叉” 变成了 “多叉”,减少翻页次数(用于数据库索引)。
代码实现
下面是 BST 核心代码及其效果演示。你可看到它是如何通过递归实现 “自动导航” 的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
|
public class MyBST {
private TreeNode root; public static class TreeNode { int data; TreeNode left; TreeNode right;
public TreeNode(int data) { this.data = data; } }
public void insert(int data) { this.root = insert(this.root, data); }
private TreeNode insert(TreeNode curRoot, int data) { if (curRoot == null) { return new TreeNode(data); } if (data < curRoot.data) { curRoot.left = insert(curRoot.left, data); } else if (data > curRoot.data) { curRoot.right = insert(curRoot.right, data); } return curRoot; }
public void remove(int data) { this.root = remove(this.root, data); }
private TreeNode remove(TreeNode curRoot, int data) { if (curRoot == null) { return null; }
if (data < curRoot.data) { curRoot.left = remove(curRoot.left, data); } else if (data > curRoot.data) { curRoot.right = remove(curRoot.right, data); } else { if (curRoot.left == null) { return curRoot.right; } if (curRoot.right == null) { return curRoot.left; }
TreeNode successor = findMin(curRoot.right); curRoot.data = successor.data; curRoot.right = remove(curRoot.right, successor.data); } return curRoot; }
private TreeNode findMin(TreeNode node) { while (node.left != null) { node = node.left; } return node; }
public boolean search(int value) { TreeNode curRoot = this.root; while (curRoot != null) { if (value == curRoot.data) return true; curRoot = value < curRoot.data ? curRoot.left : curRoot.right; } return false; }
public void printTree() { printTree(root, 0); }
private void printTree(TreeNode node, int level) { if (node == null) return;
printTree(node.right, level + 1);
if (level == 0) { System.out.println(node.data); } else { String subPrefix = "| ".repeat(level - 1); System.out.println(subPrefix + "|----" + node.data); }
printTree(node.left, level + 1); }
public static void main(String[] args) { MyBST bst = new MyBST();
int[] nodes = {25, 15, 35, 10, 20, 30, 40}; Arrays.stream(nodes).forEach(bst::insert);
bst.printTree(); System.out.println();
bst.remove(35); bst.printTree(); System.out.println();
System.out.println(bst.search(20) ? "存在" : "不存在"); System.out.println(bst.search(99) ? "存在" : "不存在"); } }
|
测试效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| | |----40 |----35 | |----30 25 | |----20 |----15 | |----10
|----40 | |----30 25 | |----20 |----15 | |----10
存在 不存在
|
二叉树和堆的关系
简单来说,堆(Heap) 就是一个非常有 “纪律” 的完全二叉树。完全满足两个条件就可以称之为“堆”:
- 条件 A:结构必须是 “完全二叉树”:普通的二叉树可以长得歪七拧八,左边一长串,右边空荡荡。但堆要求树的每一层必须是满的。如果最后一层不满,也必须从左到右依次排开,不能跳着长。就像排队报数,必须先站满第一排,再站第二排,且第二排的人必须靠左站齐,不能中间留位子。
- 条件 B:必须满足 “堆序性”:在二叉树里,节点之间可能没啥大小逻辑。但在堆里,必须满足 父节点 > 子节点(大顶堆)或 父节点 < 子节点(小顶堆)。
有了树,为什么要搞出个 “堆” 来?主要还是为了解决 “找极值” 的问题。
- 效率极高:如果你想从100万个数字里不停地找出最大的那个,用普通数组你得翻遍全身;但用大顶堆,最大值就在最顶端,一眼就能看到。
- 动态调整:当你拿走最大的数,或者新进来一个数,堆能通过一套简单的 “比武招亲” 规则(上浮或下沉),快速重新排好顺序。
说白了,堆的最大用处就是 “动态管理最值”。只要你的需求符合以下描述,选堆那就准没错:
- 数据一直在变动(新元素加入或旧元素删除)。
- 你永远只想关注这群数据里最大、最小或排在最前面的那几个。