排序算法经过了很长时间的演变,产生了很多种不同的方法。对于初学者来说,对它们进行整理便于理解记忆显得很重要。每种算法都有它特定的使用场合,很难通用。因此,我们很有必要对所有常见的排序算法进行归纳。
排序大的分类可以分为两种:内排序和外排序。在排序过程中,全部记录存放在内存,则称为内排序,如果排序过程中需要使用外存,则称为外排序。下面讲的排序都是属于内排序。
内排序有可以分为以下几类:
(1)、插入排序:直接插入排序、shell排序、折半插入排序。
(2)、选择排序:直接选择排序、堆排序。
(3)、交换排序:冒泡排序、快速排序。
(4)、归并排序
(5)、桶式排序
(6)、基数排序
表格版
排序方法 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 稳定性 | 复杂性 |
---|---|---|---|---|---|---|
直接插入排序 | O(n2)O(n2) | O(n2)O(n2) | O(n)O(n) | O(1)O(1) | 稳定 | 简单 |
希尔排序 | O(nlog2n)O(nlog2n) | O(n2)O(n2) | O(n)O(n) | O(1)O(1) | 不稳定 | 较复杂 |
直接选择排序 | O(n2)O(n2) | O(n2)O(n2) | O(n2)O(n2) | O(1)O(1) | 不稳定 | 简单 |
堆排序 | O(nlog2n)O(nlog2n) | O(nlog2n)O(nlog2n) | O(nlog2n)O(nlog2n) | O(1)O(1) | 不稳定 | 较复杂 |
冒泡排序 | O(n2)O(n2) | O(n2)O(n2) | O(n)O(n) | O(1)O(1) | 稳定 | 简单 |
快速排序 | O(nlog2n)O(nlog2n) | O(n2)O(n2) | O(nlog2n)O(nlog2n) | O(nlog2n)O(nlog2n) | 不稳定 | 较复杂 |
归并排序 | O(nlog2n)O(nlog2n) | O(nlog2n)O(nlog2n) | O(nlog2n)O(nlog2n) | O(n)O(n) | 稳定 | 较复杂 |
基数排序 | O(d(n+r))O(d(n+r)) | O(d(n+r))O(d(n+r)) | O(d(n+r))O(d(n+r)) | O(n+r)O(n+r) | 稳定 | 较复杂 |
① 插入排序
•思想:每步将一个待排序的记录,按其顺序码大小插入到前面已经排序的字序列的合适位置,直到全部插入排序完为止。
•关键问题:在前面已经排好序的序列中找到合适的插入位置。
•方法:
–直接插入排序
–二分插入排序
–希尔排序
(1)直接插入排序(从后向前找到合适位置后插入)
基本思想:每步将一个待排序的记录,按其顺序码大小插入到前面已经排序的字序列的合适位置(从后向前找到合适位置后),直到全部插入排序完为止。
$arr = [8,6,9,20,5,3];
function directInsertSort($arr) {
$len = count($arr);
for($i=1; $i<$len; $i++) { //待排序的元素
$tmp = $arr[$i];
for($j=$i-1; $j>=0; $j--) { //前边已经排序好的元素
if($tmp >= $arr[$j]) { //减少循环次数
break;
} else { //置换
$arr[$j+1] = $arr[$j];
$arr[$j] = $tmp;
}
}
}
return $arr;
}
var_dump(directInsertSort($arr));die;
(2)希尔排序
基本思想:先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1,即所有记录放在同一组中进行直接插入排序为止。(实际上就是一个分组插入排序,先对数据分组,然后对每个分组做插入排序)
function shellSort($arr) {
$len = $d = count($arr);
while($d>1) {
$d = ceil($d/2); //步长
//直接插入排序
for($i=$d; $i<$len; $i+=$d) {
$tmp = $arr[$i];
for($j= $i-$d; $j>=0; $j-=$d) {
if($tmp >= $arr[$j]) { //减少内部循环次数
break;
} else {
$arr[$j+$d] = $arr[$j];
$arr[$j] = $tmp;
}
}
}
}
return $arr;
}
var_dump(shellSort($arr));
② 选择排序
(1)直接选择排序
基本思想:在要排序的一组数中,选出最小的一个数与第一个位置的数交换;然后在剩下的数当中再找最小的与第二个位置的数交换,如此循环到倒数第二个数和最后一个数比较为止。
function selectSort($arr) {
$len = count($arr);
for($i=0; $i<$len; $i++) {
$pos = $i; //记录最小值位置,避免内部循环中多次置位
for($j=$i+1; $j<$len; $j++) {
if($arr[$j] < $arr[$pos]) {
$pos = $j;
}
}
if($pos != $i) {
$tmp = $arr[$i];
$arr[$i] = $arr[$pos];
$arr[$pos] = $tmp;
}
}
return $arr;
}
var_dump(selectSort($arr));
(2)堆排序
1、基本思想:
堆排序是一种树形选择排序,是对直接选择排序的有效改进。
堆的定义下:具有n个元素的序列 (h1,h2,…,hn),当且仅当满足(hi>=h2i,hi>=2i+1)或(hi<=h2i,hi<=2i+1) (i=1,2,…,n/2)时称之为堆。在这里只讨论满足前者条件的堆。由堆的定义可以看出,堆顶元素(即第一个元素)必为最大项(大顶堆)。完全二叉树可以很直观地表示堆的结构。堆顶为根,其它为左子树、右子树。
思想:初始时把要排序的数的序列看作是一棵顺序存储的二叉树,调整它们的存储序,使之成为一个堆,这时堆的根节点的数最大。然后将根节点与堆的最后一个节点交换。然后对前面(n-1)个数重新调整使之成为堆。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。所以堆排序有两个函数组成。一是建堆的渗透函数,二是反复调用渗透函数实现排序的函数。
function buildMaxHeap(&$arr)
{
global $len;
for ($i = floor($len/2); $i >= 0; $i--) {
heapify($arr, $i);
}
}
function heapify(&$arr, $i)
{
global $len;
$left = 2 * $i + 1;
$right = 2 * $i + 2;
$largest = $i;
if ($left < $len && $arr[$left] > $arr[$largest]) {
$largest = $left;
}
if ($right < $len && $arr[$right] > $arr[$largest]) {
$largest = $right;
}
if ($largest != $i) {
swap($arr, $i, $largest);
heapify($arr, $largest);
}
}
function swap(&$arr, $i, $j)
{
$temp = $arr[$i];
$arr[$i] = $arr[$j];
$arr[$j] = $temp;
}
function heapSort($arr) {
global $len;
$len = count($arr);
buildMaxHeap($arr);
for ($i = count($arr) - 1; $i > 0; $i--) {
swap($arr, 0, $i);
$len--;
heapify($arr, 0);
}
return $arr;
}
var_dump(heapSort($arr));
③ 交换排序
(1)冒泡排序
1、基本思想:在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。
//下沉
function sinkBubbleSort($arr) {
$len = count($arr);
for($i=0; $i<$len; $i++) { //共进行$len次(轮)冒泡
$swapFlag = false; //标记是否置换
for($j=0; $j<$len-$i-1; $j++) { //下沉,控制$j的最大位置,因为最下边的$i个已经完成排序。正着循环,因为可以节省下边已经排好序的循环。
if($arr[$j] > $arr[$j+1]) {
$swapFlag = true;
$tmp = $arr[$j];
$arr[$j] = $arr[$j+1];
$arr[$j+1] = $tmp;
}
}
if(!$swapFlag) { //如果没有置换发生,说明已是一个有序数组
return $arr;
}
}
return $arr;
}
var_dump(sinkBubbleSort($arr));
//上浮
function goUpBubbleSort($arr) {
$len = count($arr);
for($i=0; $i<$len-1; $i++) { //共进行$len次(轮)冒泡
$swapFlag = false; //标记是否置换
for($j=$len-1; $j>$i; $j--) { //下沉,控制$j的最小位置,因为最上边$i个已经完成排序。倒着循环,因为可以节省上边已经排好序的循环
if($arr[$j] > $arr[$j-1]) {
$swapFlag = true;
$tmp = $arr[$j];
$arr[$j] = $arr[$j-1];
$arr[$j-1] = $tmp;
}
}
if(!$swapFlag) {
return $arr;
}
}
return $arr;
}
var_dump(goUpBubbleSort($arr));
(2)快速排序
1、基本思想:选择一个基准元素,通常选择第一个元素或者最后一个元素,通过一趟扫描,将待排序列分成两部分,一部分比基准元素小,一部分大于等于基准元素,此时基准元素在其排好序后的正确位置,然后再用同样的方法递归地排序划分的两部分。
function quickSort($arr) {
$len = count($arr);
if($len <= 1) {
return $arr;
}
$base = $arr[0]; //选取一个基准值
$left = $right = [];
for($i=1; $i<$len; $i++) { //除去基准值
if($arr[$i] < $base) { //小于基准值
$left[] = $arr[$i];
} else { //不小于基准值
$right[] = $arr[$i];
}
}
//递归调用
$left = quickSort($left);
$right = quickSort($right);
return array_merge($left, [$base], $right);
}
var_dump(quickSort($arr));
④ 归并排序
1、基本思想:归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
function merge($left, $right) {
$result = [];
while(count($left) > 0 && count($right) > 0) {
if($left[0] > $right[0]) {
$result[] = array_shift($right);
} else {
$result[] = array_shift($left);
}
}
while(count($left) > 0) {
$result[] = array_shift($left);
}
while(count($right) > 0) {
$result[] = array_shift($right);
}
return $result;
}
function mergeSort($arr) {
$len = count($arr);
if($len <= 1) {
return $arr;
}
$middle = floor($len/2);
$left = array_slice($arr, 0, $middle);
$right = array_slice($arr, $middle);
return merge(mergeSort($left), mergeSort($right));
}
var_dump(mergeSort($arr));