一、快排的几种实现方法
1.左右指针法
a>算法思想:选定数组的最后一个数作为关键值(key),从左边开始遍历找比key大的,从右边找比key小的,找到后交换左右指针,让比key大的数据向后调,比key小的数据调到前面,等到左右指针相等的时候,把数组的最后一位和指针所指位置交换,这样一趟排序过后,key值的前面都是比它小的数,后面都比它大。此时key相当于划分了它左右两个区间,那么两个区间再次分别像刚才一样进行选key交换,会不断的划分新区间,直至区间只包含一个数,就有序了。
单趟排序的图示
我们知道,每次单趟排序之后将数组分为了两个区间,而每个区间也都需要分别排序,故我们可以考虑用递归去实现。
b>优化:假设每一次选的key值都是数组里最大或者最小的数,那快排就会变得很慢很慢,可以认为变成了冒泡排序,所以,key的选取比较关键,我们希望每次选到的是值比较居中的数,所以,key的选取使用三数取中法,即比较区间的第一个数,中间的数和最后一个数,选出值位于中间的数与最后一位交换,因为我们每次取的key是区间的最后一位,这样就尽可能的保证key的值贴近于中间值。
c>时间复杂度:快排每次都会划分区间,直至区间中只有一个数,这一过程跟二叉树结构比较类似,单趟排序交换数的过程近似相当于遍历了这n个数,外层需要高度次递归,故,时间复杂度为O(n*log n).
d>空间复杂度:递归了高度次,即空间复杂度为O(log n)
e>代码实现
//左右指针法
int PartSort(int* a, int begin, int end) //单趟排序
{
int left = begin;
int right = end;
int mid = GetBestKey(a, begin, end); //三数取中法得到key
swap(a[mid], a[end]);
int key =a[end];
while (left < right)
{
while (left < right && a[left] <= key)
{
left++;
}
while (left < right && a[right] >= key)
{
right--;
}
if (a[left]>a[right])
swap(a[left], a[right]);
}
swap(a[left], a[end]);
return left;
}
void QuickSortR(int* a,int begin,int end) //递归版
{
int mid;
if (begin < end)
{
mid = PartSort(a, begin, end);
QuickSortR(a, begin, mid - 1);
QuickSortR(a, mid + 1, end);
}
}
//key取值的优化
int GetBestKey(int* a,int begin,int end)
{
if ((end - begin) < 1)
return end;
int mid = begin + (end - begin) / 2;
if (a[begin] < a[end])
{
if (a[mid] < a[begin])
return begin;
else if (a[mid]>a[end])
return end;
else
return mid;
}
else
{
if (a[end] > a[mid])
return end;
else if (a[mid] > a[begin])
return begin;
else
return mid;
}
}
2.挖坑法
a>算法思想:挖坑法其实和前后指针法也比较相似,区间的最后一个数保存在key里之后,最后一个位置就视为一个坑,先从左边开始找比key大的,找到了放进刚才的坑里,左边又产生了一个新坑,右边找比key小的,找到了填到左边的坑。最后左右相遇于坑处,key放到此处,单趟排序就结束了,之后再递归,和第一种方法一样。所以时间复杂度,空间复杂度和第一种方法一致。
b>优化:对排序整体进行优化,我们知道,当大量数据进行排序时,递归是划算的,可是如果当区间已经被划分的很小的话,不用去递归,改用直接插入排序也很快,因为区间很小,由快排的特性知,此时的小区间接近有序,而接近有序的时候插入排序是O(n)。还节省了快排递归时压栈的开销。(所有的快排方法都可以进行这样的优化)
c>代码实现
//挖坑法进行单趟排序
int PartSort1(int*a, int begin, int end)
{
int mid = GetBestKey(a, begin, end);
swap(a[mid], a[end]);
int key = a[end];
int left = begin;
int right = end;
while (left < right)
{
while (left < right && a[left] <= key)
{
left++;
}
a[right] = a[left];
while (left<right && a[right] >= key)
{
right--;
}
a[left] = a[right];
}
a[left] = key;
return left;
}
void QuickSortR(int* a,int begin,int end) //递归版
{
int mid;
if (begin < end)
{
mid = PartSort1(a, begin, end);
if (end - begin < 4) //区间数值个数小于4的时候直接插入排序
{
InSertSort(a + begin, end - begin + 1);
}
mid = PartSort2(a, begin, end);
QuickSortR(a, begin, mid - 1);
QuickSortR(a, mid + 1, end);
}
}
3.前后指针法
a>算法思想:定义两个指针prev和cur,cur指向区间第一个数,prev指向它的前一个。从左向右找比key小的,若当前数比key大,则cur向前移动,直到找到一个比key小的,此时prev向前移动一位,若prev和cur不指向同一位置,则prev所指位置的数值一定比cur大,所以交换prev和cur所指向的数。等到cur指向了最右边的数,再把最后一个数和prev的下一个位置的数交换,一趟排序就结束了。
//前后指针法
int PartSort2(int*a, int begin, int end)
{
int prev = begin - 1;
int cur = begin;
int mid = GetBestKey(a, begin, end);
swap(a[mid], a[end]);
int key = a[end];
while (cur < end)
{
while (a[cur] < key && ++prev != cur)
{
swap(a[cur], a[prev]);
}
++cur;
}
swap(a[end], a[++prev]);
return prev;
}
c>单趟排序后还要继续往下排,之前我们用的都是递归的方法,此处我们把它转化为
非递归,要用到栈。代码如下
void QuickSortNoR(int *a, int begin, int end)//非递归
{
assert(a);
stack<int> s;
if (begin < end)
{
s.push(end);
s.push(begin);
while (!s.empty())
{
int left = s.top();
s.pop();
int right = s.top();
s.pop();
int mid = PartSort2(a, left, right);
if (mid - 1 > left)
{
s.push(mid - 1);
s.push(left);
}
if (right > mid + 1)
{
s.push(right);
s.push(mid + 1);
}
}
}
}
快排的三种方式都理解了之后,个人觉得前后指针法最简单,代码实现起来很容易,同时要注意,如果是大量数据排序的话,最好进行一下方法二里面提到的优化,一是能提高效率,二也能防止不断的递归导致栈溢出。