实现堆

原文:https://www.studytonight.com/advanced-data-structures/implementing-heaps

在上一篇文章中,我们了解了堆的所有知识。现在是实施它们的时候了。数组是实现堆的首选,因为堆有完整的二叉树属性,我们可以确信不会浪费数组位置。

让我们考虑如下所示的二进制堆,这个堆可以用一个数组来表示,如下图所示。

Array Heap

实现堆

让我们看看如何实现一个堆数据结构。

1.堆的结构

我们需要一个数组来存储堆的元素,此外,我们还需要一个变量来表示堆的大小。考虑下面的代码片段:

private int[] items = new int[10];
private int size;

初始容量也可以是动态的,但这里我选择了 10 的固定长度来表示相同的容量。

2.插入到堆中

当我们想要将一个项目插入到堆中时,我们将简单地将它插入到数组中,然后如果它不满足任何堆属性,就执行bubbleUp()操作。代码如下所示:

private void insert(int item){
        if(isFull()){
            throw new IllegalStateException();
        }
        items[size++] = item;
        bubbleUp();
}

一个案例可能会到达堆已满的地方,然后如果我们试图向其中插入更多的项目,它应该会返回一个异常。isFull()方法是这样的:

private boolean isFull(){ return size == items.length; }

在这个isFull()调用返回 false 之后,我们向前移动,将项目插入到我们的堆中,并增加数组的大小。现在,最重要的部分是纠正我们在堆中插入的这个元素的位置。我们用bubbleUp()方法来做。bubbleUp()方法代码如下:

private void bubbleUp(){
        int index = size - 1;
        while(index > 0 && items[index] > items[parent(index)]){
            swap(index,parent(index));
            index = parent(index);
        }
}

在上面的方法中,我们获取刚刚插入的元素的索引,然后在这个元素比父元素大的情况下用父元素交换它,同时我们更新索引,这样我们就不会选择错误的元素。swap()方法是这样的:

private void swap(int left,int right){
        int temp = items[left];
        items[left] = items[right];
        items[right] = temp;
}

还需要注意的是,我们在bubbleUp()方法中调用的是一个父方法,该父方法返回给我们一个当前索引的父。该方法的代码如下所示:

private int parent(int index){ return (index - 1)/2; }

以上方法是我们将元素插入二进制堆所需的全部方法。

3.从堆中移除

与插入堆相比,从堆中移除元素是一个有点棘手的过程,因为在这个例子中,我们需要bubbleDown(),这个向下冒泡的过程包括检查父元素的有效性,以及尽可能向左插入树以保持完整的二叉树属性的方法。

remove()方法是这样的:

private void remove(){
        if(isEmpty()){
            throw new IllegalStateException();
        }
        items[0] = items[--size];
        bubbleDown();
}

第一个检查是确保我们没有试图从空堆中移除任何东西。然后我们在开始处插入元素,然后bubbleDown()将元素放到它的正确位置。bubbleDown()方法是这样的:

private void bubbleDown(){
        int index = 0;
        while(index <= size && !isValidParent(index)){
            int largerChildIndex = largerChildIndex(index);
            swap(index,largerChildIndex);
            index = largerChildIndex;
        }
}

我们正在检查我们拥有的索引应该小于大小,并确保父节点不应该是有效的(应该小于子值和其他检查)。largerChildIndex()方法返回给我们子项目,我们将在向下冒泡时用它替换当前项目。largerChildIndex()的代码是这样的:

private int largerChildIndex(int index){
        if(!hasLeftChild(index)) return index;
        if(!hasRightChild(index)) return leftChildIndex(index);
        return (leftChild(index) > rightChild(index)) ? leftChildIndex(index) : rightChildIndex(index);
}

isValidParent()方法代码如下:

private boolean isValidParent(int index){
        if(!hasLeftChild(index)) return true;
        var isValid = items[index] >= leftChild(index);
        if(hasRightChild(index)){
            isValid &= items[index] >= rightChild(index);
        }
        return isValid;
}

在上面的代码片段中,我们只是确保父级是有效的,并且如果左边的子级是 null,那么我们返回 true。leftChild()rightChild()hasLeftChild()hasRightChild()的方法是这样的:

private int leftChild(int index){
        return items[leftChildIndex(index)];
}

private int rightChild(int index){
        return items[rightChildIndex(index)];
}

private boolean hasLeftChild(int index){
        return leftChildIndex(index) <= size;
}

private boolean hasRightChild(int index){
        return rightChildIndex(index) <= size;
}

上面的方法调用方法来获取二进制堆元素的左右子元素,它们是:

private int leftChildIndex(int index){
        return 2*index + 1;
}

private int rightChildIndex(int index){
        return 2*index + 2;
}

现在,我们已经完成了从堆中插入或删除元素所需的所有方法。完整的代码如下:

public class Heap {

    private int[] items = new int[10];
    private int size;

    private void insert(int item){
        if(isFull()){
            throw new IllegalStateException();
        }
        items[size++] = item;
        bubbleUp();
    }

    private void remove(){
        if(isEmpty()){
            throw new IllegalStateException();
        }
        items[0] = items[--size];
        bubbleDown();
    }

    private int largerChildIndex(int index){
        if(!hasLeftChild(index)) return index;
        if(!hasRightChild(index)) return leftChildIndex(index);
        return (leftChild(index) > rightChild(index)) ? leftChildIndex(index) : rightChildIndex(index);
    }

    private boolean isValidParent(int index){
        if(!hasLeftChild(index)) return true;
        var isValid = items[index] >= leftChild(index);
        if(hasRightChild(index)){
            isValid &= items[index] >= rightChild(index);
        }
        return isValid;
    }

    private int leftChild(int index){
        return items[leftChildIndex(index)];
    }

    private int rightChild(int index){
        return items[rightChildIndex(index)];
    }

    private int leftChildIndex(int index){
        return 2*index + 1;
    }

    private int rightChildIndex(int index){
        return 2*index + 2;
    }

    private boolean hasLeftChild(int index){
        return leftChildIndex(index) <= size;
    }

    private boolean hasRightChild(int index){
        return rightChildIndex(index) <= size;
    }

    private boolean isFull(){
        return size == items.length;
    }

    private boolean isEmpty(){
        return size == 0;
    }

    private void bubbleUp(){
        int index = size - 1;
        while(index > 0 && items[index] > items[parent(index)]){
            swap(index,parent(index));
            index = parent(index);
        }
    }

    private void bubbleDown(){
        int index = 0;
        while(index <= size && !isValidParent(index)){
            int largerChildIndex = largerChildIndex(index);
            swap(index,largerChildIndex);
            index = largerChildIndex;
        }
    }

    private int parent(int index){
        return (index - 1)/2;
    }

    private void swap(int left,int right){
        int temp = items[left];
        items[left] = items[right];
        items[right] = temp;
    }

    private int getMax(){
        return items[0];
    }

    public static void main(String[] args) {
        Heap heap = new Heap();
        heap.insert(10);
        heap.insert(5);
        heap.insert(17);
        heap.insert(4);
        heap.insert(22);
       /// heap.remove();
        System.out.println("Done!");
    }
}

结论

  • 我们学习了如何实现堆,主要是 bubbleUp()bubbleDown() 方法。