概述
排序有内部排序和外部排序,内部排序就是数据在内存中排序,而外部排序是因为数据量很大,在排序过程中需要访问外存。
这里我们只谈内部排序。
排序算法在很多领域都有相当重要的地位,尤其是在处理大数据方面,一个优秀的算法可以节省大量的资源。那么算法很多,选择如何使用一般需要考虑以下几点:
1.时间复杂度(最差,平均和最好性能),根据排序的大小(n)
按数量级递增排序,常见的时间复杂度有:常数阶(O(1)),对数阶(O(logn)),线性阶(O(n)),线性对数阶(O(nlogn)),平方阶(O(n^2)),立方阶(O(n^3))...k次方阶(O(n^k)),指数阶(O(2^n))。
随着n不断增大,上述时间复杂度不断增大,算法执行效率越低。
一般而言,好的性能是O(nlogn),差的性能是O(n^2),理想的性能是O(n)。
2.空间复杂度
指执行这个算法所需要的内存空间。
3.稳定性
一个排序算法是稳定的,就是当有两个相等记录的关键字A和B,且在原来列表中A在B之前,在排序后A也在B之前。
下面就分析下各类算法的具体实现
1. 插入排序之直接插入排序
基本思想:
每次从无序表中取第一个元素,把它插入到有序表的合适位置,使有序表依旧有序。
算法实现(由小到大排序):
<?php
function insertSort($datas) {
$length = count($datas);
for ($i = 1; $i < $length; $i++) {
for ($j = 0; $j < $i; $j++) {
if ($datas[$j] > $datas[$i]) {
$temp = $datas[$j];
$datas[$j] = $datas[$i];
$datas[$i] = $temp;
}
}
}
return $datas;
}
$oldData = array(24, 99, -13, 145, 2, 13, 27);
$newData = insertSort($oldData);
print_r($newData);
2.插入排序之希尔排序
希尔排序又叫缩小增量排序
基本思想:
将整个待排元素序列分割成若干个子序列(由相隔某个‘增量’的元素组成)分别进行直接插入排序,然后依次缩减增量再进行排序,直至‘增量’变为1,即所有记录放在一个组里排序。
以n=9的一个数组24,99,-13,145,2,13,27,55,4为例:
第一次 增量([n / 2])为4
24 99 -13 145 2 13 27 55 4
A0 A1 A2
B0 B1
C0 C1
D0 D1
A0,A1,B0,B1等为分组标记,大写字母相同表示在同组内,数字表示该组下标,每次对同一组进行直接插入排序。即分成了4组(24,2,4)(99,13)(-13,27)(145,55),排序后就变成(2,4,24)(13,99)(-13,27)(55,145),下同。
第二次 增量 为 2
第一次排序后
2 13 -13 55 4 99 27 145 24
A0 A1 A2 A3 A4
B0 B1 B2 B3
第三次 增量 为1
第二次排序后
-13 13 2 55 4 99 24 145 27
A0 A1 A2 A3 A4 A5 A6 A7 A8
进行直接插入排序后得到数组:-13,2,4,13,24,27,55,99,145
算法实现(由小到大排序):
<?php
function shellInsertSort($datas) {
$length = count($datas);
$increment = floor($length / 2);
while (1 <= $increment) {
for ($i = 0; $i < $increment; $i++) {
for ($j = $i + $increment; $j < $length; $j += $increment) {
for ($z = $i; $z < $j; $z += $increment) {
if ($datas[$z] > $datas[$j]) {
$temp = $datas[$z];
$datas[$z] = $datas[$j];
$datas[$j] = $temp;
}
}
}
}
$increment = floor($increment / 2);
}
return $datas;
}
$oldData = array(24, 99, -13, 145, 2, 13, 27, 55, 4);
$newData = shellInsertSort($oldData);
print_r($newData);
基本思想:
在待排序的一组数中,选择最小(最大)的数与第一个位置的数比较交换,然后在剩下的数中再找出最小(最大)的数与第二个位置的数比较交换,直到第n-1个数与第n个数比较交换。
算法实现(由小到大排序):
<?php
function selectSort($datas) {
$length = count($datas);
for ($i = 0; $i < $length - 1; $i++) {
$minIndex = $i;
for ($j = $i + 1; $j < $length; $j++) {
if ($datas[$minIndex] > $datas[$j]) {
$minIndex = $j;
}
}
if ($datas[$i] > $datas[$minIndex]) {
$temp = $datas[$minIndex];
$datas[$minIndex] = $datas[$i];
$datas[$i] = $temp;
}
}
return $datas;
}
$oldData = array(24, 99, -13, 145, 2, 13, 27, 55, 4);
$newData = selectSort($oldData);
print_r($newData);
基本思想:
堆是一种完全二叉树或者近似完全二叉树。
堆的定义如下:n个元素的序列(k1,k2,k3...kn),当且仅当满足
1. ki <= k(2i) 且 ki <= k(2i+1) (1 <= i <= n/2)
或者
2. ki >= k(2i) 且 ki >= k(2i+1) (1 <= i <= n/2)
时称之为堆。ki相当于二叉树的非叶子结点,k(2i)是左子结点,k(2i+1)是右子结点。
由定义可以看出,1的每个结点都大于父结点,此称之为小根堆;反之2称之为大根堆。
24,99,-13,145,2,13,27,55,4
1.小根堆
存储结构:-13,2,4,13,24,27,55,99,145
2.大根堆
存储结构:145,99,55,27,24,13,4,2,-13
初始时把待排序的n个数看成是一棵顺序存储的二叉树,调整它们的存储顺序使之成为堆,将堆顶最大(最小)的数输出(即与第n个数交换位置),此时剩下n-1个数的堆被破坏,对n-1个数进行堆调整,输入堆顶最大(最小)的数,然后再对n-2个数进行堆调整并输出。依次类推,直到只有两个结点的堆,交换它们,最终得到n个有序序列,这个过程称为堆排序。
因此,实现堆排序需要解决两个问题;1.将n个待排序列初始化堆;2.将剩余n-1个数调整使其成为一个堆。
初始化堆事实上也是调整堆的过程,只不过初始化是对所有非叶子结点都进行调整。
初始化为大根堆的方法:
1)n个结点的完全二叉树,最后一个结点是第[n/2]个结点的子数;
2)从第[n/2]个结点为根的子树开始,使子树成为堆;
3)然后向前对各个结点为根的子树进行调整,使之成为堆,直到根结点。
如图初始化堆的过程:24,99,-13,145,2,13,27,55,4
调整n-1个数的方法:
1)有n个元素的大根堆,输出堆顶元素后(堆顶元素与第n个元素交换),剩下n-1个元素,堆被破坏;
2)将根结点与左右子树中较大的进行比较交换;
3)若与左子树交换:若左子树的堆被破坏,重复2)
4)若与右子树交换:若右子树的堆被破坏,重复2)
5)继续对不满足堆性质的子树进行交换,直到叶子结点,堆建成。
如图调整堆的过程:
算法实现(由小到大排序):
<?php
/**
* 调整堆
* @param dArray是待调整的数组
* @param nodeIndex是待调整数组元素的位置
* @param length是数组长度
**/
function heapAdjust(&$dArray, $nodeIndex, $length) {
$child = 2 * $nodeIndex + 1; // 左子树结点位置
while ($child < $length) {
if ($child + 1 < $length && $dArray[$child] < $dArray[$child + 1]) { // 找到左右子树中较大一个的位置
$child++;
}
if ($dArray[$nodeIndex] < $dArray[$child]) {
$temp = $dArray[$child];
$dArray[$child] = $dArray[$nodeIndex];
$dArray[$nodeIndex] = $temp;
} else {
break;
}
$nodeIndex = $child;
$child = 2 * $nodeIndex + 1;
}
}
function heapSort($datas) {
$length = count($datas);
// 初始化堆
for ($i = floor($length / 2); $i > 0; $i--) {
heapAdjust($datas, $i - 1, $length);
}
for ($i = 0, $j = $length; $i < $length - 1; $i++) {
// 输出堆顶元素
if ($datas[0] > $datas[$j - 1]) {
$temp = $datas[$j - 1];
$datas[$j - 1] = $datas[0];
$datas[0] = $temp;
}
$j--;
// 对被破坏的堆进行调整
heapAdjust($datas, 0, $j);
}
return $datas;
}
$oldData = array(24, 99, -13, 145, 2, 13, 27, 55, 4);
$newData = heapSort($oldData);
print_r($newData);
基本思想:
冒泡排序是非常容易理解和实现的,还是以从小到大排序为例:
1. 自上而下对相邻两个数进行比较和调整,较大的往下沉,较小的往上冒;
2. 这样对n个数遍历过一次后,最大的数就在第n的位置上;
3. 重复上述步骤直至排序完成。
算法实现(由小到大排序):
<?php
function bubbleSort($datas) {
$length = count($datas);
for ($i = 0; $i < $length - 1; $i++) {
for ($j = 0; $j < $length - 1 - $i; $j++) {
if ($datas[$j] > $datas[$j + 1]) {
$temp = $datas[$j + 1];
$datas[$j + 1] = $datas[$j];
$datas[$j] = $temp;
}
}
}
return $datas;
}
$oldData = array(24, 99, -13, 145, 2, 13, 27, 55, 4);
$newData = bubbleSort($oldData);
print_r($newData);
6.交换排序之快速排序
快速排序因为排序效率在几种同为O(nlogn)的几种排序方法中效率较高,所以经常被采用。
基本思想:
通过一趟排序将待排序的数据分割成独立的两部分,其中一部分数据都要比另一部分数据小,然后分别对两部分进行快速排序,以此达到整个数据变成有序序列。
1)选择一个基准元素,通常选第一个或最后一个;
2)通过一趟排序分割成两部分,其中一部分都比基准元素小,另一部分都比基准元素大;
3)接着用同样的方法分别对两部分进行排序,直至序列有序。
取第一个数为基准元素,初始时 i = 0,j = 8,key = a[0] = 24
此时 i = 1,j = 8,key = 24
可以看出a[4]前面的数都小于它,a[4]后面的数都大于它,接着再分别对a[0...4]和a[5...8]这两个部分重复上述步骤即可。
7.归并排序
8.基数排序
总结
一趟排序的过程(如下是个待排序的数组a):
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
数据 | 24 | 99 | -13 | 145 | 2 | 13 | 27 | 55 | 4 |
现在目的就是要找到比24都小的数且放在24的左边,找到比24都大的数且放在24的右边,慢慢来,一个一个找,因为是以第一个元素作为基准元素,所以我们从最右边开始找。
可以这样理解,因为我们取走了a[0]位置上的数,现在需要找个数填补a[0]的位置,从右边开始找(j--)找到第一个小于24的数(是4),于是把4放在a[0]上,此时a[8]的位置又空缺出来,接着从左边开始找(i++)找到第一个大于24的数(是99),于是把99放在a[8]的位置上,数组变成:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
数据 | 4 | 24 | -13 | 145 | 2 | 13 | 27 | 55 | 99 |
重复上面的步骤,相当于a[1]位置空了,需要从右向左找,再从左向右找
此时 i = 3,j = 5, key = 24
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
数据 | 4 | 13 | -13 | 24 | 2 | 145 | 27 | 55 | 99 |
接着填补a[3]的位置,找到了2(a[4]),此时j变成了4,接着填补a[4]的位置,发现 i = 4 = j,此时查找结束,将key(24)填回a[4]的位置,数组变成:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
数据 | 4 | 13 | -13 | 2 | 24 | 145 | 27 | 55 | 99 |
对上述过程简单总结:
1)i = L,j = R,将基准数取出形成空缺a[i];
2)j--由右向左找到第一个比它小的数,填回a[i],形成空缺a[j];
3)i++由左向右找到第一个比它大的数,填回a[j];
4)重复2)3),直到i == j,将基准数填入a[i]。
算法实现(由小到大排序):
<?php
// 不断挖坑填数
function quickAdjust(&$datas, $l, $r) {
if ($l < $r) {
$i = $l;
$j = $r;
$key = $datas[$i]; // 基准数
while ($i < $j) {
while ($i < $j && $key < $datas[$j]) {
$j--;
}
if ($i < $j) {
$datas[$i] = $datas[$j]; // 填补i上的位置,形成j位置的空缺
$i++;
}
while ($i < $j && $datas[$i] < $key) {
$i++;
}
if ($i < $j) {
$datas[$j] = $datas[$i]; // 填补j上的位置,形成i位置的空缺
$j--;
}
}
$datas[$i] = $key; // 将基准书补回
quickAdjust($datas, $l, $i - 1);
quickAdjust($datas, $i + 1, $r);
}
}
function quickSort($datas) {
$length = count($datas);
quickAdjust($datas, 0, $length - 1);
return $datas;
}
$oldData = array(24, 99, -13, 145, 2, 13, 27, 55, 4);
$newData = quickSort($oldData);
print_r($newData);
7.归并排序
基本思想:
归并排序是将两个(或两个以上)的有序列合并成一个新的有序列。即把待排序列分成若干个有序的子序列,再把有序的子序列合并成整体有序序列。该算法是采用分治法的一个非常典型的应用。
归并,顾名思义,需要先归再并。我们先分别来分析。
首先考虑如何将两个有序列合并,这个比较简单:
假设a[i...n]有两个有序子列a[i...m]和a[m+1...n],有个临时数组temp(刚开始为空)
1)i = i,j = m + 1,分别从子列的第一个数开始比较;
2)若 i > m 或 j > n,则转至步骤4);
3)若 a[i] < a[j],则将a[i]放入temp,i加1;转至步骤2)
若 a[i] < a[j],则将a[j]放入temp,j加1;转至步骤2)
4)此时至少一个子列已经处理完成,将另一个子列剩下的部分加入temp
若 i < m,将 a[i...m]加入temp;若j < n,将 a[j...n]加入temp。
5)合并完成。
再考虑如何分成若干个有序子列:
1个元素总是有序的,所以对n个待排序列,每一个元素都可以看成有序的,再合并成相邻的两个小组就可以了,这样两两合并,直到生成有序序列。
算法实现(由小到大排序):
<?php
// 将a[i...m]和a[m+1...n]合并成有序列
function merge(&$datas, $first, $mid, $last)
{
$tempArray = array();
$i = $first;
$j = $mid + 1;
$k = 0;
while (($i <= $mid) && ($j <= $last)) {
if ($datas[$i] < $datas[$j]) {
$tempArray[] = $datas[$i];
$i++;
$k++;
} else {
$tempArray[] = $datas[$j];
$j++;
$k++;
}
}
while ($i <= $mid) {
$tempArray[] = $datas[$i];
$i++;
$k++;
}
while ($j <= $last) {
$tempArray[] = $datas[$j];
$j++;
$k++;
}
for ($z = 0; $z < $k; $z++) {
$datas[$first + $z] = $tempArray[$z];
}
}
function mergeSort(&$datas, $first, $last)
{
if ($first < $last) {
$mid = floor(($first + $last) / 2);
mergeSort($datas, $first, $mid);
mergeSort($datas, $mid + 1, $last);
merge($datas, $first, $mid, $last);
}
}
function main($datas)
{
$length = count($datas);
mergeSort($datas, 0, $length - 1);
return $datas;
}
$oldData = array(24, 99, -13, 145, 2, 13, 27, 55, 4);
$newData = main($oldData);
print_r($newData);
8.基数排序
基本思想:
将所有待比较的数值(正整数)统一为同样的位数长度(k1,k2...kn),位数较短的数前面补零,然后就有两种处理方法:
1)最高位优先法(MSD):
a. 先按k1排序分组,将序列分成若干个子序列,同一序列中,k1相等;
b. 再对各组按k2排序分组,之后依次对后面的关键码继续这样的分组,直到按kn对各子组排序后;
c. 再将各组连接起来,便得到一个有序序列。
2)最低位优先法(LSD):
a. 先对kn排序分组,按次序将各组连接起来;
b. 再对kn-1排序分组,然后再连接起来,重复步骤直到k1;
c. 将子组连接起来,便得到有序序列。
以LSD为例,有如下一串数值:
72,93,24,65,99,62,45,89,42
首先根据个位数进行分配:
0
1
2 72 62 42
3 93
4 24
5 65 45
6
7
8
9 99 89
把各组按次序收集起来:
72,62,42,93,24,65,45,99,89
再根据十位数进行分配:
0
1
2 24
3
4 42 45
5
6 62 65
7 72
8 89
9 93 99
再收集:
24,42,45,62,65,72,89,93,99
排序完成,变成有序序列。
算法实现(由小到大排序):
<?php
// 找到number上n位的数字
function getDigit($number, $n)
{
$temp = 1;
for ($i = 0; $i < $n - 1; $i++) {
$temp *= 10;
}
return ($number / $temp) % 10;
}
// LSD法
function radixLSDSort($datas)
{
$length = count($datas);
$n = 1; // 从个位开始分配
$flag = true;
while ($flag) {
$sumN = 0;
$temp = array(); // 临时空间
for ($i = 0; $i < $length; $i++) {
$placeNum = getDigit($datas[$i], $n);
$temp[$placeNum][] = $datas[$i];
$sumN += $placeNum;
}
if (0 == $sumN) {
$flag = false;
} else {
$datas = array();
for ($j = 0; $j <= 9; $j++) { // 各组按次序收集起来
isset($temp[$j]) && $datas = array_merge($datas, $temp[$j]);
}
$n++;
}
unset($temp);
}
return $datas;
}
$oldData = array(24, 99, 13, 145, 2, 13, 27, 55, 4);
$newData = radixLSDSort($oldData);
print_r($newData);
总结
每种算法的代码都可以深度挖掘,继续优化,但编程到了极致,核心就是思想,学习算法最重要的是开拓思路,转换思维。确保自己真正明白算法的思想,然后自己写出代码,才是真正掌握。