数据结构和算法--堆和堆排序

什么是堆

堆是一种特殊的树,他有俩个要求

1 堆是一个完全二叉树

2 堆每一个节点的值,都大于等于(或小于等于)其左右子树节点的值

解释一下:

第一点,完全二叉树就是除了最后一层,其它层的节点数都是满的,最后一层的节点都靠左排列

每个节点都大于等于左右子树节点的叫做大顶堆,每个节点都小于等于左右子树节点的叫做小顶堆
在这里插入图片描述
12 是大顶堆,3是小顶堆,4不是堆

如何实现一个堆

要实现一个堆,首先我们需要知道如何储存一个堆

我们之前讲过,完全二叉树比较适合数组来储存,用数组去储存一个完全二叉树,非常的节省空间单纯的通过下标,就可以找到一个节点的左右子节点,和父节点

在这里插入图片描述
下标为i的节点,左子节点的下标为i2,右子节点的下标i2+1,父节点的下标i/2

向堆中插入一个数据

每当插入一个数据,我们还需要继续满足堆的俩个特性

如果我们把新插入的数据放到堆的最后,那么就不满足堆的俩个特性,那么就需要调整一下,让他重新复合这个特性,这个过程我们叫做堆化

堆化有俩种方式,从下向上和从上向下,我们先说从下向上

在这里插入图片描述
我们让新插入的节点,和父节点进行比较,如果不满足堆的特性,那么久交换一下,一直重复这个过程,直到满足为止
在这里插入图片描述

public class Head {
    //存储堆的数组
    private int[] head;
    //堆存储的最大数据的个数
    private int n;
    //堆中目前数据的个数
    private int count;

    public Head(int capacity) {
        //为什么要+1,因为下标为0的位置不能用
        head = new int[capacity + 1];
        n = capacity;
        count = 0;
    }

    public void insert(int data) {
        //如果堆满了就直接返回
        if (count == n) return;

        ++count;

        //把数据插入到最后
        head[count] = data;

        //自下向上堆化
        int i = count;
        while (i / 2 > 0 && head[i] > head[i / 2]) {
            int temp = head[i / 2];
            head[i / 2] = head[i];
            head[i] = temp;
            i = i / 2;
        }
    }
}

删除堆顶元素

从堆的定义来看,堆顶储存的是堆中最大元素或者最小元素,假如我们删除的是大顶堆,堆顶是最大元素,那该如何正确的删除呢?

我们可以把最后一个元素放到堆顶,然后从堆顶开始,对比父子节点的数据,不满足要求的交换,一直重复,直到满足父子节点的关系为止,这个就是从上向下的堆化

在这里插入图片描述

public void removeMax() {
        if (count == 0) return; // 堆中没有数据
        head[1] = head[count];
        --count;
        heapify(head, count, 1);
    }

    private void heapify(int[] a, int n, int i) {
        // 自上往下堆化
        int maxPos = i;
        while (true) {
            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;
            //交换数据
            int temp = head[i];
            head[i] = head[maxPos];
            head[i] = temp;
            i = maxPos;
        }
    }

一个完全二叉树,高度不大于log2n,而堆化是根据节点的所在路径进行交换的,所以跟树的盖度成正比时间复杂度为O(logn),插入和删除主要逻辑就是堆化,说以时间复杂度为O(logn)

如何实现堆排序

我们可以把堆排序分为俩个操作,建堆排序

建堆
建堆指的是,把一个无需数组建成一个符合堆定义的数组,建堆有俩种思路,一种就是上面的插入,我们可以假设堆中只有一个元素,然后把2到n的元素依次插入,这样就组成了堆

第二种思路和第一种相反,第一种是从前向后处理数据,从下向上堆化,第二种是从数组后向前处理数据,从上向下堆化

因为叶子节点向下堆化只能和自己比较,所以我们需要从第一个非叶子节点的开始堆化
在这里插入图片描述

private 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) {
  int maxPos = i;
  while (true) {
    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;
     //交换数据
      int temp = head[i];
      head[i] = head[maxPos];
      head[i] = temp;
    i = maxPos;
  }
}

第一个非叶子节点是从 n/2 开始的,n/2+1 到 n 都是叶子节点我们不需要堆化,建堆的时间复杂度为O(n)

排序
经过建堆以后,现在已经是大顶堆了,堆顶就是最大元素,我们把堆顶移到下标n的位置,然后剩下的进行堆化,然后重复上方的操作,和删除堆顶元素差不多,直到堆中只有一个元素,就排序完成了
在这里插入图片描述
堆排序的完整代码

// n 表示数据的个数,数组 a 中的数据从下标 1 到 n 的位置。
public static void sort(int[] a, int n) {
  //第一步 建堆
  buildHeap(a, n);
  int k = n;
  //直到剩下一个元素不在循环
  while (k > 1) {
  //交换堆顶和最后一个元素的位置
    int temp = a[1];
    a[1] = a[k];
    a[k] = temp;
    --k;
    //把剩下的元素进行堆化
    heapify(a, k, 1);
  }
}

整个堆排序后只需要极少的临时空间,所以堆排序是原地排序
建堆时间复杂度O(n),排序是将复杂度为O(nlogn),所以堆排序的时间复杂度为O(nlogn)

堆排序不是稳定排序,因为在堆排序过程中,存在把最后一个元素和第一个元素交换的过程,会打乱原有的顺序

上面所有的操作都是假设数组是从下标1出开始的,如果下标是从0开始的,处理的思路是没有任何改变的,唯一的变化就是计算子树坐标的公式

如果下标从0开始,那么节点的下标是i,左子节点下标就是2i+1,右子节点就是2i+2,父节点就是i-1/2。

所有的代码

/**
 * 大顶堆
 */
public class Head {
    //存储堆的数组
    private int[] head;
    //堆存储的最大数据的个数
    private int n;
    //堆中目前数据的个数
    private int count;

    public Head(int capacity) {
        //为什么要+1,因为下标为0的位置不能用
        head = new int[capacity + 1];
        n = capacity;
        count = 0;
    }

    //堆中插入数据
    public void insert(int data) {
        //如果堆满了就直接返回
        if (count == n) return;

        ++count;

        //把数据插入到最后
        head[count] = data;

        //自下向上堆化
        int i = count;
        while (i / 2 > 0 && head[i] > head[i / 2]) {
            int temp = head[i / 2];
            head[i / 2] = head[i];
            head[i] = temp;
            i = i / 2;
        }

    }

    //堆中删除堆顶元素
    public void removeMax() {
        if (count == 0) return; // 堆中没有数据
        head[1] = head[count];
        --count;
        heapify(head, count, 1);
    }

    private void heapify(int[] a, int n, int i) {
        // 自上往下堆化
        int maxPos = i;
        while (true) {
            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;
            //交换数据
            int temp = head[i];
            head[i] = head[maxPos];
            head[i] = temp;
            i = maxPos;
        }
    }


    //堆排序  假设数组是从下标1开始的
    public void sort(int[] a, int n) {
        //第一步 建堆
        buildHeap(a, n);
        int k = n;
        //直到剩下一个元素不在循环
        while (k > 1) {
            //交换堆顶和最后一个元素的位置
            int temp = a[1];
            a[1] = a[k];
            a[k] = temp;
            --k;
            //把剩下的元素进行堆化
            heapify(a, k, 1);
        }
    }

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


}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值