1. 划分
快速排序中最重要的步骤就是划分,即选择一个元素(很多地方称为枢纽元素),将序列划分成两部分,比枢纽元素大的放在枢纽元素的右边,比枢纽元素小的放在枢纽元素的左边。
划分函数的思想:
1. 选择一个枢纽元素(以选择序列的第一个元素为例),暂存到临时变量中
2. 从序列的右边向左查找,找到一个比枢纽元素小的值。若找到,则将这个值填入到枢纽元素的位置
3. 从序列的左边向右查找,找到一个比枢纽元素大的值。若找到,则将这个值填入到step 2中那个比枢纽元素小的元素的位置。
4. 重复step2, step3,直到向右查找的“指针”(索引)和向左查找的“指针”(索引)碰到一起。表明一次划分就完成了。
按照上述思想实现代码如下:
int partition(int *arr, int l, int r)
{
int i = l, j = r;
int key = arr[l]; //枢纽元素
while (i < j)
{
while (i < j && arr[j] >= key) j--; //从右往左找到一个比枢纽小的
if (i < j)
{
arr[i] = arr[j];
i++;
}
while (i < j && arr[i] < key) i++; //从右往左找到一个比枢纽大的
if (i < j)
{
arr[j] = arr[i];
j--;
}
}
arr[i] = key;
return i;
}
2. 递归快排
递归的方式是最常见的方式,因为递归的写法代码简短。
算法思想:对元素进行划分,划分一次之后分别对两个子序列进行划分。直到子序列的元素个数变成1为止。
void QuickSort(int *arr, int l, int r)
{
if (l < r)
{
int par = partition(arr, l, r);
QuickSort(arr, l, par - 1);
QuickSort(arr, par + 1, r);
}
}
3. 使用栈作为辅助空间的快排
递归的过程其实就可以理解为一个栈,因为递归是依靠函数调用压栈的方式实现的。所以我们可以手动将中间过程保留,来进行快排。
算法思想:
1. 对序列进行一次划分,划分后的两个子序列头尾都压入栈中。
2. 在栈非空的情况下,从栈中取出两个元素(子序列的头尾位置)。对子序列进行划分。
3. 同样,对子序列进行划分的时候也将划分后的序列头尾压入栈中。
4. 重复step2 step3,知道栈为空为止。
void QuickSort2(int *arr, int l, int r)
{
stack<int> tmp; //临时栈
if (l < r) //
{
int par = partition(arr, l, r); //
if (l < par - 1) //
{
tmp.push(l); //
tmp.push(par - 1);
}
if (par + 1 < r)
{
tmp.push(par + 1);
tmp.push(r);
}
while (!tmp.empty())
{
r = tmp.top(); tmp.pop();
l = tmp.top(); tmp.pop();
par = partition(arr, l, r);
if (l < par - 1)
{
tmp.push(l); tmp.push(par - 1);
}
if (par + 1 < r)
{
tmp.push(par + 1); tmp.push(r);
}
}
}
}
4. 使用队列作为辅助空间的快排
这个实现其实和用栈作为辅助空间的思想是相同的,无非就是将存储中间结果的辅助空间换成了队列。
void QuickSort3(int *arr, int l, int r)
{
queue<int> tmp; //临时队列
if (l < r)
{
int mid = partition(arr, l, r); //一次划分
if (l < mid - 1) //划分中间结果入队列
{
tmp.push(l);
tmp.push(mid - 1);
}
if (mid + 1 < r)
{
tmp.push(mid + 1);
tmp.push(r);
}
while (!tmp.empty())
{
l = tmp.front();
tmp.pop();
r = tmp.front();
tmp.pop();
mid = partition(arr, l, r);
if (l < mid - 1)
{
tmp.push(l); tmp.push(mid - 1);
}
if (mid + 1 < r)
{
tmp.push(mid + 1);
tmp.push(r);
}
}
}
}
5. 快排什么时候适合使用,什么时候不适合使用?
快排是基于划分的,最好的情况就是每次划分恰好都是在序列的中间,这样算法的复杂度就是O(n*lgn)。所以说枢纽元素的选择会直接影像到排序的效率。
随机序列快排:枢纽元素的选择也将是随机的,这时候的划分不会导致出现最差的结果。
已序序列快排:假设采用枢纽元素总是选择第一个元素的方式,很明显,每次划分都只能排序一个元素,这时候排序的效率就是最差的(也就是严重的不平衡树)。此时效率为O(n^2)。
那么对于这种已序的序列什么排序比较适合呢?插入排序!!对于插入排序,若序列是已序的表明序列不需要作很多的移动,此时效率为O(n)。
这也就可以联系到STL中sort函数的实现:
STL中的sort(),在数据量大时,采用quicksort,分段递归排序;一旦分段后的数量小于某个门限值,改用Insertion sort,避免quicksort深度递归带来的过大的额外负担,如果递归层次过深,还会改用heapsort。