堆和堆排序:为什么堆排序没有快排快?

堆和堆排序:为什么堆排序没有快排快?

堆(HEAP)是一种原地的、时间复杂度为O(nlogn)的排序算法

在实际的软件开发中,快排的性能为什么要比堆排序好?

如何理解堆?

满足这两点就是一个堆

  • 堆是一个完全二叉树
  • 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值

对于每个节点的值都大于等于子树中每个节点值的堆,叫做“大顶堆”,反之则是“小顶堆”

如何实现一个堆?

堆都支持哪些操作?如果存储一个堆?

完全二叉树适合用数组来存储,只需要存储左右子节点的指针,单纯的通过数组的下标,就可以找到一个节点的左右子节点和父节点

数组中下标为i的节点的左子节点,就是下标为2i的节点,右子节点就是下标为2i+1的节点,父节点就是下标为i/2的节点

堆的核心操作是往堆中插入一个元素和删除堆顶元素(一般是大顶堆)

1.往堆中插入一个元素

插入一个元素后,需要继续满足堆的两个特性,但是如果把新插入的元素都放到堆的最后,就不符合堆的特性了(比如新插入的元素比较大,就不符合第二条特性了),所以就需要进行调整,让其重新满足特性,这个过程叫堆化

堆化有两种,从下往上和从上往下,先讲从下往上的堆化方法:顺着节点所在的路径,向上或者向下,对比,然后交换

public class Heap{
  private int[] a ;  //数组,从下标1开始存储数据
  private int n;     //堆可以存储的最大数据个数
  private int count;// 堆中已经存储的数据个数
  
  public Heap(int capacity){
    a = new int[capacity + 1];
    n = capacity;
    count = 0;
  }
  
  public void insert(int data){
    if(count >= n )  return;//堆满了
    ++count;
    a[count] = data;
    int i = count;
    while(i/2 > 0 && a[i] > a[i/2]){    //自下往上堆化
      swap(a,i,i/2);   //swap()函数作用:交换小标为i 和i/2的两个元素
      i=i/2;
    }
  }
}
2.删除堆顶元素

把最后一个节点放到堆顶,然后利用同样的父子节点对比方法,对于不满足父子节点大小关系的,互换两个节点,直到父子节点之间满足大小关系为止,这是从上往下的堆化方法。

我们移除的是数组中最后一个元素,而在堆化的过程中,都是交换操作,不会出现“空洞”

public void removeMax(){
  if(count == 0) return -1;   //堆中没有数据
  a[1] = a[count];   //把最后一个元素放到堆顶
  --count;   //堆的规模-1
  heapify(a,count,1);   //堆化完成,count是长度
}

public void heapify(int[] a ,int n ,int i){   //自上往下堆化
  while(true){
    int maxPos = i ;
    if(i*2 <= n && a[i] < a[i*2]) maxPos = i*2+1;
    if(maxPos == i) break;
    swap(a ,i ,maxPos);
    i = maxPos;
  }
}

一个包含n个节点的完全二叉树,树的高度不超过log2n,堆化的过程是顺着节点所在路径比较交换的,所以堆化的时间复杂度跟树的高度成正比,也就是O(log2n),往里面插入一个元素和删除堆顶元素的时间复杂度都是O(logn)

如何实现堆排序?

排序方法的时间复杂度是O(nlogn),堆排序的过程分解成两个大步骤:建堆和排序

1.建堆

将数组原地建个堆,就是在原数组上操作

一:在堆中插入一个元素的思路,假设起初堆中只有1个数据,就是下标为1的数据,调用插入操作,将下标为2到n的数据依次插入到堆中,这样就将n个数据的数据组织成了堆

二:叶子节点往下对话只能自己跟自己比较,所以我们直接从第一个非叶子节点开始,依次堆化即可

public static void buildHeap(int[] a ,int n){
	for (int i = n/2 ; i >= 1 ;--i){
		heapify(a,n,i);
	}
}

private static void heapify(int[] a ,int n,int i){
	while(true){
		int maxPos = i;
		if(i*2 <= n && a[i] < a[i*2]) maxPos = i*2;
		if(i*2+1 <= n && a[maxPos] < a[i*2+1]) maxPos = i*2+1;
		if(maxPos == i) break;
		swap(a,i,maxPos);
		i = maxPos;
	}
}

从n/2开始到1的数据进行堆化,下表示n/2+1到n的节点是叶子节点,不需要堆化

实际上,堆排序的建堆过程的时间复杂度是O(n)

叶子节点不用堆化,所以需要堆化的节点从倒数第二层开始,每个节点堆化的过程中,需要比较和交换的节点个数,跟这个节点的高度k成正比

2.排序

建堆之后,数组中的数据是按照大顶堆的特性来组织的

数组中的第一个元素就是堆顶,也就是最大元素,跟最后一个元素交换,那最大元素就放到下标为n的位置,当堆顶元素移除之后,把下标为n的元素放到堆顶,再通过堆化,将剩下n-1个元素重新构建,堆化之后,再取堆顶元素,放到n-1的位置,一直重复,直到剩下标为1 的一个元素

//n表示数据的个数,数组a中的数据从下标1到n的位置
public static void sort(int[] a ,int n){
	buildHeap(a,n);
	int k = n;
	while (k > 1){
		swap(a,1,k);
		--k;
		heapify(a,k,1);
	}
}

堆排序不是稳定的排序算法,因为在排序过程中,存在将堆的最后一个节点跟堆顶节点互换的操作,所以就有可能改变值相同数据的原始相对顺序

在实际开发中,为什么快排比堆排序性能好?

一:堆排序数据访问的方式没有快排友好

快排的数据是顺序访问的,堆排序数据是跳着访问的

二:对于同样的数据,排序过程中,堆排序算法的数据交换次数多于快排

发布了76 篇原创文章 · 获赞 9 · 访问量 9192

猜你喜欢

转载自blog.csdn.net/ywangjiyl/article/details/104400691
今日推荐