为了账号安全,请及时绑定邮箱和手机立即绑定

算法与数据结构之索引堆

标签:
算法

主要介绍索引堆,以及索引堆的优化。
<!--more-->

何为索引堆?

索引堆是对堆进行了优化。

优化了什么?

在堆中,构建堆、插入、删除过程都需要大量的交换操作。在之前的实现中,进行交换操作是直接交换datas数组中两个元素。而索引堆交换的是这两个元素的索引,而不是直接交换元素。

有什么好处?

主要有两个好处:

  • 减小交换操作的消耗,尤其是对于元素交换需要很多资源的对象来说,比如大字符串。
  • 可以根据原位置找到元素,即便这个元素已经换了位置。
如何做到的?

索引堆使用了一个新的int类型的数组,用于存放索引信息。部分代码如下:

// 属性
$data = array();// 存放数据的数组 datas[1..n]
$indexes = array(); // 索引数组

这里这个indexes数组,存放的是什么信息呢?它是如何工作的呢?假如我们有这样一个最小堆:

paste

那么用数组表示就是:

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;

看这个例子:

paste

indexes[1] = 10;
而reverses[1]存储的是在indexes数组中值为10的索引1在indexes中的位置,它的值为8,有
reverses[1] = 8;代表index数组中第8个

反向索引的维护

虽然使用反向索引提高了某些时候的查询效率,但会使得程序变得更加复杂。因为在插入和删除时都要维护这个数组。

核心思想

核心思想是:不管任何操作,都要维护indexes数组和reverse数组的性质。

和堆相关的一些问题

使用堆实现优先队列

  • 动态选择优先级最高的任务执行

paste

像操作系统的进程管理:每次都使用堆找到优先级最高的进程执行,如果来了新的进程只需要将其插入堆中,如果需要更改进行的优先级,只需要使用change函数进行更改

  • 在游戏中选择攻击的对象

paste

可以将需要攻击的敌人放入堆中,使用堆选择最需要攻击的敌人。如果有新的敌人进入则插入堆。

  • 在100万个元素中选出前100名(在N个元素中选出前M个元素)

    • 我们可以使用快速排序算法排序, 复杂度为:O(n*logN)
    • 使用优先队列:O(NlogM)
      • 使用一个最小堆,保证每次这个堆的元素都不大于100;初始先将100个元素放入这个堆中,形成最小堆,后面每加入一个元素,首先将最小的元素踢出,然后加入新的元素(需要保持堆的结构,复杂度为O(logN),遍历后面每一个元素,直到最后一个元素,最后形成的100个元素的堆,将为这100万中元素最大的100个元素。
  • 多路归并排序

paste

* merge的时候,将各个分割的字块的第一个元素形成一个最小堆,每次取堆顶元素进行merge
* 如果n个元素进行n路归并,其实归并算法就成了,堆排序算法。
  • d叉堆 d-ary heap

paste

堆的实现细节优化
  • shiftup 和shiftDown 中使用复制操作替换swap操作

原创首发于慕课网

点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
PHP开发工程师
手记
粉丝
8992
获赞与收藏
336

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消