1.要看堆,先看完全二叉树
如果对第一节的完全二叉树不感兴趣,请用目录跳到第二节。
事实上,本人对二叉树的高度和深度一直搞不太清楚。
我发现没有一个统一的定义,连代码都不知道怎么写了,那我们定义:
(1)一颗完全二叉树层数从0开始计算,第 i 层最多有 2^i 个结点
(2)结点的高度:从本结点到叶子的最长简单下降路径边的数目
(3)基于第2点,空的完全二叉树高度为-1,只有根结点的高度为0(没有边),高度为k(k>=-1)的二叉树的最大结点2^(k+1) - 1
(4)具有n个结点的完全二叉树高度 ceil( log2(n)+1),ceil()是向下取整
(5)树的深度:树的所有结点的最大层数(照这么定义,树的深度 == 根的高度,深度形容树,高度形容结点)
完全二叉树定义
(第一种)二叉树深度为H,结点数为N,它与一棵深度为H的满二叉树的【1..N】结点,唯一对应,这样的二叉树叫完全二叉树。(第二种)二叉树的每一层除了最后一层都是满的,最后一层的结点连续集中在最左侧。
.
完全二叉树特点
能回到这些问题吗?
(1)想到完全二叉树,你会想到什么数据结构来描述它
(2)高度为h,那么每一层
最多有多少结点?总共
最多有多少结点?
(3)知道了父结点,怎么求孩子结点吗?
(4)知道了孩子结点,怎么求父结点吗?
(5)怎么立刻知道一个父结点是否拥有两个孩子结点?拥有一个孩子结点呢?
(6)怎么立刻确定一个结点是叶子结点呢?
如果你不懂,以上问题都是很简单的问题,只要你跟着以下几张图走,就明白了!
如果你懂,请跳过 第 1 大节!
(一)基于完全二叉树的特性,很明显地,我们用数组而不是链表来描述它
(二)高度为h,每一层有2^(h)个结点,总共最多有2^(h+1) - 1个结点
(三)知道父亲结点,求孩子结点?
如果父亲是i且2*i<=n(边界条件),那么左孩子就是2*i
如果孩子是i且2*i+1 <= n(边界条件),那么右孩子就是2*i+1
(四)知道孩子结点,求父亲结点?
i结点的父亲是i/2,取整,其中i>1(边界条件)
(五)立刻确定某父亲结点 i 拥有两个孩子?
2*i+1 <= n
(六)立刻确定某结点为叶子结点?
2*i > n
我们可以很明显的推导出来,叶子结点:[n/2],[n/2]+1,[n/2]+2,..,n
(后面BuildMaxHeap()要用到哦~)
完全二叉树叶子结点数算法
n0 = floor(n/2)证明:已知共有结点n,度为0的结点为n0(也就是叶结点),度为1的结点为n1,度为2的结点为n2
(1)n = n0+ n1+n2
(2)二叉树特性:入度 =出度(入的分支数 = 出的分支数) => n-1 = 2*n2 + 1* n1 + 0*n0
那么:n-1 = n0+n1+n2-1 = 2*n2 + n1
那么:n2 +1 = n0
(3)在完全二叉树中,n1的个数要么为0,要么为1
n = n0 + n2 + n1
= n0 + n0-1 + 0 ( n0 + n0-1 +1)
= 2*n0 -1 (2*n0)
那么 n0 = floor(n/2),floor() 向上取整
2.堆排序-一个时间复杂度为O(nlgn)的排序
为了方便以及可移植性,我们定义一个
CHeap类:
class CHeap
{
public:
CHeap(int heapSize):mHeapSize(heapSize)
{
mHeap = new int[heapSize];
if(NULL != mHeap)
{
memset(mHeap,0,sizeof(int)*heapSize);
}
}
~CHeap()
{
delete[] mHeap;
}
void MaxHeapify(int i);
void MaxHeapify_iteration(int i);
void MaxHeapify_iteration_2(int i);
void BuildMaxHeap();
void HeapSort();
inline void SetMHeapValue(int *a,int size)
{
for(int i=0; i<size;i++ )
{
mHeap[i] = a[i];
}
}
inline int LeftChild(int i)
{
return (i<<1)+1;
}
inline int RightChild(int i)
{
return (i<<1)+2;
}
inline void Swap(int &a,int &b)
{
int tmp = a;
a=b;
b=tmp;
}
public:
int mHeapSize;
int* mHeap;
};
MaxHeapify(int i)
功能:使以 i 为祖先的分支成为最大堆。
void CHeap::MaxHeapify(int i)
{
int left = LeftChild(i);
int right = RightChild(i);
int max = i;
//父亲,左孩子,右孩子 三者取MAX
if(left < mHeapSize && mHeap[i] < mHeap[left]) { max = left;}
if(right < mHeapSize && mHeap[max] < mHeap[right]) { max = right;}
//如果父亲没有孩子大,就和孩子换
if(max != i)
{
Swap(mHeap[max],mHeap[i]);
//交换后可能破坏了最大堆性质,还要对交换后的父亲结点MaxHeapify
MaxHeapify(max);
}
}
解读:
(1)在二叉树中像这种从上到下的算法,很明显时间复杂度就是lg(n)。
(2)就是保证父亲比两个孩子都大,小了就交换,换完再次确保被换下来的父亲依旧满足这个规则即可。
重点解读:
(1)T(n)时间复杂度,可以分解为规模为T(2n/3)的子情况+执行必要代码的Θ(1)
(2)为什么是2n/3的子情况呢?因为最后一层刚刚半满,子情况中结点数为总的结点数的2/3
(3)T(n) = T(2n/3) +Θ1
(4)递归式的主方法可得:g(n) = n^(log2/3(1)) = n^0 = 1 = f(n)
T(n) = O(lgn)
BuildMaxHeap()
功能:建立最大堆。
void CHeap::BuildMaxHeap()
{
for(int i = mHeapSize/2 -1 ; i >= 0;i--)
{
MaxHeapify(i);
}
}
(1)我们分析过了,叶子结点:[n/2],[n/2]+1,[n/2]+2,...n
(2)我们在建立最大堆的时候,要从有孩子的父结点开始从底往上建立!
(3)单个叶子结点因为没有孩子而被默认为最大堆了(想想看,建堆的时候有一个过程:父,左,右三者取大)
(1)根据0x13-1节的重点解读,我们知道MaxHeapify()的时间复杂度为 O(lgn)
(2)明显,BuildMaxHeap() 时间复杂度为 nlgn,但是《算法导论》说,呵呵,可以逼近
(3)
然后用了个级数的积分和微分公式,逼近成了O(n)
HeapSort()
功能:堆排序。
void CHeap::HeapSort()
{
BuildMaxHeap();
for(int i = mHeapSize-1; i >= 1 ; i--)
{
Swap(mHeap[i],mHeap[0]);
mHeapSize --;
MaxHeapify(0);
}
}
(1)BuildMaxHeap()建立最大堆之后,根元素就是最大的!我们取出来,放到堆尾巴
(2)放完根元素后,堆的规模减1
因为本来的堆尾跑到了堆的根,我们要来一次MaxHeapify(0);保持堆的性质(爸爸比孩子大)
(3)直到堆的规模为1的时候结束(留下的最后一个不用排)
(1)BuildMaxHeap() 时间复杂度为O(n)
(2)MaxHeapify() 时间复杂度为O(lgn),加个循环:O(nlgn)
(3)加起来是O(n) + O(nlgn) = O(nlgn)
迭代版MaxHeapify()
实际上,迭代版本避免了进出函数所费的时间,比递归版本要高效些。
我们可以用循环来代替递归:
void CHeap::MaxHeapify_iteration(int i)
{
int left = LeftChild(i);
int right = RightChild(i);
int max = i;
if(left < mHeapSize && mHeap[i] < mHeap[left]) { max = left;}
if(right < mHeapSize && mHeap[max] < mHeap[right]) { max = right;}
//和递归版本区别在置换后对"堆结构的破坏"的修复
while(max != i)
{
swap(mHeap[i], mHeap[max]);
i = max;
left = LeftChild(i);
right = RightChild(i);
if(left < mHeapSize && mHeap[left] > mHeap[i]) { max = left;}
if(right < mHeapSize && mHeap[right] > mHeap[max]) { max = right;}
}
}
算法导论给出的迭代版:
void CHeap::MaxHeapify_iteration_2(int i)
{
int left = LeftChild(i);
int right = RightChild(i);
while(left < mHeapSize)
{
int max = i;
if(left < mHeapSize && mHeap[left] > mHeap[i]){ max = left;}
if(right < mHeapSize && mHeap[right] > mHeap[max]){max = right;}
if(max != i)
{
Swap(mHeap[max],mHeap[i]);
i = max;
left = LeftChild(i);
right = RightChild(i);
}
else
break;
}
}
不稳定的排序
如果两个相同的值,排序后位置不变,则稳定。
否则,不稳定。
我们看看HeapSort(),为了方便说明,举个例子:
序列:39,27,36,27(用二叉树建立)
这是一个最大堆!为了方便,我们把第一个27标志位27_1,第二个27标志位27_2
(1)排序的时候把根元素39放到堆尾巴
(2)27_2会被提到根元素,进行一轮排序,27_2在与27_1比较的时候并不用调换位置
(3)轮到27输出的时候,27_2先于27_1输出
结论:堆排序是不稳定的排序!
编程应用
使用方法:
使用代码如:
int size = 5;
CHeap heap(size);
int A[5]={3,21,1,51,42};
heap.SetMHeapValue(A,size);
heap.HeapSort();
导入CHeap.h
#ifndef CHEAP_H_INCLUDED
#define CHEAP_H_INCLUDED
#include <memory.h>
#include <string.h>
#include <iostream>
class CHeap
{
public:
CHeap(int heapSize):mHeapSize(heapSize)
{
mHeap = new int[heapSize];
if(NULL != mHeap)
{
memset(mHeap,0,sizeof(int)*heapSize);
}
}
~CHeap()
{
delete[] mHeap;
}
void MaxHeapify(int i);
void MaxHeapify_iteration(int i);
void MaxHeapify_iteration_2(int i);
void BuildMaxHeap();
void HeapSort();
inline void SetMHeapValue(int *a,int size)
{
for(int i=0; i<size;i++ )
{
mHeap[i] = a[i];
}
}
inline int LeftChild(int i)
{
return (i<<1)+1;
}
inline int RightChild(int i)
{
return (i<<1)+2;
}
inline void Swap(int &a,int &b)
{
int tmp = a;
a=b;
b=tmp;
}
public:
int mHeapSize;
int* mHeap;
};
void CHeap::MaxHeapify(int i)
{
int left = LeftChild(i);
int right = RightChild(i);
int max = i;
//父亲,左孩子,右孩子 三者取MAX
if(left < mHeapSize && mHeap[i] < mHeap[left]) { max = left;}
if(right < mHeapSize && mHeap[max] < mHeap[right]) { max = right;}
//如果父亲没有孩子大,就和孩子换
if(max != i)
{
Swap(mHeap[max],mHeap[i]);
//交换后可能破坏了最大堆性质,还要对交换后的父亲结点MaxHeapify
MaxHeapify(max);
}
}
void CHeap::MaxHeapify_iteration(int i)
{
int left = LeftChild(i);
int right = RightChild(i);
int max = i;
if(left < mHeapSize && mHeap[i] < mHeap[left]) { max = left;}
if(right < mHeapSize && mHeap[max] < mHeap[right]) { max = right;}
//和递归版本区别在置换后对"堆结构的破坏"的修复
while(max != i)
{
Swap(mHeap[i], mHeap[max]);
i = max;
left = LeftChild(i);
right = RightChild(i);
if(left < mHeapSize && mHeap[left] > mHeap[i]) { max = left;}
if(right < mHeapSize && mHeap[right] > mHeap[max]) { max = right;}
}
}
//算法导论上给出的迭代版本
void CHeap::MaxHeapify_iteration_2(int i)
{
int left = LeftChild(i);
int right = RightChild(i);
while(left < mHeapSize)
{
int max = i;
if(left < mHeapSize && mHeap[left] > mHeap[i]){ max = left;}
if(right < mHeapSize && mHeap[right] > mHeap[max]){max = right;}
if(max != i)
{
Swap(mHeap[max],mHeap[i]);
i = max;
left = LeftChild(i);
right = RightChild(i);
}
else
break;
}
}
void CHeap::BuildMaxHeap()
{
for(int i = mHeapSize/2 -1 ; i >= 0;i--)
{
MaxHeapify(i);
}
}
void CHeap::HeapSort()
{
BuildMaxHeap();
for(int i = mHeapSize-1; i >= 1 ; i--)
{
Swap(mHeap[i],mHeap[0]);
mHeapSize --;
MaxHeapify(0);
}
}
#endif // CHEAP_H_INCLUDED