用JAVA模拟堆的特性

一、堆的特性

这里的堆统指储存数据的结构方式,并不是内存堆栈,首先它具有以下特性:

  • 堆中任意节点的值总是不大于(不小于)其子节点的值;
  • 堆是完全二叉树
  • 常常用数组实现

它是完全二叉树,除了树的最后一层节点不需要是满的,其它的每一层从左到右都是满的。注意下面两种情况,第二种最后一层从左到右中间有断隔,那么也是不完全二叉树。

  1. 它通常用数组来实现。这种用数组实现的二叉树,假设节点的索引值为index,那么:节点的左子节点是 2*index+1,节点的右子节点是 2*index+2,节点的父节点是 (index-1)/2。
  2. 堆中的每一个节点的关键字都大于(或等于)这个节点的子节点的关键字。

最大堆

 

从1到n编号,就得到结点的一个线性系列。每一层结点个数恰好是上一层结点个数的2倍,也因此通过一个节点的编号就可以推知他的左右孩子节点的编号。

 

通过分析和数学归纳可以得出一个结论,很方便的知道他的左右孩子节点和父节点。

  1. 父节点 parent(i) = (i - 1) / 2,算下结点10的父节点 (7 - 1) / 2 = 3 就是 60 
  2. 左孩子 left child(i) = 2 * i + 1,可以算出 10 的左孩子 7 * 2 + 1 = 15 > 7 (这里的7为最大索引值)没有左孩子这个结点
  3. 右孩子 right child(i) = 2 * i + 2,可以算出 10 的右孩子 7 * 2 + 2 = 16 > 7 没有右孩子这个结点

这里要注意堆和二叉搜索树的区别,二叉搜索树中所有节点的左子节点关键字都小于右子节点关键字,在二叉搜索树中通过一个简单的算法就可以按序遍历节点。但是在堆中,按序遍历节点是很困难的,如上图所示,堆只有沿着从根节点到叶子节点的每一条路径是降序排列的,指定节点的左边节点或者右边节点,以及上层节点或者下层节点由于不在同一条路径上,他们的关键字可能比指定节点大或者小。所以相对于二叉搜索树,堆是弱序的,要想遍历其中的节点很困难。

二、堆排序

我们知道,堆分为"最大堆"和"最小堆"。最大堆通常被用来进行"升序"排序,而最小堆通常被用来进行"降序"排序。 鉴于最大堆和最小堆是对称关系,理解其中一种即可。本文将对最大堆实现的升序排序进行详细说明。

堆每添加一个元素,则将其与父节点进行比较,如果新添加节点小于等于父节点,则添加元素到该位置;否则,继续向上寻找父节点,直到找到某个位置,使得位于该位置的新元素的值小于等于对应父节点的元素的值,并且将原位置上的元素向后挪动。

我们先构建一个最大堆:

public class MaxHeap<E extends Comparable<E>> {
    //使用数组存储
     private Array<E> data;
     public MaxHeap(){
         data = new Array<>();
     }
     public MaxHeap(int capacity){
        data = new Array<>(capacity);
     }
     public int size(){
         return this.data.getSize();
     }
     public boolean isEmpty(){
        return this.data.isEmpty();
     }
     
     //根据当前节点索引index计算其父节点的索引
     private int parent(int index) {
         if(index ==0){
             throw new IllegalArgumentException("该节点为根节点");
         }
        return (index - 1) / 2;//这里为什么不分左右?因为java中 / 运算符只保留整数位。
     }
 
     
     //返回索引为 index 节点的左孩子节点的索引
     private int leftChild(int index){
         return index*2 + 1;
     }
 
     //返回索引为 index 节点的右孩子节点的索引
     private int rightChild(int index){
        return index*2 + 2;
     }
 }

上浮shift up

现在再来模拟上浮shift up操作,时间复杂度为O(logn)(在最大二叉堆中,要堆化一个元素,需要向上查找,找到它的父节点,大于父节点则交换两个元素,重复该过程直到每个节点都满足堆的性质为止。这个过程我们称之为上浮操作。):

我们先用图来说明:假设我们对下面的最大堆加入新元素52,放在数组最后一位,52大于父节点16,因此需要调整

