快排的原理讲解
快速排序是1962年由hoare提出的一种二叉树排序方法,其基本思想是:任取待排序元素中的某一值为基准值,通过基准值将元素序列划分为左右子序列,将大于基准值的元素放到右子序列,将小于基准值的元素放到左子序列,然后左右子序列重复上述过程,直到所有元素都排到相应的位置上为止。
将序列划分为左右子序列常用的方法有三种分别是:
假设按照对arr数组的元素进行升序排列;
int arr[] = { 6,1,2,7,9,3,4,5,10,8 };//定义数组arr
1.hoare版本
1.1原理
在数组中取数组的最后一个元素作为基准值,将基准值的保存起来,定义一个指针begin指向数组的起始位置,定义一个指针end,指向数组的最后,然后begin开始向前走,当begin所指的数组元素大于基准值,此时begin停止,end向前走,当end所指的元素小于基准值时,end停止,交换begin和end所指的元素,begin继续向前走,end继续向后走,当begin小于等于end时循环结束,然后交换begin所指的元素和基准值,返回此时数组的下标也就是基准值。
1.2代码实现
int PartQsort1(int * a,int begin,int end)
{
int key = end;//记录基准值的位置
while (begin < end)//循环结束的条件
{
while (a[begin] <= a[key] && begin <end)//防止begin在移动的过程中大于end
{
begin++;//如果从数组begin开始的位置找到大于等于基准值的值
}
while (a[end] >= a[key] && begin < end)
{
end--;//从end开始找到小于等于基准值的值
}
Swap(&a[begin], &a[end]);//将大于基准值的值放到右子序列,将小于基准值的值放到左子序列
}
Swap(&a[end], &a[key]);//将基准值放到左右子序列的中间
return begin;//返回基准值的下标
}
1.3图解
2. 挖坑法
2.1原理
将数组的最后一个元素当做基准值,然后从数组第一个元素开始,begin指向数组的第一个元素,begin向后走,找到大于基准值的元素,填到end所指的数组位置,从end向前找到小于基准值的元素,填到begin所指的位置,当begin小于等于end时结束循环,将基准值填到begin所指的数组位置。
2.1代码实现
int PartQsort3(int* a, int begin, int end)//挖坑法
{
int key = a[end];//找基准值,并保存
while (begin < end)
{
while(a[begin] < key&&begin<end)//确保begin小于end并且找到大于基准值的数组元素
{
begin++;
}
a[end] = a[begin];//填坑,将大于基准值的元素填到坑中
while (a[end] >= key && begin < end)//找到小于基准值的元素
{
end--;
}
a[begin] = a[end];//填坑
}
a[begin] = key;//将基准值填到最后一个坑中
return begin;//返回基准值所在的数组下标
}
2.3图解
3. 前后指针版本
3.1原理
定义指针cur和prev,cur指向begin(开始位置),prev指向begin-1;如果cur指向的值小于标准值,++prev然后交换cur和prev所指的值,cur继续向后走寻找小于基准值的元素,当cur>=end时循环结束,++prev交换prev所指的元素和基准值。
3.2代码实现
int PartQsort2(int* a, int begin, int end)//前后指针法
{
int cur = begin;
int prev = begin - 1;
while (cur < end)
{
if (a[cur] < a[end] && ++prev != cur)//如果cur指向的元素小于标准值且与++prev所指向的元素不同则交换cur指向的元素和prev指向的元素
{
Swap(&a[prev], &a[cur]);
}
cur++;//否则cur向后走
}
Swap(&a[++prev], &a[end]);//循环结束后++prev,然后交换prev指向的值和标准值
return prev;//返回标准值
}
3.3图解
快排的代码实现
void Swap(int* p1, int* p2)//交换两个数
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
int PartQsort2(int* a, int begin, int end)//前后指针法
{
int cur = begin;
int prev = begin - 1;
while (cur < end)
{
if (a[cur] < a[end] && ++prev != cur)//如果cur指向的元素小于标准值且与++prev所指向的元素不同则交换cur指向的元素和prev指向的元素
{
Swap(&a[prev], &a[cur]);
}
cur++;//否则cur向后走
}
Swap(&a[++prev], &a[end]);//循环结束后++prev,然后交换prev指向的值和标准值
return prev;//返回标准值
}
void QuickSort(int* a, int left, int right)
{
int div = PartQsort3(a, left, right);//求出分割下标
if (left >= right)//如果左边序列大于右边序列循环结束
{
return;
}
//按照基准值堆数组a[left,right]进行划分
//划分成功后以div为边界将数组分成左右两个子数组
QuickSort(a, left, div - 1);//递归排列左右两个子区间
if (div + 1 <= right)
{
QuickSort(a, div + 1, right);
}
}
快排的图解
快排的优化
优化1
如果用快排进行处理的数据本来就是有序的,这时候再去用快排排序,时间复杂度就会很大,因为每次取的数据都是有序的。如果每次排序取的是最后一个数, 那么每次都只能让取出的最后一个数有序,这样会导致每趟快排就只能排好一个数据,这时候的时间复杂度就会很大,达不到我们预期的效果,因此需要来优化快排,避免出现上面情况,优化的方法是三数取中。具体操作就是每次取基准值时,将数组开始的值(begin所指向的值),数据中间的值(mid所指向的值),数据末尾的值(end所指向的值),进行比较取出中间值作为基准值。
优化1的代码
static int GetMidIndex(int *a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left] <a[mid])//求出三个数的中间数
{
if (a[left] > a[right])
{
return left;
}
else if (a[left] < a[right] )
{
return right;
}
}
else
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[mid] < a[right] )
{
return right;
}
}
return left;
}
优化二
因为每次排序左右区间就要递归调用函数,这样会浪费大量的栈空间,因此在递归到小区间时可以使用插入排序,(因为插入排序在数据有序或者接近有序时,时间复杂度很低),来降低栈空间的消耗。
优化二代码实现
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; ++i)
{
int end = i;
// 单趟排序:[0, end]有序 end+1位置的值,插入进入,保持它依旧有序
int tmp = a[end + 1];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];
--end;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)//如果左边序列大于右边序列循环结束
return;
if (right - left + 1 > 10)
{
int div = PartQsort3(a, left, right);//求出分割下标
QuickSort(a, left, div - 1);//递归排列左右两个子数组
if (div + 1 <= right)
QuickSort(a, div + 1, right);
}
else
{
InsertSort(*(a + left), right - left + 1);//小区间直接插入排序
}
}
快排的时间复杂度
在没有优化时最好情况下时间复杂度度是O(N*logN),(这时数组是无序的),如果数组是有序的时间复杂度就会变大,如果数组完全有序,那么每次快排就只能将一个数字排成有序,此时的时间复杂度就是O(N*N);这种情况通过优化是可以避免的,因此可以将快排的时间复杂度看成O(N*logN)。
完整代码
void Swap(int* pa, int* pb)
{
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
// 时间复杂度:O(N^2) -- 逆序
// 最好 O(N) -- 顺序有序 或 接近顺序有序
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; ++i)
{
int end = i;
// 单趟排序:[0, end]有序 end+1位置的值,插入进入,保持它依旧有序
int tmp = a[end + 1];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];
--end;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
int PartQsort3(int* a, int begin, int end)//挖坑法
{
int tmp = GetMidIndex(a, begin, end);//优化--避免在数组有序情况下进行的效率极低的排序
Swap(&a[end], &a[tmp]);
int key = a[end];//找基准值,并保存
while (begin < end)
{
while(a[begin] < key&&begin<end)//确保begin小于end并且找到大于基准值的数组元素
{
begin++;
}
a[end] = a[begin];//填坑,将大于基准值的元素填到坑中
while (a[end] >= key && begin < end)//找到小于基准值的元素
{
end--;
}
a[begin] = a[end];//填坑
}
a[begin] = key;//将基准值填到最后一个坑中
return begin;//返回基准值所在的数组下标
}
void QuickSort(int* a, int left, int right)//快速排序--时间复杂度是(n*logN----N的平方)--优化可以避免出现n的平方
{
int div = PartQsort3(a, left, right);//求出分割下标
if (right - left-1 > 10)
{
if (left >= right)//如果左边序列大于右边序列循环结束
{
return;
}
//按照基准值堆数组a[left,right]进行划分
//划分成功后以div为边界将数组分成左右两个子数组
QuickSort(a, left, div - 1);//递归排列左右两个子数组
if (div + 1 <= right)
{
QuickSort(a, div + 1, right);
}
}
else
{
InsertSort(a + left, right - left + 1);//小区间直接插入排序
}
}