问题
给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,
原地 对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
必须在不使用库内置的 sort 函数的情况下解决这个问题。
思路
荷兰国旗问题
荷兰国旗问题的解决方案,这个问题是由高德纳提出的,因此也被称为“高德纳排序”或“三向切分快速排序”。
这个问题的关键在于,我们有三个颜色的元素,而不是两个,
这就需要我们在遍历数组时,维护三个指针:
一个指向红色元素应该放置的位置(red),
一个指向当前遍历的位置(current),
以及一个指向蓝色元素应该放置的位置(blue)。
以下是解决这个问题的步骤:
初始化三个指针:
red 指向数组的开始,current 也指向数组的开始,blue 指向数组的末尾。
遍历数组,从 current 开始,直到 current 等于 blue。
如果 nums[current] 为 0(红色),则交换 nums[red] 和 nums[current],然后 red 和 current 都向前移动一位。
如果 nums[current] 为 1(白色),则 current 向前移动一位。
如果 nums[current] 为 2(蓝色),则交换 nums[blue] 和 nums[current],然后 blue 向后移动一位。
重复步骤 2,直到 current 等于 blue。
这种方法不需要额外的存储空间,因为我们在原地对数组进行排序,且时间复杂度为 O(n)。
算法
class Solution:
def sortColors(self, nums: list[int]) -> None:
red, current, blue = 0, 0, len(nums) - 1
while current <= blue:
if nums[current] == 0:
nums[red], nums[current] = nums[current], nums[red]
red += 1
current += 1
elif nums[current] == 1:
current += 1
else: # 也就是nums[current] == 2
nums[blue], nums[current] = nums[current], nums[blue]
blue -= 1
# 示例
if __name__ == '__main__':
nums = [2, 0, 2, 1, 1, 0]
Solution().sortColors(nums)
print(nums) # 输出: [0, 0, 1, 1, 2, 2]
时间和空间复杂度
因为它只需要遍历数组一次。空间复杂度是 O(1),因为只使用了常量额外空间。
举例:
通过一个具体的例子 `[1, 2, 0]` 来解释荷兰国旗排序算法的应用过程,并通过图形来展示每一步的变化。
### 初始状态
数组为 `[1, 2, 0]`,我们有三个指针:`red` 指向 0,`current` 指向 0,`blue` 指向 2。
### 第一步
- `current` 指向 1(白色),不满足任何条件,`current` 移动到下一个位置。
- 状态变为 `[1, 2, 0]`,`current` 指向 2。
### 第二步
- `current` 指向 2(蓝色),触发 `else` 分支,交换 `nums[blue]` 和 `nums[current]`。
- 交换后,数组变为 `[1, 0, 2]`,`blue` 向左移动,`current` 不动。
### 第三步
- `current` 仍然指向 0(红色),触发 `if` 分支,交换 `nums[red]` 和 `nums[current]`。
- 交换后,数组变为 `[0, 1, 2]`,`red` 和 `current` 都向右移动。
### 结束条件
- `current` 已经越过 `blue`,循环结束。
最终,数组被排序为 `[0, 1, 2]`,红色、白色、蓝色元素按顺序排列。
通过这种方式,我们可以看到数组是如何通过维护三个指针来实现原地排序的。这种方法的时间复杂度是 O(n),因为它只需要遍历数组一次。空间复杂度是 O(1),因为只使用了常量额外空间。
疑惑
1、为什么是 while current <= blue:,而不是 while red <= blue?
回答:
在荷兰国旗排序问题中,使用 `while current <= blue:` 而不是 `while red <= blue:` 的原因是我们需要确保在遍历数组时,`current` 指针能够正确地处理所有三种颜色的元素(红色、白色和蓝色),并且能够正确地与 `red` 和 `blue` 指针交互。
这里是为什么使用 `while current <= blue:` 的原因:
1. **`current` 指针的作用**:`current` 指针用于遍历数组中的每个元素。它从数组的开始位置(索引 0)开始,一直移动到数组的末尾(索引 `len(nums) - 1`)。
2. **处理所有颜色**:`current` 指针需要检查数组中的每个元素,并根据元素的颜色(0、1 或 2)进行相应的操作。如果元素是红色(0),则与 `red` 指针指向的元素交换;如果元素是蓝色(2),则与 `blue` 指针指向的元素交换;如果元素是白色(1),则不需要交换,`current` 指针直接向前移动。
3. **维护 `red` 和 `blue` 指针**:`red` 指针用于跟踪下一个红色元素应该放置的位置,而 `blue` 指针用于跟踪下一个蓝色元素应该放置的位置。这两个指针分别从数组的两端向中间移动,确保红色和蓝色元素被正确地放置在数组的两端。
4. **避免重复检查**:如果使用 `while red <= blue:`,那么在某些情况下,`current` 指针可能会跳过一些元素,导致算法无法正确地处理所有元素。例如,如果 `current` 指针在 `red` 和 `blue` 之间,那么这些元素将不会被检查和排序。
5. **确保稳定性**:使用 `while current <= blue:` 可以确保算法在一次遍历中完成排序,这是原地排序算法的一个重要特性。它保证了算法的时间复杂度为 O(n),并且不需要额外的存储空间。
综上所述,使用 `while current <= blue:` 可以确保算法正确、高效地对数组进行排序,同时满足原地排序的要求。
2、为什么不对白色元素维护指针呢
回答:
在荷兰国旗问题中,确实不需要为白色元素维护一个专门的指针,原因如下:
1. **白色元素的自然排序**:在这个问题中,白色元素(用数字1表示)是介于红色元素(用数字0表示)和蓝色元素(用数字2表示)之间的。通过维护红色和蓝色元素的指针,我们可以确保所有红色元素都在白色元素的前面,所有蓝色元素都在白色元素的后面。这样,白色元素自然会被“挤压”到中间的位置。
2. **简化问题**:通过只关注红色和蓝色元素,我们可以将问题简化为两个部分:将红色元素移动到数组的前面,将蓝色元素移动到数组的后面。白色元素不需要额外的操作,它们会自动填充剩余的空间。
3. **减少复杂性**:如果为每种颜色都维护一个指针,算法的复杂性会增加,因为需要处理更多的指针移动和元素交换。在荷兰国旗问题中,通过只使用两个指针,我们可以保持算法的简洁性和高效性。
4. **算法效率**:在荷兰国旗问题中,我们的目标是尽量减少元素交换的次数,以提高算法的效率。由于白色元素不需要特定的排序操作,它们可以自然地填充在红色和蓝色元素之间,这有助于减少不必要的交换。
5. **空间复杂度**:题目要求使用常量额外空间,这意味着我们不能使用额外的数组或数据结构来存储元素。通过只使用两个指针,我们满足了这一要求。
综上所述,不对白色元素维护指针是因为在荷兰国旗问题中,白色元素可以自然地排序在红色和蓝色元素之间,这样做可以简化算法,提高效率,并且满足空间复杂度的要求。
3、 荷兰国旗问题中,如何确定红色和蓝色元素的边界?
回答:
在荷兰国旗问题中,确定红色和蓝色元素的边界是通过使用两个指针来实现的,这两个指针分别从数组的两端向中间移动。这里是一个详细的步骤说明:
1. **初始化指针**:
- `red` 指针初始化在数组的起始位置,即 `red = 0`。
- `blue` 指针初始化在数组的末尾位置,即 `blue = len(nums) - 1`。
- `current` 指针用于遍历数组,初始化在数组的起始位置,即 `current = 0`。
2. **遍历数组**:
- 从 `current` 指针开始,遍历数组直到 `current` 达到 `blue` 指针的位置。
- 在遍历过程中,根据 `nums[current]` 的值来决定如何移动指针。
3. **处理红色元素**:
- 如果 `nums[current]` 的值为 0(代表红色),则需要将 `current` 位置的元素与 `red` 位置的元素交换。
- 交换后,`red` 指针向右移动一位(`red++`),`current` 指针也向右移动一位(`current++`)。
4. **处理白色元素**:
- 如果 `nums[current]` 的值为 1(代表白色),不需要进行交换,只需将 `current` 指针向右移动一位(`current++`)。
5. **处理蓝色元素**:
- 如果 `nums[current]` 的值为 2(代表蓝色),则需要将 `current` 位置的元素与 `blue` 位置的元素交换。
- 交换后,`blue` 指针向左移动一位(`blue--`),但 `current` 指针不移动,因为我们需要重新检查交换后的元素。
6. **循环条件**:
- 循环继续,直到 `current` 指针超过 `blue` 指针。这是因为在最坏的情况下,所有元素都可能是同一种颜色,导致 `current` 需要遍历整个数组。
7. **结束条件**:
- 当 `current` 指针超过 `blue` 指针时,循环结束。此时,所有红色元素都在 `red` 指针的左边,所有蓝色元素都在 `blue` 指针的右边,中间的元素都是白色的。
通过这种方法,我们可以确保红色和蓝色元素的边界被正确地确定,而不需要额外的空间。这种算法的时间复杂度是 O(n),因为它只需要遍历数组一次。
学习
1、初始时,red 指向数组的开头,white 也指向数组的开头,blue 指向数组的结尾。
应该写为:red, current, blue = 0, 0, len(nums) - 1
#这个意思是 nums[0] = nums[red]、nums[white], 指针指向的是索引,所以指针就是角标
2、结尾这个:
else :
nums[current], nums[blue] = nums[blue], nums[current]
blue -= 1
current += 1
不能再 current += 1
这样会换多了