From 394ab75400e7fe5f011f5fc9fb9a6de9ded34214 Mon Sep 17 00:00:00 2001 From: ITCharge Date: 2023年8月24日 17:38:00 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=A0=86=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E6=A8=A1=E7=89=88=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Templates/01.Array/Array-MaxHeap.py | 83 +++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 Templates/01.Array/Array-MaxHeap.py diff --git a/Templates/01.Array/Array-MaxHeap.py b/Templates/01.Array/Array-MaxHeap.py new file mode 100644 index 00000000..c9c918d0 --- /dev/null +++ b/Templates/01.Array/Array-MaxHeap.py @@ -0,0 +1,83 @@ +class MaxHeap: + def __init__(self): + self.max_heap = [] + + def peek(self) -> int: + # 大顶堆为空 + if not self.max_heap: + return None + # 返回堆顶元素 + return self.max_heap[0] + + def push(self, val: int): + # 将新元素添加到堆的末尾 + self.max_heap.append(val) + + size = len(self.max_heap) + # 从新插入的元素节点开始,进行上移调整 + self.__shift_up(size - 1) + + def __shift_up(self, i: int): + while (i - 1) // 2>= 0 and self.max_heap[i]> self.max_heap[(i - 1) // 2]: + self.max_heap[i], self.max_heap[(i - 1) // 2] = self.max_heap[(i - 1) // 2], self.max_heap[i] + i = (i - 1) // 2 + + def pop(self) -> int: + # 堆为空 + if not self.max_heap: + raise IndexError("堆为空") + + size = len(self.max_heap) + self.max_heap[0], self.max_heap[size - 1] = self.max_heap[size - 1], self.max_heap[0] + # 删除堆顶元素 + val = self.max_heap.pop() + # 节点数减 1 + size -= 1 + + self.__shift_down(0, size) + + # 返回堆顶元素 + return val + + + def __shift_down(self, i: int, n: int): + while 2 * i + 1 < n: + # 左右子节点编号 + left, right = 2 * i + 1, 2 * i + 2 + + # 找出左右子节点中的较大值节点编号 + if 2 * i + 2>= n: + # 右子节点编号超出范围(只有左子节点 + larger = left + else: + # 左子节点、右子节点都存在 + if self.max_heap[left]>= self.max_heap[right]: + larger = left + else: + larger = right + + # 将当前节点值与其较大的子节点进行比较 + if self.max_heap[i] < self.max_heap[larger]: + # 如果当前节点值小于其较大的子节点,则将它们交换 + self.max_heap[i], self.max_heap[larger] = self.max_heap[larger], self.max_heap[i] + i = larger + else: + # 如果当前节点值大于等于于其较大的子节点,此时结束 + break + + +class Solution: + def maxHeapOperations(self): + max_heap = MaxHeap() + max_heap.push(3) + print(max_heap.peek()) + max_heap.push(2) + print(max_heap.peek()) + max_heap.push(4) + print(max_heap.peek()) + max_heap.pop() + print(max_heap.peek()) + + + +print(Solution().maxHeapOperations()) \ No newline at end of file From f431310870183afceafe298f749f84af897b1495 Mon Sep 17 00:00:00 2001 From: ITCharge Date: 2023年8月24日 17:38:08 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=A0=86=E6=8E=92?= =?UTF-8?q?=E5=BA=8F=E6=A8=A1=E7=89=88=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Templates/01.Array/Array-MaxHeapSort.py | 138 ++++++++++++++++------- Templates/01.Array/Array-MinHeapSort.py | 144 +++++++++++++++--------- 2 files changed, 188 insertions(+), 94 deletions(-) diff --git a/Templates/01.Array/Array-MaxHeapSort.py b/Templates/01.Array/Array-MaxHeapSort.py index 1e935777..fc0052de 100644 --- a/Templates/01.Array/Array-MaxHeapSort.py +++ b/Templates/01.Array/Array-MaxHeapSort.py @@ -1,45 +1,99 @@ -class Solution: - # 调整为大顶堆 - def heapify(self, arr: [int], index: int, end: int): - # 根节点为 index,左节点为 2 * index + 1, 右节点为 2 * index + 2 - left = index * 2 + 1 - right = left + 1 - while left <= end: - # 当前节点为非叶子结点 - max_index = index - if arr[left]> arr[max_index]: - max_index = left - if right <= end and arr[right]> arr[max_index]: - max_index = right - if index == max_index: - # 如果不用交换,则说明已经交换结束 - break - arr[index], arr[max_index] = arr[max_index], arr[index] - # 继续调整子树 - index = max_index - left = index * 2 + 1 - right = left + 1 - - # 初始化大顶堆 - def buildMaxHeap(self, arr: [int]): - size = len(arr) - # (size - 2) // 2 是最后一个非叶节点,叶节点不用调整 - for i in range((size - 2) // 2, -1, -1): - self.heapify(arr, i, size - 1) - return arr +class MaxHeap: + def __init__(self): + self.max_heap = [] + + def peek(self) -> int: + # 大顶堆为空 + if not self.max_heap: + return None + # 返回堆顶元素 + return self.max_heap[0] + + def push(self, val: int): + # 将新元素添加到堆的末尾 + self.max_heap.append(val) + + size = len(self.max_heap) + # 从新插入的元素节点开始,进行上移调整 + self.__shift_up(size - 1) + + def __shift_up(self, i: int): + while (i - 1) // 2>= 0 and self.max_heap[i]> self.max_heap[(i - 1) // 2]: + self.max_heap[i], self.max_heap[(i - 1) // 2] = self.max_heap[(i - 1) // 2], self.max_heap[i] + i = (i - 1) // 2 + + def pop(self) -> int: + # 堆为空 + if not self.max_heap: + raise IndexError("堆为空") + + size = len(self.max_heap) + self.max_heap[0], self.max_heap[size - 1] = self.max_heap[size - 1], self.max_heap[0] + # 删除堆顶元素 + val = self.max_heap.pop() + # 节点数减 1 + size -= 1 + + self.__shift_down(0, size) + + # 返回堆顶元素 + return val - # 升序堆排序,思路如下: - # 1. 先建立大顶堆 - # 2. 让堆顶最大元素与最后一个交换,然后调整第一个元素到倒数第二个元素,这一步获取最大值 - # 3. 再交换堆顶元素与倒数第二个元素,然后调整第一个元素到倒数第三个元素,这一步获取第二大值 - # 4. 以此类推,直到最后一个元素交换之后完毕。 - def maxHeapSort(self, arr: [int]): - self.buildMaxHeap(arr) - size = len(arr) + + def __shift_down(self, i: int, n: int): + while 2 * i + 1 < n: + # 左右子节点编号 + left, right = 2 * i + 1, 2 * i + 2 + + # 找出左右子节点中的较大值节点编号 + if 2 * i + 2>= n: + # 右子节点编号超出范围(只有左子节点 + larger = left + else: + # 左子节点、右子节点都存在 + if self.max_heap[left]>= self.max_heap[right]: + larger = left + else: + larger = right + + # 将当前节点值与其较大的子节点进行比较 + if self.max_heap[i] < self.max_heap[larger]: + # 如果当前节点值小于其较大的子节点,则将它们交换 + self.max_heap[i], self.max_heap[larger] = self.max_heap[larger], self.max_heap[i] + i = larger + else: + # 如果当前节点值大于等于于其较大的子节点,此时结束 + break + + def __buildMaxHeap(self, nums: [int]): + size = len(nums) + # 先将数组 nums 的元素按顺序添加到 max_heap 中 for i in range(size): - arr[0], arr[size - i - 1] = arr[size - i - 1], arr[0] - self.heapify(arr, 0, size - i - 2) - return arr + self.max_heap.append(nums[i]) + + # 从最后一个非叶子节点开始,进行下移调整 + for i in range((size - 2) // 2, -1, -1): + self.__shift_down(i, size) - def sortArray(self, nums: List[int]) -> List[int]: - return self.maxHeapSort(nums) \ No newline at end of file + def maxHeapSort(self, nums: [int]) -> [int]: + # 根据数组 nums 建立初始堆 + self.__buildMaxHeap(nums) + + size = len(self.max_heap) + for i in range(size - 1, -1, -1): + # 交换根节点与当前堆的最后一个节点 + self.max_heap[0], self.max_heap[i] = self.max_heap[i], self.max_heap[0] + # 从根节点开始,对当前堆进行下移调整 + self.__shift_down(0, i) + + # 返回排序后的数组 + return self.max_heap + +class Solution: + def maxHeapSort(self, nums: [int]) -> [int]: + return MaxHeap().maxHeapSort(nums) + + def sortArray(self, nums: [int]) -> [int]: + return self.maxHeapSort(nums) + +print(Solution().sortArray([10, 25, 6, 8, 7, 1, 20, 23, 16, 19, 17, 3, 18, 14])) \ No newline at end of file diff --git a/Templates/01.Array/Array-MinHeapSort.py b/Templates/01.Array/Array-MinHeapSort.py index a8c9faa9..4ef5ed5e 100644 --- a/Templates/01.Array/Array-MinHeapSort.py +++ b/Templates/01.Array/Array-MinHeapSort.py @@ -1,59 +1,99 @@ -class Solution: - # 调整为小顶堆 - def heapify(self, nums: [int], index: int, end: int): - left = index * 2 + 1 - right = left + 1 - while left <= end: - # 当前节点为非叶子结点 - min_index = index - if nums[left] < nums[min_index]: - min_index = left - if right <= end and nums[right] < nums[min_index]: - min_index = right - if index == min_index: - # 如果不用交换,则说明已经交换结束 - break - nums[index], nums[min_index] = nums[min_index], nums[index] - # 继续调整子树 - index = min_index - left = index * 2 + 1 - right = left + 1 - +class MinHeap: + def __init__(self): + self.min_heap = [] + + def peek(self) -> int: + # 大顶堆为空 + if not self.min_heap: + return None + # 返回堆顶元素 + return self.min_heap[0] - # 初始化小顶堆 - def buildMinHeap(self, nums: [int]): - size = len(nums) - # (size - 2) // 2 是最后一个非叶节点,叶节点不用调整 - for i in range((size - 2) // 2, -1, -1): - self.heapify(nums, i, size - 1) - return nums + def push(self, val: int): + # 将新元素添加到堆的末尾 + self.min_heap.append(val) + + size = len(self.min_heap) + # 从新插入的元素节点开始,进行上移调整 + self.__shift_up(size - 1) + + def __shift_up(self, i: int): + while (i - 1) // 2>= 0 and self.min_heap[i] < self.min_heap[(i - 1) // 2]: + self.min_heap[i], self.min_heap[(i - 1) // 2] = self.min_heap[(i - 1) // 2], self.min_heap[i] + i = (i - 1) // 2 + + def pop(self) -> int: + # 堆为空 + if not self.min_heap: + raise IndexError("堆为空") + + size = len(self.min_heap) + self.min_heap[0], self.min_heap[size - 1] = self.min_heap[size - 1], self.min_heap[0] + # 删除堆顶元素 + val = self.min_heap.pop() + # 节点数减 1 + size -= 1 + + self.__shift_down(0, size) + + # 返回堆顶元素 + return val + + def __shift_down(self, i: int, n: int): + while 2 * i + 1 < n: + # 左右子节点编号 + left, right = 2 * i + 1, 2 * i + 2 + + # 找出左右子节点中的较大值节点编号 + if 2 * i + 2>= n: + # 右子节点编号超出范围(只有左子节点 + larger = left + else: + # 左子节点、右子节点都存在 + if self.min_heap[left] <= self.min_heap[right]: + larger = left + else: + larger = right + + # 将当前节点值与其较小的子节点进行比较 + if self.min_heap[i]> self.min_heap[larger]: + # 如果当前节点值小于其较大的子节点,则将它们交换 + self.min_heap[i], self.min_heap[larger] = self.min_heap[larger], self.min_heap[i] + i = larger + else: + # 如果当前节点值大于等于于其较大的子节点,此时结束 + break - # 升序堆排序,思路如下: - # 1. 先建立小顶堆 - # 2. 让堆顶最小元素与最后一个交换,然后调整第一个元素到倒数第二个元素,这一步获取最小值 - # 3. 再交换堆顶元素与倒数第二个元素,然后调整第一个元素到倒数第三个元素,这一步获取第二小值 - # 4. 以此类推,直到最后一个元素交换之后完毕。 - def minHeapSort(self, nums: [int]): - self.buildMinHeap(nums) + def __buildMinHeap(self, nums: [int]): size = len(nums) + # 先将数组 nums 的元素按顺序添加到 min_heap 中 for i in range(size): - nums[0], nums[size - i - 1] = nums[size - i - 1], nums[0] - self.heapify(nums, 0, size - i - 2) - return nums + self.min_heap.append(nums[i]) + + # 从最后一个非叶子节点开始,进行下移调整 + for i in range((size - 2) // 2, -1, -1): + self.__shift_down(i, size) + + def minHeapSort(self, nums: [int]) -> [int]: + # 根据数组 nums 建立初始堆 + self.__buildMinHeap(nums) + + size = len(self.min_heap) + for i in range(size - 1, -1, -1): + # 交换根节点与当前堆的最后一个节点 + self.min_heap[0], self.min_heap[i] = self.min_heap[i], self.min_heap[0] + # 从根节点开始,对当前堆进行下移调整 + self.__shift_down(0, i) + + # 返回排序后的数组 + return self.min_heap - # 升序堆排序,思路如下: - # 1. 先建立大顶堆 - # 2. 让堆顶最大元素与最后一个交换,然后调整第一个元素到倒数第二个元素,这一步获取最大值 - # 3. 再交换堆顶元素与倒数第二个元素,然后调整第一个元素到倒数第三个元素,这一步获取第二大值 - # 4. 以此类推,直到最后一个元素交换之后完毕。 - def maxHeapSort(self, arr: [int]): - self.buildMaxHeap(arr) - size = len(arr) - for i in range(size): - arr[0], arr[size - i - 1] = arr[size - i - 1], arr[0] - self.heapify(arr, 0, size - i - 2) - return arr +class Solution: + def minHeapSort(self, nums: [int]) -> [int]: + return MinHeap().minHeapSort(nums) + + def sortArray(self, nums: [int]) -> [int]: + return self.minHeapSort(nums) - def sortArray(self, nums: List[int]) -> List[int]: - return self.maxHeapSort(nums) \ No newline at end of file +print(Solution().sortArray([10, 25, 6, 8, 7, 1, 20, 23, 16, 19, 17, 3, 18, 14])) \ No newline at end of file From ae3d67223065d735f76ab274b4d12e6b14252479 Mon Sep 17 00:00:00 2001 From: ITCharge Date: 2023年8月24日 17:38:21 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E5=A0=86=E6=8E=92=E5=BA=8F=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../02.Array-Sort/07.Array-Heap-Sort.md | 283 ++++++++++++------ 1 file changed, 184 insertions(+), 99 deletions(-) diff --git a/Contents/01.Array/02.Array-Sort/07.Array-Heap-Sort.md b/Contents/01.Array/02.Array-Sort/07.Array-Heap-Sort.md index d6f7343a..6f69dc6d 100644 --- a/Contents/01.Array/02.Array-Sort/07.Array-Heap-Sort.md +++ b/Contents/01.Array/02.Array-Sort/07.Array-Heap-Sort.md @@ -1,133 +1,218 @@ -## 1. 堆排序算法思想 +## 1. 堆结构 -> **堆排序(Heap sort)基本思想**: -> -> 借用「堆结构」所设计的排序算法。将数组转化为大顶堆,重复从大顶堆中取出数值最大的节点,并让剩余的堆结构继续维持大顶堆性质。 +「堆排序(Heap sort)」是一种基于「堆结构」实现的高效排序算法。在介绍「堆排序」之前,我们先来了解一下什么是「堆结构」。 ### 1.1 堆的定义 -**堆(Heap)**:符合以下两个条件之一的完全二叉树: +> **堆(Heap)**:一种满足以下两个条件之一的完全二叉树: +> +> - **大顶堆(Max Heap)**:任意节点值 ≥ 其子节点值。 +> - **小顶堆(Min Heap)**:任意节点值 ≤ 其子节点值。 -- **大顶堆**:根节点值 ≥ 子节点值。 -- **小顶堆**:根节点值 ≤ 子节点值。 +![堆结构](https://qcdn.itcharge.cn/images/20230823133321.png) -### 1.2 堆排序算法步骤 +### 1.2 堆的存储结构 -1. **建立初始堆**:将无序序列构造成第 `1` 个大顶堆(初始堆),使得 `n` 个元素的最大值处于序列的第 `1` 个位置。 -2. **调整堆**:交换序列的第 `1` 个元素(最大值元素)与第 `n` 个元素的位置。将序列前 `n - 1` 个元素组成的子序列调整成一个新的大顶堆,使得 `n - 1` 个元素的最大值处于序列第 `1` 个位置,从而得到第 `2` 个最大值元素。 -3. **调整堆**:交换子序列的第 `1` 个元素(最大值元素)与第 `n - 1` 个元素的位置。将序列前 `n - 2` 个元素组成的子序列调整成一个新的大顶堆,使得 `n - 2` 个元素的最大值处于序列第 `1` 个位置,从而得到第 `3` 个最大值元素。 -4. 依次类推,不断交换子序列的第 `1` 个元素(最大值元素)与当前子序列最后一个元素位置,并将其调整成新的大顶堆。直到子序列剩下一个元素时,排序结束。此时整个序列就变成了一个有序序列。 +堆的逻辑结构就是一颗完全二叉树。而我们在「07.树 - 01.二叉树 - 01.树与二叉树的基础知识」章节中学过,对于完全二叉树(尤其是满二叉树)来说,采用顺序存储结构(数组)的形式来表示完全二叉树,能够充分利用存储空间。 -从堆排序算法步骤中可以看出:堆排序算法主要涉及「调整堆」和「建立初始堆」两个步骤。 +当我们使用顺序存储结构(即数组)来表示堆时,堆中元素的节点编号与数组的索引关系为: -## 2. 调整堆方法 +- 如果某二叉树节点(非叶子节点)的下标为 $i,ドル那么其左孩子节点下标为 2ドル \times i + 1,ドル右孩子节点下标为 2ドル \times i + 2$。 +- 如果某二叉树节点(非根结点)的下标为 $i,ドル那么其根节点下标为 $\lfloor \frac{i - 1}{2} \rfloor$(向下取整)。 -### 2.1 调整堆方法介绍 +```python +class MaxHeap: + def __init__(self): + self.max_heap = [] +``` + +![堆的存储结构](http://qcdn.itcharge.cn/images/20230824154601.png) -**调整堆方法**:把移走了最大值元素以后的剩余元素组成的序列再构造为一个新的堆积。具体步骤如下: +### 1.3 访问堆顶元素 -1. 从根节点开始,自上而下地调整节点的位置,使其成为堆积。 - 1. 判断序号为 `i` 的节点与其左子树节点(序号为 `2 * i`)、右子树节点(序号为 `2 * i + 1`)中值关系。 - 2. 如果序号为 `i` 节点大于等于左右子节点值,则排序结束。 - 3. 如果序号为 `i` 节点小于左右子节点值,则将序号为 `i` 节点与左右子节点中值最大的节点交换位置。 -2. 因为交换了位置,使得当前节点的左右子树原有的堆积特性被破坏。于是,从当前节点的左右子树节点开始,自上而下继续进行类似的调整。 -3. 依次类推,直到整棵完全二叉树成为一个大顶堆。 +> **访问堆顶元素**:指的是从堆结构中获取位于堆顶的元素。 -### 2.2 调整堆方法演示 +在堆中,堆顶元素位于根节点,当我们使用顺序存储结构(即数组)来表示堆时,堆顶元素就是数组的首个元素。 -![](http://qcdn.itcharge.cn/images/20211019172530.gif) +```python +class MaxHeap: + ...... + def peek(self) -> int: + # 大顶堆为空 + if not self.max_heap: + return None + # 返回堆顶元素 + return self.max_heap[0] +``` -1. 交换序列的第 `1` 个元素 `90` 与最后 `1` 个元素 `19` 的位置,此时当前节点为根节点 `19`。 -2. 判断根节点 `19`与其左右子节点值,因为 `17 < 19 < 36`,所以将根节点 `19` 与左子节点 `36` 互换位置,此时当前节点为根节点 `19`。 -3. 判断当前节点 `36` 与其左右子节点值,因为 `19 < 25 < 26`,所以将当前节点 `19` 与右节点 `26` 互换位置。调整堆结束。 +访问堆顶元素不依赖于数组中元素个数,因此时间复杂度为 $O(1)$。 -## 3. 建立初始堆方法 +### 1.4 向堆中插入元素 -### 3.1 建立初始堆方法介绍 +> **向堆中插入元素**:指的将一个新的元素添加到堆中,调整堆结构,以保持堆的特性不变。 -1. 如果原始序列对应的完全二叉树(不一定是堆)的深度为 `d`,则从 `d - 1` 层最右侧分支节点(序号为 $\lfloor \frac{n}{2} \rfloor$)开始,初始时令 $i = \lfloor \frac{n}{2} \rfloor,ドル调用调整堆算法。 -2. 每调用一次调整堆算法,执行一次 `i = i - 1`,直到 `i == 1` 时,再调用一次,就把原始序列调整为了一个初始堆。 +向堆中插入元素的步骤如下: -### 3.2 建立初始堆方法演示 +1. 将新元素添加到堆的末尾,保持完全二叉树的结构。 +2. 从新插入的元素节点开始,将该节点与其父节点进行比较。 + 1. 如果新节点的值大于其父节点的值,则交换它们,以保持最大堆的特性。 + 2. 如果新节点的值小于等于其父节点的值,说明已满足最大堆的特性,此时结束。 +3. 重复上述比较和交换步骤,直到新节点不再大于其父节点,或者达到了堆的根节点。 -![](https://qcdn.itcharge.cn/images/20220818111455.gif) +这个过程称为「上移调整(Shift Up)」。因为新插入的元素会逐步向堆的上方移动,直到找到了合适的位置,保持堆的有序性。 -1. 原始序列为 `[2, 7, 26, 25, 19, 17, 1, 90, 3, 36]`,对应完全二叉树的深度为 `3`。 -2. 从第 `2` 层最右侧的分支节点,也就序号为 `5` 的节点开始,调用堆调整算法,使其与子树形成大顶堆。 -3. 节点序号减 `1`,对序号为 `4` 的节点,调用堆调整算法,使其与子树形成大顶堆。 -4. 节点序号减 `1`,对序号为 `3` 的节点,调用堆调整算法,使其与子树形成大顶堆。 -5. 节点序号减 `1`,对序号为 `2` 的节点,调用堆调整算法,使其与子树形成大顶堆。 -6. 节点序号减 `1`,对序号为 `1` 的节点,调用堆调整算法,使其与子树形成大顶堆。 -7. 此时整个原始序列对应的完全二叉树就成了一个大顶堆,建立初始堆完毕。 +```python +class MaxHeap: + ...... + def push(self, val: int): + # 将新元素添加到堆的末尾 + self.max_heap.append(val) + + size = len(self.max_heap) + # 从新插入的元素节点开始,进行上移调整 + self.__shift_up(size - 1) + + def __shift_up(self, i: int): + while (i - 1) // 2>= 0 and self.max_heap[i]> self.max_heap[(i - 1) // 2]: + self.max_heap[i], self.max_heap[(i - 1) // 2] = self.max_heap[(i - 1) // 2], self.max_heap[i] + i = (i - 1) // 2 +``` -## 4. 堆排序方法完整演示 +在最坏情况下,向堆中插入元素的时间复杂度为 $O(\log n),ドル其中 $n$ 是堆中元素的数量,这是因为堆的高度是 $\log n$。 -![](http://qcdn.itcharge.cn/images/20211019172547.gif) +### 1.5 删除堆顶元素 -1. 原始序列为 `[2, 7, 26, 25, 19, 17, 1, 90, 3, 36]`,先根据原始序列建立一个初始堆。 -2. 交换序列中第 `1` 个元素(`90`)与第 `10` 个元素(`2`)的位置。将序列前 `9` 个元素组成的子序列调整成一个大顶堆,此时堆顶变为 `36`。 -3. 交换序列中第 `1` 个元素(`36`)与第 `9` 个元素(`3`)的位置。将序列前 `8` 个元素组成的子序列调整成一个大顶堆,此时堆顶变为 `26`。 -4. 交换序列中第 `1` 个元素(`26`)与第 `8` 个元素(`2`)的位置。将序列前 `7` 个元素组成的子序列调整成一个大顶堆,此时堆顶变为 `25`。 -5. 以此类推,不断交换子序列的第 `1` 个元素(最大值元素)与当前子序列最后一个元素位置,并将其调整成新的大顶堆。直到子序列只剩下最后一个元素 `1` 时,排序结束。此时整个序列变成了一个有序序列,即 `[1, 2, 3, 7, 17, 19, 25, 26, 36, 90]`。 +> **删除堆顶元素**:指的是从堆中移除位于堆顶的元素,并重新调整对结果,以保持堆的特性不变。 -## 5. 堆排序算法分析 +删除堆顶元素的步骤如下: -- **时间复杂度**:$O(n \times \log_2 n)$。 - - 堆积排序的时间主要花费在两个方面:「建立初始堆」和「调整堆」。 - - 设原始序列所对应的完全二叉树深度为 $d,ドル算法由两个独立的循环组成: - 1. 在第 1ドル$ 个循环构造初始堆积时,从 $i = d - 1$ 层开始,到 $i = 1$ 层为止,对每个分支节点都要调用一次调整堆算法,而一次调整堆算法,对于第 $i$ 层一个节点到第 $d$ 层上建立的子堆积,所有节点可能移动的最大距离为该子堆积根节点移动到最后一层(第 $d$ 层) 的距离,即 $d - i$。而第 $i$ 层上节点最多有 2ドル^{i-1}$ 个,所以每一次调用调整堆算法的最大移动距离为 2ドル^{i-1} * (d-i)$。因此,堆积排序算法的第 1ドル$ 个循环所需时间应该是各层上的节点数与该层上节点可移动的最大距离之积的总和,即:$\sum_{i = d - 1}^1 2^{i-1} (d-i) = \sum_{j = 1}^{d-1} 2^{d-j-1} \times j = \sum_{j = 1}^{d-1} 2^{d-1} \times {j \over 2^j} \le n \sum_{j = 1}^{d-1} {j \over 2^j} < 2n$。这一部分的时间花费为 $O(n)$。 - 2. 在第 2ドル$ 个循环中,每次调用调整堆算法一次,节点移动的最大距离为这棵完全二叉树的深度 $d = \lfloor \log_2(n) \rfloor + 1,ドル一共调用了 $n - 1$ 次调整堆算法,所以,第 2ドル$ 个循环的时间花费为 $(n-1)(\lfloor \log_2 (n)\rfloor + 1) = O(n \times \log_2 n)$。 - - 因此,堆积排序的时间复杂度为 $O(n \times \log_2 n)$。 -- **空间复杂度**:$O(1)$。由于在堆积排序中只需要一个记录大小的辅助空间,因此,堆积排序的空间复杂度为:$O(1)$。 -- **排序稳定性**:堆排序是一种 **不稳定排序算法**。 +1. 将堆顶元素(即根节点)与堆的末尾元素交换。 +2. 移除堆末尾的元素(之前的堆顶),即将其从堆中剔除。 +3. 从新的堆顶元素开始,将其与其较大的子节点进行比较。 + 1. 如果当前节点的值小于其较大的子节点,则将它们交换。这一步是为了将新的堆顶元素「下沉」到适当的位置,以保持最大堆的特性。 + 2. 如果当前节点的值大于等于其较大的子节点,说明已满足最大堆的特性,此时结束。 +4. 重复上述比较和交换步骤,直到新的堆顶元素不再小于其子节点,或者达到了堆的底部。 -## 6. 堆排序代码实现 +这个过程称为「下移调整(Shift Down)」。因为新的堆顶元素会逐步向堆的下方移动,直到找到了合适的位置,保持堆的有序性。 ```python -class Solution: - # 调整为大顶堆 - def heapify(self, arr: [int], index: int, end: int): - # 根节点为 index,左节点为 2 * index + 1, 右节点为 2 * index + 2 - left = index * 2 + 1 - right = left + 1 - while left <= end: - # 当前节点为非叶子结点 - max_index = index - if arr[left]> arr[max_index]: - max_index = left - if right <= end and arr[right]> arr[max_index]: - max_index = right - if index == max_index: - # 如果不用交换,则说明已经交换结束 +class MaxHeap: + ...... + def pop(self) -> int: + # 堆为空 + if not self.max_heap: + raise IndexError("堆为空") + + size = len(self.max_heap) + self.max_heap[0], self.max_heap[size - 1] = self.max_heap[size - 1], self.max_heap[0] + # 删除堆顶元素 + val = self.max_heap.pop() + # 节点数减 1 + size -= 1 + + # 下移调整 + self.__shift_down(0, size) + + # 返回堆顶元素 + return val + + + def __shift_down(self, i: int, n: int): + while 2 * i + 1 < n: + # 左右子节点编号 + left, right = 2 * i + 1, 2 * i + 2 + + # 找出左右子节点中的较大值节点编号 + if 2 * i + 2>= n: + # 右子节点编号超出范围(只有左子节点 + larger = left + else: + # 左子节点、右子节点都存在 + if self.max_heap[left]>= self.max_heap[right]: + larger = left + else: + larger = right + + # 将当前节点值与其较大的子节点进行比较 + if self.max_heap[i] < self.max_heap[larger]: + # 如果当前节点值小于其较大的子节点,则将它们交换 + self.max_heap[i], self.max_heap[larger] = self.max_heap[larger], self.max_heap[i] + i = larger + else: + # 如果当前节点值大于等于于其较大的子节点,此时结束 break - arr[index], arr[max_index] = arr[max_index], arr[index] - # 继续调整子树 - index = max_index - left = index * 2 + 1 - right = left + 1 - - # 初始化大顶堆 - def buildMaxHeap(self, arr: [int]): - size = len(arr) - # (size - 2) // 2 是最后一个非叶节点,叶节点不用调整 - for i in range((size - 2) // 2, -1, -1): - self.heapify(arr, i, size - 1) - return arr - - # 升序堆排序,思路如下: - # 1. 先建立大顶堆 - # 2. 让堆顶最大元素与最后一个交换,然后调整第一个元素到倒数第二个元素,这一步获取最大值 - # 3. 再交换堆顶元素与倒数第二个元素,然后调整第一个元素到倒数第三个元素,这一步获取第二大值 - # 4. 以此类推,直到最后一个元素交换之后完毕。 - def maxHeapSort(self, arr: [int]): - self.buildMaxHeap(arr) - size = len(arr) - for i in range(size): - arr[0], arr[size - i - 1] = arr[size - i - 1], arr[0] - self.heapify(arr, 0, size - i - 2) - return arr +``` + +删除堆顶元素的时间复杂度通常为$O(\log n),ドル其中 $n$ 是堆中元素的数量,因为堆的高度是 $\log n$。 + +## 2. 堆排序 - def sortArray(self, nums: List[int]) -> List[int]: +### 2.1 堆排序算法思想 + +> **堆排序(Heap sort)基本思想**: +> +> 借用「堆结构」所设计的排序算法。将数组转化为大顶堆,重复从大顶堆中取出数值最大的节点,并让剩余的堆结构继续维持大顶堆性质。 + +### 2.2 堆排序算法步骤 + +1. **构建初始大顶堆**: + 1. 定义一个数组实现的堆结构,将原始数组的元素依次存入堆结构的数组中(初始顺序不变)。 + 2. 从数组的中间位置开始,从右至左,依次通过「下移调整」将数组转换为一个大顶堆。 + +2. **交换元素,调整堆**: + 1. 交换堆顶元素(第 1ドル$ 个元素)与末尾(最后 1ドル$ 个元素)的位置,交换完成后,堆的长度减 1ドル$。 + 2. 交换元素之后,由于堆顶元素发生了改变,需要从根节点开始,对当前堆进行「下移调整」,使其保持堆的特性。 + +3. **重复交换和调整堆**: + 1. 重复第 2ドル$ 步,直到堆的大小为 1ドル$ 时,此时大顶堆的数组已经完全有序。 + + +### 2.3 堆排序代码实现 + +```python +class MaxHeap: + ...... + def __buildMaxHeap(self, nums: [int]): + size = len(nums) + # 先将数组 nums 的元素按顺序添加到 max_heap 中 + for i in range(size): + self.max_heap.append(nums[i]) + + # 从最后一个非叶子节点开始,进行下移调整 + for i in range((size - 2) // 2, -1, -1): + self.__shift_down(i, size) + + def maxHeapSort(self, nums: [int]) -> [int]: + # 根据数组 nums 建立初始堆 + self.__buildMaxHeap(nums) + + size = len(self.max_heap) + for i in range(size - 1, -1, -1): + # 交换根节点与当前堆的最后一个节点 + self.max_heap[0], self.max_heap[i] = self.max_heap[i], self.max_heap[0] + # 从根节点开始,对当前堆进行下移调整 + self.__shift_down(0, i) + + # 返回排序后的数组 + return self.max_heap + +class Solution: + def maxHeapSort(self, nums: [int]) -> [int]: + return MaxHeap().maxHeapSort(nums) + + def sortArray(self, nums: [int]) -> [int]: return self.maxHeapSort(nums) + +print(Solution().sortArray([10, 25, 6, 8, 7, 1, 20, 23, 16, 19, 17, 3, 18, 14])) ``` +### 2.4 堆排序算法分析 + +- **时间复杂度**:$O(n \times \log n)$。 + - 堆积排序的时间主要花费在两个方面:「建立初始堆」和「下移调整」。 + - 设原始数组所对应的完全二叉树深度为 $d,ドル算法由两个独立的循环组成: + 1. 在第 1ドル$ 个循环构造初始堆积时,从 $i = d - 1$ 层开始,到 $i = 1$ 层为止,对每个分支节点都要调用一次调整堆算法,而一次调整堆算法,对于第 $i$ 层一个节点到第 $d$ 层上建立的子堆积,所有节点可能移动的最大距离为该子堆积根节点移动到最后一层(第 $d$ 层) 的距离,即 $d - i$。而第 $i$ 层上节点最多有 2ドル^{i-1}$ 个,所以每一次调用调整堆算法的最大移动距离为 2ドル^{i-1} * (d-i)$。因此,堆积排序算法的第 1ドル$ 个循环所需时间应该是各层上的节点数与该层上节点可移动的最大距离之积的总和,即:$\sum_{i = d - 1}^1 2^{i-1} (d-i) = \sum_{j = 1}^{d-1} 2^{d-j-1} \times j = \sum_{j = 1}^{d-1} 2^{d-1} \times {j \over 2^j} \le n \times \sum_{j = 1}^{d-1} {j \over 2^j} < 2 \times n$。这一部分的时间花费为 $O(n)$。 + 2. 在第 2ドル$ 个循环中,每次调用调整堆算法一次,节点移动的最大距离为这棵完全二叉树的深度 $d = \lfloor \log_2(n) \rfloor + 1,ドル一共调用了 $n - 1$ 次调整堆算法,所以,第 2ドル$ 个循环的时间花费为 $(n-1)(\lfloor \log_2 (n)\rfloor + 1) = O(n \times \log n)$。 + - 因此,堆积排序的时间复杂度为 $O(n \times \log n)$。 +- **空间复杂度**:$O(1)$。由于在堆积排序中只需要一个记录大小的辅助空间,因此,堆积排序的空间复杂度为:$O(1)$。 +- **排序稳定性**:在进行「下移调整」时,相等元素的相对位置可能会发生变化。因此,堆排序是一种 **不稳定排序算法**。 From 75d30389b0fb27eacc57be7091597e2512c9b4fe Mon Sep 17 00:00:00 2001 From: ITCharge Date: 2023年8月24日 20:01:21 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E6=95=B0=E7=BB=84=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E5=86=85=E5=AE=B9=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../01.Array/01.Array-Basic/01.Array-Basic.md | 51 +++++++------------ 1 file changed, 17 insertions(+), 34 deletions(-) diff --git a/Contents/01.Array/01.Array-Basic/01.Array-Basic.md b/Contents/01.Array/01.Array-Basic/01.Array-Basic.md index ff535448..1c5f4011 100644 --- a/Contents/01.Array/01.Array-Basic/01.Array-Basic.md +++ b/Contents/01.Array/01.Array-Basic/01.Array-Basic.md @@ -76,10 +76,6 @@ arr = ['python', 'java', ['asp', 'php'], 'c'] > 1. 只需要检查 $i$ 的范围是否在合法的范围区间,即 0ドル \le i \le len(nums) - 1$。超出范围的访问为非法访问。 > 2. 当位置合法时,由给定下标得到元素的值。 -访问操作不依赖于数组中元素个数,因此时间复杂度为 $O(1)$。 - -示例代码如下: - ```python # 从数组 nums 中读取下标为 i 的数据元素值 def value(nums, i): @@ -90,6 +86,8 @@ arr = [0, 5, 2, 3, 7, 1, 6] value(arr, 3) ``` +「访问数组元素」的操作不依赖于数组中元素个数,因此,「访问数组元素」的时间复杂度为 $O(1)$。 + ### 2.2 查找元素 > **查找数组中元素值为 $val$ 的位置**: @@ -98,10 +96,6 @@ value(arr, 3) > 2. 在找到元素的时候返回元素下标。 > 3. 遍历完找不到时可以返回一个特殊值(例如 $-1$)。 -在数组无序的情况下,只能通过将 $val$ 与数组中的数据元素逐一对比的方式进行检索,也称为线性查找。线性查找操作依赖于数组中元素个数,因此时间复杂度为 $O(n)$。 - -示例代码如下: - ```python # 从数组 nums 中查找元素值为 val 的数据元素第一次出现的位置 def find(nums, val): @@ -114,6 +108,8 @@ arr = [0, 5, 2, 3, 7, 1, 6] print(find(arr, 5)) ``` +在「查找元素」的操作中,如果数组无序,那么我们只能通过将 $val$ 与数组中的数据元素逐一对比的方式进行查找,也称为线性查找。而线性查找操作依赖于数组中元素个数,因此,「查找元素」的时间复杂度为 $O(n)$。 + ### 2.3 插入元素 插入元素操作分为两种:「在数组尾部插入值为 $val$ 的元素」和「在数组第 $i$ 个位置上插入值为 $val$ 的元素」。 @@ -123,14 +119,10 @@ print(find(arr, 5)) > 1. 如果数组尾部容量不满,则直接把 $val$ 放在数组尾部的空闲位置,并更新数组的元素计数值。 > 2. 如果数组容量满了,则插入失败。不过,Python 中的 list 列表做了其他处理,当数组容量满了,则会开辟新的空间进行插入。 -在尾部插入元素的操作不依赖数组个数,其时间复杂度为 $O(1)$。 - Python 中的 list 列表直接封装了尾部插入操作,直接调用 `append` 方法即可。 ![插入元素](https://qcdn.itcharge.cn/images/20210916222517.png) -示例代码如下: - ```python arr = [0, 5, 2, 3, 7, 1, 6] val = 4 @@ -138,20 +130,18 @@ arr.append(val) print(arr) ``` +「在数组尾部插入元素」的操作不依赖数组个数,因此,「在数组尾部插入元素」的时间复杂度为 $O(1)$。 + > **在数组第 $i$ 个位置上插入值为 $val$ 的元素**: > > 1. 先检查插入下标 $i$ 是否合法,即 0ドル \le i \le len(nums)$。 > 2. 确定合法位置后,通常情况下第 $i$ 个位置上已经有数据了(除非 $i == len(nums)$),要把第 $i \sim len(nums) - 1$ 位置上的元素依次向后移动。 > 3. 然后再在第 $i$ 个元素位置赋值为 $val,ドル并更新数组的元素计数值。 -因为移动元素的操作次数跟元素个数有关,最坏和平均时间复杂度都是 $O(n)$。 - Python 中的 list 列表直接封装了中间插入操作,直接调用 `insert` 方法即可。 ![插入中间元素](https://qcdn.itcharge.cn/images/20210916224032.png) -示例代码如下: - ```python arr = [0, 5, 2, 3, 7, 1, 6] i, val = 2, 4 @@ -159,6 +149,8 @@ arr.insert(i, val) print(arr) ``` +「在数组中间位置插入元素」的操作中,由于移动元素的操作次数跟元素个数有关,因此,「在数组中间位置插入元素」的最坏和平均时间复杂度都是 $O(n)$。 + ### 2.4 改变元素 > **将数组中第 $i$ 个元素值改为 $val$**: @@ -166,12 +158,8 @@ print(arr) > 1. 需要先检查 $i$ 的范围是否在合法的范围区间,即 0ドル \le i \le len(nums) - 1$。 > 2. 然后将第 $i$ 个元素值赋值为 $val$。 -改变元素操作跟访问元素操作类似,访问操作不依赖于数组中元素个数,因此时间复杂度为 $O(1)$。 - ![改变元素](https://qcdn.itcharge.cn/images/20210916224722.png) -示例代码如下: - ```python def change(nums, i, val): if 0 <= i <= len(nums) - 1: @@ -183,6 +171,8 @@ change(arr, i, val) print(arr) ``` +「改变元素」的操作跟访问元素操作类似,访问操作不依赖于数组中元素个数,因此,「改变元素」的时间复杂度为 $O(1)$。 + ### 2.5 删除元素 删除元素分为三种情况:「删除数组尾部元素」、「删除数组第 $i$ 个位置上的元素」、「基于条件删除元素」。 @@ -191,34 +181,28 @@ print(arr)> > 1. 只需将元素计数值减一即可。 -这样原来的数组尾部元素不再位于合法的数组下标范围,就相当于删除了。时间复杂度为 $O(1)$。 - Python 中的 list 列表直接封装了删除数组尾部元素的操作,只需要调用 `pop` 方法即可。 ![删除尾部元素](https://qcdn.itcharge.cn/images/20210916233914.png) -示例代码如下: - ```python arr = [0, 5, 2, 3, 7, 1, 6] arr.pop() print(arr) ``` +「删除数组尾部元素」的操作,不依赖于数组中的元素个数,因此,「删除数组尾部元素」的时间复杂度为 $O(1)$。 + > **删除数组第 $i$ 个位置上的元素**: > > 1. 先检查下标 $i$ 是否合法,即 0ドル \le i \le len(nums) - 1$。 > 2. 如果下标合法,则将第 $i + 1$ 个位置到第 $len(nums) - 1$ 位置上的元素依次向左移动。 > 3. 删除后修改数组的元素计数值。 -删除中间位置元素的操作同样涉及移动元素,而移动元素的操作次数跟元素个数有关,因此删除中间元素的最坏和平均时间复杂度都是 $O(n)$。 - Python 中的 list 列表直接封装了删除数组中间元素的操作,只需要以下标作为参数调用 `pop` 方法即可。 ![删除中间元素](https://qcdn.itcharge.cn/images/20210916234013.png) -示例代码如下: - ``` arr = [0, 5, 2, 3, 7, 1, 6] i = 3 @@ -226,19 +210,18 @@ arr.pop(i) print(arr) ``` -> **基于条件删除元素**:这种操作一般不给定被删元素的位置,而是给出一个条件要求删除满足这个条件的(一个、多个或所有)元素。这类操作也是通过循环检查元素,查找到元素后将其删除。 - -删除多个元素操作中涉及到的多次移动元素操作,可以通过算法改进,将多趟移动元素操作转变为一趟移动元素,从而将时间复杂度降低为 $O(n)$。一般而言,这类删除操作都是线性时间操作,时间复杂度为 $O(n)$。 +「删除数组中间位置元素」的操作同样涉及移动元素,而移动元素的操作次数跟元素个数有关,因此,「删除数组中间位置元素」的最坏和平均时间复杂度都是 $O(n)$。 -示例代码如下: +> **基于条件删除元素**:这种操作一般不给定被删元素的位置,而是给出一个条件要求删除满足这个条件的(一个、多个或所有)元素。这类操作也是通过循环检查元素,查找到元素后将其删除。 ```python arr = [0, 5, 2, 3, 7, 1, 6] -i = 3 arr.remove(5) print(arr) ``` +「基于条件删除元素」的操作同样涉及移动元素,而移动元素的操作次数跟元素个数有关,因此,「基于条件删除元素」的最坏和平均时间复杂度都是 $O(n)$。 + --- 到这里,有关数组的基础知识就介绍完了。下面进行一下总结。 @@ -247,7 +230,7 @@ print(arr) 数组是最基础、最简单的数据结构。数组是实现线性表的顺序结构存储的基础。它使用一组连续的内存空间,来存储一组具有相同类型的数据。 -数组的最大特点的支持随机访问。其访问元素、改变元素的时间复杂度为 $O(1),ドル在尾部插入、删除元素的时间复杂度也是 $O(1),ドル普通情况下插入、删除元素的时间复杂度为 $O(n)$。 +数组的最大特点的支持随机访问。访问数组元素、改变数组元素的时间复杂度为 $O(1),ドル在数组尾部插入、删除元素的时间复杂度也是 $O(1),ドル普通情况下插入、删除元素的时间复杂度为 $O(n)$。 ## 参考资料

AltStyle によって変換されたページ (->オリジナル) /