【算法】算法初步:聊一聊常见排序的算法

在一个工程中一旦建立了某一个数据库后,就可能需要对数据库中数据进行不同方式的排序,比如对姓名进行字母排序,年龄进行大小排序等等。排序在编程中非常的重要,但又可能十分的复杂。这篇博文主要介绍一下几种简单而且常见的排序算法。

如何排序

让我们假设一个场景。体育课上,同学们排成一列。现在要按照身高从高到底排队(最矮的在最左边),应该怎么排队呢?
如果在现实生活中这是很简单的事情,我们可以一眼看到哪个最高,从而毫不费力的把顺序排好。而对计算机而言这是不行的,计算机程序不能像人一样通览所有的数据,它只能通过比较一步一步的解决问题。
一般最简单的排序都包括一下步骤:

  • 比较两个数据
  • 交换两个数据,或者复制其中一个数据
    但是每种算法的细节有所不同。

冒泡排序

冒泡排序是一种十分简单,但是运算起来又非常慢的一种算法。但是对于一个刚开始研究算法的程序员来说又是一种非常好的算法。
假设上述场景中有N个同学,我们给每个同学的位置进行按照从左到右进行编号。从0到N-1。
冒泡排序执行过程如下:从队列最左边开始即0位置开始,比较0号位置和1号位置的队员。如果0号位置高一些,就把0号位置的同学和1号位置的同学互换;否则,什么也不做,比较1号位置和2号位置的同学。这个排序的过程如下所示:【算法】算法初步:聊一聊常见排序的算法_第1张图片

以下是这个排序的准则:

  • 比较两个数据
  • 如果左边的数据较大(小),那么交换两个数据的位置。
  • 向右移动一个位置,比较接下来两个数据
    按照以上的准则进行一轮排序,我们会发现最高的同学现在已经在最右边了。但是前N-1的同学的顺序是否正确我们很难保证,所以需要再进行排序,直到同学们按由高到低的顺序站成一列。假如一组数据中有N数据,那么用冒泡排序的进行排列的话要进行N-1次排序。关于冒泡排序的Java代码详见冒泡排序Java代码

冒泡排序效率分析

一般来说,数组中有N个数据,则第一趟排序中有N-1次比较,第二趟中有N-2次比较,这样算下来一个有N个数据的数组,用冒牌算法进行排序的话约做了N²/2(确切来说是N*(N-1)/2)次比较。时间复杂度是O(n²)级别的。所以效率是很慢的。假如我们的数据中前i项是无序的,后N-i项是有序的,那么使用冒泡排序的后N-1-i次排序都是毫无意义的。

选择排序

选择排序是改进了冒泡排序的一种算法。将时间复杂度从O(n²)降低到O(n)。
让我们回归到我们的场景中。在选择排序中,不再只比较两个相邻的同学,而是把所有同学的身高都扫描一遍。选出最矮的和站在队列最左边的同学交换位置。现在0号位置的同学是有序的了。然后我们再扫描队列的时候就从1号位置开始,还是搜寻最矮的那个,然后和1号位置进行交换。然后重复这个过程直到所有同学都排定。这个排序过程如下所示:
【算法】算法初步:聊一聊常见排序的算法_第2张图片

更详细的描述

排序从队员的最左边开始,记录同学身高。并在0号同学面前放一面旗子,然后和右边的同学的依次进行身高比较,如果遇到比这个同学矮的,则把旗子放在该同学位置前,并把两个人交换顺序。现在0好同学依然是当前最矮的,然后0好同学继续和以后的同学进行比较,遇到更矮的就重复移动旗子和交换位置的动作,一轮比较下来,0号位置的同学就是最矮的那位。具体代码详见选择排序的Java代码实现

选择排序效率分析

选择排序和冒泡排序执行了相同的比较次数:N*(N-1)/2。所以时间复杂度和冒泡排序一样是O(n²)。但是选择排序无疑更快,因为选择排序的交换要少的多。

插入排序

