p4-堆+桶排序

p4 堆排序、桶排序、排序总结

堆是一个完全二叉树。用数组对应完全二叉树,则i位置左孩子为2*i+1,右孩子为2*i+2,父为(i-1)/2。
堆结构重要性大于堆排序

大根堆小根堆

对于每一颗子树,其最大值都是头节点的值。

构建堆的过程:heapInsert

对于每一个新节点,来了之后,递归的和自己父亲比较,比父亲大则交换,直到到头或者没父亲大了。时间复杂度O(logN)

调整堆:heap

对于一个需要调整的节点(如删除堆顶时,将堆顶与堆尾进行交换,此时堆顶需要调整),要寻找其左右孩子,获得大的进行交换。持续进行,直至没有孩子或者孩子都没自己大。时间复杂度O(logN)

堆排序

对于一个数组而言,先将其构建为大根堆。代码如下

class HeapSort {
    public static void heapSort(int[] nums, int left, int right){
        if(nums==null||nums.length<2){
            return;
        }
        for(int i=0;i<nums.length;i++){
            heapInsert(nums, i);
        }
        int heapSize = nums.length;
        swap(nums, 0, --heapSize);
        while(heapSize>0){
            heapify(nums, 0, heapSize);
            swap(nums, 0, --heapSize);
        }
    }

    public static void heapInsert(int[] nums, int index){
        while(nums[index]>nums[(index-1)/2]){
            swap(nums, index, (index-1)/2));
            index = (index-1)/2;
        }
    }

    public static void heapify(int[] nums, int index, int heapSize) {
        int left = index*2+1; // 左孩子下标
        while(left<heapSize){ // 左孩子不越界,那么右孩子也不会越界,即存在孩子
            int maxIndex = (left+1)<heapSize && nums[left]<nums[left+1] ? left:left+1;
            maxIndex = nums[maxIndex] > nums[index]? maxIndex:index;
            if(maxIndex == index){
                break;
            }
            swap(nums, maxIndex, index);
            index = maxIndex;
            left = idnex*2+1;
        }
    }

    public static void swap(int[] nums, int index1, int index2){
        int temp = nums[index1];
        nums[index1] = nums[index2];
        nums[index2] = temp;
    }
    
    public static void main(String[] args) throws Exception {
        int[] nums = new int[]{9,5,7,3,1,6,8,4,2};
        mergeSort(nums, 0, nums.length-1);
        for(int num:nums){
            System.out.println(num);
        }
    }
}

补充:假设一开始就有整个数组

则构建大根堆的过程,可以不按照从头到尾依次heapInsert。可以从尾到头,依次进行heapify。这个过程会更好一些。
分析:对于叶子节点,共N次,每次时间1;倒数第二层,共N/2次,每次时间2;倒数第三层,共N/4,每次时间3;。。。。
T(N) = N/2 *1 + N/4 *2 + N/8 3 + …
2
T(N) = N/2 *2 + N/2 *2 + N/4 3 + …
2
T(N) - T(N) = N + N/2 + N/4 + … = O(N)

故排序算法可以这样写:实际上总时间复杂度没有变化,但是构建最大堆的过程变快了

public static void heapSort(int[] nums, int left, int right){
        if(nums==null||nums.length<2){
            return;
        }
        // for(int i=0;i<nums.length;i++){
        //     heapInsert(nums, i);
        // }
        for(int i=nums.length-1;i>=0;i--){
            heapify(nums, i, nums.length);
        }
        int heapSize = nums.length;
        swap(nums, 0, --heapSize);
        while(heapSize>0){
            heapify(nums, 0, heapSize);
            swap(nums, 0, --heapSize);
        }
    }

堆拓展题目

已知一个几乎有序的数组,请选择一个合适的排序算法对其排序。几乎有序:如果把数组排序后,每个元素移动的距离不超过k,且k针对于数组而言比较小。

解:设定一个长度为k+1的小根堆,先放前7个数,进行heapInsert,将堆顶弹出放于数组[0];然后将第8个数加入堆,调整堆结构,将堆顶放置于数组[1],周而复始,直至没有数可以压入堆。此时依次弹出堆中元素,放于数组之中。时间复杂度O(N*logK)

public void sortedArrDistanceLessK(int[] nums, int k){
    PriorityQueue<Integer> heap = new PriorityQueue<>();
    int index = 0;
    for(;index<=Math.min(nums.length, k);index++){
        heap.add(nums[index]);
    }
    int i = 0;
    for(;index<nums.length;i++, index++){
        heap.add(nums[index]);
        nums[i] = heap.poll();
    }
    while(!heap.isEmpty()){
        nums[i++] = heap.poll();
    }
}

堆的细节

