(1) 堆结构就是用数组实现的完全二叉树结构
(2) 完全二叉树中如果每棵子树的最大值都在顶部就是大根堆
(3) 完全二叉树中如果每棵子树的最小值都在顶部就是小根堆
(5) PriorityQueue底层就是堆结构
堆结构中i位置的左孩子是2i+1,右孩子为2i+2,父节点为Math.floor( i − 1 2 \frac{i-1}{2} 2i−1):
完全二叉树中前n层的节点数为 1 + 2 + 4 + . . . + 2 n − 1 = 2 n − 1 1+2+4+...+2^{n-1} = 2^n-1 1+2+4+...+2n−1=2n−1 (等比数列求和),设i位置当前第n行前面有x个节点,则 i = 2 n − 1 − 1 + x i=2^{n-1}-1+x i=2n−1−1+x
左孩子位于n+1行,前面还有2x个节点(因为i在第n层前面有x个,每一个节点在下一层都会有2个节点),所以左孩子的位置为 2 n − 1 + 2 x = 2 ( 2 n − 1 + x ) − 1 = 2 ( 2 n − 1 − 1 + x ) + 1 = 2 i + 1 2^n-1+2x=2(2^{n-1}+x)-1=2(2^{n-1}-1+x)+1=2i+1 2n−1+2x=2(2n−1+x)−1=2(2n−1−1+x)+1=2i+1
右孩子位置为左孩子位置+1,故为2i+2
设父节点位置为k,其左孩子或右孩子位置为i,则有2k+1=i,k=(i-1)/2;或2k+2=i, k=(i-2)/2。由于java中自动采用向下取整,对于右孩子来说 i − 2 2 = i 2 − 1 \frac{i-2}{2}=\frac{i}{2}-1 2i−2=2i−1的结果和 i 2 − 1 2 \frac{i}{2}-\frac{1}{2} 2i−21向下取整是一样的,所以父节点的位置都用 i − 1 2 \frac{i-1}{2} 2i−1记录即可。
(1)将数组变为大根堆(heapinsert);
(2)让最后一位和堆顶交换,heapsize-1(即将最大值保存至数组最后的位置,继续对前面的部分进行堆操作)
(3)再重新变为大根堆,相当于把堆顶元素变小再重排(heapify)
(4)直到堆大小为1停止。
heapInsert:
在大根堆中(末尾)新加入一个节点,不断向上与父节点比较及swap,直至父节点>=它时停止,形成一个新的大根堆。复杂度为O(logN),因为需要swap的次数至多为堆的高度
heapify:
假设数组中一个值变小了,重新调整为大根堆的过程。
这里步骤(2)后从根节点开始向下与子节点比较及与子节点中更大的数swap,直至子节点都比当前数小或没有子节点,使整个数组恢复堆结构
上述步骤(1)(从上往下添数)的时间复杂度为O(NlogN),使用floyd建堆法(自下而上,即数组从右往左heapify将子树恢复成堆)可以下降至O(N)级别。
分析:
假设目标堆是一个满堆,即第 k 层节点数为 2ᵏ。输入数组规模为 n, 堆的高度为 h, 那么 n 与 h 之间满足 n=2ʰ⁺¹ - 1,可化为 h=log₂(n+1) - 1。 (层数 k 和高度 h 均从 0 开始,即只有根节点的堆高度为0,空堆高度为 -1)。
建堆过程中每个节点需要一次下滤操作,交换的次数等于该节点到叶节点的深度。那么每一层中所有节点的交换次数为节点个数乘以叶节点到该节点的深度(如第一层的交换次数为 2⁰ · h,第二层的交换次数为 2¹ · (h-1),如此类推)。从堆顶到最后一层的交换次数 Sn 进行求和:
Sn = 2⁰ · h + 2¹ · (h - 1) + 2² · (h - 2) + … + 2ʰ⁻² · 2 + 2ʰ⁻¹ · 1 + 2ʰ · 0
对①等于号左右两边乘以2,记为②式:
②: 2Sn = 2¹ · h + 2² · (h - 1) + 2³ · (h - 2) + … + 2ʰ⁻¹ · 2 + 2ʰ
②-①错位相减得到③式:
③ = Sn =-h + 2¹ + 2² + 2³ + … + 2ʰ⁻¹ + 2ʰ
等比数列求和,a=2, q=2,n=h,则
S n = − h + a 1 ∗ ( 1 − q n ) 1 − q = − h + 2 ∗ ( 1 − 2 h ) 1 − 2 = 2 h + 1 − ( h + 2 ) = 2 log 2 ( n + 1 ) − ( log 2 ( n + 1 ) + 1 ) = n − log 2 ( n + 1 ) S_n=-h+\frac{a_1*(1-q^n)}{1-q} =-h+ \frac{2*(1-2^h)}{1-2}=2^{h+1}-(h+2)=2^{\log_2(n+1)}-(\log_2(n+1)+1)=n-\log_2(n+1) Sn=−h+1−qa1∗(1−qn)=−h+1−22∗(1−2h)=2h+1−(h+2)=2log2(n+1)−(log2(n+1)+1)=n−log2(n+1), 渐进为O(N)。
// 堆排序
public int[] heapSort(int[] nums){
if (nums == null || nums.length<=1) {
return nums;
}
// O(NlogN)
// for (int i=0; i=0; i--) {
heapify(nums, i, nums.length);
}
int heapsize = nums.length;
swap1(nums, 0, --heapsize); //将第一位和最后一位进行交换,heapsize-1
while (heapsize >0) { // O(N)
heapify(nums, 0, heapsize); // O(logN)
swap1(nums, 0, --heapsize); // O(1)
}
return nums;
}
public void heapInsert(int[] nums, int index) { //在index位置插入新数
while (nums[index] > nums[(index-1)/2]) {
swap1(nums, index, (index-1)/2);
index = (index-1)/2;
}
}
public void heapify(int[] nums, int index, int heapsize) { //从index位置开始heapify,即index处的数变小后重新整理数组恢复为大根堆结构的过程
int left = 2*index+1;
while (left< heapsize) { //下方还有孩子的时候
//储存子结点中更大的数的下标 (有右比大,没右左大)
int larger = ((left+1nums[left])) ? (left+1) : left;
//父节点和较大孩子之间进行比较
int largest = nums[index]>nums[larger] ? index : larger;
if (largest == index) { //若父节点已经比子节点都大,则大根堆结构已完成
break;
}
swap1(nums, index, largest);
index = largest;
left = 2*index+1;
}
}
heapSort的时间复杂度为O(NlogN),额外空间复杂度为O(1)
已知一个几乎有序的数组(几乎有序是指,如果把数组顺序排号的话,每个元素移动的距离不超过k,并且k相对于数组来说比较小)。选择合适的算法对数组排序。
思路:
建立一个heapSize为k+1的小根堆,数组中最小的数一定在这个小根堆中;重复操作弹出最小值、小根堆范围在数组中往后挪1位并heapify(滑动窗口),直至heapSize==1;
该方法复杂度为O(Nlog(k+1));考虑k时相对数组来说很小的数,可以渐进为O(N)
此题可以用java中内置的PriorityQueue作小根堆,因为只需要弹出和添数的操作。
注1:系统内置的堆结构,改中间的值不支持自动调整;只支持弹出和添数。
注2:由数组维持的堆结构,添数时考虑扩容情况(满时新开辟一个length*2的空间,并把原来的全部复制一遍,1->2->4->8…)的时间复杂度仍是O(logN)级别。分析:扩容的次数为logN,乘上N个数复制的总操作是NlogN,但平均到添加每个数每个数就是O(logN)。
public void sortedArrDistanceLessK(int[] arr, int k) {
PriorityQueue minHeap = new PriorityQueue(); //系统默认是小根堆
int index=0;
for (; index<=Math.min(arr.length-1, k); index++) {
minHeap.add(arr[index]);
}
int i=0;
for (; index
e.g.
[0, 5, 3, 3, -2, -3, -5, 0, -5, 2] 最大值5,最小值-5,共11 个元素。
得到计数数组:[2, 0, 1, 1, 0, 2, 0, 1, 2, 0, 1]
计数排序的时间复杂度为O(N+k),k为数组元素的范围;空间复杂度为O(k)。
如果要求直接修改原数组,则有序数组的空间复杂度不可省略,空间复杂度是 O(n + k)。
但这种方法的使用取决于数据状况,只适用于小范围的数字;对于非整数的比较、大范围的比较都不适用,所以该方法并不常用
如何将频次数组转化为顺序数组:
e.g.
将计数数组转换成前缀和数组:[2, 2, 3, 4, 4, 6, 6, 7, 9, 9, 10]
反向遍历填充:
[_, _, _, _, _, _,2, _, _, _ ],[2,2,3,4,4,6,6,6,9,9,10]
[_,−5, _, _, _, _,2, _, _, _],[1,2,3,4,4,6,6,6,9,9,10]
public int[] countingSort(int[] nums) {
int min = nums[0], max = nums[0];
for (int i=1; imax ? nums[i] : max;
}
int[] counts = new int[max-min+1];
//统计频次
for (int i=0; i=0;i--) {
int index = --counts[nums[i]-min];
sorted[index] = nums[i];
}
return sorted;
}
出桶操作采用和计数排序一样的反向遍历填充法
基数排序相对于计数排序,是几进制的数字就只需要准备几个桶。但这些非基于比较的排序都取决于数据状况,基数排序也不能用于非数字的比较。
public int[] radixSort(int[] nums, int L, int R) {
final int radix = 10;
//有多少个数就准备多大的辅助空间
int[] help = new int[R-L+1];
//考虑有负数的情况,将数组中每个数减去min,使得最小值为0
int min = nums[0];
int max = nums[0];
for (int i=1; imax ? nums[i] : max;
}
for (int i=0; i=L; i--) {
int unit = getDigit(nums[i], d);
help[--count[unit]] = nums[i];
}
//将排好序的数组倒回原数组
for (int i=L,j=0; i<=R; i++,j++) {
nums[i] = help[j];
}
}
//反向平移
for (int i=0; i
时间复杂度:O(d(n + k))O(d(n+k)),其中 n 是数组的长度,k 是基数排序使用的进制数,d 是每个元素最多的有效位数。
空间复杂度:O(n + k)O(n+k),其中 nn 是数组的长度,kk 是基数排序使用的进制数。
桶排序的原理是将数组中的元素分到多个桶内,对每个桶内的元素分别排序,最后将每个桶内的有序元素合并,即可得到排序后的数组。
稳定性:在排序后,若相等的数值的相对次序不改变,则该算法视为稳定的。
不具有稳定性的排序:选择,快速,堆
具有稳定性的排序:冒泡,插入,归并,一切桶排序思想下的排序
选择排序:无法做到稳定(最小值和前面的值交换的时候会把原先排在前面的挪到后面);
冒泡排序:可做到稳定的,当遇到相等的数值时不交换;
插入排序:可做到稳定的,当遇到相等的数值时不交换;
归并排序:可做到稳定的,当遇到相等的数值先拷贝左边数组中的值(小和问题中遇到相等值先拷贝右边的,丧失了稳定性);
快速排序:无法做到稳定(partition时<=pivot的数会和<=区的下一个数交换,会将原先排在前面的挪到后面);
堆排序:无法做到稳定(堆结构无法保持稳定。如[5,4,4,6])在heapInsert [6]时,6就会和第一个4交换);
桶排序思想(不基于比较):都是稳定的,均保持先入桶先出桶
时间复杂度 | 空间复杂度 | 稳定性 | |
---|---|---|---|
选择排序 | O ( N 2 ) O(N^2) O(N2) | O ( 1 ) O(1) O(1) | 不稳定 |
冒泡排序 | O ( N 2 ) O(N^2) O(N2) | O ( 1 ) O(1) O(1) | 稳定 |
插入排序 | O ( N 2 ) O(N^2) O(N2) | O ( 1 ) O(1) O(1) | 稳定 |
归并排序 | O ( N log N ) O(N\log N) O(NlogN) | O ( N ) O(N) O(N) | 稳定 |
快速排序(3.0) | O ( N log N ) O(N\log N) O(NlogN) | O ( log N ) O(\log N) O(logN) | 不稳定 |
堆排序 | O ( N log N ) O(N\log N) O(NlogN) | O ( 1 ) O(1) O(1) | 不稳定 |
public void partition(int[] nums, int L, int R) {
if (L >= R) {
return;
}
//nums[L,R] 小样本
if (R - L < 60) {
// 在nums[L,R] 插入排序
return;
}
int pivot = (int) (Math.random() * (R-L+1) + L);
int[] equalArea = sortColors(nums, nums[pivot], L, R);
partition(nums, L, equalArea[0]-1);
partition(nums, equalArea[1]+1, R);
}
reference:
堆排序中 i 位置的节点的子节点位置为 2i+1, 2i+2, 父节点为 (i-1) / 2
第二课:荷兰国旗问题,快速排序,堆排序,排序算法的稳定性,桶排序
堆排序中建堆过程时间复杂度O(n)怎么来的? - SCVTheDefect的回答 - 知乎
leetcode912-解题-stormsunshine