从效率最高的 堆排序 / 快速排序/归并排序/基数排序开始记录。
堆排序
辅助性理解图:https://www.cnblogs.com/chengxiao/p/6129630.html
1. 堆的理解
- 堆的概念
我们一般提到堆排序里的堆指的是二叉堆(binary heap),是一种完全二叉树,二叉堆有两种:最大堆和最小堆,特点是父节点的值大于(小于)两个小节点的值。 - 基础知识
完全二叉树有一个性质是,除了最底层,每一层都是满的,这使得堆可以利用数组来表示,每个结点对应数组中的一个元素,如下图所示
对于给定的某个结点的下标 i(从1开始),可以很容易的计算出这个结点的父结点、孩子结点的下标:
父节点与孩子节点的相对关系计算如下:
Parent(i) = floor((i-1)/2),i 的父节点下标
Left(i) = 2i + 1,i 的左子节点下标
Right(i) = 2(i + 1),i 的右子节点下标
2. 堆的基本操作
-
最大堆调整
该操作主要用于维持堆的基本性质。假设数组A和下标i,假定以Left(i)和Right(i)为根结点的左右两棵子树都已经是最大堆,节点i的值可能小于其子节点。调整节点i的位置,使得子节点永远小于父节点,过程如下图所示:
由于一次调整后,堆仍然违反堆性质,所以需要递归的测试,使得整个堆都满足堆性质。 -
创建最大堆
创建最大堆(Build-Max-Heap)的作用是将一个数组改造成一个最大堆,接受数组和堆大小两个参数,Build-Max-Heap 将自下而上的调用 Max-Heapify 来改造数组,建立最大堆。因为 Max-Heapify 能够保证下标 i 的结点之后结点都满足最大堆的性质,所以自下而上的调用 Max-Heapify 能够在改造过程中保持这一性质。如果最大堆的数量元素是 n,那么 Build-Max-Heap 从 Parent(n) 开始,往上依次调用 Max-Heapify。流程如下:
c++实现创建大顶堆
void build_max_heap(int *datas,int length)
2 {
3 int i;
4 //build max heap from the last parent node
5 for(i=length/2;i>0;i--)
6 adjust_max_heap(datas,length,i);
7 }
3. 堆排序算法
堆排序算法过程为:先调用创建堆函数将输入数组A[1…n]造成一个最大堆,使得最大的值存放在数组第一个位置A[1],然后用数组最后一个位置元素与第一个位置进行交换,并将堆的大小减少1,并调用最大堆调整函数从第一个位置调整最大堆。
void heap_sort(int *datas,int length)
{
int i,temp;
//bulid max heap
build_max_heap(datas,length);
i=length;
//exchange the first value to the last unitl i=1
while(i>1)
{
temp = datas[i];
datas[i] = datas[1];
datas[1] =temp;
i--;
//adjust max heap,make sure the fisrt value is the largest
adjust_max_heap(datas,i,1);
}
}
其中,建立大顶堆的算法如下:
void build_max_heap(int *datas,int length)
2 {
3 int i;
4 //build max heap from the last parent node
5 for(i=length/2;i>0;i--)
6 adjust_max_heap(datas,length,i);
7 }
调整为大顶堆的算法如下:
void adjust_max_heap(int *datas,int length,int i)
{
int left,right,largest;
int temp;
while(1)
{
left = LEFT(i); //left child
right = RIGHT(i); //right child
//find the largest value among left and rihgt and i.
if(left <= length && datas[left] > datas[i])
largest = left;
else
largest = i;
if(right <= length && datas[right] > datas[largest])
largest = right;
//exchange i and largest
if(largest != i)
{
temp = datas[i];
datas[i] = datas[largest];
datas[largest] = temp;
i = largest;
continue;
}
else
break;
}
}
Java算法如下:
import java.util.Arrays;
/**
*
* @author Administrator
*
*/
public class HeapSort {
public static void main(String []args){
int []arr = {7,6,7,11,5,12,3,0,1};
System.out.println("排序前:"+Arrays.toString(arr));
sort(arr);
System.out.println("排序前:"+Arrays.toString(arr));
}
public static void sort(int []arr){
//1.构建大顶堆
for(int i=arr.length/2-1;i>=0;i--){
//从第一个非叶子结点从下至上,从右至左调整结构
adjustHeap(arr,i,arr.length);
}
//2.调整堆结构+交换堆顶元素与末尾元素
for(int j=arr.length-1;j>0;j--){
swap(arr,0,j);//将堆顶元素与末尾元素进行交换
adjustHeap(arr,0,j);//重新对堆进行调整
}
}
/**
* 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)
* @param arr
* @param i
* @param length
*/
public static void adjustHeap(int []arr,int i,int length){
int temp = arr[i];//先取出当前元素i
for(int k=i*2+1;k<length;k=k*2+1){//从i结点的左子结点开始,也就是2i+1处开始
if(k+1<length && arr[k]<arr[k+1]){//如果左子结点小于右子结点,k指向右子结点
k++;
}
if(arr[k] >temp){//如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
arr[i] = arr[k];
i = k;
}else{
break;
}
}
arr[i] = temp;//将temp值放到最终的位置
}
/**
* 交换元素
* @param arr
* @param a
* @param b
*/
public static void swap(int []arr,int a ,int b){
int temp=arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}
4. 时间复杂度与排序稳定性
我们知道n个元素的完全二叉树的深度h=floor(logn),分析各个环节的时间复杂度如下。
- 堆调整时间复杂度
从堆调整的代码可以看到是当前节点与其子节点比较两次,交换一次。父节点与哪一个子节点进行交换,就对该子节点递归进行此操作,设对调整的时间复杂度为T(k)(k为该层节点到叶节点的距离),那么有:
T(k)=T(k-1)+3, k∈[2,h]
T(1)=3
迭代法计算结果为:
T(h)=3h=3floor(log n)
所以堆调整的时间复杂度是O(log n) 。
- 建堆的时间复杂度
n个节点的堆,树高度是h=floor(log n)。
对深度为于h-1层的节点,比较2次,交换1次,这一层最多有2(h-1)个节点,总共操作次数最多为3(12(h-1));对深度为h-2层的节点,总共有2(h-2)个,每个节点最多比较4次,交换2次,所以操作次数最多为3(22(h-2))……
以此类推,从最后一个父节点到根结点进行堆调整的总共操作次数为:
s=3*[2^(h-1) + 22^(h-2) + 32^(h-3) + … + h2^0] a
2s=3[2^h + 22^(h-1) + 32(h-2) + … + h2^1] b
b-a,得到一个等比数列,根据等比数列求和公式
s = 2s - s = 3[2^h + 2^(h-1) + 2^(h-2) + … + 2 - h]=3*[2^(h+1)- 2 - h]≈3*n
所以建堆的时间复杂度是O(n)。
- 堆排序时间复杂度
从上面的代码知道,堆排序的时间等于建堆和进行堆调整的时间之和,所以堆排序的时间复杂度是O(nlog n + n) =O(nlog n)。
5. 稳定性
堆排序是不稳定的算法,它不满足稳定算法的定义。它在交换数据的时候,是比较父结点和子节点之间的数据,所以,即便是存在两个数值相等的兄弟节点,它们的相对顺序在排序也可能发生变化。