Skip to content

Commit

Permalink
0104 supplement
Browse files Browse the repository at this point in the history
  • Loading branch information
yk committed Jan 8, 2024
1 parent 743eb36 commit 9b1df07
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 1 deletion.
144 changes: 143 additions & 1 deletion content/posts/2024/01/0104.md
Original file line number Diff line number Diff line change
Expand Up @@ -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位置重新建堆;
}
}
```

> 几乎有序的数组排序,可以使用堆排序
## 稳定性

稳定性就是相同的数字在排序后仍然保持着相对的位置。这种特性还是比较重要的,比如对一个年级的学生,先按照成绩排序,再按照班级排序。
Expand Down Expand Up @@ -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;
}
```
23 changes: 23 additions & 0 deletions content/posts/algorithm/必须掌握的排序算法.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
```

## 非比较排序

非比较排序的主要思想就是利用空间换时间,因此一般都会有使用前提条件。
Expand Down

0 comments on commit 9b1df07

Please sign in to comment.