最难以理解的排序算法 - 堆排序(超详解)

堆排序基本介绍

  1. 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。
  2. 要理解堆排序,必须先要理解堆这种数据结构

    堆是具有以下性质的完全二叉树:

    1. 每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆, 注意 : 没有要求结点的左孩子的值和右孩子的值的大小关系。
    2. 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
  3. 大顶堆示意图:
    在这里插入图片描述
    我们对堆中的结点按层进行编号,映射到数组中就是下面这个样子:
    在这里插入图片描述
    大顶堆特点:arr[i] >= arr[2*i+1] && arr[i] >= arr[2*i+2] // i 对应第几个节点,i从0开始编号
    注意:这里需要先理解顺序存储二叉树的知识,可以参考我的文章顺序存储二叉树
  4. 小顶堆示意图:
    5.
    小顶堆:arr[i] <= arr[2*i+1] && arr[i] <= arr[2*i+2] // i 对应第几个节点,i从0开始编号
  5. 一般升序采用大顶堆,降序采用小顶堆

堆排序基本思想

堆排序的基本思想是:
  1. 将待排序序列构造成一个大顶堆
  2. 此时,整个序列的最大值就是堆顶的根节点。
  3. 将其与末尾元素进行交换,此时末尾就为最大值。
  4. 然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。

可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了

堆排序思路和步骤:

