線段樹是基於區間的統計查詢,線段樹是一種 二叉搜索樹,它將一個區間劃分成一些單元區間,每個單元區間對應線段樹中的一個葉結點。使用線段樹可以快速的查找某一個節點在若干條線段中出現的次數,時間複雜度為O(logN),線段樹是一顆 平衡二叉樹。
2.線段樹示意圖如下圖所示,數組 E中,假設區間 0-9 一共 10 個元素,每個兒子節點區間元素的個數都是父親節點元素個數的一半,若出現 奇數 的情況,則右兒子元素區間比 左兒子 元素區間多一個:
Tips:如圖所示的中節點中區間指的是數組 E 的索引值。
3.線段樹需要空間分析假設我們把 線段樹 看做是一顆 滿二叉樹,並且不考慮添加元素的情況(即區間固定),對於區間有 n 個元素的數組若 n=2^k(k是正整數) 則需要 2n 的空間,最差的情況是若 n=2^k+1 則需要 4n 的空間,如下圖所示,最下面一層沒有元素的節點使用 null 填充:
Tips: 若索引是從 i=0 開始的,左兒子 left(i) = 2*i+1,右兒子 right(i) = 2*i+2,parent(i) = (i-1)/2 取整;
對於滿二叉樹來說,需要的節點數如下:
若當 n=2^k+1 需要的的空間數:
Tips:對於區間有 n 個元素的數組若 n=2^k(k是正整數) 則需要 2n 的空間,最差的情況是若 n=2^k+1 則需要 4n 的空間就足夠了。
4.定義 SegmentTree 線段樹類其中定義了 leftSon($i) 方法,表示求某個節點左兒子節點索引值的方法,rightSon($i) 表示求某個節點右兒子節點 索引值 的方法:
<?php
class SegmentTree
{
private $data = []; //用於存儲原始數組
private $tree = []; //用於存儲線段樹節點元素的值
/**
* 構造函數 初始化線段樹
* SegmentTree constructor.
* @param array $arr
*/
public function __construct(array $arr)
{
for ($i = 0; $i < count($arr); $i++) {
$this->data[$i] = $arr[$i];
}
//若是靜態語言需要開 4n 空間來表示 $this->tree
}
public function getSize()
{
return count($this->data);
}
public function get(int $index)
{
if ($index < 0 || $index >= count($this->data)) {
echo "索引錯誤";
exit;
}
return $this->data[$index];
}
/**
* 獲取某個節點兒子節點索引,若索引是從 i=0 開始的,左兒子 left(i) = 2*i+1
* @param $i
* @return int
*/
private function leftSon($i): int
{
return $i * 2 + 1;
}
/**
* 獲取某個節點右兒子節點索引,若索引是從 i=0 開始的,右兒子 left(i) = 2*i+2
* @param $i
* @return int
*/
private function rightSon($i): int
{
return $i * 2 + 2;
}
}
接下來使用遞歸思想去 創建線段樹,下面給出遞歸函數 PHP 代碼:
if ($left == $right) {
$this->tree[$i] = $this->data[$left]; //處理遞歸到葉子節點時 並賦值最原始的 $data 對應的索引值
} else {
$leftSon = $this->leftSon($i); //左兒子索引
$rightSon = $this->rightSon($i); //右兒子索引
$mid = $left + ceil(($right - $left) / 2);//求區間中值
$this->buildSegmentTree($leftSon, $left, $mid - 1); //遞歸左兒子樹
$this->buildSegmentTree($rightSon, $mid, $right); //遞歸右兒子樹
$this->tree[$i] = $this->merge->operate($this->tree[$leftSon], $this->tree[$rightSon]); //這裡是根據業務來定節點需要存儲的元素
}
Tips:其中節點元素存儲的值需要根據業務來定,如上面代碼表示的是每個節點存儲的是 區間求和 的值,很顯然這種方式不靈活,用戶在實例化該類的時候可以傳入一個 merge 對象用於元素操作的。
6.節點元素計算規則上述SegmentTree類中可以在 __construct() 方法中傳入一個 $merge 對象,$merge 中可以定義一個 operate() 方法計算得出節點元素值,如下:
<?php
class SegmentTree
{
private $data = [];
private $tree = [];
private $merge = null; //表示的是一個 節點操作的對象
/**
* 構造函數 初始化線段樹
* SegmentTree constructor.
* @param array $arr
*/
public function __construct(array $arr, $merge)
{
$this->merge = $merge;
for ($i = 0; $i < count($arr); $i++) {
$this->data[$i] = $arr[$i];
}
//若是靜態語言需要開 4n 空間來表示 $this->tree
//遞歸創建線段樹
$this->buildSegmentTree(0, 0, count($this->data) - 1);
}
private function buildSegmentTree(int $i, int $left, int $right)
{
if ($left == $right) {
$this->tree[$i] = $this->data[$left]; //處理遞歸到葉子節點時 並賦值最原始的 $data 對應的索引值
} else {
$leftSon = $this->leftSon($i); //左兒子索引
$rightSon = $this->rightSon($i); //右兒子索引
$mid = $left + ceil(($right - $left) / 2);//求區間中值
$this->buildSegmentTree($leftSon, $left, $mid - 1); //遞歸左兒子樹
$this->buildSegmentTree($rightSon, $mid, $right); //遞歸右兒子樹
$this->tree[$i] = $this->merge->operate($this->tree[$leftSon], $this->tree[$rightSon]); //這裡是根據業務來定節點需要存儲的元素
}
}
public function getSize()
{
return count($this->data);
}
public function get(int $index)
{
if ($index < 0 || $index >= count($this->data)) {
echo "索引錯誤";
exit;
}
return $this->data[$index];
}
/**
* 獲取某個節點兒子節點索引,若索引是從 i=0 開始的,左兒子 left(i) = 2*i+1
* @param $i
* @return int
*/
private function leftSon($i): int
{
return $i * 2 + 1;
}
/**
* 獲取某個節點右兒子節點索引,若索引是從 i=0 開始的,右兒子 left(i) = 2*i+2
* @param $i
* @return int
*/
private function rightSon($i): int
{
return $i * 2 + 2;
}
}
如下定義就可以很靈活的處理每個節點的計算規則:
class Merge{
public funcrion operate($left,$right){
//這裡可以定義需要操作的規則
return $left+$right; //如求平均值,這裡可以 return ($left+$right)/2;
}
}
若是各個線段區間存儲的是區間求和,則 Merge 類中的 operate() 方法返回是兩個元素的和,代碼如下:
<?php
require 'SegmentTree.php';
class Merge
{
public function operate($left, $right)
{
return $left + $right;
}
}
$merge = new Merge();
$arr = [0,1,2,3,4,5,6,7,8,9];
$merge = new Merge();
$segmentTree = new SegmentTree($arr,$merge);
print_r($segmentTree);
輸出如下:
此時線段樹的節點元素值示意圖如下:
這裡以查詢 [2-6] 區間為例,若要查詢區間 [2-6] 的求和需要根據區間來尋找需要求的值,示意圖如下:
PHP 代碼使用遞歸思想實現如下:
public function query($qleft, $qright)
{
if ($qleft < 0 || $qright >= count($this->data) || $qright < $qleft) {
echo "索引範圍錯誤";
exit;
}
return $this->recursionQuery(0, 0, count($this->data) - 1, $qleft, $qright);
}
/**
* 遞歸查詢區間
* @param $left 當前節點區間左端值
* @param $right 當前節點區間右端值
* @param $qleft 需要查詢的區間左端值
* @param $qright 需要查詢的區間右端值
*/
private function recursionQuery($i, $left, $right, $qleft, $qright)
{
$mid = $left + ceil(($right - $left) / 2);//求區間中值向上取整
//先處理滿足區間條件的情況
if ($qleft == $left && $qright == $right) { //查詢左右端和當前節點左右端重合
return $this->tree[$i];
} elseif ($qright < $mid) { //查詢左右端在中值左邊,那麼結果區間在左兒子樹
return $this->recursionQuery($this->leftSon($i), $left, $mid - 1, $qleft, $qright);
} elseif ($qleft >= $mid) { //查詢左右端在中值右邊,那麼結果區間在右兒子樹
return $this->recursionQuery($this->rightSon($i), $mid, $right, $qleft, $qright);
} else { //中值在查詢左右端中間 將區間分成兩邊,結果在左右兒子樹上都有
$leftSon = $this->recursionQuery($this->leftSon($i), $left, $mid - 1, $qleft, $mid - 1);
$righttSon = $this->recursionQuery($this->rightSon($i), $mid, $right, $mid, $qright);
return $this->merge->operate($leftSon, $righttSon);
}
}
輸出如下:
<?php
class SegmentTree
{
private $data = [];
private $tree = [];
private $merge = null; //表示的是一個 節點操作的對象
/**
* 構造函數 初始化線段樹
* SegmentTree constructor.
* @param array $arr
*/
public function __construct(array $arr, $merge)
{
$this->merge = $merge;
for ($i = 0; $i < count($arr); $i++) {
$this->data[$i] = $arr[$i];
}
//若是靜態語言需要開 4n 空間來表示 $this->tree
//遞歸創建線段樹
$this->buildSegmentTree(0, 0, count($this->data) - 1);
}
public function query($qleft, $qright)
{
if ($qleft < 0 || $qright >= count($this->data) || $qright < $qleft) {
echo "索引範圍錯誤";
exit;
}
return $this->recursionQuery(0, 0, count($this->data) - 1, $qleft, $qright);
}
/**
* 遞歸查詢區間
* @param $left 當前節點區間左端值
* @param $right 當前節點區間右端值
* @param $qleft 需要查詢的區間左端值
* @param $qright 需要查詢的區間右端值
*/
private function recursionQuery($i, $left, $right, $qleft, $qright)
{
$mid = $left + ceil(($right - $left) / 2);//求區間中值向上取整
//先處理滿足區間條件的情況
if ($qleft == $left && $qright == $right) { //查詢左右端和當前節點左右端重合
return $this->tree[$i];
} elseif ($qright < $mid) { //查詢左右端在中值左邊,那麼結果區間在左兒子樹
return $this->recursionQuery($this->leftSon($i), $left, $mid - 1, $qleft, $qright);
} elseif ($qleft >= $mid) { //查詢左右端在中值右邊,那麼結果區間在右兒子樹
return $this->recursionQuery($this->rightSon($i), $mid, $right, $qleft, $qright);
} else { //中值在查詢左右端中間 將區間分成兩邊,結果在左右兒子樹上都有
$leftSon = $this->recursionQuery($this->leftSon($i), $left, $mid - 1, $qleft, $mid - 1);
$righttSon = $this->recursionQuery($this->rightSon($i), $mid, $right, $mid, $qright);
return $this->merge->operate($leftSon, $righttSon);
}
}
private function buildSegmentTree(int $i, int $left, int $right)
{
if ($left == $right) {
$this->tree[$i] = $this->data[$left]; //處理遞歸到葉子節點時 並賦值最原始的 $data 對應的索引值
} else {
$leftSon = $this->leftSon($i); //左兒子索引
$rightSon = $this->rightSon($i); //右兒子索引
$mid = $left + ceil(($right - $left) / 2);//求區間中值
$this->buildSegmentTree($leftSon, $left, $mid - 1); //遞歸左兒子樹
$this->buildSegmentTree($rightSon, $mid, $right); //遞歸右兒子樹
$this->tree[$i] = $this->merge->operate($this->tree[$leftSon], $this->tree[$rightSon]); //這裡是根據業務來定節點需要存儲的元素
}
}
public function getSize()
{
return count($this->data);
}
public function get(int $index)
{
if ($index < 0 || $index >= count($this->data)) {
echo "索引錯誤";
exit;
}
return $this->data[$index];
}
/**
* 獲取某個節點兒子節點索引,若索引是從 i=0 開始的,左兒子 left(i) = 2*i+1
* @param $i
* @return int
*/
private function leftSon($i): int
{
return $i * 2 + 1;
}
/**
* 獲取某個節點右兒子節點索引,若索引是從 i=0 開始的,右兒子 left(i) = 2*i+2
* @param $i
* @return int
*/
private function rightSon($i): int
{
return $i * 2 + 2;
}
}
<?php
require 'SegmentTree.php';
class Merge
{
public function operate($left, $right)
{
return $left + $right;
}
}
$merge = new Merge();
$arr = [0,1,2,3,4,5,6,7,8,9];
$merge = new Merge();
$segmentTree = new SegmentTree($arr,$merge);
echo $segmentTree->query(2,6);
代碼倉庫 :https://gitee.com/love-for-poetry/data-structure
掃碼關注愛因詩賢