分治解决问题的思想就是大事化小,将一个大问题分解成若干个子问题。
1.颜色分类
这道题其实就是让我们对数组进行操作,使操作之后的数组呈现出三段区间[0,left],[left+1,right-1],[right,size-1],这三个区间的元素分别都是0,1,2.
既然是进行数组分块,我们就可以使用双指针的思想来解决:首先定义一个指针i,用来扫描数组;然后在分别创建两个指针left和right。我们的目的是将数组分为三块,所以left表示左边区间的右端点,right表示右边区间的左端点。有了这两个端点,那么我们就可以知道中间区间的左右端点[left+1, right-1].
综上,我们借助双指针的思想,设计出了三指针来解决该题,下来我们分析这三个指针的移动问题:
刚开始,i还没有扫描任何数据,所以左右区间都还没有数据,我们应该让left = -1,right = size。
i开始扫描数组,当扫描到一个数是0的时候,此时该数应该归到左区间,此时我们应该让i位置的元素,与left+1位置的元素进行交换。 因为left位置向前都是0了,直接与left交换没有意义。扫描出了0,就应该让0区间扩大1,所以要与left+1位置进行交换;接着让i++,扫描下一个位置。
i扫描到一个数是2的话,与上面类似,2要归到右区间,所以要与right-1进行交换。但是注意,交换之后i不能++,因为这个位置本来就是没有被扫描的位置。
i扫描到一个数是1的话,则直接让i++,因为我们在扫描的过程中,一直都保持了left是0的最后一个,right是2的第一个,所以中间的就是1。遇到1就直接让i++即可。
当i == right时,就说明所有的数都已经被扫描了,此时循环结束。
时间复杂度:O(N),只是用i指针遍历了一遍数组
空间复杂度:O(1),该算法只使用了3个int变量来表示指针
// C++
class Solution
{
public:
void sortColors(vector<int>& nums)
{
int i=0, left=-1, right=nums.size();
while(i < right)
{
if(nums[i] == 1) i++;
else if(nums[i] == 0) swap(nums[++left],nums[i++]);
else swap(nums[--right],nums[i]);
}
}
};
# Python
class Solution:
def sortColors(self, nums: List[int]) -> None:
i, left, right = 0, -1, len(nums)
while i < right:
if nums[i] == 0:
nums[left+1],nums[i] = nums[i],nums[left+1]
left += 1
i += 1
elif nums[i] == 1:
i += 1
else:
nums[right-1],nums[i] = nums[i],nums[right-1]
right -= 1
2.排序数组
使用算法使数组变成升序,而且时间复杂度限制为O(nlogn),且要使用低的空间复杂度,很显然我们要使用快速排序来解决。
快速排序的过程是:选择一个基准元素pivot,然后遍历该数组,将数组划分为两个子区间,左边区间全都小于pivot,右边区间全都大于等于pivot。然后再分别递归左右子区间。直到区间只剩一个元素为止。这也是快速排序的最重要的一步——哨兵划分(partition)
但是当我们有了上一题的基础,我们可以将数组划分为三块,而不再是两块了。
当我们了解了哨兵划分,接下来的问题就是选择基准元素pivot。快速排序默认以区间的左端点的值作为基准值。但是这样选择基准值会导致快速排序的性能下降。如果数组是本身就是升序的,我们选择左边的元素作为基准值,此时划分之后的区间,左区间没有元素,而右区间为n-1个元素。接着重复该过程。每次会减少一个元素,每次都会遍历整个数组,所以时间复杂度就变成了O(n2)。
所以我们要对选取基准值进行优化:
1、随机选取基准值:我们生成一个随机数x,然后让x%(right-left+1),即区间长度,就可以使随机值的范围再[0,right-left],但是我们的区间是从上一层划分下来的,所以不一定是从0开始的,我们再给这个区间加上left,新区间为[left,right],刚好是我们进行partition的区间。此时就会生成一个在区间内的随机数。
int getRandomKey(vector<int>& nums, int left, int right){
int r = rand() % (right - left + 1) + left;
return nums[r];
}
2、三数取中:我们对待分块的区间进行三数取中,这三个数分别是左右端点以及区间中点的值,我们取其中不大不小的值作为基准值pivot,即次大的数。
int medianThree(vector<int> &nums, int left, int mid, int right) {
int l = nums[left], m = nums[mid], r = nums[right];
if ((l <= m && m <= r) || (r <= m && m <= l))
return mid; // m 在 l 和 r 之间
if ((m <= l && l <= r) || (r <= l && l <= m))
return left; // l 在 m 和 r 之间
return right;
}
采用上面两种基准值的选法,就会降低快速排序退化到O(n2)的概率。
总结:快速排序的时间复杂度之所以为O(nlogn),是因为快速排序划分区间进行递归地行为就像二叉树的前序遍历一样。 而这棵树的高度是logn,而每层都要遍历数组,所以整体的时间复杂度为O(nlogn)
时间复杂度:O(nlogn)
空间复杂度:O(logn),最坏情况下O(n),空间复杂度主要在于递归调用开辟的栈空间。
// C++
class Solution
{
public:
int getRandomKey(vector<int>& nums, int left, int right)
{
int r = rand() % (right - left + 1) + left;
return nums[r];
}
void qsort(vector<int>& nums, int l, int r)
{
if (l >= r) return;
int key = getRandomKey(nums,l,r);
int i = l, left = l-1, right = r+1;
while(i < right)
{
if(nums[i] < key) swap(nums[++left],nums[i++]);
else if(nums[i] == key) i++;
else swap(nums[--right],nums[i]);
}
// [l,left] [left+1,right] [right,r]
qsort(nums,l,left);
qsort(nums,right,r);
}
vector<int> sortArray(vector<int>& nums)
{
srand(time(NULL));
qsort(nums,0,nums.size()-1);
return nums;
}
};
3.数组中最大的第k个元素
找出数组排序后的第k大的元素。若我们排成升序数组,那么第k大其实就是倒数第k个元素。
这里我们采用快速选择算法——基于快速排序的选择算法:
我们依旧先对数组进行partition操作,将数组分为[0,left],[left+1, right-1],[right,size-1]。我们分别求出这三个区间的元素个数a\b\c。
如果c >= k的话,那么说明我们要找的倒数第k个元素就在右区间中。此时我们只需要递归右区间即可; 如果b+c>=k,那么说明这个倒数第k个元素一定在中间区间,而中间这个区间的值都是pivot,所以我们直接返回pivot即可; 如果a+b+c>=k的话,那么说明倒数第k个元素在左区间,此时我们只需要递归左区间即可,但是在递归左区间的时候就不再是寻找倒数第k个元素了,而是倒数第k-a-b个数了。中间和右边的元素有b+c个,对于整个数组是倒数第k个,对于左边区间,它是倒数第k-b-c个,因为左边区间一共才a个数,不可能还是倒数第k个。
当区间只剩一个数时,这个就是答案。
时间复杂度:O(n),快速选择算法在快速排序的基础上,对递归地区间进行选择,算法导论这本书里有严格的证明
空间复杂度:O(logn),递归所使用的栈空间
// C++
class Solution
{
public:
int getRandomPivot(vector<int>& nums,int l, int r)
{
return nums[rand()%(r-l+1)+l];
}
int qsort(vector<int>& nums, int l, int r, int k)
{
// 如果区间仅剩一个值,那么该值就是第k大的元素
if(l == r) return nums[l];
int key = getRandomPivot(nums,l,r);// 随机基准值
int left=l-1, right=r+1, i=l;// 待partition的区间
// 通过partition将区间分为三部分
// [l,left] [left+1,right-1] [right,r]
// <pivot == pivot >pivot
while(i<right)
{
if(nums[i] < key) swap(nums[++left],nums[i++]);
else if(nums[i] == key) i++;
else swap(nums[--right],nums[i]);
}
// 记录对应区间的元素个数a、b、c
int b = right - left - 1, c = r - right + 1;
// 根据这三个区间的个数进行分类讨论
// 第k大元素,其实就是排序后的倒数第k个元素
// 首先需要清楚的就是,这三个区间就算再进行排序,元素也只会再各自的区间内移动交换,不会换到其他区间
// 如果c>=k,则表明倒数第k个元素一定就在这个区间中,接下来只需要递归该区间即可,而且对于整个数组来说倒数第k个和右边区间的倒数第k个是一样的
// a b c
// [l, left] [left+1, right-1] [right, r]
// ^
// 如果b+c>=k且c<k,则表明倒数第k个元素一定再中间的区间,而中间的区间的值全都是pivot,所以直接返回基准值
// a b c
// [l, left] [left+1, right-1] [right, r]
// ^
// 剩下一种情况就是a+b+c>=k了,bc两个区间合起来都没有k个数,那么倒数第k个数一定在左边区间
// 而当我们对左边区间进行递归时,对于这个区间就不再是寻找倒数第k个了,中间和右边的元素有b+c个,对于整个数组是倒数第k个,对于左边区间,它是倒数第k-b-c个,因为左边区间一共才a个数,不可能还是倒数第k个
// a b c
// [l, left] [left+1, right-1] [right, r]
// ^
if(c>=k) return qsort(nums,right,r,k);
else if(b+c >= k) return key;
else return qsort(nums,l,left,k-b-c);
}
int findKthLargest(vector<int>& nums, int k)
{
srand(time(NULL));// 生成随机数种子
return qsort(nums,0,nums.size()-1,k);
}
};
4.最小的k个数
题目简单来说就是要返回最小的cnt个数。
法一:优先级队列
优先级队列,priority_queue默认是大堆,我们需要传模板参数greater<int>使其成为小堆,借助就循环cnt次,依次取出堆顶元素即可。
//法一:优先队列
vector<int> inventoryManagement(vector<int>& stock, int cnt)
{
priority_queue<int, std::vector<int>, greater<int>> heap(stock.begin(),stock.end());
vector<int> ret;
while(cnt--)
{
int x = heap.top();
heap.pop();
ret.emplace_back(x);
}
return ret;
}
法二:快速选择算法
我们依旧借助快排的思想,将最小的k个数调整到数组的最前面即可,返回[0,cnt-1]这个区间的元素即可。
这道题与上面一道题一样,我们依旧将数组进行partition分为三块,[0,left],[left+1, right-1],[right,size-1],这三个区间的元素个数a\b\c。
如果a>cnt,说明前cnt个数就在左区间内,但是不能直接返回,虽然前cnt个元素在左区间,但是左区间还有大于前cnt元素的值在,如果直接返回,就有可能导致错误结果。正确的方法是对左区间进行递归,在左区间中继续寻找前cnt个数。
如果a+b>=cnt,此时前cnt个数就已经分布在了数组的[0,cnt]处。因为中间区间的数都是一样的,所以不用担心,直接返回即可。
如果a+b+c>=cnt,说明前cnt个元素遍布这三个区间,但是我们只需要在对有区间进行遍历即可。因为左区间和中间区间的所有数都肯定小于右区间的数,所以这两个区间的数就是前cnt的某个数,我们只需要在对右区间进行递归,找出其前cnt-a-b个元素即可。
当我们在递归地过程中,发现递归地区间个数r-l+1 == cnt,那么直接返回即可。
时间复杂度:O(n),快速选择算法的时间复杂度在算法导论中有证明。
空间复杂度:O(logn),递归中的栈空间消耗
// C++
class Solution
{
public:
// 法二: 快速选择算法
int getRandomPivot(vector<int>& nums, int l, int r)
{
return nums[rand() % (r - l + 1) + l];
}
vector<int> partition(vector<int>& nums, int l, int r, int pivot)
{
int left = l - 1, right = r + 1, i = l;
while (i < right)
{
if (nums[i] < pivot) swap(nums[++left], nums[i++]);
else if (nums[i] == pivot) i++;
else swap(nums[--right], nums[i]);
}
return { left,right };
}
void quick_select(vector<int>& nums, int l, int r, int cnt)
{
if (r - l + 1 == cnt) return;
int pivot = getRandomPivot(nums, l, r);
vector<int> tmp = partition(nums, l, r, pivot); // 返回left和right的最终位置
// [l, left] [left + 1, right - 1] [right, r]
int a = tmp[0] - l + 1, b = tmp[1] - tmp[0] - 1;
if (a > cnt) return quick_select(nums, l, tmp[0], cnt);
else if (a + b >= cnt) return;
else return quick_select(nums, tmp[1], r, cnt - a - b);
}
vector<int> inventoryManagement(vector<int>& stock, int cnt)
{
srand(time(NULL));
quick_select(stock, 0, stock.size() - 1, cnt);
return { stock.begin(),stock.begin() + cnt };
}
};