文章目录
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
简单选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 稳定 |
直接插入排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
希尔排序 | O(nlogn)~O(n²) | O(n^1.3) | O(n²) | O(1) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
快速排序 | O(nlogn) | O(nlogn) | O(n²) | O(logn)~O(n) | 不稳定 |
1.冒泡排序
O(n²):从后往前称之为冒泡,两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止,稳定排序
最好的情况:待排序表本身有序,O(n)
最坏的情况:待排序表为逆序, O(n²)
//交换函数
function swap(&$arr,$i,$j){
$tmp = $arr[$i];
$arr[$i] = $arr[$j];
$arr[$j] = $tmp;
}
//主冒泡排序函数
function BubbleSort(&$arr){
$len = count($arr);
//冒泡排序优化:如果某一次循环,没发生元素的交换,则整个数组已经有序
$flag = true;
for($i = 0;$i<$len-1 && $flag;++$i){
$flag = false;
for($j = $len-2;$j>=$i;--$j){
if($arr[$j] > $arr[$j+1]){
swap($arr,$j,$j+1);
$flag = true;
}
}
}
}
$arr = [9,1,5,8,3,0,7,4,6,2];
BubbleSort($arr);
print_r($arr);
//Array ( [0] => 0 [1] => 1 [2] => 2 [3] => 3 [4] => 4 [5] => 5 [6] => 6 [7] => 7 [8] => 8 [9] => 9 )
2.简单选择排序
O(n²):通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1<=i<=n)个记录交换之,先比较再交换,为稳定排序
对于比较次数来说,最好的情况与最坏的情况一样,都是需要比较n(n-1)/2 ,O(n²)
对于比较次数来说,最好的情况为0次,即初始已排好序
最坏的情况为n-1次,即初始逆序
//交换函数
function swap(&$arr,$i,$j){
$tmp = $arr[$i];
$arr[$i] = $arr[$j];
$arr[$j] = $tmp;
}
//主选择排序函数
function SelectSort(&$arr){
$len = count($arr);
for($i=0;$i<$len-1;++$i){
//记录第$i个元素后的元素的最小值下标
$min = $i;
for($j=$i+1;$j<$len;++$j){
if($arr[$j] < $arr[$min]){
$min = $j;
}
}
if($min != $i)
swap($arr,$min,$i);
}
}
$arr = [9,1,5,8,3,0,7,4,6,2];
SelectSort($arr);
print_r($arr);
//Array ( [0] => 0 [1] => 1 [2] => 2 [3] => 3 [4] => 4 [5] => 5 [6] => 6 [7] => 7 [8] => 8 [9] => 9 )
3.直接插入排序
O(n²):从无序表中依次取出元素,把它插入到有序表的合适的位置,为稳定排序
最好的情况:初始已排好序
最坏的情况:初始逆序
//主插入排序函数
function InsertSort(&$arr){
$len = count($arr);
for($i = 1;$i<$len;++$i){
$tmp = $arr[$i];//设置哨兵
//直到找到比哨兵小的元素,插入在它之前
for($j = $i-1;$j>=0 && $arr[$j]>$tmp;--$j)
$arr[$j+1] = $arr[$j];
$arr[$j+1] = $tmp;
}
}
$arr = [9,1,5,8,3,0,7,4,6,2];
InsertSort($arr);
print_r($arr);
//Array ( [0] => 0 [1] => 1 [2] => 2 [3] => 3 [4] => 4 [5] => 5 [6] => 6 [7] => 7 [8] => 8 [9] => 9 )
4.希尔排序
O(nlogn~n²):在记录数少的时候或者记录本身基本有序的情况下,直接插入排序才比较高效。希尔排序是在插入排序的基础上进行改进,分割成若干个子序列,此时子序列待排序的记录数减少,然后在这些子序列内部分别进行直接插入排序,当整个序列基本有序时,再对全体记录进行一次直接插入排序。为不稳定排序。
采用跳跃分割的策略:将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序的而不是局部有序的
//主希尔排序函数
function ShellSort(&$arr){
$len = count($arr);
$inc = $len;//增量
do{
$inc = ceil($inc/3);
for($i = $inc;$i<$len;++$i){
$tmp = $arr[$i]; //设置哨兵
//元素后移
for($j = $i-$inc;$j>=0 && $tmp<$arr[$j];$j-=$inc)
$arr[$j+$inc] = $arr[$j];
$arr[$j+$inc] = $tmp;
}
}while ($inc>1); //增量为1时,停止循环
}
$arr = [9,1,5,8,3,0,7,4,6,2];
ShellSort($arr);
print_r($arr);
//Array ( [0] => 0 [1] => 1 [2] => 2 [3] => 3 [4] => 4 [5] => 5 [6] => 6 [7] => 7 [8] => 8 [9] => 9 )
5.堆排序
O(nlogn):将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次大值。如此反复执行,便能得到一个有序序列。为不稳定排序
最好,最坏,平均的时间复杂度,均为O(nlogn)
由于初始构建堆所需的比较次数较多,堆排序并不适合待排序序列个数较少的情况
//交换函数
function swap(&$arr,$i,$j){
$tmp = $arr[$i];
$arr[$i] = $arr[$j];
$arr[$j] = $tmp;
}
//堆调整函数:保证堆始终是大顶堆
function HeapAdjust(&$arr,$s,$m){
$tmp = $arr[$s];
for($j = 2*$s;$j <= $m;$j *= 2){
if($j<$m && $arr[$j] < $arr[$j+1])
++$j;
if($tmp >= $arr[$j])
break;
$arr[$s] = $arr[$j];
$s = $j;
}
$arr[$s] = $tmp;
}
//主堆排序函数
function HeapSort(&$arr){
//堆排序与数组下标关联很重要,因此不能有0下标,所以先插入头元素,之后再去除即可
array_unshift($arr, 0);
$len = count($arr)-1;
//初始构造大顶堆
for($i = floor($len/2);$i>0;--$i){
HeapAdjust($arr,$i,$len);
}
//依次交换堆顶
for($i = $len;$i>1;--$i){
swap($arr,1,$i);
HeapAdjust($arr,1,$i-1);
}
array_shift($arr);
}
$arr = [9,1,5,8,3,0,7,4,6,2];
HeapSort($arr);
print_r($arr);
//Array ( [0] => 0 [1] => 1 [2] => 2 [3] => 3 [4] => 4 [5] => 5 [6] => 6 [7] => 7 [8] => 8 [9] => 9 )
6.归并排序
O(n²):利用归并的思想实现的排序方法。假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到floor(n/2)个长度为2或1的有序子序列;再两两归并,……,如此重复,直至得到一个长度为n的有序序列为止,这种排序叫做2路归并排序。为稳定排序
最好,最坏,平均的时间复杂度,均为O(nlogn)
(1)自顶向下(递归)
//数组合并重排过程
function Merge(&$arr,$start,$mid,$end){
$i = $k = $start;
$j = $mid+1;
$tmp = [];
while ($i != $mid+1 && $j != $end+1) {
if($arr[$i] >= $arr[$j])
$tmp[$k++] = $arr[$j++];
else
$tmp[$k++] = $arr[$i++];
}
//剩余的数组,塞进临时数组里
while ($i != $mid+1)
$tmp[$k++] = $arr[$i++];
while ($j != $end+1)
$tmp[$k++] = $arr[$j++];
//重新复制原数组
for($i = $start;$i<=$end;++$i){
$arr[$i] = $tmp[$i];
}
}
//递归归并
function MSort(&$arr,$start,$end){
//当子序列长度为1时,$start = $end,不需再分组
if($start < $end){
$mid = floor(($start + $end)/2);
MSort($arr,$start,$mid);
MSort($arr,$mid+1,$end);
Merge($arr,$start,$mid,$end);
}
}
//主归并排序函数
function MergeSort(&$arr){
$start = 0;
$end = count($arr)-1;
MSort($arr,$start,$end);
}
$arr = [9,1,5,8,3,0,7,4,6,2];
MergeSort($arr);
print_r($arr);
//Array ( [0] => 0 [1] => 1 [2] => 2 [3] => 3 [4] => 4 [5] => 5 [6] => 6 [7] => 7 [8] => 8 [9] => 9 )
(2)自底向上(非递归)
非递归的迭代方法,避免了递归时深度为log2n的栈空间,空间复杂度为O(n),并且避免递归也在时间性能上有一定的提升,所以在使用归并排序时,尽量考虑使用非递归方法
//数组合并重排过程
function Merge(&$arr,$start,$mid,$end){
$i = $k = $start;
$j = $mid+1;
$tmp = [];
while ($i != $mid+1 && $j != $end+1) {
if($arr[$i] >= $arr[$j])
$tmp[$k++] = $arr[$j++];
else
$tmp[$k++] = $arr[$i++];
}
//剩余的数组,塞进临时数组里
while ($i != $mid+1)
$tmp[$k++] = $arr[$i++];
while ($j != $end+1)
$tmp[$k++] = $arr[$j++];
//重新复制原数组
for($i = $start;$i<=$end;++$i){
$arr[$i] = $tmp[$i];
}
}
//主归并排序函数
function MergeSort2(&$arr){
$len = count($arr);
for($jump = 1;$jump<$len;$jump*=2){
$flag = 1; //标记位:如果没有进行下方循环,则当前仅剩两个子序列,进行最后的合并重拍
for($i = 0; ($i+2*$jump-1)<$len; $i+=2*$jump){
$flag = 0;
/**
* $i是下一个开始交换的点的下标, $i+2*$jump-1是结束的下标
* jump 1 2 4 8
* $start = $i; 0 2 4 6 8 10x 0 4 8 0 8 0
* $mid = $i+$jump-1; 0 2 4 6 8 10x 1 5 3
* $end = $i+2*$jump-1 1 3 5 7 9 3 7 7 x
*/
Merge($arr,$i,$i+$jump-1,$i+2*$jump-1);
}
if($flag){
Merge($arr,$i,$i+$jump-1,$len-1);
}
}
}
$arr = [9,1,5,8,3,0,7,4,6,2];
MergeSort($arr);
print_r($arr);
//Array ( [0] => 0 [1] => 1 [2] => 2 [3] => 3 [4] => 4 [5] => 5 [6] => 6 [7] => 7 [8] => 8 [9] => 9 )
PHP的单链表的插入排序和归并排序解法:【PHP解法==LeetCode链表类型(链表排序)】147.对链表进行插入排序 && 148.排序链表
7.快速排序
O(nlogn):通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。为不稳定排序
最好的情况:、Partition每次都将序列划分的很均匀
最坏的情况:待排序的序列为正序或倒序,每次划分只得到一个比上一次划分少一个记录的子序列
PS:Partition函数最终$low和$high都会指向分好的中间节点 图解Partition
//交换函数
function swap(&$arr,$i,$j){
$tmp = $arr[$i];
$arr[$i] = $arr[$j];
$arr[$j] = $tmp;
}
//分区函数
function Partition(&$arr,$low,$high){
$pivotkey = $arr[$low];
while ($low<$high) {
while ($low<$high && $arr[$high] >= $pivotkey) {
$high--;
}
swap($arr,$low,$high);
while ($low<$high && $arr[$low] < $pivotkey) {
$low++;
}
swap($arr,$low,$high);
}
return $low;
}
//快速排序递归函数
function Qsort(&$arr,$low,$high){
if($low<$high){
$pivot = Partition($arr,$low,$high);
Qsort($arr,$low,$pivot-1);
Qsort($arr,$pivot+1,$high);
}
}
//快速排序主函数
function QuickSort(&$arr){
$low = 0;
$high = count($arr) - 1;
Qsort($arr,$low,$high);
}
$arr = [9,1,5,8,3,0,7,4,6,2];
QuickSort($arr);
print_r($arr);
//Array ( [0] => 0 [1] => 1 [2] => 2 [3] => 3 [4] => 4 [5] => 5 [6] => 6 [7] => 7 [8] => 8 [9] => 9 )
快速排序优化方案
(1)优化选取中枢
三数取中法:取三个关键字先进行排序,将中间数作为枢轴,一般是取左端,右端和中间三个数,也可以随机选择
//分区函数
function Partition(&$arr,$low,$high){
/***** 三数取中法 *****/
//start
$mid = $low + floor(($high - $low)/2); //计算数组中间的元素的下标
if($arr[$low] > $arr[$high])
swap($arr,$low,$high); //交换左右端元素,保证左端较小
if($arr[$mid] > $arr[$high])
swap($arr,$mid,$high); //交换中右端元素,保证中端较小
if($arr[$low] > $arr[$mid])
swap($arr,$mid,$high); //交换左中端元素,保证左端较小
//end
$pivotkey = $arr[$low]; //此时$arr[$low]已经为三个关键字的中间值
while ($low<$high) {
while ($low<$high && $arr[$high] >= $pivotkey) {
$high--;
}
swap($arr,$low,$high);
while ($low<$high && $arr[$low] < $pivotkey) {
$low++;
}
swap($arr,$low,$high);
}
return $low;
}
也可以进行九数取中
(2)优化不必要的交换
swap只作替换作用,最终当low和high会合,即找到了枢轴位置,此时再将$pivotkey赋值给$arr[$low]
当中少了多次交换的操作,在性能上又得到了部分的提高
//分区函数
function Partition(&$arr,$low,$high){
/***** 此处省略三数取中法 *****/
$pivotkey = $arr[$low]; //枢轴值
while ($low<$high) {
while ($low<$high && $arr[$high] >= $pivotkey) {
$high--;
}
$arr[$low] = $arr[$high];
while ($low<$high && $arr[$low] < $pivotkey) {
$low++;
}
$arr[$high] = $arr[$low];
}
$arr[$low] = $pivotkey;
return $low;
}
(3)优化小数组时的排序方案
如果数组非常小时,直接插入排序比快速排序来的更好,直接插入排序是简单排序算法中性能最好的
//快速排序递归函数
function Qsort(&$arr,$low,$high){
$max_length_insert_sort = 7; //定义数组长度阈值
if(($high-$low)>$max_length_insert_sort){
//进行快速排序
$pivot = Partition($arr,$low,$high);
Qsort($arr,$low,$pivot-1);
Qsort($arr,$pivot+1,$high);
}else{
//进行直接插入排序
InsertSort($arr);
}
}
(4)优化递归操作
递归对性能是有一定影响的,QSort函数在其尾部有两次递归操作,每次递归都会耗费一定的栈空间。如果能减少,将会大大提高性能
//快速排序递归函数
function Qsort(&$arr,$low,$high){
$max_length_insert_sort = 7; //定义数组长度阈值
if(($high-$low)>$max_length_insert_sort){
while ($low<$high) {
$pivot = Partition($arr,$low,$high);
Qsort($arr,$low,$pivot-1);
$low = $pivot+1;
}
}else{
InsertSort($arr);
}
}
改成while后,因为第一次递归以后,变量low就没有用处了,所以可以将pivot+1赋值给low
再循环一次,此时Partition(&$arr,$low,$high)的执行效果等同于Qsort(&$arr,$pivot+1,$high)
结果相同,但因为采用了迭代而不是递归的方法,可以缩减堆栈深度,从而提高整体性能
本博文参考书籍《大话数据结构》