我在做力扣中“数组中的第k个最大元素”问题中,有用到快速排序的思想。现在回顾一下快速排序。
快速排序的主要思想是选择一个基准,把数组中大于该基准的放在基准右边,小于该基准的放在基准左边。
基于力扣题解的快速选择,改造成了快速排序,代码如下:
void quickselect(vector<int>& nums, int l, int r) {
int i=l,j=r;
int partition=nums[l];
while(i<j){
while(nums[i]<partition)i++;
while(nums[j]>partition)j--;
if(i<j)swap(nums[i],nums[j]);
}
if(l<r){
quickselect(nums, l, j-1);
quickselect(nums, j+1, r);
}
}
定义两个指针,i移动到大于基准的最左边元素,j移动到小于基准的最右边元素,然后交换;再次移动i和j的指针,知道i==j,进入递归。
另外一种比较好理解的快速排序为:
void quickselect(vector<int>& nums,int l,int r){
if(l>=r){
return;//终止条件:子数组长度为0或1时无需排序
}
int lt=l;//小于区域的右边界
int gt=r;//大于区域的左边界
int i=l+1;//当前元素指针
int partition=nums[l];//基准元素
while(i<=gt){
if(nums[i]<partition){
swap(nums[i],nums[lt]);//将当前元素交换到小于区域中
lt++;
i++;
}
if(nums[i]>partition){
swap(nums[i],nums[gt]);
gt--;
}
}
//递归处理小于区域,大于区域
quickselect(nums, l, lt-1);
quickselect(nums, gt+1, r);
}
即二路快速排序,在函数开始首先判断子数组长度,如果为0或者1时就不用排序直接返回即可。
然后我们定义两个区域:小于区域和大于区域,基准的左边为小于区域,右边为大于区域。
由于数组本身有左右边界,所以我们只用定义小于区域的右边界和大于区域的左边界就可以了。
然后i表示当前遍历元素的指针,从基准的后边一位开始,由于在最开始我们已经排除了数组长度为0或1的情况,所以i=l+1不会超出数组边界。
i小于等于大于区域的左边界进入循环。
在循环中我们进行当前元素与基准元素的判断,如果小于说明当前元素应该在小于区域,这时候需要将当前元素交换到小于区域中,同时小于区域的右边界++,i++
如果大于基准元素,将当前元素交换到大于区域,大于区域的左边界--
我们看一下交换的本质,当前元素大于基准的情况下,先将当前元素与gt位置元素进行交换之后gt-1,因此gt之后的元素都是大于基准元素的。
而当前元素小于基准的情况下,当前元素与lt交换,因此lt位置之前的元素都是小于基准的。
已知lt与i是同时++的,那么当i=gt的时候此时lt与gt之间相差1
所以我们可以知道:
左边的红色圈中的元素都小于基准,右边都大于基准。
并且lt位置的元素是基准,因为i是从l+1开始的,基准的开始为l,那么只有在第一个if中涉及到交换基准,而之后i的位置都会在基准的后一个位置。
所以如果进入第一个if,基准会在gt位置,然后退出循环,满足基准左侧是小于区域,右侧是大于区域。
如果进入第二个if,i与gt在一个位置交换后不改变,然后gt到lt的位置,同样的,满足基准左侧是小于区域,右侧是大于区域。
因此while循环的条件应该是i<=gt,由于gt位置之后才是大于区域所以i不应该超过gt。gt位置本身的元素并没有进行判断,所以i是可以等于gt的。
之后就是在两个区域的递归了。
可是这样的快速排序有一个问题,如果数组中有重复的元素,在判断中没有等于的情况,即指针会发生停滞,从而在while中造成死循环。
那么如果解决呢?
我们可以采用等于划分法,就是在判断中添加等于的判定。
代码如下:
void quickselect(vector<int>& nums, int l, int r) {
if (l >= r) {
return; // 终止条件:子数组长度为0或1时无需排序
}
int lt = l; // 小于区域的右边界
int gt = r; // 大于区域的左边界
int i = l + 1; // 当前元素指针
int partition = nums[l]; // 基准元素
while (i <= gt) {
if (nums[i] < partition) {
swap(nums[i], nums[lt]); // 将当前元素交换到小于区域
lt++;
i++;
} else if (nums[i] > partition) {
swap(nums[i], nums[gt]); // 将当前元素交换到大于区域
gt--;
} else {
i++; // 当前元素等于基准元素,移动到下一个元素
}
}
// 递归处理小于区域和大于区域
quickselect(nums, l, lt - 1); // 小于区域
quickselect(nums, gt + 1, r); // 大于区域
}
如果出现了相等的元素那么我们就不再进行交换而是直接遍历后面的元素。这样i与lt之间就会拉开距离了。i与lt之间的元素就是重复的元素。
即while循环后会出现这样的情况: