diff --git "a/Solutions/0075. 351円242円234円350円211円262円345円210円206円347円261円273円.md" "b/Solutions/0075. 351円242円234円350円211円262円345円210円206円347円261円273円.md" index 96469dd4..9ae8ffa4 100644 --- "a/Solutions/0075. 351円242円234円350円211円262円345円210円206円347円261円273円.md" +++ "b/Solutions/0075. 351円242円234円350円211円262円345円210円206円347円261円273円.md" @@ -5,21 +5,43 @@ ## 题目大意 -给定一个数组 nums,元素值只有 0、1、2,分别代表红色、白色、蓝色。将数组进行排序,使得 红色在前,白色在中间,蓝色在最后。 +**描述**:给定一个数组 `nums`,元素值只有 `0`、`1`、`2`,分别代表红色、白色、蓝色。 -要求不使用标准库函数,同时仅用常数空间,一趟扫描解决。 +**要求**:将数组进行排序,使得红色在前,白色在中间,蓝色在最后。 + +**说明**: + +- 要求不使用标准库函数,同时仅用常数空间,一趟扫描解决。 +- $n == nums.length$。 +- 1ドル \le n \le 300$。 +- `nums[i]` 为 `0`、`1` 或 `2`。 + +**示例**: + +```Python +输入:nums = [2,0,2,1,1,0] +输出:[0,0,1,1,2,2] + + +输入:nums = [2,0,1] +输出:[0,1,2] +``` ## 解题思路 -使用两个指针 left,right,分别指向数组的头尾。left 表示当前处理好红色元素的尾部,right 表示当前处理好蓝色的头部。 +### 思路 1:双指针 + 快速排序思想 + +快速排序算法中的 `partition` 过程,利用双指针,将序列中比基准数 `pivot` 大的元素移动到了基准数右侧,将比基准数 `pivot` 小的元素移动到了基准数左侧。从而将序列分为了三部分:比基准数小的部分、基准数、比基准数大的部分。 -再使用一个下标 index 遍历数组,如果遇到 nums[index] == 0,就交换 nums[index] 和 nums[left],同时将 left 右移。如果遇到 nums[index] == 2,就交换 nums[index] 和 nums[right],同时将 right 左移。 +这道题我们也可以借鉴快速排序算法中的 `partition` 过程,将 `1` 作为基准数 `pivot`,然后将序列分为三部分:`0`(即比 `1` 小的部分)、等于 `1` 的部分、`2`(即比 `1` 大的部分)。具体步骤如下: -直到 index 移动到 right 位置之后,停止遍历。遍历结束之后,此时 left 左侧都是红色,right 右侧都是蓝色。 +1. 使用两个指针 `left`、`right`,分别指向数组的头尾。`left` 表示当前处理好红色元素的尾部,`right` 表示当前处理好蓝色的头部。 +2. 再使用一个下标 `index` 遍历数组,如果遇到 `nums[index] == 0`,就交换 `nums[index]` 和 `nums[left]`,同时将 `left` 右移。如果遇到 `nums[index] == 2`,就交换 `nums[index]` 和 `nums[right]`,同时将 `right` 左移。 +3. 直到 `index` 移动到 `right` 位置之后,停止遍历。遍历结束之后,此时 `left` 左侧都是红色,`right` 右侧都是蓝色。 -注意:移动的时候需要判断 index 和 left 的位置,因为 left 左侧是已经处理好的数组,所以需要判断 index 的位置是否小于 left,小于的话,需要更新 index 位置。 +注意:移动的时候需要判断 `index` 和 `left` 的位置,因为 `left` 左侧是已经处理好的数组,所以需要判断 `index` 的位置是否小于 `left`,小于的话,需要更新 `index` 位置。 -## 代码 +### 思路 1:代码 ```Python class Solution: @@ -40,3 +62,8 @@ class Solution: index += 1 ``` +### 思路 1:复杂度分析 + +- **时间复杂度**:$O(n)$。 +- **空间复杂度**:$O(1)$。 + diff --git "a/Solutions/0215. 346円225円260円347円273円204円344円270円255円347円232円204円347円254円254円K344円270円252円346円234円200円345円244円247円345円205円203円347円264円240円.md" "b/Solutions/0215. 346円225円260円347円273円204円344円270円255円347円232円204円347円254円254円K344円270円252円346円234円200円345円244円247円345円205円203円347円264円240円.md" index 2d45c702..509fa0f3 100644 --- "a/Solutions/0215. 346円225円260円347円273円204円344円270円255円347円232円204円347円254円254円K344円270円252円346円234円200円345円244円247円345円205円203円347円264円240円.md" +++ "b/Solutions/0215. 346円225円260円347円273円204円344円270円255円347円232円204円347円254円254円K344円270円252円346円234円200円345円244円247円345円205円203円347円264円240円.md" @@ -5,51 +5,54 @@ ## 题目大意 -给定一个未排序的数组 `nums`。 +**描述**:给定一个未排序的整数数组 `nums` 和一个整数 `k`。 -要求:从中找到第 `k` 个最大的元素。 +**要求**:返回数组中第 `k` 个最大的元素。 -## 解题思路 +**说明**: -很不错的一道题,面试常考。 +- 要求使用时间复杂度为 $O(n)$ 的算法解决此问题。 +- 1ドル \le k \le nums.length \le 10^5$。 +- $-10^4 \le nums[i] \le 10^4$。 -直接可以想到的思路是:排序后输出数组上对应第 k 位大的数。所以问题关键在于排序方法的复杂度。 +**示例**: -冒泡排序、选择排序、插入排序时间复杂度 $O(n^2)$ 太高了,解答会超时。 +```Python +输入: [3,2,1,5,6,4], k = 2 +输出: 5 -可考虑堆排序、归并排序、快速排序。 -这道题的要求是找到第 `k` 大的元素,使用归并排序只有到最后排序完毕才能返回第 k 大的数。而堆排序每次排序之后,就会确定一个元素的准确排名,同理快速排序也是如此。 +输入: [3,2,3,1,2,4,5,5,6], k = 4 +输出: 4 +``` + +## 解题思路 -### 1. 堆排序 +很不错的一道题,面试常考。 -升序堆排序的思路如下: +直接可以想到的思路是:排序后输出数组上对应第 k 位大的数。所以问题关键在于排序方法的复杂度。 -1. 先建立大顶堆 +冒泡排序、选择排序、插入排序时间复杂度 $O(n^2)$ 太高了,很容易超时。 -2. 让堆顶最大元素与最后一个交换,然后调整第一个元素到倒数第二个元素,这一步获取最大值 +可考虑堆排序、归并排序、快速排序。 -3. 再交换堆顶元素与倒数第二个元素,然后调整第一个元素到倒数第三个元素,这一步获取第二大值 +这道题的要求是找到第 `k` 大的元素,使用归并排序只有到最后排序完毕才能返回第 `k` 大的数。而堆排序每次排序之后,就会确定一个元素的准确排名,同理快速排序也是如此。 -4. 以此类推,直到最后一个元素交换之后完毕。 +### 思路 1:堆排序 -这道题我们只需进行 1 次建立大顶堆, k-1 次调整即可得到第 k 大的数。 +升序堆排序的思路如下: -时间复杂度:$O(n * \log n)$ +1. 将无序序列构造成第 `1` 个大顶堆(初始堆),使得 `n` 个元素的最大值处于序列的第 `1` 个位置。 -### 2. 快速排序 +2. **调整堆**:交换序列的第 `1` 个元素(最大值元素)与第 `n` 个元素的位置。将序列前 `n - 1` 个元素组成的子序列调整成一个新的大顶堆,使得 `n - 1` 个元素的最大值处于序列第 `1` 个位置,从而得到第 `2` 个最大值元素。 -快速排序每次调整,都会确定一个元素的最终位置,且以该元素为界限,将数组分成了两个数组,前一个数组元素都比该元素小,后一个元素都比该元素大。 +3. **调整堆**:交换子序列的第 `1` 个元素(最大值元素)与第 `n - 1` 个元素的位置。将序列前 `n - 2` 个元素组成的子序列调整成一个新的大顶堆,使得 `n - 2` 个元素的最大值处于序列第 `1` 个位置,从而得到第 `3` 个最大值元素。 -这样,只要某次划分的元素恰好是第 k 个下标就找到了答案。并且我们只需关注 k 元素所在区间的排序情况,与 k 元素无关的区间排序都可以忽略。这样进一步减少了执行步骤。 +4. 依次类推,不断交换子序列的第 `1` 个元素(最大值元素)与当前子序列最后一个元素位置,并将其调整成新的大顶堆。直到获取第 `k` 个最大值元素为止。 -### 3. 借用标准库(不建议) -提交代码中的最快代码是调用了 Python 的 heapq 库,或者 sort 方法。 -这样的确可以通过,但是不建议这样做。借用标准库实现,只能说对这个库的 API 和相关数据结构的用途相对熟悉,而不代表着掌握了这个数据结构。可以问问自己,如果换一种语言,自己还能不能实现对应的数据结构?刷题的本质目的是为了把算法学会学透,而不仅仅是调 API。 -## 代码 +### 思路 1:代码 -1. 堆排序 ```Python class Solution: def findKthLargest(self, nums: List[int], k: int) -> int: @@ -89,49 +92,85 @@ class Solution: return nums[0] ``` -2. 快速排序 +### 思路 1:复杂度分析 + +- **时间复杂度**:$O(n \times \log_2n)$。 +- **空间复杂度**:$O(1)$。 + +### 思路 2:快速排序 + +使用快速排序在每次调整时,都会确定一个元素的最终位置,且以该元素为界限,将数组分成了左右两个子数组,左子数组中的元素都比该元素小,右子树组中的元素都比该元素大。 + +这样,只要某次划分的元素恰好是第 `k` 个下标就找到了答案。并且我们只需关注第 `k` 个最大元素所在区间的排序情况,与第 `k` 个最大元素无关的区间排序都可以忽略。这样进一步减少了执行步骤。 + +### 思路 2:代码 + ```Python import random + class Solution: + # 从 arr[low: high + 1] 中随机挑选一个基准数,并进行移动排序 + def randomPartition(self, arr: [int], low: int, high: int): + # 随机挑选一个基准数 + i = random.randint(low, high) + # 将基准数与最低位互换 + arr[i], arr[low] = arr[low], arr[i] + # 以最低位为基准数,然后将序列中比基准数大的元素移动到基准数右侧,比他小的元素移动到基准数左侧。最后将基准数放到正确位置上 + return self.partition(arr, low, high) + + # 以最低位为基准数,然后将序列中比基准数大的元素移动到基准数右侧,比他小的元素移动到基准数左侧。最后将基准数放到正确位置上 + def partition(self, arr: [int], low: int, high: int): + pivot = arr[low] # 以第 1 为为基准数 + i = low + 1 # 从基准数后 1 位开始遍历,保证位置 i 之前的元素都小于基准数 + + for j in range(i, high + 1): + # 发现一个小于基准数的元素 + if arr[j] < pivot: + # 将小于基准数的元素 arr[j] 与当前 arr[i] 进行换位,保证位置 i 之前的元素都小于基准数 + arr[i], arr[j] = arr[j], arr[i] + # i 之前的元素都小于基准数,所以 i 向右移动一位 + i += 1 + # 将基准节点放到正确位置上 + arr[i - 1], arr[low] = arr[low], arr[i - 1] + # 返回基准数位置 + return i - 1 + + def quickSort(self, arr, low, high, k): + size = len(arr) + if low < high: + # 按照基准数的位置,将序列划分为左右两个子序列 + pi = self.randomPartition(arr, low, high) + if pi == size - k: + return arr[size - k] + if pi> size - k: + # 对左子序列进行递归快速排序 + self.quickSort(arr, low, pi - 1, k) + if pi < size - k: + # 对右子序列进行递归快速排序 + self.quickSort(arr, pi + 1, high, k) + + return arr[size - k] + def findKthLargest(self, nums: List[int], k: int) -> int: - def randomPartition(nums, low, high): - i = random.randint(low, high) - nums[i], nums[high] = nums[high], nums[i] - return partition(nums, low, high) - - def partition(nums, low, high): - x = nums[high] - i = low-1 - for j in range(low, high): - if nums[j] <= nums[high]: - i += 1 - nums[i], nums[j] = nums[j], nums[i] - nums[i+1], nums[high] = nums[high], nums[i+1] - return i+1 - - def quickSort(nums, low, high, k): - n = len(nums) - if low < high: - pi = randomPartition(nums, low, high) - if pi == n-k: - return nums[len(nums)-k] - if pi> n-k: - quickSort(nums, low, pi-1, k) - if pi < n-k: - quickSort(nums, pi+1, high, k) - - return nums[len(nums)-k] - - return quickSort(nums, 0, len(nums)-1, k) + return self.quickSort(nums, 0, len(nums) - 1, k) ``` -3. 借用标准库 +### 思路 2:复杂度分析 + +- **时间复杂度**:$O(n)$。证明过程可参考「算法导论 9.2:期望为线性的选择算法」。 +- **空间复杂度**:$O(\log_2 n)$。递归使用栈空间的空间代价期望为 $O(\log_2n)$。 + +### 思路 3:借用标准库(不建议) + +提交代码中的最快代码是调用了 `Python` 的 `heapq` 库,或者 `sort` 方法。这种做法适合在打算法竞赛的时候节省时间,日常练习不建议不建议这样做。 + +### 思路 3:代码 ```Python class Solution: def findKthLargest(self, nums: List[int], k: int) -> int: nums.sort() - return nums[len(nums)-k] + return nums[len(nums) - k] ``` ```Python @@ -148,3 +187,8 @@ class Solution: return heapq.heappop(res) ``` +### 思路 3:复杂度分析 + +- **时间复杂度**:$O(n \times \log_2n)$。 +- **空间复杂度**:$O(1)$。 + diff --git "a/Solutions/345円211円221円346円214円207円 Offer 40. 346円234円200円345円260円217円347円232円204円k344円270円252円346円225円260円.md" "b/Solutions/345円211円221円346円214円207円 Offer 40. 346円234円200円345円260円217円347円232円204円k344円270円252円346円225円260円.md" index 9d25e4b2..d6d77b27 100644 --- "a/Solutions/345円211円221円346円214円207円 Offer 40. 346円234円200円345円260円217円347円232円204円k344円270円252円346円225円260円.md" +++ "b/Solutions/345円211円221円346円214円207円 Offer 40. 346円234円200円345円260円217円347円232円204円k344円270円252円346円225円260円.md" @@ -5,9 +5,25 @@ ## 题目大意 -给定整数数组 `arr`,再给定一个整数 `k`。 +**描述**:给定整数数组 `arr`,再给定一个整数 `k`。 -要求:返回数组 `arr` 中最小的 `k` 个数。 +**要求**:返回数组 `arr` 中最小的 `k` 个数。 + +**说明**: + +- 0ドル \le k \le arr.length \le 10000$。 +- 0ドル \le arr[i] \le 10000$。 + +**示例**: + +```Python +输入:arr = [3,2,1], k = 2 +输出:[1,2] 或者 [2,1] + + +输入:arr = [0,1,2,1], k = 1 +输出:[0] +``` ## 解题思路 @@ -15,13 +31,17 @@ 冒泡排序、选择排序、插入排序时间复杂度 $O(n^2)$ 太高了,解答会超时。 -可考虑堆排序、归并排序、快速排序。本题使用堆排序。具体做法如下: +可考虑堆排序、归并排序、快速排序。 -1. 利用数组前 `k` 个元素,建立大小为 `k` 的大顶堆。 -2. 遍历数组 `[k, size - 1]` 的元素,判断其与堆顶元素关系,如果比堆顶元素小,则将其赋值给堆顶元素,再对大顶堆进行调整。 -3. 最后输出前调整过后的大顶堆的前 `k` 个元素。 +### 思路 1:堆排序(基于大顶堆) -## 代码 +具体做法如下: + +1. 使用数组前 `k` 个元素,维护一个大小为 `k` 的大顶堆。 +2. 遍历数组 `[k, size - 1]` 的元素,判断其与堆顶元素关系,如果遇到比堆顶元素小的元素,则将与堆顶元素进行交换。再将这 `k` 个元素调整为大顶堆。 +3. 最后输出大顶堆的 `k` 个元素。 + +### 思路 1:代码 ```Python class Solution: @@ -59,6 +79,7 @@ class Solution: return arr self.buildMaxHeap(arr, k) + for i in range(k, size): if arr[i] < arr[0]: arr[i], arr[0] = arr[0], arr[i] @@ -67,3 +88,74 @@ class Solution: return arr[:k] ``` +### 思路 1:复杂度分析 + +- **时间复杂度**:$O(n\log_2k)$。 +- **空间复杂度**:$O(1)$。 + +### 思路 2:快速排序 + +使用快速排序在每次调整时,都会确定一个元素的最终位置,且以该元素为界限,将数组分成了左右两个子数组,左子数组中的元素都比该元素小,右子树组中的元素都比该元素大。 + +这样,只要某次划分的元素恰好是第 `k` 个元素下标,就找到了数组中最小的 `k` 个数所对应的区间,即 `[0, k - 1]`。 并且我们只需关注第 `k` 个最小元素所在区间的排序情况,与第 `k` 个最小元素无关的区间排序都可以忽略。这样进一步减少了执行步骤。 + +### 思路 2:代码 + +```Python +import random + +class Solution: + # 从 arr[low: high + 1] 中随机挑选一个基准数,并进行移动排序 + def randomPartition(self, arr: [int], low: int, high: int): + # 随机挑选一个基准数 + i = random.randint(low, high) + # 将基准数与最低位互换 + arr[i], arr[low] = arr[low], arr[i] + # 以最低位为基准数,然后将序列中比基准数大的元素移动到基准数右侧,比他小的元素移动到基准数左侧。最后将基准数放到正确位置上 + return self.partition(arr, low, high) + + # 以最低位为基准数,然后将序列中比基准数大的元素移动到基准数右侧,比他小的元素移动到基准数左侧。最后将基准数放到正确位置上 + def partition(self, arr: [int], low: int, high: int): + pivot = arr[low] # 以第 1 为为基准数 + i = low + 1 # 从基准数后 1 位开始遍历,保证位置 i 之前的元素都小于基准数 + + for j in range(i, high + 1): + # 发现一个小于基准数的元素 + if arr[j] < pivot: + # 将小于基准数的元素 arr[j] 与当前 arr[i] 进行换位,保证位置 i 之前的元素都小于基准数 + arr[i], arr[j] = arr[j], arr[i] + # i 之前的元素都小于基准数,所以 i 向右移动一位 + i += 1 + # 将基准节点放到正确位置上 + arr[i - 1], arr[low] = arr[low], arr[i - 1] + # 返回基准数位置 + return i - 1 + + def quickSort(self, arr, low, high, k): + size = len(arr) + if low < high: + # 按照基准数的位置,将序列划分为左右两个子序列 + pi = self.randomPartition(arr, low, high) + if pi == k: + return arr[:k] + if pi> k: + # 对左子序列进行递归快速排序 + self.quickSort(arr, low, pi - 1, k) + if pi < k: + # 对右子序列进行递归快速排序 + self.quickSort(arr, pi + 1, high, k) + + return arr[:k] + + def getLeastNumbers(self, arr: List[int], k: int) -> List[int]: + size = len(arr) + if k>= size: + return arr + return self.quickSort(arr, 0, size - 1, k) +``` + +### 思路 2:复杂度分析 + +- **时间复杂度**:$O(n)$。证明过程可参考「算法导论 9.2:期望为线性的选择算法」。 +- **空间复杂度**:$O(\log_2 n)$。递归使用栈空间的空间代价期望为 $O(\log_2n)$。 +