堆排序

160 阅读3分钟

堆的基本结构和堆的构造

堆是一个完全二叉树,分为大根堆和小根堆

  • 大根堆:每个节点的值都大于或等于其子节点的值
  • 小根堆:每个节点的值都小于或等于其子节点的值 根据完全二叉树的性质可以得出:
    堆中一个数的左孩子下标 2index+1,右孩子下标 2index+2
    堆中一个数的父节点 (index-1)/2
/* 
 * 堆,一个完全二叉树,分为大根堆和小根堆
 * 堆中一个数的左孩子 2*index+1,右孩子 2*index+2
 * 堆中一个数的父节点 (index-1)/2
 */
// 两数交换
void swap(int *arr, int s, int f)
{
    int tmp = arr[s];
    arr[s] = arr[f];
    arr[f] = tmp;
}

// heapInsert 添加一个新的数进入堆中(以大根堆为例,小根堆类似) O(logN)
void heapInsert(int *arr, int index)  // arr 数组, index 新插入的数的在数组中的位置
{
    // 停止条件,1. index位置的数不大于他的父节点。2. 如果index是头结点(0位置),(index-1)/2也是0,同一位置的数相等,也不会大于
    while (arr[index] > arr[(index-1)/2]) {  // 当前新增加的数和他的父节点比较,如果大于他的父节点,则和他的父节点交换
        swap(arr, index, (index - 1) / 2);
        index = (index - 1) / 2;      // 节点与父节点交换后,再以父节点跟父节点的父节点比较
    }
}

// heapify 堆化。场景:返回并删除堆中某位置的数(依然保持堆结构) O(logN)
void heapify(int *arr, int index, int heapSize) // index 表示要删除的数的位置(已经和最后一个元素交换了,目前要重新堆化) heapSize 当前堆的大小
{
    // 具体方法是将当前最后一个元素与要删除的元素交换,heapSize(堆的大小)-1,表示现在最后一个元素(要删除的元素)不在堆中了
    // 要保持依然是堆,从0位置开始,与左右孩子中较大的比较,将较大值放到0位置,以此类推
    int left = 2 * index + 1;    // 左孩子
    while (left < heapSize) {    // 下方还有孩子(有左孩子就说明有孩子)
        // 两个孩子中的较大值的下标,左孩子 left,右孩子 left+1
        int largest = left + 1 < heapSize && arr[left+1] > arr[left] ? left + 1 : left;

        // 父节点和较大孩子之间谁大
        largest = arr[largest] > arr[index] ? largest : index;

        // 如果index大,则不需要继续往下比较了
        if (largest == index) {
            break;
        }

        // 否则,交换,并且再向下比较
        swap(arr, largest, index);
        index = largest;
        left = 2 * index + 1;
    }
}

// 场景:随便修改堆中的某个值,执行heapInsert和heapify之后,肯定是堆

堆排序

堆排序原理: 把数组中的数进行大根堆化之后(heapSize为堆的大小)可以看出,第一个元素一定是最大的,我们把第一个元素和堆的最后一个元素进行交换,此时最大的元素已经到了他最终的位置,接下来把heapSize--,然后剩余的元素进行大根堆化,再与堆的最后一个元素交换,以此类推。直到heapSize=0时,表示已经排好序。 时间复杂度:O(NlogN)

void heapSort(int *nums, int len)
{
    if (nums == NULL || len < 2) {
        return;
    }

    int heapSize = len;

    // 建立大根堆方法1:
    /*
    for (int i = 0; i < len; i++) {    // O(N)
        heapInsert(nums, i);           // O(logN)
    }
    */
    // 建立大根堆方法2:
    for (int i = len - 1; i >= 0; i--) { // O(N)
        heapify(nums, i, len);           // 从后往前,实际上后面的叶子节点没有叶子节点,不需进行额外操作,效率更高
    }

    // 首位最大的数和最后一个数交换,并且heapSize--
    swap(nums, 0, heapSize - 1);
    heapSize--;

    // 循环处理,直到heapSize=0
    while (heapSize > 0) {             // O(N)
        heapify(nums, 0, heapSize);    // 0位置堆化 O(logN)
        swap(nums, 0, heapSize - 1);   // O(1)
        heapSize--;
    }

}

int main()
{
    int nums[10] = {12, 34, 15, 2, 5, 14, 16, 10, 9, 1};
    printf("Before sort: ");
    for (int i = 0; i < 10; i++) {
        printf(" %d", nums[i]);
    }
    printf("\n");

    heapSort(nums, 10);

    printf("After  sort: ");
    for (int i = 0; i < 10; i++) {
        printf(" %d", nums[i]);
    }
    printf("\n");  
    return 0;
}