排序算法--堆排序

1.堆

1.1什么是堆

堆是一种特殊的树,它满足两个条件:

(1)堆是一种完全二叉树,即除了最后一层,其它层都是满二叉树,最后一个节点靠最左;

(2)堆中每一个节点值都必须大于等于(或小于等于)其左右子节点的值。

其中,每个节点的值都大于等于子树中每个节点的值的堆,叫做“大顶堆”;相反,对于每个节点的值都小于等于子树中每个节点的值的堆,叫做“小顶堆”。

1.2堆的实现

考虑到堆是一种特殊的完全二叉树,我们可以使用数组来存储,因为使用数组来存储完全二叉树是非常节省内存空间的(不需要使用额外的空间存储左右子节点的指针,直接可以利用下标之间的关系,就可以找到一个节点的父节点和子节点)。

如图,我们可以知道完全二叉树数组 之间的对应关系:

数组中下标为i的节点的左子节点,就在下标为2*i+1的位置;右子节点就在下标为2*i+2的位置。

1.3堆的核心操作

(1)向堆中插入一个元素

我们向堆中加入一个元素的过程,就是建堆的过程。当然,每次我们插入一个元素都是先放到最后一个节点的位置,然后按照从下往上 (二叉树中说法)进行比较调整的过程,直到满足成为堆的条件即可。

下面以大顶堆为例,从下往上进行建堆:

例如:初始数据序列为:{3, 6, 8, 5, 7},建堆的过程如下:

如上图,就是序列建堆的过程,每次加入一个元素都放到最后,自下向上与父节点进行比较,如果大于父节点,则进行交换,直到到达根节点的位置。

自下而上建堆,时间复杂度为:O(N)

(2)从堆顶弹出堆顶元素以及自上而下调整堆

主要步骤:

1)将堆顶元素与堆中最后一个元素进行交换(出堆);

2)删除堆中最后一个元素(数组中则通过让数组长度size--,模拟删除最后一个元素的过程);

3)将堆顶元素进行自上而下,与子节点中元素比较,如果子节点中较大的元素大于父节点,则将父节点与该子节点进行交换,直到父节点大于子节点(调整堆)。

将(1)中建的堆中元素8进行出堆以及调整的过程如下:

对于堆中其他元素的出堆以及调整的过程同上,就是重复以上3个步骤。

2.堆排序的java实现

2.1堆的实现

堆是一种特殊的完全二叉树,我们采用数组结构即可实现堆,数组的下标可以实现父节点和子节点的查找。

