主要介绍索引堆,以及索引堆的优化。
<!--more-->
索引堆是对堆进行了优化。
优化了什么?在堆中,构建堆、插入、删除过程都需要大量的交换操作。在之前的实现中,进行交换操作是直接交换datas数组中两个元素。而索引堆交换的是这两个元素的索引,而不是直接交换元素。
有什么好处?主要有两个好处:
- 减小交换操作的消耗,尤其是对于元素交换需要很多资源的对象来说,比如大字符串。
- 可以根据原位置找到元素,即便这个元素已经换了位置。
索引堆使用了一个新的int类型的数组,用于存放索引信息。部分代码如下:
// 属性
$data = array();// 存放数据的数组 datas[1..n]
$indexes = array(); // 索引数组
这里这个indexes数组,存放的是什么信息呢?它是如何工作的呢?假如我们有这样一个最小堆:
那么用数组表示就是:
datas: [-, 1, 15, 20, 34, 7]
现在要维护最小堆的有序性,就需要交换15和7这两个元素。交换之后的元素数组是:
datas: [-, 1, 7, 20, 34, 15]
而此时,我们再想找到原来在datas[2]位置的元素,已经找不到了。因为此时data[2]已经换成了7,而系统并没有记录15被换到了什么地方。
可不可以既保持$data的原始特性(读取O(1))想要得到i位置的元素,直接datas[i]就可以了, 也保持堆的特性。可以的,使用索引堆。
使用索引堆使用索引堆后,初始化两个数组应该是这样的:
$datas: [-, 1, 15, 20, 34, 7]
$indexes: [-, 1, 2, 3, 4, 5]
这个时候,我们就交换indexes数组里面的索引2和5,而不操作datas数组。交换后两个数组是这个样子:
$datas: [-, 1, 15, 20, 34, 7]
$indexes: [-, 1, 5, 3, 4, 2]
这个时候,想要得到i位置的元素,就需要$datas[$indexes[i]]来获取。
代码实现:<?php
// require('../Library/SortTestHelper.php');
require('../SortingAdvance/QuickSort.php');
/**
* 索引堆
*/
class IndexMaxHeap{
private $data;
private $count;
private $indexes;
public function __construct(){
$this->data = array();
$this->indexes = array();
$this->count = 0;
}
// public function __construct($arr){
// }
public function insert($item){
//从1开始
$this->data[$this->count + 1] = $item;
$this->indexes[$this->count + 1] = $item;
$this->_shiftUp($this->count+1);
$this->count++;
}
public function extractMax(){
$ret = $this->data[$this->indexes[1]];
swap( $this->indexes, 1 , $this->count);
$this->count--;
$this->_shiftDown(1);
return $ret;
}
/**
* [extractMaxIndex 让外界感觉从0开始]
* @return [type] [description]
*/
public function extractMaxIndex(){
$ret = $this->indexes[1] - 1;
swap( $this->indexes, 1 , $this->count);
$this->count--;
$this->_shiftDown(1);
return $ret;
}
public function getMaxIndex(){
return $this->indexes[1] - 1;
}
public function getMax(){
return $this->data[1];
}
public function isEmpty(){
return $this->count == 0;
}
public function getData(){
return $this->data;
}
/**
* [change 修改一个元素的值]
* @param [type] $i [description]
* @param [type] $newItem [description]
* @return [type] [description]
*/
public function change( $i , $newItem ){
$i += 1;
$this->data[$i] = $newItem;
// 找到indexes[j] = i, j表示data[i]在堆中的位置
// 之后shiftUp(j), 再shiftDown(j)
for( $j = 1 ; $j <= $this->count ; $j ++ ){
if( $this->indexes[$j] == $i ){
shiftUp($j);
shiftDown($j);
return;
}
}
}
/**
* [_shiftUp 新加入到堆中的元素直接放在数组后面,再与父元素比较后交换位置,直到根节点]
* @param [type] $k [description]
* @return [type] [description]
*/
private function _shiftUp($k){
//如果叶子节点的值比父元素大交换位置,并更新k的值
while( $k > 1 && $this->data[$this->indexes[(int)($k/2)]] < $this->data[$this->indexes[$k]] ){
// swap( $this->data[(int)($k/2)], $this->data[$k] );
swap( $this->indexes, (int)($k/2) , $k);
$k = (int)($k/2);
}
}
/**
* [_shiftDown 元素出堆的时候,需要维护此时的堆依然是一个大根堆, 此时将数组元素的最后一个值与第一个值交换,后从上往下维护堆的性质]
* @param [type] $k [description]
* @return [type] [description]
*/
private function _shiftDown($k){
//2k代表该节点的左子节点
while( 2*$k <= $this->count ){
$j = 2*$k;
//判断右节点是否存在,并且右节点大于左节点
if( $j+1 <= $this->count && $this->data[$this->indexes[$j+1]] > $this->data[$this->indexes[$j]] ) $j ++;
if( $this->data[$this->indexes[$k]] >= $this->data[$this->indexes[$j]] ) break;
// swap( $this->data[$k] , $this->data[$j] );
swap( $this->indexes, $k , $j );
$k = $j;
}
}
}
function heapSortUsingIndexMaxHeap($arr, $n){
$indexMaxHeap = new IndexMaxHeap();
for( $i = 0 ; $i < $n ; $i ++ ){
$indexMaxHeap -> insert($arr[$i] );
}
print("形成大根索引堆后, 从大大小输出为:\n");
for( $i = $n-1 ; $i >= 0 ; $i -- ){
// $arr[$i] = $indexMaxHeap -> extractMax();
$tmp = $indexMaxHeap -> extractMax();
print($tmp."\n");
}
}
$n = 10;
$arr = generateRandomArray($n, 0, $n);
print_r("生成的元素数组为:\n");
print_r( $arr);
$arr = heapSortUsingIndexMaxHeap($arr, $n);
?>
测试结果:
生成的元素数组为:
Array
(
[0] => 5
[1] => 7
[2] => 3
[3] => 2
[4] => 1
[5] => 6
[6] => 6
[7] => 3
[8] => 7
[9] => 9
)
形成大根索引堆后, 从大大小输出为:
7
7
6
6
6
6
5
3
3
1
反向索引
接着上面的Case,我们现在能够获得类似于这样的数据:arr排序后,第2大的数
arr[indexes[1]]
而现在有这样一个需求:我想知道原来arr数组中第i个位置,排好序后在哪个位置。应该怎样做?
常规的方法是遍历indexes数组,像这样:
for( $j = 1 ; $j <= $this->count ; $j ++ ){
if( $this->indexes[$j] == $i ){
shiftUp($j);
shiftDown($j);
return;
}
}
这个复杂度最差为O(N);
那么有没有什么方法可以提高性能呢?
有,那就是再一用一个数组reverses,作为反向索引。反向索引存放的数据通俗来讲就是这样:
reverses[i] == j
indexes[j] == i
进而推导出:
reverses[indexes[i]] = i;
indexes[reverses[i]] = i;
看这个例子:
indexes[1] = 10;
而reverses[1]存储的是在indexes数组中值为10的索引1在indexes中的位置,它的值为8,有
reverses[1] = 8;代表index数组中第8个
反向索引的维护
虽然使用反向索引提高了某些时候的查询效率,但会使得程序变得更加复杂。因为在插入和删除时都要维护这个数组。
核心思想
核心思想是:不管任何操作,都要维护indexes数组和reverse数组的性质。
和堆相关的一些问题使用堆实现优先队列
- 动态选择优先级最高的任务执行
像操作系统的进程管理:每次都使用堆找到优先级最高的进程执行,如果来了新的进程只需要将其插入堆中,如果需要更改进行的优先级,只需要使用change函数进行更改
- 在游戏中选择攻击的对象
可以将需要攻击的敌人放入堆中,使用堆选择最需要攻击的敌人。如果有新的敌人进入则插入堆。
-
在100万个元素中选出前100名(在N个元素中选出前M个元素)
- 我们可以使用快速排序算法排序, 复杂度为:O(n*logN)
- 使用优先队列:O(NlogM)
- 使用一个最小堆,保证每次这个堆的元素都不大于100;初始先将100个元素放入这个堆中,形成最小堆,后面每加入一个元素,首先将最小的元素踢出,然后加入新的元素(需要保持堆的结构,复杂度为O(logN),遍历后面每一个元素,直到最后一个元素,最后形成的100个元素的堆,将为这100万中元素最大的100个元素。
- 多路归并排序
* merge的时候,将各个分割的字块的第一个元素形成一个最小堆,每次取堆顶元素进行merge
* 如果n个元素进行n路归并,其实归并算法就成了,堆排序算法。
- d叉堆 d-ary heap
- shiftup 和shiftDown 中使用复制操作替换swap操作
原创首发于慕课网