写在前面
分治(Divide and Conquer)是一种通过将复杂问题分解为更小、更易解决的子问题,递归处理子问题后再合并结果的算法设计思想。在我们工作中经常能碰到将庞大的问题不断拆分化解成各个小问题逐个击破的场景,很多编程思想中也常常能看到这种思想的背影,比如运用递归的算法。今天要谈的快速排序便是一种完美应用了分治思想的算法。
快速排序(Quicksort),是一种采用分治思想的算法,由C. A. R. Hoare在1960年提出。是对冒泡排序算法的一种改进。
一、核心思路
基准值:在一个无序的序列中选取一个任意的基准元素pivot。
分区:利用pivot将待排序的序列分成两部分,前面部分元素均小于或等于基准元素,后面部分均大于或等于基准元素。
递归:采用递归的方法分别对前后两部分重复上述操作,直到将无序序列排列成有序序列。
分区的实现基本围绕在如何使用指针上,一般可分为两种:左右指针法和前后指针法。其中左右指针又根据交换效率上不同分为霍尔法和挖洞法。
二、实现方法
1.左右指针法——霍尔法
左右指针法的核心思想是通过两个指针从数组两端向中间 “对撞”,交换不符合基准值条件的元素,最终将数组划分为两部分(左侧≤基准值,右侧≥基准值),并返回分区点的位置。
流程图解
第一步:确定指针和基准值。设置左右两个指针分别为数组的是左右边界索引 leftIndex,rightIndex指向的值。并且同时将基准值设置为右边界索引对应的值。
第二步:移动左指针。左指针与基准值进行比较,如果左指针对应的值小于基准值则并且左右指针未重合,则左指针一格一格往右移动,直到左指针对应值大于基准值停止移动。在本示例中左指针会移动到第4格数值为8的单元格。
第三步:移动右指针。右指针与基准值进行比较,如果右指针对应的值大于基准值则并且左右指针未重合,则右指针一格一格往左移动,直到右指针对应值小于基准值停止移动。在本示例中右指针会移动到第7格数值为5的单元格。
第四步:交换左右指针对应的值。
第五步:重复左右指针逐步靠中心的流程知道两者移动到同一个格停止
第六步:左右指针移动到同一格后,将该单元格与基准值交换,并返还该单元格的索引,将数组分割分为两部分。
后续:递归调用分割后两个数组,按照如上的逻辑不停的划分数组。在执行左右指针交换前,如果左指针对应的数小于右指针对应的数。则直接交换两个数并返回。余下的两个数组分别是【4,1,3,5,2】和【9,8,7】
代码如下(示例):
public class QuickSort_Hoare
{
//构造函数传来指定的数组
private readonly int[] _array;
public QuickSort_Hoare(int[] array)
{
_array = array;
}
/// <summary>
/// 实现快速排序算法的递归函数。
/// </summary>
/// <param name="leftIndex">待排序部分的左边界索引。</param>
/// <param name="rightIndex">待排序部分的右边界索引。</param>
/// <returns>返回排序后的数组。</returns>
public int[] quickSort(int leftIndex, int rightIndex)
{
// 当左边界小于右边界时,执行排序
if (leftIndex < rightIndex)
{
// 寻找基准元素的正确位置,并将其作为分割点
int pivotIndex = compare(_array, leftIndex, rightIndex);
// 递归排序基准元素左边的子数组
quickSort(leftIndex, pivotIndex - 1);
// 递归排序基准元素右边的子数组
quickSort(pivotIndex + 1, rightIndex);
}
// 返回排序后的数组
return _array;
}
/// <summary>
/// 使用快速排序的分区方法对数组进行处理
/// </summary>
/// <param name="_array">待处理的数组</param>
/// <param name="leftIndex">分区的左边界索引</param>
/// <param name="rightIndex">分区的右边界索引</param>
/// <returns>分区后基准元素的索引</returns>
private int compare(int[] _array, int leftIndex, int rightIndex)
{
// 初始化基准元素索引为右边界
int pivotIndex = rightIndex;
// 获取基准元素的值
int pivot = _array[rightIndex];
// 当左指针小于右指针时,进行循环
while (leftIndex < rightIndex)
{
// 从左向右遍历,直到找到大于基准元素的元素
while (leftIndex < rightIndex && _array[leftIndex] <= pivot)
{
leftIndex++;
}
// 从右向左遍历,直到找到小于基准元素的元素
while (leftIndex < rightIndex && _array[rightIndex] >= pivot)
{
rightIndex--;
}
// 交换左右指针指向的元素
_array.exchangeNumLocation(leftIndex, rightIndex);
}
// 将基准元素放到正确的位置
_array.exchangeNumLocation(leftIndex, pivotIndex);
// 返回基准元素的索引
return leftIndex;
}
}
/// <summary>
/// 交换两个数字的位置
/// </summary>
/// <param name="array"></param>
/// <param name="leftIndex"></param>
/// <param name="rightIndex"></param>
public static void exchangeNumLocation(this int[] array, int leftIndex, int rightIndex)
{
int temp = array[leftIndex];
array[leftIndex] = array[rightIndex];
array[rightIndex] = temp;
}
2.左右指针法——挖坑法
挖坑法和霍尔法本质上都属于左右指针法,它两的区别在于交换值得方式。
挖坑法先将基准值保存为pivot,形成初始 “坑”。right指针从右往左找小于pivot的元素,将其填入坑中,此时right位置成为新坑;接着left指针从左往右找大于pivot的元素,填入新坑,left位置成为新坑。重复此过程直到指针相遇,最后将pivot填入最终的坑中。
流程图解
第一步:确定指针和基准值。设置左右两个指针分别为数组的是左右边界索引 leftIndex,rightIndex指向的值。并且同时将基准值设置为右边界索引对应的值。这里将挖出来基准值单独放在旁边
第二步:移动左指针。左指针与基准值进行比较,如果左指针对应的值小于基准值则并且左右指针未重合,则左指针一格一格往右移动,直到左指针对应值大于基准值停止移动。在本示例中左指针会移动到第4格数值为8的单元格。
第三步:将左指针对应的值赋值给基准值的空位
第三步:移动右指针。右指针与基准值进行比较,如果右指针对应的值大于基准值则并且左右指针未重合,则右指针一格一格往左移动,直到右指针对应值小于基准值停止移动。在本示例中右指针会移动到第7格数值为5的单元格。
第四步:交换左右指针对应的值。
第五步:重复左右指针逐步靠中心的流程直到两者移动到同一个格停止
第六步:左右指针移动到同一格后,将该单元格与基准值交换,并返还该单元格(基准值)的索引,将数组分割分为两部分。
后续:递归调用分割后两个数组,按照如上的逻辑不停的划分数组。在执行左右指针交换前,如果左指针对应的数小于右指针对应的数。则直接交换两个数并返回。余下的两个数组分别是【4,1,3,5,2】和【9,7,8】
internal class QuickSort_DigHole
{
private readonly int[] _array;
public QuickSort_DigHole(int[] array)
{
_array = array;
}
/// <summary>
/// 快速排序方法入口
/// </summary>
/// <param name="leftIndex">排序的起始索引</param>
/// <param name="rightIndex">排序的结束索引</param>
/// <returns>返回排序后的数组</returns>
public int[] quickSort(int leftIndex, int rightIndex)
{
if (leftIndex < rightIndex)
{
// 分区操作,返回基准值的索引
int pivotIndex = compare(leftIndex, rightIndex);
// 对左半部分进行递归排序
quickSort(leftIndex, pivotIndex - 1);
// 对右半部分进行递归排序
quickSort(pivotIndex + 1, rightIndex);
}
return _array;
}
/// <summary>
/// 分区操作,选择最右元素作为基准值,将小于基准值的元素放在基准值左边,大于基准值的元素放在基准值右边
/// </summary>
/// <param name="leftIndex">分区的起始索引</param>
/// <param name="rightIndex">分区的结束索引</param>
/// <returns>返回基准值元素的索引</returns>
private int compare(int leftIndex, int rightIndex)
{
// 选择最右侧元素作为基准
int digIndex = rightIndex;
int digVal = _array[rightIndex];
while (leftIndex < rightIndex)
{
while (leftIndex < rightIndex && _array[leftIndex] <= digVal)
{
leftIndex++;
}
_array[digIndex] = _array[leftIndex];
digIndex = leftIndex;
// 从右向左寻找小于基准值的元素
while (leftIndex < rightIndex && _array[rightIndex] >= digVal)
{
rightIndex--;
}
_array[digIndex] = _array[rightIndex];
digIndex = rightIndex;
}
// 将基准值元素放到正确的位置
_array[digIndex] = digVal;
return digIndex;
}
}
3.前后指针法——Lomuto 分区方案
前后指针法的核心思想是使用两个指针,prev(前指针)指向起始位置leftIndex,标记小于基准的元素边界,而cur(后指针)从leftIndex + 1开始遍历数组。当cur指向的元素小于基准值时,prev后移一位,并交换prev和cur指向的元素,无论是否交换,cur始终右移,直到遍历完所有数组。将小于基准值的元素放到基准值的左边,大于基准值的元素放到右边,直到两个指针相遇。
前后指针法说起来有点抽象,咱上图解步骤图。
流程图解
第一步:确定指针和基准值。设置前后两个指针分别为数组的是左边界索引 prevIndex和左边界往右一个curIndex,基准值为左边界索引指向的值。
第二步:curIndex指针与基准值进行比较,如果curIndex指针对应的值大于基准值则并且前后指针未重合,则curIndex指针指针一格一格往右移动,直到curIndex指针指针对应值大于基准值停止移动,再将prevIndex右移动一位。在本示例中curIndex指针指针会移动到第2格数值为1的单元格。prevIndex也会移动到第2格数值为1的单元格。最后交换两个单元格数据。
第三步:重复上一步骤,一直到curIndex超出边界。
在本示例中curIndex和prevIndex都为2指向单元格3时,curIndex要持续移动到5指向单元格2,才触发小于基准值的判断。交换curIndex右移一个单元格和prevIndex对应的值
第四步:移动curIndex指针到curIndex超出边界。prevIndex+1,并与基准值交换值。返还该单元格(curIndex)的索引,将数组分割分为两部分。
后续:递归调用分割后两个数组,按照如上的逻辑不停的划分数组。在执行左右指针交换前,如果左指针对应的数小于右指针对应的数。则直接交换两个数并返回。余下的两个数组分别是【2,1,3】和【7,8,9,5,6】
代码如下(示例):
/// <summary>
/// 快速排序_双指针法
/// </summary>
internal class QuickSort_TwoPointer
{
//构造函数传来指定的数组
private readonly int[] _array;
public QuickSort_TwoPointer(int[] array)
{
_array = array;
}
/// <summary>
/// 实现快速排序算法的递归函数。
/// </summary>
/// <param name="leftIndex">待排序部分的左边界索引。</param>
/// <param name="rightIndex">待排序部分的右边界索引。</param>
/// <returns>返回排序后的数组。</returns>
public int[] quickSort(int leftIndex, int rightIndex)
{
// 当左边界小于右边界时,执行排序
if (leftIndex < rightIndex)
{
// 寻找基准元素的正确位置,并将其作为分割点
int pivotIndex = compare(_array, leftIndex, rightIndex);
// 递归排序基准元素左边的子数组
quickSort(leftIndex, pivotIndex - 1);
// 递归排序基准元素右边的子数组
quickSort(pivotIndex + 1, rightIndex);
}
// 返回排序后的数组
return _array;
}
private int compare(int[] _array, int leftIndex, int rightIndex)
{
// 选择第一个元素作为基准值
int key = _array[leftIndex];
int prev = leftIndex;
int cur = leftIndex + 1;
while (cur <= rightIndex)
{
if (_array[cur] < key){
_array.exchangeNumLocation(++prev, cur);
}
cur++;
}
_array.exchangeNumLocation(leftIndex, prev);
return prev;
}
}
总结
以上就是关于快速排序的讨论,以及实现的三种方式。希望能帮到大家去理解快速排序,理解分治的思想。