partition算法有着非常重要的应用,这个算法的思想虽然简单,但具体实现的细节却比较多,今天我重点复习了这个算法,本文记录我对这个算法的理解。
Partition算法解析
二分Partition
快速排序作为非常著名的排序算法,其思想却很简单:每次从数组中选一个数作为pivot,然后将数组划分为2部分,小于等于pivot数的在其左边,大于等于pivot的数在其右边,然后分别对pivot的左边和右边进行递归。
二分partition的实现用到了双指针,left寻找一个比pivot大的数,right寻找一个比pivot小的数,然后交换它们,循环这一过程直到left == right,将pivot放到这个位置。
注意: 如果pivot选的是最左的元素,则要先移动right;如果pivot选的是最右的元素,则要先移动left。
int partition(vector<int>& nums, int left, int right) {
int pivot = nums[left], base = left;
while (left < right) {
while (left < right && nums[right] >= pivot) right--;
while (left < right && nums[left] <= pivot) left++;
swap(nums[left], nums[right]);
}
nums[base] = nums[left];
nums[left] = pivot;
return left;
}
三分Partition
三分partition自然就是把数组划分成3部分,小于pivot的在左边,等于pivot的在中间,大于pivot的在右边。
由于三分partition要保证中间的那部分,所以像二分partition那么划分是肯定不行的。为了解决这个问题,我们需要先定义循环不变量:
[0, zero)内的元素都等于0[zero, curr)内的元素都等于1[curr, two]内的元素还未被检查(two, len - 1]内的元素都等于2
我们用curr来遍历数组,遍历的范围是[0, two]。在整个遍历过程中,始终要保证循环不变量的正确,因此,当遇到0时,要与nums[zero]交换,遇到2时,要与nums[two]交换。
细节: 每次交换时,边界zero或者two自然是要相应地进行移动,那么curr呢?
nums[curr] == 1时,显然curr++nums[curr] == 0时,如果curr == zero,那么由于zero需要右移,curr则也必须右移,否则curr < zero,破坏了循环不变量;如果curr > zero,此时nums[zero] == 1,那么会把1交换到curr的位置,即使不立即curr++,下次循环时也会由于nums[curr] == 1而curr++nums[curr] == 2时,num[two]的值是不确定的,所以我们只进行交换和左移two,不移动curr
初始化: 初始化要保证除了[curr, two]外的3个区间都为空
二分Partition应用
我们以leetcode第215题:找数组中的第k个最大元素为例来说明二分partition算法在快速排序中的应用。
这道题虽然不是直接让我们进行快速排序,但我们可以想到,每次partition之后都会返回pivot最终在数组中的位置mid,如果mid就是要找的位置,那就不用再继续递归partition下去了,答案已经找到;否则,继续在mid的左边或者右边进行递归partition。
时间复杂度
快速排序的时间复杂度跟选取的pivot有关,如果每次都选最左边的作为pivot,而它刚好又是数组中最小的元素,那么这次partition需要遍历完整个数组,即遍历n次;接着递归右部分(有n-1个元素),如果同样最左边的是最小的,那么需要遍历n-1次…
如果每次都将数组划分成1和n-1两部分,每次又继续递归n-1的那部分,那么就会导致快排的最差时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)。
为了改善这种情况,可以随机选取pivot。
代码
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
srand(time(0));
int left = 0, right = nums.size() - 1;
int target = nums.size() - k;
int mid = partition(nums, left, right);
while (mid != target) {
if (mid < target) {
left = mid + 1;
mid = partition(nums, left, right);
}
else {
right = mid - 1;
mid = partition(nums, left, right);
}
}
return nums[mid];
}
private:
int partition(vector<int>& nums, int left, int right) {
// 使用随机来加速快排, 效果明显
int i = rand() % (right - left + 1) + left;
swap(nums[i], nums[left]);
int pivot = nums[left], base = left;
while (left < right) {
while (left < right && nums[right] >= pivot) right--;
while (left < right && nums[left] <= pivot) left++;
swap(nums[left], nums[right]);
}
nums[base] = nums[left];
nums[left] = pivot;
return left;
}
void swap(int& num1, int& num2) {
int tmp = num1;
num1 = num2;
num2 = tmp;
}
};
三分Partition应用
leetcode第75题:颜色分类就是一道典型的三分partition应用题。
代码
class Solution {
public:
void sortColors(vector<int>& nums) {
int left = 0, right = nums.size() - 1;
int curr = 0;
while (curr <= right) {
if (nums[curr] == 0) swap(nums[left++], nums[curr++]);
else if (nums[curr] == 2) swap(nums[right--], nums[curr]);
else curr++;
}
}
private:
void swap(int& num1, int& num2) {
int tmp = num2;
num2 = num1;
num1 = tmp;
}
};
本文详细探讨了Partition算法,包括二分Partition和三分Partition的原理及实现细节。二分Partition在快速排序中的应用,如解决找数组中第k个最大元素的问题;三分Partition的应用举例是颜色分类问题。文章强调了正确维护循环不变量的重要性,并指出随机选择pivot能优化快速排序的时间复杂度。
1984

被折叠的 条评论
为什么被折叠?



