堆是一種非常常用的資料結構,常常用來實現優先佇列。下面來說一下,堆的一些特性。
堆(二叉堆)的成立條件如下:具有n個元素的序列:{k1,k2,ki,…,kn} ,(ki <= k2i,ki <= k2i+1) 或者 (ki >= k2i,ki >= k2i+1), (i = 1,2,3,4…n/2)。滿足第一個條件的我們稱之為最小堆,滿足第二個條件的我們稱之為最大堆。其實堆(二叉堆)我們可以用完全二叉樹進行描述(堆本身我們可以理解成完全二叉樹,不理解完全二叉樹的可以先看一下),每一個父節點對應最多兩個子節點,且父節點的值都是大於或者兩個子節點的值的。下面的圖片就是用完全二叉樹描述的最大堆以及最小堆,大家可以看一下。
在實現堆這種資料結構的時候,通常都會採用連結串列或者陣列的形式,這裡我們以陣列的形式來實現一個堆,事實上利用陣列來實現堆是一種經典的堆的實現方式。
利用陣列實現堆的原理:假設我們將以陣列中index為1的位置開始來存放堆中的元素,假設index為n的位置上存在一個元素,那麼它的子元素在陣列中存放位置就是index = 2*n 以及index = 2*n +1 。這裡需要注意的是我們是以index為1的位置開始存放元素的,並不是以index=0來存放的,如果以index=0來存放,那麼它的子元素在陣列中存放的位置就是index = 2*n + 1以及index = 2*n + 2。
下面我們就用php初始化一個堆,先來看一下具體的思路。假設現在我們向length=n的陣列堆中新增一個元素,那麼陣列堆中的前n個元素繼續保持堆的特性,新新增的元素以後,length=n+1的陣列就不再滿足堆的特性了,所以我們需要對最後一個元素,也就是新新增的元素進行位置的調整。根據新新增的元素在陣列中的索引,來計算出其父節點的索引,然後對新元素和父節點元素進行大小的比較,如果新新增的元素大於父節點元素,就需要進行資料的交換,如果新新增的元素小於等於父節點元素,就不需要進行比較了,因為新新增的元素正在其合適的位置之上。資料交換完成以後,可能還是破壞了堆的特性,需要繼續進行剛才的步驟,直到需要比較的節點的索引小於1為止。
下面來看程式碼:
//初始化陣列堆
$heap_arr = [];
//定義向堆中新增元素的方法
function heap_add(&$heap_arr,$value){
$heap_arr[] = $value;
$count_arr = count($heap_arr);
//進行堆的調整
if($count_arr > 1){
$n = $count_arr-1;
while($n >= 2){
//查詢父節點id
$parent_n = floor($n/2);
//如果子節點的value大於其父節點的value,就進行交換
if($heap_arr[$n]>$heap_arr[$parent_n]){
//進行資料的交換
$temp = $heap_arr[$n];
$heap_arr[$n] = $heap_arr[$parent_n];
$heap_arr[$parent_n] = $temp;
$n = $parent_n;
}else{
//如果子節點的value小於等於父節點的value,直接退出
break;
}
}
}
}
heap_add($heap_arr,0);
heap_add($heap_arr,9);
heap_add($heap_arr,6);
heap_add($heap_arr,10);
heap_add($heap_arr,4);
heap_add($heap_arr,1);
heap_add($heap_arr,10);
heap_add($heap_arr,11);
heap_add($heap_arr,5);
print_r($heap_arr);複製程式碼
上面的程式碼我們就實現了向陣列堆中新增元素,並且保證了陣列$heap_arr一直保持堆的性質。下面,我們就來看一下如何獲取上面陣列堆中的資料。先看一下思路:我們上面實現的堆其實是一個最大堆,也就是說陣列中索引為1的位置存放著整個陣列中最大的元素(最小堆實現方式一樣)。當我們獲取出陣列堆中最大值以後,當前的陣列就不再滿足樹形的結構了,也就不再滿足堆的特性了。但是因為堆本身就是一個完全二叉樹,那麼我們可以將陣列中的最後一個元素放到索引為1的位置上去,這樣的話,陣列繼續保持這樹形結構,但仍然可能不是堆的特性。為了保證陣列繼續保持堆的特性,就需要調整當前的陣列。調整的步驟,回去當前節點(索引為1的節點)的子節點,獲取子節點中的最大值,然後對比當前節點以及子節點中的最大值進行比較,如果當前節點小於子節點中的最大值,就進行交換,依此類推,直到需要比較的節點索引大於陣列最大的索引值為止。
具體看程式碼:
//定義獲取最大值元素的函式
function heap_del(&$heap_arr){
$count_arr = count($heap_arr);
if($count_arr<=1){
return "";
}
if($count_arr==2){
return $heap_arr[1];
}
//如果陣列的長度大於2個的時候
$last_value = $heap_arr[$count_arr-1];
//刪除最後一個元素
unset($heap_arr[$count_arr-1]);
//將陣列中最後一個元素放到陣列的第二個位置上去
$heap_arr[1] = $last_value;
//進行下沉操作
$n = 1;
$max_node = $count_arr-2;
while($n<$max_node){
$left_son_node = $n*2;
$right_son_node = $n*2+1;
if($left_son_node>$max_node && $right_son_node>$max_node){
break;
}
if($left_son_node<=$max_node && $right_son_node>$max_node){
$swap_node = $left_son_node;
}
if($left_son_node>$max_node && $right_son_node<=$max_node){
$swap_node = $right_son_node;
}
if($left_son_node<=$max_node && $right_son_node<=$max_node){
//進行比較,取出較大值
if($heap_arr[$left_son_node]>=$heap_arr[$right_son_node]){
$swap_node = $left_son_node;
}else{
$swap_node = $right_son_node;
}
}
//進行資料的交換
if($heap_arr[$swap_node]>$heap_arr[$n]){
$temp = $heap_arr[$swap_node];
$heap_arr[$swap_node] = $heap_arr[$n];
$heap_arr[$n] = $temp;
$n = $swap_node;
}else{
break;
}
}
}
heap_del($heap_arr);
print_r($heap_arr);複製程式碼
上面我們就實現了獲取最大值的方法。那麼如何進行堆排序那?其實上面的獲取最大值方式就是堆排序的實現方式,依次獲取當前陣列堆中的最大值,並且保持堆的性質。堆排序也是非常重要的一種排序方式,其時間複雜度是O(NlogN),並且是一種穩定的排序方式。但是上面的堆排序的程式碼我們可以繼續優化,以提高排序的效能。排序的思路如下:根據上面的介紹,傳遞進來的陣列是滿足最大完全二叉樹的結構的,但是整體不一定滿足堆的特性。並且這個完全二叉樹中所有的葉子節點都是滿足堆的特性的,但是非葉子節點及其子節點就不一定滿足堆的特性了。那麼我們就可以找到完全二叉樹中最後一個非葉子節點,然後對這個節點進行”下沉”操作,使其及其子節點保證滿足堆的特點,然後依次處理所有的非葉子節點,直到讓整個陣列都滿足堆的特性。
下面來看程式碼:
//隨便定義一個無序的陣列
$test_array = [0,1,2,3,4,5,6,7,8,2];
//定義一個堆化函式
function heapify(&$array){
$count = count($array);
//獲取最後一個非葉子節點的index
$last_index = floor(($count-1-1)/2);
while($last_index>=0){
heap_down($array,$last_index);
$last_index-=1;
}
}
//對陣列進行堆化處理
heapify($test_array);
echo "<pre/>";
print_r($test_array);複製程式碼
使用上面的定義好的heapify()堆化函式繼續堆排序的時間複雜度為O(N),在排序速度上要比之前的堆排序要快很多。
好了,說完了堆排序,我們繼續說索引堆。什麼是索引堆那?所謂索引堆,簡單地說,就是在堆裡頭存放的不是資料,而是資料所在陣列的索引,也就是下標(本文中索引和下標是一個意思,互相換用),根據資料的某種優先順序來調整各個元素對應的下標在堆中的位置。
為什麼需要使用索引堆那?這是因為堆這種資料結構存在兩個缺點。(1):在堆中的元素體積非常大的情況下,經常性的移動元素是低效的。(2):如果在堆的使用過程中,堆中的元素的值要改變,則普通堆對此無能為力。如果是單純的修改元素的值,那麼陣列就不再滿足堆的特性了。
我們先來看一下下面的索引堆例項:
index | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
heap | 10 | 9 | 7 | 8 | 5 | 6 | 3 | 1 | 4 | 2 |
data | 15 | 17 | 19 | 13 | 22 | 16 | 28 | 30 | 41 | 62 |
陣列堆中存放的是其對應的元素在data陣列中的索引。如果我們在data陣列中新增了一個元素,那麼我們就將新新增的元素在data陣列中的索引新增到heap陣列的尾部。那麼heap陣列因為新新增了元素,就需要進行堆的調整。和以前實現程式碼的思路一樣,將新新增到heap陣列中的元素與其父節點進行比較(實際是那heap中的元素作為索引所對應的data陣列中元素進行比較的,這點比較繞),如果子節點對應的元素大於父節點對應的資料就進行交換。獲取索引堆中的最大值等操作和上面的思路是一樣的,這裡就不在贅述了。
這裡主要說一下,更改data陣列中資料的操作。首先,獲取需要修改的data陣列中的資料所對應的索引值以及需要修改成值,然後修改data中的資料。根據data陣列中所對應的索引,去heap陣列中查詢值為data中索引的索引(節點,這裡有點繞),然後對這個節點進行上浮以及下沉的操作,保證陣列滿足堆的特性。
下面我們直接看實現索引堆的程式碼:
//索引堆的實現
//陣列堆,用來存放index
$heap_array = [];
//元素陣列
$array = [];
//新增元素到佇列中
function init($item,&$heap_array,&$array){
//新增元素到陣列中
$array[] = $item;
//獲取新增的元素所對應的index
$last_index = count($array)-1;
//陣列堆
$heap_array[] = $last_index;
//進行堆化處理,維持堆的性質
heap_up($heap_array,$array,$last_index);
}
//獲取最大堆中的堆頂元素(最大元素)
function get_max(&$heap_array,&$array){
$count_heap_array = count($heap_array);
if($count_heap_array<1){
return;
}
//獲取最大值所對應的索引
$index = $heap_array[0];
//獲取最大值
$return_value = $array[$index];
//獲取堆中最後一個元素
$last_index = $heap_array[count($heap_array)-1];
$heap_array[0] = $last_index;
unset($heap_array[count($heap_array)-1]);
//進行堆的下沉操作
heap_down($heap_array,$array);
//返回最大值
return $return_value;
}
//修改陣列中的內容
function change(&$heap_array,&$array,$index,$value){
if($index<0 || $index>(count($array)-1)){
return false;
}
//修改$array中的資料
$array[$index] = $value;
//在$heap_array查詢$index的位置
for($i=0;$i<count($heap_array);$i++){
if($index = $heap_array[$i]){
//進行調整
heap_down($heap_array,$array,$i);
heap_up($heap_array,$array,$i);
}
}
}
//堆的下沉操作
function heap_down(&$heap_array,&$array,$index=0){
//$index = 0;
$last_index = count($heap_array)-1;
while($index<$last_index){
$left_index = 2*$index + 1;
$right_index = 2*$index + 2;
//比較left_index與right_index所對應的值
if($left_index>$last_index){
break;
}
if($left_index<=$last_index && $right_index<=$last_index){
if($array[$heap_array[$left_index]]<$array[$heap_array[$right_index]]){
$swap_index = $right_index;
}else{
$swap_index = $left_index;
}
}
if($left_index<=$last_index && $right_index>$last_index){
$swap_index = $left_index;
}
//進行資料的交換
if ($array[$heap_array[$swap_index]] > $array[$heap_array[$index]]) {
$temp = $heap_array[$swap_index];
$heap_array[$swap_index] = $heap_array[$index];
$heap_array[$index] = $temp;
$index = $swap_index;
} else {
break;
}
}
}
//堆的上浮操作
function heap_up(&$heap_array,&$array,$index){
while($index>0){
//獲取當前節點的父節點
$parent_index = floor(($index-1)/2);
//將當前節點與父節點所對應的資料進行比較
if($array[$heap_array[$index]]>$array[$heap_array[$parent_index]]){
//進行資料的交換
$temp = $heap_array[$index];
$heap_array[$index] = $heap_array[$parent_index];
$heap_array[$parent_index] = $temp;
$index = $parent_index;
}else{
break;
}
}
}
//新增元素的操作
init(1,$heap_array,$array);
init(0,$heap_array,$array);
init(3,$heap_array,$array);
init(4,$heap_array,$array);
init(5,$heap_array,$array);
init(6,$heap_array,$array);
init(2,$heap_array,$array);
init(0,$heap_array,$array);
init(1,$heap_array,$array);
echo "<pre/>";
echo "heap_array<br/>";
print_r($heap_array);
echo "array<br/>";
print_r($array);
change($heap_array,$array,0,100);
echo "修改資料<br/>";
echo "heap_array<br/>";
print_r($heap_array);
echo "array<br/>";
print_r($array);
echo "<br/>";
echo get_max($heap_array,$array);複製程式碼