排序
通常来说, 排序是为了进行快速排序
衡量排序算法的优劣方式
- 时间复杂度: 关键字的比较次数 与 记录的移动次数
- 空间复杂度: 排序算法中需要多少辅助内存
- 稳定性: 如果a原本在b前面, 且a=b, 排序之后a仍然在b的前面
排序前: [1, 2, 5, 3, 2]
排序后: [1, 2, 2, 3, 5]
可以看到第一个数组中有两个2, 如果排序后, 第一个2还是在前面, 第二个2在其后面, 称这个排序算法是稳定的
排序的分类
- 内部排序: 整个排序过程不需要借助于外部存储器(如磁盘等), 所有排序操作都在内存中完成
- 外部排序: 参与排序的数据较多, 数据量较大, 计算机内存发放全部加载, 必须借助于外部存储器(如磁盘);
外部排序最常见的是多路归并排序, 可以认为, 外部排序是由多次内部排序组成的.
算法的5大特征
- 输入: 有0个或多个输入数据, 这些输入必须有清楚的描述和定义
- 输出: 至少有一个或多个输出结果, 不可以没有输出结果
- 有穷性: 算法在有限的步骤之后会自动结束而不会无限循环, 并且每一个步骤可以在可接受的时间内完成
- 确定性: 算法中的每一步都有确定的含义, 不会出现歧义性(二义性)
- 可行性: 算法的每一步都是清楚且可行性的, 能让用户用纸和笔计算而求出答案
常见的排序算法及复杂度
算法分类
时间复杂度
算法比较分析
- 从平均时间而言: 快速排序最佳, 但是在最坏的情况下时间性能不如堆排序和归并排序
- 从算法简单性看: 由于选择排序/插入排序和冒泡排序的算法比较简单, 将其认为是加单算法; 对于希尔排序/堆排序/快速排序和归并排序算法, 其算法比较复杂, 认为是复杂排序
- 从稳定性看: 插入排序/冒泡排序和归并排序是最稳定的; 选择排序/快速排序/希尔排序和堆排序是不稳定的
- 从待排序的记录数n的大小看: n较小时, 宜采用简单排序, 而n较大时宜采用快速排序
算法的选择
- 若N较小(如
n
≤
50
n ≤ 50
n≤50), 可以采用插入排序或者选择排序;
当记录规模较小时, 插入排序较好, 否则, 因为选择排序移动的记录数少于插入排序, 选择排序比较合适 - 若文件初始状态基本有序(正序), 应选用插入排序/冒泡排序或快速排序
- 若N较大, 应采用时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)的排序方法: 快速排序/堆排序/归并排序
常见算法的实现
冒泡排序(Bubble Sort)
依次比较相邻元素的排序, 若发现逆序, 则交换, 使较大的元素交换到最后面
动图演示
public static int[] maoPaoSort() {
/*
* 两个 for 循环的结束条件是重点; 每进行一次大循环, 就减少一个参与比较的数
*/
int[] arrMaoPao = new int[] { 46, 89, 23, 45, 12, 99, 55, 44, 43 };
for (int i = 0; i < arrMaoPao.length - 1; i++) {
for (int j = 0; j < arrMaoPao.length - i - 1; j++) {
if (arrMaoPao[j] > arrMaoPao[j + 1]) {
int tempNum = arrMaoPao[j + 1];
arrMaoPao[j + 1] = arrMaoPao[j];
arrMaoPao[j] = tempNum;
}
}
}
return arrMaoPao;
}
选择排序(Selection Sort)
它的工作原理: 首先在未排序的序列中找到最小(大)元素, 存放到排序序列的起始位置, 然后, 再从剩余未排序元素中继续寻找最小(大)元素, 然后放到已排序序列的末尾. 以此类推, 直到所有元素均排序完毕
- 初始状态: 无序区为R[1…n], 有序区为空
- 第i趟排序(i=1, 2, 3…n-1)开始时, 当前有序区和无序区分别为R[1…i-1]和R(i…n). 该趟排序从当前无序区中-选出关键字最小的记录 R[k], 将它与无序区的第1个记录R交换, 使R[1…i]和R[i+1…n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区
- n-1趟结束, 数组有序化了
表现最稳定的排序算法之一, 因为无论什么数据进去都是 O ( n 2 ) O(n^{2}) O(n2) 的时间复杂度, 所以用到它的时候, 数据规模越小越好. 唯一的好处可能就是不占用额外的内存空间了吧. 理论上讲, 选择排序可能也是平时排序一般人想到的最多的排序方法.
动图演示
public static int[] SelectionSort() {
int[] arrQuick = new int[] { 46, 89, 23, 45, 12, 99, 55, 44, 43 };
for (int i = 0; i < arrQuick.length - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < arrQuick.length; j++) {
if (arrQuick[j] < arrQuick[minIndex]) { // 寻找最小的数
minIndex = j; // 将最小数的索引保存
}
}
int temp = arrQuick[i];
arrQuick[i] = arrQuick[minIndex];
arrQuick[minIndex] = temp;
}
return arrQuick;
}
插入排序(Insertion Sort)
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法. 它的工作原理是通过构建有序序列, 对于未排序数据, 在已排序序列中从后向前扫描, 找到相应位置并插入.
插入排序在实现上, 通常采用in-place排序(即只需用到O(1)的额外空间的排序), 因而在从后向前扫描过程中, 需要反复把已排序元素逐步向后挪位, 为最新元素提供插入空间.
一般来说, 插入排序都采用in-place在数组上实现. 具体算法描述如下:
- 从第一个元素开始, 该元素可以认为已经被排序
- 取出下一个元素, 在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素, 将该元素移到下一位置
- 重复步骤3, 直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤
2~5
动图演示
public static int[] InsertSort() {
int[] arrInsert = new int[] { 46, 89, 23, 45, 12, 99, 55, 44, 43 };
for (int i = 1; i < arrInsert.length; i++) {
int preIndex = i - 1;
int current = arrInsert[i];
while (preIndex >= 0 && arrInsert[preIndex] > current) {
arrInsert[preIndex + 1] = arrInsert[preIndex];
preIndex--;
}
arrInsert[preIndex + 1] = current;
}
return arrInsert;
}
希尔排序(Shell Sort)
1959年Shell发明, 第一个突破 O ( n 2 ) O(n^{2}) O(n2) 的排序算法, 是简单插入排序的改进版. 它与插入排序的不同之处在于, 它会优先比较距离较远的元素. 希尔排序又叫缩小增量排序.
希尔排序的核心在于间隔序列的设定. 既可以提前设定好间隔序列, 也可以动态的定义间隔序列. 动态定义间隔序列的算法是《算法(第4版)》的合著者Robert Sedgewick提出的.
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序, 具体算法描述:
- 选择一个增量序列 t 1 , t 2 , … , t k , t_1, t_2, …, t_k, t1,t2,…,tk, 其中 t i > t j , t k = 1 t_i > t_j, t_k=1 ti>tj,tk=1
- 按增量序列个数k, 对序列进行k 趟排序;
- 每趟排序, 根据对应的增量ti, 将待排序列分割成若干长度为m 的子序列, 分别对各子表进行直接插入排序. 仅增量因子为1 时, 整个序列作为一个表来处理, 表长度即为整个序列的长度.
动图演示
public static void shellSort(int[] arrays) {
//增量每次都/2
for (int step = arrays.length / 2; step > 0; step /= 2) {
//从增量那组开始进行插入排序, 直至完毕
for (int i = step; i < arrays.length; i++) {
int j = i;
int temp = arrays[j];
// j - step 就是代表与它同组隔壁的元素
while (j - step >= 0 && arrays[j - step] > temp) {
arrays[j] = arrays[j - step];
j = j - step;
}
arrays[j] = temp;
}
}
}
归并排序(Merge Sort)
归并排序是建立在归并操作上的一种有效的排序算法. 该算法是采用分治法(Divide and Conquer)的一个非常典型的应用. 将已有序的子序列合并, 得到完全有序的序列; 即先使每个子序列有序, 再使子序列段间有序. 若将两个有序表合并成一个有序表, 称为2-路归并.
归并排序是一种稳定的排序方法. 和选择排序一样, 归并排序的性能不受输入数据的影响, 但表现比选择排序好的多, 因为始终都是O(nlogn)的时间复杂度. 代价是需要额外的内存空间.
- 把长度为n的输入序列分成两个长度为 n 2 \frac{n}{2} 2n 的子序列
- 对这两个子序列分别采用归并排序
- 将两个排序好的子序列合并成一个最终的排序序列
动图演示
快速排序(Quick Sort)
快速排序(Quick Sort)由图灵奖获得者Tony Hoare发明, 被列为20世纪十大算法之一, 是迄今为止所有内排序算法中速度最快的一种, 冒泡排序的升级版, 快速排序的时间复杂度为
O
(
n
l
o
g
(
n
)
)
O(nlog(n))
O(nlog(n))
通常明显比同为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn) 的其他算法更快, 因此常被采用, 而且快排采用了分治法的思想
快速排序的基本思想: 通过一趟排序将待排记录分隔成独立的两部分, 其中一部分记录的关键字均比另一部分的关键字小, 则可分别对这两部分记录继续进行排序, 以达到整个序列有序.
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists). 具体算法描述如下:
- 从数列中挑出一个元素, 称为 “基准”(pivot)
- 重新排序数列, 所有元素比基准值小的摆放在基准前面, 所有元素比基准值大的摆在基准的后面(相同的数可以到任一边). 在这个分区退出之后, 该基准就处于数列的中间位置. 这个称为分区(partition)操作
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序
基数选择的方式:
- 固定基准数
- 随机基准数
- 三数取中
动图演示
public class ArraySort {
private static void printArr(int[] arr) {
for (int anArr : arr) {
System.out.print(anArr + " ");
}
}
private static int partition(int[] arr, int left, int right) {
int temp = arr[left];
while (right > left) {
// 先判断基准数和后面的数依次比较
while (temp <= arr[right] && left < right) {
--right;
}
// 当基准数大于了 arr[right], 则填坑
if (left < right) {
arr[left] = arr[right];
++left;
}
// 现在是 arr[right] 需要填坑了
while (temp >= arr[left] && left < right) {
++left;
}
if (left < right) {
arr[right] = arr[left];
--right;
}
}
arr[left] = temp;
return left;
}
private static void quickSort(int[] arr, int left, int right) {
if (arr == null || left >= right || arr.length <= 1)
return;
int mid = partition(arr, left, right);
quickSort(arr, left, mid);
quickSort(arr, mid + 1, right);
}
public static void main(String[] args) {
int[] arr = {6, 4, 3, 2, 7, 9, 1, 8, 5};
quickSort(arr, 0, arr.length - 1);
printArr(arr);
}
}
堆排序(Heap Sort)
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法.堆积是一个近似完全二叉树的结构, 并同时满足堆积的性质: 即子结点的键值或索引总是小于(或者大于)它的父节点.
- 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆, 此堆为初始的无序区
- 将堆顶元素R[1]与最后一个元素R[n]交换, 此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n]
- 由于交换后新的堆顶R[1]可能违反堆的性质, 因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆, 然后再次将R[1]与无序区最后一个元素交换, 得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn).不断重复此过程直到有序区的元素个数为n-1, 则整个排序过程完成
动图演示
计数排序(Counting Sort)
计数排序不是基于比较的排序算法, 其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中.作为一种线性时间复杂度的排序, 计数排序要求输入的数据必须是有确定范围的整数.
计数排序是一个稳定的排序算法.当输入的元素是 n 个 0到 k 之间的整数时, 时间复杂度是O(n+k), 空间复杂度也是O(n+k), 其排序速度快于任何比较排序算法.当k不是很大并且序列比较集中时, 计数排序是一个很有效的排序算法.
- 找出待排序的数组中最大和最小的元素
- 统计数组中每个值为i的元素出现的次数, 存入数组C的第i项
- 对所有的计数累加(从C中的第一个元素开始, 每一项和前一项相加)
- 反向填充目标数组: 将每个元素i放在新数组的第C(i)项, 每放一个元素就将C(i)减去1
动图演示
桶排序(Bucket Sort)
桶排序是计数排序的升级版.它利用了函数的映射关系, 高效与否的关键就在于这个映射函数的确定.桶排序 (Bucket sort)的工作的原理: 假设输入数据服从均匀分布, 将数据分到有限数量的桶里, 每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排).
桶排序最好情况下使用线性时间O(n), 桶排序的时间复杂度, 取决与对各个桶之间数据进行排序的时间复杂度, 因为其它部分的时间复杂度都为O(n).很显然, 桶划分的越小, 各个桶之间的数据越少, 排序所用的时间也会越少.但相应的空间消耗就会增大.
- 设置一个定量的数组当作空桶
- 遍历输入数据, 并且把数据一个一个放到对应的桶里去
- 对每个不是空的桶进行排序
- 从不是空的桶里把排好序的数据拼接起来.
图片演示
基数排序(Radix Sort)
基数排序是按照低位先排序, 然后收集; 再按照高位排序, 然后再收集; 依次类推, 直到最高位.有时候有些属性是有优先级顺序的, 先按低优先级排序, 再按高优先级排序.最后的次序就是高优先级高的在前, 高优先级相同的低优先级高的在前.
基数排序基于分别排序, 分别收集, 所以是稳定的.但基数排序的性能比桶排序要略差, 每一次关键字的桶分配都需要O(n)的时间复杂度, 而且分配之后得到新的关键字序列又需要O(n)的时间复杂度.假如待排数据可以分为d个关键字, 则基数排序的时间复杂度将是 O ( d × 2 n ) O(d×2n) O(d×2n) , 当然d要远远小于n, 因此基本上还是线性级别的.
基数排序的空间复杂度为O(n+k), 其中k为桶的数量.一般来说n>>k, 因此额外空间需要大概n个左右.
- 取得数组中的最大数, 并取得位数
- arr为原始数组, 从最低位开始取每个位组成radix数组
- 对radix进行计数排序(利用计数排序适用于小范围数的特点)
图片演示