快速排序的空间复杂度
最差情况,每次都取到最大/最小值,一共展开 n 层递归,需要的空间复杂度为 ;
最好情况,每次都取到中点值,相当于完全二叉树展开,需要的空间复杂度为
注意:用迭代代替递归无法节省空间!因为节省不下来!
递归实现了对于中点位置的存储,每层递归结束后才会释放相应资源,因为有了中点位置,快排才能借助递归实现。但是,若使用迭代,也需要手动记录中点位置,并使用栈结构,告诉计算机返回什么位置,以及该位置左右各是什么数。(自己的理解还是不太清楚,见谅)
堆
逻辑概念上,堆是一个完全二叉树结构。
完全二叉树
若某个树为满二叉树,或其为从上往下从左往右依次含有节点的树,则称之为完全二叉树。
任意数组从下标为 0 出发的任意一段都可以转化为完全二叉树。
例:如上左图,取数组前 7 位构成完全二叉树。
在上述数组中任取 i 位置,其左孩子下标为 2 * i + 1,其右孩子下标为 2 * i + 2,父节点为.
堆的分类
大根堆:任意子树的最大值为头节点的值
小根堆:任意子树的最小值为头节点的值
堆的基本操作
添加堆
给出一个新的数字,将新的数字插入已有的大根堆 / 小根堆中。
方法:插入节点后,与其父节点 的比较大小。
例:
注意:需要向下取整,且 i = 0 时,运算结果为 0 .
代码中使用 heapInsert 函数实现:
while 循环反复检查是否符合大根堆的条件,若不符合,令子节点与父节点处的数值交换,并更新 index 为其父节点,再次进入循环判断。
出堆
在已有的大根堆 / 小根堆中,弹出堆顶元素,并做相应调整。
方法:大根堆中,先将末尾元素补至堆顶,令 heapSize - 1,然后判断是否还有左右孩子,若有,求其左右孩子的最大值并与之比较,若其最大,则停止;否则,交换两者位置并重复上述步骤。
代码中使用 heapify 函数实现:
修改堆
在堆中,修改某一位置的元素,要求仍然保持大根堆 / 小根堆。
方法:检查元素变大 / 变小,若变大,则调用 heapInsert 函数;若变小,则调用 heapify 函数。
时间复杂度
完全二叉树的高度
已知有 N 个节点,则完全二叉树的高度为 logN 。
相应地,堆操作中,无论是插入、弹出、调整,时间复杂度都是 级别。
堆排序
先建堆,然后进行堆排序操作。
堆排序中,每次将堆顶与堆末数字交换,然后在 heapSize-- 的空间上整理为有序堆,再重复上述操作。当 heapSize == 0 时,排序完成。
演示:
代码实现
其中 heapInsert() 函数和 heapify() 函数如前文所示,前者负责建堆,后者负责整理堆为有序堆。
时间复杂度 。
优化:
heapInsert() 函数中,若一次性给出全部数组,则可使用该方法:
思想:从右往左从下往上做 heapify ,每次只需解决对应子树中的问题
堆排序扩展题目
注意:Java中,优先队列就是小根堆!
关于堆的实际使用,补充以下几点:
- 扩容问题:以数组存储堆结构时,每次耗尽都成倍扩容。故数组长度为2,4,6,8...,每次扩容需要O(N) ,扩容次数为O(logN),故整体扩容代价为 !
- *黑盒理念*:编系统提供的堆结构是一个”黑盒“,只能高效执行输入一个(add)和输出一个(poll)数。缺点:在已有的堆结构上改变某个数,并令其重新调整为堆结构,此时调整代价很高!但是自己手写的堆结构可以实现修改后的高效调整。
若需要实现高效调整,只能手写堆!
代码实现:
建堆 -> 加入&弹出 -> 弹出剩余
比较器
例:
准备数组:
下图中,第一个参数为对应数组,第二个参数则是设置比较器。
比较器实现举例:
上图注释说明了比较器的返回规则:
- 负数——第一个参数在前
- 正数——第二个参数在前
- 0——任意在前
上图中绿色部分其实等价于:
用上图更清晰,而用一句 return 则更简洁。
同理也可依照此对其他参数进行排序,如:
作用:减少代码量
桶排序和基数排序
特点:不是基于”比较“的排序,而是基于数据状况产生的排序!
实际使用需要根据数据状况进行定制。
桶排序
基数排序
补充:计数排序 —— 基于类型进行统计、计数后排序
代码实现
相关函数实现:
tips:仅对于非负值!
解释:使用 count 数组完成分片!(或者说使用计数排序的结果模拟”桶“的概念)