數據結構(2)堆

什麼是堆(二叉堆)?

堆,這裏是二叉堆的簡稱,其實就是一棵二叉樹,還是二叉樹裏比較特殊的完全二叉樹。如果不熟悉的二叉樹的相關概念的朋友,需要先複習一下,起碼要知道二叉樹的概念,比如子節點,葉子節點,樹的層級這些概念。
完全二叉樹:除了最後一層以外的所有層的節點都是被填滿的,最後一層的葉子節點如果沒有被填滿,那麼葉子節點的順序是從左向右排列的。
當然,堆也有區分是 大頂堆小頂堆

  • 大頂堆:根節點是整棵二叉樹中最大的數字。
  • 小頂堆:根節點是整棵二叉樹中最大的數字。

用堆來實現什麼?

有的時候,我們需要利用有限的空間來獲取一些Top K這種數據,這種數據可以是最大的,也可以是最小的。
爲啥說是有限空間?比如我們有個10G的數據,但是我們只有200M的內存,我們要取出前一萬個最大的數字。常規的做法就是排序唄,但是這個內存太小,不足以讓你在內存裏面排序。
那咋整,每次拿個幾萬數據來排序,然後再和原來的那個一萬個逐個比較?這是一種辦法,但是時間複雜度太高。每次排序都要O(NlogN)O(NlogN),然後還要算上和原來的排好序的數組的進行比較,交換,搬移的時間,時間太久了。
那麼有沒有一個比較好的方法來完成這個任務呢?
答案就是利用優先隊列,也就是這個數據結構,至於如何操作我們等等再講。

爲什麼要用堆?

如果不是堆這個數據結構,那麼可以用哪些來實現優先隊列呢?

  1. 數組:入隊(插入)時候,時間複雜度O(1)O(1);出隊(刪除)的時候,需要遍歷整個數組,刪除,並且把後面的元素前移,時間複雜度爲O(n)O(n)
  2. 鏈表:入隊時間複雜度爲O(1)O(1);出隊的時候,需要遍歷整個數組,並且刪除,但是不用進行數據搬移。
  3. 二叉查找樹:入隊的時間複雜度爲O(log(n))O(log(n));出隊時候時間複雜度爲O(1)O(1)。但是有個弊端,就是在出隊時候,會出現子樹不平衡的現象,容易變成單鏈表,比如,如果是小頂堆,那麼左子樹就會一直減少;如果是大頂堆,那麼右子樹就會一直減少。
  4. 平衡二叉查找樹:入隊時間O(log(n))O(log(n)),出隊的時間複雜度O(1)O(1)。但是平衡二叉查找樹也支持許多不需要的特性,使用起來顯得有點浪費,特別是不管哪種平衡二叉查找樹的實現都是複雜的實現。

如何實現二叉堆?

(1)實現哪些接口

首先,我們要想清楚,這個二叉堆要什麼功能?
一般數據結構個,增加,刪除,查詢,修改,這幾個基本操作是必須的。
同樣,身爲數據結構,二叉堆也有以上幾種操作,不過,不同的是,它 “變異” 了。
二叉堆的實現是接口:

public interface Heap<E extends Comparable> {
    /**
     * add element.
     * @param e element
     */
    void add(E e);

    /**
     * delete max element or min element.
     * @return max element or min element.
     */
    E delete();

    /**
     * find max element or min element.
     * @return max element or min element.
     */
    E find();

    /**
     * replace max element or min element.
     * @param e to replacing-element.
     */
    void replace(E e);
}

爲什麼用Comparable這個排序接口,主要是有compareTo這個接口,我們的這個二叉堆肯定是要去有比較前後大小性質的,所以需要進行排序。
add: 這個不用說,就是增加節點的操作。
注意:以下都是“變異”後的操作了
delete: 這個操作有點特殊,不是刪除任意的節點,而是刪除二叉堆中最大的或者是最小的點 。至於說到底是最大還是最小,這個取決於你實現的是最大堆還是最小堆
find: 這個不是按照查找任意節點,而是查找最大的或者最小的點。至於說到底是最大還是最小,這個取決於你實現的是最大堆還是最小堆
replace: 就是把最大的,或者最小的那個節點替換掉。至於說到底是最大還是最小,這個取決於你實現的是最大堆還是最小堆

(2)用最大堆作爲例子

首先我們需要明白一個概念,雖然二叉堆是個二叉樹,但是它如果用樹的結構來存儲數據是很耗費空間;因爲它是個完全的二叉樹,所以我們可以用數組來存儲數據。
要理解這段話並不容易,首先我們要知道,之所以二叉樹樹的節點的數據結構,一般都要存一個左節點和右節點,我們可以根據當前節點來獲取左節點和右節點,有的二叉樹的節點結構有獲取父節點的需求,還會增加一個父節點的點。所以,我們可以知道一條信息,就是你如果獲取了某個樹的節點,那麼相應的,你需要獲取這個二叉樹的左孩子的節點的值,右孩子的節點的值。
因爲二叉堆是個滿二叉樹,所以,我們只要知道某個索引,我們就可以根據索引來算出這個座標。
先來看看下面這張圖:
在這裏插入圖片描述
其實就是把完全二叉樹給塞進數組裏面。
這裏我們要知道計算左孩子,右孩子,父親節點的索引幾個公式。
父親節點:(index1)/2(index - 1) / 2
左孩子:index2+1index * 2 + 1
右孩子:index2+2index * 2 + 2
我們用19這個節點來作爲例子:
19的索引 :2
父親的索引:0
左孩子的索引:5
右孩子的索引:6
可以帶入公式,看看是否正確。
知道如何去保存數據之後,我們再來看看堆需要幾個基本操作。
增加:
首先就是我們無論如何都要保存堆的一個性質,我們假設我們增加了一個元素,我們要保證增加元素過後,依然保證最大堆的性質不變,就是隨便在什麼地方,父親節點大於孩子節點。
那麼這個增加元素在最後,我們要怎麼修改呢?
把增加的元素和他的父親節點去比,如果比他父親大,就交換,如果比父親小,那麼就結束。代碼實現就是:

