diff --git a/content/posts/2024/01/0104.md b/content/posts/2024/01/0104.md index ac7c360..6ea2226 100644 --- a/content/posts/2024/01/0104.md +++ b/content/posts/2024/01/0104.md @@ -305,20 +305,97 @@ private static int[] partition(int[] arr, int l, int r) { 堆就是一组数字从左往右逐个填满二叉树,这就是满二叉树,也就是堆了。特殊的堆是大/小根堆,也叫优先队列。比如 React 的底层就用了小根堆,java 中的 PriorityQueue 默认也是小根堆,可以传入比较器来控制。 +### 节点关系 + +- 左子节点:2\*i + 1 +- 右子节点:2\*i + 2 +- 父节点:(i - 1)/2,注意:java 中可以这么做因为 java 中 -1 / 2 == 0;js 中就可以使用绝对值和位运算来简化操作。 + ### 上浮和下沉 上浮就是当数字一个一个进入堆中,从最后往上走,构建出大/小根堆。 ```java +/** + * 上浮操作:一组数,逐个插入堆中,形成大/小根堆,时间复杂度O(logn) + */ +public static void shiftUp(int[] arr, int index) { + while (arr[index] > arr[(index - 1) / 2]) { // 子大于父 就交换 (大根堆) + swap(arr, index, (index - 1) / 2); + index = (index - 1) / 2; + } +} +/** + * 建堆,注意时间复杂度是 O(nlogn) + */ +int[] data = new int[]{3, 1, 2, 3, 8, 6, 6, 4, 9, 3, 7}; +for (int i = 0; i < data.length; i++) { + shiftUp(data, i); +} ``` 下沉一般是在堆顶出堆后(与最后一个交换), ```java +/** + * @param arr + * @param index + * @param heapSize,传size是为了方便当出堆的时候,重新建堆 + */ +public static void shiftDown(int[] arr, int index, int heapSize) { + int left = 2 * index + 1; + while (left < heapSize) { // 证明存在子节点 + int largest = left + 1 < heapSize && 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 = 2 * index + 1; + } +} +``` +注意:一个数一个数加入堆中上浮的方式建堆的时间复杂度是 O(nlogn),一般我们也可以直接对 n/2 的数据进行下沉操作来进行优化,整体时间复杂度是 `O(n)`。 + +```java +int[] data = new int[]{3, 1, 2, 3, 8, 6, 6, 4, 9, 3, 7}; +for (int i = data.length / 2; i >= 0; --i) { + shiftDown(data, i, data.length); +} ``` +时间复杂度证明: + +- T(n) = n/2 \* 1 + n/4 \* 2 + n/8 \* 3 + ... +- 2T(n) = n/2 \* 2 + n/2 \* 2 + n/4 \* 3 + ... + +错位相减: `T(n) = n + n/2 + n/4 + n/8 + ...`, 等比数列求和公式:sn = an。所以时间复杂度是 O(n)。 + +### 堆排序实现 + +有了上浮和下沉的 api,堆排序就是堆顶不断出堆的过程。(堆顶与最后一个交换,同时减少 heap 的 size,从头部 shiftDown 重新建堆) + +```java +public static void heapSort(int[] arr) { + // 初始建堆 + for (int i = arr.length / 2; i >= 0; --i) { + shiftDown(arr, i, arr.length); + } + // 进行排序 + int heapSize = arr.length; + for (int i = arr.length - 1; i >= 0; --i) { + swap(arr, 0, i); + heapSize--; + shiftDown(arr, 0, heapSize); // 不断与最后一个数字切断连接,对0位置重新建堆; + } +} +``` + +> 几乎有序的数组排序,可以使用堆排序 + ## 稳定性 稳定性就是相同的数字在排序后仍然保持着相对的位置。这种特性还是比较重要的,比如对一个年级的学生,先按照成绩排序,再按照班级排序。 @@ -425,8 +502,73 @@ public static void countSort(int[] arr, int k) { ### 基数排序 -基数排序比基数排序的使用范围更加广一点,因为是根据每个进位来产生桶,最多也就 0-9 十个桶。 +基数排序比计数排序的使用范围更加广一点,因为是根据每个进位来产生桶,最多也就 0-9 十个桶。 + +相对于计数排序,根据进位,多次入桶。 ```java +public static void main(String[] args) { + // 这个数据的范围就是 0 ~ 13 + int[] data = new int[]{4, 5, 1, 8, 13, 0, 9, 200}; + int maxBit = maxBit(data); + radixSort(data, 0, data.length - 1, maxBit); + System.out.println(Arrays.toString(data)); +} +/** + * 基数排序 + */ +public static void radixSort(int[] arr, int l, int r, int maxBit) { + final int radix = 10; // 基数 0 ~ 9 一共10位 + int i = 0, j = 0; + int[] bucket = new int[r - l + 1]; // 桶的大小和原数组一样大小 + for (int d = 0; d < maxBit; d++) { // 对每个进行进行单独遍历入桶、出桶 + int[] count = new int[radix]; + // 进行基数统计 + for (i = l; i <= r; i++) { + j = getDigit(arr[i], d); + count[j]++; + } + // 求前缀和 + for (i = 1; i < count.length; i++) { + count[i] += count[i - 1]; + } + // 从右向左遍历原数组,根据前缀和找到它对应的位置,入桶 + for (i = r; i >= l; --i) { + j = getDigit(arr[i], d); + bucket[count[j] - 1] = arr[i]; + count[j]--; + } + // 出桶还原到原数组 + for (i = l, j = 0; i <= r; i++, j++) { + arr[i] = bucket[j]; + } + } +} +/** + * 找到最大数有多少位 + */ +public static int maxBit(int[] arr) { + int max = Integer.MIN_VALUE; + for (int digit : arr) { + max = Math.max(max, digit); + } + int bit = 0; + while (max != 0) { + bit++; + max = max / 10; + } + return bit; +} + +/** + * 取出数x进位d上的数字 + * + * @param x; + * @param d + * @return + */ +public static int getDigit(int x, int d) { + return (x / (int) Math.pow(10, d)) % 10; +} ``` diff --git "a/content/posts/algorithm/\345\277\205\351\241\273\346\216\214\346\217\241\347\232\204\346\216\222\345\272\217\347\256\227\346\263\225.md" "b/content/posts/algorithm/\345\277\205\351\241\273\346\216\214\346\217\241\347\232\204\346\216\222\345\272\217\347\256\227\346\263\225.md" index 844c7b4..c09646a 100644 --- "a/content/posts/algorithm/\345\277\205\351\241\273\346\216\214\346\217\241\347\232\204\346\216\222\345\272\217\347\256\227\346\263\225.md" +++ "b/content/posts/algorithm/\345\277\205\351\241\273\346\216\214\346\217\241\347\232\204\346\216\222\345\272\217\347\256\227\346\263\225.md" @@ -249,6 +249,29 @@ function heapify(arr, i, size) { } ``` +2024/01/08 更新:最好还是不要用递归的方式下沉吧 + +```java +/** + * @param arr + * @param index + * @param heapSize,传size是为了方便当出堆的时候,重新建堆 + */ +public static void shiftDown(int[] arr, int index, int heapSize) { + int left = 2 * index + 1; + while (left < heapSize) { // 证明存在子节点 + int largest = left + 1 < heapSize && 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 = 2 * index + 1; + } +} +``` + ## 非比较排序 非比较排序的主要思想就是利用空间换时间,因此一般都会有使用前提条件。