经典排序算法的自我理解

顾名思义,所谓排序,就是使一串数据或记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

而排序算法就是如何使得记录按照要求排列的方法。排序算法在社会很多领域中都有涉及,尤其是大量数据的系统处理里面,排序算法可以极大的减小资源占用,找到符合条件的数据。

目前有十大经典排序算法:
1. 冒泡排序
2. 选择排序
3. 快速排序
4. 插入排序
5. 归并排序
6. 希尔排序
7. 基数排序
8. 堆排序
9. 计数排序
10. 桶排序

这里我能力有限,只能理解其中几个排序,其他的排序仍在学习中。

-- 冒泡排序 --

首先理解一下冒泡排序:
冒泡排序是多家企业面试的常规试题,我也被问过很多次。它们的主要思路就是每两两相邻的数据互相比较,大的往上冒泡,小的往下沉底,直到最后没有数据比较。

冒泡排序的步骤:
a. 从第一个数据开始,每相邻的两两数据相比较,如果第一个数字比第二个数字大,就交换这两个,把大的数字调到后面,小的数字放在前面。这样每一个循环走完,排到最后的数据一定是这个循环里最大的。

b. 针对所有的数据重复以上的步骤,除了最后一个(因为最后一个在上一个循环中绝对是最大的),因此下一个循环排序,是上个循环比较数据的数量要减 1。

c. 持续每次对越来越少的数据重复上述步骤,直到没有任何一对数据需要比较。

从这里看出,冒泡排序需要两个for,代码如下所示:

class BubbleSort
{
    function bubble($arr)
    {
        $len = count($arr);
        for ($i = 0; $i < $len; $i++) {
            for ($j = 0; $j < $len - 1 - $i; $j++) {
                if ($arr[$j] > $arr[$j + 1]) {
                    $temp = $arr[$j];
                    $arr[$j] = $arr[$j + 1];
                    $arr[$j + 1] = $temp;
                }
            }
        }
        return $arr;
    }
}

var_dump((new BubbleSort())->bubble([5, 2, 6, 8, 3, 1, 6, 8, 4, 54, 78, 123, 564, 44]));
var_dump((new BubbleSort())->bubble([1, 40, 9, 4, 6, 213, 43, 88346, 852, 31, 8456, 3237, 88346, 92, 35, 2342, 537]));

-- 选择排序 --

除了冒泡排序,还有个选择排序也和它一样是两层for循环,只不过选择排序的循环条件是另一种方式。

选择排序的主要思路是在尚未排序的几个数字中选出最小或最大的数字,并将选出的数字放在起始位置,接下来就要对余下的数字进行重复选择,选出的数字排在已选的数字后面。
直观的理解就是,假如:
A的未排序数字是 5, 2, 6, 8, 3, 1, 6, 8, 4, 54, 78, 123, 564, 44,这里面最小的数字是1,这时候把1筛选出来放入B中,B的数字就是1。
A中的1被筛选出来之后,余下的数字最小的就是2,把2筛选出来放在B的后面,所以B的数字就变成了1,2。以此类推,B的最终数字就是正序排列了。

那么怎么找出来一排数字里最小的呢?

a. 我们对A中的排列数字中,从第一个数字开始,假设第一个数字是最小的,设置它的最小下标 $minIndex,在for循环下,用最小下标的数字和后面的所有数字都比较一下,如果有数字比它的数字小,就交换下标,在找出最小下标结束的for循环后,已经确定好了这一排最小值,和刚开始设置的最小下标的数字兑换(我觉得可以赋值新的数组)。

b. 第一次循环,已经找到最小值并排在第一位了,所以第一位数字不用再比较,直接从第二位开始比较,表明每一次最外循环,都会进位一个。直到循环结束。

代码如下所示:

class SelectSort
{
    function select($arr)
    {
        $len = count($arr);
        for ($i = 0; $i < $len - 1; $i++) {
            $minIndex = $i;
            for ($j = $i + 1; $j < $len; $j++) {
                if ($arr[$j] < $arr[$minIndex]) {
                    $minIndex = $j;
                }
            }
            $temp = $arr[$i];
            $arr[$i] = $arr[$minIndex];
            $arr[$minIndex] = $temp;
        }
        return $arr;
    }
}

-- 插入排序 --

选择排序讲完了,接下来就是插入排序,插入排序的代码实现没有选择和冒泡排序那么简单粗暴。
插入排序的原理,是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

听到这句话是不是还是难以理解,没关系,我进一步理解下:

a. 假如有A的无序列数字 5, 2, 3, 8, 1, 4, 6,我们需要在这里面的数字做一个分割线,这个分割线的意思就是为了构建一个有序序列和无序序列。但是我们无法判断一个数字到底是有序的,所以这个时候我们需要假设第一个数字5就是一个有序序列,第一个数字后面的所有数字都是无序序列。

