引言
算法是码农的套路。而提炼算法中的套路,然后学以致用,才能进阶。
思想
快速排序算法的基本思想是分治策略(Divide-and-Conquer Method)。步骤如下:
1. 选取一个基准元素(pivot)
2. 比pivot小的放到pivot左边,比pivot大的放到pivot右边
3. 对pivot左边的序列和右边的序列分别递归的执行步骤1和步骤2,直到各区间只有一个数。
伪代码如下:
QUICKSORT(A, begin, end)
if begin < end
q = PARTITION(A, begin, end)
QUICKSORT(A, begin, q-1)
QUICKSORT(A, q+1, end)
//将数组分为两个子数组,左边的小于主元,右边的大于主元,返回临界值下标
PARTITION(A, begin, end)
pivot = A[end] //以最后一个数为主元(pivot element)
left_end = begin-1 //left_end指向左子数组的尾部
for i = begin to end-1//遍历整个数组
if A[i] < pivot//找到了左子数组中的元素
left_end = left_end+1 //增加左子数组的大小
exchange A[left_end] with A[i] //将A[i]换入左子数组,换出的left_end肯定比主元大
exchange A[left_end+1] with A[end] //将主元从数组末尾移动至子数组之间
return left_end + 1
关于“换出的left_end肯定比主元大”:遍历到大于主元的数时,仅i增加,left_end不变;一旦遍历到小于主元的数,left_end = left_end+1,left_end指向下一个大于主元的数,将该数与最新遍历到的小于主元的数交换。
例如:i从begin开始遍历,假如第一个数begin大于主元,此时++i,不交换;假如第二个数begin+1小于主元,则left_end = left_end+1,此时left_end指向begin,交换后将小于主元的A[i]换入左子数组,大于主元的left_end换出。
代码
int mypartition(vector<int>&arr, int begin, int end)
{
int pivot = arr[end];//选最后一个元素作为主元
int location = begin-1;//location指向比pivot小的元素段的尾部
for(int i = begin; i < end; i++)//比主元小的元素依次放在前半部分
if(arr[i] < pivot)
swap(arr[i], arr[++location]);
swap(arr[end], arr[location+1]);
return location+1;
}
快速排序主函数:
void quicksort(vector<int>&arr, int begin, int end)
{
if(begin < end)
{
int middle = mypartition(arr, begin, end);
quicksort(arr, begin, middle-1);
quicksort(arr, middle+1, end);
}
}
其他
1.快速排序另一种写法更便于理解:
int mypartition(vector<int>&arr, int low, int high)
{
int pivot = arr[low];//选第一个元素作为枢纽元
while(low < high)
{
while(low < high && arr[high] >= pivot)high--;
arr[low] = arr[high];//从后面开始找到第一个小于pivot的元素,放到low位置
while(low < high && arr[low] <= pivot)low++;
arr[high] = arr[low];//从前面开始找到第一个大于pivot的元素,放到high位置
}
arr[low] = pivot;//最后枢纽元放到low的位置
return low;
}
2.与上面思想类似的套路:
int mypartition(vector<int>&a, int left, int right)
{
int i,j,t,temp;
temp=a[left]; //temp中存的就是基准数
i=left;
j=right;
while(i!=j)
{
//顺序很重要,要先从右边开始找
while(a[j]>=temp && i<j)
j--;
//再找右边的
while(a[i]<=temp && i<j)
i++;
//交换两个数在数组中的位置
if(i<j)
{
t=a[i];
a[j]=a[j];
a[j]=t;
}
}
//最终将基准数归位
a[left]=a[i];
a[i]=temp;
return i;
}
3.关于基准数选取:最好的方法是用一个随机函数产生一个取位于 left 和 right 之间的随机数 k(left≤k≤right),这样可以使划分基准的选择是随机的,从而可以期望划分是较对称的。
4.Partition函数除了用在快速排序,还可以用O(n)的平均时间复杂度从无序数组中寻找第k大的值:
int GetArrayMaxK(int array[], int nStart, int nEnd, int k)
{
if (k <= 0)
{
throw;
}
int nPos = -1;
while (true)
{
nPos = Partition(array, nStart, nEnd);
if ((nPos+1) == k) //左边的数均小于array[nPos]
{
return array[nPos];
}else if ((nPos+1) > k)
{
nEnd = nPos - 1;
}else
{
nStart = nPos + 1;
}
}
}
参考
快速排序partition过程常见的两种写法+快速排序非递归实现
坐在马桶上看算法:快速排序
算法导论-排序(二)快速排序、随机化快速排序
排序算法(三)–关于快速排序Partition的思考