Java數據結構之堆和優先隊列


概述

在談堆之前,我們先了解什么是優先隊列。我們每天都在排隊,銀行,醫院,購物都得排隊。排在隊首先處理事情,處理完才能從這個隊伍離開,又有新的人來排在隊尾。但僅僅這樣就能滿足我們生活需求嗎,明顯不能。醫院里,患者排隊准備看病,這時有個重症患者入隊,醫生如果按隊列的方式一個一個往下處理,等排到這位重病患者時,可能他就因為傷情過重掛了,之后就會引發醫患糾紛,這明顯不是我們想要的結果。優先隊列就成為我們解決此類事情的關鍵,重病患者入隊(掛號),醫生根據他的傷情緊急(優先級)優先處理他的病情。

 如果非要用專業術語來區分他們二者的區別

  1. 隊列先進先出,后進后出
  2. 優先隊列,出隊與入隊時的順序無關,與優先級有關。

了解了優先隊列,那這個堆又是什么玩意,可能很多人聽過內存堆棧。特別要聲明和注意的是,這里的堆僅僅是存儲數據的一種結構方式,與內存的堆棧不是一個概念。

  1. 二叉堆是一顆完全二叉樹結構(不懂什么是樹的同學請面壁),說的通俗點,堆就是滿足一些特殊性質的樹,所以二叉堆就是有特殊性質的二叉樹。
  2. 父節點的值大於(小於)兩個子節點的值,又稱為最大堆和最小堆,我們要定義的是最大堆(最小堆跟他相反)。

實例

我們先來看下什么是滿的二叉樹

每一層所有節點都有兩個兒子結點的二叉樹,就叫滿的二叉樹,計算他節點個數的公式2^3 - 1 = 7。有七個節點

完全二叉樹(最大堆)

堆和優先隊列有什么關系

知道了什么是堆和優先隊列,它們之間有什么關系哪。說穿了就一句話,堆是優先隊列這種數據結構的一種實現方式。

注意:優先隊列可以用不同的底層實現(普通線性結構),時間復雜度不同。

數組實現完全二叉樹(最大堆)

 也可以定義二叉樹來實現完全二叉樹,但是通過觀察會發現其結構的特點,都是用順序存儲方式存儲。從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 沒有右孩子這個結點

定義一個我們自己的數組Array類,也可以用Java提供的Array

Array類

public class Array<E> {

    private E[] data;

    private int size;

    //構造函數,傳入數組的容量capacity構造Array
    public Array(int capacity) {
        this.data = (E[]) new Object[capacity];
        size = 0;
    }
//無參數構造函數
    public Array() {
        this(10);
    }

    //獲取數組的個數
    public int getSize() {
        return size;
    }

    //獲取數組的容量
    public int getCapacity() {
        return data.length;
    }

    //數組是否為空
    public boolean isEmpty() {
        return size == 0;
    }

    //添加最后一個元素
    public void addLast(E e) {
        add(size,e);
    }

    //添加第一個元素
    public void addFirst(E e){
        add(0,e);
    }

    //獲取inde索引位置的元素
    public E get(int index){
        if (index < 0 || index >= size){
            throw new IllegalArgumentException("Get failed,index is illegal");
        }
        return data[index];
    }

    public void set(int index,E e){
        if (index < 0 || index >= size){
            throw new IllegalArgumentException("Get failed,index is illegal");
        }
        data[index] =  e;
    }

    //獲取最后一個元素
    public E getLast(){
        return this.get(size - 1);
    }

    //獲取第一個元素
    public E getFirst(){
        return this.get(0);
    }


    //添加元素
    public void add(int index,E e){
        if (index > size || index < 0){
            throw new IllegalArgumentException("add failed beceause index > size or index < 0,Array is full.");
        }
        if (size == data.length){
            resize(data.length * 2);
        }
        for (int i = size - 1; i >= index; i--) {
            data[i+1] = data[i];
        }
        data[index] = e;
        size ++;

    }

    //擴容數組
    private void resize(int newCapacity) {
        E[] newData = (E[]) new Object[newCapacity];
        for (int i = 0; i < size; i++) {
            newData[i] = data[i];
        }
        data = newData;
    }

    public E[] getData() {
        return data;
    }

    //查找數組中是否有元素e
    public boolean contains(E e){
        for (int i = 0; i < size; i++) {
            if (data[i].equals(e)){
                return true;
            }
        }
        return false;
    }

    //根據元素查看索引
    public int find(E e){
        for (int i = 0; i < size; i++) {
            if (data[i].equals(e)){
                return i;
            }
        }
        return -1;
    }

