文章目录
- 1.常见排序
- 2.快速排序
- 2.1hoare版本
- 2.2快速排序优化
- 2.3挖坑法实现
- 2.4前后指针实现
1.常见排序
2.快速排序
快速排序(Quick Sort) 是一种常见的排序算法,也是一种基于分治算法的排序。该算法的基本思想是将一个数据集分成两个子集,其中一个子集比另一个子集的所有元素都小,然后递归地对子集进行排序,最终得到排好序的数据集。
具体地说,快速排序的具体步骤为:首先从待排序的数据集中选取一个元素作为“基准”(key),然后将所有小于基准的元素移动到基准的左边,将所有大于基准的元素移动到基准的右边,最后将基准置于两个子集的中间位置。然后递归地对左右两个子集进行快速排序,直到所有子集只包含一个元素,排序完成。
快速排序具有时间复杂度为O(nlogn)的优秀排序效率,并且实现简单,因此被广泛应用于各种排序场景。
快速排序的实现思想:
快速排序的实现类似于二叉树的前序遍历,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
2.1hoare版本
hoare版本快速排序的实现思想:
Hoare版本的快速排序是一种基于分治思想的快速排序算法。其基本思路是:
(1)选取基准元素(key),通常是从中间取,将待排序的数据集分成两部分。
(2)分别从第一部分和第二部分(即数据集的两端)开始,设定两个指针i和j,然后让i从左往右扫描,找到第一个大于等于key的元素,然后让j从右往左扫描,找到第一个小于等于key的元素,最后将i和j所指的元素交换位置。
(3)重复2中的操作,直到i >= j,也就是第一次遍历时找到的第一个小于等于key的元素和第一个大于等于key的元素已经相遇(或者说交叉)。
(4)将当前i所指的元素(即第一个大于等于key的元素)和key交换位置。
(5)此时,所有小于等于key的元素都在左边,所有大于key的元素都在右边,而key本身也已经到了中间的位置,也就是分割线的位置。
(6)对左右两部分分别重复上述步骤,直到每部分只剩下一个元素或者为空。
值得注意的是,在Hoare版本中,交换i和j所指的元素时并不需要额外的判断,因为我们在第一步就选取了某一个元素作为基准,i和j所指向的元素必须满足小于或等于key或大于或等于key的条件,这样才可以保证算法的正确性。
hoare版本快速排序的实现:
算法首先判断left和right的大小,如果left >= right,则数组已经有序,直接返回。
然后定义begin为left,end为right,keyi为基准元素的下标,一开始取的是left。
接下来,算法使用两个while循环来对数组进行分割。具体来说,第一个while循环先从右端开始,找到第一个小于基准元素a[keyi]的元素a[right],第二个while循环从左端开始,找到第一个大于基准元素a[keyi]的元素a[left],然后交换a[left]和a[right]的值。
当两个while循环都结束后,交换基准元素a[keyi]和a[left]的值,并将keyi更新为left。
接下来,算法根据keyi的值将a数组分成两部分,分别对左半部分 [begin, keyi-1] 和右半部分 [keyi+1, end] 进行递归调用,直到每个子数组只有一个元素,排序完成。
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void QuickSortHoare(int* a, int left, int right)
{
if (left >= right)
return;
int begin = left, end = right;
int keyi = left;
while (left < right)
{
while (left < right && a[right] >= a[keyi])
{
--right;
}
while (left < right && a[left] <= a[keyi])
{
++left;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
keyi = left;
// [begin, keyi-1] keyi [keyi+1, end]
// 递归
QuickSortHoare(a, begin, keyi - 1);
QuickSortHoare(a, keyi + 1, end);
}
2.2快速排序优化
快速排序虽然是一种高效的排序算法,但是在一些情况下,它也可能会表现出很差的性能。当所需要排序数组有序时,它近似于冒泡排序,时间复杂度为 O(n^2)。
(1)随机化选取基准元素(key):为了避免快速排序在面对特定输入时产生性能问题,我们也可以通过随机化选取基准元素来保证快速排序的平均性能。具体来说,我们可以随机选取数组中的一个元素作为基准元素。
(2)三数取中法:为了避免快速排序在对于已经有序或者基本有序的数组排序时性能退化,我们可以采用三数取中法来选择基准元素。具体来说,我们可以从待排序数组的左、中、右三个位置选取一个中间大小的数字作为基准元素。还有其他如插入排序或左右端点优化的方法。
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
int GetMidNumi(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return left;
}
else
{
return right;
}
}
else // a[left] > a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
void QuickSortHoare(int* a, int left, int right)
{
if (left >= right)
return;
int begin = left, end = right;
//随机选key
//int randi = left + (rand() % (right - left));
//Swap(&a[left], &a[randi]);
//三数取中
int midi = GetMidNumi(a, left, right);
if (midi != left)
Swap(&a[midi], &a[left]);
int keyi = left;
while (left < right)
{
while (left < right && a[right] >= a[keyi])
{
--right;
}
while (left < right && a[left] <= a[keyi])
{
++left;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
keyi = left;
// [begin, keyi-1] keyi [keyi+1, end]
// 递归
QuickSortHoare(a, begin, keyi - 1);
QuickSortHoare(a, keyi + 1, end);
}
2.3挖坑法实现
挖坑法快速排序的实现思想:
挖坑法的基本思路是选取一个元素作为基准(通常是中间元素),然后定义两个指针L和R,L指向序列的第一个元素,R指向序列的最后一个元素。然后,先从R开始向左遍历,找到第一个小于基准的数,并把它填入L所指的坑中,然后L再从左开始向右遍历,找到第一个大于基准的数,并把它填入R所指的坑中,直到L >= R为止。此时,所有小于等于基准的数都在L的左边,而所有大于基准的数都在R的右边。
最后,把基准值与i所指的位置互换,从而将基准值放入正确的位置,然后分别对左右两个部分递归进行快速排序,直到每个部分的长度不超过1,排序完成。
挖坑法的优点是实现简单,且不需要额外的数组空间来存储分割点。
挖坑法快速排序的实现:
我们首先使用GetMidNumi三数取中函数来选取一个中间大小的数作为基准(key),然后将它与数组的第一个位置交换。
接下来,定义key为基准值,hole为坑的位置指针,初始值为left。使用while循环来扫描待排序的部分,将小于等于基准值的数放到key左边,将大于基准值的数放到key右边。while循环中,先从右边开始找到第一个小于key的数,然后将它填到左边hole所指的位置,让hole指针指向这个位置;然后再从左边开始找到第一个大于key的数,将他放到hole所指的位置,然后hole指针指向这个位置。直到left >= right为止。
最后,将key值放到hole所指的位置,这样,左边的数都小于key,右边的数都大于key。
这个函数所作的操作是对数组的一部分进行划分,并返回key的位置hole。对于这个hole所在的位置左侧的数据来说,都是小于等于基准key的;而hole所在位置的数据就是基准key;右侧的数据都是大于key的。需要注意的是,最后可能存在多个大于或小于key的数字,只有最后一个hole放置key之后,才可以确定划分的位置。
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
int QuickSortHole(int* a, int left, int right)
{
// 三数取中
int midi = GetMidNumi(a, left, right);
if (midi != left)
Swap(&a[midi], &a[left]);
int key = a[left];
int hole = left;
while (left < right)
{
// 右边找小
while (left < right && a[right] >= key)
--right;
a[hole] = a[right];
hole = right;
// 左边找大
while (left < right && a[left] <= key)
++left;
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
2.4前后指针实现
前后指针法快速排序的实现思想:
(1)选取任意一个元素作为基准值key,通常是选取待排序数组的第一个元素。
(2)定义两个指针,分别指向待排序数组的第一个元素和第二一个元素,用cur表示后指针,prev表示前指针。
(3)然后不断将cur和prev指针不断后移,向所指向的元素与基准值进行比较,如果cur大于基准值且prev小于基准值,交换prev和cur所指向的元素。prev指针向后移动一个位置;将cur指针向后移动一个位置,
(4)重复上述过程,直到cur指针越界。
(5)最后将基准值所在的位置与prev所在的位置交换,然后分别以基准值所在的位置为分界点,对左右两侧的部分进行递归排序。
前后指针法快速排序的实现:
函数主体使用了前后指针对数组元素进行交换排序。定义prev和cur两个指针并初始化为left和left+1,分别指向前一个位置和当前位置。循环遍历数组,当a[cur]小于a[keyi]时,将cur所指的值与++prev所指的值交换,从而保证prev之前的元素都是小于基准值的,cur之后的元素都是大于等于基准值的。
当循环结束后,将基准值与prev所指的位置互换,将基准值归位并返回其位置。
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
int QucikSortPointer(int* a, int left, int right)
{
// 三数取中
int midi = GetMidNumi(a, left, right);
if (midi != left)
Swap(&a[midi], &a[left]);
int keyi = left;
int prev = left;
int cur = left + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[cur], &a[prev]);
++cur;
}
Swap(&a[prev], &a[keyi]);
keyi = prev;
return keyi;
}
快速排序的特性总结:
(1)快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
(2)时间复杂度:O(N*logN)
(3)空间复杂度:O(logN)
(4)稳定性:不稳定
这些就是数据结构中有关快速排序三种实现的简单介绍了😉
如有错误❌望指正,最后祝大家学习进步✊天天开心✨🎉