插入排序在一般情况下要比冒泡排序快一倍,比选择排序也要快一些,虽然它比选择排序还有冒泡排序都要麻烦一些。插入排序通常被用在较为复杂排序算法的最后阶段,如快速排序。
依然是我们上面的场景,在开始前我们假设这个队列已经局部有序。此时,在队伍中有一个同学作为标记(放一面旗子在该同学面前)。在这个同学左边的队员已经局部有序了。这就意味他左边的同学已经由高到底的排序好了,但不一定是最终的位置。下面要做的在有序的部分中的适当位置插入被标记的同学,而要做到这一点我们可以让标记的同学先出列,然后就腾出了空间,可以把有许的同学进行右移,最终把被标记的同学插到适当的位置。现在,局部有序的部分里多了一个队员,而为排序的部分里少了一个队员。则标记(旗子)向右移动一个位置。重复这个过程知道所有为排序额队员都插入到局部有许队列中的适当位置。这个排序的过程如下:
【算法】算法初步:聊一聊常见排序的算法_第3张图片
具体代码详见插入排序Java实现

插入排序效率分析

对于已经局部有序的数据来说,插入排序要好的多。当数据有序的时候内层循环的条件总是不成立的,所以它编程了外层循环中的一条简单的豫剧,执行N-1次,这种时间复杂度为O(n)。所以插入排序的时间复杂度总是介于O(n)与O(n²)之间。相比冒泡排序和选择排序还是一个简单有效的方法。

归并排序

关于递归

再讲归并排序之前,让我们来聊一聊递归。递归是一种方法调用自己的编程技术。再通俗一点的解释就是函数的自调用。递归在解决一些问题的时候十分的方便,使用递归会大大的缩减代码量,但是也会导致一些问题。比如栈的溢出。在学习python的过程中,有看到过优化递归的方法,通过尾递归优化。

解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。可是非常遗憾,大多数编程语言没有针对尾递归做优化,即使把函数改成尾递归方式,也会导致栈溢出。

归并排序

归并排序要比上面提到的三种排序方法有效的多,至少速度上是这样的。归并排序的实现也相当容易,理解起来也不困难。
在我们的场景中,把同学分为若干组,为了方便起见我们就分为两组,两组的人数不一定要相同,可以任意分配。先把两个组分别进行排序,使两个组内部有序。然后就可以合并这两个组了。如何合并呢,我们可以先比较两个组中最左边的同学,比较矮的同学单独站一列。然后再比较两组中最左边的同学,较矮的同学站在之前单独站一列同学的右边,直到一个组的同学全部出列,另一个组的同学依次站在右边就完成了排序。排序的过程如下所示:【算法】算法初步:聊一聊常见排序的算法_第4张图片
具体代码详见归并排序Java实现

归并排序效率分析

归并排序的运行时间是O(NlogN)。很明显要比之前的几种排序的方法更快,并且归并排序的比较次数是要比数据复制的次数少一些的。但是归并排序有一个缺点,就是它需要在存储器中有另一个大小等于被排序的数据项数目的数组。如初始数组很大,沾满了整个容器,那么归并排序是不能工作的。

快速排序

聊了这么多,终于到快速排序了。毫无疑问,快速排序是当下最流行的排序算法了。因为在大多情况下快速排序都是最快的。快速排序算法的本质是通过把一个数组分成两个子数组,然后递归地调用自身为每一个子数组进行快速排序来实现。下面先给出代码,然后再分析快速排序。代码详见快速排序Java实现
参照代码可以看出快速排序有三个基本步骤:

  • 把数组或者子数组划分成两组(左右两组)。
  • 递归对左边一组组进行排序
  • 递归对右边一组进行排序
    经过一次划分后,所有在左边的数据项都小于在右边的数组的数据。自要对两个数字分别排序,证个数组自然就有序了。对与子数组的排序自然用递归的方法就可以了。整个排序的过程如下:
    【算法】算法初步:聊一聊常见排序的算法_第5张图片

总结

这些算法是大一就学习了的,当时还在写C++,冒牌排序,插入排序或许都很容易理解。但是归并排序和快速排序理解起来是有些难度的。由于现在大三了,面临找工作的压力,也就复习了一下这些算法,还是废了不少精力的。
以上。

你可能感兴趣的:(算法)