《算法4》索引优先队列笔记-----java实现

一、前言

前面已经讲了优先队列(堆)的实现,https://blog.csdn.net/weixin_43696529/article/details/104672731,但是其很明显有一个缺点,那就是无法直接访问已经在队列中的元素,更新或是删除它们,在Dijistra算法中就非常需要此性质,因此要解决此问题就需用到索引优先队列

二、索引优先队列数据结构讲解

索引优先队列使用一个int[] pq数组作为索引队列, 保存对象在数组中的位置,使用T[] keys数组保存对象关联的值,使用int[] qp表示对象在索引队列pq中的位置。

如有这样一组数据:

0 -> f
1 -> a
3 -> c
5 -> r
7 -> g
10-> i

我们将0,1,3,5,7,10称为数据对象的索引

对应的pqqp以及keys数组就是:

	  0   1   2   3   4   5   6   7   8   9   10
pq:   1   3   0   7   10  5
qp:   2   0       1       5       3            4
keys: f   a       c       r       g            i

可以看到
keys[i] : i就是数据对象索引,keys[i] 就是与之关联的值 keys[0]=f,keys[1]=a
qp[i] : 对象索引在索引队列中的位置,qp[0]=2 ,对象索引 0 在队列中的位置为2
pq[i] : 即按照对应索引关联的值进行排序,如a是所有元素中最小的,因此其索引1在pq中的位置就是0,排在第一位

画出对应的二叉树会更容易理解:
在这里插入图片描述
节点外边的就是pq的下标j,节点中的数字就是对象索引即pq[j]的值,节点中的字符就是 索引关联的对象,保存在keys中

因此对于 1->a 这样一个数据,用 i 表示其索引1
则有以下关系:


x=qp[i]

pq[x]=i
keys[pq[x]]=a
keys[i] = a
pq[qp[i]] = i

数据结构如下:

public class IndexMinPQ<Key extends Comparable<? super Key>> implements Iterable<Integer> {
    /**
     * 索引优先队列,保存对象在数组中的位置,按索引值(即keys[i],i为索引)进行小堆排序
     */
    private int[] pq;
    /**
     * pq的逆序,保存对象索引在pq中的位置
     */
    private int[] qp;
    /**
     * 队列最大元素数
     */
    private int maxSize;
    /**
     * 当前队列元素数量
     */
    private int currentSize;

    /**
     * 具体元素
     */
    private Key[] keys;

    public IndexMinPQ(int maxSize) {
        if (maxSize<0){
            throw new IllegalArgumentException("参数非法");
        }
        this.maxSize = maxSize;
        pq=new int[maxSize + 1 ];
        qp=new int[maxSize + 1];
        keys= (Key[]) new Comparable[maxSize+1];
        currentSize=0;

        for (int i = 0; i < maxSize + 1; i++) {
        	//qp 初始化为-1 ,表示没有索引关联的对象
            qp[i] = -1;
        }
    }
}

这样就可以得到几个常用的方法:

  • keyOf(int i) 返回索引 i 关联的需

     public Key keyOf(int i){
            checkIndex(i);
            if (!contains(i))throw new NoSuchElementException("不存在该索引");
            return keys[i];
        }
    
  • minIndex() 返回最小的索引

     /**
         * 返回最小的索引,即索引队列pq[1]对应的索引对象
         * @return
         */
        public int minIndex(){
            if (isEmpty()){
                throw new NoSuchElementException("队列为空");
            }
            return pq[1];
        }
    
  • contains(int index) :索引index是否包含在队列中,即index是否关联了对象

      public boolean contains(int index){
            checkIndex(index);
            //如果存在,则qp[index]一定不为-1,且指向 该对象index在 索引队列pq中的位置
            return qp[index] != -1;
        }
       private void checkIndex(int index){
        if (index<0 || index>= maxSize){
            throw new IndexOutOfBoundsException("参数越界");
        }
    }   
    
三、插入操作

