排序算法总结
查找和排序算法是算法的入门知识,其经典思想可以用于很多算法当中。因为其实现代码较短,应用较常见。所以在面试中经常会问到排序算法及其相关的问题。但万变不离其宗,只要熟悉了思想,灵活运用也不是难事。
一般在面试中最常考的是快速排序和归并排序,并且经常有面试官要求现场写出这两种排序的代码。对这两种排序的代码一定要信手拈来才行。还有插入排序、冒泡排序、堆排序、基数排序、桶排序等。面试官对于这些排序可能会要求比较各自的优劣、各种算法的思想及其使用场景,分析算法的时间和空间复杂度。
排序算法分类、性能比较及使用场景
一、排序算法种类
我们通常所说的排序算法往往指的是内部排序算法,即数据记录在内存中进行排序。
排序算法大体可分为两种:
- 比较排序:时间复杂度最少可达到O(nlogn),主要有:冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序等。
- 非比较排序:时间复杂度可以达到O(n),主要有:计数排序,基数排序,桶排序等。
二、性能比较
下表给出了常见比较排序算法的性能:
排序算法稳定性
1) 稳定的:如果存在多个具有相同排序码的记录,经过排序后,这些记录的相对次序仍然保持不变,则这种排序算法称为稳定的。
插入排序、冒泡排序、归并排序、非比较排序(基数、计数、桶式)都是稳定的排序算法。
2)不稳定的:直接选择排序、堆排序、希尔排序、快速排序。
三、使用场景
- 若n较小(如n≤50),可采用直接插入或直接选择排序。当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插人,应选直接选择排序为宜。
- 若文件初始状态基本有序(指正序),则应选用直接插入、冒泡或随机的快速排序为宜;
- 若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序。
- 快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
- 堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。
- 若要求排序稳定,则可选用归并排序。通常可以将它和直接插入排序结合在一起使用:先利用直接插入排序求得较长的有序子文件,然后再两两归并之。因为直接插入排序是稳定 的,所以改进后的归并排序仍是稳定的。
首先定义了一个Swap类,并且实现静态方法swap用于交换数组指定位置上的元素值。
/**
* 交换数组指定位置上两个数的值
* @param arr 数组
* @param i 第一个数字在数组的下标
* @param j 第二个数字在数组的下标
*/
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
下面是各个排序算法的具体思想和实现
简单排序算法
冒泡排序
冒泡排序是最简单的排序之一了,其大体思想就是通过与相邻元素的比较和交换来把小的数交换到最前面。这个过程类似于水泡向上升一样,因此而得名。
举个例子,对5,3,8,6,4这个无序序列进行冒泡排序。首先从后向前冒泡,4和6比较,把4交换到前面,序列变成5,3,8,4,6。同理4和8交换,变成5,3,4,8,6,3和4无需交换。5和3交换,变成3,5,4,8,6,3。这样一次冒泡就完了,把最小的数3排到最前面了。对剩下的序列依次冒泡就会得到一个有序序列。
冒泡排序的时间复杂度为: O(n2) 。
算法实现如下(Java):
public static void main(String[] args) {
int[] arr = {
5, 3, 4, 8, 6};
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
/**
* 冒泡排序:稳定。属于交换排序算法的一种,另外一种是快速排序。
* 时间复杂度:最好为 O(n^2),最差为 O(n^2),平均为 O(n^2)。空间复杂度:O(1)
* @param arr 数组
*/
public static void bubbleSort(int[] arr){
if (arr == null || arr.length == 0) { // 数组为null或者没有元素
return;
}
for (int i = 0; i < arr.length - 1; i++) {
for (int j = arr.length - 1; j > i; j--) {
if (arr[j] < arr[j - 1]) {
Swap.swap(arr, j, j - 1);
}
}
}
}
选择排序
思想其实和冒泡排序有点类似,都是在一次排序后把最小的元素放到最前面。但是过程不同,冒泡排序是通过相邻的比较和交换。而选择排序是通过对整体的选择。
举个例子,对5,3,8,6,4这个无序序列进行简单选择排序,首先要选择5以外的最小数来和5交换,也就是选择3和5交换,一次排序后就变成了3,5,8,6,4。对剩下的序列一次进行选择和交换,最终就会得到一个有序序列。其实选择排序可以看成冒泡排序的优化,因为其目的相同,只是选择排序只有在确定了最小数的前提下才进行交换,大大减少了交换的次数。
选择排序的时间复杂度为: O(n2) 。
算法实现如下(Java):
public static void main(String[] args) {
int[] arr = {
5, 3, 4, 8, 6};
selectSort(arr);
System.out.println(Arrays.toString(arr));
}
/**
* 选择排序:不稳定。另外一种是堆排序。
* 时间复杂度:最好为 O(n^2),最差为 O(n^2),平均为 O(n^2)。空间复杂度:O(1)
* @param arr
*/
public static void selectSort(int[] arr){
if (arr == null || arr.length == 0) {
return;
}
int minIndex = 0; // 记录每次循环最小值得数组下标,在循环外创建节省空间
for (int i = 0; i < arr.length - 1; i++) { // 只需要比较n-1次
minIndex = i; // 每次初始化为当前位置
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j; // 保证minIndex对应的元素为这次循环中的最小元素
}
}
if (minIndex != i) { // 如果minIndex不为i,说明找到了更小的值,交换之
Swap.swap(arr, i, minIndex);
}
}
}
插入排序
不是通过交换位置而是通过比较找到合适的位置插入元素来达到排序的目的的。相信大家都有过打扑克牌的经历,特别是牌数较大的。在分牌时可能要整理自己的牌,牌多的时候怎么整理呢? 就是拿到一张牌,找到一个合适的位置插入。这个原理其实和插入排序是一样的。
举个例子,对5,3,8,6,4这个无序序列进行简单插入排序,首先假设第一个数的位置时正确的,想一下在拿到第一张牌的时候,没必要整理。然后3要插到5前面,把5后移一位,变成3,5,8,6,4.想一下整理牌的时候应该也是这样吧。然后8不用动,6插在8前面,8后移一位,4插在5前面,