一、先从"二分查找"说起
假设你有一本按页码编号的电话簿,从第1页到第100页。你想找第37页上的人名。
普通查找:从第1页开始翻,一页一页往后翻,翻到第37页。最坏情况要翻100次。
二分查找:
-
先翻到中间第50页。37 < 50,所以目标在前半本。
-
再翻到前半本的中间第25页。37 > 25,所以在后半本(25~50之间)。
-
再翻到25~50的中间第37页。找到了。
只翻了3次。这就是二分------每次把搜索范围砍掉一半。
二、从"二分查找"到"树"
现在问题变了:你不再只是找某一页,而是要频繁地查任意连续几页里,人名拼音最靠后的是谁(相当于最大值)。
比如问你:"第30页到第50页之间,人名拼音最靠后的是谁?"
如果你每次都从第30页翻到第50页,一页一页比过去,那就太慢了。
能不能像二分查找那样,提前把一些"汇总信息"存好,查的时候直接拿?
这就是"树"要做的事情。
三、画一棵"汇总树"
假设我们有8个时间戳:1, 2, 3, 4, 5, 6, 7, 8。每个时间戳上有一个数值。
我们先把它们两两分组,每组选出一个最大值:
第1层(原始数据):
时间戳: 1 2 3 4 5 6 7 8
数值: 10 5 8 3 12 7 9 4
第2层(两两一组取最大值):
(1,2) → max(10,5) = 10
(3,4) → max(8,3) = 8
(5,6) → max(12,7) = 12
(7,8) → max(9,4) = 9
第3层(再两两一组取最大值):
(1~4) → max(10,8) = 10
(5~8) → max(12,9) = 12
第4层(最终):
(1~8) → max(10,12) = 12
把这个过程画成树的样子:
(1~8): 12
/ \
(1~4): 10 (5~8): 12
/ \ / \
(1~2): 10 (3~4): 8 (5~6): 12 (7~8): 9
/ \ / \ / \ / \
1:10 2:5 3:8 4:3 5:12 6:7 7:9 8:4
这棵树就是线段树。
-
最下面一排是原始数据(叶子节点)。
-
上面每一层都是下一层的"汇总"------两个子节点的最大值。
-
最上面是整棵树的"总最大值"。
四、用这棵树来查询
现在问你:"时间戳3到时间戳6之间的最大值是多少?"
不用一个一个看。我们从树顶往下走:
-
根节点 (1~8):太大了,不完全在3~6内,所以往下走。
-
左孩子 (1~4):部分重叠(3~4在范围内),继续往下。
-
右孩子 (5~8):部分重叠(5~6在范围内),继续往下。
-
(1~4) 的左孩子 (1~2):完全不重叠(1~2不在3~6内),跳过。
-
(1~4) 的右孩子 (3~4):完全在3~6内,直接拿它的值 = 8。
-
(5~8) 的左孩子 (5~6):完全在3~6内,直接拿它的值 = 12。
-
(5~8) 的右孩子 (7~8):完全不重叠,跳过。
结果:max(8, 12) = 12。
我们只看了几个节点,就得到了答案,不需要遍历所有8个时间戳。
五、为什么叫"动态开点"?
刚才的例子中,我们把所有8个时间戳都画出来了。但实际题目中,时间戳范围是1到10^9,你不可能真的把这10亿个节点都建好------内存不够。
动态开点 的意思是:只有当你真正用到某个时间戳时,才创建它对应的节点。
比如你只收到了时间戳1、5、10的数据,那树就只长成这样:
(1~10^9)
/ \
(1~5 * 10^8) (5 * 10^8+1 ~ 10^9)
/ \
(1~2.5 * 10^8) (2.5 * 10^8+1 ~ 5 * 10^8)
/ \
(1~1) (2~2.5 * 10^8)
|
1:10
实际上,树只会沿着你用到的路径生长。没用到的区间,节点根本就不存在(None)。这就叫"动态开点"------用多少,长多少。
六、总结:线段树到底干了什么?
| 你的问题 | 线段树的回答 |
|---|---|
| 我要在时间戳5上存一个值 | 从树顶往下走,找到时间戳5的叶子,存进去。然后一路回溯,更新沿途所有父节点的最大值。 |
| 我要查时间戳3~6的最大值 | 从树顶往下走,碰到完全在3~6内的节点就直接拿值,碰到完全不在的就跳过,部分重叠的就继续往下。最后把所有拿到的值取最大。 |
| 时间戳范围太大 | 动态开点,只用到的节点才创建,不浪费内存。 |
整个过程就是"二分思想"的延伸:把一个大区间不断对半拆分,每个小区间都提前算好"汇总值"。查询时,通过二分快速定位到需要的小区间,直接拿汇总值,不用一个一个数。
七、一个让你彻底理解的类比
想象你是一个班主任,手下有1000个学生,学号从1到1000。你想随时知道任意连续学号段里,最高分是多少。
笨办法:每次有人问,你就把那个学号段的所有学生成绩翻一遍。1000个学生问1000次,就是100万次操作。
线段树办法:你提前把所有学生按学号分组:
-
1~1000 为一组,记下最高分
-
分成两组:1~500 和 501~1000,分别记下最高分
-
再分:1~250、251~500、501~750、751~1000,分别记下最高分
-
......
-
直到每个学生单独一组
现在有人问"学号237到学号684的最高分是多少",你只需要:
-
看237~684覆盖了哪些"提前分好组"的区间
-
把这些区间的最高分拿出来比一比
-
得出答案
你不需要看每一个学生的成绩,只需要看几个"小组汇总"就够了。
这就是线段树。