PHP 實現堆, 堆排序以及索引堆

codefly發表於2019-02-14

堆是一種非常常用的資料結構,常常用來實現優先佇列。下面來說一下,堆的一些特性。

堆(二叉堆)的成立條件如下:具有n個元素的序列:{k1,k2,ki,…,kn} ,(ki <= k2i,ki <= k2i+1) 或者 (ki >= k2i,ki >= k2i+1), (i = 1,2,3,4…n/2)。滿足第一個條件的我們稱之為最小堆,滿足第二個條件的我們稱之為最大堆。其實堆(二叉堆)我們可以用完全二叉樹進行描述(堆本身我們可以理解成完全二叉樹,不理解完全二叉樹的可以先看一下),每一個父節點對應最多兩個子節點,且父節點的值都是大於或者兩個子節點的值的。下面的圖片就是用完全二叉樹描述的最大堆以及最小堆,大家可以看一下。

PHP 實現堆, 堆排序以及索引堆

在實現堆這種資料結構的時候,通常都會採用連結串列或者陣列的形式,這裡我們以陣列的形式來實現一個堆,事實上利用陣列來實現堆是一種經典的堆的實現方式。

利用陣列實現堆的原理:假設我們將以陣列中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);複製程式碼

相關文章