基础班:第二节

版权声明:本文为博主DarkAngel1228原创文章,未经博主允许不得转载。 https://blog.csdn.net/DarkAngel1228/article/details/82958889

一.荷兰国旗问题

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)

思想:
  1. 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
  2. 将堆顶元素与末尾元素进行交换,使末尾元素最大。
    然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。
    如此反复进行交换、重建、交换。
代码:
	/*
	 * 小根堆(倒序)
	 */
    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 桶排序

思想:

  1. 假设待排序的一组数统一的分布在一个范围中,并将这一范围划分成几个子范围,
    也就是桶。
  2. 将待排序的一组数,分档规入这些子桶,并将桶中的数据进行排序。
  3. 将各个桶中的数据有序的合并起来。
代码:
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 ,

  1. 分配。
    统计每个元素出现的频率,得到 counter=[2, 1, 1, 0, 1] ,
    例如 counter[0] 表示值 0+minValue=1 出现了2次。
  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。

猜你喜欢

转载自blog.csdn.net/DarkAngel1228/article/details/82958889