排序算法原理
快速排序(Quick Sort)的原理简述如下:
(以升序为例)
1.利用分治的思想。选择数组中某元素为“基准数”,将所有小于它的元素移至其左侧,暂称为“左子数组”;大于它的元素移至其右侧,暂称为“右子数组”。对左右子数组重复执行此步骤;
2.执行步骤1,直至子数组划分为最小单位,则完成排序。
代码示例
此处以c#代码为例,其他语言大同小异(转换为所需语言请看<语法说明>板块以作参考)
考虑到,需要对每个子数组进行同样的操作步骤,即将所有小于基准数的元素移至其左侧,大于它的元素移至其右侧(我们称之为哨兵划分)。因此需先提供该方法以供调用:
·哨兵划分
private static int Partition(int[] nums, int left, int right)
{
int i = left;
int j = right;
while (i < j)
{
// 以nums[left]为基准数
while (i < j && nums[j] >= nums[left])
j--;
while (i < j && nums[i] <= nums[left])
i++;
Swap(nums, i, j);
}
Swap(nums, i, left);
return i;
}
// 交换数组中的两个值
private static void Swap<T>(T[] arr, int i, int j
{
(arr[i], arr[j]) = (arr[j], arr[i]);
}
对原数组递归进行哨兵划分,直至最小数组单位,即完成排序
·快速排序
public static void QuickSort(int[] nums, int left, int right)
{
while (left < right)
{
int pivot = Partition(nums, left, right);
QuickSort(nums, left, pivot - 1);
QuickSort(nums, pivot + 1, right);
}
}
算法优化
注意到,假如出现极端情况,如原数组为倒序,那么在递归进行哨兵划分时,总有一个数组长度为0,分治策略则无法实现。因此对基准数的选取进行优化:每次选取数组左中右三个元素,取其中间值使用。
同时考虑到,当前算法每轮递归都需开启两个新的快速排序,递归树高度将达到n-1,这也会占用较大的栈帧空间。因此对算法进行尾递归优化(仅限于支持尾递归优化的语言)。
优化后的代码
public static void QuickSort(int[] nums, int left, int right)
{
//尾递归优化
while (left < right)
{
int pivot = Partition(nums, left, right);
int temp;
if (pivot - left < right - pivot)
{
temp = left;
left = pivot + 1;
QuickSort(nums, temp, pivot - 1);
}
else
{
temp = right;
right = pivot - 1;
QuickSort(nums, pivot + 1, temp);
}
}
}
// 获取三个值的中间值
private static int MedianThree(int[] nums, int left, int mid, int right)
{
if ((nums[left] < nums[mid]) ^ (nums[left] < nums[right]))
return left;
else if ((nums[left] < nums[mid]) ^ (nums[mid] > nums[right]))
return mid;
return right;
}
private static int Partition(int[] nums, int left, int right)
{
//为防止出现极端情况(如数组完全倒序),导致一端子数组长度持续为0,从而劣化为冒泡排序。对基数选择做出调整。
int mid = MedianThree(nums, left, (left + right) / 2, right);
Swap(nums, left, mid);
int i = left;
int j = right;
while (i < j)
{
while (i < j && nums[j] >= nums[left])
j--;
while (i < j && nums[i] <= nums[left])
i++;
Swap(nums, i, j);
}
Swap(nums, i, left);
return i;
}
语法说明
为便于其他语言使用者进行代码转换,此处对可能存在的语法疑问作出注解。
1.public表示该函数是公开的,可根据具体语言及需求调整;
2.static表示该函数是静态的,可根据具体语言及需求调整;
3.void表示该函数返回值为空,可根据具体语言调整;
4.int是表示Int32类型的关键字,意为4个字节的有符号整形;
5.bool是表示Boolean类型的关键字,值为true或false;
6.for循环的语法结构为:
for(初始化索引;保持循环的条件;每次循环结束执行的语句){循环体}
这与C,C++,Java,Go,JS,TS,Dart类似。在Python,Swift,Rust中可替换为for index in range结构;
7.while循环的语法结构为:
while(保持循环的条件){循环体} 与其他语言类似,可进行简单替换;
8.方法名<T>的写法表示泛型,有些语言也称作参数多态或模板;
9.Func<T, T, bool> func的写法表示委托,相当于Java的接口和匿名内部类;Python的函数指针、高阶函数和回调函数;JS的回调函数和事件处理程序;Swift的代理等。在示例代码中,意为将一个函数作为参数传入,该函数需要两个类型为T的参数,并有一个bool类型的返回值。
算法特性
时间复杂度O(n log n):优化后算法较为稳定。
空间复杂度O(1)到O(log n):仅对原数组进行操作。