冒泡排序
冒泡排序介绍
它是一种较简单的排序算法。它会遍历若干次要排序的数列,每次遍历时,它都会从前往后依次的比较相邻两个数的大小;如果前者比后者大,则交换它们的位置。这样,一次遍历之后,最大的元素就在数列的末尾! 采用相同的方法再次遍历时,第二大的元素就被排列在最大元素之前。重复此操作,直到整个数列都有序为止!
冒泡排序实现
下面以数列{20,40,30,10,60,50}为例,演示它的冒泡排序过程(如下图)。
我们先分析第1趟排序
- 当i=5,j=0时,a[0]<a[1]。此时,不做任何处理!
- 当i=5,j=1时,a[1]>a[2]。此时,交换a[1]和a[2]的值;交换之后,a[1]=30,a[2]=40。
- 当i=5,j=2时,a[2]>a[3]。此时,交换a[2]和a[3]的值;交换之后,a[2]=10,a[3]=40。
- 当i=5,j=3时,a[3]<a[4]。此时,不做任何处理!
- 当i=5,j=4时,a[4]>a[5]。此时,交换a[4]和a[5]的值;交换之后,a[4]=50,a[3]=60。
于是,第1趟排序完之后,数列{20,40,30,10,60,50}变成了{20,30,10,40,50,60}。此时,数列末尾的值最大。
根据这种方法:
- 第2趟排序完之后,数列中a[5…6]是有序的。
- 第3趟排序完之后,数列中a[4…6]是有序的。
- 第4趟排序完之后,数列中a[3…6]是有序的。
- 第5趟排序完之后,数列中a[1…6]是有序的。整个数列也就是有序的了。
冒泡排序时间复杂度
冒泡排序的时间复杂度是**O(N^2)**。 假设被排序的数列中有N个数。遍历一趟的时间复杂度是O(N),需要遍历多少次呢? N-1次!因此,冒泡排序的时间复杂度是O(N^2)。
冒泡排序稳定性
冒泡排序是稳定的算法,它满足稳定算法的定义。
算法稳定性 – 假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!
代码实现
1 | public class BubbleSort { |
快速排序
快速排序介绍
它的基本思想是: 选择一个基准数,通过一趟排序将要排序的数据分割成独立的两部分:其中一部分的所有数据都比另外一部分的所有数据都要小。然后,再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
快速排序实现
- 从数列中挑出一个基准值。
- 将所有比基准值小的摆放在基准前面,所有比基准值大的摆在基准的后面(相同的数可以到任一边);在这个分区退出之后,该基准就处于数列的中间位置。
- 递归地把”基准值前面的子数列”和”基准值后面的子数列”进行排序。
下面以数列a={30,40,60,10,20,50}为例,演示它的快速排序过程(如下图)。
上图只是给出了第1趟快速排序的流程。在第1趟中,设置x=a[i],即x=30。
- 从”右 –> 左”查找小于x的数: 找到满足条件的数a[j]=20,此时j=4;然后将a[j]赋值a[i],此时i=0;接着从左往右遍历。
- 从”左 –> 右”查找大于x的数: 找到满足条件的数a[i]=40,此时i=1;然后将a[i]赋值a[j],此时j=4;接着从右往左遍历。
- 从”右 –> 左”查找小于x的数: 找到满足条件的数a[j]=10,此时j=3;然后将a[j]赋值a[i],此时i=1;接着从左往右遍历。
- 从”左 –> 右”查找大于x的数: 找到满足条件的数a[i]=60,此时i=2;然后将a[i]赋值a[j],此时j=3;接着从右往左遍历。
- 从”右 –> 左”查找小于x的数: 没有找到满足条件的数。当i>=j时,停止查找;然后将x赋值给a[i]。此趟遍历结束!
按照同样的方法,对子数列进行递归遍历。最后得到有序数组!
快速排序稳定性
快速排序是不稳定的算法,它不满足稳定算法的定义。
算法稳定性
– 假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!
快速排序时间复杂度
快速排序的时间复杂度在最坏情况下是O(N^2),平均的时间复杂度是O(N*lgN)。
这句话很好理解: 假设被排序的数列中有N个数。遍历一次的时间复杂度是O(N),需要遍历多少次呢? 至少lg(N+1)次,最多N次。
- 为什么最少是 lg(N+1) 次? 快速排序是采用的分治法进行遍历的,我们将它看作一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据完全二叉树的定义,它的深度至少是 **lg(N+1)**。因此,快速排序的遍历次数最少是 lg(N+1) 次。
- 为什么最多是N次? 这个应该非常简单,还是将快速排序看作一棵二叉树,它的深度最大是N。因此,快读排序的遍历次数最多是N次。
代码实现
1 | /** |
插入排序
插入排序介绍
直接插入排序(Straight Insertion Sort)的基本思想是: 把 n 个待排序的元素看成为一个有序表和一个无序表。开始时有序表中只包含1个元素,无序表中包含有 n-1 个元素,排序过程中每次从无序表中取出第一个元素,将它插入到有序表中的适当位置,使之成为新的有序表,重复 n-1 次可完成排序过程。
插入排序实现
图中将数列分为有序区和无序区。我们需要做的工作只有两个: (1)取出无序区中的第1个数,并找出它在有序区对应的位置。(2)将无序区的数据插入到有序区;若有必要的话,则对有序区中的相关数据进行移位。
插入排序时间复杂度
直接插入排序的时间复杂度是**O(N^2)**。
假设被排序的数列中有N个数。遍历一趟的时间复杂度是O(N),需要遍历多少次呢? N-1!因此,直接插入排序的时间复杂度是O(N^2)。
插入排序稳定性
直接插入排序是稳定的算法,它满足稳定算法的定义。
算法稳定性
– 假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!
代码实现
1 | /** |
希尔排序
希尔排序介绍
希尔排序实质上是一种分组插入方法,插入排序经过改进之后的一个更高效的版本。它的基本思想是: 对于 n 个待排序的数列,取一个小于 n 的整数 gap (gap被称为步长)将待排序元素分成若干个组子序列,所有距离为 gap 的倍数的记录放在同一个组中;然后,对各组内的元素进行直接插入排序。 这一趟排序完成之后,每一个组的元素都是有序的。然后减小 gap 的值,并重复执行上述的分组和排序。重复这样的操作,当 gap=1 时,整个数列就是有序的。
希尔排序实现
下面以数列{80,30,60,40,20,10,50,70}为例,演示它的希尔排序过程。
第1趟: (gap=4)
当 gap=4 时,意味着将数列分为4个组: {80,20},{30,10},{60,50},{40,70}。 对应数列: {80,30,60,40,20,10,50,70} 对这4个组分别进行排序,排序结果: {20,80},{10,30},{50,60},{40,70}。 对应数列: {20,10,50,40,80,30,60,70}
第2趟: (gap=2)
当 gap=2 时,意味着将数列分为2个组: {20,50,80,60}, {10,40,30,70}。 对应数列: {20,10,50,40,80,30,60,70} 注意: {20,50,80,60}实际上有两个有序的数列{20,80}和{50,60}组成。 {10,40,30,70}实际上有两个有序的数列{10,30}和{40,70}组成。 对这2个组分别进行排序,排序结果: {20,50,60,80}, {10,30,40,70}。 对应数列: {20,10,50,30,60,40,80,70}
第3趟: (gap=1)
当 gap=1 时,意味着将数列分为1个组: {20,10,50,30,60,40,80,70} 注意: {20,10,50,30,60,40,80,70}实际上有两个有序的数列{20,50,60,80}和{10,30,40,70}组成。 对这1个组分别进行排序,排序结果: {10,20,30,40,50,60,70,80}
希尔排序时间复杂度
希尔排序的时间复杂度与增量(即,步长gap)的选取有关。例如,当增量为1时,希尔排序退化成了直接插入排序,此时的时间复杂度为O(N^2),而 Hibbard 增量的希尔排序的时间复杂度为O(N3/2)。
希尔排序稳定性
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小, 插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比O(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
算法稳定性
– 假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!
代码实现
1 | public class ShellSort { |
选择排序
选择排序介绍
它的基本思想是: 首先在未排序的数列中找到最小(or最大)元素,然后将其存放到数列的起始位置;接着,再从剩余未排序的元素中继续寻找最小(or最大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
选择排序实现
- 将数组的第一个数字设置为标志位最小数并记录最小数下标。
- 向后遍历,发现更小数后将该数与标志位互换位置并更新最小数与最小数下标。
- 循环完成排序
下面以数列{1,6,2,3,8,9,5,4,7}为例,演示它的选择排序过程(如下图)。
选择排序时间复杂度
选择排序的时间复杂度是O(N^2)。
假设被排序的数列中有N个数。遍历一趟的时间复杂度是O(N),需要遍历多少次呢? N-1!因此,选择排序的时间复杂度是O(N^2)。
选择排序稳定性
选择排序的稳定性是有一些争议的,不过一般提到排序算法,往往默认是数组实现,所以通常认为选择排序是不稳定的。
- 回顾:什么是排序算法的稳定性?
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
- 数组实现和链表实现的差异
用数组实现的选择排序是不稳定的,用链表实现的选择排序是稳定的。
不过,一般提到排序算法时,大家往往会默认是数组实现,所以选择排序是不稳定的。
- 此外,排序算法的稳定性也是可以改变的,只是需要额外的时间和空间
有很多办法可以将任意排序算法变成稳定的,但是,往往需要额外的时间或者空间;而我们默认情况谈算法的稳定性是不考虑这种实现的。
代码实现
1 | public class SelectSort { |
堆排序
堆排序介绍
堆分为”最大堆”和”最小堆”。最大堆通常被用来进行”升序”排序,而最小堆通常被用来进行”降序”排序。 鉴于最大堆和最小堆是对称关系,理解其中一种即可。本文将对最大堆实现的升序排序进行详细说明。
最大堆进行升序排序的基本思想:
- 始化堆: 将数列a[1…n]构造成最大堆,此时,整个序列的最大值就是堆顶的根节点,即a[1]。
- 交换数据: 将其与末尾元素进行交换,此时末尾就为最大值,即将a[1]和a[n]交换,使a[n]是a[1…n]中的最大值。
- 然后将a[1…n-1]重新调整为最大堆。 接着,将a[1]和a[n-1]交换,使a[n-1]是a[1…n-1]中的最大值;然后将a[1…n-2]重新调整为最大值。 依次类推,直到整个数列都是有序的。
可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了。
下面,通过图文来解析堆排序的实现过程。注意实现中用到了”数组实现的二叉堆的性质”。 在第一个元素的索引为 0 的情形中:
- 性质一: 索引为 i 的左孩子的索引是 (2*i+1);
- 性质二: 索引为 i 的右孩子的索引是 (2*i+2);
- 性质三: 索引为 i 的父结点的索引是 floor((i-1)/2);
堆排序实现
下面演示heap_sort_asc(a, n)对a={20,30,90,40,70,110,60,10,100,50,80}, n=11进行堆排序过程。下面是数组 a 对应的初始化结构:
初始化堆
在堆排序算法中,首先要将待排序的数组转化成二叉堆。 下面演示将数组 {20,30,90,40,70,110,60,10,100,50,80} 转换为最大堆 {110,100,90,40,80,20,60,10,30,50,70} 的步骤。
- 从最后一个非叶子结点开始(叶子结点自然不用调整,第一个非叶子结点为 arr.length/2-1=11/2-1=4 ,也就是下面的4结点) , 从右至左,从下至上进行调整。
上面是maxheap_down(a, 4, 9)调整过程。maxheap_down(a, 4, 9)的作用是将a[4…9]进行下调;a[4]的左孩子是a[9],右孩子是a[10]。调整时,选择左右孩子中较大的一个(即a[10])和a[4]交换。
- 找到第二个非叶节点 i=3
上面是maxheap_down(a, 3, 9)调整过程。maxheap_down(a, 3, 9)的作用是将a[3…9]进行下调;a[3]的左孩子是a[7],右孩子是a[8]。调整时,选择左右孩子中较大的一个(即a[8])和a[4]交换。
- 找到第三个非叶节点 i=2
上面是maxheap_down(a, 2, 9)调整过程。maxheap_down(a, 2, 9)的作用是将a[2…9]进行下调;a[2]的左孩子是a[5],右孩子是a[6]。调整时,选择左右孩子中较大的一个(即a[5])和a[2]交换。
- 找到第四个非叶节点 i=1
上面是maxheap_down(a, 1, 9)调整过程。maxheap_down(a, 1, 9)的作用是将a[1…9]进行下调;a[1]的左孩子是a[3],右孩子是a[4]。调整时,选择左右孩子中较大的一个(即a[3])和a[1]交换。交换之后,a[3]为30,它比它的右孩子a[8]要大,接着,再将它们交换。
- 找到第五个非叶节点 i=0
上面是maxheap_down(a, 0, 9)调整过程。maxheap_down(a, 0, 9)的作用是将a[0…9]进行下调;a[0]的左孩子是a[1],右孩子是a[2]。调整时,选择左右孩子中较大的一个(即a[2])和a[0]交换。交换之后,a[2]为20,它比它的左右孩子要大,选择较大的孩子(即左孩子)和a[2]交换。
调整完毕,就得到了最大堆。此时,数组{20,30,90,40,70,110,60,10,100,50,80}也就变成了{110,100,90,40,80,20,60,10,30,50,70}。
交换数据
在将数组转换成最大堆之后,接着要进行交换数据,从而使数组成为一个真正的有序数组。 交换数据部分相对比较简单,下面仅仅给出将最大值放在数组末尾的示意图。
上面是当 n=10 时,交换数据的示意图。 当n=10时,首先交换a[0]和a[10],使得a[10]是a[0…10]之间的最大值;然后,调整a[0…9]使它称为最大堆。交换之后: a[10]是有序的! 当n=9时, 首先交换a[0]和a[9],使得a[9]是a[0…9]之间的最大值;然后,调整a[0…8]使它称为最大堆。交换之后: a[9…10]是有序的! … 依此类推,直到a[0…10]是有序的。
堆排序时间复杂度
堆排序的时间复杂度是 *O(NlgN)**。
假设被排序的数列中有N个数。遍历一趟的时间复杂度是O(N),需要遍历多少次呢? 堆排序是采用的二叉堆进行排序的,二叉堆就是一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据完全二叉树的定义,它的深度至少是lg(N+1)。最多是多少呢? 由于二叉堆是完全二叉树,因此,它的深度最多也不会超过lg(2N)。因此,遍历一趟的时间复杂度是O(N),而遍历次数介于lg(N+1)和lg(2N)之间;因此得出它的时间复杂度是O(N*lgN)。
堆排序稳定性
堆排序是不稳定的算法,它不满足稳定算法的定义。它在交换数据的时候,是比较父结点和子节点之间的数据,所以,即便是存在两个数值相等的兄弟节点,它们的相对顺序在排序也可能发生变化。
算法稳定性
– 假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!
代码实现
1 | public class HeapSort { |
归并排序
归并排序介绍
归并排序(Merge sort)是建立在归并操作上的一种有效、稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。根据具体的实现,归并排序包括”从上往下”和”从下往上”2种方式。
从下往上的归并排序
将待排序的数列分成若干个长度为 1 的子数列,然后将这些数列两两合并;得到若干个长度为 2 的有序数列,再将这些数列两两合并;得到若干个长度为 4 的有序数列,再将它们两两合并;直接合并成一个数列为止。这样就得到了我们想要的排序结果。
从上往下的归并排序
它与”从下往上”在排序上是反方向的。它基本包括3步:
分解
– 将当前区间一分为二,即求分裂点 mid = (low + high)/2;求解
– 递归地对两个子区间a[low…mid] 和 a[mid+1…high]进行归并排序。递归的终结条件是子区间长度为1。合并
– 将已排序的两个子区间a[low…mid]和 a[mid+1…high]归并为一个有序的区间a[low…high]。
归并排序实现
从上往下的归并排序
从上往下的归并排序采用了递归的方式实现。它的原理非常简单,如下图:
通过”从上往下的归并排序”来对数组{80,30,60,40,20,10,50,70}进行排序时:
- 将数组{80,30,60,40,20,10,50,70}看作由两个有序的子数组{80,30,60,40}和{20,10,50,70}组成。对两个有序子树组进行排序即可。
- 将子数组{80,30,60,40}看作由两个有序的子数组{80,30}和{60,40}组成。将子数组{20,10,50,70}看作由两个有序的子数组{20,10}和{50,70}组成。
- 将子数组{80,30}看作由两个有序的子数组{80}和{30}组成。将子数组{60,40}看作由两个有序的子数组{60}和{40}组成。将子数组{20,10}看作由两个有序的子数组{20}和{10}组成。将子数组{50,70}看作由两个有序的子数组{50}和{70}组成。
从下往上的归并排序
从下往上的归并排序的思想正好与”从下往上的归并排序”相反。如下图:
通过”从下往上的归并排序”来对数组{80,30,60,40,20,10,50,70}进行排序时:
- 将数组{80,30,60,40,20,10,50,70}看作由8个有序的子数组{80},{30},{60},{40},{20},{10},{50}和{70}组成。
- 将这8个有序的子数列两两合并。得到4个有序的子树列{30,80},{40,60},{10,20}和{50,70}。
- 将这4个有序的子数列两两合并。得到2个有序的子树列{30,40,60,80}和{10,20,50,70}。
- 将这2个有序的子数列两两合并。得到1个有序的子树列{10,20,30,40,50,60,70,80}。
归并排序时间复杂度
归并排序的时间复杂度是*O(NlgN)**。
假设被排序的数列中有N个数。遍历一趟的时间复杂度是O(N),需要遍历多少次呢? 归并排序的形式就是一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据完全二叉树的可以得出它的时间复杂度是O(N*lgN)。
归并排序稳定性
归并排序是稳定的算法,它满足稳定算法的定义。
算法稳定性
– 假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!
代码实现
1 | public class MergeSort { |
桶排序
桶排序介绍
假设待排序的数组 a 中共有 N 个整数,并且已知数组 a 中数据的范围[0, MAX)。在桶排序时,创建容量为MAX的桶数组 r,并将桶数组元素都初始化为0;将容量为 MAX 的桶数组中的每一个单元都看作一个”桶”。
在排序时,逐个遍历数组a,将数组 a 的值,作为”桶数组r”的下标。当 a 中数据被读取时,就将桶的值加1。例如,读取到数组a[3]=5,则将r[5]的值+1。
桶排序实现
假设a={8,2,3,4,3,6,6,3,9}, max=10。此时,将数组a的所有数据都放到需要为0-9的桶中。如下图:
在将数据放到桶中之后,再通过一定的算法,将桶中的数据提出来并转换成有序数组。就得到我们想要的结果了。
桶排序复杂度
- 平均时间复杂度: O(n + k),n为待排元素数,k为桶数。
- 最佳时间复杂度: O(n + k)
- 最差时间复杂度: O(n ^ 2)
- 空间复杂度: O(n * k)
桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
桶排序稳定性
稳定性: 稳定
代码实现
1 | public class BucketSort { |
基数排序
基数排序介绍
基数排序是桶排序的扩展,它的基本思想是: 将整数按位数切割成不同的数字,然后按每个位数分别比较。 具体做法是: 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
基数排序实现
通过基数排序对数组{389, 12, 3, 36, 66, 89, 13, 30}
- 准备足够的存储空间,bucket存放数字,elementCount计数。
- 第一次循环,把每个数先除1,再除10求余数,余数是几就把该数据放到下标是几的数组里面,对应的elementCount加一。
- 依次把数据从bucket里面拿出来,然后依次填回arr数组里面里面。elementCount置零。
arr数组是按个位数有序的。
- 第二次循环,把每个数先除10,再除10求余数,余数是几就把该数据放到下标是几的数组里面,对应的elementCount加一。
基数排序复杂度
O(d(r+n)),r 代表关键字的基数,d 代表长度,n 代表关键字的个数
基数排序稳定性
稳定
代码实现
- 确定最大数的位数
1 | int max=arr[0]; |
1 | public class RadixSort { |