同优先队列思想类似。
若插入一对数据为: index -> key
这里首先将对象索引index保存在优先队列pq的最后一个元素(先让currentSize+1),因此 qp[index] = currentSize,pq[currentSize]=index,keys[index]=key`,接着从优先队列pq的最后一个节点,即刚刚加入的节点开始执行上滤操作(同堆排序中的上滤基本相同),但这里需要注意的是,在上滤的过程中,比较的是 keys[pq[i]],即比较的是索引index关联的值key,发生交换时不光要交换pq的值,还要更新qp的值。
实现如下:
插入:

/**
     * 插入一对值
     * @param index 索引
     * @param key 索引关联的值
     */
    public void  insert(int index,Key key){
        checkIndex(index);
        if (contains(index)){
            throw new IllegalArgumentException("索引"+index+"已存在");
        }
        //增加元素数
        currentSize++;
        //当前对象的索引为队列尾部
        qp[index]=currentSize;
        //该索引指向的对象在key中的位置为index
        pq[currentSize]=index;
        keys[index] = key;
        //对尾部元素上滤
        percolateUp(currentSize);
    }

上滤操作:

/**
     * 上滤
     * @param n
     */
    private void percolateUp(int n) {
    	//将n和n/2(n的父亲)比较,如果父亲大,则交换两个节点
        for (; n>1&&compareTo(n,n/2)<0  ; n/=2) {
            swap(n,n/2);
        }
    }

compareTo 比较关联的值:

  /**
     * 比较索引队列i和j上的对应的key值大小
     * @param i
     * @param j
     * @return
     */
    private int compareTo(int i,int j){
        return keys[pq[i]].compareTo(keys[pq[j]]);
    }

交换pq[i]和pq[j]的元素:

 /**
     * 交换pq[i]和pq[j]的元素
     * 并更新qp,qp[pq[i]]=i;
     * @param i
     * @param j
     */
    private void swap(int i, int j) {
        int temp= pq[i];
        pq[i] = pq[j];
        pq[j] = temp;

        qp[pq[i]] = i;
        qp[pq[j]] = j;
    }
四、删除最小键

删除最小键(即keys[pq[1]])比较简单:

  1. 获取最小键索引minIndex,并将索引队列pq的第一个元素和最后一个元素交换,并让currentSize减一
  2. 同优先队列一样,从队列第一个元素开始下滤,将第一个元素放在满足堆序的位置
  3. 此时只需让qp[minIndex]=-1keys[minIndex]=null 即可

实现:

 /**
     * 删除最小键并返回其关联的索引。
     * @return 关联的索引 即pq[1]
     */
    public int delMin(){
        if (currentSize == 0) {
            return -1;
        }

        int minIndex=pq[1];
        swap(1,currentSize--);
        percolateDown(1);
        //删除当前对象,即pq中不存在该对象了
        qp[minIndex] = -1;

        pq[currentSize+1]=-1;//不是必须的
        keys[minIndex]=null;
        return minIndex;
    }

下滤:
逻辑同优先队列(堆)

  /**
     * 下滤
     * @param k
     */
    private void percolateDown(int k) {
        int child;
        for (; k*2 <= currentSize ; k =child) {
            child= 2 * k;
            if (child < currentSize &&compareTo(child+1,child)<0){
                child++;
            }
            if (compareTo(child,k)<0){
                swap(child,k);
            }else {
                break;
            }
        }

    }
五、删除指定索引关联的key

删除指定索引的关联的key同删除最小值基本一样,只是交换时是交换pq[qp[i]]与pq[currentSize]的元素,i为待删除对象索引。
且交换完后不能只进行下滤,因为我们不确定待删除的是pq[1]所关联的对象,因此需要进行上滤和下滤两次操作。

 /**
     * 删除与索引i关联的key
     * @param i
     */
    public void delete(int i){
        checkIndex(i);
        if (!contains(i)){
            throw new IllegalArgumentException("索引不存在");
        }
        int index=qp[i];
        swap(index,currentSize--);
        //上滤
        percolateUp(index);
        //再下滤 顺序随意
        percolateDown(index);
        keys[index]=null;
        qp[i] = -1;
    }

五、修改索引i关联的key值

因为只是修改了i关联的值,所以只要修改keys[i]=newKey,而 qp[i] 和 pq[qp[i]] 指的值并没有改变。但因为pq[qp[i]] 关联的对象改变了,因此需要对qp[i] 进行上滤和下滤,重新调整索引优先队列。

/**
     * 修改索引i关联的key值
     * @param i
     * @param key
     */
    public void  changeKey(int i,Key key){
        checkIndex(i);
        if (!contains(i))throw new NoSuchElementException("不存在该索引");

        keys[i]=key;
        percolateUp(qp[i]);
        percolateDown(qp[i]);
    }
六、增大或减小索引i关联的key值

以减小为例,首先要判断新的key是否小于原key值,如果小于,则重新指定keys[i]的值,并进行上滤,因为key比原来的key小,其子节点必然比它小,但是父节点可能会比新的key大,因此只需上滤。
增大与之相反。

 /**
     * 将与索引 i 关联的键减小为指定值。
     * @param i
     * @param key
     */
    public void decreaseKey(int i,Key key){
        checkIndex(i);
        if (!contains(i))throw new NoSuchElementException("不存在该索引");
        if(keys[i].compareTo(key)<0){
            throw new IllegalArgumentException("当前key无法缩小原key值");
        }
        keys[i]=key;
        //当前key变小,因此上滤即可
        percolateUp(qp[i]);

    }
    /**
     * 将与索引 i 关联的键增加为指定值。
     * @param i
     * @param key
     */
    public void increaseKey(int i, Key key) {
        checkIndex(i);
        if (!contains(i))throw new NoSuchElementException("不存在该索引");
        if (keys[i].compareTo(key) >= 0)
            throw new IllegalArgumentException("当前key无法增加原key值");
        keys[i] = key;
        //当前key变大,因此下滤即可
        percolateDown(qp[i]);
    }

参考:
《算法第四版》2.4节

  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值