数据结构排序算法——选择排序(堆排序)

选择排序—堆排序(Heap Sort)

一、算法基本思想:

堆分为"最大堆"和"最小堆":最大堆通常被用来进行"升序"排序,而最小堆通常被用来进行"降序"排序。

每个结点的值都大于其左孩子和右孩子结点的值,称之为大根堆:

每个结点的值都小于其左孩子和右孩子结点的值,称之为小根堆:

二、堆排序的基本步骤:

1.首先将等待排序的数组构造成一个类似大根堆的树,构造结束后整个数组当中的最大值就是堆结构的顶端;

2.然后将顶端的数与末尾的数交换位置,交换结束后末尾的数为最大值,剩下其他的待排序的数组个数为n-1个;

3.将剩余的n-1个数再此构造成一个类似大根堆的树,再将顶端数与n-1位置的末尾数交换位置,重复上述步骤可以最终得到一个有序数组。

三、基本排序演示:

堆排序分为堆调整和堆排序。

堆调整:

给定一个无序数组a[]={16,7,3,20,17,8},首先根据该数组构建一个完全二叉树

然后构造初始堆,进行数据交换排序。

第一轮排序过后根节点位置已经排序完毕:

如图所示,堆的调整就已经结束了。

下面我们开始进行堆排序:

将堆顶元素(序列中的最大项)与队中的最后一个元素进行交换(最大项应该在序列的最后)。

不考虑已经换到最后的那个元素,只考虑前n-1个元素构成的子序列,显然,该子序列已经不是堆,但左右子树仍然是堆,可以经该子序列再次调整为堆。

 四、堆排序代码展示:

C语言代码:

void print(int a[], int n){  
    for(int j= 0; j<n; j++){  
        cout<<a[j] <<"  ";  
    }  
    cout<<endl;  
}  
  
  
  
/** 
 * 已知H[s…m]除了H[s] 外均满足堆的定义 
 * 调整H[s],使其成为大顶堆.即将对第s个结点为根的子树筛选,  
 * 
 * @param H是待调整的堆数组 
 * @param s是待调整的数组元素的位置 
 * @param length是数组的长度 
 * 
 */  
void HeapAdjust(int H[],int s, int length)  
{  
    int tmp  = H[s];  
    int child = 2*s+1; //左孩子结点的位置。(i+1 为当前调整结点的右孩子结点的位置)  
    while (child < length) {  
        if(child+1 <length && H[child]<H[child+1]) { // 如果右孩子大于左孩子(找到比当前待调整结点大的孩子结点)  
            ++child ;  
        }  
        if(H[s]<H[child]) {  // 如果较大的子结点大于父结点  
            H[s] = H[child]; // 那么把较大的子结点往上移动,替换它的父结点  
            s = child;       // 重新设置s ,即待调整的下一个结点的位置  
            child = 2*s+1;  
        }  else {            // 如果当前待调整结点大于它的左右孩子,则不需要调整,直接退出  
             break;  
        }  
        H[s] = tmp;         // 当前待调整的结点放到比其大的孩子结点位置上  
    }  
    print(H,length);  
}  
  
  
/** 
 * 初始堆进行调整 
 * 将H[0..length-1]建成堆 
 * 调整完之后第一个元素是序列的最小的元素 
 */  
void BuildingHeap(int H[], int length)  
{   
    //最后一个有孩子的节点的位置 i=  (length -1) / 2  
    for (int i = (length -1) / 2 ; i >= 0; --i)  
        HeapAdjust(H,i,length);  
}  
/** 
 * 堆排序算法 
 */  
void HeapSort(int H[],int length)  
{  
    //初始堆  
    BuildingHeap(H, length);  
    //从最后一个元素开始对序列进行调整  
    for (int i = length - 1; i > 0; --i)  
    {  
        //交换堆顶元素H[0]和堆中最后一个元素  
        int temp = H[i]; H[i] = H[0]; H[0] = temp;  
        //每次交换堆顶元素和堆中最后一个元素之后,都要对堆进行调整  
        HeapAdjust(H,0,i);  
  }  
}   
  
int main(){  
    int H[10] = {3,1,5,7,2,4,9,6,10,8};  
    cout<<"初始值:";  
    print(H,10);  
    HeapSort(H,10);  
    //selectSort(a, 8);  
    cout<<"结果:";  
    print(H,10);  
  
}  

 JAVA代码:

public class HeapSort {
 
	public static void main(String[] args) {
		int arr[] = {6,5,7,2,9,10,3};
		sort(arr);  //对堆进行排序
		for (int a : arr)
			System.out.print(a + " ");
	}
 
	public static void swap(int arr[], int a, int b) {
		arr[a] = arr[a] + arr[b];
		arr[b] = arr[a] - arr[b];
		arr[a] = arr[a] - arr[b];
	}
 
	public static void buildHeap(int arr[], int n) { //在arr的(0,n-1)进行建堆
		for (int j = 0; j < n; j++) {
			for (int i = n - 1; i > j; i--) {	//从堆顶元素至下依次排好。
				if (arr[i / 2] < arr[i])
					swap(arr, i / 2, i);
			}
		}
	}
	
	public static void sort(int arr[]){  //将堆的根
		for(int i = arr.length - 1 ; i > 0 ; i--){
			buildHeap(arr,i); //调整建堆。
			swap(arr,i,0);
		}
	}
}

五、堆排序算法优劣分析:

由于堆排序是由两部分(堆调整 + 堆排序)完成的,所以时间复杂度也应该是两部分之和。

有序堆的构造:等价于对非叶子节点从下至上的下沉操作

  1. 假设堆高h为一整数,堆有n个节点,那么h = log2n
  2. 该堆有2h个叶子节点,由于叶子节点是最底层,所以无需下沉
  3. 倒数第二层的节点有2h-1个,该层节点下沉到叶子节点至多需要比较2次(兄弟节点的比较,父节点与较大的兄弟节点的比较),交换1次(如果父节点小于子节点则交换)
  4. 倒数第三层有2h-2个非叶子节点,他们下沉到倒数第二层之后还要继续下沉到叶子节点(倒数第三层的节点可能比叶子节点还小,所以还要下沉到叶子节点),所以对倒数第二层的节点其下沉至多需要比较4次,交换2次。
  5. 以此类推,倒数第n层的下沉所需操作数 = 该层节点数 * 该层节点下沉所需的操作数 ,前者为2h-n+1,后者为3(n-1)。
  6. 所以根节点作为倒数第h+1层,它的下沉所需操作数就为2h-(h+1)+1 * 3 (h+1-1) = 20 * 3h。
  7. 将各层的下沉所需操作数从上至下相加,就是构造整个有序堆的所需操作数:3 ( 20h + 21(h-1) + 22(h-2) +…2h-1 ),设该式为a,则2a - a = 3 ( 21 + 22 +…2h - h ),根据等比序列的求和公式,我们可以知道a = 3 ( 2h+1 - 2 - h),代入h,得a = 3(2n - 2 - log2n) ≈ 6n。
  8. 综上,建有序堆的时间复杂度是O(n)。
  9. 不断交换堆顶元素和堆底元素
  10. 共进行n-1次交换,所需操作数为n-1
  11. 并且每次交换都会导致根节点不再是堆中的最大元素,所以需要对新的根节点进行下沉操作,所需操作数为3(n-1)h,即3(n-1)log2n
  12. 综上,该过程所需操作数为n-1 + 3(n-1)log2n = (n-1)(3log2n + 1),时间复杂度即为O(nlogn)
  13. 将O(nlogn),O(n)两个时间复杂度相加,就是堆排序的时间复杂度O(nlogn)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值