排序算法
对于排序过程中待排序的记录是否全部放置在内存中,排序分为内排序和外排序。
- 内排序:是指在整个排序过程中,待排序的所有记录都放在内存中,主要有插入排序、交换排序、选择排序和归并排序
- 外排序:是指待排序记录没有同时放置在内存中,整个排序过程需要在内外存之间多次交换数据才能进行
按照策略分类主要的排序算法有:插入排序、选择排序、交换排序、归并排序、分配排序
插入排序
插入排序的基本思想:每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的记录集中,直到所有排序记录全部插入成功。
与选择排序一样的是,当前索引左边的所有元素都是有序的,但是最终位置还不确定,后续可能还会移动,当索引达到数组最右端时,数组排序完成
直接插入排序
当插入第i的元素时,前面R1,R2…Rn-1均已经排好序,因此,将第i个记录依次与前面的进行比较,找到合适的位置插入
- 将数组分为未排序部分和已排序部分,首先对数组的前两个数据进行从小到大的排序
- 接着对第三个数据与排好序的两个数据进行比较,将第三个数据插入到合适的位置
- 然后将第4个数据插入到排好序的前三个数据中
- 重复上述操作
假设排序数据存放在数组A[1…n]中,则A[1]则可以看做是一个有序序列,让i从2开始,依次将A[i]插入到有序序列A[1…i-1]中,A[n]插入完毕结束
过程举例
【】中的数据为有序的
待排序的数据: | 【 23】 56 2 9 17 |
---|---|
i =2 | 【 23 56】 2 9 17 |
i=3 | 【 2 23 56】 9 17 |
i=4 | 【 2 9 23 56】17 |
i=5 | 【 2 9 17 23 56】 |
代码实现
1 | int[] arrays = {67,67,14,52,29,9,90,54,87,71}; |
平均时间复杂度O($n^2$),最坏情况下为O($n^2$),最好情况下是O(n),稳定的算法
插入排序对于部分有序的数组十分高效
希尔排序
希尔排序也是使用插入排序的思想,基于插入排序的快速的排序算法,由于大规模乱序数组使用插入排序很慢,只会交换相邻元素,一点一点地从数组的一端移动到另一端。
希尔排序为了加快速度简单地改进了插入排序,取一个步长,交换该步长距离的元素放到同一组中,组内进行直接插入排序
- 将有n个元素的数组分为n/2个数字序列,第一个数据和第n/2+1个数据为一对…
- 一次循环使每一个序列对排好顺序
- 然后再变成n/4个序列,再次排序
任取一个小于n的正数S1作为增量,将所有的元素分为S1个组,先在各组进行直接插入排序,然后取第二个增量重复上述的分组和排序,直到所取的增量等于1,即所有记录放在同一个分组中为止
由于是每个组进行直接插入排序,所以是在直接插入排序上进行步长的设置
代码实现
步长的设置需要考量,慢慢尝试
1 | int[] arrays = {67,67,14,52,29,9,90,54,87,71}; |
时间复杂度为O($n^2$)
希尔排序比插入排序快很多,并且数组越大,优势越大,是不稳定的排序算法
交换排序
交换排序的基本思想是:两两比较待排序的数据,发现两个数据的次序相反则进行交换,直到没有反序为止
冒泡排序
冒泡排序是一种交换排序,基本思想是:两两比较相邻数据,如果反序则交换,直到没有反序为止
- 对数组中各数据,依次比较相邻的两个元素大小
- 如果前面的数据大于后面的数据,就交换这两个数据,每一轮都能把最大的数据排好
- 进行多轮就可以把数据按照从小到大的顺序排好
最多进行n-1趟排序,每趟排序自底向上扫描,一旦发现相邻的元素不符合规则,则交换,第一趟排序后,最小值在A[1],第二次排序后,最小值在A[2],依次类推,第n-1趟排序之后,所有元素按照顺序排列
代码实现
1 | int[] arr = {67,67,14,52,29,9,90,54,87,71}; |
平均时间复杂度为O($n^2$),稳定
快速排序
快排是冒泡排序的升阶版,从两端向中间进行排序,将一个数组分为两个子数组,将两部分独立地排序
采用的是分治的思想
- 首先设定一个分界值,通过分界值将数组分为左右两部分,与分界值进行交换
- 将大于分界值的数据集中到数组右边,小于分界值的数据集中到数组左边,此时左边部分中各元素小于等于分界值,而右边部分中各元素大于等于分界值
- 然后左边和右边数据可以独立排序,分别取一个分界值来进行排序
- 重复上述过程
从集合中任取一个元素作为比较的基准X,将数据划分为左右两部分,A[1…i-1] <=X<=A[i+1…n],然后在分别对它们进行划分,直到全部有序为止
代码实例
1 | public class QuickSort { |
平均时间复杂度为O(nlogn),最坏情况下为O($n^2$),最好情况下为O(nlogn),不稳定的算法
选择排序
选择排序的基本思想是:每一趟从待排序的数据中选出最小元素,顺序放在已排好序的数据最后,直到全部数据排序完毕。随着排序的进行,每次需要检查的元素数在逐渐减少,最后一次需要检查的元素只有一个
直接选择排序
- 从原始数组中选择最小的1个数据,将其和位于第1个位置的数据交换
- 接着从剩下的n-1个数据中选择次小的1个元素,将其与第2个位置的数据交换
- 不断重复上述步骤,完成排序
代码实现
1 | for(int i = 0;i<data.length-1;i++){ |
时间复杂度为O($n^2$),不稳定的排序算法(交换的不是相邻的)
堆排序
堆排序是一种树形选择排序,分为大顶堆和小顶堆
- 大顶堆:每个节点的值都大于等于其孩子节点的值
- 小顶堆:每个节点的值都小于等于其孩子的值
小顶堆
根节点的元素最小是小顶堆(小于左右子节点的值)
graph TD 0((0)) --- 1((1)) --- 3((3)) 1((1)) --- 4((4)) 0((0)) --- 2((2))
大顶堆
根节点的元素最大是大顶堆(大于左右子节点的值)
graph TD 0((4)) --- 1((3)) --- 3((1)) 1((3)) --- 4((0)) 0((4)) --- 2((2))
以小顶堆为例,由于每个节点的值都小于等于其子节点的值,而且其是一个完全二叉树,也就意味着父节点和其左右子节点的索引关系为2n+1和2n+2
值得关系为arr[n] <= arr[2n+1] arr[n]<=arr[2n+2]
1 | public static void main(String[] args) { |
时间复杂度为O(nlogn),不稳定
归并排序
归并排序是将待排序的数组分成几个长度几乎相等的子数组,然后使用插入排序对两个子数组中的每一个进行排序,再把两个排好序的子数组归并为一个,以此类推,若干个已排序的数组合并成一个有序的数组
1 | static int[] data = {67,67,14,52,29,9,90,54,87,71}; |
平均时间复杂度为O(nlogn),稳定的算法
各种排序对比
选择排序和冒泡排序
- 选择排序是从第一个数开始,与后面的数进行比较,找到最小的数,放在第一个位置。每轮只交换一次,交换的次数少
- 冒泡排序是从左往右,两两相互比较,然后交换位置。交换的次数多。
直接选择排序和直接插入排序
- 直接选择排序是从未排序的数组中比较取出来最值,放到已排序的数组中的末尾
- 直接插入排序是从未排序的数组中取出来第一个然后和已排序的数组进行比较放到相应的位置
是否稳定的判断标准
排序算法的稳定性是指在排序过程中保持相等元素的相对顺序不变。如果一个排序算法能够保证相等元素的顺序不发生改变,那么它就是稳定的。
例:
21、32、13、45、27、38、76、21*
,在进行排序时21和21*
的相对顺序不变就说明是稳定的排序
相邻元素进行比较大小的肯定是稳定的,跳着比较的可能是不稳定的
稳定的有冒泡排序、直接插入排序、归并排序
不稳定的有堆排序、快速排序、希尔排序、直接选择排序
排序算法的选择
- 如果待排序的序列比较小,可以使用直接插入排序和直接选择排序。由于直接插入排序所需的记录移动操作较直接选择排序多,因而当记录本身信息量大时,用直接选择排序比较好
- 如果待排序的序列基本有序,应该采用直接插入排序
- 如果待排序的序列比较大,应该用时间复杂度低的快速排序、堆排序和归并排序