一.荷兰国旗问题
1.1
给定一个数组Arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。
要求额外空间复杂度O(1),时间复杂度O(N)
思想:
维持一个小于等于区域的index,
如果一个数小于等于num,则和index位置交换
如果一个数大于num,则直接跳下一个
代码:
protected function splitArray(&$data, $left, $right)
{
$num = 47;
$i = 0;
for ($j=0; $j < $right ; $j++) {
if ($data[$j] >= $num) {
$this->swapData($data, $i++, $j);
}
}
}
1.2 荷兰国旗问题
给定一个数组Arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。
要求额外空间复杂度O(1),时间复杂度O(N)
思想:
总体的策略是:
遇到小的数字,那么把它往前面移动,
遇到大的数字,把它让后面移动,
遇到相等的数字,不动
维持两个三个变量来进行交换 less , more , current
其中more的值应该为len(arr),这样在判断的时候多判断一次,
并且j-=1写在后面 more要等于len(arr),并且运算的时候先要减一,
否则会出现这种情况: [7,6,4,1,2,5,4] 变为[2, 4, 4, 1, 5, 6, 7],
因为这种情况下面并没有对中间的1进行排序,少了一次排序的次数
代码:
public function netherlandsFlag ()
{
$data = $this->sampleData();
$current = 0;
$less = 0;
$more = count($data);
$num = 47;
while ($current < $more) {
# 如果小于,那么less和current要进行交换,并且都推进一位
if ($data[$current] < $num) {
$this->swapData($data, $current++, $less++);
# 如果大于,那么--more和current要进行交换,more向下推进一位
} else if ($data[$current] > $num) {
$this->swapData($data, $current, --$more);
# 如果等于,那么直接向下进行
} else {
$current++;
}
}
}
二.快速排序
时间复杂度O(N*logN),空间复杂度O(logN)
口诀:
分区递归和while,选中端点做中点
思想:
1.先从数列中取出一个数作为基准数,记为x。
2.分区过程,将不小于x的数全放到它的右边,不大于x的数全放到它的左边。
(这样key的位置左边的没有大于key的,右边的没有小于key的,只需对左右区间排序即可)
3.再对左右区间重复第二步,直到各区间只有一个数
代码:
protected function qSort(&$data)
{
$l = 0;
$r = count($data) - 1;
$this->quickSort($data, $l, $r);
}
// 递归
protected function quickSort(&$data, $l, $r)
{
if ($l < $r) {
$p = $this->partition($data, $l, $r);
$this->quickSort($data,$l, $p[0]-1);
$this->quickSort($data, $p[1] + 1, $r);
}
}
// 分区
protected function partition(&$data, $l, $r)
{
$start = $l - 1;
$end = $r;
while ($l < $end) {
if ($data[$l] < $data[$r]) {
$this->swapData($data, ++$start, $l++);
} else if($data[$l] > $data[$r]) {
$this->swapData($data, --$end, $l);
} else {
$l++;
}
}
$this->swapData($data, $end, $r);
return [$start+1, $end];
}
三.堆排序
时间复杂度O(N*logN),额外空间复杂度O(1)
思想:
- 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
- 将堆顶元素与末尾元素进行交换,使末尾元素最大。
然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。
如此反复进行交换、重建、交换。
代码:
/*
* 小根堆(倒序)
*/
protected function heapSort(&$data)
{
$dataNum = count($data);
// 将第一次排序抽离出来,因为最后一次排序不需要再交换值了
$this->buildHeap($data, $dataNum);
for ($i=$dataNum-1; $i > 0 ; $i--) {
$this->swapData($data, $i, 0);
$dataNum--;
$this->buildHeap($data, $dataNum);
}
}
// 用数组建立小根堆
protected function buildHeap(&$data, $dataNum)
{
// 计算出开始下标,比较每一个子树的父节点和子节点,将最小值存入父节点中
// 从$index处对一个数进行循环比较,形成最小堆
for ($index=intval($dataNum/2)-1; $index >= 0 ; $index--) {
// 如果有左子节点,将其下标存进最小值$min
if ($index*2+1 < $dataNum) {
$min = $index*2+1;
// 如果有右子节点,比较左右子节点的大小
// 如果右子节点更小,将其节点的下标记录进最小值$min
if ($index*2+2 < $dataNum && $data[$index*2+2] < $data[$min]) {
$min = $index*2+2;
}
// 将子节点中较小的和父节点比较,若子节点较小,
// 与父节点交换位置,同时更新较小
if ($data[$min] < $data[$index]) {
$this->swapData($data, $min, $index);
}
}
}
}
/*
* 大根堆(正序)
*/
protected function heapSort(&$data)
{
$dataNum = count($data);
if (!$data || $dataNum < 2) {
return $data;
}
// 把$data中的数依次构建成堆结构
for ($i=0; $i < $dataNum; $i++) {
$this->heapInsert($data, $i);
}
// 第一次交换堆顶和堆尾元素 将第一次排序抽离出来,因为最后一次排序不需要再交换值了
$this->swap($data, 0, --$dataNum);
while ($dataNum > 0) {
$this->heapify($data, 0, $dataNum);
$this->swap($data, 0, --$dataNum);
}
}
// 构建大根堆
protected function heapInsert(&$data, $index)
{
while ($data[$index] > $data[($index - 1) / 2]) {
$this->swap($data, $index, ($index -1) / 2);
$index = ($index - 1) / 2;
}
}
protected function heapify(&$data, $index, $dataNum)
{
// 计算出开始下标,比较每一个子树的父节点和子节点,将最大值存入父节点中
// 从$index处对一个数进行循环比较,形成最小堆
$left = $index * 2 + 1;
while ($left < $dataNum) {
$largest = $left + 1 < $dataNum && $data[$left + 1] > $data[$left] ? $left + 1 : $left;
$largest = $data[$largest] > $data[$index] ? $largest : $index;
if ($largest == $index) {
break;
}
$this->swap($data, $largest, $index);
$index = $largest;
$left = $index * 2 + 1;
}
}
四.排序算法的稳定性及其汇总
4.1 稳定性
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,
若经过排序,这些记录的相对次序保持不变,
即在原序列中,r[i]=r[j],且r[i]在r[j]之前,
而在排序后的序列中,r[i]仍在r[j]之前,
则称这种排序算法是稳定的;否则称为不稳定的。
4.2 汇总
4.2.1 冒泡排序
冒泡排序就是把小的元素往前调或者把大的元素往后调。
比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改 变,所以冒泡排序是一种稳定排序算法。
4.2.2 选择排序
选择排序是给每个位置选择当前元素最小的,
比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个 元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么 交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9, 我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。
4.2.3 插入排序
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。
当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开 始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相 等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳 定的。
4.2.4 快速排序
快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] > a[center_index]。如果i和j都走不动了,i <= j, 交换a[i]和a[j],重复上面的过程,直到i>j。 交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为 5 3 3 4 3 8 9 10 11, 现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻。
4.2.5 归并排序
归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有 序的长序列,不断合并直到原序列全部排好序。
可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定 性。那么,在短的有序序列合并的过程中,稳定是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结 果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。
4.2.6 基数排序
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。
有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优 先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。
4.2.7 希尔排序(shell)
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小, 插入排序对于有序的序列效率很高。
所以,希尔排序的时间复杂度会比o(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元 素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
4.2.8 堆排序
我们知道堆的结构是节点i的孩子为2i和2i+1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n /2-1, n/2-2, …1这些个父节点选择元素时,就会破坏稳定性。有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没 有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。
综上,得出结论: 选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法,而冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法。
五.有关排序问题的补充
5.1 归并排序的额外空间复杂度可以变成O(1),但是非常难,不需要掌握,可以搜索“归并排序 内部缓存法”
5.2 快速排序可以做到稳定性问题,但是非常难,不需要掌握,可以搜“01 stable sort”
5.3 有一道题目,是奇数放在数组左边,偶数放在数组右边,还要求原始的相对次序不变。
奇偶是01标准, 和快速排序一样。
这个题,要保证额外空间复杂度为o(1)的情况下,时间复杂度只能做到o(n^2) ,利用快排思想并不能保证稳定性,利用归并思想做不到常量空间
六.比较器的使用
6.1 PHP - 一维数组的排序函数
sort() - 以升序对数组排序
rsort() - 以降序对数组排序
asort() - 根据值,以升序对关联数组进行排序
ksort() - 根据键,以升序对关联数组进行排序
arsort() - 根据值,以降序对关联数组进行排序
krsort() - 根据键,以降序对关联数组进行排序
6.2 二维数组的排序:使用array_multisort和usort可以实现
例如像下面的数组:
$users = array(
array('name' => 'tom', 'age' => 20),
array('name' => 'anny', 'age' => 18),
array('name' => 'jack', 'age' => 22)
);
6.2.1 使用array_multisort
使用这个方法,会比较麻烦些,要将age提取出来存储到一维数组里,然后按照age升序排列。具体代码如下:
$ages = array();
foreach ($users as $user) {
$ages[] = $user['age'];
}
array_multisort($ages, SORT_ASC, $users);
执行后,$users就是排序好的数组了,可以打印出来看看。如果需要先按年龄升序排列,再按照名称升序排列,方法同上,就是多提取一个名称数组出来,最后的排序方法这样调用:
array_multisort($ages, SORT_ASC, $names, SORT_ASC, $users);
6.2.2 使用usort
使用这个方法最大的好处就是可以自定义一些比较复杂的排序方法。例如按照名称的长度降序排列:
usort($users, function($a, $b) {
$al = strlen($a['name']);
$bl = strlen($b['name']);
if ($al == $bl) {
return 0;
}
return ($al > $bl) ? -1 : 1;
});
这里使用了匿名函数,如果有需要也可以单独提取出来。其中$a, $b可以理解为$users数组下的元素,可以直接索引name值,并计算长度,而后比较长度就可以了。
七.桶排序、计数排序、基数排序
1.非基于比较的排序,与被排序的样本的实际数据状况很有关系,所以实际中并不经常使用。
2.时间复杂度O(N),额外空间复杂度O(N)
3.稳定的排序
7.1 桶排序
思想:
- 假设待排序的一组数统一的分布在一个范围中,并将这一范围划分成几个子范围,
也就是桶。- 将待排序的一组数,分档规入这些子桶,并将桶中的数据进行排序。
- 将各个桶中的数据有序的合并起来。
代码:
function bucketSort($max, $array)
{
//填充木桶
$arr = array_fill(0, $max+1, 0);
//开始标示木桶
for($i = 0; $i <= count($array)-1 ; $i++)
{
$arr[$array[$i]] ++;
}
$mutomg = array();
//开始从木桶中拿出数据
for($i = 0; $i <= $max; $i ++)
{
for($j = 1; $j <= $arr[$i]; $j ++)
{ //这一行主要用来控制输出多个数字
$mutong[] = $i;
}
}
return $mutong;
}
7.2 计数排序
计数排序本质上是一种特殊的桶排序,当桶的个数最大的时候,就是计数排序。
思想:
举个例子, nums=[2, 1, 3, 1, 5] , 首先扫描一遍获取最小值和最大值,
maxValue=5 , minValue=1 ,于是开一个长度为5的计数器数组 counter ,
- 分配。
统计每个元素出现的频率,得到 counter=[2, 1, 1, 0, 1] ,
例如 counter[0] 表示值 0+minValue=1 出现了2次。- 收集。
counter[0]=2 表示 1 出现了两次,那就向原始数组写入两个1,
counter[1]=1 表示 2 出现了1次,那就向原始数组写入一个2,
依次类推,最终原始数组变为 [1,1,2,3,5] ,排序好了。
// 样本数组
protected function sampleArray()
{
return [6 ,1 ,2 ,7 ,9 ,3 ,4 ,5 ,10 ,8];
}
// 返回num存入的桶的下标
protected function judgeBucket($num, $len, $min, $max){
return floor(($num - $min) * $len / ($max - $min));
}
// 桶排序
protected function bucketSort(&$data)
{
$len = count($data);
if ($data == null || $len < 2) {
return 0;
}
$min = min($data);
$max = max($data);
if ($min == $max) {
return 0;
}
// 每个桶信息的描述,分别为存在标识,最小值,最大值
$hasNum = [];
for ($i=0; $i < $len; $i++) {
$sign = (int)$this->judgeBucket($data[$i], $len, $min, $max);
$hasNum[$sign] = $data[$i];
}
for ($j=0; $j < $len+1; $j++) {
if (array_key_exists($j, $hasNum)) {
$arr[] = $hasNum[$j];
}
}
$data = $arr;
}
八.桶排序实践
给定一个数组,求如果排序之后,相邻两数的最大差值,要求时间复杂度O(N),且要求不能用基于比较的排序
思想:
先遍历N个数的数组,找到最大值Max与最小值Min。
最小值与最大值之间的范围等分成N+1份。
最小值放到0号桶里,最大值放到N号桶里,中间至少存在一个空桶。
统计每个桶最大值与最小值。
非空桶最小值与其左边的最近的非空桶的最大值之间的差值。
// 样本数组
protected function sampleArray()
{
return [6 ,1 ,2 ,7 ,9 ,3 ,4 ,5 ,10 ,8];
}
// 返回num存入的桶的下标
protected function judgeBucket($num, $len, $min, $max){
return floor(($num - $min) * $len / ($max - $min));
}
// 计算最大差值
protected function maximumDifferenceByBucketSort($data)
{
$len = count($data);
if ($data == null || $len < 2) {
return 0;
}
$min = min($data);
$max = max($data);
if ($min == $max) {
return 0;
}
// 每个桶信息的描述,分别为存在标识,最小值,最大值
$hasNum = [];
for ($i=0; $i < $len+1 ; $i++) {
$hasNum[$i] = 0;
}
$mins = [];
$maxs = [];
for ($i=0; $i < $len; $i++) {
$sign = (int)$this->judgeBucket($data[$i], $len, $min, $max);
$hasNum[$sign] = 1;
$mins[$sign] = array_key_exists($sign, $mins) ? min($mins[$sign], $data[$i]) : $data[$i] ;
$maxs[$sign] = array_key_exists($sign, $maxs) ? max($maxs[$sign], $data[$i]) : $data[$i] ;
}
$res = 0;
// 遍历返回最大差值
for ($j=1; $j < $len+1; $j++) {
if ($hasNum[$j]) {
$min = $mins[$j];
// 非空桶最小值与其左边的最近的非空桶的最大值之间的差值
$t = $j;
while ($t > 0) {
if ($hasNum[$t-1]) {
$max = $maxs[$t-1];
break;
}
$t--;
}
$res = max($res, $min - $max);
}
}
returned $res;
}
九.工程中综合排序算法
9.1
- 若你需要排序的是基本数据类型,则选择快速排序。
- 若你需要排序的是引用数据类型,则选择归并排序。(基于稳定性考虑)
因为基本数据类型之间无差异,不需要考虑排序算法稳定性,而归并排序则可以实现算法的稳定性。
9.2
- 当你需要排序的样本数量小于60,直接选择插入排序,虽然插入排序的时间复杂度为O(N²),我们是忽略常数项得出来的O(N²),但在魔数60以内,插入排序的时间复杂度为O(N²)的劣势体现不出来,反而插入排序常数项很低,导致在小样本情况下,插入排序极快
- 如果一开始数组容量很大,但可以分治处理,分治后如果数组容量(L>R - 60)小于60,可以直接选择插排。当大样本下考虑情况1。