优先队列,java版本

优先队列(自己动手写API系列二)

前言:假设你有n个数据 然后输入一个m 删除其中前m大的值

有人就想到了 遍历嘛 找到一个最大的 一删.

没错 这样可以 但是如果这个n和m都很大呢? 是不是要跑的很慢

有人又想了 那就排个序嘛 这样也可以 其实也不是很好

所以 接下来就有了 这个数据结构--优先队列

这里会给大家先介绍一下优先队列 然后给大家讲一下堆排序

先来看一下优先队列. 其实优先队列的原型有点像二叉树

但是其实我们在底层是用数组实现的 不会二叉树的也别怕

这是一个子树 其中结点的数字代表这个结点的编号

多画两个

让我们接下来看一个细思极恐的事情 先来看 4号结点和5号结点

4号结点除2是几? 4/2 = 2 对吗?

5号结点除2是几? 5/2 = 2.5 但是我们在里面用int型保存 所以 你得到的结果是整除

也就是 5/2 = 2

而 2号结点是什么呢? 是他们的父结点?是不是有点恐怖

巧合? 不存在的 再让我们看 2 和 3号结点

2/2 = 1                          3/2 = 1

怎么样?

至于 再多结点

你可以验证一下

好了 当前结点除2就回到了父结点

那怎么从父结点去到子结点呢?

其实就是 乘2 和乘2+1

有人就想了 为什么回到父结点只是/2 去子结点要乘 2 或者 乘2+1呢?

因为当前结点的父结点只有1个 而子结点却有两个啊

我们现在有一个约定

1.父结点比左右结点都大.

2.当一个结点有子结点 有右儿子 必须有左儿子

也就是不容许这种情况

不容许啊 也就是我们必须依次从左向右填充元素

按照我们刚才的约定 其实你应该已经发现了 1号结点就是最大的.

你要是想维持最小的也可以

好了 你先看到这里 让我们看一下 关于自己动手写API系列二 优先队列的方法

public class MaxPQ<Key extends Comparable<Key>> //类的声明 其实你接受过来的对象比如实现了Comparable接口

MaxPQ() //无参构造方法

MaxPQ(int size) // 带大小的构造方法

public boolean isEmpty()
//判断队列是否为空 是返回true

public int size()//返回队列大小

public void insert(Key Date)//插入一个数

public Key delMax() //删除最大值

这是所有的共有方法 因为 队列其实在内部处理了很多 主要是私有方法 让我们先看一下 很多东西都要要讲

private void resize(int max) 
//调整数组大小 提高数组利用率 待会讲

private void swap(int i, int j)
//交换下表为 i 和 j的两个值

private boolean cmp(int i, int j)
//比较两个值的大小

private void swim(int k)//上浮操作

private void sink(int k)//下沉操作

其中上述方法 核心是上浮和下沉操作

让我们慢慢来讲.

先来看一下讲的顺序(我觉得这样会比较好理解) 

1.两个构造方法 

2.isEmpty() 和 size()方法

3.resize()方法 swap()方法 cmp()方法

4.swim() 和 sink() 方法

5.insert() 和 delMax()方法

先给大家看一下 成员变量把

 private Key[] elements; // 对象数组

    private int N = 0; // 你记录的个数

    private int size; // resize的时候会用到 你数组的开辟的空间

1.两个构造方法

 MaxPQ() {
        this.size = 10;
        elements = (Key[]) new Comparable[size];
    }

    MaxPQ(int size) {
        this.size = size;
        elements = (Key[]) new Comparable[this.size];
    }

就是创建数组 默认的开始大小为10 第二个 有设置大小.

这个应该很好理解 就是给数组开辟空间

2.isEmpty() 和 size()方法

public boolean isEmpty() { return N == 0; }

    public int size() { return N; }

然后这也是两个简单的方法

第一个判断队列是不是空 是空的返回true 当N==0代表没有任何元素 他就是0

第二个 返回队列的大小 其实就是N的个数 因为我们用的就是N在记录

3.resize()方法 swap()方法 cmp()方法

a>resize()方法 其实我之前系列一 中写的比较详细 下面贴出来 在这里再讲一下

https://blog.csdn.net/qq_42011541/article/details/80671518

 private void resize(int max) {
        this.size = max;
        Key[] temp = (Key[]) new Comparable[this.size];
        for (int i = 1; i <= N; i++) {
            temp[i] = elements[i];
        }
        elements = temp;
    }

其实就是对数组扩容 用temp 建造一个 传过来要扩容为max大小的数组

再把之前的复制进来 让elements 的引用 指向temp 这个时候之前的数组就会被当做垃圾

JVM会去回收你的垃圾

通俗点讲解 本来你现在大小只有10 现在你又放进来一个数据 但是你发现你的数组填满了

所以你需要调用这个resize()方法 参数传20就可以 这样就实现了扩容

private void swap(int i, int j) {
        Key temp = elements[i];
        elements[i] = elements[j];
        elements[j] = temp;
    }

swap()方法这个简单的可追溯到有两个数a,b用一个c去完成交换a,b

不说了把

private boolean cmp(int i, int j) {
        return elements[i].compareTo(elements[j]) < 0;
    }

cmp这个方法也很简单 comparaTo()这个方法你觉得实在绕 就自己写.

其实就是v.comparaTo(m) v 比 m 小 返回 小于0的值

相等 返回0 v 比 m大 返回大于0的值

其实就是做判断的 一般和swap() 连起来用

4.swim() 和 sink() 方法

这两个方法 我一开始其实有提到

我们想一想 k/2的那个操作 回到父结点

而swim() 这个方法其实就是上浮的意思.

