文章目录
快速排序简介
介绍
快速排序(Quick Sort)是一种常用且高效的排序算法,它的思想是通过分治法将一个大问题分解成多个小问题来解决。具体而言,快速排序将待排序的数组按照一个基准元素进行划分,并将比基准元素小的元素放在基准元素的左侧,将比基准元素大的元素放在基准元素的右侧,然后对左右两个子数组递归地进行同样的操作,直到每个子数组只包含一个元素为止。
核心思路
快速排序的核心思路是通过分治法将一个大问题划分为多个小问题来解决。具体而言,它的步骤如下:
1,选择一个基准元素:从待排序的数组中选择一个元素作为基准元素(通常选择最后一个元素)。
2,划分操作:将数组中的其他元素与基准元素进行比较,将比基准元素小的元素放在基准元素的左侧,将比基准元素大的元素放在基准元素的右侧。这样,基准元素就位于正确的位置上,并且左侧的元素都小于或等于它,右侧的元素都大于或等于它。
3,递归操作:对基准元素左侧和右侧的子数组分别进行相同的操作,直到每个子数组只包含一个元素或为空数组。
合并操作:递归操作结束后,所有子数组都已经排序完成,只需要将它们合并起来,即可得到完整的排序数组。
通过不断地递归划分和合并操作,最终实现了整个数组的排序。
快速排序的优势在于其平均时间复杂度为 O(nlogn),且具有原地排序的特点,即不需要额外的存储空间。然而,最坏情况下的时间复杂度为 O(n^2),这通常发生在基准元素选择不当的情况下。因此,在实际应用中,可以通过选择合适的基准元素来改善算法的性能。
快速排序实现
问题思考
1,如何快速地分开基准元素两边的元素?
2,如何递归实现?
分开基准元素实现
一:挖坑法
挖坑法(Hoare Partition Scheme)是快速排序算法中用于划分数组的一种方法。它是由英国计算机科学家C.A.R. Hoare提出的。
挖坑法的核心思想是选择两个指针,一个从数组的左侧开始向右移动,一个从数组的右侧开始向左移动。然后,根据基准元素,将比基准元素小的元素放在它的左边,将比基准元素大的元素放在它的右边。当两个指针相遇时,将基准元素放置在它们相遇的位置上。
具体而言,挖坑法的步骤如下:
1,选择基准元素:从待排序的数组中选择一个元素作为基准元素(通常选择第一个元素或随机选择一个元素)。
2,挖坑操作:定义两个指针,一个指向数组的左侧(左指针),一个指向数组的右侧(右指针)。左指针开始向右移动,右指针开始向左移动。
3,比较与交换操作:左指针先移动,找到一个比基准元素大的元素;右指针后移动,找到一个比基准元素小的元素。将这两个元素进行交换,并继续移动指针,直到左指针和右指针相遇。
4,基准元素归位:将基准元素放在指针相遇的位置上,即左指针和右指针相遇的位置。
5,递归操作:对基准元素左侧和右侧的子数组分别进行相同的挖坑操作,直到每个子数组只包含一个元素或为空数组。
通过不断地递归挖坑操作,最终实现了整个数组的排序。
挖坑法与传统的快速排序算法相比,可以减少元素的交换次数,提高算法的效率。然而,在某些情况下,挖坑法仍可能导致最坏情况下的时间复杂度为 O(n^2),因此选择合适的基准元素仍然很重要。
挖坑法代码实现
void quicksort(int* arr, int left,int right)
{
// left:左元素下标,right:右元素下标
int start = left;
int end = right;
if (end - start <= 0 || arr == NULL)
{
// 判断是否合理
return;
}
int hole = start; //坑的下标
int key = arr[hole]; //基准元素储存,挖坑
while (start < end)
{
//开始循环
while (end > start && arr[end] >= key)
{
//坑在左边,从右边开始移,找小于基准元素的数,
end--;
}
arr[hole] = arr[end]; //找到元素后将他填坑
hole = end; //更新坑的位置,这个位置的元素移别的位置了,变成了坑
while (start < end && arr[start] <= key)
{
//坑在右边,从左边开始移,找比基准元素大的元素。
start++;
}
arr[hole] = arr[start]; //原理同上操作
hole = start;
}
arr[hole] = key; //最后将基准元素填坑到end与start相同的位置
}
二:比较法
比较法的核心思想是选择一个基准元素,并将数组分成两个部分:左侧的元素小于或等于基准元素,右侧的元素大于基准元素。具体而言,比较法的步骤如下:
1,选择基准元素:从待排序的数组中选择一个元素作为基准元素(通常选择最后一个元素)。
2,比较与交换操作:定义一个指针i,初始值为数组的起始位置。然后,遍历数组中的每个元素,将小于或等于基准元素的元素与指针i对应的元素进行交换,并将指针i向后移动一个位置。
3,基准元素归位:将基准元素放在指针i对应的位置上。
通过上述操作,可以使得基准元素左侧的元素都小于或等于基准元素,右侧的元素都大于基准元素。
比较法相对于挖坑法来说更为简单,在实现上更直观。然而,相较于挖坑法,比较法需要进行更多的元素交换操作,因此效率可能稍低。在实际应用中,根据具体情况选择适合的划分方案。
比较法代码实现
int partition(int arr[], int low, int high)
{
int mid = midnum(arr, low, high);
if (high != mid)
swap(&arr[mid], &arr[high]);
int pivot = arr[high]; // 基准元素
int i = (low - 1); // 比基准元素小的区域边界
for (int j = low; j <= high - 1; j++)
{
if (arr[j] <= pivot)
{
i++;
if (i != j)
{
swap(&arr[i], &arr[j]);
}
}
}
if(i + 1 < high)
swap(&arr[i + 1], &arr[high]);
return (i + 1);
}
递归实现
思考
1,递归的终止条件。
2,递归的位置
代码实现
void quicksort(int* arr, int left,int right)
{
// left:左元素下标,right:右元素下标
int start = left;
int end = right;
if (end - start <= 0 || arr == NULL)
{
// 判断是否合理
//递归条件,元素小于等于1个时
return;
}
int hole = start; //坑的下标
int key = arr[hole]; //基准元素储存,挖坑
while (start < end)
{
//开始循环
while (end > start && arr[end] >= key)
{
//坑在左边,从右边开始移,找小于基准元素的数,
end--;
}
arr[hole] = arr[end]; //找到元素后将他填坑
hole = end; //更新坑的位置,这个位置的元素移别的位置了,变成了坑
while (start < end && arr[start] <= key)
{
//坑在右边,从左边开始移,找比基准元素大的元素。
start++;
}
arr[hole] = arr[start]; //原理同上操作
hole = start;
}
arr[hole] = key; //最后将基准元素填坑到end与start相同的位置
//递归左边的数字
quicksort(arr, left, hole - 1);
//递归右边的数字
quicksort(arr, hole + 1, right);
}
优化最坏的情况
方法:
在快速排序算法中,最坏情况发生在划分操作不平衡的情况下,即每次划分得到的子数组大小差距很大。这种情况下,快速排序的时间复杂度可能达到O(n^2),效率较低。为了优化最坏情况,可以采取以下策略:
1,选择合适的基准元素:基准元素的选择对快速排序的性能影响很大。一般来说,随机选择基准元素或者选择数组中间位置的元素作为基准元素,有助于避免最坏情况的发生。
2,使用三数取中法:三数取中法是一种改进基准元素选择的方法。它通过比较数组的第一个、中间和最后一个元素,选择其中的中间值作为基准元素。这样可以尽量避免极端情况下的划分不平衡。
3,随机化划分元素位置:在进行划分操作时,将基准元素与数组中的任意一个元素交换位置,而不仅限于选择最后一个元素作为基准元素。这样可以增加随机性,减少最坏情况的发生概率。
对小规模子数组使用插入排序:当子数组的大小小于一定阈值时,可以切换到使用插入排序等简单但效率较高的排序算法。这是因为在小规模问题上,插入排序通常比快速排序更高效。
三数取中实现
- 介绍一下其中一种,其它的读者可以自己试试
三数取中:取头尾中三个数中中间大的数,进而避免分开的两部分极不平衡的情况,可以比较稳定避免极端情况发生。
取中代码实现:
int midnum(int arr[], int left, int right)
{
int mid = left + (right - left) / 2;
int r = arr[right];
int l = arr[left];
int m = arr[mid];
if (r >= m && m >= l || l >= m && m >= r )
{
// r m l l m r
return mid;
}
else if (m >= r && r >= l || l >= r && r >= m)
{
// m r l l r m
return right;
}
else
{
return left;
}
}
完全代码:
取中后互换开头元素与中间大的元素的位置
void quicksort(int* arr, int left,int right)
{
// left:左元素下标,right:右元素下标
int start = left;
int end = right;
if (end - start <= 0 || arr == NULL)
{
// 判断是否合理
return;
}
if(start != midnum(arr,left,right))
swap(&arr[start], &arr[midnum(arr, left, right)]);
int hole = start; //坑的下标
int key = arr[hole]; //基准元素储存,挖坑
while (start < end)
{
//开始循环
while (end > start && arr[end] >= key)
{
//坑在左边,从右边开始移,找小于基准元素的数,
end--;
}
arr[hole] = arr[end]; //找到元素后将他填坑
hole = end; //更新坑的位置,这个位置的元素移别的位置了,变成了坑
while (start < end && arr[start] <= key)
{
//坑在右边,从左边开始移,找比基准元素大的元素。
start++;
}
arr[hole] = arr[start]; //原理同上操作
hole = start;
}
arr[hole] = key; //最后将基准元素填坑到end与start相同的位置
quicksort(arr, left, hole - 1);
quicksort(arr, hole + 1, right);
}
总结
快速排序是适用于大部分情况的排序算法,并且效率极高,c++,java内置库中排序一般是用的快速排序的,再次基础上还可以使用一些优化,读者可以自行探索。
有收获可以点个赞!