这篇开始讲时间复杂度为O(N*logN)之三的堆排序.
我们所说的这个堆其实是个数组, 但是我们在脑子里要把它想象成一棵完全二叉树, 这里的值是下标
至于为什么能把数组跟二叉树完美地结合呢, 重点要说的是它是一颗完全二叉树, 完全二叉树的特点是如果二叉树的层数是N, 那么这颗树的N-1层一定是满二叉树(节点数为2^(N-1)-1), 并且在N层的叶节点一定是按照从左往右依次排列, 不会缺少任意一个叶节点.
从上面可以看出, 数组中找左右孩子和父亲均可以通过下标找出, 不需要遍历整个数组, 公式如下(变量i是下标, 虚线框内示例):
1. 左孩子 i*2 + 1, 3*2 + 1 = 7
2. 右孩子 i*2 + 2, 3*2 + 2 = 8
3. 父节点 (i-1)/2, (3-1)/2 = 1 # 向下取整
堆分成大根堆, 小根堆
顾名思义:
1. 大根堆, 任何一棵子树的根节点不小于全部的子节点
2. 小根堆, 任何一棵子树的根节点不大于全部的子节点
我们后面的部分均以大根堆进行讨论
我们不妨假设原始数组是[2, 5, 8, 6, 5, 7, 2, 9, 6]
大根堆的建立, 每增加一个数做以下三步
1. 首先放入数组到末尾
2. 如果该数比父节点数大, 则该节点和父节点数据交换
3. 继续第二步, 直到不比父节点数大
大根堆的整体建立过程
每增加一个数需要做的heapInsert具体步骤(上图虚线框内)
我们知道大根堆的0索引一定是全场最大值, 但是要实现排序, 还必须有heapify的过程
1. 把最大的数和最末尾的数交换
2. 如果新的值比它的任何一个孩子要小, 这把左右孩子中较大的跟新值交换
3. 继续第二步, 直到不比左右子节点更小
利用大根堆的整体排序过程
每次拿掉最大的数之后, 所经历的heapify的过程(上图虚线框内)
至此通过大根堆排序的过程结束, 分析一下时间复杂度
1. 建立大根堆, 每次增加一个数, 每次需要交换的最大次数是logN, 所以堆的建立过程时间复杂度是O(N*logN), 这只是从上往下的堆的建立过程, 如果是从下往上建立的话, 其时间复杂度是O(N), 有机会再聊.
2. 排序, 一共进行N-1轮, 每轮需要交换的最大次数是 logN, 所以时间复杂度也是O(N*logN).
延伸:
我们知道heapify的过程是每次替换掉最大值, 那有没有可能我们其实是要删掉其中任意一个索引的值, 如果我们把最后一位补到改位置, 再让它根据父节点, 左右孩子的大小决定是往上还是往下, 依次重复, 直到不能调整为止. 这样我们只要确定删除的索引的情况下, 用O(logN)的时间复杂度, 删除数据, 并且还保持删除之后的最大值.
需要上移的情况
需要下移的情况
—————END—————
喜欢本文的朋友,欢迎关注公众号 程序员Song,后续精彩内容不再错过