    //刪除某個索引元素
    public E remove(int index){
        if(index < 0 || index >= size){
            throw new IllegalArgumentException("detele is fail,index < 0 or index >= size");
        }
        E ret = data[index];
        for (int i = index + 1; i < size; i++) {
            data[i - 1] = data[i];
        }
        size --;
        data[size] = null;
        if (size < data.length / 2){
            resize(data.length / 2);
        }
        return  ret;
    }

    //刪除首個元素
    public E removeFirst(){
        return this.remove(0);
    }

    //刪除最后一個元素
    public E removeLast(){
        return this.remove(size - 1);
    }

    //從數組刪除元素e
    public void removeElemen(E e){
        int index = find(e);
        if (index != -1){
            remove(index);
        }

    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("");
        sb.append(String.format("Array:size = %d,capacity = %d \n",size,data.length));
        sb.append("[");
        for (int i = 0; i < size; i++) {
            sb.append(data[i]);
            if (i != size - 1){
                sb.append(",");
            }
        }
        sb.append("]");
        return sb.toString();
    }
}

有了Array數組類,接下來很快的,把我們剛才描述的事情用代碼實現出來之后,在考慮出隊和入隊的操作,因為父節點要大於或小於他們的子節點。所以我們的節點要能相互比較,在Java繼承Comparable類就可以了。

最大堆(MaxHeap類)

public class MaxHeap<E extends Comparable<E>>
{
    private Array<E> data;

    public MaxHeap(int capacity)
    {
        data = new Array<>(capacity);
    }

    public MaxHeap()
    {
        data = new Array<>();
    }

    //堆里的元素個數
    public int size()
    {
        return data.getSize();
    }

    //堆是否為空
    public boolean isEmpty()
    {
        return data.isEmpty();
    }

    //根據一個元素的索引,獲取他父親索引
    private int parent(int index)
    {
        if (index == 0)
        {
            throw new IllegalArgumentException("index - 0 does't have parent.");
        }
        return (index - 1) / 2;
    }

    //根據一個元素的索引,獲取他右孩子的索引
    private int leftChild(int index)
    {

        return index * 2 + 1;
    }

    //根據一個元素的索引,獲取他左孩子的索引
    private int rightChild(int index)
    {
        return index * 2 + 2;
    }


}

 向堆中添加一個元素,在堆的內部要進行一個上浮的操作,保證用數組實現的二叉堆還符合我們最大堆的性質(父節點的值大於兩個子節點的值)。

82大於他的父節點60,兩個結點交換位置,82還大於他的父結點80,兩個節點交換位置。80小於現在的父結點90,結束交換。這個操作很多人稱為上浮操作(個人認為名稱貼切)上浮操作完成。

用代碼實現我們剛才的操作,已經知道他父結點的位置(公式),交換兩個人的位置就變得很簡單,MaxHeap添加函數。

    //堆中添加元素
    public void add(E e)
    {
        data.addLast(e);
        siftUp(data.getSize() - 1);
    }

    //上浮操作
    private void siftUp(int i)
    {
        while (i > 0 && data.get(parent(i)).compareTo(data.get(i)) < 0)
        {
            //交換位置
            data.swap(i,parent(i));
       i = parent(i) } }

Array類,添加交換位置的函數

    public void swap(int i,int j)
    {
        if (i < 0 || i >= size || j < 0 || j > size)
        {
            throw new IllegalArgumentException("索引越界");
        }
        E t = data[i];
        data[i] = data[j];
        data[j] = t;

    }

有添加就有取出,取出堆中元素其實很簡單,因為最大堆決定了只取堆頂元素(數組的第一個元素),直接取出即可。困難的是如何維護二叉堆的性質不變。

取出堆頂元素后

取出堆頂元素,剩下兩個子樹,將兩顆子樹糅合成一個二叉堆,現在直接將60這個元素作為堆頂,就滿足了完全二叉樹的性質但並不符合最大堆性質。

和上浮的操作相反,現在我們要進行下沉的操作,60的左右孩子都比60來得大,要選擇左右孩子最大的那個數進行交換,82和60進行交換,80比60來得大,交換他們的位置,10比60來得小,符合二叉堆的性質。交換結束。

用代碼描述剛才取出的操作。