扩容:堆的底层是个数组,那么数组耗尽时需要扩容。java扩容时每次大小翻倍,即100->200->400。每次扩容后需要拷贝,此时O(N),但是扩容次数时O(LogN),那么平均单次扩容代价为O(N*logN)/N = O(logN)
操作:对系统内部提供的堆,只能使用add、poll,不能单独改动对某一个节点进行改动。自己手写的是可以支持的。所以部分题目需要手写堆

比较器

调用sort函数的时候,如果希望人为的定义比较的规则,需要参数中加一个 new Comparator(),这个Comparator方法是自己定义的,但是定义时也有潜规则。

  1. 返回负数,则第一个参数排前面
  2. 返回正数,则第二个参数放前面
  3. 返回0,无所谓

比较器亦可使用在有序结构中,如堆。那么堆的构造函数就应该传入一个比较器。它的潜规则如下:

  1. 返回负数,则第一个参数排上面
  2. 返回正数,则第二个参数放上面
  3. 返回0,无所谓

计数排序

此前的排序都是基于比大小。而即将讲的排序应用范围较窄,但是适用于特定数据状态的数据,实际上是一种定制算法。

例:数组nums,数据范围为0200,解:开辟0201的数组,下标代表源数组中值为下标的元素的个数。遍历一遍源数组,获得词频数组;然后将词频数组还原,即排序完成。
但是不适用于数据范围过大的数组,如-231~231-1

基数排序

举例:[17,25,72,13,100]数据最高三位,所以补齐成三位:[017,025,072,013,100]。准备十个桶:0~9。首先根据个位数字进桶
0 1 2 3 … 5 … 7
100 072 013 025 017
从前往后出桶:100,072,013,025,017,根据十位数字进桶
0 1 2 3 … 5 … 7
100 013 025 072
017
出桶:100 013 017 025 072,根据百位数字进桶
0 1 …
013 100
017
025
072
出桶:013 017 025 072 100,排序完毕

但是如果排的数字没有进制,即不可使用基数排序。

基数排序优化后的代码

本来应该按照位数上的数字分别进桶,然后从0桶到9桶依次出桶,然后判断下一位置。但是可以优化成词频数组来代替桶。
举例:[13,21,11,52,62]
词频数组:[0,2,2,1,0…]
改造为前缀和数组:[0,2,4,5,5…],意义:[2] = 4,某位数上小于等于2的数有4个。那么最后入桶的(最右边的)一定在下标3(4-1)的位置上。因为先入桶的先出桶。这里模拟成了后入桶的后出桶。
从右向左遍历原数组,遇到62,找前缀和数组[2] = 4那么62应该放在数组的4-1=3的位置,前缀和数组更新;同理52应放在3-1=2的位置
最终:[21,11,52,62,13]
再在十位上进行该操作,最终结果[11,13,21,52,62]

本质上利用count数组来分片,通过从右向左遍历,完成了入桶出桶的操作。例如:[2] = 4,某位数上小于等于2的数有4个。那么最后入桶的(最右边的)一定在下标3(4-1)的位置上。因为先入桶的先出桶。这里模拟成了后入桶的后出桶。

排序的总结

不稳定排序:选择、快排、堆排
稳定:冒泡、插入、归并、一切桶排序思想下的排序
目前未找到O(NlogN)且空间O(1)又稳定的排序
堆排:O(N
logN)且空间O(1)但不稳定

选择 N^2 1 不稳定
冒泡 N^2 1 稳定
插入 N^2 1 稳定
归并 NLogN N 稳定 稳定,但是空间大
快排 N
LogN logN 不稳定 常数项快,但不稳定,空间大
堆 N*LogN 1 不稳定 空间使用小,但不稳定

问题1:基于比较的排序,能不能时间小于 NlogN, 解:目前看来不行
问题2:基于比较的排序,时间小于 N
logN,空间N以下,能不能稳定,解:不行

常见坑:

  1. 归并排序额外空间复杂度可以为 O(1),但是难,而且会破坏稳定性。可以搜索"归并排序 内部缓存法"
  2. 原地归并排序帖子,会是时间为 N^2
  3. 快排可以稳定,但是会上升空间复杂度为 N
  4. 题目:奇数放在左边,偶数放在右边,且原始相对次序不变,而且要求原地,遇到这个问题可以怼面试官。解:快排中的划分行为,将小的放左边,大的放右边,本质上是一种非0即1的行为。那么也可以对应为奇数放左边,偶数放右边。但是快排做不到稳定,所以我也不会,面试官你来讲讲

排序改进

NLogN 可以和 N^2结合。如快排中,如果左右范围小于某个指标,如60,此处可以将这部分范围改为插入排序。因为小范围内时间差别不明显,而常数项间插入排序显然小,所以可以综合两者优势,在大范围上用快排调度,小范围上用插入排序,最终提高排序速度

Arrays.sort()如果发现排序的数据是基础类型,则会使用快排,如果是非基础类型,则会使用归并,为什么?解:稳定性。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值