public void add(E e) {
   data.add(e);
   size++;
   shiftUp(size - 1);
}
/**
 * 上移操作:在底層,開始往上移動。
 * @param index 下移的座標
 */
private void shiftUp(int index) {
    int parentIndex = (index - 1) / 2;
    // 與父親節點比較,如果大於父親節點,就交換,
    while (parentIndex >= 0 && data.get(parentIndex).compareTo(data.get(index)) < 0) {
        swap(index, parentIndex);
        index = parentIndex;
        parentIndex = (index - 1) / 2;
    }
}

刪除
把最大的節點刪除,那麼就會空出一個節點,別的節點也要相應的佔位上來;其實這樣看來,是比較複雜的,但是我們可以吧最後一個節點賦值到第一個節點,然後把最後一個結點刪除,
然後去吧第一個節點進行比較,然後歸位。

@Override
public E delete() {
    if (size <= 0) {
        throw new IllegalArgumentException("size under 0.");
    }
    E ans = data.get(0);
    data.set(0, data.get(size - 1));
    data.remove(size - 1);
    size--;
    shiftDown(0);
    return ans;
}
/**
 * 下移操作
 * @param index 下移的座標
 */
private void shiftDown(int index) {
    int maxIndex = index;
    while (true) {
        int leftChildIndex = 2 * index + 1;
        if (leftChildIndex < size && data.get(leftChildIndex).compareTo(data.get(maxIndex)) > 0) {
            maxIndex = leftChildIndex;
        }
        int rightChildIndex = 2 * index + 2;
        if (rightChildIndex < size && data.get(rightChildIndex).compareTo(data.get(maxIndex)) > 0) {
            maxIndex = rightChildIndex;
        }
        if (maxIndex == index) {
            break;
        }
        swap(maxIndex, index);
        index = maxIndex;
    }
}

其實堆裏面,最重要就是兩個操作,一個上移,一個下沉,只要搞懂了這個兩個操作,堆你就拿下了。
我們來看看完整的代碼。

public class MaxHeapMine<E extends Comparable> implements Heap<E> {

    private ArrayList<E> data;
    private int size;

    public MaxHeapMine() {
        data = new ArrayList<>();
        size = 0;
    }

    public MaxHeapMine(E[] elements) {
        data = new ArrayList<>();
        size = 0;
        for (E item : elements) {
            add(item);
        }
    }
    @Override
    public void add(E e) {
        data.add(e);
        size++;
        shiftUp(size - 1);
    }

    @Override
    public E delete() {
        if (size <= 0) {
            throw new IllegalArgumentException("size under 0.");
        }
        E ans = data.get(0);
        data.set(0, data.get(size - 1));
        data.remove(size - 1);
        size--;
        shiftDown(0);
        return ans;
    }

    @Override
    public E find() {
        if (size <= 0) {
            throw new IllegalArgumentException("size under 0.");
        }
        return data.get(0);
    }

    @Override
    public void replace(E e) {
        data.set(0, e);
        shiftDown(0);
    }

 	/**
     * 上移操作
     * @param index 下移的座標
     */
    private void shiftUp(int index) {
        int parentIndex = (index - 1) / 2;
        while (parentIndex >= 0 && data.get(parentIndex).compareTo(data.get(index)) < 0) {
            swap(index, parentIndex);
            index = parentIndex;
            parentIndex = (index - 1) / 2;
        }
    }

    /**
     * 下移操作
     * @param index 下移的座標
     */
    private void shiftDown(int index) {
        int maxIndex = index;
        while (true) {
            int leftChildIndex = 2 * index + 1;
            if (leftChildIndex < size && data.get(leftChildIndex).compareTo(data.get(maxIndex)) > 0) {
                maxIndex = leftChildIndex;
            }
            int rightChildIndex = 2 * index + 2;
            if (rightChildIndex < size && data.get(rightChildIndex).compareTo(data.get(maxIndex)) > 0) {
                maxIndex = rightChildIndex;
            }
            if (maxIndex == index) {
                break;
            }
            swap(maxIndex, index);
            index = maxIndex;
        }
    }

    private void swap(int a, int b) {
        E temp = data.get(a);
        data.set(a, data.get(b));
        data.set(b, temp);
    }

    public static void main(String[] args) {
        Integer[] test = {1, 2, 3, 4, 0, 9, 8, 7, 6, 5, 20, 19, 18, 17, 16};
        MaxHeapMine<Integer> maxHeapMine = new MaxHeapMine<>(test);
        for (int i = 0, len = test.length; i < len; ++i) {
            System.out.println(maxHeapMine.delete());
            System.out.println(maxHeapMine.find());
        }
    }
}

你可以把代碼直接拷貝運行,看看效果如何,最後會打印出一組排好序的數字的。
其實上面的代碼可以繼續優化,減少交換的次數。不過有些不太好理解。有興趣的同學可以是試試。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章