持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第三天,点击查看活动详情
最近在看左神的数据结构与算法,考虑到视频讲的内容和给出的资料的PDF有些出入,不方便去复习,打算出一个左神的数据结构与算法的笔记系列供大家复习,同时也可以加深自己对于这些知识的掌握,该系列以视频的集数为分隔,今天是第二篇:P5|详解桶排序以及排序内容大总结
一、快排的空间复杂度
快排的空间复杂度与时间复杂度一样,也是一个概率累加,求出的一个长期数学期望值
1、递归算法的空间复杂度
左神在课程中并没有提到空间复杂度的计算方法,因此我去搜了一下递归算法的空间复杂度的计算规则 对于递归算法,由于运行时有附加堆栈,比如: 存在递归算法 func() 第一次传参5 即执行func(5) 在函数中递归调用 func(4) 然后以此类推 func(3)、func(2)、func(1)
- 这就是递归算法在递归的时候进行压栈而导致的空间复杂度
- 这里给出空间复杂度的计算公式:递归的深度 × 每次压栈所需要的空间个数
- 递归的深度:递归的深度是指栈最大的那次,也就是栈的最大大小。
- 比如现有一个 Stack,进行操作 push(1) push(2) pop(2) push(3),栈最大深度是2,栈的深度就是2
- 每次压栈需要的空间个数直接计算即可
2、分析
1、整体分析
- 其实不难发现,快排的所有元素都是要压到栈中去的,直到全部压进去才会使用返回结果进行逐步弹栈
- 因此我们可以得到:递归的深度为 N,因为所有的元素都压栈进去了,然后开始弹栈,最大深度就是开始弹栈的前一步
- 而每次压栈所需要的空间个数就会分情况,下面分析了两种极端的情况
2、最差的情况
- 先有一个数组 1,2,3,4,5,6,7,8,9
- 那么如果每次做
partition以最后那个数做partition,所需要的空间个数就是 N-1 - 因此不难得出 最差的情况的空间复杂度为 O(N²)
3、最好的情况
- 还是同样的数组,但是如果每次选的值都是中间的值,那么可以正好把数据二分,左边的数据量和右边的数据量一致,所需要的空间个数就是 logN
4、结论
- 还是与时间复杂度一样,快排的空间复杂度使用概率论相关知识求长期数学期望,得到最终的结论为:快排的空间复杂度为O(N * logN)
二、堆
1、堆结构
其实堆结构就是用数组实现的一个完全二叉树,即在逻辑上是一个完全二叉树 完全二叉树:所有的节点要么没有子节点、要么只有左孩子、或者有左孩子和右孩子,总之就是不能只有右孩子
- 补充一些树的基础知识:已知父节点的索引值为 i
- 左孩子的索引值为
2i + 1 - 右孩子的索引值为
2i + 2 - 父节点的索引值为
(i - 1) / 2
- 左孩子的索引值为
2、分类
堆结构可以分为大根堆和小根堆: 1.大根堆:任何一个结点的值都比子节点的值要大 2.小根堆:任何一个结点的值都比子节点的值要小
3、HeapInsert
堆插入思路
- 在插入后通过索引值的计算得到
(i - 1) / 2的父节点的索引值 - 与父节点的值做比较,如果比父节点的值大,就通过索引去交换二者的值
- 然后继续比较替换之后的位置的父节点的值,以此类推,一直到交换不了为止
举例
- 先有一个堆:7,7,5,3,6 结构如下:
- 如图我们插入一个 8,heapSize++
- 8 的索引值为 heapSize - 1 = 5 父节点的索引值为 2 对应的值为 5
- 比较后发现 8 比 5 大,就把索引值为 5 的和索引值为 2 的值做交换
- 然后继续比较索引值为 0 的 7 发现还是比 7 大,继续交换
- 此时 8 就到达了根节点,在计算不出父节点的时候就会退出循环
4、Heapify
假设现在要把堆中某个数换成一个未知的数,还要保证原本的大根堆的结构,此时就要分两种情况
情况一:换之后的数比换之前的数大
- 因为换之后的数更大,因此对于子节点,肯定还是满足条件的
- 只需要向上做比较,类似于 heapInsert 的过程
情况二:换之后的数比换之前的数小
- 因为换之后的数更小,因此对于父节点,肯定还是满足条件的
- 此时就需要向下做比较,也就是这里的 heapify
Heapify 实现思路
- 首先利用当前位置的索引得出左孩子和右孩子的索引值
- 然后得到左孩子和右孩子的索引对应的值,取出两个孩子的值的最大值
- 把换之后的值与最大值做比较,如果比最大值大,那么结构就不需要变
- 而如果最大值要大,那就交换二者的值,然后继续向下做 heapify
三、堆排序
1、代码实现
public static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
int size = arr.length;
swap(arr, 0, --size);
while (size > 0) {
heapify(arr, 0, size);
swap(arr, 0, --size);
}
}
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 ;
}
}
public static void heapify(int[] arr, int index, int size) {
int left = index * 2 + 1;
while (left < size) {
int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left;
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) {
break;
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
2、实现思路
- 首先遍历数组使用
heapInsert插入堆中,按照大根堆或者小根堆排序 - 然后弹出根节点,
heapSize --,把堆中的最后一个元素放在根节点,做heapify - 一直循环到
heapSize == 0
3、举例
- 数组
「1,2,4,532,623,4,7,5」 - 首先遍历数组使用 heapInsert,得到一个大根堆
「623,532,7,1,4,2,4」 - 然后弹出根节点 623 把尾结点 4 放在根节点做 heapify
- 得到大根堆,然后弹出新的大根堆的根节点 532,以此类推
四、计数排序
前面的排序基本上都是基于比较的排序,这里的计数排序和基数排序并不基于比较,这种比较方式也有它的缺陷
1、实现思路
- 先如果要对员工的年龄做排序,首先我们可以知道年龄肯定是在 0-200 区间的,我们就可以维护一个数组长度为201的数组
- 然后有多少岁的员工,我们就在索引为哪个值上加1
- 比如说员工年龄为:23,24,25,23,25,26,24,23
- 那么就有
array[23]++ array[24]++ array[25]++ array[23]++
array[25]++ array[26]++ array[24]++ array[23]++
- 就能得到 array[23]=3 array[24]=2 array[25]=2 array[26]=1
- 即有3个23岁的,两个24岁的,两个25岁的,一个26岁的
- 排序就是遍历数组然后输出:23,23,23,24,24,25,25,26
2、缺陷
- 如果此时不是对员工的年龄做排序,负的范围为
-2^32 + 1正的范围为2^32 - 1这个范围,再去用计数排序,很明显就太耗时了,不合适了 - 不基于比较的排序,通常是根据数据状况而设计的排序,没有基于比较的排序适用范围广
五、基数排序
1、实现思路
- 首先找到最大数的位数,有多少位就进行多少次循环进桶和出桶
- 然后遍历数组依次进“桶”,此处的“桶”是队列,现进先出,进桶的规则是看某一位上的数字,第一次就是看个位上的数字,依次进各自的桶
- 所有数都进桶后,桶从左到右进行遍历,依次出桶,按照先进先出的规则出桶
- 然后十位上依次进桶出桶,直到遍历完最大数的位数,此时得到的数组就排序好了
2、举例
- 对于数组
17,13,25,100,72首先得到最大数为100,即有三位 - 把其他的数补到与最大叔相同的位数:
017,013,025,100,072 - 然后按照个位数依次进桶:
- 017进7桶;013进3桶;025进5桶;100进0桶;072进2桶
- 然后从左到右倒出桶的数据:
100,072,013,025,017 - 然后按照十位数依次进桶:
- 100进0桶;072进7桶;013进1桶;025进2桶;017进1桶
- 然后从左到右倒出桶中的数据(注意:按照队列的结构,下面的先出):
100,013,017,025,072 - 然后按照百位数依次进桶:
- 100进1桶;013,017,025,072进0桶
- 然后从左到右倒出桶中数据:
013,017,025,072,100 - 排序结束
3、代码实现
public static void radixSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
radixSort(arr, 0, arr.length - 1, maxbits(arr));
}
public static int maxbits(int[] arr) {
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
max = Math.max(max, arr[i]);
}
int res = 0;
while (max != 0) {
res++;
max /= 10;
}
return res;
}
public static void radixSort(int[] arr, int begin, int end, int digit) {
final int radix = 10;
int i = 0, j = 0;
int[] bucket = new int[end - begin + 1];
for (int d = 1; d <= digit; d++) {
int[] count = new int[radix];
for (i = begin; i <= end; i++) {
j = getDigit(arr[i], d);
count[j]++;
}
for (i = 1; i < radix; i++) {
count[i] = count[i] + count[i - 1];
}
for (i = end; i >= begin; i--) {
j = getDigit(arr[i], d);
bucket[count[j] - 1] = arr[i];
count[j]--;
}
for (i = begin, j = 0; i <= end; i++, j++) {
arr[i] = bucket[j];
}
}
}
public static int getDigit(int x, int d) {
return ((x / ((int) Math.pow(10, d - 1))) % 10);
}