MaxHeap類

    //堆中最大元素
    public E findMax()
    {
        if (data.getSize() == 0)
        {
            throw new IllegalArgumentException("堆為空,無法查看值");
        }
        return data.get(0);
    }
    //取出堆頂元素
    public E extractMax()
    {
        E ret = findMax();
        data.swap(0,data.getSize() - 1);
        data.removeLast();
        siftDown(0);
        return ret;
    }

    //下沉操作
    private void siftDown(int i)
    {
        //比較到他左右孩子那個比他大進行交換操作
        while (leftChild(i) < data.getSize())
        {
            int j = leftChild(i);
            if (j + 1 < data.getSize() && data.get(j + 1).compareTo(data.get(j)) > 0) //右節點
            {
                j = rightChild(i);
            }
            if (data.get(i).compareTo(data.get(j)) >= 0)
            {
                break;
            }
            data.swap(i,j);
            i = j;
        }
    }

 現在我們堆結構基本完成,簡單測試一下

Main類

public class Main
{
    public static void main(String[] args) {
        MaxHeap<Integer> maxHeap = new MaxHeap<>();
        int[] nums = {90,80,70,60,50,60,20,10};
        for (int i = 0; i < nums.length; i++)
        {
            maxHeap.add(nums[i]);
        }
        System.out.println("堆頂:" + maxHeap.findMax());
        maxHeap.add(82);//添加82
        System.out.println("取出堆頂值:" + maxHeap.extractMax());
        System.out.println("堆頂:" + maxHeap.findMax());//是否為82
        maxHeap.add(85);//添加85
        System.out.println("堆頂:" + maxHeap.findMax()); //是否為85
        System.out.println("測試結束");
    }
}

輸出

堆頂:90
取出堆頂值:90
堆頂:82
堆頂:85
測試結束

用定義的最大堆去實現一個優先隊列就變得十分簡單了,優先隊列本質上來說還是一個隊列,用堆來實現隊列的接口。

Queue接口類

public interface Queue<E> {

    int getSize();

    boolean isEmpty();

    void enqueue(E e);

    E dequeue();

    E getFront();
}

優先隊列(PriorityQueue類)

public class PriorityQueue<E extends Comparable<E>> implements Queue<E>
{
    private MaxHeap<E> maxHeap;

    public PriorityQueue()
    {
        maxHeap = new MaxHeap<>();
    }

    @Override
    public int getSize() {
        return maxHeap.size();
    }

    @Override
    public boolean isEmpty() {
        return maxHeap.isEmpty();
    }

    @Override
    public void enqueue(E e) {
        maxHeap.add(e);
    }

    @Override
    public E dequeue() {
        return maxHeap.extractMax();
    }

    @Override
    public E getFront() {
        return maxHeap.findMax();
    }
}

實例

在股票市場,很多股民向股票代理打電話,股票代理公司優先處理vip客戶(有錢¥)再處理普通的用戶。把他們的money當做他們的優先程度

Customer類

public class Customer implements Comparable<Customer> {
    private int money;

    private String name;

    public Customer(int money, String name) {
        this.money = money;
        this.name = name;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public int compareTo(Customer another) {
        if (this.money < another.money)
        {
            return -1;
        }else if (this.money > another.money)
        {
            return 1;
        }else {
            return 0;
        }
    }
}

Main類

public class Main
{
    public static void main(String[] args) {
        //優先隊列使用示例
        Queue<Customer> queue = new PriorityQueue<>();
        Random random = new Random();
        for (int i = 0; i < 10; i++)
        {
            int money = random.nextInt(1000000);
            queue.enqueue(new Customer(money,"客戶" + i ));
        }
        while (true)
        {
            if (queue.isEmpty())
            {
                break;
            }
            Customer customer = queue.dequeue();
            System.out.println("優先處理 " + customer.getName() + " 因為他的money為:" + customer.getMoney() + "¥");
        }
    }

}

輸出

優先處理 客戶4 因為他的money為:842917¥
優先處理 客戶7 因為他的money為:628183¥
優先處理 客戶8 因為他的money為:578457¥
優先處理 客戶0 因為他的money為:551270¥
優先處理 客戶1 因為他的money為:538859¥
優先處理 客戶5 因為他的money為:297316¥
優先處理 客戶3 因為他的money為:262908¥
優先處理 客戶9 因為他的money為:250763¥
優先處理 客戶6 因為他的money為:144102¥
優先處理 客戶2 因為他的money為:96273¥

隨機數,輸出結果不確定。但一定是從大到小排序,如果要從小到大很簡單,改比較符即可。這邊實現的是最大堆,Java提供的優先隊列(PriorityQueue)底層是最小堆。

 

============================================

如發現錯誤請留言提醒lz,好及時修改,避免誤導別人。拜謝


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM