【笔记】3.详解桶排序以及排序内容大总结

本文详细讲解了堆结构、大根堆和小根堆的概念,重点阐述了堆排序的过程,包括heapInsert和heapify操作,以及堆排序的优化和拓展。同时讨论了排序算法的稳定性,并对比了快速排序、归并排序和堆排序的优缺点。
摘要由CSDN通过智能技术生成

目录

一、基本概念

1.堆结构

1.1 定义:

1.2 实现:

2.大根堆和小根堆

二、排序算法

1.堆排序

1.1 heapInsert过程

1.2 heapify堆化过程

1.3 堆排序详解

1.3.1 实现:

1.3.2 优化:

1.3.3 补充说明:

1.4 堆排序拓展

2. 比较器的使用

2.1 规定:

2.2 案例:

3. 桶排序

3.1 计数排序

3.2 基数排序

三、排序内容大总结

1. 排序算法的稳定性

2. 总结

3. 常见的坑

4. 工程上堆排序的改进


目录结构不太合理,基本按照教学顺序记的,暂时不纠结这个,等全部学完会重新梳理一遍

以下内容如有错误欢迎指出

一、基本概念

1.堆结构

1.1 定义:

用数组实现的完全二叉树的结构,优先级队列结构就是堆结构

完全二叉树:满二叉树或者是从左往右依次遍满的状态

1.2 实现:

数组从0出发的连续一段对应成完全二叉树

heapSize=7,与数组长度无关,决定了堆的内容有哪些(即0~6)

下标对应关系:

  • 左孩子=2*i+1
  • 右孩子=2*i+2
  • 父=(i-1)/2

2.大根堆和小根堆

大根堆:

完全二叉树中如果每棵子树的最大值都在顶部就是大根堆

小根堆:

完全二叉树中如果每棵子树的最小值都在顶部就是小根堆

二、排序算法

1.堆排序

1.1 heapInsert过程

保证每进来一个数,都保持是大根堆的结构

新进来的数和父比较,比父大则交换,直到PK不过父了或是到头了为止,这里就是新进来的6要与5交换

代码:

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

1.2 heapify堆化过程

去掉堆中最大值,仍然保持大根堆的结构

将堆中最后一个数字先放到0位置上,从头节点开始再依次向下比较,先在左孩子和右孩子中选一个最大值和父位置比较,比父大则交换,直到没有孩子比自己大或者没孩子为止

这里是将6去掉,将末尾的4放到0位置上,heapSize-1(即5位置上的数与堆断开,是无效区域),3和5中选最大的5和4比较,4和5交换,4到尾了停止

代码:

 public static void heapify(int[] arr, int index,int heapsize)
    {
        int left = 2 * index + 1; 

        while (left< heapsize)
        {
            int largest = (left + 1) < heapsize && arr[left] < arr[left + 1] ? left + 1 : left;
            largest = arr[largest] > arr[index] ? largest : index;
            if (largest == index)
            {
                break;
            }
            swap(arr, index, largest);
            index = largest;
            left = 2 * index + 1;
        }
    }

如果将大根堆中任意一个数做了调整,只要做一遍heapify+heapInsert就仍然能保持堆结构(不是上去就是下去)

调整一个数的时间复杂度是O(logN)     ->    只关注父或子一条路径上的高度是logN级别的

1.3 堆排序详解

1.3.1 实现:

①等同于一个个加数字,每加一个数字都保持大根堆的结构(做heapInsert操作),直到heapSize为数组长度

②将最大值与最后一个数交换,heapSize-1,即最大值与堆断开联系,最大值排好,换到最前面的数做heapfiy操作保持大根堆结构

③重复②操作直到heapSize减到0,全部排好

时间复杂度O(NlogN),空间复杂度O(1)

代码:

public static void heapSort(int[] arr)
    {
        if (arr == null || arr.Length < 2)
        {
            return ;
        }
        //for (int i = 0; i < arr.Length; i++) //O(N)
        //{
        //    heapInsert(arr, i); //O(logN)
        //}

        for (int i = arr.Length-1; i >=0; i--)
        {
            heapify(arr, i, arr.Length);
        }

        int heapsize = arr.Length;
        while (heapsize>0) //O(N)
        {                      
            swap(arr, 0, --heapsize);//O(1)
            heapify(arr, 0, heapsize); //O(logN)
        }

    }
1.3.2 优化:

第一步可以做优化,假设所有数字都有,从倒数第二层节点开始做heapify,逐渐往上检查调整成大根堆,最底层子节点数量是N/2级别的,可以不用做heapify

时间复杂度的估计:

最底层子节点看一眼 N/2   ->  1 

倒数第二层节点看一眼+1次heapify N/4   ->  2

倒数第三层节点看一眼+2次heapify N/4   ->  3

T(N)=N/2*1+N/4*2+N/8*3+...

2T(N)=N/2*2+N/2*2+N/4*3+...

2T(N)-T(N)=N+N/2+N/4+...

T(N)=O(N)

1.3.3 补充说明:

①扩容问题

底层是用数组形式存的堆结构,但是数组长度有限,数组长度不够用的时候需要扩容,每次成倍扩容,扩容一次成本是O(N),添加N个数扩容次数是logN次,那么添加一个数的成本就是N*logN/N即O(logN)

②用系统自带堆结构还是手写堆结构

系统自带的堆结构相当于一个黑盒,支持add或者poll的基础操作,不支持以很小的代价调整其中一个数还维持堆结构,手写的可以,所以某些情况要手写堆结构来提高效率

1.4 堆排序拓展

已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离不超过k,且k相对于数组来说比较小,请选择一个合适的排序算法针对这个数据进行排序

假设k=6,那么0位置上的数不可能是7位置之后的,所以只要将0-6位置上的7个数扔到小根堆里,小根堆的最小值即为0位置上的数,依次往后7个数重新放到小根堆,再选出最小值放到1位置,周而复始,到最后不满7个数的时候,依次弹出继续保持小根堆结构即可。

时间复杂度为O(Nlogk)

每个语言都有现成的小根堆算法,Java->PriorityQueue,C#还真没找到

代码:

2. 比较器的使用

比较器的实质就是重载运算符,可以很好的应用再特殊标准的排序上,可以很好的应用在根据特殊标准排序的结构上

2.1 规定:

返回负数,第一个参数在前面

返回正数,第二个参数在前面

返回0,谁在前面无所谓

2.2 案例:

①自定义一个student结构,包含姓名,id,年龄三个参数,要求按照id升序排序

代码:

②将默认的小根堆改成大根堆

代码:


3. 桶排序

桶排序思想下的排序都是不基于比较的排序,时间复杂度为O(N),额外空间负载度O(M),应用范围有限,需要样本的数据状况满足桶的划分

3.1 计数排序

对于特殊数据可以使用计数排序,例如针对员工年龄进行排序,假设员工年龄范围为0-100岁,那么准备一个0-100的数组,遍历一遍原数组,在计数数组中按顺序记录年龄出现的频次,再将词频还原成有序的数组,本质上是一种特殊的桶排序

时间复杂度为O(N)

3.2 基数排序

10进制的数,准备10个桶,将数据从左往右依次按照个位数大小放桶

再按顺序倒出来,先进先出

再按照十位数依次进桶,按顺序倒出来,最后按照百位数进桶 ,倒出来排好序

