排序
一、概述
1.1、排序的分类
- 比较的次数
在这种方法中,采用基于比较的次数对排序算法进行分类。对于基于比较的排序算法,在最好情况下时间复杂度为O(nlogn),而在最坏情况下则为O(n2)。基于比较的排序算法通过关键字比较操作来排列序列元素,并且对于大多数输人至少需要O(nlog)次比较。 - 交换的次数
在此方法中,排序算法以交换的次数来对算法分类。 - 内存使用
有些排序算法是"原地的"(即不占用额外的内存空间),仅需要O(1)或O(1og)的内存开销用于创建临时排序数据的辅助存储位置。 - 递归
排序算法可以是递归(如快速排序)或非递归(如选择排序和插入排序)排序,也有同时采用递归和非递归的排序算法(如归并排序) - 稳定性
假设对于所有的索引i和j使得键值A[门等于A[门,并且在原始文件中R[]领先于R[门。若在排序后序列中记录R[门仍然位于R[门前面,则称这种排序算法是稳定的。少数排序算法能够维持具有相等键值元素的相对次序(即使排序后相等元素仍然保持它们的相对位置)。 - 适应性
少数排序算法的复杂度依赖于序列的初始排列情况(如快速排序),输入的初始排列将会影响算法的运行时间,有这种情况出现的算法称作适应算法。
1.2、其它分类方法
- 内部排序
在排序时仅使用主存储器的排序算法称为内部排序(internal sort)。该算法对所有的存储器都能够高速随机存取。 - 外部排序
在排序时需要使用磁带或磁盘等外部存储器的排序算法都属于外部排序(external sort).。
二、排序算法
2.1、冒泡排序
冒泡排序(bubble sort)是一种最简单的排序算法。其基本思想是迭代地对输入序列中的第一个元素到最后一个元素进行两两比较,当需要时交换这两个元素(位置)。该过程持续迭代直到在一趟排序过程中不需要交换操作为止。冒泡排序得名于键值较小的元素如同"气泡"一样逐渐漂浮到序列的顶端。通常,插入排序比冒泡排序有更好的性能。
由于冒泡排序的简单性和复杂度,有些学者建议不讲授该排序算法。相比于其他排序算法冒泡排序的唯一显著优势是,它可以检测输人序列是否已经是排序的。
java
/**
* 冒泡排序
* @param arr
*/
public void bubbleSort(int arr[]) {
int n = arr.length;
boolean swapped;
for (int i = 0; i < n - 1; i++) {
swapped = false;
for (int j = 0; j < n - i - 1; j++) {
// 比较相邻元素,如果顺序错误则交换
if (arr[j] > arr[j + 1]) {
// 交换 arr[j+1] 和 arr[j]
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = true; // 表示发生了交换
}
}
// 如果在这一趟遍历中没有发生交换,说明数组已经有序,可以提前结束排序
if (!swapped) break;
}
}
- 最坏情况下时间复杂度:O(n2)
- 最好情况下时间复杂度(改进版):O(n)
- 平均情况下时间复杂度(基本版):O(n2)
- 最坏情况下空间复杂度:O(1)辅助
2.2、选择排序
选择排序(selection sort))是一种原地(in-place)排序算法,适用于小文件。由于选择操作是基于键值的且交换操作只在需要时才执行,所以选择排序常用于数值较大和键值较小的文件。
- 优点
- 容易实现。
- 原地排序(不需要额外的存储空间)。
- 缺点
- 扩展性较差:O(n2)。
- 算法
- 寻找序列中的最小值。
- 用当前位置的值交换最小值。
- 对所有元素重复上述过程,直到整个序列排序完成。
该算法称为选择排序,因为它重复选择最小的元素。
java
/**
* 选择排序
* @param arr
*/
public void selection(int arr[]) {
int i, j, min, temp;
for (i = 0; i < arr.length - 1; i++) {
min = i;
for (j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min])
min = j;
}
//交换元素
temp = arr[min];
arr[min] = arr[i];
arr[i] = temp;
}
}
- 最坏情况下时间复杂度:O(n2)
- 最好情况下时间复杂度:O(n)
- 平均情况下时间复杂度:O(n2)
- 最坏情况下空间复杂度:O(1)辅助
2.3、插入排序
插入排序(insertion sort)是一种简单且有效的比较排序算法。在每次迭代过程中算法随机地从输入序列中移除一个元素,并将该元素插人待排序序列的正确位置。重复该过程,直到所有输入元素都被选择一次。
优点:
- 实现简单。
- 数据量较少时效率高。
- 适应性(adaptive):如果输入序列已预排序(可能是不完全的预排序),则时间复杂度为O(n+d),d是反转的次数。
- 算法的实际运行效率优于选择排序和冒泡排序,即使在最坏情况下三个算法的时间复杂度均为O(n)。
- 稳定性(stable):键值相同时它能够保持输人数据的原有次序。
- 原地(in-place):仅需要常量O(1)的辅助内存空间。
- 即时(online):插入排序能够在接收序列的同时对其进行排序
插入排序重复如下过程:每次从输入数据中移除一个元素并将其插入已排序序列的正确位置,直到所有输入元素都插入有序序列中。插入排序是典型的原地排序。经过k次迭代后数组具有性质:前十1个元素已经排序。

每个与x比较且大于x的元素复制到x的右边。
java
/**
* 插入排序
* @param arr
*/
public void insertionSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
示例
6 8 1 4 5 3 7 2 ,按升序排序。
6 8 1 4 5 3 7 2 (考虑索引位置0)
6 8 1 4 5 3 7 2 (考虑索引位置0~1)
1 6 8 4 5 3 7 2 (考虑索引位置02:在6和8前插入1)
1 4 6 8 5 3 7 2 (重复以上过程直到序列被排序)
1 4 5 6 8 3 7 2
1 3 4 5 6 7 8 2
1 2 3 4 5 6 7 8(已排序的序列!)
- 最坏情况下时间复杂度:O(n2)
- 最好情况下时间复杂度:O(n2)
- 平均情况下时间复杂度:O(n2)
- 最坏情况下空间复杂度:O(n2) 总计,O(1) 辅助
插入排序是一种最坏情况下时间复杂度为O(n2)的基本排序算法。在数据几乎都已经排序或者输入数据规模较小时可以使用插入排序。由于上述原因以及插入排序的稳定性,插入排序可用于归并和快速排序等高开销的分治排序算法的递归基础情形(当问题规模小时)。
2.4、希尔排序
希尔排序(shell sort)又称为缩小增量排序(diminishing increment sort),算法由Donnald Shell提出而得名。该算法是一个泛化的插入排序。插人排序在输入序列几乎已经有序的情况下非常有效。希尔排序也称为n间距(n-gap)插入排序。希尔排序分多路并使用不同的间距来比较相邻元素,而不是仅比较相邻对。通过逐步减少间距,最终以1为间距或者进行一次常规的插入排序即可。
插入排序中的比较是在两个相邻元素之间进行的,每次比较最多进行1次反转交换。希尔排序利用可变增量使算法直到最后一步才比较相邻元素,所以希尔排序的最后一步是一个有效的插入排序算法。希尔排序通过允许比较和交换具有一定距离的元素对插排序进行改进。希尔排序是比较排序算法中第一个低于二次复杂度的算法。
本质上希尔排序是对插入排序的一种简单扩展。两者的主要差异在于希尔排序具有交换相距较远元素的能力,这使得它能较快地把元素交换到所需位置。例如,如果初始数组中最小的元素恰好位于数组的末端,那么插入排序需要经过对整个数组的比较和交换才能把最小元素放置到数组的开头,而希尔排序使元素能够一次移动多步而非一步,从而能够以较少的交换次数把元素放到合适的位置。
希尔排序的主要思想是比较和交换数组中每个距离为h的元素。对其进一步解释可使算法的思路更加清晰:h决定相距多远的元素能够进行交换,例如,如果h值为13,则第1个元素(索引0)将与第14个元素(索引13)进行交换(如果需要)。第2个元素与第15个元素进行交换,以此类推。如果h为1,则希尔排序就与常规插入排序完全一样。
希尔排序首先选择足够大的间距h(但不能超过数组的大小)开始排序,这样能允许相具较远的合适元素进行交换。一旦以某个特定的五完成排序,则称数组是以h间距排序的数组。接下来的步骤是以某一确定的序列依次减少间距h的值,并重新开始新一轮以h为间距的排序。一旦h变为1且是h间距排序的,则数组排序完成。注意,由于h的最后一个序列值为1,所以最后一次排序总是一个插入排序,但此时数组已经变得基本有序且更容易排序。
希尔排序使用的序列h1,h2,...,ht称为增量序列。任何增量序列都是可以的只要h1=1,且某些增量序列的效果要优于其他的增量序列。希尔排序把输入序列分成大小相同的多路子序列,然后对每路利用插入排序算法进行排序。希尔排序通过快速移动元素值到目的位置来提高插入排序的效率。
希尔排序算法步骤:
- 选择初始增量:通常选择数组长度的一半作为初始增量。
- 分组:将数组分成多个子序列,每个子序列的间隔为当前的增量。
- 插入排序:对每个子序列进行插入排序。
- 缩小增量:重复上述步骤,每次将增量减半,直到增量为1。
- 最终排序:当增量为1时,整个数组作为一个子序列进行插入排序,此时算法完成。
java
/**
* 希尔排序
* @param arr
*/
public void shellSort(int[] arr) {
int n = arr.length;
//初始增量设为数组长度的一半
for (int gap = n / 2; gap > 0; gap /= 2) {
//对每个子序列进行插入排序
for (int i = gap; i < n; i++) {
//保存当前元素
int temp = arr[i];
//对当前元素及之前之前的元素进行比较和插入排序
int j;
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
arr[j] = arr[j - gap];
}
arr[j] = temp;
}
}
}
希尔排序对中等大小的序列非常有效,对于较大的序列它不是最好的选择,但希尔排序是所有时间复杂度为O(n)的排序算法中最快的算法。
希尔排序的缺点是它的算法思路复杂且远不及归并、堆和快速排序有效。希尔排序明显比归并、堆和快速排序慢,但它却是一种相对简单的算法。若不考虑速度的重要性,
希尔排序对于数据量小于5000的序列是不错的选择。对较小的列表进行重复排序时,希尔排序也是一个非常好的选择。
使用希尔排序的最佳情况是序列已经完全排序,此时比较次数较少。希尔排序的运行时间取决于所选择的增量序列。
- 最坏情况下时间复杂度取决于间隔序列。最好情况:O(nlog2n)
- 最好情况下时间复杂度:O(n)
- 平均情况下时间复杂度取决于间隔序列
- 最坏情况下空间复杂度:O(n)
2.5、并归排序
归并排序(merge sort)是分治的一个实例。
- 归并是把两个已排序文件合并成一个更大的已排序文件的过程。
- 选择是把一个文件分成包含k个最小元素和一k个最大元素两个部分的过程。
- 选择和归并互为逆操作:
- 选择把一个序列分成两部分。
- 归并把两个文件合并成一个文件。
- 归并排序是快速排序的补充。
- 归并排序以连续的方式访问数据。
- 归并排序适用于链表排序。
- 归并排序对输人的初始次序不敏感。
- 快速排序中的大部分任务在递归调用前完成。快速排序从最大子文件开始并以最小子文件结束,因此需要栈结构。此外,快速排序算法也不稳定。归并排序把序列分为两个部分,并对每个部分分别处理。归并排序从最小子文件开始并以最大子文件结束,因此不需要栈,并且归并排序算法是稳定的算法。
归并排序首先将数组分成半,直到每个子数组只有一个元素,然后开始合并这些子数组,每次合并都将它们排序,直到整个数组变得有序。
java
/**
* 并归排序
* @param arr
*/
// 主函数,用于调用归并排序
public void sort(int[] arr) {
if (arr.length < 2) {
return; // 基准情形,如果数组只有一个元素或为空,则无需排序
}
int[] temp = new int[arr.length]; // 辅助数组,用于合并过程中的临时存储
mergeSort(arr, temp, 0, arr.length - 1);
}
// 递归的归并排序函数
private void mergeSort(int[] arr, int[] temp, int leftStart, int rightEnd) {
if (leftStart >= rightEnd) {
return; // 基准情形,如果子数组只有一个元素,则无需排序
}
int middle = (leftStart + rightEnd) / 2;
mergeSort(arr, temp, leftStart, middle); // 左边归并排序,使左半部分有序
mergeSort(arr, temp, middle + 1, rightEnd); // 右边归并排序,使右半部分有序
mergeHalves(arr, temp, leftStart, rightEnd); // 合并两个有序的半部分
}
// 合并两个有序的半部分
private static void mergeHalves(int[] arr, int[] temp, int leftStart, int rightEnd) {
int leftEnd = (rightEnd + leftStart) / 2;
int rightStart = leftEnd + 1;
int size = rightEnd - leftStart + 1;
int left = leftStart;
int right = rightStart;
int index = leftStart;
// 复制数据到temp数组
while (left <= leftEnd && right <= rightEnd) {
if (arr[left] <= arr[right]) {
temp[index] = arr[left];
left++;
} else {
temp[index] = arr[right];
right++;
}
index++;
}
// 将左边剩余的元素复制到temp中(如果有)
System.arraycopy(arr, left, temp, index, leftEnd - left + 1);
// 将右边剩余的元素复制到temp中(如果有)
System.arraycopy(arr, right, temp, index, rightEnd - right + 1);
// 将排序后的元素从temp复制回原数组中
System.arraycopy(temp, leftStart, arr, leftStart, size);
}
- 最坏情况下时间复杂度:O(nlogn)
- 最好情况下时间复杂度:O(nlogn)
- 平均情况下时间复杂度:O(nlogn)
- 最坏情况下空间复杂度:O(n) 辅助