我們有意調(diào)整了排序的順序,最后講這個(gè)堆排序。不是因?yàn)樗茈y,而是它涉及到了基本的數(shù)據(jù)結(jié)構(gòu)知識(shí)。
# 以下py版摘自百度百科
def big_endian(arr,start,end):
root=start
child=root*2+1 #左孩子
while child<=end:
#孩子比最后一個(gè)節(jié)點(diǎn)還大,也就意味著最后一個(gè)葉子節(jié)點(diǎn)了,就得跳出去一次循環(huán),已經(jīng)調(diào)整完畢
if child+1<=end and arr[child]<arr[child+1]:
#為了始終讓其跟子元素的較大值比較,如果右邊大就左換右,左邊大的話就默認(rèn)
child+=1
if arr[root]<arr[child]:
#父節(jié)點(diǎn)小于子節(jié)點(diǎn)直接交換位置,同時(shí)坐標(biāo)也得換,這樣下次循環(huán)可以準(zhǔn)確判斷:是否為最底層,
#是不是調(diào)整完畢
arr[root],arr[child]=arr[child],arr[root]
root=child
child=root*2+1
else:
break
def heap_sort(arr): #無序區(qū)大根堆排序
first=len(arr)//2 - 1
for start in range(first,-1,-1):
#從下到上,從左到右對(duì)每個(gè)節(jié)點(diǎn)進(jìn)行調(diào)整,循環(huán)得到非葉子節(jié)點(diǎn)
big_endian(arr,start,len(arr)-1) #去調(diào)整所有的節(jié)點(diǎn)
for end in range(len(arr)-1,0,-1):
arr[0],arr[end]=arr[end],arr[0] #頂部尾部互換位置
big_endian(arr,0,end-1) #重新調(diào)整子節(jié)點(diǎn)的順序,從頂開始調(diào)整
return arr
堆,又名“優(yōu)先隊(duì)列”,是一個(gè)帶有優(yōu)先級(jí)(就是一定順序)的隊(duì)列,而隊(duì)列則是一種基礎(chǔ)數(shù)據(jù)結(jié)構(gòu),它的特點(diǎn)是“先進(jìn)先出”,i.e.,先進(jìn)入隊(duì)列的元素,在做移除操作時(shí)會(huì)被首先移除出隊(duì)列。其實(shí),優(yōu)先隊(duì)列這種隊(duì)列,并沒有說最先進(jìn)入的會(huì)被最先移除,因?yàn)閹в幸欢ǖ捻樞颍紫冗M(jìn)入的元素也會(huì)根據(jù)要求放到合適的位置,而不是最前端,而刪除時(shí),假設(shè)是默認(rèn)刪除,會(huì)刪除最上方的元素。
我們的堆,一般情況下都是二叉堆,就是由完全二叉樹構(gòu)成的對(duì)象。二叉樹是一種樹形結(jié)構(gòu),每個(gè)父節(jié)點(diǎn)只有2個(gè)子節(jié)點(diǎn);而完全二叉樹則是說,在排下一個(gè)子節(jié)點(diǎn)時(shí),必須保證這一層之前所有的位置都有節(jié)點(diǎn)。
這樣的結(jié)構(gòu)就會(huì)有以下幾個(gè)特點(diǎn),
- 假設(shè)一個(gè)元素的編號(hào)是 i,一個(gè)元素的2個(gè)子節(jié)點(diǎn)——左孩子和右孩子分別是 2i 和 2i+1。
- 其父節(jié)點(diǎn)的編號(hào)是 i/2,是向下取整的。
所以,當(dāng)我們構(gòu)建一個(gè)(最小)二叉堆時(shí),我們首先需要一個(gè)基本數(shù)據(jù)結(jié)構(gòu)——描述堆的struct或者class:
template <typename T>
struct Heap
{
int Capacity; //描述堆的能力,i.e.,堆一共能放幾個(gè)數(shù)
int Size; //堆目前放了幾個(gè)數(shù)
T *elements; //一個(gè)放置元素的數(shù)組
};
而我們?cè)谑褂玫臅r(shí)候,可以這樣定義一下:
typedef struct Heap *PriorityQueue;
就會(huì)有一個(gè)指向此優(yōu)先隊(duì)列的指針。
而初始化這個(gè)優(yōu)先隊(duì)列也很簡單,給指向優(yōu)先隊(duì)列的指針一個(gè)空間,給放置元素的數(shù)組開辟一個(gè)空間,給結(jié)構(gòu)Heap一個(gè)初始值:
template<typename T>
PriorityQueue Initialize( int MaxSize )
{
PriorityQueue H;
H = malloc( sizeof( struct Heap ) ); //開辟指針的空間
if ( H == NULL )
Error( "out of space" ); //空間開辟失敗
H->Elements = malloc( ( MaxSize + 1 ) * sizeof( T ) ); //把最前邊的作為哨兵
if ( H == NULL )
Error( "out of space" );
//Heap初始化
H->Capacity = MaxSize;
H->Size = 0;
H->Elements[0] = Min; //這個(gè)Min是什么大家可以自己定義,比如INT_MIN
return H;
}
這個(gè)多一位的“哨兵”有2個(gè)好處,一個(gè)是左右孩子可以直接取2i和2i+1(不然的話第0位的2倍沒有意義),第二個(gè)是在執(zhí)行元素的插入操作時(shí)可以作為結(jié)束循環(huán)的判斷。
那咱們就寫一下如何Insert元素吧,因?yàn)槎咽怯许樞虻模圆迦霑r(shí)必須要考慮如何把它放到合適的位置,一般,咱們先在堆的末端添加一個(gè)空位置,然后判斷空位置放此元素是不是合理的,不合理的話就把此位置的父節(jié)點(diǎn)下移,然后嘗試父節(jié)點(diǎn)的位置放置此元素是否合理,直到根節(jié)點(diǎn):
template <typename T>
void insert( T x, PriorityQueue H )
{
int i;
if( Queue已經(jīng)滿了 )
{
Error;
return;
}
for( i = ++H->Size; H->Elements[i/2] > x; i /= 2) //i從一個(gè)新位置(size+1)開始
//這樣在父節(jié)點(diǎn)的移動(dòng)時(shí)就可以避免繁瑣的swap例程,只用依次放入;而循環(huán)條件也很簡單,
//只要父元素大于它,就把父元素下移,繼續(xù)尋找父元素
H->Elements[i] = H->Elements[i/2];
H->Elements[i] = x;
}
我覺得我已經(jīng)在code里把操作敘述的很清楚了,這里就不再多說,不過只說一點(diǎn)(還是要說啊),這個(gè)循環(huán)條件語句可以用簡單的大小判斷,就是因?yàn)槲覀儼?位置設(shè)為了哨兵(一個(gè)很小的值),這樣即使我們需要插入的值是最小值,當(dāng)它插入到根節(jié)點(diǎn)的位置后,就必然大于0位置的值(1/2=0),從而結(jié)束循環(huán)。
有插入就有刪除,我們實(shí)現(xiàn)一種刪除,就是刪除最小的元素:堆頂元素。其他的刪除可以參考它:
template<typename T>
T DeleteTop( PriorityQueue H )
{
int i, int child;
T minElement, lastElement;
if( empty( H ) )
{
Error; //空
return H->Elements[0]; //返回第0個(gè)元素
}
minElement = H->Elements[1];
lastElement = H->Elements[H->Size--]; //賦給最后一個(gè)元素,同時(shí)size減1,因?yàn)橐獎(jiǎng)h除一個(gè)元素
for( i = 1; i * 2 <= H->Size; i = child)
//條件是(左)兒子在范圍內(nèi),因?yàn)槿绻蠛⒆硬辉谀敲从液⒆颖厝徊辉? {
child = i * 2;
if( child != H->size && H->elements[child + 1] < H->elements[child])
//這里我們沒有明顯檢測(cè)右孩子是否存在,但是其實(shí)用了child != H->size來控制,這里的!=
//其實(shí)就是<,只有它不是最后一位,那么就保證了child+1位置的存在
child++;
if( lastElements>H->elements[child] )
H->elements[i] = H->elements[child];
else
break;
}
H->elements[i] = lastElements;
return minElements;
}
有了這個(gè)刪除堆頂(最小)元素的例程,我們就可以做刪除任意元素的操作:把想要?jiǎng)h除的元素賦值為最小元素,即給它一個(gè)小于堆頂元素的值,然后Insert到堆頂,最后執(zhí)行DeleteTop就好了。
我們寫了很多關(guān)于二叉堆的操作,其實(shí)和馬上要寫的堆排序代碼并不太一致,至少不需要掌握復(fù)雜的插入刪除,但它是我們理解堆排序的必要知識(shí)。
現(xiàn)在我們來看堆排序就很簡單了,它的主要操作就是:
- 拿數(shù)據(jù)建堆。
- 按照規(guī)則刪堆(解散堆),因?yàn)橹粍h除堆頂?shù)模首詈蟮玫降氖且粋€(gè)有序序列。
假設(shè)我們還是需求一個(gè)非減序列,那么相應(yīng)的,我們最好建一個(gè)最大堆,因?yàn)樽畲蠖训膖op delete之后可以放到堆尾,這樣的排序不需要額外的空間,可以說是相當(dāng)成功了。
作為一個(gè)可以用的排序,我覺得應(yīng)該從數(shù)組的第0位開始(廢話,就這么一個(gè)數(shù)組做原地排序,你不從第0位開始從哪開始啊?),也因?yàn)槭菑牡?位開始的,左孩子就不能是2i了(上邊講過),得是2i+1。那么,我們寫寫?
#define LeftChild(i) (2*(i)+1)
template<typename T>
void heap( T a[], int i, int n )
{
int child;
T tmp;
for( tmp = a[i]; LeftChild(i) < n; i = child )
{
child = LeftChild(i);
if( child != n - 1 && a[child+1] > a[child] )
child++;
if( tmp < a[child] ) //建立最大堆,那么就是parent小于child時(shí),child上移
a[i] = a[child];
else
break; //找到了合適的位置,停止循環(huán)
}
a[i] = tmp;
}
這個(gè)建堆的基礎(chǔ)操作,被我們拿來既用作建堆,也用做刪堆,當(dāng)然在這里更好的稱呼是排序:建堆就不用講了,就是從最后一位元素開始逐漸建立小堆,然后合并成大堆;而排序的過程就是把top元素放入堆尾,對(duì)其他元素做重建堆:
template<typename T>
void HeapSort( T a[], int n )
{
int i;
for( i = n/2; i >= 0; i-- )
heap( a, i, n ); //建堆
for( i = n - 1; i > 0; i--) //一個(gè)微小的點(diǎn):i>0,不需要=,因?yàn)樽詈笠豁?xiàng)不用排序
{
swap( a[0], a[i] ); //交換第0項(xiàng)和最后一項(xiàng)后,排除最后一項(xiàng)建堆
heap( a, 0, i );
}
}
以上2段代碼只有2個(gè)需要注意的地方,一是HeapSort()
建堆的過程中,從i=n/2;
開始,而不是很多代碼的i=n-1;
,因?yàn)樽詈笠粚硬]有什么可建的,完全是浪費(fèi)時(shí)間;二是在HeapSort()
執(zhí)行heap()
時(shí),最后一個(gè)參數(shù)是數(shù)組的數(shù)量,也就是數(shù)組的最后一項(xiàng)加1,因?yàn)樵?code>heap()中的循環(huán)條件決定了我們是拿n
來比較的,這和其他排序的例程中使用n-1
稍微有所區(qū)別。
目前,我們已經(jīng)把所有經(jīng)典的排序都過了一遍,順便還講了遞歸式的證明,以及二叉堆,在一般的算法課上,這大概需要不到一個(gè)月的時(shí)間,希望大家喜歡。