首先交换索引为5和11数组中数值的位置吗,也就是52与16互换。

 此时依然52大于父节点41,继续交换。

此时满足堆的定义,这就是上浮shift up。

用代码表示:

public void add(E e){
        // 向数组尾部添加元素
         this.data.addLast(e);
         siftUp(data.getSize() - 1);
     }
 
     private void siftUp(int k) {
         // 上浮,如果大于父节点,进行交换
         while(k > 0 && get(k).compareTo(get(parent(k))) > 0){
             data.swap(k, parent(k));
             k = parent(k);
        }
     }

下沉shift down

一般的如果我们取出堆顶元素,我们选择将该数组中的最后一个元素替换堆顶元素,返回堆顶元素,删除最后一个元素。然后再对该元素做下沉操作 sift down,时间复杂度为O(logn)。

 先取出最大元素,我们会将最后一位数也就是16放到根节点,此时不满足最大堆的定义。

 调整的过程是将这个根节点 16 一步一步向下挪,16 比子节点都小,先比较子节点 52 和 30 哪个大,和大的交换。

 继续比较 16 的子节点 28 和 41,41 大,所以 16 和 41 交换位置。

 最后满足堆的特性。

用代码实现:

public E extractMax(){
          E ret = findMax();
          this.data.swap(0, (data.getSize() - 1));
          data.removeLast();
          siftDown(0);
          return ret;
     }
     //下沉shift down
     public void siftDown(int k){
         while(leftChild(k) < data.getSize()){
             // 从左节点开始,如果左节点小于数组长度,就没有右节点了
             int j = leftChild(k);
             if(j + 1 < data.getSize() && get(j + 1).compareTo(get(j)) > 0){
             // 选举出左右节点最大的那个
                 j ++;
             }
             if(get(k).compareTo(get(j)) >= 0){
             // 如果当前节点大于左右子节点,循环结束
                break;
             }
             data.swap(k, j);
             k = j;
         }
     }

完整代码:

public class MaxHeap<E extends Comparable<E>> {
    //使用数组存储
     private Array<E> data;
     public MaxHeap(){
         data = new Array<>();
     }
     public MaxHeap(int capacity){
        data = new Array<>(capacity);
     }
     public int size(){
         return this.data.getSize();
     }
     public boolean isEmpty(){
        return this.data.isEmpty();
     }
     
     //根据当前节点索引index计算其父节点的索引
     private int parent(int index) {
         if(index ==0){
             throw new IllegalArgumentException("该节点为根节点");
         }
        return (index - 1) / 2;//这里为什么不分左右?因为java中 / 运算符只保留整数位。
     }
 
     
     //返回索引为 index 节点的左孩子节点的索引
     private int leftChild(int index){
         return index*2 + 1;
     }
 
     //返回索引为 index 节点的右孩子节点的索引
     private int rightChild(int index){
        return index*2 + 2;
     }
     
     //向堆中添加元素
     public void add(E e){
        // 向数组尾部添加元素
         this.data.addLast(e);
         siftUp(data.getSize() - 1);
     }
 
     private void siftUp(int k) {
         // 上浮,如果大于父节点,进行交换
         while(k > 0 && get(k).compareTo(get(parent(k))) > 0){
             data.swap(k, parent(k));
             k = parent(k);
        }
     }
     
     //取出堆中最大元素
     public E extractMax(){
          E ret = findMax();
          this.data.swap(0, (data.getSize() - 1));
          data.removeLast();
          siftDown(0);
          return ret;
     }
     //下沉shift down
     public void siftDown(int k){
         while(leftChild(k) < data.getSize()){
             // 从左节点开始,如果左节点小于数组长度,就没有右节点了
             int j = leftChild(k);
             if(j + 1 < data.getSize() && get(j + 1).compareTo(get(j)) > 0){
             // 选举出左右节点最大的那个
                 j ++;
             }
             if(get(k).compareTo(get(j)) >= 0){
             // 如果当前节点大于左右子节点,循环结束
                break;
             }
             data.swap(k, j);
             k = j;
         }
     }
 }

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值