算法通关村第十四关——堆结构(青铜)

算法通关村第十四关——堆结构(青铜)

1 堆的概念与特征

堆是将一组数据按照完全二叉树的存储顺序,将数据存储在一个一维数组中的结构。堆有

两种结构,一种称为大顶堆,一种称为小顶堆,如下图。

小顶堆:任意节点的值均小于等于它的左右孩子,并且最小的值位于堆顶,即根节点

处。

大顶堆:任意节点的值均大于等于它的左右孩子,并且最大的值位于堆顶,即根节点

处。 有些地方也叫大根堆、小根堆,或者最大堆、最小堆都一个意思。大和小的特征

等都是类似的,只是比较的时候是按照大还是小来定,我们本章在原理方面的介绍就

按照最大堆来进行,后面的题目再根据情况来定。

image-20230827115015819

既然是将一组数据按照树的结构存储在一维数组中,而且还是完全二叉树,那么父子之间关系的建立就很重要了。

假设一个节点的下标为i。 1、当i = 0时,为根节点。 2、当i>=1时,父节点为(i - 1)/2。

有个概念需要注意一下,我们在做题时经常会看到有些地方叫堆,有些地方叫优先级队列,两者到底啥关系呢?

**优先队列:**说到底还是一种队列,他的工作就是poll()/peek()出队列中最大/最小的那个元素,所以叫带有优先级的队列。能够实现优先功能的策略不一定只有堆,例如二项堆、平衡树、线段树、C++里会用二进制分组的vector来实现一个优先队列。

**堆:**堆是一个很大的概念 他并不一定是完全二叉树。我们之前用完全二叉树是因为这个很容易被数组储存,但是除了这种二叉堆之外,我们还有二项堆、斐波那契堆、这种堆就不属于二叉树。

所以说,优先队列和堆不是一个同level的概念 ,但是java的PriorityQueue就是堆实现的,因此在java领域可以认为堆就是优先级队列,优先级队列就是堆,换做其他场景则不行。

2 堆的构造过程

使用数组构建堆时,就是先按照层次将所有元素依次填入二叉树中,使其成为二叉树,然后再不断调整,最终使其符合堆结构。

使用数组构建堆的方法通常称为"自底向上"或者"自顶向下"的调整。

首先,我们将元素按照层次遍历的顺序依次填入一个数组中。然后,从数组的第一个非叶子节点开始,逐步向上调整每个节点,使其满足堆的性质。

具体来说,对于最小堆而言,需要保证父节点的值小于等于其子节点的值。我们可以通过比较父节点和其两个子节点的值,找到其中最小的值,并交换位置。这样,我们逐步向上调整所有的非叶子节点,直至根节点,就可以得到一个符合堆结构的二叉树。

以下是一个示例,展示了如何使用数组构建堆的过程:

原始数组:[16, 7 ,3 ,20 ,17 ,8 ]

构建堆的过程:

首先,我们需要了解大顶堆的定义和特性。大顶堆是一种完全二叉树,满足以下条件:

  • 父节点的值大于或等于其左右子节点的值

  • 根节点的值是最大的

下面是将给定数组变成大顶堆的详细步骤:

  1. 首先根据该数组元素构建一个完全二叉树,得到

img

  1. 需要构造初始堆,则从最后一个非叶节点开始调整,也就是int i = size / 2 - 1 = (size - 2) / 2,调整过程如下:

img

  1. 20和16交换后导致16不满足堆的性质,因此需重新调整

img

这样就得到了初始堆。

即每次调整都是从父节点、左孩子节点、右孩子节点三者中选择最大者跟父节点进行交换(交换之后可能造成被交换的孩子节点不满足堆的性质,因此每次交换之后要重新对被交换的孩子节点进行调整)。有了初始堆之后就可以进行排序了。

代码如下:

以下是将一个数组转化为大顶堆的构造过程的代码:

public class HeapifyExample {
    public static void main(String[] args) {
        int[] arr = {16, 7, 3, 20, 17, 8};
        heapify(arr);
        
        System.out.println("转化后的大顶堆数组:");
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }

    public static void heapify(int[] arr) {
        int n = arr.length;
        
        // 从最后一个非叶子节点开始进行堆化
        for (int i = n / 2 - 1; i >= 0; i--) {
            siftDown(arr, i, n);
        }
    }

    public static void siftDown(int[] arr, int index, int length) {
        int parent = index;
        int child = 2 * parent + 1; // 左孩子节点

        while (child < length) {
            // 如果右孩子存在且右孩子比左孩子大,则选取右孩子作为交换对象
            if (child + 1 < length && arr[child] < arr[child + 1]) {
                child++;
            }
            
            // 如果父节点小于等于交换对象,则交换两者的值,并继续向下判断
            if (arr[parent] <= arr[child]) {
                swap(arr, parent, child);
                parent = child;
                child = 2 * parent + 1;
            } else {
                break;
            }
        }
    }

    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

该代码使用了经典的堆化算法,从最后一个非叶子节点开始逐个向上进行调整,使得每个父节点都大于或等于其子节点。最终得到的数组就是一个大顶堆。

3 插入操作

从上面可以看到根节点和其左右子节点是堆里的老大,老二和老三,其他结点则没有太明显的规律,那如果要插入一个新元素,该怎么做呢?直接说规则,将元素插入到保持其为完全二叉树的最后一个位置,然后顺着这条支路一直向上调整,每前进一层就要保证其子树都满足堆否则就去处理子树,直到完全满足要求。

当我们将新元素15插入到大顶堆的过程中,每一步找到新元素的位置都可以用图示来表示。

初始状态的大顶堆数组为:[16, 7, 3, 20, 17, 8]

第一步:
将新元素15添加到数组的末尾,此时数组变为:[16, 7, 3, 20, 17, 8, 15]
此时需要比较15与其父节点16的大小,并根据大小进行交换。