代码:

     public static void radixSort(int[] arr)
    {
        if (arr == null || arr.Length < 2)
        {
            return;
        }
        radixSort(arr, 0, arr.Length - 1, maxbits(arr));
    }

    public static void radixSort(int[] arr,int L,int R ,int digit)
    {
        int radix = 10;
        int i, j = 0;
        int[] bucket = new int[R - L + 1];

        for (int d = 1; d <= digit; d++)
        {
            int[] count = new int[radix];
            for (i = L; i<=R;i++)
            {
                j = getDigit(arr[i], d);
                count[j]++;
            }
            for (i = 1; i < radix; i++)
            {
                count[i] = count[i] + count[i - 1];
            }
            for (i = R; i >= L; i--)
            {
                j = getDigit(arr[i], d);
                bucket[count[j] - 1] = arr[i];
                count[j]--;
            }
            for (i = L, j = 0; i <= R; i++, j++)
            {
                arr[i] = bucket[j];
            }
        
        }
    }

    public static int maxbits(int[] arr)
    {
        int max = int.MinValue;
        for (int i = 0; i < arr.Length-1; i++)
        {
            max = Mathf.Max(max,arr[i]);
        }
        int res = 0;
        while (max != 0)
        {
            res++;
            max /= 10;
        }
        return res;
    }

    public static int getDigit(int x ,int d)
    {
        return ((x / ((int)Mathf.Pow(10, d - 1))) % 10);
    }

代码层面做了很大程度的优化

count数组记录词频

将count数组变成前缀和数组(和前一个数做累加),含义从原来个位数=2的数有2个变成个位数小于等于2的有4个

再从右往左,根据词频表,将数据放到对应的Help数组(与原数组等大),这里062,个位数是2,根据词频表知道个位数小于等于2的有4个,所以这个数只能放到第0,1,2,3上,因为是最右侧的数是最后进桶的,要最后出桶,所以放到3位置

这里利用count数组实现分片等同于完成一次出桶入桶的操作

从右往左,保证了先进先出?

三、排序内容大总结

1. 排序算法的稳定性

同样值得个体之间,如果不因排序而改变相对次序,就是这个排序是有稳定性的,否则就没有。

对于基础类型的数组稳定性并没有什么用,因为都是等效的,但是对于特殊结构的数据,稳定性是有必要的。

例如定义一个学生结构,它有班级和年龄两个属性,先按照年龄从小到大排序,再按照班级排序,如果是具有稳定性的排序,那么在第二次排班级的时候,每一个班级里面的数据也会保持第一次排序的结果是从小到大的,否则就又乱掉了。

再比如将商品的价格从低到高排一下,再按照好评率从高到低排一下,如果是具有稳定性的排序,就能得到物美价廉的产品,是有这种实际需求的。

不具备稳定性的排序:

选择排序、快速排序、堆排序

  • 选择排序:

  • 快速排序:

  • 堆排序:

具备稳定的排序:

冒泡排序,插入排序、归并排序、一切桶排序思想下的排序

  • 冒泡排序和插入排序在遇到数值相等的时候不换就可以保持稳定性
  • 归并排序在遇到数值相等的时候先拷贝左边的就可以保持稳定性(小和问题里改写的归并排序需要先拷贝右边的就丧失了稳定性)
  • 桶本身具有先进先出的性质,入桶和出桶的顺序是可以维持的,具有稳定性的

2. 总结

排序方法时间复杂度空间复杂度稳定性
选择排序O(N²)O(1)×
冒泡排序O(N²)O(1)
插入排序O(N²)O(1)
归并排序O(NlogN)O(N)
快速排序O(NlogN)O(logN)×
堆排序O(NlogN)O(1)×

一般会选择使用快速排序,因为经过实验的结果,快排的常数项最低,实在是有空间的限制可以用堆排,或者对稳定性有要求的用归并排序

3. 常见的坑

第5条这里可以类比快排,放在左边和右边都是非0即1的选择,是等效的

4. 工程上堆排序的改进

①充分利用O(NlogN)和O(N²)排序各自优势

针对大样本量使用快速排序O(NlogN)的调度方式,对于拆分好的小样本样本量使用插入排序O(N²),因为插入排序的常数项极低,样本量小的时候N²的瓶颈没有那么明显,反而常数项变得重要,这是一种综合排序

②稳定性的考虑

系统的排序方法,针对基础类型数据使用快速排序,非基础类型排序使用归并排序

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值