步骤一 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)

  1. 假设给定无序序列结构如下
    在这里插入图片描述

  2. 此时我们从数组的最后一个非叶子结点(即下标为arr.length/2-1)开始(叶结点自然不用调整,最后一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从右至左,从下至上进行调整。
    在这里插入图片描述

  3. 找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。
    在这里插入图片描述

  4. 这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。
    在这里插入图片描述

    此时,我们就将一个无序序列构造成了一个大顶堆。

步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。

  1. 将堆顶元素9和末尾元素4进行交换
    在这里插入图片描述

  2. 重新调整结构,使其继续满足堆定义

在这里插入图片描述

  1. 再将堆顶元素8与末尾元素5进行交换,得到第二大元素8
    在这里插入图片描述
  2. 后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
    在这里插入图片描述

堆排序的代码实现

堆排序的代码不是很好理解,代码里面有详细注释,可以仔细阅读代码注释加深对堆排序理解

public class HeapSort {
    public static void main(String[] args) {
		int[] array = new int[80000];
        for (int i = 0; i < array.length; i++) {
            // 随机生成一个0到8000000的随机数
            Random random = new Random();
            int nextInt = random.nextInt(8000000);
            array[i] = nextInt;
        }
        // 排序前时间,h毫秒
        long beforeSortTimeMillis = System.currentTimeMillis();
        heapSort(array);
        // 排序后时间
        long afterSortTimeMillis = System.currentTimeMillis();
        System.out.println("排序80000个数据总共花费时间为:" + (afterSortTimeMillis - beforeSortTimeMillis) + "毫秒");
        
       int[] arr = {4,6,8,5,9,74,1,45,23,46,26,26}; // 调整成大顶堆为 9,6,8,5,4
       heapSort(arr);
       System.out.println(Arrays.toString(arr));
    }

    /**
     * 按堆排序,把数组变成一个升序的数组
     * @param array
     */
    public static void heapSort(int[] array) {

        // 把该数组看成一个顺序存储的二叉树,升序排序,先把该数组调整成一个大顶堆,
        // 调整成大顶堆,先从该顺序二叉树的最后非叶子节点开始调整,从右到左,从下到上
        // 最后一个非叶子节点的 下标为 array.length/2-1
        for (int i = array.length/2-1; i >= 0; i--) {
            adjustHeap(array,i,array.length);
        }

        // 代码走到这该数组已经是一个大顶堆,头结点是最大值,即数组的第一个元素是最大值
        // 把该数组的头节点与末尾交换,然后把除去数组末尾的数继续调整成大顶堆
        for (int n = array.length - 1; n > 0 ; n--) {
            int temp = array[0];
            array[0] = array[n];
            array[n] = temp;

            // 然后把该数组的从下标为0开始,长度为n的数组继续调整成一个大顶堆
            // 即以数组的第一个元素为根节点开始调整,
            // 因为以array[0]根节点的树,该树下面的每一颗子树都已经是大顶堆了
            // 参考到adjustHeap()方法的作用,所以可以直接调用该方法,
            // 把array[0]调整到该树合适的位置,让该树继续是一个大顶堆
            adjustHeap(array,0,n);
        }
    }

    /**
     * 该方法总的作用是把以array[i]为根节点的树的头节点array[i]按大小调整到其合适的位置,从上往下,逐层比较,最后找打array[i]合适的位置
     * 1.该方法把以下标为i的数作为根节点的树调整成一个大顶堆,
     * 2.要满足1,必须先把以arrry[i]为根节点的树下面的每一颗树都必须先调整成一个大顶堆,
     * 3.也就是想要把以array[i]为根节点的数调整成大顶堆,必须要循环调用该方法,
     * 先从该树的最后一个非叶子节点开始递减调整,才能把该树调整成一个大顶堆
     * @param array 看做一个顺序存储的二叉树的数组
     * @param i 以i为根节点数
     * @param length 要调整的数组长度
     */
    public static void adjustHeap(int[] array, int i, int length) {
        // 先保存该根节点
        int temp = array[i];
        // 左子节点 i*2+1   右子节点 i*2+2

        // 循环遍历,使k指向左子节点,每次循环在指向下一个左子节点
        // 该循环中右2个变量需要理解,i是父节点,k是其子节点,注意i和k的变化,可以理解成指针
        for (int k = i*2+1; k < length; k=k*2+1) {
            // 比较左右节点的大小,如果右节点大于左节点就让k指向右节点
            if (k+1 < length && array[k+1] > array[k]) {
                k++;
            }
            // 在比较初始根节点的值(即保存在temp的值)和array[k](即左右节点中较大的那个值)的大小
            if (array[k] > temp) {
                // 如果大于则把该左右节点较大的值赋值给其父节点
                array[i] = array[k];
                // 让i指向k,即让i移动到左右节点中较大那个节点,然后继续循环下一次
                i=k;
            } else {
                // 代码走到这说明,该左右子节点的较大值不大于该树的原始根节点(即temp,)
                // 也就不用继续比较下去了,头节点已经找到其合适的位置,循环终止退出
                break;
            }
        }
        // 代码执行到这,说明已经找到合适的位置即下标为i的位置,最后把头节点的值放到到他合适的位置
        array[i] = temp;
    }
}

运行结果:

排序80000个数据总共花费时间为:8毫秒
[1, 4, 5, 6, 8, 9, 23, 26, 26, 45, 46, 74]

从结果可以看出堆排序是非常快的,排序80000个数据才用了8毫秒,堆排序的事件复杂度是O(nlogn)

对以上代码再次说明:

  1. 以上代码中要理解方法adjustHeap()方法的作用,该方法并不是把传入的一个数组变成大顶堆,而是结合heapSort()方法中的代码:
    for (int i = array.length/2-1; i >= 0; i--) {
                adjustHeap(array,i,array.length);
            }
    
    当这段for循环结束,才会把数组变成一个大顶堆
  2. 然后heapSort()方法下的代码:
    for (int n = array.length - 1; n > 0 ; n--) {
                int temp = array[0];
                array[0] = array[n];
                array[n] = temp;
    
                // 然后把该数组的从下标为0开始,长度为n的数组继续调整成一个大顶堆
                // 即以数组的第一个元素为根节点开始调整,
                // 因为以array[0]根节点的树,该树下面的每一颗子树都已经是大顶堆了
                // 参考到adjustHeap()方法的作用,所以可以直接调用该方法,
                // 把array[0]调整到该树合适的位置,让该树继续是一个大顶堆
                adjustHeap(array,0,n);
            }
    
    此代码的作用是把数组头元素和尾元素进行交换,然后继续调用adjustHeap()把头元素调整到合适位置,让除去已找到最大值的数组继续是一个大顶堆,在交换如此反复执行,直到数组是一个有序数组。
  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
当谈到排序算法时,有许多经典的排序算法可以使用。以下是十大常见的排序算法及其详细解释,使用Python编写的示例代码: 1. 冒泡排序(Bubble Sort): 冒泡排序是一种简单的交换排序算法。它重复地遍历要排序的列表,比较相邻的元素并交换它们,直到整个列表排序完成。 ```python def bubble_sort(arr): n = len(arr) for i in range(n-1): for j in range(n-i-1): if arr[j] > arr[j+1]: arr[j], arr[j+1] = arr[j+1], arr[j] return arr ``` 2. 选择排序(Selection Sort): 选择排序是一种简单直观的排序算法。它将列表分为已排序和未排序两部分,每次从未排序部分选择最小的元素,并将其放入已排序部分的末尾。 ```python def selection_sort(arr): n = len(arr) for i in range(n-1): min_idx = i for j in range(i+1, n): if arr[j] < arr[min_idx]: min_idx = j arr[i], arr[min_idx] = arr[min_idx], arr[i] return arr ``` 3. 插入排序(Insertion Sort): 插入排序是一种简单且高效的排序算法。它将列表分为已排序和未排序两部分,每次从未排序部分选择一个元素,并将其插入到已排序部分的正确位置。 ```python def insertion_sort(arr): n = len(arr) for i in range(1, n): key = arr[i] j = i - 1 while j >= 0 and arr[j] > key: arr[j + 1] = arr[j] j -= 1 arr[j + 1] = key return arr ``` 4. 快速排序(Quick Sort): 快速排序是一种高效的分治排序算法。它选择一个基准元素,将列表分为小于基准值和大于基准值的两部分,然后递归地对这两部分进行排序。 ```python def quick_sort(arr): if len(arr) <= 1: return arr pivot = arr[len(arr) // 2] left = [x for x in arr if x < pivot] middle = [x for x in arr if x == pivot] right = [x for x in arr if x > pivot] return quick_sort(left) + middle + quick_sort(right) ``` 5. 归并排序(Merge Sort): 归并排序是一种稳定的排序算法,它采用分治策略将列表分为较小的部分,然后递归地合并这些部分以获得排序结果。 ```python def merge_sort(arr): if len(arr) <= 1: return arr mid = len(arr) // 2 left = arr[:mid] right = arr[mid:] left = merge_sort(left) right = merge_sort(right) return merge(left, right) def merge(left, right): result = [] i, j = 0, 0 while i < len(left) and j < len(right): if left[i] < right[j]: result.append(left[i]) i += 1 else: result.append(right[j]) j += 1 result.extend(left[i:]) result.extend(right[j:]) return result ``` 6. 堆排序(Heap Sort): 堆排序利用堆的性质进行排序。它将列表视为完全二叉树,并使用堆数据结构来选择最大或最小元素并将其放在正确的位置上。 ```python def heapify(arr, n, i): largest = i left = 2 * i + 1 right = 2 * i + 2 if left < n and arr[i] < arr[left]: largest = left if right < n and arr[largest] < arr[right]: largest = right if largest != i: arr[i], arr[largest] = arr[largest], arr[i] heapify(arr, n, largest) def heap_sort(arr): n = len(arr) for i in range(n // 2 - 1, -1, -1): heapify(arr, n, i) for i in range(n - 1, 0, -1): arr[i], arr[0] = arr[0], arr[i] heapify(arr, i, 0) return arr ``` 7. 计数排序(Counting Sort): 计数排序是一种稳定的整数排序算法,它使用额外的计数数组来确定每个元素在排序后的位置。它适用于元素范围较小的情况。 ```python def counting_sort(arr): max_val = max(arr) count = [0] * (max_val + 1) for num in arr: count[num] += 1 sorted_arr = [] for i in range(len(count)): sorted_arr.extend([i] * count[i]) return sorted_arr ``` 8. 桶排序(Bucket Sort): 桶排序是一种将元素分配到

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值