          20
         /  \
        17   8
       /  \
     16    7
    /  \ 
   3   15   

第二步:
由于15小于其父节点16,所以不需要进行交换。此时已满足大顶堆的条件。

          20
         /  \
        17   8
       /  \
     16    7
    /  \ 
   3   15   

最终插入后的大顶堆为:

          20
         /  \
        17   8
       /  \
     16    7
    /  \ 
   15   3   

以下是向大顶堆插入新元素并使其保持合格的二叉树结构的代码:

public class InsertHeapExample {
    public static void main(String[] args) {
        int[] arr = {16, 7, 3, 20, 17, 8};
        int newElement = 15;
        
        System.out.println("插入前的大顶堆数组:");
        for (int num : arr) {
            System.out.print(num + " ");
        }
        
        insertToHeap(arr, newElement);
        
        System.out.println("\n插入后的大顶堆数组:");
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }

    public static void insertToHeap(int[] arr, int newElement) {
        // 将新元素添加到数组最后
        int n = arr.length;
        arr[n] = newElement;

        // 向上调整,直到满足大顶堆的条件
        int i = n;
        while (i > 0 && arr[i] > arr[(i - 1) / 2]) {
            swap(arr, i, (i - 1) / 2);
            i = (i - 1) / 2;
        }
    }

    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

在这段代码中,我们首先将新元素添加到数组的末尾。然后,我们使用"向上调整"方法来确保新元素满足大顶堆的条件。"向上调整"方法比较新元素与其父节点的大小,如果新元素大于父节点,则交换它们的位置,并继续向上比较,直到满足大顶堆的条件。

下面是插入新元素后的大顶堆的示意图:

          20
         /  \
        17   8
       /  \
     16    7
    /  \ 
   15   3

4 删除操作

堆本身比较特殊,一般对堆中的数据进行操作都是针对堆顶的元素,即每次都从堆中获得最大值或最小值,其他的不关心,所以我们删除的时候,也是删除堆顶。如果直接删掉堆顶,整个结构被破坏了,群龙无首就不易管理了。所以实际策略是:

  1. 先将堆中最后一个元素(假如为A)和堆顶元素进行替换,然后删除堆中最后一个元素。
  2. 之后再从根开始逐步与左右比较,谁更大谁上位。
  3. 然后A再继续与子树比较,如果有更大的继续交换,直到自己所在的子树也满足大顶堆。

上面的过程可以理解为皇上突然驾崩了,这时候先找个顾命大臣维持局面,大臣先看左右两个皇子谁更强谁就是老大。然后大臣自己再逐步隐退,直到找到属于自己的位置。

当我们从大顶堆中删除元素的过程中,每一步都可以用图示来表示。

初始状态的大顶堆数组为:[20, 17, 16, 15, 8, 3]

第一步:
交换堆顶元素20与最后一个元素3,并将最后一个元素移除。此时数组变为:[3, 17, 16, 15, 8]
需要对交换后的堆顶元素3进行向下调整,找到其在子树中的合适位置。

          3
         /  \
        17   16
       /  
      15    
     /
    8

第二步:
由于3小于其左子节点17和右子节点16,所以需要将3与较大的子节点17进行交换。

          17
         /  \
        3    16
       /  
      15    
     /
    8

第三步:
由于3小于其左子节点15,所以需要将3与左子节点15进行交换。

          17
         /  \
        15   16
       /  
      3    
     /
    8

第四步:
由于3小于其右子节点16,所以需要将3与右子节点16进行交换。

          17
         /  \
        15   3
       /  
      16    
     /
    8

最终删除后的大顶堆为:

          17
         /  \
        15   3
       /  
      16    
     /
    8

以下是从大顶堆删除元素并使其保持合格的二叉树结构的代码:

public class DeleteHeapExample {
    public static void main(String[] args) {
        int[] arr = {20, 17, 16, 15, 8, 3};
        
        System.out.println("删除前的大顶堆数组:");
        for (int num : arr) {
            System.out.print(num + " ");
        }
        
        deleteFromHeap(arr);
        
        System.out.println("\n删除后的大顶堆数组:");
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }

    public static void deleteFromHeap(int[] arr) {
        int n = arr.length;
        
        // 交换堆顶元素与最后一个元素,并将最后一个元素移除
        swap(arr, 0, n - 1);
        
        // 对交换后的堆顶元素进行向下调整,直到满足大顶堆的条件
        siftDown(arr, 0, n - 1);
    }

    public static void siftDown(int[] arr, int index, int length) {
        int parent = index;
        int child = 2 * parent + 1; // 左孩子节点

        while (child < length) {
            // 如果右孩子存在且右孩子比左孩子大,则选取右孩子作为交换对象
            if (child + 1 < length && arr[child] < arr[child + 1]) {
                child++;
            }
            
            // 如果父节点小于等于交换对象,则交换两者的值,并继续向下判断
            if (arr[parent] <= arr[child]) {
                swap(arr, parent, child);
                parent = child;
                child = 2 * parent + 1;
            } else {
                break;
            }
        }
    }

    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

在这段代码中,我们首先将堆顶元素与最后一个元素交换,并将最后一个元素从数组中移除。然后,我们对交换后的堆顶元素进行向下调整,直到满足大顶堆的条件。

删除元素后的大顶堆示意图如下:

          17
         /  \
        15   16
       /  \
      8    3
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值