b. 这个时候,已经初始化的有序序列是 B: 5, 无序序列是 C:2, 3, 8, 1, 4, 6。在无序序列里的第一个数字2开始对有序序列开始从尾部向头部进行依次比较,如果无序序列的第一个数字2比5小,就插入到5的前面。

c. 这时候,构成的有序序列就是B:2,5, 无序序列就是C:3, 8, 1, 4, 6。这里从无序序列的第一个数字3开始,对有序序列从尾部向头部进行依次比较,比较到3比5小,所以3插到5的前面,继续往前比较,3比2大,3就不用往前插入了,2后面已经没有数字了,所以这个时候,3插入到了2和5的中间,此时,B:2,3,5。 C:8,1,4,6。

d. 以此类推,在最后无序序列没有数字的时候,有序序列就已经完整形成了。

从上诉表述,代码的编写应该有了雏形吧。首先分割序列,第一个作为有序,后面作为无序,for的循环里$i=1 开始算,到 < 总长度$len截至。
设置$current为该下标的数字,这个数字即将和前面的数做比较,进行插入,并设置前面的数字的下标为$preIndex = $i-1,然后在对比$preIndex不低于0且$preIndex所属数字大于比较数$current时,循环逐个对比,每一次对比都会对$preIndex减1,$preIndex下标不会超过0以下。由于$preIndex是前面的有序序列里的数字下标,一旦有数字低于这个$current,就要停止循环,将当前的$current插入到$preIndex的后面。

代码如下所示:

class InsertionSort
{
    function insertion($arr)
    {
        $len = count($arr);
        for ($i = 1; $i < $len; $i++) {
            $preIndex = $i - 1;
            $current = $arr[$i];
            while ($preIndex >= 0 && $arr[$preIndex] > $current) {
                $arr[$preIndex + 1] = $arr[$preIndex];
                $preIndex--;
            }
            $arr[$preIndex + 1] = $current;
        }
        return $arr;
    }
}

var_dump((new InsertionSort())->insertion([5, 2, 3, 8, 1, 4, 6]));

-- 快速排序 --

快速排序懂了递归之后还是蛮简单的,他的原理也易懂,快速排序用到了分治法策略,把一个串行分成两个子串。

具体的意思就是,它是在一些无序数字里面选出一个数字作为基准,这个基准会对除了它之外的其他数字进行比较,比基准大的数字就放在右边,比基准小的数字就放在左边,在一整串数字排完之后会发现,这个基准已经排在了这串数字的中间。
这个时候已经分为了两个字串,左子串和右子串。然后用递归的方法,将每个字串再一次按照新的基准再分区。直到分区的数字只有一个或没有的时候,结束递归,得到的最终的数字就是每个分区的并集
。这时候分区的每个数都已经排好了。

代码如下所示:

class QuickSort
{
    function quick($arr)
    {
        if (count($arr) <= 1) {
            return $arr;
        }
        //选择一个基数
        $main = $arr[0];
        //统计长度,要和基数做比较
        $len = count($arr);
        $left = $right = [];
        //对除了基数之外的数做比较
        for ($i = 1; $i < $len; $i++) {
            if ($arr[$i] > $main) {
                //如果大于基数,置右边,做新分区
                $right[] = $arr[$i];
            } else {
                //如果小于基数,置左边,做新分区
                $left[] = $arr[$i];
            }
        }
        //对左边新区按quicksort一样递归排序,直到排序结束,左边的数一定比基数小,且有顺序
        $left = $this->quick($left);
        //基数放左边的最后下标,表示中间数
        $left[] = $main;
        //对右边新区按quicksort递归排序,结束后右边的数一定比基数大,且有顺序
        $right = $this->quick($right);

        return array_merge($left, $right);
    }
}

var_dump((new QuickSort())->quick([5, 2, 6, 8, 3, 1, 6, 8, 4, 54, 78, 123, 564, 44]));
var_dump((new QuickSort())->quick([1, 40, 9, 4, 6, 213, 43, 88346, 852, 31, 8456, 3237, 88346, 92, 35, 2342, 537]));

还有好多其他的排序,暂时先不说了,只是觉得比较麻烦,等后面再来写点...PHP里有很多自带的排序函数,sort等。还有一些已经封装好了的抽象类,比如heapMap()。有时候如果用不了经典十大排序算法的时候,用这些内置函数或类库,也是不错的。
但是呢!尽管排序算法看着比较麻烦,但实际上,用起来的时候那可是真香反应~

posted @ 2021-09-26 18:01  CloudDre  阅读(138)  评论(0编辑  收藏  举报