数据结构--堆的实现-大根堆/小根堆/堆排序/堆排序稳定性证明/TOP-K

       前言 

        逆水行舟,不进则退!!!     


 

       目录

       认识堆

       堆的创建

        1,向下调整的方法建立堆

        2,以向下调整的方式建立小根堆

        3,向上调整的方式建堆

       堆的插入

       堆的删除       

       堆排序

         堆排序稳定性证明

       TOP-K问题

       实现堆操作的完整代码


       认识堆

        堆其实是一棵完全二叉树,完全二叉树是一种特殊的二叉树,除了最后一层外,每一层都被完全填满,最后一层从左到右填充。       

对于完全二叉树(根节点下标为0)中任意一个下标为 i 的结点,它的左孩子结点下标为2i+1,右孩子结点下标为2i+2, 父节点下标为(i-1) / 2(向下取整)。 

        堆也是存储在数组中,假设 i 为结点在数组中的下标,则有:

                如果 i 为0,则 i 表示的节点为根节点,否则 i 节点的双亲节点为 (i - 1)/2
                如果2 * i + 1 小于节点个数,则节点 i 的左孩子下标为2 * i + 1,否则没有左孩子
                如果2 * i + 2 小于节点个数,则节点 i 的右孩子下标为2 * i + 2,否则没有右孩子


 


       堆的创建

        堆有大根堆和小根堆,大根堆的每个结点的值都大于或等于其子节点的值,而小根堆的每个结点的值都小于或等与其子节点的值。 

        在大根堆中,根节点的值最大,因此也称为最大堆。在小根堆中,根节点的值最小,因此也称为最小堆。  大根堆和小根堆常用于堆排序、优先队列等算法中。

        1,向下调整的方法建立堆

        以建大根堆为例,对某个根节点进行一次向下调整的过程为:从一棵完全二叉树的子树的根节点开始,找出两个孩子结点的最大值,然后与根节点进行比较:如果孩子节点大于其父节点的值,则交换孩子结点与父结点的值,保证根节点的值大于孩子结点的值,然后将被交换的孩子结点作为父结点,检查其的子树是否符合大根堆的特点;如果孩子结点小于其父结点的值,则说明以父结点为根结点的这棵树是大根堆。       

··      向下调整的方法是从一个完全二叉树的最后一个非叶子节点开始,依次往回遍历,直到根节点,保证每一棵子树都是大根堆。

        向下调整的方法是从下往上遍历,但对遍历的每一个子根节点都是从上往下调整。这样遍历的好处是只要能确保根节点的值大于孩子节点的最大值,就可以保证该子树是大根堆。

以向下调整的方式建立大根堆

        

        向下调整部分的代码如下:       

 //建大根堆代码
    public int[] createBigHeap() {

 //从最后一个非叶子结点开始遍历,直到根节点结束
        for(int parent = (usedSize-1-1)/2; parent >= 0; parent--) {
            shiftdownBigHeap(parent, usedSize);
        }

        return elem;
    }

    //大根堆向下调整
    private void shiftdownBigHeap(int parent, int len) {
        int child = parent*2+1;
        while(child  < len) {
            if(child+1 < len && elem[child] < elem[child+1]) {  //这里的两个条件不能交换,否则可能产生越界问题,
                child = child+1;
            }
            //上面代码走完后确保child是两个孩子的最大值
            if(elem[child] > elem[parent]) {  //如果孩子结点值大于父亲结点值
                swap(elem, child, parent);    // 交换子节点和父结点
                parent = child;          //这里是继续向下调整,以子节点作为父结点,
                child = parent*2+1;      //继续向下找子节点
            }else {        //如果子节点小于父结点,就直接退出,因为子树已经是调整好的大根堆了
                break;
            }
        }
    }


    private void swap(int[] array, int i, int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }
        2,以向下调整的方式建立小根堆

        小根堆与大根堆的区别就在于根节点的值要小于等于孩子结点的最小值。其他的都一样,也是从最后一个非叶子结点开始遍历,直到根结点结束。

