堆
引言
堆是計算機科學中一類特殊的數據結構,它雖然也被叫做優先隊列,但它並不是隊列,它並不是按照先進先出的原則進行存儲數據的,它更像是一棵二叉樹。
最大堆
定義
最大堆的實現是通過構造二叉堆,而二叉堆的實質是一棵完全二叉樹,它具備以下性質:
- 任一節點都是小於(最小堆)或大於(最大堆)其子節點,而根節點是最大或最小值。
- 堆總是一棵完全二叉樹,即除最後一層外,其它層都是滿的,而最後一層也應是自左向右填入數據
圖示:
實現
代碼實現
public class MaxHeap<T extends Comparable> {
private T[] data;//數據
private int count;//保存的數據個數
public MaxHeap(int capacity) {
data = (T[]) new Comparable[capacity + 1];//因爲是從1開始存放數據,所以容量要比傳進來的容量加一
}
public MaxHeap(T arr[]) {//使用傳進來的值進行構造
this(arr.length);
System.arraycopy(arr, 0, data, 1, arr.length);//複製數組
count = arr.length;
Hepify();
}
private void Hepify() {
int end = count / 2;//找到最後一個有孩子的父親節點
while (end > 0) {
shift_down(end--);//從這最後一個有孩子的父節點開始做shift_down操作直到根節點
}
}
public int size() {
return count;
}
public boolean isEmpty() {
return count == 0;
}
public void insert(T item) {//直接在最後數組最後一個位置進行插入,之後再進行shift_up操作
assert count + 1 <= data.length;
data[++count] = item;
shift_up();
}
//核心代碼
private void shift_up() {//該方法是將最後一個元素不斷向上比較並交換,直到其值小於其父節點結束交換
int index = count;
while (index > 1 && data[index / 2].compareTo(data[index]) < 0) {
swap(index, index / 2);//如果當前索引不是根節點且索引處的值大於其父親節點的值,則交換二者
index /= 2;//更新索引
}
}
public T pop() {//拋出根節點的元素之後進行shift_down操作
if (count == 0)
return null;
T element = data[1];//保存要返回的值
data[1] = data[count];//將根節點元素與最後一個元素進行調換位置
data[count--] = null;//將最後一個元素設爲空
shift_down(1);
return element;//返回保存的值
}
//核心代碼
private void shift_down(int index) {//該操作在拋出元素後進行,爲了將拋出後的堆重新排序
//不斷將index處的元素向下調換位置,直到該元素比兩個子節點都大
T value = data[index];//先保存index處的值,最後在找到value該放的位置之後再將值賦給該位置,減少交換次數
while (index * 2 <= count) {//判斷該節點是否有孩子
int bigger = index * 2;//保存兩個孩子中較大值的索引
if (bigger + 1 <= count && (data[bigger].compareTo(data[bigger + 1]) < 0)) {
//判斷是否越界,然後再比較兩個子節點,選出較大的那個和index處的元素進行比較
bigger++;
}
if (value.compareTo(data[bigger]) < 0) {//如果比較大的那個子節點小,則交換位置
data[index] = data[bigger];
index = bigger;//更新索引的值
} else {//否則就跳出循環
break;
}
}
data[index] = value;//將value該存放的位置賦值
}
private void swap(int i, int j) {
T temp = data[i];
data[i] = data[j];
data[j] = temp;
}
}
核心代碼講解
在這裏,我們是使用數組來實現堆這種數據結構,習慣上一般從1
這個索引開始存放數據,因爲這樣可以十分方便的檢索其左右孩子節點。
在上面代碼中有兩個方法是核心代碼,一個是shift_up,該方法是將新插入的元素不斷向上比較,直到比父節點小或是已經在根節點上了爲止;另一個是shift_down,該方法是將傳入索引處的值不斷與兩個子節點中較大的值比較,直到比兩個子節點都大或已經處在葉子節點爲止。
衍生
使用堆這種數據結構可以很方便的實現一種排序算法,即堆排序。算法的核心是使用堆這種數據結構,因爲堆的特性決定了其根元素是整個堆的最大值,那麼我們只要不斷將這個最大值進行拋出即可,因爲堆自身會對拋出操作後的數據進行自動維護操作,所以我們無需進行其他操作。
代碼
void Sort(Integer[] nums) {
MaxHeap<Comparable> heap = new MaxHeap<>(nums);//直接使用nums這個數組初始化heap
for (int i = nums.length - 1; i >= 0; i--) {
nums[i] = (Integer) heap.pop();//進行拋出操作,之後堆回自動維護
}
}
優化
上述方法是借用堆這種數據結構進行的堆排序,但我們還可以對其進行空間上的優化,使其不用再開闢新的內存空間,直接在待排序數組上進行排序操作。
代碼
void Sort(Integer[] nums) {
for (int i = (nums.length - 1) / 2; i >= 0; i--) {//從最後一個有孩子的葉子節點開始進行shiftDown操作
ShiftDown(nums, i, nums.length - 1);
}
for (int i = nums.length - 1; i > 0; i--) {
int temp = nums[0];//將最大值放到最後
nums[0] = nums[i];
nums[i] = temp;
ShiftDown(nums, 0, i - 1);//需要將i再減1是因爲已經將未排序中最大的元素放在了未排序數中最後一個的位置, 而且也不要對其再次進行shiftDown操作了
}
}
//該處shift_down操作與heap中的基本一致
private void ShiftDown(Integer[] nums, int index, int n) {//n代表最後一個元素的索引
int temp = nums[index];
while (index * 2 + 1 <= n) {//因爲是從0開始進行編號的,所以左孩子的編號是2*index+1
int bigger = index * 2 + 1;
if (bigger + 1 <= n && nums[bigger] < nums[bigger + 1]) {
bigger++;
}
if (temp < nums[bigger]) {
nums[index] = nums[bigger];
index = bigger;
} else {
break;
}
}
nums[index] = temp;
}
最大索引堆
上面實現的最大堆優點十分明顯,就是穩定,並且衍生出來的堆排序在時間效率上面也很可觀,屬於nlogn級別的算法,但與此同時缺點也十分明顯,主要有兩點:
在元素的數據比較複雜的時候,移動元素比較之低效,因爲我們在移動元素時是直接移動元素本身,如果該元素是一個上千字節的字符串,那效率就會十分之低;
無法更改元素的值,因爲元素一旦入堆之後,它的位置就不再確定,同時如果更改其值就破壞了堆本身的性質;
這兩個缺點,第一個解決還比較之容易,可以直接更改指針的指向,而第二個就比較之棘手了,雖然也可以解決,但會直接導致算法在某些情況下的時間複雜度降至O(n²),這不是我們所想要的,所以這時候就產生了索引堆這個概念。
索引堆定義
所謂索引堆就是不再像之前實現的普通最大堆那樣直接對元素進行操作,而是對元素的索引進行操作,這樣就從根本上解決了上述兩個難題。
圖示:
圖中index表示的是在這個堆中,該處節點在data中索引的值,以圖中index[1]爲例,該處的值爲10,意思就是data[10]這個元素時處在堆中1(也就是根節點)這個位置上的。
索引堆實現
代碼:
(此處我加了一個優化,即reverse數組,後面有講解)
public class Index_MaxHeap<T extends Comparable> {
private T[] data;//存放數據
private int count;//數據的個數
private int[] indexes;//索引
private int[] reverse;//索引數組的索引
private int capacity;//容量
public Index_MaxHeap(int capacity) {//初始化
this.capacity = capacity;
data = (T[]) new Comparable[capacity + 1];
count = 0;
reverse = new int[capacity + 1];
indexes = new int[capacity + 1];
}
public void insert(int index, T item) {//將元素插入到index處
assert count + 1 <= capacity;
assert index + 1 >= 1 && index + 1 <= capacity;//防止數組越界
index++;//之所以要加一,是因爲在裏面是從1開始索引的,而外界是從0開始索引的
data[index] = item;//插入元素
indexes[++count] = index;//index保存的是data的索引
reverse[index] = count;//count保存的是indexes的索引
shift_up(count);
}
private void shift_up(int index) {//index是在indexs裏面的索引,indexes[index]是指data裏面的索引
while (index > 1 && data[indexes[index / 2]].compareTo(data[indexes[index]]) < 0) {//如果父節點小於子節點
//交換二個索引的位置
int cache = indexes[index / 2];
indexes[index / 2] = indexes[index];
indexes[index] = cache;
reverse[indexes[index / 2]] = index / 2;//reverse的索引是data的索引,reverse的值是indexes的索引,reverse將二者相互關聯了起來
reverse[indexes[index]] = index;
index /= 2;
}
}
public T extractMax() {//刪除並返回最大值
assert count > 0;
T temp = data[indexes[1]];//先保存要返回的元素
indexes[1] = indexes[count];//將根節點的值賦值爲最後一個索引
indexes[count] = 0;//清除原始數據
reverse[indexes[1]] = 1;
reverse[indexes[count--]] = 0;
shift_down(1);
return temp;
}
public int extractMaxIndex() {//刪除並返回最大值索引
assert count > 0;
int temp = indexes[1] - 1;
indexes[1] = indexes[count];
indexes[count] = 0;
reverse[indexes[1]] = 1;
reverse[indexes[count--]] = 0;
shift_down(1);
return temp;
}
public T getMax() {//獲取最大值
assert count > 0;
return data[indexes[1]];
}
public int getMaxIndex() {//獲取最大值的索引
assert count > 0;
return indexes[1] - 1;
}
public T getItem(int index) {//通過索引查找數據
assert contain(index);
index++;
return data[index];
}
public int getIndex(T item) {//通過數據查找索引
assert count > 0;
for (int i = 1; i <= count; i++) {
if (data[indexes[i]].compareTo(item) == 0) {
return indexes[i] - 1;
}
}
return -1;
}
private boolean contain(int i) {//判斷該元素是否存在
assert i + 1 >= 1 && i + 1 <= capacity;
return reverse[i + 1] != 0;
}
public void change(int index, T item) {//更改指定元素的值
assert contain(index);
index++;
data[index] = item;
shift_down(reverse[index]);
shift_up(reverse[index]);
}
private void shift_down(int index) {//index是指indexes裏面的
while (index * 2 <= count) {
int bigger = index * 2;//找到較大的子節點
if (bigger < count && data[indexes[bigger]].compareTo(data[indexes[bigger + 1]]) < 0) {
bigger++;
}
//將父節點與較大的子節點比較
if (data[indexes[bigger]].compareTo(data[indexes[index]]) > 0) {
int cache = indexes[bigger];
indexes[bigger] = indexes[index];
indexes[index] = cache;
reverse[indexes[index]] = index;
reverse[indexes[bigger]] = bigger;
} else {
break;
}
}
}
}
在上述實現中有一個reverse數組,該數組是indexes的索引,讓我們來梳理一下data,indexes和reverse它們之間的關係:
即假設有一個在data中的元素,位置是i,那麼reverse[i]就是indexes中該元素的位置,例如圖中的data[1]=15,reverse[1]指向的值是8,indexes[8]所保存的值1就是data中15的位置。更淺顯一點就是indexes的值是data的索引,而reverse的值是data裏每一個元素在堆的中位置,因爲indexes的索引就是堆中的位置,二者等價。
總結
堆這種數據結構在計算機科學中是一種十分重要的數據結構,它可以實現許多重要的應用,例如操作系統的任務調度,遊戲中AI自動攻擊對象的優先級等等,另外堆除了上面的最大堆和最大索引堆還有其它例如二項堆,斐波那契堆等,這些可能會在以後的文章裏面進行歸納介紹。