2.2 建堆(依次向堆中插入元素

自下而上进行建堆的核心代码:

// 构造大根堆(自下而上的建堆过程)  
public static void heapInsert(int[] arr){  // arr存储着待排序元素
    for (int i = 0; i < arr.length; i++) {  
        // 当前插入的索引  
        int currentIndex = i;  
        // 父节点索引  
        int fatherIndex = (currentIndex - 1) / 2;  
        // 如果当前插入的值大于父节点的值,则交换,并将索引指向父节点  
        // 然后继续向上与父节点比较,直到不大于父节点,退出循环  
        while (arr[currentIndex] > arr[fatherIndex]){  
            swap(arr, currentIndex, fatherIndex);    // 交换父子元素的值  
            currentIndex = fatherIndex;          // 继续向上比较  
            fatherIndex = (currentIndex - 1) / 2;      //肯定能回归到索引0,保证到达根节点  
        }  
    }  
}

2.3弹出堆顶元素并自上而下调整堆

弹出堆顶元素(数组模拟,不是真正的弹出),并将最后一个节点放到堆顶,再自上而下调整,使得其成为新的堆,核心代码如下:

// 弹出堆顶元素并将剩余的数构造成大根堆(通过自上而下进行比较)  
public static void heapify(int[] arr, int index, int size){  
    int left = 2*index + 1;     // 左子节点索引
    int right = 2*index + 2;     // 右子节点索引
    while (left < size){    // 保证进行了一次完整的调整
        int largestIndex;  
        // 判断左右孩子中较大的值的索引(要确保右孩子在size范围之内)  
        if (arr[left] < arr[right] && right < size){  
            largestIndex = right;  
        }else {  
            largestIndex = left;  
        }  
  
        // 比较父节点与孩子中较大的值,确定最大值的索引  
        if (arr[largestIndex] < arr[index]){  
            largestIndex = index;  
        }  
        // 如果父节点的索引是最大值的索引,退出循环  
        if (index == largestIndex){  
            break;  
        }  
  
        // 父节点不是最大值,与孩子中较大值交换  
        swap(arr, largestIndex, index);  
        index = largestIndex;  //将索引指向孩子中的较大值  
  
        // 重新计算交换之后的孩子的索引  
        left = 2*index + 1;  
        right = 2*index + 2;  
    }  
}

2.4堆排序的完整代码实现

import java.util.Arrays;  
  
/**  
 *  堆排序      时间复杂度O(nlogn)  空间复杂度O(1)  
 *      1. 建堆  
 *      2. 出堆  
 */  
public class heapSort {  
  
    public static void main(String[] args) {  
        int[] arr = {3, 6, 8, 5, 7};  
        heapSort(arr);    //大顶堆,排序后为从小到大  
        System.out.println(Arrays.toString(arr));  
    }  
  
  
    // 堆排序  
    public static void heapSort(int[] arr){  
        // 构造大根堆  
        heapInsert(arr);  
        int size = arr.length;  
        while (size > 1){  
            // 固定最大值  
            swap(arr, 0, size-1);  
            size--;  
            // 构造大根堆  
            heapify(arr, 0, size);  
        }  
    }  
  
    // 构造大根堆(通过新插入的数上升)  
    public static void heapInsert(int[] arr){  
        for (int i = 0; i < arr.length; i++) {  
            // 当前插入的索引  
            int currentIndex = i;  
            // 父节点索引  
            int fatherIndex = (currentIndex - 1) / 2;  
            // 如果当前插入的值大于父节点的值,则交换,并将索引指向父节点  
            // 然后继续向上与父节点比较,直到不大于父节点,退出循环  
            while (arr[currentIndex] > arr[fatherIndex]){  
                swap(arr, currentIndex, fatherIndex);    // 交换  
                currentIndex = fatherIndex;          // 继续向上比较  
                fatherIndex = (currentIndex - 1) / 2;      //肯定能回归到索引0  
            }  
        }  
    }  
  
    // 将剩余的数构造成大根堆(通过顶端的数向下比较)  
    public static void heapify(int[] arr, int index, int size){  
        int left = 2*index + 1;  
        int right = 2*index + 2;  
        while (left < size){  
            int largestIndex;  
            // 判断左右孩子中较大的值的索引(要确保右孩子在size范围之内)  
            if (arr[left] < arr[right] && right < size){  
                largestIndex = right;  
            }else {  
                largestIndex = left;  
            }  
  
            // 比较父节点与孩子中较大的值,确定最大值的索引  
            if (arr[largestIndex] < arr[index]){  
                largestIndex = index;  
            }  
            // 如果父节点的索引是最大值的索引,退出循环  
            if (index == largestIndex){  
                break;  
            }  
  
            // 父节点不是最大值,与孩子中较大值交换  
            swap(arr, largestIndex, index);  
            index = largestIndex;  //将索引指向孩子中的较大值  
  
            // 重新计算交换之后的孩子的索引  
            left = 2*index + 1;  
            right = 2*index + 2;  
        }  
    }  
  
    // 交换数组中的两个元素的值  
    public static void swap(int[] arr, int i, int j){  
        int temp = arr[i];  
        arr[i] = arr[j];  
        arr[j] = temp;  
    }  
}

堆排序的时间复杂度为:O(nlogn); 空间复杂度为:O(1)。

3.堆排序的应用

堆排序常用于解决处理中位数这类问题的效率非常高,可以借助两个堆一个大顶堆和一个小顶堆,将数据进行划分为两部分,然后根据数据长度(奇数还是偶数)就能快速找到中位数。

3.1问题描述(源自码题集)

数据流的中位数:

对于数据流问题,小码哥需要设计一个在线系统,这个系统不断的接受一些数据,并维护这些数据的一些信息。

中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。

请帮小码哥设计一个支持以下两种操作的系统:

+num -从数据流中添加一个整数k到系统中(0<k <232 )。

? -返回目前所有元素的中位数。

3.2解决思想

借助一个大顶堆q1和一个小顶堆q2,依次将输入的数据平均放到这两个堆中,如果数据总数为偶数,那么两个堆中的元素个数相同各存储一半的数据;如果数据总数为奇数,我们就约定将多出的这个放到大顶堆中。同时还要保证,大顶堆中的数据小于等于小顶堆中的数据,这样就知道前n/2小的数据存在大顶堆中,后n/2大的数据存在小顶堆中。

1.加入一个数x的处理思想:

1)如果大顶堆中元素个数大于小顶堆中的元素个数,先将该数x存到大顶堆中,堆调整好后,再将大顶堆堆顶元素弹出,存放到小顶堆中;

