# 堆简介
- 堆结构就是用数组实现的完全二叉树结构
- 完全二叉树中如果每棵子树的最大值都在顶部就是大根堆
- 完全二叉树中如果每棵子树的最小值都在顶部就是小跟堆
- 堆结构的 heapInsert 与 heapify 操作
- 堆结构的增大和减少
- 优先级队列结构,就是堆结构
import java.util.Arrays; | |
/** | |
* @Author: LightRain | |
* @Description: heapInsert 操作 | |
* @DateTime: 2023-05-06 16:28 | |
* @Version:1.0 | |
**/ | |
public class HeapSorting { | |
/** | |
* @Author: LightRain | |
* @Date: 2023/5/6 16:47 | |
* @Param: [arr, index] | |
* @Return: void | |
* @Description: 某个数现在处在 index 位置,往上继续移动 | |
* @since 17 | |
*/ | |
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; | |
} | |
} | |
/** | |
* @Author: LightRain | |
* @Date: 5/5/2023 下午 6:38 | |
* @Param: [arr, i, j] | |
* @Return: void | |
* @Description: 交换 | |
* @since 17 | |
*/ | |
private static void swap(int[] arr, int i, int j) { | |
int tmp = arr[i]; | |
arr[i] = arr[j]; | |
arr[j] = tmp; | |
} | |
} |
import java.util.Arrays; | |
/** | |
* @Author: LightRain | |
* @Description: heapify 操作 | |
* @DateTime: 2023-05-06 16:28 | |
* @Version:1.0 | |
**/ | |
public class HeapSorting { | |
/** | |
* @Author: LightRain | |
* @Date: 2023/5/6 16:48 | |
* @Param: [arr, index, hespSize] | |
* @Return: void | |
* @Description: 某个数在 index 位置,能否往下移动 | |
* @since 17 | |
*/ | |
public static void heapify(int[] arr, int index, int hespSize) { | |
// 左孩子的下标 | |
int left = index * 2 + 1; | |
// 下方还有孩子的时候进入 while 循环 | |
while (left < hespSize) { | |
// 两个孩子中,谁的值大,把下标给 largest | |
int largest = left + 1 < hespSize && arr[left + 1] > arr[left] ? left + 1 : left; | |
// 父和较大的孩子之间,谁的值大,把下标给 largest | |
largest = arr[largest] > arr[index] ? largest : index; | |
if (largest == index) { | |
break; | |
} | |
swap(arr, largest, index); | |
index = largest; | |
left = index * 2 + 1; | |
} | |
} | |
/** | |
* @Author: LightRain | |
* @Date: 5/5/2023 下午 6:38 | |
* @Param: [arr, i, j] | |
* @Return: void | |
* @Description: 交换 | |
* @since 17 | |
*/ | |
private static void swap(int[] arr, int i, int j) { | |
int tmp = arr[i]; | |
arr[i] = arr[j]; | |
arr[j] = tmp; | |
} | |
} |
# 堆排序
import java.util.Arrays; | |
/** | |
* @Author: LightRain | |
* @Description: 堆排序时间复杂度:O (N*logN) 空间复杂度:O (1) | |
* @DateTime: 2023-05-06 16:28 | |
* @Version:1.0 | |
**/ | |
public class HeapSorting { | |
public static void main(String[] args) { | |
long startTime = System.currentTimeMillis(); | |
int[] arr = new int[]{90, 61, 92, 31, 78, 59}; | |
heapSort(arr); | |
System.out.println("Arrays.stream(arr).toArray() = " + Arrays.toString(Arrays.stream(arr).toArray())); | |
long endTime = System.currentTimeMillis(); | |
System.out.println("程序运行时间: " + (endTime - startTime) + "ms"); | |
} | |
public static void heapSort(int[] arr) { | |
if (arr == null || arr.length < 2) { | |
return; | |
} | |
//O(N) | |
// for (int i = 0; i < arr.length; i++) { | |
// heapInsert(arr, i); | |
// } | |
// 将数组变成大根堆 O (N) | |
for (int i = arr.length - 1; i >= 0; i--) { | |
heapify(arr, i, arr.length); | |
} | |
//O (N*logN) 的时间复杂度 空间复杂度为 O (1) | |
int heapSize = arr.length; | |
swap(arr, 0, --heapSize); | |
//O(N) | |
while (heapSize > 0) { | |
//O(logN) | |
heapify(arr, 0, heapSize); | |
swap(arr, 0, --heapSize); | |
} | |
} | |
/** | |
* @Author: LightRain | |
* @Date: 2023/5/6 16:47 | |
* @Param: [arr, index] | |
* @Return: void | |
* @Description: 某个数现在处在 index 位置,往上继续移动 | |
* @since 17 | |
*/ | |
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; | |
} | |
} | |
/** | |
* @Author: LightRain | |
* @Date: 2023/5/6 16:48 | |
* @Param: [arr, index, hespSize] | |
* @Return: void | |
* @Description: 某个数在 index 位置,能否往下移动 | |
* @since 17 | |
*/ | |
public static void heapify(int[] arr, int index, int hespSize) { | |
// 左孩子的下标 | |
int left = index * 2 + 1; | |
// 下方还有孩子的时候进入 while 循环 | |
while (left < hespSize) { | |
// 两个孩子中,谁的值大,把下标给 largest | |
int largest = left + 1 < hespSize && arr[left + 1] > arr[left] ? left + 1 : left; | |
// 父和较大的孩子之间,谁的值大,把下标给 largest | |
largest = arr[largest] > arr[index] ? largest : index; | |
if (largest == index) { | |
break; | |
} | |
swap(arr, largest, index); | |
index = largest; | |
left = index * 2 + 1; | |
} | |
} | |
/** | |
* @Author: LightRain | |
* @Date: 5/5/2023 下午 6:38 | |
* @Param: [arr, i, j] | |
* @Return: void | |
* @Description: 交换 | |
* @since 17 | |
*/ | |
private static void swap(int[] arr, int i, int j) { | |
int tmp = arr[i]; | |
arr[i] = arr[j]; | |
arr[j] = tmp; | |
} | |
} |
- 数组扩容并不会影响性能:O (N*logN)=O (N) 水平
- 使用系统自带堆结构不能高效自定义需求,需要自己手写堆结构完成自定义需求
- 有些情况下是必须手写堆结构才可以做到高效的
- 如果只需要给定一个数字然后弹出,可以使用语言自带堆结构 API 完成
# 堆排序扩展题目
- 已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过 K,并且 K 相对于数组来说比较小。
- 请选择一个合适的排序算法针对这个数据进行排序。
import java.util.Arrays; | |
import java.util.PriorityQueue; | |
/** | |
* @Author: LightRain | |
* @Description: 小根堆排序 | |
* @DateTime: 2023-05-06 17:47 | |
* @Version:1.0 | |
**/ | |
public class SmallRootHeap { | |
public static void main(String[] args) { | |
long startTime = System.currentTimeMillis(); | |
int[] arr = new int[]{90, 61, 92, 31, 78, 59}; | |
sortedArrDistanceLessk(arr,3); | |
System.out.println("Arrays.stream(arr).toArray() = " + Arrays.toString(Arrays.stream(arr).toArray())); | |
long endTime = System.currentTimeMillis(); | |
System.out.println("程序运行时间: " + (endTime - startTime) + "ms"); | |
} | |
public static void sortedArrDistanceLessk(int[] arr, int k) { | |
// 默认小根堆 | |
PriorityQueue<Integer> heap = new PriorityQueue<>(); | |
int index = 0; | |
// 将前几个数放入到小根堆内 | |
for (; index <= Math.min(arr.length, k); index++) { | |
heap.add(arr[index]); | |
} | |
int i = 0; | |
for (; index < arr.length; i++, index++) { | |
// 新加一个数放入小根堆内 | |
heap.add(arr[index]); | |
// 每次弹出一个数放到 arr [i] 位置 | |
arr[i] = heap.poll(); | |
} | |
while (!heap.isEmpty()){ | |
// 将每个数组弹出放入 arr [i++] | |
arr[i++]= heap.poll(); | |
} | |
} | |
} |
# 比较器
- 比较器的实质就是重载比较运算符。
- 比较器可以很好的应用在特殊标准的排序上。
- 比较器可以很好的应用在根据特殊标准排序的结构上。
import java.util.Comparator; | |
/** | |
* @Author: LightRain | |
* @Description: 自定义比较器的实现 | |
* @DateTime: 2023-05-09 13:11 | |
* @Version:1.0 | |
**/ | |
public class Student { | |
public String name; | |
public int id; | |
public int age; | |
public Student(String name, int id, int age) { | |
this.name = name; | |
this.id = id; | |
this.age = age; | |
} | |
/** | |
* @Author: LightRain | |
* @Description: 自定义比较器需要实现 Comparator<?> 接口并且重写 compare 方法来实现自定义 | |
* @DateTime: 2023-05-09 13:11 | |
* @Version:1.0 | |
**/ | |
public static class IdAscendingComparator implements Comparator<Student> { | |
// 返回负数的时候,第一个参数排在前面 | |
// 返回正数的时候,第二个参数排在前面 | |
// 返回 0 的时候,谁在前面无所谓 | |
@Override | |
public int compare(Student o1, Student o2) { | |
return o1.id - o2.id; | |
} | |
} | |
@Override | |
public String toString() { | |
return "Student{" + | |
"name='" + name + '\'' + | |
", id=" + id + | |
", age=" + age + | |
'}'; | |
} | |
} |
import java.util.Arrays; | |
/** | |
* @Author: LightRain | |
* @Description: 自定义比较器的使用 | |
* @DateTime: 2023-05-09 13:33 | |
* @Version:1.0 | |
**/ | |
public class Main { | |
public static void main(String[] args) { | |
// 创建对象 根据 id 升序排列 | |
Student s1 = new Student("A", 2, 20); | |
Student s2 = new Student("B", 3, 21); | |
Student s3 = new Student("C", 1, 22); | |
// 将对象添加到一个数组中 | |
Student[] students = {s1, s2, s3}; | |
// 进行数组排序并添加排序策略 | |
Arrays.sort(students, new Student.IdAscendingComparator()); | |
System.out.println("students = " + Arrays.toString(students)); | |
} | |
} |
# 桶排序思想下的排序
# 计数排序代码
import java.util.Arrays; | |
/** | |
* 计数排序 | |
* 中间数组 通过数组下标来表示原始数组的值,来统计每个元素出现的次数 | |
* 然后新建一个数组将 中间数组 出现了几次,我就打印几次 到新的数组 | |
* <p> | |
* 计数排序自己的理解: | |
* 就是将原始数组中的数值出现的频率(次数)记录在新数组下标中, | |
* 然后通过遍历循环这个新数组赋值给另一个新数组 | |
* | |
* 时间复杂度 | |
* 从代码看,第一个 for 循环时间复杂度是 O (k), 第二个是 O (n), 第三个是 O (k), 第四个是 O (n),所以总的是 O (k+n), 特别当 n==k 的时候,时间复杂度是 O (n)。 | |
* 计数排序不需要比较操作,也不需要交换操作,是一种简单的排序方式,但是这是一种空间换时间的排序方式,类似的空间换时间的排序还有桶排序等。 | |
* 特别的当 O (k)>=O (nlogn) 的时候,计数排序就不那么有效了。 | |
*/ | |
public class CountingSort { | |
public static void main(String[] args) { | |
int[] arr= new int[]{90, 61,92,31,78,59}; | |
int[] jspx = jspx(arr); | |
System.out.println("Arrays.stream(arr).toArray() = " + Arrays.toString(Arrays.stream(jspx).toArray())); | |
} | |
public static int[] jspx(int[] a) { | |
// 一:求取最大值和最小值,计算中间数组的长度:中间数组是用来记录原始数据中每个值出现的频率 | |
int max = a[0], min = a[0]; | |
for (int i : a) { | |
if (i > max) { | |
max = i; | |
} | |
if (i < min) { | |
min = i; | |
} | |
} | |
// 二:有了最大值和最小值能够确定中间数组的长度 | |
// 存储 5-0+1 = 6 | |
int[] pxA = new int[max - min + 1]; | |
// 三。循环遍历旧数组计数排序:就是统计原始数组值出现的频率到中间数组 B 中 | |
for (int i : a) { | |
// 数的位置 上 + 1 | |
pxA[i - min] += 1; | |
} | |
// 四。遍历输出 | |
// 创建最终数组,就是返回的数组,和原始数组长度相等,但是排序完成的 | |
int[] result = new int[a.length]; | |
// 记录最终数组的下标 | |
int index = 0; | |
// 先循环每一个元素 在计数排序器的下标中 | |
for (int i = 0; i < pxA.length; i++) { | |
// 循环出现的次数,pxA [i]: 这个数出现的频率 | |
for (int j = 0; j < pxA[i]; j++) { | |
// 以为原来减少了 min 现在加上 min,值就变成了原来的值 | |
result[index++] = i + min; | |
} | |
} | |
return result; | |
} | |
} |
# 基数排序代码
import java.util.Arrays; | |
/** | |
* @Author: LightRain | |
* @Description: 基数排序 | |
* @DateTime: 2023-05-09 14:09 | |
* @Version:1.0 | |
**/ | |
public class RadixSort { | |
public static void main(String[] args) { | |
long startTime = System.currentTimeMillis(); | |
int[] arr = new int[]{90, 61, 92, 31, 78, 59}; | |
radixSort(arr); | |
System.out.println("Arrays.stream(arr).toArray() = " + Arrays.toString(Arrays.stream(arr).toArray())); | |
long endTime = System.currentTimeMillis(); | |
System.out.println("程序运行时间: " + (endTime - startTime) + "ms"); | |
} | |
public static void radixSort(int[] arr) { | |
if (arr == null || arr.length < 2) { | |
return; | |
} | |
radixSort(arr, 0, arr.length - 1, maxbits(arr)); | |
} | |
/** | |
* @Author: LightRain | |
* @Date: 2023/5/9 14:13 | |
* @Param: [arr] | |
* @Return: void | |
* @Description: 求最大位值有几个十进制位 | |
* @since 17 | |
*/ | |
public static int maxbits(int[] arr) { | |
int max = Integer.MIN_VALUE; | |
for (int i : arr) { | |
max = Math.max(max, i); | |
} | |
int res = 0; | |
while (max != 0) { | |
res++; | |
max /= 10; | |
} | |
return res; | |
} | |
/** | |
* @Author: LightRain | |
* @Date: 2023/5/9 14:23 | |
* @Param: [arr, l, r, digit] | |
* @Return: void | |
* @Description: arr [begin...end] 排序 | |
* digit 表示这个数组中最大的值有几个十进制位 | |
* @since 17 | |
*/ | |
public static void radixSort(int[] arr, int l, int r, int digit) { | |
// 固定长度 | |
final int radix = 10; | |
int i = 0; | |
int j = 0; | |
// 有多少个数准备多少个辅助空间 | |
int[] bucket = new int[r - l + 1]; | |
// 有多少位就进出几次,有多少个十进制位就要进行多次进桶和出桶操作 | |
for (int d = 1; d <= digit; d++) { | |
//10 个空间 | |
//count [0] 当前为 (d 位) 是 0 的数字有多少个 | |
//count [1] 当前为 (d 位) 是 (0 和 1) 的数字有多少个 | |
//count [2] 当前为 (d 位) 是 (0、1 和 2) 的数字有多少个 | |
//count [i] 当前为 (d 位) 是 (0~i) 的数字有多少个 | |
//count[0...9] | |
int[] count = new int[radix]; | |
for (i = l; i <= r; i++) { | |
//d 如果为 1 就去取出个位数字 | |
//d 如果为 2 就去取出十位数字 | |
//d 如果为 3 就去取出呗百位数字 | |
j = getDigit(arr[i], d); | |
count[j]++; | |
} | |
for (i = 1; i < radix; i++) { | |
// 把 count 处理成前缀和,累加和 | |
count[i] = count[i] + count[i - 1]; | |
} | |
// 数组从右往左遍历 | |
for (i = r; i >= l; i--) { | |
// 每个数根据你现在到了第几位,把那个位的数字取出来 | |
j = getDigit(arr[i], d); | |
// 根据 count 值 - 1,添加到辅助数组里面 | |
bucket[count[j] - 1] = arr[i]; | |
// 单个个位的瓷频 -- | |
count[j]--; | |
} | |
for (i = l, j = 0; i <= r; i++, j++) { | |
// 将辅助空间数据导入到源数组中 | |
arr[i] = bucket[j]; | |
} | |
} | |
} | |
/** | |
* @Author: LightRain | |
* @Date: 2023/5/9 14:40 | |
* @Param: [x, d] | |
* @Return: int | |
* @Description: 获取数字 | |
* @since 17 | |
*/ | |
public static int getDigit(int x, int d) { | |
return ((x / ((int) Math.pow(10, d - 1))) % 10); | |
} | |
} |
# 排序算法的稳定性及其汇总
- 同样值的个体之间,如果不因为排序改变相对次序,就是这个排序是有稳定性的,否则就没有。
- 不具备稳定性的排序:
- 选择排序
- 快速排序
- 堆排序
- 具备稳定性的排序:
- 冒泡排序
- 插入排序
- 归并排序
- 一切桶排序思想下的排序
- 目前没有找到时间复杂度 O (N*logN), 额外空间复杂度 O (1), 又稳定的排序。
排序算法 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|
选择排序 | O(N²) | O(1) | × |
冒泡排序 | O(N²) | O(1) | √ |
插入排序 | O(N²) | O(1) | √ |
归并排序 | O(N*logN) | O(N) | √ |
快速排序 (随机) | O(N*logN) | O(logN) | × |
堆排序 | O(N*logN) | O(1) | × |
- 如果有空间的限制就使用堆排序。
- 需要稳定性的时候使用归并排序。
- 一般选择快速排序,经过常数实验快速排序是最快的排序,能用快排就用快排。
# 常见的坑
- 归并排序的额外空间复杂度可以变成 O (1), 但是非常难,不需要掌握,有兴趣可以搜‘归并排序’内部缓存法,但是会丧失稳定性。
- “原地归并排序” 的帖子都是垃圾,会让归并排序的时间复杂度变成 O (N²)。
- 快速排序可以做到稳定性问题,但是非常难,不需要掌握,可以搜 “01 stable sort”。
- 所有的改进都不重要,因为目前没有找到时间复杂度 O (N*logN),额外复杂度 O (1),又稳定的排序。
- 有一道题目,是奇数放在数组左边,偶数放在数组右边,还有求原始的相对次序不变,碰到这个问题可以怼面试官。
# 工程上对排序的改进
- 充分利用 O (N*logN) 和 O (N²) 排序各自的优势。
- 稳定性的考虑。