Skip to content

Commit 1c931a4

Browse files
committed
SegmentTree 线段树完善
1 parent 531bb7f commit 1c931a4

File tree

2 files changed

+236
-9
lines changed

2 files changed

+236
-9
lines changed

MaxHeap/README.md

+20-9
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ public interface Queue<E> {
3939
|顺序线性结构|O(N):顺序放入元素|O(1)|
4040
|二叉堆|O(log(N))|O(log(N))|
4141

42-
## 2、什么是二叉堆
42+
## 2、二叉堆的实现
43+
44+
### 2.1 什么是二叉堆
4345

4446
&emsp;&emsp;二叉堆是一个**完全二叉树**。那什么是完全二叉树呢?
4547
&emsp;&emsp;**满二叉树**就是除了最下面一层,其他的节点都是具有左右孩子节点,就类似于这样。
@@ -68,7 +70,7 @@ public interface Queue<E> {
6870

6971
可以看出任意子树的最大值永远是自己的父亲节点。
7072

71-
### 2.1、实现方法
73+
### 2.2、二叉堆的结构
7274

7375
&emsp;&emsp;这里我们可以看出来二叉堆是一层一层的从左到右这么依次排列的,所以这里我们使用数组进行存储二叉树。通过数组索引找到节点。
7476
<div align=center>
@@ -87,7 +89,7 @@ leftChild(i) = i * 2 + 1
8789
rightChild(i) = i * 2 + 2
8890
```
8991

90-
### 2.2、初始化操作
92+
### 2.3、初始化操作
9193

9294
&emsp;&emsp;在最大堆这个数据结构当中我们使用的是数组的底层实现,当然我们也就需要动态数组来实现这个动态大小的最大堆。关于Array动态数组这一章可以参考[Array 动态数组](/Array/README.md)。当然也可以直接使用Java自带的动态数组。
9395

@@ -119,7 +121,7 @@ private int rightChild(int index) {
119121
}
120122
```
121123

122-
### 2.3、添加元素
124+
### 2.4、添加元素
123125

124126
&emsp;&emsp;这里的操作底层实现其实是上浮(SiftUp)操作。下面我们就来看看是如何上浮的。
125127
+ 向数组末尾添加一个元素,也就是向树的最下角添加一个元素;
@@ -145,7 +147,7 @@ private void siftUp(int index) {
145147
}
146148
}
147149
```
148-
### 2.4、提取最大值
150+
### 2.5、提取最大值
149151

150152
&emsp;&emsp;对于我们上面实现的最大堆,看得出来,最大值的地方存在于根节点的位置。也就是数组索引位0的位置。而且我们需要维护二叉堆的性质。
151153
步骤:
@@ -181,16 +183,25 @@ private void siftDown(int index) {
181183
}
182184
```
183185

184-
### 2.5 查询操作
186+
### 2.6、查询操作
185187

186188
&emsp;&emsp;查询操作就是查找元素最大的值,这里就是根节点位置,也就是索引为 0 的位置。
189+
**程序实现:**
190+
```java
191+
public E findMax() {
192+
if (isEmpty())
193+
throw new IllegalArgumentException("Empty");
194+
return data.get(0);
195+
}
196+
```
187197

188-
### 2.6、replace操作
198+
### 2.7、replace操作
189199

190200
&emsp;&emsp;replace替换操作主要包括:去除最大元素,放入一个新的元素。这其实是一个组合操作。但这里我们准备封装一下,并对其进行优化。
191201

192202
&emsp;&emsp;优化的方式就是在删除元素这里,如果我们分extraMax和add操作就需要两次 O(log(N)) 级别的时间复杂度。在replace操作中,我们可以直接将待添加的元素元素替换到根节点的位置,然后在执行下沉操作就可以,这样就是一次 O(log(N)) 级别的时间复杂度。
193203

204+
**程序实现:**
194205
```java
195206
public E replace(E e) {
196207
E ret = findMax();
@@ -200,7 +211,7 @@ public E replace(E e) {
200211
}
201212
```
202213

203-
### 2.7、Heapify数组堆化
214+
### 2.8、Heapify数组堆化
204215

205216
&emsp;&emsp;操作就是将任意数组整理成堆的形状。
206217
具体的过程就是:
@@ -222,7 +233,7 @@ public MaxHeap(E[] arr) {
222233
}
223234
```
224235

225-
## 3、优先队列的实现——基于二叉堆
236+
## 3、优先队列的实现——基于最大堆
226237

227238
&emsp;&emsp;具体的函数方法其实在最大堆已经映射过了。
228239
||优先队列|最大堆|

SegmentTree/README.md

+216
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
<h1 align=center>SegmentTree 线段树(区间树)</h1>
2+
<div align="center">
3+
<image src="https://img.shields.io/badge/Github-LiYangSir-brightgreen">
4+
<image src="https://img.shields.io/badge/author-teaUrn-green">
5+
<image src="https://img.shields.io/badge/Language-Java-orange">
6+
<image src="https://img.shields.io/badge/Version-1.0-blue">
7+
</div>
8+
9+
-----
10+
11+
## 1、为什么使用线段树
12+
13+
&emsp;&emsp;相信大家都见过一个经典的比赛题目(区间染色):在一个数组结构当中,对某一端区间不断的进行染色。在m次操作后,在这个数组中包含了多少种颜色。以及在m次操作后我们在某一区间可以看见多少种颜色。
14+
15+
&emsp;&emsp;其中主要包含两种操作,染色操作(更新区间)以及查询操作(查询区间)。我们发现我们主要是针对区间进行操作,而且我们只关心区间的颜色种类的个数,并不关心它的颜色是什么。
16+
17+
&emsp;&emsp;如果我们采用最基本的数组遍历的操作。使用数组实现,两种操作的时间复杂度均为O(N)级别的操作。这样我们的线段树显得就尤为重要。
18+
19+
&emsp;&emsp;还有一种经典问题就是区间查询:例如我们在一段数组内查询某一个区间的最大值、最小值或者区间数字和。针对区间进行操作的我们都要想要线段树这种数据结构。由于我们线段树采用的树结构,所以时间复杂度就会很低。
20+
21+
**时间复杂度:**
22+
23+
||数组实现|线段树实现|
24+
|:---:|:---:|:---:|
25+
|更新操作|O(N)|O(log(N))|
26+
|查询操作|O(N)|O(log(N))|
27+
28+
## 2、线段树的基本结构
29+
&emsp;&emsp;在线段树中我们并不考虑增加元素和删除元素,我们只考虑在已有的数组结构中构建线段树这种数据结构。
30+
31+
<div align=center>
32+
<img src=https://markdown-liyang.oss-cn-beijing.aliyuncs.com/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/8-SegmentTree/%E5%9F%BA%E6%9C%AC%E7%BB%93%E6%9E%84.png width=70% alt=基本结构>
33+
</div>
34+
<br/>
35+
36+
&emsp;&emsp;我们可以看出,一个数组分成了很多区间,基本上都是对半劈开,小伙伴可能会问了,结构变得更加复杂了。在这里正是运用了计算机领域的一句话 :
37+
> **用空间换取时间**
38+
39+
40+
**注意:** 每一个区间并不是存储一个区间,而是一个值。例如我们以求和为例,==A[0 ··· 3]== 节点存储的是这个区间的和。如果是最大值,那么存储的就是这一个区间的最大值。
41+
42+
**例子:** 当我们查询A[2-5]区间的和,那我们只需要知道A[2-3]和A[4-5]的和就可以啦,我们并不需要遍历2-5的所有数据。
43+
44+
### 2.1、线段树的一般结构
45+
46+
&emsp;&emsp;上面的例子当中,正好数组大小正好是 $2^3 = 8$,所以看起来像是~~满二叉树~~,其实不是。我们看下面的图:
47+
48+
<div align=center>
49+
<img src=https://markdown-liyang.oss-cn-beijing.aliyuncs.com/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/8-SegmentTree/%E5%9F%BA%E6%9C%AC%E7%BB%93%E6%9E%84-%E9%9D%9E.png width=70% alt=基本结构>
50+
</div>
51+
<br/>
52+
&emsp;&emsp;看得出来,这并不是一个满的二叉树,也不是一个完全二叉树。但是这是一个平衡二叉树。
53+
54+
> **平衡二叉树:** 最大深度和最小深度之间的差值为 1 。
55+
56+
### 2.2、线段树存储所需空间
57+
58+
&emsp;&emsp;我们知道线段树是我们损失空间来减小时间的,那我们实际过程中,需要多少空间呢。下面我们就来推导一下。
59+
|层数|节点个数|
60+
|:---:|:---:|
61+
|0|1|
62+
|1|2|
63+
|2|4|
64+
|3|8|
65+
|···|···|
66+
|h - 1|$2^{h-1}$|
67+
68+
&emsp;&emsp;对于满的二叉树来说,$h$ 层一共有$2^h-1$个节点。所以大约等于 $2^h$ 个。而且我们看到最后一层的个数为$2^{h-1}$正好等于总个数的一半。于是我们可以得到:
69+
70+
> **最后一层的节点数大致等于(差1)前面所有层数节点之和。**
71+
72+
&emsp;&emsp;假设我们的数组有 n 个元素,按照满的二叉树的形式来看,最底下一层元素的个数为 n。那么他上面的所有节点和也为n,所以对应的总个数为==2 * n==。这只是还在满二叉树的情况下。如果我们的元素不是2的整数次幂,那么它就构成了平衡二叉树,所以需要向下扩充一层,这一层的个数等于我们上面所有节点的和也就是 2 * n。加起来就是 4 * n。我们可以得出结论:
73+
74+
> **对于存储 n 个元素的线段树,我们需要开辟 4 倍的空间大小。**
75+
76+
&emsp;&emsp;虽然我们浪费了大量的空间来存储,但是我们在时间复杂度上有着巨大的提升。随着社会的发展,存储已经变得不再是问题,问题变成了时间速度问题,所以说**牺牲空间提升时间**是一件非常有意义的事情。
77+
78+
## 3、线段树的实现
79+
80+
### 3.1、Merge 函数
81+
82+
&emsp;&emsp;在实际过程中底层并不确定用户对线段树执行什么操作,求和,最大值还是最小值操作,所以这里我们引入merge函数,用户来指定他们需要对线段树执行什么操作。
83+
**Merge接口函数:**
84+
```java
85+
public interface Merger<E> {
86+
E merge(E a, E b);
87+
}
88+
```
89+
90+
### 3.2、构造函数
91+
&emsp;&emsp;在构造函数里面需要用户传入用户设定的Merge实例对象,以对线段树进行用户想要类型的操作。
92+
**构造函数实现:**
93+
```java
94+
public SegmentTree(E[] arr, Merger<E> merger) {
95+
96+
this.merger = merger; // 指定merge实例
97+
98+
data = (E[])new Object[arr.length]; //拷贝数组
99+
System.arraycopy(arr, 0, data, 0, arr.length);
100+
101+
tree = (E[])new Object[4 * arr.length]; //开启4倍的空间
102+
buildSegmentTree(0, 0, data.length - 1);
103+
}
104+
```
105+
### 3.3、基本操作函数
106+
&emsp;&emsp;主要包含数据的接口操作,例如getSize和get操作。
107+
```java
108+
public int getSize() {
109+
return data.length;
110+
}
111+
112+
public E get(int index) {
113+
if (index < 0 || index >= data.length)
114+
throw new IllegalArgumentException("Index is illegal");
115+
return data[index];
116+
}
117+
```
118+
&emsp;&emsp;这里我们线段树的底层依然是数组,所以我们依然可以按照我们之前的[ MaxHeap 最大堆 ](/MaxHeap/README.md)那种结构来实现。
119+
```java
120+
private int leftChild(int index) {
121+
return index * 2 + 1;
122+
}
123+
124+
private int rightChild(int index) {
125+
return index * 2 + 2;
126+
}
127+
```
128+
129+
### 3.4、构建线段树
130+
131+
&emsp;&emsp;我们需要在treeIndex索引的位置创建数组区间。这里我们采用递归方式进行构建树结构。为什么采用递归的形式呢,因为我们需要慢慢回朔到顶层,而且在构建的时候我们需要采用**后序遍历**的形式,这样才能保证节点最后合并孩子节点。
132+
133+
```java
134+
private void buildSegmentTree(int treeIndex, int l, int r) {
135+
if (l == r) {
136+
tree[treeIndex] = data[l];
137+
return;
138+
}
139+
int mid = l + (r - l) / 2; //中间值
140+
buildSegmentTree(leftChild(treeIndex), l, mid);
141+
buildSegmentTree(rightChild(treeIndex), mid + 1, r);
142+
tree[treeIndex] = merger.merge(tree[leftChild(treeIndex)], tree[rightChild(treeIndex)]);
143+
}
144+
```
145+
146+
### 3.5、查询操作
147+
148+
&emsp;&emsp;查询操作主要涉及的问题就是区间查找匹配的问题,所以在递归的问题匹配主要分为三种情况:
149+
+ 查询区间完全在右孩子区间上
150+
+ 查询区间完全在左孩子区间上
151+
+ 查询区间部分在左孩子区间上,部分在右孩子区间上
152+
153+
```java
154+
public E query(int queryL, int queryR) {
155+
if (queryL < 0 || queryL >= data.length || queryR < 0 || queryR >= data.length)
156+
throw new IllegalArgumentException("index is illegal");
157+
158+
return query(0,0,data.length - 1, queryL, queryR);
159+
}
160+
161+
private E query(int treeIndex, int l, int r, int queryL, int queryR) {
162+
if (l == queryL && r == queryR)
163+
return tree[treeIndex];
164+
int mid = l + (r - l) / 2; //中间值
165+
int leftTreeIndex = leftChild(treeIndex); //左索引
166+
int rightTreeIndex = rightChild(treeIndex); //右索引
167+
168+
if (queryL >= mid + 1) //去右子树查找
169+
return query(rightTreeIndex, mid + 1, r, queryL, queryR);
170+
else if (queryR <= mid) // 去左子树查找
171+
return query(leftTreeIndex, l, mid, queryL, queryR);
172+
E left = query(leftTreeIndex, l, mid, queryL, mid); // 分开查询
173+
E right = query(rightTreeIndex, mid + 1, r, mid + 1, queryR);
174+
return merger.merge(left, right); //将分开的元素合并
175+
}
176+
```
177+
178+
### 3.6、更改操作
179+
180+
&emsp;&emsp;当我们对数组的某一个索引位置进行修改,我们需要先找到这个树结构的索引位置,然后不断回朔,重新合并。
181+
182+
**程序实现:**
183+
```java
184+
public void set(int index, E e) {
185+
if (index < 0 || index >= data.length)
186+
throw new IllegalArgumentException("Index is illegal");
187+
data[index] = e;
188+
set(0, 0, data.length - 1, index, e);
189+
}
190+
191+
private void set(int treeIndex, int l, int r, int index, E e) {
192+
if (l == r){
193+
tree[treeIndex] = e;
194+
return;
195+
}
196+
int mid = l + (l - r) / 2;
197+
if (index > mid)
198+
set(rightChild(treeIndex), mid + 1, r, index, e);
199+
else
200+
set(leftChild(treeIndex), l, mid, index, e);
201+
tree[treeIndex] = merger.merge(tree[leftChild(treeIndex)], tree[rightChild(treeIndex)]);
202+
}
203+
```
204+
205+
## 最后
206+
207+
更多精彩内容,大家可以转到我的主页:[曲怪曲怪的主页](http://quguai.cn:8090/)
208+
209+
或者关注我的微信公众号:**TeaUrn**
210+
211+
或者扫描下方二维码进行关注。里面有惊喜等你哦。
212+
213+
**源码地址**:可在公众号内回复 **数据结构与算法源码** 即可获得。
214+
215+
<img src="https://markdown-liyang.oss-cn-beijing.aliyuncs.com/%E5%85%AC%E4%BC%97%E5%8F%B7%E4%BA%8C%E7%BB%B4%E7%A0%81.jpg" width=40%>
216+

0 commit comments

Comments
 (0)