以向下调整的方式建立小根堆

        向下调整建立小根堆的代码如下

 private void swap(int[] array, int i, int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

    //建小根堆代码
    public int[] createSmallgHeap() {


        for(int parent = (usedSize-1-1)/2; parent >= 0; parent--) {
            shiftdownSmallHeap(parent, usedSize);
        }

        return elem;
    }

    //小根堆向下调整
    private void shiftdownSmallHeap(int parent, int len) {
        int child = parent*2+1;
        while(child  < len) {
            if(child+1 < len && elem[child] > elem[child+1]) {  //这里的两个条件不能交换,否则可能产生越界问题,
                child = child+1;
            }
            //上面代码走完后确保child是两个孩子的最大值
            if(elem[child] < elem[parent]) {
                swap(elem, child, parent);
                parent = child;
                child = parent*2+1;
            }else {
                break;
            }
        }
    }

               

向下调整的方式建堆的时间复杂度:O(N) = N; 

        向下调整时间复杂度的推导:

        

        3,向上调整的方式建堆

        向上调整一般用于堆的插入,虽说也可以用来建堆,但是是以插入的方式进行,时间复杂度较高,一般情况下我们是不使用这种方式建堆的。不过这里还是分析一下。

        向上调整的方式建堆,就是将数组中的每个元素,依次插入到堆中,每插入一个元素,进行一次向上调整。以建大根堆为例

向上调整建立大根堆

        向上调整建立大根堆的代码如下:

 public void shiftUpSetHeap() {
        int[] array = new int[10];
        int usedsize = 0;
        for(int i = 0; i < elem.length; i++) {
            array[i] = elem[i];
            shiftUp(i,array);
            usedsize++;
        }
    }
    
    //向上调整
    private void shiftUp(int child, int[] elem) {
        int parent = (child-1)/2;
        while(child > 0) {
            if(elem[child] > elem[parent]) {
                swap(elem, child, parent);
                child = parent;
                parent = (child -1 )/2;
            }else {
                break;
            }
        }
    }

向上调整的时间复杂度为:O(N) =  N*logN;

         向上调整时间复杂度的推导:


       堆的插入

        堆的插入已经很清晰明了了,上述的向上调整建堆就是用插入的方式进行建堆的。将插入的元素放到最后,对插入的元素进行向上调整。与建堆不同的是,多了一个判断的操作,判断数组是否满了,满了的话就二倍扩容。

    //向上调整
    private void shiftUp(int child, int[] elem) {
        int parent = (child-1)/2;
        while(child > 0) {
            if(elem[child] > elem[parent]) {
                swap(elem, child, parent);
                child = parent;
                parent = (child -1 )/2;
            }else {
                break;
            }
        }
    }
    public void push (int val) {
        if(isFull()){
            elem = Arrays.copyOf(elem, 2*elem.length);
        }
        elem[usedSize] = val;
        //向上调整
        shiftUp(usedSize,elem);
        usedSize++;
    }


    //判满
    public boolean isFull() {
        return usedSize == elem.length;
    }



       堆的删除       

        堆的删除与堆的插入由异曲同工之妙,堆的每次删除都是删除堆的根结点,也就是将堆的根结点与最后一个叶子结点进行交换,然后对新的根结点进行向下调整,最后将usedSize--;就删除完成了。要删除的结点本质上还在数组中,但是usedSize已将将其标记为不可访问。同样的是,堆的删除中也多了一个判断处理,判断数组是否为空。

其实计算机中的绝大多数的删除操作都是如此,并不是将要删除的元素从内存中剔除,而是标记为可使用的新空间,当新的数据将新空间的上一个值覆盖后,那个数据才算从计算机中剔除了。所以删除操作是可以进行恢复的,只是恢复的是那些还没有被覆盖的空间中的值。

 

    //交换
    private void swap(int[] array, int i, int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

    //判空
    public boolean isEmpty() {
        return usedSize == 0;
    }

    //堆的删除
    public int pop() {
        if(isEmpty()) {
            return -1;
        }
        int tmp = elem[0];
        swap(elem,0, usedSize-1);
        //shiftdownSmallHeap(0, usedSize-1);
        shiftdownBigHeap(0, usedSize-1);
        usedSize--;
        return tmp;
    }
    

       堆排序

        实现堆排序,最重要的问题就是,无论大根堆还是小根堆都只是层次间有序,具体到层内就是无序的。也就是说根结点的左右孩子的顺序是不确定的。所以我们应该如何使整个堆变得有序?

        其实这个问题的思路在堆的删除中已经给出了,将堆顶元素与最后一个元素进行交换,然后对新的堆顶元素进行向下调整。因为堆顶是最大或最小的元素,此时最后一个元素已经有序。如此反复,就可以将整个堆进行排序。堆排序的代码很简单,主要就是看思路能不能想通。

        如果要实现从小到大排序,那么我们要先建好大根堆。

//已经建好大根堆
//堆排序
    public void heapSort() {
        int end = usedSize-1;
        while(end > 0) {
            swap(elem,0,end);
            shiftdownBigHeap(0,end);
            end--;
        }
    }

       

计算:建堆的时间复杂度 O(N) = N    加上  堆排序的时间复杂度O(N) = N*logN;

堆排序的时间复杂度:O(N) =  N + N*logN  约等于  O(N) = N*logN;

堆排序的空间复杂度:O(1);

        又有一个新的问题,堆排序是否稳定?我们用一组例子来证明。

         堆排序稳定性证明

结论: 

堆排序的时间复杂度:O(N) =  N + N*logN  约等于  O(N) = N*logN;

堆排序的空间复杂度:O(1);

                      稳定性:堆排序不稳定。


       TOP-K问题

问题:在大量数据中,取出其最大或最小的前K个数 

类似问题:高考前50名,世界500强,富豪榜等问题。

        对于TOP-K问题,能想到的最简单直接的解决方式就是排序,但是:如果数据量过大,排序就不现实了(因为可能内存无法加载全部数据)。此时,就可以考虑用堆来解决。

求:在10000个数据中找前K个最小的元素

解:

        1,取前K个元素,建立大小为K的大根堆,此时堆顶元素为前K个元素中最大的;

        2,从第K+1个元素开始,依次与堆顶元素进行比较,如果某个元素比堆顶元素小,那么先删除堆顶元素,再将该元素插入堆中,然后再进行向上调整,确保堆为大根堆。如此往复,遍历完10000-K个 元素后,此时的堆,就是前K个最小的元素。

理解:就好似用漏斗过滤一般。当找前K个最大的元素时,就取前K个元素建立小根堆,原理同上。


       实现堆操作的完整代码

         

import java.util.Arrays;

public class TestHeap {
    public int[] elem;
    public int usedSize;

    public TestHeap(){
        this.elem = new int[10];
    }

    public void initElem(int[] array) {

        for (int i = 0; i < array.length; i++) {
            elem[i] = array[i];
            usedSize++;
        }
    }

    //建大根堆代码
    public int[] createBigHeap() {


        for(int parent = (usedSize-1-1)/2; parent >= 0; parent--) {
            shiftdownBigHeap(parent, usedSize);
        }

        return elem;
    }

    //大根堆向下调整
    private void shiftdownBigHeap(int parent, int len) {
        int child = parent*2+1;
        while(child  < len) {
            if(child+1 < len && elem[child] < elem[child+1]) {  //这里的两个条件不能交换,否则可能产生越界问题,
                child = child+1;
            }
            //上面代码走完后确保child是两个孩子的最大值
            if(elem[child] > elem[parent]) {
                swap(elem, child, parent);
                parent = child;
                child = parent*2+1;
            }else {
                break;
            }
        }
    }

    public void showHeap() {
        for (int i = 0; i < usedSize; i++) {

            System.out.print(elem[i]+" ");
        }
    }

    private void swap(int[] array, int i, int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

    //建小根堆代码
    public int[] createSmallgHeap() {


        for(int parent = (usedSize-1-1)/2; parent >= 0; parent--) {
            shiftdownSmallHeap(parent, usedSize);
        }

        return elem;
    }

    //小根堆向下调整
    private void shiftdownSmallHeap(int parent, int len) {
        int child = parent*2+1;
        while(child  < len) {
            if(child+1 < len && elem[child] > elem[child+1]) {  //这里的两个条件不能交换,否则可能产生越界问题,
                child = child+1;
            }
            //上面代码走完后确保child是两个孩子的最大值
            if(elem[child] < elem[parent]) {
                swap(elem, child, parent);
                parent = child;
                child = parent*2+1;
            }else {
                break;
            }
        }
    }

    /*public void shiftUpSetHeap() {
        int[] array = new int[10];
        int usedsize = 0;
        for(int i = 0; i < elem.length; i++) {
            array[i] = elem[i];
            shiftUp(i,array);
            usedsize++;
        }
    }*/

    //向上调整
    private void shiftUp(int child, int[] elem) {
        int parent = (child-1)/2;
        while(child > 0) {
            if(elem[child] > elem[parent]) {
                swap(elem, child, parent);
                child = parent;
                parent = (child -1 )/2;
            }else {
                break;
            }
        }
    }
    public void push (int val) {
        if(isFull()){
            elem = Arrays.copyOf(elem, 2*elem.length);
        }
        elem[usedSize] = val;
        //向上调整
        shiftUp(usedSize,elem);
        usedSize++;
    }


    //判满
    public boolean isFull() {
        return usedSize == elem.length;
    }

    //判空
    public boolean isEmpty() {
        return usedSize == 0;
    }

    //堆的删除
    public int pop() {
        if(isEmpty()) {
            return -1;
        }
        int tmp = elem[0];
        swap(elem,0, usedSize-1);
        //shiftdownSmallHeap(0, usedSize-1);
        shiftdownBigHeap(0, usedSize-1);
        usedSize--;
        return tmp;
    }

}


        我是专注学习的章鱼哥~

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
1. 树状大根堆 树状大根堆是一种二叉树,满足以下性质: 1. 每个节点的值都大于等于其子节点的值。 2. 树的最后一层节点都靠左排列。 在Python中,我们可以使用列表来表示二叉树,其中第i个元素的左子节点为2i,右子节点为2i+1,父节点为i//2。 以下是创建树状大根堆的代码: ```python class MaxHeap: def __init__(self, arr=None): self.heap = [0] if arr: self.heap.extend(arr) self._build_heap() def _build_heap(self): n = len(self.heap) - 1 for i in range(n // 2, 0, -1): self._heapify(i) def _heapify(self, i): n = len(self.heap) - 1 largest = i left = 2 * i right = 2 * i + 1 if left <= n and self.heap[left] > self.heap[largest]: largest = left if right <= n and self.heap[right] > self.heap[largest]: largest = right if largest != i: self.heap[i], self.heap[largest] = self.heap[largest], self.heap[i] self._heapify(largest) def push(self, val): self.heap.append(val) i = len(self.heap) - 1 while i > 1 and self.heap[i] > self.heap[i // 2]: self.heap[i], self.heap[i // 2] = self.heap[i // 2], self.heap[i] i //= 2 def pop(self): if len(self.heap) == 1: return None if len(self.heap) == 2: return self.heap.pop() root = self.heap[1] self.heap[1] = self.heap.pop() self._heapify(1) return root ``` 2. 堆排序 堆排序是一种排序算法,基于树状大根堆实现。其思路是先将数组构建成树状大根堆,然后将顶元素与最后一个元素交换,再将前面的元素重新构建成树状大根堆,重复此过程直到数组有序。 以下是堆排序的代码: ```python def heap_sort(arr): n = len(arr) heap = [0] + arr for i in range(n // 2, 0, -1): _heapify(heap, i, n) for i in range(n, 0, -1): heap[1], heap[i] = heap[i], heap[1] _heapify(heap, 1, i - 1) return heap[1:] def _heapify(heap, i, n): largest = i left = 2 * i right = 2 * i + 1 if left <= n and heap[left] > heap[largest]: largest = left if right <= n and heap[right] > heap[largest]: largest = right if largest != i: heap[i], heap[largest] = heap[largest], heap[i] _heapify(heap, largest, n) ``` 3. 取前k个值 在树状大根堆中,顶元素是最大的元素,可以通过不断取出顶元素来获得最大的k个元素。 以下是取前k个值的代码: ```python def top_k(arr, k): n = len(arr) heap = [0] + arr[:k] for i in range(k // 2, 0, -1): _heapify(heap, i, k) for i in range(k, n): if arr[i] > heap[1]: heap[1] = arr[i] _heapify(heap, 1, k) return heap[1:] ``` 以上是树状大根堆堆排序和取前k个值的Python实现
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值