百度一道面試題說起,題目是這樣的: 給出一個長度是N的數組,現在要找出最小的兩個元素,最少要多少次比較。
分析: 如果找出1個最小的,比較次數無疑是 n - 1, ;如果用選擇排序,再取選擇第二個最小的又得比較n-2次。這種尋找的辦法其實是可以優化的,在第一次尋找最小元素過程中,其實我們已經比較了很多元素了,那么為什么不利用前面比較的結果來尋找第二個最小的呢。
這用到錦標賽排序的方法,這樣就可以再使用 logn就可以找到了第二小的元素。
錦標賽排序原理
錦標賽排序又叫樹型排序,屬於選擇排序的一種。直接選擇排序之所以不夠高效就是因為沒有把前一趟比較的結果保留下來,每次都有很多重復的比較。錦標賽排序就是要克服這一缺點。它的基本思想與體育淘汰賽類似,首先取得n個元素的關鍵字,進行兩兩比較,得到 n/2 個比較的優勝者,將其作為第一次比較的結果保留下來,然后對這些元素再進行關鍵值的兩兩比較,…,如此重復,直到選出一個關鍵字最小的對象為止。
下面舉個例子,假設arr[] = {3,4,1,6,2,8,7,9},我們首先需要建立一棵完全二叉樹,注意如果不夠arr的長度沒得2的冪次方,我們需要補一些元素。注意看,數組arr的元素其實分布在葉子節點上,其他分支幾點是存儲了比賽的結果。根據這個分析,那么我們用n-1次比較就可以建立如下圖所示的完全二叉樹:


注:上圖中,樹其實利用數組存儲的,深顏色表示葉子節點,前面白色表示父親節點,分別指向孩子節點中的最小值。
根據上面的示意圖,其實我們還需要一個變量來存儲勝者的索引。於是結構體,應該這樣定義:
struct node{
int nData;
int id;//記錄勝者的索引
node(int n,int i){nData=n;id=i;}
};
那么,根據這個定義,我們再來畫一下圖,逗號后面記錄了勝者的索引:

於是,第一次建樹,就成上面這樣了,我們可以輕松通過根節點得到最后的勝者(最小節點),並且同時知道勝者的索引號。下次在搜索最小值的時候,我們需要將剛才勝者值替換為最大值,然后沿着紅色的線比較一遍就行了,這次只需要比較三次就行了(logn),請看下圖。

然后,一直進行這個過程中就可以完成對數組的排序,排序的時間復雜度。
從這個演示可以看出這個算法真正吸引我們的地方就是當決出一個勝者后,要取得下一個勝者的比較只限於從根到剛才選出的外結點這一條路徑上。可以看出除第一次比較需要n-1次外,此后選出次小,再次小......的比較都是log2 n次,故其復雜度為O(n*log2 n)。但是對於有n個待排元素,錦標賽算法需要至少2n-1個結點來存放勝者樹。故,這是一個拿空間換時間的算法。
代碼實現
下面代碼我寫得很差,主要是練練手,我代碼也是參考別人的。
可以看看這個哥們java寫的:http://blog.csdn.net/hopeztm/article/details/7921686。
#include <iostream>
#include <cassert>
using namespace std;
#define MAX 0x7fffffff
struct node{
int nData;
int id;
node(int n,int i){nData=n;id=i;}
};
node* BuildTree(int data[],int len,int &nTreeSize)
{
int nNodes = 1;
while(nNodes<len)//為了構建完全二叉樹,不夠的要補
nNodes <<= 1;
nTreeSize = nNodes*2 - 1;
node *trees = (node*)malloc(sizeof(node)*nTreeSize);
assert(trees);
for(int i=nNodes-1; i<nTreeSize; i++){
int idx = i - (nNodes - 1);
if(idx<len)
trees[i] = node(data[idx],i);
else
trees[i] = node(MAX,-1);//對於補充的數據,我們初始化成最大。
}
for(int i=nNodes-2; i>=0; --i){ //初始化,前面白色節點,指向孩子節點的最小值
if(trees[i*2+1].nData < trees[i* 2+2].nData)
trees[i] = trees[i*2+1];
else
trees[i] = trees[i*2+2];
}
return trees;
}
void Adjust(node *data, int idx)//當去除最小元素以后,我們要調整數組
{
while(idx != 0) //從后向前調整
{
if(idx%2 == 1)//當前id是奇數,說明並列的是idx + 1, 父節點是 (idx-1)/2
{
if(data[idx].nData < data[idx + 1].nData) //idx+1為兄弟節點
data[(idx-1)/2] = data[idx];
else
data[(idx-1)/2] = data[idx+1];
idx = (idx-1)/2;
}
else
{
if(data[idx-1].nData < data[idx].nData) //idx-1為兄弟節點
data[idx/2-1] = data[idx-1];
else
data[idx/2-1] = data[idx];
idx = (idx/2-1);
}
}
}
void sort(node *trees,int len)//返回排序的結果
{
int dataLen = len/2+1;
int *data = new int[dataLen];
assert(data);
for(int i=0; i<dataLen; i++){
data[i] = trees[0].nData;//輸出
trees[trees[0].id].nData = MAX;//輸出節點替換為最大值
Adjust(trees,trees[0].id);//調整樹
}
for(int i=0;i<dataLen;i++){
cout<<data[i]<<" ";
}
cout<<endl;
delete[] data;
}
void PrintArr(node *arr,int len)
{
assert(arr && len>0);
for(int i=0; i<len; ++i){
cout<<arr[i].nData<<" ";
}
cout<<endl;
}
int main()
{
int treeLen;
node *trees;
int arr[] = {3,4,1,6,2,8,7,9};
trees = BuildTree(arr,8,treeLen);
PrintArr(trees,treeLen);
sort(trees,treeLen);
delete[] trees;
system("pause");
return 0;
}
后記
那么第一次最大值,后面求第二大值,也可以類似的做。只不過勝者是值大的。這樣,樹的根節點就是最大值了而不是最小值。