想一想

如果你有一个新数 肯定被排到了末尾了 而这个数非常大呢?

他应该在的位置是不是不应该在末尾啊

所以 让他上浮到他应有的位置就好了.

其实我每进来一个数 都让他上浮 其实 本来就是遵循我们的约定的

随便画几个给你看一下过程 

画图理解一下 

注:这下结点里面的数字我就填写数字的大小了!!

然后现在有一个新的结点 数值为17

明显他不应该在这里 他其实 应该最大的 在最上层对吧 

所以 第一步 先问一下 我17比父节点大还是小

然后发现我比我父结点都大 那我做父结点把

然后 现在我还得再问 我17比我的父结点10哪个大.

嗯. 我比你大. 那还是我做父结点吧

然后继续问 因为我没有碰到一个比我大我的 我也还没有碰到头 

假设 如果我不是17 是 9的话 是不是和8换一次就停止了?

但是 现在我大 我还得换 发现又比13大了

最后就是这样的了 你发现换完 其实 结点还都是符合我们的约定的

父结点都比子结点大

其实 每一个进来都上浮 不会破坏这个队列的结构

所以 swim() 代码 你可以理解了嘛? 这里 你可以大概先写一下 不要着急往后看

源码

private void swim(int k) {
        while (k > 1 && cmp(k/2, k)) {
            swap(k/2,k);
            k = k/2;
        }
    }

看看这个k是不是已经是最大的父结点了 所以 k>1 这个很重要

而且 k>1 &&cmp(k/2, k)的顺序不能颠倒

 你可以试试为什么

可能会报一个空指针异常 原理是什么 动一下小脑子就明白了

cmp()判断一下k/2(父结点) 是不是比 k(子结点)小了

成立我就交换 然后 k = k/2 继续搜索

sink() 可能会比swim()稍微难一下 不过也并没有那么难

我们怎么删除最大的 你看看我这样做合理不?

1.我第一个结点也就是最大的了.我让第一个结点直接等于null

然后判断一下 左儿子大还是右儿子 让大的当父结点

嗯...貌似有点道理. 但是这是错的!!! 我们这里是数组啊.. 并不是链表

有人可能说了 你可以全部往前挪一位嘛. 那这样是不是还要跑一个线性(也就是遍历一遍)

这样就破坏了我们 高效的初衷

最好的操作应该是.

第一步 : 用一个临时变量保存最大的.

第二步 : 交换最大的和最后一位.

第三步 : 让最后一位变成null 因为第二步的时候 最大的已经到最后一位了

我们现在这个第三步就相当于删除了

第四步 : 让现在的第一位也就是后换上去的 那个 进行 下沉操作 下沉到他应该有的地方

看一下图解

还是这个样子 我现在要删除17 你看怎么办?

第一步 先找个临时变量 保存 最大的

然后 最后一个和第一个交换位置

外面多出来的那个就临时的

然后 最后一位和第一位交换位置

然后 让 最后一位 为空

然后就是让 8 的那个节点 下沉了

我现在要问的就是 我 8 和我的左儿子(13) 右儿子(12) 有没有比我大的 有的话 我肯定得换一个最大的

才能完美的保证那个约定

所以 我第一步判断一下我的左儿子大还是我的右儿子大.

左儿子大 就拿左二子和父结点比较 否则拿右儿子 和 父结点比较

我发现 我左儿子最大 且比父结点大 所以换位置 

还得继续判断啊 发现左二子 10大 而且也比8大 所以交换

看一下我还有儿子 发现 我儿子都没我大 返回 这是不是又归位了? 看一下 8个数我只比较了3次 

完美的全是了 log2N的时间复杂度

想一想看看自己能不能写出来 我要扔源码了

private void sink(int k) {
        while ( k*2 <= N ) {//判断还有没有左儿子 因为你有儿子肯定有左儿子 不一定有右儿子
            int temp = k * 2; //temp 指向左儿子的结点
            if (temp < N && cmp(temp,temp+1)) temp++; // 判断一下是不是只有左儿子 并且哪个儿子大
            if (cmp(temp, k)) break;//判断一下最大的儿子和父结点大还是小
            swap(temp, k);
            k = temp;
        }
    }

还是一样的

if (temp < N && cmp(temp,temp+1)) temp++; 

这一行的判断顺序很重要

然后temp就是你最大的儿子的那个结点编号了

问一下父结点和最大儿子哪个大 儿子大 交换 否则返回

好了 sink() 讲完了 

其实我的insert() 和 delMax() 也讲完了 

哈哈

 public void insert(Key Date) {
        if (N + 1 == size) resize(size << 1);
        elements[++N] = Date;
        swim(N);
    }

    public Key delMax() {
        Key temp = elements[1];
        swap(1, N);
        elements[N--] = null;
        sink(1);
        if(N < size/4) resize(size >> 1);
        return temp;
    }

看一下 是不是我们之前讲的步骤?

其实 你在这里多理解一下sink和 swim的真谛 堆排序真的就很简单了

public static void sort(Comparable[] arr) {
        int n = arr.length;
        for (int k = n/2; k >= 1; k--) {
            sink(arr, k, n);
        }
        while (n > 1) {
            swap(arr,1,n--);
            sink(a,1,n);
        }
    }

这是堆排代码 我之前sink 你改一下 参数就好 加了个数组对象也就是之前的elements

n代表个数.

其实 第一个for循环只是在维护一个无序数组变成一个有序的堆.也就是我们队列那样的约束

然后 后面遍历每一个去排序

这里可以自己试试

猜你喜欢

转载自blog.csdn.net/qq_42011541/article/details/80678405