锦标赛排序(胜者树,记录胜者)
百度一道面试题说起,题目是这样的: 给出一个长度是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; }
后记
那么第一次最大值,后面求第二大值,也可以类似的做。只不过胜者是值大的。这样,树的根节点就是最大值了而不是最小值。