2)不满足1)条件,即只可能是大顶堆q1中的元素个数等于小顶堆q2中的元素个数(*默认约定是大顶堆q1元素个数大于等于小顶堆q2元素*),直接将数x压入到大顶堆q1中即可。然后,还需判断数x压入q1后,q1的堆顶(最大数)是否大于q2的堆顶(最小数),如果满足,则将q1栈顶弹出先压入到q2,然后q2调整后,再弹出栈顶元素压入到q1;不满足则不需要额外的调整。

2.根据数据元素的个数是奇数还是偶数进行分别处理:

1)如果元素个数为偶数,将q1和q2的栈顶元素弹出,求出这两个数的平均值即为该数据流的中位数;

2)如果元素个数为奇数,弹出q1的栈顶元素,即为该数据流的中位数。

3.3代码实现(java)

public class MidNum_heap {  
  
    public static void main(String[] args) {  
  
        PriorityQueue<Integer> queue1 = new PriorityQueue<>(new Comparator<Integer>() {  
            @Override  
            public int compare(Integer t1, Integer t2) {  
                return t2 - t1;  
            }  
        });   //大顶堆  
        PriorityQueue<Integer> queue2 = new PriorityQueue<>();    //小顶堆  
  
        Scanner input = new Scanner(System.in);  
        // code here  
        int n = input.nextInt();  
        input.nextLine();   // 读取回车  
        String[] strings = new String[n];  
        for (int i = 0; i < n; i++) {  
            strings[i] = input.nextLine();  
            String[] s = strings[i].split(" ");  
            if (s[0].equals("+")){  
                int x = Integer.parseInt(s[1]);  
                push(queue1, queue2, x);  
            }else{  
                if (queue1.size() > queue2.size()){  
                    System.out.println(queue1.peek());  
                }else {  
                    double res = queue1.peek() + queue2.peek();  
                    System.out.println(res * 1.0 /2);  
                }  
            }  
        }  
        input.close();  
  
  
    }  
  
    public static void push(PriorityQueue<Integer> queue1, PriorityQueue<Integer> queue2, int x){  
        if (queue1.size() > queue2.size()){  
            queue1.add(x);  
            queue2.add(queue1.peek());  
            queue1.poll();  
        }else {  
            queue1.add(x);  
            if (queue2.size() != 0 && queue2.peek() < queue1.peek()){  
                queue2.add(queue1.peek());  
                queue1.poll();  
                queue1.add(queue2.peek());  
                queue2.poll();  
            }  
        }  
    }  
  
}

注:此代码使用的是java中的优先队列实现,java的底层中优先队列就是使用的数据结构**堆**实现的,使用的是一个Object[ ]数组实现。

3.4运行结果

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值