1. 排序和顺序统计量
若果输入的数组中仅有常数个元素需要在排序过程中存储在数组之外,则称这个排序算法是原址的,归并排序并不是原址的因为在合并的时候要借助n个数组长度存储要合并的数据,堆排序和归并排序是渐进最优的比较排序算法。时间复杂度为:nlgn。
下面是常见的排序方法的时间复杂度:
2.堆排序
2.1堆
(二叉)堆是一个数组,他可以被近似的看成一个完全的二叉树,因此堆的性质可以结合完全二叉树的性质分析比较,树上的每一个节点,对应数组中的每一个元素,因此分析时可以建立树与数组(存储空间)上的映射,解决堆的问题。存储堆的数组有两个属性,A.length是给出的数组元素的个数,A.heapSize是存放堆中的有效元素,在进行堆排序时,将最大的元素(数组的0位置)与最后一个元素交换位置,并把交换后的最大元素移除,实现“移除”就是通过改变heapSize的值来虚拟的移除最后一个元素,说到改变下标实现数组的移除,通过改变下表的方法还可以简化很多操作,比如对矩阵进行分割,不用对每一块矩阵进行复制,只要指定下标的范围同样可以实现矩阵分割的效果比如在:分治求矩阵乘积中的应用。好了,再回到堆,这个堆其实是一个结构体,在JAVA中实现可以使用类中组合一个数组,在类中定义对堆操作的所有方法。(下面的 代码没有使用这种数据结构,只是把heapSize传入方法中)。
堆的性质:
1. 按层次的遍历方法对二叉树进行标号(1……n),根节点的左右孩子分别为2n和2n+1;每个节点的双亲节点为n/2;
2.堆的高度:节点的高度从该节点到叶节点最长简单路径上边的数目。从而把堆的高度定义为根节点的高度。如上图堆得高度为 3;注意区别树的深度(层数)
3在堆中计算元素个数都是通过堆的高度计算的,计算时间复杂度也是使用高度。所以一个高度为h的堆,节点的数目最多为:(层数=h+1);
4含有n个元素的堆的高度为:,层数为+1;
5:最大堆:根节点大于等于左右孩子节点。
最小堆:根节点小于等于左右孩子节点,
6堆中叶子节点对应编号下标:n/2+1,n/2+2,……n 这一点性质非常有用,在你建堆的过程中不用对每一个节点都进行最大堆的维护,只需对非叶子节点进行维护即可,因为叶子节点没左右孩子,肯定是自身最大。至于这条性质的证明,最后一个叶子节点对应的双亲结点肯定是最后一个非叶子节点,为n/2;
7.对于任意包含n个元素的堆,最多个高度为h的节点。注意:叶子节点不管是否在同一层高度都为0;谨记高度的定义。
注意:上面这些标号并不是对应数组中的标号,比如那些,求i的孩子节点,要转化为数组中的位置,所以就要仔细判断边界。
2.2维护堆的性质
对于最大堆,堆中需要维护的节点是一个较小的值把这个节点原来的值替换了。比如插入元素的时候是不用维护的,一但交换,是这两个节点处的值都比以前变大了,肯定比左右孩子大,所以不用维护。
堆的维护:选定要维护的节点i(在数组中的下标,假定其他节点都满足最大堆的条件),然后A[i]是否为与其左右孩子中的最大值,如果是则退出,如果不是则A[i]与最大的值交换,因为交换了,所以原来最大值下标处的值变小了,此节点可能不满足最大堆,所以递归。注意:求解其左右孩子时,不要让其左右孩子下标越界(可能没有左右孩子),所以if else 比较出最大值时,一定要加越界判断。刚开始求解三个数最大值下标时:用的一个问号表达式,结果最后不能判断 只有左孩子的情况,所以注释了。下面给出维护最大堆的递归与非递归实现:
/**
* @param 要维护的堆(使用数组储存)
* @param 维护堆中的哪一个根节点,当这个根节点不是左右孩子的最大值,交换再次维护原最大值
* 所在节点,
* 最大堆的维护:注意在求出左右孩子下标时一定要判断是否越界
* 1.递归实现
*/
public void maxHeapify(int []a,int i,int heapSize)
{
int left=2*i+1;
int right=2*i+2;
int large;
//这里不能用一个return 因为这个没有判断 左右孩子是否越界,并且还有可能只有一个左孩子
//此时右孩子是越界的,还不能同时判断,所以要分开判断
//!large=a[i]>a[left]?(a[i]>a[right]?i:right):(a[left]>a[right]?left:right);
//if else求三者最大值,先两两比较找出最大值,再用这个最大值和第三个数比较
if(left<heapSize&&a[left]>a[i])
large=left;
else
large=i;
if(right<heapSize&&a[right]>a[large])
large=right;
if(large!=i)
{
int temp=a[i];
a[i]=a[large];
a[large]=temp;
maxHeapify(a, large,heapSize);//交换之后可能使large所在下标的子树,不符合最大堆了;所以递归。
}
}
/**
* @param a
* @param i
* 2.非递归实现
*/
public void maxHeapify2(int []a,int i,int heapSize)
{
int large=1000;
while(true)
{
int left=2*i+1;
int right=2*i+2;
if(left<heapSize&&a[left]>a[i])
large=left;
else
large=i;
if(right<heapSize&&a[right]>a[large])
large=right;
if(large!=i)
{
int temp=a[i];
a[i]=a[large];
a[large]=temp;
i=large;
}
else
break;
}
}
2.3建堆
建立最大堆,就是自底向下依次对非叶子结点,进行维护,对叶子节点维护无意义。为什么是自底向下?因为维护是当元素互换位置后,保证下面的节点也维持最大堆。
/**
* @param a建立最大堆
*/
public void bulidMaxHeap(int a[])
{
int len=a.length;
int start=len/2-1;
for(int i=start;i>=0;i--)
maxHeapify2(a, i,len);
}
2.4堆排序
堆排序原理:先对原数组建立最大堆,此时此时数组0号元素为根节点,即这些元素的最大值,所以把A[0]与最后一个元素互换位置;接着,只剩下原堆的左右子树,因为原根节点的左右孩子是符合最大堆,只有交换上去的叶子节点需要进行维护,维护完成后,又是一个最大堆,如此循环直到找出前n-1个最大的元素为止,所以也要交换n-1次。
在最坏的情况下时间复杂度为:nlgn
注意:交换后,那个最大的元素A[n]已经“移除”堆了,并不是真正的移除,只是通过改变堆的有效长度,heapSize虚拟的移除。
/**
* @param a堆排序
*/
public void heapSort(int a[])
{
int heapSize=a.length;
bulidMaxHeap(a);
for(int i=a.length-1;i>=1;i-- )//需要交换的次数
{
int temp=a[0];
a[0]=a[i];
a[i]=temp;
heapSize--;//交换之后的有效长度减一,对去除最后一个元素的堆进行维护,堆的长度改变
maxHeapify(a, 0, heapSize);
}
}
2.5优先队列
堆排序是一个优秀的算法,但是快速排序的性能一般由于堆排序。尽管如此,堆可以作为高效的调度队列,优先队列有两种形式:最大优先队列和最小优先队列。两者的思想都是一样的,最大堆实现最大优先队列,最小堆实现最小优先队列。下面我们只研究最大优先队列:其主要实现的功能就是 1 返回权重最大的值 2 将一个元素的key进行增值
3 插入元素(相当于在数组最后插入一个key很小的叶子节点然后对它增值)4移除根节点,通过控制heapSize进行移除。我没有将堆写成一个结构体或者java类的形式。封装的话只需加上heapSize。下面是代码:
/**
* 二叉堆在优先队列上的应用
* 1.返回堆中的根节点
*/
public int heapMax(int a[])
{
return a[0];
}
/**
* 2.移除堆中的根节点
*
*/
public int extractMax(int a[])
{
int max=a[0];
int heapSize=a.length;
a[0]=a[heapSize-1];
heapSize--;
maxHeapify(a, 0, heapSize);
return max;
}
public void increaseKey(int a[],int i,int key)
{
if(a[i]>key)
{
System.out.println("error");
return ;
}
int parent=(i+1)/2-1;
a[i]=key;
if(parent>=0&&a[parent]<a[i])
{
int temp=a[parent];
a[parent]=a[i];
a[i]=temp;
increaseKey(a, parent, key);
}
}
public void heapInsert(int a[],int key)
{
increaseKey(a, a.length-1, key);
}
看了这种插入的方式,其实,借助插入排序的思想和heapInsert也可以进行建堆,时间复杂度为nlgn
/**
* @param a利用插入堆的方法进行建堆,这时需要使用heapSize不能用数组总长度。
* 要插入的下标:1……n-1; 因为我没写成数据结构的形式所以 不大方便使用insert使用的
* increaseKey
*/
public void bulidMaxHeap2(int a[])
{
for(int i=1;i<a.length;i++)
{
increaseKey(a, i, a[i]);
}
}
对于d叉堆:
一个d叉堆在一个数组中表示方法如下:堆的根元素放在A[1]里面,它的d个孩子分别放在A[2]到A[d+1]中。下面程序描绘了第i个元素与它的父亲结点以及它的第j个孩子的对应关系:
D-ARY-PARENT(i)
return[(i-2)/d+1]d
d叉堆i节点的第一个孩子为 i+(后面的为第i所在层元素的个数)c为i所在的层数
b. 因为每一个结点有d个孩子,因此该树的高度是Θ(logdn)。
c. HEAP-EXTRACT-MAX算法就很适用于d叉堆;但问题在于MAX-HEAPIFY算法。在此需要将处理的结点和它所有的孩子进行比较。所以运行的时间应为Θ(dlogdn)。
d. MAX-HEAP-INSERT算法也适用于此。最坏情况下的运行时间就是堆的高度即Θ(logdn)。
e. HEAP-INCREASE-KEY算法适用,运行时间为O(logdn)。