外排序:由于数据太大,内存一次不能容纳全部的排序记录,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
衡量排序算法的优劣:
时间复杂度:它主要是分析关键字的比较次数和记录的移动次数。
空间复杂度:分析排序算法中需要多少辅助内存。
稳定性:若两个记录A和B的关键字值相等,排序后A,B的先后次序保持不变,则称这种排序算法是稳定的;反之,就是不稳定的。
1.插入排序
1.1直接插入排序(Straight Insertion Sort)
1.1.1算法描述
对一个有n个元素的数据序列,排序需要进行n-1趟插入操作:
第1趟插入,将第2个元素插入前面的有序子序列--此时前面只有一个元素,当然是有序的。
第2趟插入,将第3个元素插入前面的有序子序列,前面2个元素是有序的。
......
第n-1趟插入,将第n个元素插入前面的有序子序列,前面n-1个元素是有序的。
要点:设立哨兵,作为临时存储和判断数组边界之用。
1.1.2算法实现
// 最好:n-1次比较,0次移动 ,时间复杂度为O(n)
// 最差:(n+2)(n-1)/2次比较,(n+4)(n-1)/2次移动,时间复杂度为 O(n^2)
public static int[] insertionSort(int[] arr) {
int out, in;
for (out = 1; out < arr.length; out++) {
if (arr[out] < arr[out - 1]) {
int temp = arr[out];
for (in = out - 1; in >= 0 && arr[in] > temp; in--) {
arr[in + 1] = arr[in];
}
arr[in+1]=temp;
}
}
return arr;
}
其他的插入排序有:二分插入排序(折半插入排序),2-路插入排序。
1.2二分插入排序(Two insertion sort)
1.2.1算法描述
折半插入排序法,又称二分插入排序法,是直接插入排序法的改良版,也需要执行n-1趟插入,不同之处在于,第i趟插入,先找出第i+1个元素应该插入的的位置,假定前i个数据是已经处于有序状态。
1.2.2算法实现
public static void binaryInsertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
if (arr[i] < arr[i - 1]) {
int tmp = arr[i];
int low = 0; // 记录搜索范围的左边界
int high = i - 1; // 记录搜索范围的右边界
while (low <= high) {
int mid = (low + high) / 2; // 记录中间位置
if (arr[mid] < tmp) { // 比较中间位置数据和i处数据大小,以缩小搜索范围
low = mid + 1;
} else {
high = mid - 1;
}
}
// 将low~i处数据整体向后移动1位
for (int j = i; j > low; j--) {
arr[j] = arr[j - 1];
}
arr[low] = tmp;
}
}
}
2.插入排序—希尔排序(Shell`s Sort)
2.1算法描述
希尔排序又叫缩小增量排序,先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
操作方法:
1.选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
2.按增量序列个数k,对序列进行k趟排序;
3.每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
常用的增量序列由Knuth提出,该序列从1开始,通过如下公式产生:h = 3 * h +1,反过来程序需要反向计算h序列,应该使用h=(h-1)/3。
希尔排序时效分析很难,关键码的比较次数与记录移动次数依赖于增量因子序列d的选取,特定情况下可以准确估算出关键码的比较次数和记录的移动次数。目前还没有人给出选取最好的增量因子序列的方法。增量因子序列可以有各种取法,有取奇数的,也有取质数的,但需要注意:增量因子中除1 外没有公因子,且最后一个增量因子必须为1。
2.2算法实现
// O(n^(3/2))
// 不稳定排序算法
public static void shellSort(int[] arr) {
// 计算出最大的h值
int h = 1;
while (h < arr.length) {
h = h * 3 + 1;
}
while (h > 0) {
for (int i = h; i < arr.length; i += h) {
if (arr[i] < arr[i - h]) {
int temp = arr[i];
int j;
for (j = i - h; j >= 0 && arr[j] > temp; j -= h) {
arr[j + h] = arr[j];
}
arr[j + h] = temp;
}
}
// 计算出下一个h值
h = (h - 1) / 3;
}
}
3.选择排序—简单选择排序(Simple Selection Sort)
3.1算法描述
在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。
操作方法:
第一趟,从n 个记录中找出关键码最小的记录与第一个记录交换;
第二趟,从第二个记录开始的n-1 个记录中再选出关键码最小的记录与第二个记录交换;
以此类推.....
第i 趟,则从第i 个记录开始的n-i+1 个记录中选出关键码最小的记录与第i 个记录交换,
直到整个序列按关键码有序。
3.2算法实现
// 最差:n(n-1)/2次比较,n-1次交换,因此时间复杂度为O(n^2)
// 最好:n(n-1)/2次比较,不交换,因此时间复杂度为O(n^2)
// 好于冒泡排序
public static int[] selectionSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
int min = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[min] > arr[j]) {
min = j;
}
}
if (min != i) {
int temp = arr[min];
arr[min] = arr[i];
arr[i] = temp;
}
}
return arr;
}
3.3简单选择排序的改进——二元选择排序
3.3.1算法描述
简单选择排序,每趟循环只能确定一个元素排序后的定位。我们可以考虑改进为每趟循环确定两个元素(当前趟最大和最小记录)的位置,从而减少排序所需的循环次数。改进后对n个数据进行排序,最多只需进行[n/2]趟循环即可
3.3.2算法实现
public static int[] selectSort(int arr[]) {
int i, j, min, max, tmp;
for (i = 0; i <= arr.length / 2; i++) {
// 做不超过n/2趟选择排序
min = i;
max = i; // 分别记录最大和最小关键字记录位置
for (j = i; j < arr.length - i; j++) {
if (arr[j] > arr[max]) {
max = j;
continue;
}
if (arr[j] < arr[min]) {
min = j;
}
}
// 先交换最小值
tmp = arr[i];
arr[i] = arr[min];
arr[min] = tmp;
// 如果找出的最大值是在开始(i)的地方,说明和最小值已经经过交换了,则把max重新赋值
// 如果不等,说明最小值没有交换,则max的值不变。
max = (max == i) ? min : max;
// 交换最大值
tmp = arr[arr.length - i - 1];
arr[arr.length - i - 1] = arr[max];
arr[max] = tmp;
}
return arr;
}
4.选择排序—堆排序(Heap Sort)
4.1算法描述
堆排序是一种树形选择排序,是对简单选择排序的有效改进。
大根堆:任意父节点都比子节点大;
小根堆:任意父节点都比子节点小;
若以一维数组存储一个堆,则堆对应一棵完全二叉树,且所有非叶结点的值均不大于(或不小于)其子女的值,根结点(堆顶元素)的值是最小(或最大)的。如:
(a)大顶堆序列:(96, 83,27,38,11,09)
(b)小顶堆序列:(12,36,24,85,47,30,53,91)
堆排序有两个关键步骤:
(1)构造堆,将一个无序序列初始化为一个堆;
(2)调整堆,在输出了堆的根节点之后,调整剩余元素成为一个新的堆.
调整堆:输出堆顶元素后,对剩余n-1元素重新建成堆的调整过程。
调整小顶堆的方法:
1)设有m 个元素的堆,输出堆顶元素后,剩下m-1 个元素。将堆底元素送入堆顶((最后一个元素与堆顶进行交换),堆被破坏,其原因仅是根结点不满足堆的性质。
2)将根结点与左、右子树中较小元素的进行交换。
3)若与左子树交换:如果左子树堆被破坏,即左子树的根结点不满足堆的性质,则重复方法 (2).
4)若与右子树交换,如果右子树堆被破坏,即右子树的根结点不满足堆的性质。则重复方法 (2).
5)继续对不满足堆性质的子树进行上述交换操作,直到叶子结点,堆被建成。
构建堆:
建堆方法:对初始序列建堆的过程,就是一个反复进行筛选的过程。
1)n 个结点的完全二叉树,则最后一个结点是第个结点的子树。
2)筛选从第个结点为根的子树开始,该子树成为堆。
3)之后向前依次对各结点为根的子树进行筛选,使之成为堆,直到根结点。
如图建堆初始过程:无序序列:(49,38,65,97,76,13,27,49)
4.2算法实现
下标均以1开始,i是父节点,其左儿子的位置在2i上,右儿子的位置在2i+1上,双亲的位置则在i/2上。
// 堆排序
// 时间复杂度为O(nlogn)
// 不稳定排序算法
// 辅助空间为1
// 不适合排序个数较少的序列
public static int[] heapSort(int[] arr) {
int tmp[] = new int[arr.length + 1];
tmp[0] = -1;
for (int i = 0; i < arr.length; i++) {
tmp[i + 1] = arr[i];
}
// 构建大根堆:O(n)
for (int i = arr.length / 2; i >= 1; i--) {
makeMaxRootHeap(tmp, i, arr.length);
}
// 重建:O(nlogn)
for (int i = arr.length; i > 1; i--) {
int temp = tmp[i];
tmp[i] = tmp[1];
tmp[1] = temp;
makeMaxRootHeap(tmp, 1, i - 1);
}
for (int i = 1; i < tmp.length; i++) {
arr[i - 1] = tmp[i];
}
return arr;
}
private static void makeMaxRootHeap(int[] arr, int low, int high) {
int tmp = arr[low];
int j;
for (j = 2 * low; j <= high; j *= 2) {
if (j < high && arr[j] < arr[j + 1]) {
j++;
}
if (tmp >= arr[j]) {
break;
}
arr[low] = arr[j];
low = j;
}
arr[low] = tmp;
}
5.交换排序—冒泡排序(Bubble Sort)
5.1算法描述
冒泡排序思想:两两相邻元素之间的比较,如果前者大于后者,则交换;
5.2算法实现
// 冒泡排序
public static int[] bubbleSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
for (int j = arr.length - 1; j > i; j--) {
if (arr[j] < arr[j - 1]) {
int temp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = temp;
}
}
}
return arr;
}
5.3冒泡排序算法改进一
增加了判断条件的冒泡排序
// 最好:n-1次比较,不移动,因此时间复杂度为O(n),不占用辅助空间
// 最坏:n(n-1)/2次比较和移动,因此O(n^2),占用交换的临时空间,大小为1;
public static int[] bubbleSort1(int[] arr) {
boolean isChanged = true;
for (int i = 0; i < arr.length - 1 && isChanged; i++) {
isChanged = false;
for (int j = arr.length - 1; j > i; j--) {
if (arr[j] < arr[j - 1]) {
int temp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = temp;
isChanged = true;
}
}
}
return arr;
}
5.4冒泡排序算法改进二
传统冒泡排序中每一趟排序操作只能找到一个最大值或最小值,我们考虑利用在每趟排序中进行正向和反向两遍冒泡的方法一次可以得到两个最终值(最大者和最小者) , 从而使排序趟数几乎减少了一半。
public static int[] bubbleSort2(int arr[]) {
int low = 0;
int high = arr.length - 1; // 设置变量的初始值
int tmp, j;
while (low < high) {
for (j = low; j < high; ++j)
// 正向冒泡,找到最大者
if (arr[j] > arr[j + 1]) {
tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
--high; // 修改high值, 前移一位
for (j = high; j > low; --j)
// 反向冒泡,找到最小者
if (arr[j] < arr[j - 1]) {
tmp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = tmp;
}
++low; // 修改low值,后移一位
}
return arr;
}
6.1算法描述
1)选择一个基准元素,通常选择第一个元素或者最后一个元素,
2)通过一趟排序将待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小,另一部分记录的元素值比基准值大。
3)此时基准元素在其排好序后的正确位置。
4)然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序。
快速排序的示例:
(a)一趟排序的过程:
(b)排序的全过程
6.2算法实现
// 不稳定排序算法
// 时间复杂度:最好:O(nlogn) 最坏:O(n^2)
// 空间复杂度:O(logn)
public static int[] quickSort(int[] arr) {
qsort(arr, 0, arr.length - 1);
return arr;
}
private static void qsort(int[] arr, int low, int high) {
int pivot;
if (low < high) {
pivot = partition(arr, low, high);
qsort(arr, low, pivot - 1);
qsort(arr, pivot + 1, high);
}
}
private static int partition(int[] arr, int low, int high) {
int pivotkey;
pivotkey = arr[low];// 选择pivot,此处可以优化
while (low < high) {
while (low < high && arr[high] >= pivotkey)
high--;
arr[low] = arr[high];
while (low < high && arr[low] <= pivotkey)
low++;
arr[high] = arr[low];
}
arr[low] = pivotkey;
return low;
}
1)选取pivot:选取pivot的值对于快速排序至关重要,理想情况,pivot应该是序列的中间数;
若初始序列按关键码有序或基本有序时,快排序反而蜕化为冒泡排序。为改进之,通常以“三者取中法”来选取基准记录,即将排序区间的两个端点与中点作为三个记录关键码,从中选取三点中值居中的一个作为基准点。
2)对于小数组使用插入排序:因为快速排序适合大数组排序,如果是小数组,则效果可能没有简单插入排序来得好;
7. 归并排序(Merge Sort)
7.1算法描述
归并排序(Merge)是将两个(或两个以上)有序表合并成一个新的有序表,(可用顺序存储结构、也易于在链表上实现)即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。若将两个有序表合并成一个有序表,称为2-路归并。归并排序算法稳定,数组需要O(n)的额外空间,链表需要O(log(n))的额外空间,时间复杂度为O(nlog(n)),算法不是自适应的,不需要对数据的随机读取。
归并排序包括两个步骤,分别为:
1)划分子表
2)合并半子表
工作原理:
1、申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
2、设定两个指针,最初位置分别为两个已经排序序列的起始位置
3、比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
4、重复步骤3直到某一指针达到序列尾
5、将另一序列剩下的所有元素直接复制到合并序列尾
7.2算法实现
// 归并排序
// 稳定排序;
// 时间复杂度O(nlogn)
// 空间复杂度:O(n+logn)
public static void mergeSort(int[] data) {
sort(data, 0, data.length - 1);
}
public static void sort(int[] data, int left, int right) {
if (left >= right)
return;
int center = (left + right) / 2;// 找出中间索引
sort(data, left, center);// 对左边数组进行递归
sort(data, center + 1, right);// 对右边数组进行递归
merge(data, left, center, right);// 合并
}
public static void merge(int[] data, int left, int center, int right) {
int[] tmpArr = new int[data.length];
int mid = center + 1;
int third = left;
int tmp = left;
while (left <= center && mid <= right) {
if (data[left] <= data[mid]) {
tmpArr[third++] = data[left++];
} else {
tmpArr[third++] = data[mid++];
}
}
while (mid <= right) {
tmpArr[third++] = data[mid++];
}
while (left <= center) {
tmpArr[third++] = data[left++];
}
// 将临时数组中的内容拷贝回原数组中
while (tmp <= right) {
data[tmp] = tmpArr[tmp++];
}
}
8. 桶排序/基数排序(Radix Sort)
8.1算法描述--桶排序
基本思想:简单来说,就是把数据分组,放在一个个的桶中,然后对每个桶里面的再进行排序。
例如要对大小为[1..1000]范围内的n个整数A[1..n]排序
1)首先,可以把桶设为大小为10的范围,具体而言,设集合B[1]存储[1..10]的整数集合,B[2]存储(10..20]的整数,……集合B[i]存储((i-1)*10,i*10]的整数,i=1,2,..100。总共有100个桶。
2)然后,对A[1..n]从头到尾扫描一遍,把每个A[i]放入对应的桶B[j]中。再对这100个桶中每个桶里的数字排序,这时可用冒泡,选择,乃至快排,一般来说任何排序法都可以。
3)最后,依次输出每个桶里面的数字,且每个桶中的数字从小到大输出,这样就得到所有数字排好序的一个序列了。
假设有n个数字,有m个桶,如果数字是平均分布的,则每个桶里面平均有n/m个数字。如果对每个桶中的数字采用快速排序,那么整个算法的复杂度是
O(n + m * n/m*log(n/m))= O(n + nlogn - nlogm) 从上式看出,当m接近n的时候,桶排序复杂度接近O(n)。
桶排序的缺点:
1)首先是空间复杂度比较高,需要的额外开销大。排序有两个数组的空间开销,一个存放待排序数组,一个就是所谓的桶,比如待排序值是从0到m-1,那就需要m个桶,这个桶数组就要至少m个空间。
2)其次待排序的元素都要在一定的范围内等等。
8.2桶排序算法实现
集合B大小为1情况下桶排序(即是计数排序)
// 桶排序
// min -- 待排序中最小值
// max -- 待排序中最大值
public static void bucketSort(int[] data, int min, int max) {
int[] tmp = new int[data.length];
// buckets用于记录待排序元素的信息
// buckets数组定义了max-min个桶
int[] buckets = new int[max - min + 1];
// 计算每个元素在序列出现的次数
for (int i = 0; i < data.length; i++) {
buckets[data[i] - min]++;
}
// 计算“落入”各桶内的元素在有序序列中的位置
for (int i = 1; i < max - min + 1; i++) {
buckets[i] = buckets[i] + buckets[i - 1];
}
// 将data中的元素完全复制到tmp数组中
System.arraycopy(data, 0, tmp, 0, data.length);
// 根据buckets数组中的信息将待排序列的各元素放入相应位置
for (int k = data.length - 1; k >= 0; k--) {
data[--buckets[tmp[k] - min]] = tmp[k];
}
}
8.3算法描述--基数排序
基数排序的总体思路就是将待排序数据拆分成多个关键字进行排序,也就是说,基数排序的实质是多关键字排序。基数排序过程无须比较关键字,而是通过“分配”和“收集”过程来实现排序。它们的时间复杂度可达到线性阶:O(n)。
两种多关键码排序方法:
多关键码排序按照从最主位关键码到最次位关键码或从最次位到最主位关键码的顺序逐次排序,分两种方法:
最高位优先(Most Significant Digit first)法,简称MSD 法:
1)先按k1排序分组,将序列分成若干子序列,同一组序列的记录中,关键码k1相等。
2)再对各组按k2排序分成子组,之后,对后面的关键码继续这样的排序分组,直到按最次位关键码kd 对各子组排序后。
3)再将各组连接起来,便得到一个有序序列。扑克牌先花色后面值排序即是MSD 法。
最低位优先(Least Significant Digit first)法,简称LSD 法:
1) 先从kd 开始排序,再对kd-1进行排序,依次重复,直到按k1排序分组分成最小的子序列后。
2) 最后将各个子序列连接起来,便可得到一个有序的序列, 扑克牌先面值后花色排序即是LSD 法。
“多关键字排序”的思想实现“单关键字排序”。对数字型或字符型的单关键字,可以看作由多个数位或多个字符构成的多关键字,此时可以采用“分配-收集”的方法进行排序,这一过程称作基数排序法,其中每个数字或字符可能的取值个数称为基数。基数排序:是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。
public static void main(String[] args) {
int[] arr = { 12, 43, 4, 23, 56, 89, 43 };
radixSort(arr, 10, 2);
display(arr);//打印数组元素
}
// 基数排序
// radix--为基数
// d--最大数位数
public static void radixSort(int[] data, int radix, int d) {
// 缓存数组
int[] tmp = new int[data.length];
int[] buckets = new int[radix];// buckets用于记录待排序元素的信息
for (int i = 0, rate = 1; i < d; i++) {
// 重置count数组,开始统计下一个关键字
Arrays.fill(buckets, 0);
// 将data中的元素完全复制到tmp数组中
System.arraycopy(data, 0, tmp, 0, data.length);
// 计算每个待排序数据的子关键字
for (int j = 0; j < data.length; j++) {
int subKey = (tmp[j] / rate) % radix;
buckets[subKey]++;
}
for (int j = 1; j < radix; j++) {
buckets[j] = buckets[j] + buckets[j - 1];
}
// 按子关键字对指定的数据进行排序
for (int m = data.length - 1; m >= 0; m--) {
int subKey = (tmp[m] / rate) % radix;
data[--buckets[subKey]] = tmp[m];
}
rate *= radix;
}
}
参考文章来源: