1. 插入排序
1.1 直接插入排序-原理:
整个区间被分为:1. 有序区间;2. 无序区间;每次选择无序区间的第一个元素,在有序区间内选择合适的位置插入。
1.2 实现:
public class InsertSort {
public static void sort(int[] array){
// 一共要取多少个元素来进行插入过程(无序区间里有多少个元素)
for (int i = 0; i < array.length - 1; i++) {
// 有序区间 [0, i] 至少在 i == 0 的时候得有一个元素
// 无序区间 [i + 1, n)
// 先取出无序区间的第一个元素,记为 k
int k = array[i + 1];
// 从后往前,遍历有序区间[0,i]
// 找到属于无序第一个元素,即k的位置。
int j = i;
for (; j >= 0 && k < array[j]; j--) {
array[j + 1] = array[j]; // 将不符合条件的数据往后般一格
}
array[j + 1] = k;
}
}
}
1.3 性能分析:数据可以保持稳定性。插入排序,初始数据越接近有序,时间效率越高。
1.4 折半插入排序(了解)
在有序区间选择数据应该插入的位置时,因为区间的有序性,可以利用折半查找的思想。
public static void bsInsertSort(int[] array) {
for (int i = 1; i < array.length; i++) {
int v = array[i];
int left = 0;
int right = i;
// [left, right)
// 需要考虑稳定性
while (left < right) {
int m = (left + right) / 2;
if (v >= array[m]) {
left = m + 1;
} else {
right = m;
}
}
// 搬移
for (int j = i; j > left; j--) {
array[j] = array[j - 1];
}
array[left] = v;
}
}
2. 希尔排序
2.1 原理:【算法思想】先将待排序记录序列分割成若干个“较稀疏的”子序列,分别进行直接插入排序。经过上述粗略调整,整个序列中的记录已经基本有序,最后再对全部记录进行一次直接插人排序。
①首先选定记录间的距离为 di (i=1),在整个待排序记录序列中将所有间隔为 d1 的记录分成一组,进行组内直接插人排序。
②然后取 i=i+1,记录间的距离为 di ( di < d(i-1) ), 在整个待排序记录序列中,将所有间隔为 di 的记录分成一组,进行组内直接插入排序。
重复步骤②多次,直至记录间的距离 di=1,此时整个只有一个子序列,对该序列进行直接插入排序,完成整个排序过程。
2.2 实现
public class ShellSort {
public static void sort(int[] a){
//1.根据a的长度确定增长量h
int h = 1;
while (h < a.length/2){
h = 2*h+1;
}
//2.希尔排序
while ( h>=1 ){
//排序:找到待插入的元素,
for (int i = h; i < a.length; i++) {
for (int j = i; j >= h ; j-=h) {
if (a[j-h]>a[j]){
//交换元素
swap(a,j-h,j);
}else {//j-h 比 j 小,不用交换。
break;
}
}
}
h=h/2;
}
}
private static void swap(int[] a, int i, int j){
int emp;
emp = a[i];
a[i] = a[j];
a[j] = emp;
}
}
2.3 稳定性:不稳定
3. 选择排序
3.1 直接选择排序-原理
【算法思想】
第一趟简单选择排序时,从第一个记录开始,通过 n-1 次关键字的比较,从 n 个记录中选出关键字最小的记录,并和第一个记录进行交换。
第二趟简单选择排序时,从第二个记录开始,通过 n-2 次关键字的比较,从 n-1 个记录中选出关键字最小的记录,并和第二个记录进行交换。
第 i 趟简单选择排序时,从第 i 个记录开始,通过 n-i 次关键字的比较,从 n-i+1 个记录中选出关键字最小的记录,并和第i个记录进行交换。
如此反复,经过 n-1 趟简单选择排序,将把 n-1 个记录排到位,剩下一个最小记录直接在最后,所以共需进行 n-1 趟简单选择排序。
3.2 实现
public class SelectSort {
public static void sort(int[] array){
for (int i = 0; i < array.length-1; i++) {
int k = i;
for (int j = i+1; j < array.length; j++) {
if (array[j] < array[k]){
k = j;
}
}
swap(array,k,i);
}
}
private static void swap(int[] a, int i, int j){
int emp;
emp = a[i];
a[i] = a[j];
a[j] = emp;
}
}
3.3稳定性:不稳定
3.4 双向选择排序(了解)
每一次从无序区间选出最小 + 最大的元素,存放在无序区间的最前和最后,直到全部待排序的数据元素排完 。
public static void selectSort(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
// 无序区间: [0, array.length - i)
// 有序区间: [array.length - i, array.length)
int max = 0;
for (int j = 1; j < array.length - i; j++) {
if (array[j] > array[max]) {
max = j;
}
}
int t = array[max];
array[max] = array[array.length - i - 1];
array[array.length - i - 1] = t;
}
}
4. 堆排序
4.1 原理基本原理也是选择排序,只是不在使用遍历的方式查找无序区间的最大的数,而是通过堆来选择无序区间的最大的数。
【算法思想】
①将待排序记录按照堆的定义建初堆(算法 9.9),并输出堆顶元素。
②调整剩余的记录序列,利用筛选法将前 n-i 个元素重新筛选建成为一个新堆,再输出堆顶元素。
③重复执行步骤②,进行 n-1 次筛选,新筛选成的堆会越来越小,而新堆后面的有序关键
字会越来越多,最后使待排序记录序列成为一个有序的序列,这个过程称之为堆排序。
画图过程比较繁琐,大家可以按照思路以及代码自己画着理解一下。
4.2 实现
public class HeapSort {
public static void sort(int[] array){
//建初堆:升序建大堆,降序建小堆。
for (int i = (array.length-2)/2; i >=0 ; i--) {
shiftDown(array,array.length,i);
}
//维护堆:堆顶元素与最后一个元素交换后,堆顶的“堆性质”被破环,需要维护。此时维护的堆大小应该是依次减小的。
for (int i = 0; i < array.length-1; i++) {
swap(array,0,array.length-i-1);
shiftDown(array,array.length-i-1,0);
}
}
private static void shiftDown(int[] array, int length, int index) {
while (index*2+1 < length){
int left = index*2+1;
int right = left+1;
int max = left;
if (right < length && array[left] < array[right]){
max = right;
}
if (array[index] >= array[max]){
return;
}
swap(array,index,max);
index = max;
}
}
private static void swap(int[] a, int i, int j){
int emp;
emp = a[i];
a[i] = a[j];
a[j] = emp;
}
}
4.3 稳定性:不稳定
5. 冒泡排序
5.1 原理:在无序区间,通过相邻数的比较,将最大的数冒泡到无序区间的最后,持续这个过程,直到数组整体有序
5.2 实现:
public class BubbleSort{
public void sort(long[] array) {
for (int i = 0; i < array.length - 1; i++) {
boolean sorted = true;
for (int j = 0; j < array.length - i - 1; j++) {
if (array[j] > array[j + 1]) {
SortUtil.swap(array, j, j + 1);
sorted = false;
}
}
if (sorted) {
return;
}
}
}
}
5.3 稳定性:稳定
6. 快速排序(重要)
6.1 原理-总览
1. 从待排序区间选择一个数,作为基准值(pivot);
2. Partition: 遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的
右边;
3. 采用分治思想,对左右两个小区间按照同样的方式处理,直到小区间的长度 == 1,代表已经有序,或者小区间的长度 == 0,代表没有数据。
6.2 原理-partition:快速排序的精髓所在就是 partition 操作,实现这一操作有很多方法,总的一点就是根据 pivot 来分割数据。
6.3 稳定性:不稳定
6.4 原理-基准值的选择
1. 选择边上(左或者右)
2. 随机选择
3. 几数取中(例如三数取中):array[left], array[mid], array[right] 大小是中间的为基准值
6.5 代码实现:
public class QuickSort {
public static void sort(int[] array) {
quickSortRange(array,0,array.length-1);
}
// 为了代码书写方便,我们选择使用左闭右闭的区间表示形式
// from,to 下标的元素都算在区间的元素中
// 左闭右闭的情况下,区间内的元素个数 = to - from + 1;
private static void quickSortRange(int[] array, int from, int to) {
if (to - from +1 <= 1) {
// 区间中元素个数 <= 1 个
return;
}
// 挑选中区间最右边的元素 array[to],
//int pi = partitionMethodA(array, from, to);
//经过该步处理后数组array中的数据呈现: [from,pi)的元素是小于 pivot ;(pi,array.length-1]元素是大于 pivot ;
//pivot == array[pi];
// 按照分治算法的思路,使用相同的方式,处理相同性质的问题,只是问题的规模在变小
int[] index = partitionD(array,from,to);
int left = index[0];
int right = index[1];
quickSortRange(array, from, left); // 针对小于等于 pivot 的区间做处理
quickSortRange(array, right, to); // 针对大于等于 pivot 的区间做处理
}
/**
* 以区间最右边的元素 array[to] 最为 pivot,遍历整个区间,从 from 到 to,移动必要的元素
* 进行分区
* @param array
* @param from
* @param to
* @return 最终 pivot 所在的下标
*/
/*
<= pivot: [from,left];
> pivot : [right,to];
未比较 : (left,right);
*/
private static int partitionA(int[] array, int from, int to) {
int left = from;
int right = to;
int pivot = array[to];
while (left < right){
while (left < right && array[left] <= pivot){
left++;
}
while(left < right && array[right] >= pivot){
right--;
}
swap(array,left,right);
}
swap(array,left,to);
return left;
}
/*
<= pivot: [from,left];
> pivot : [right,to];
未比较 : (left,right);
*/
public static int partitionB(int[] array, int from, int to){
int pivot = array[to];
int left = from;
int right = to;
while(left < right){
while(left < right && array[left] < pivot){
left++;
}
array[right] = array[left];
while(left < right && array[right] > pivot){
right--;
}
array[left] = array[right];
}
array[left] = pivot;
return left;
}
/**
* 对 array 的 [from, to] 区间进行分区
* 分区完成之后,区间被分割为 [<= pivot] pivot [>= pivot]
* 分区过程中,始终保持
* [from, s) 小于 pivot
* [s, i) 大于等于 pivot
* [i, to) 未比较过的元素
* [to, to] pivot
* @param array
* @param from
* @param to
* @return pivot 最终所在下标
*/
public static int partitionC(int[] array,int from,int to){
int s = from;
int pivot = array[to];
for (int i = from; i < to; i++) { // 遍历 [from, to)
// 这里加 == 号也保证不了稳定性,有交换操作
if (array[i] < pivot) {
// TODO: 可以进行简单的优化:如果 i == s,就不交换
swap(array,i,s);
s++;
}
}
array[to] = array[s];
array[s] = pivot;
return s;
}
public static int[] partitionD(int[] array,int from,int to){
int s = from;
int i = from;
int g = to;
int pivot = array[to];
while (g-i+1 > 0){
if (array[i] == pivot){
i++;
}else if (array[i] < pivot){
swap(array,s,i);
s++;i++;
}else {
swap(array,g,i);
g--;
}
}
return new int[] {s-1,i};
}
public static int partitionE(int[]array,int left,int right){
int d = left + 1;
int pivot = array[left];
for (int i = left+1; i <=right ; i++) {
if(array[i] < pivot) {
swap(array,i,d);
d++;
}
}
swap(array,d,left);
return d;
}
private static void swap(int[] a, int i, int j){
int emp;
emp = a[i];
a[i] = a[j];
a[j] = emp;
}
}
6.7 优化总结
1. 选择基准值很重要,通常使用几数取中法
2. partition 过程中把和基准值相等的数也选择出来
3. 待排序区间小于一个阈值时,使用直接插入排序
6.8 总结
1. 在待排序区间选择一个基准值
1. 选择左边或者右边
2. 随机选取
3. 几数取中法
2. 做 partition,使得小的数在左,大的数在右
1. hoare
2. 挖坑
3. 前后遍历
4. 将基准值相等的也选择出来(了解)
3. 分治处理左右两个小区间,直到小区间数目小于一个阈值,使用插入排序
7. 归并排序(重要)
7.1 原理-总览:归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divideand Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
7.2 原理-合并两个有序数组
7.3 实现:
public class MergeSort {
private static int[] assist;
public static void sort(int[] array) {
assist = new int[array.length];
int lo = 0, hi = array.length - 1;
sort(array, lo, hi);
}
private static void sort(int[] array, int lo, int hi) {
if (hi <= lo) return;
int mid = lo + (hi - lo) / 2;
sort(array, lo, mid);
sort(array, mid + 1, hi);//以上两步均为分组,
merge(array, lo, mid, hi);//将array中从 lo 到 hi 的元素合并为有序数组。
}
private static void merge(int[] array, int lo, int mid, int hi) {
int i = lo, p1 = lo, p2 = mid + 1;//三个指针
while (p1 <= mid && p2 <= hi) {
if (array[p1] < array[p2]) {
assist[i++] = array[p1++];
} else {
assist[i++] = array[p2++];
}
}
while (p1 <= mid) {
assist[i++] = array[p1++];
}
while (p2 <= hi) {
assist[i++] = array[p2++];
}
System.arraycopy(assist, lo, array, lo, hi - lo + 1);
}
private static void exchange(int[] a, int i, int j) {
int emp;
emp = a[i];
a[i] = a[j];
a[j] = emp;
}
}
7.4 稳定性:稳定
7.5 优化总结
在排序过程中重复利用两个数组,减少元素的复制过程。
8.外部排序
在前面讨论的各种排序方法中待排序记录及有关信息都是存储在内存中的,整个排序过程全部是在内存中完成的,并不涉及数据的内外存交换,
统称为内部排序。若待排序的记录数目很大,以致无法一次性调人内存,整个排序过程必须借用外部存储器分批调人才能完成,称这类排序为外部排序
。本结将主要介绍基于直接存取设备(磁盘存储器)和顺序存取设备(磁带存储器)的外部排序方法的基本思想。
8.1 外排序的基本方法
最常用的外部排序方法是归并排序法。该方法由两个阶段组成: 第一阶段待排序记录分批读入内存,把文件逐段输入到内容,用有效的内排序方
法对文件的各个段进行排序,经排序的文件段称为顺串(或归并段),当它们生成后立即以子文件方式写到外存上,这样在外存上就形成了许多初始
顺串;第二阶段是子文件多路归并,对这些顺串用某种归并方法(如 2-路归并法)进行多趟归并,使顺串的长度逐渐由小至大,直至变成一个顺串,即整个文件有序为止。外部排序可使用磁带、磁盘等外存,最初形成的顺串文件长度取决于内存所能提供的排序区
大小和最初排序策略,归并路数取决于能提供的外部设备数。
9.排序总结:
9.1总的分为以下几种排序:
9.2从算法的平均时间复杂度、最坏时间复杂度以及算法所需的辅助储存空间三方面,对各种排序方法加以比较。
排序方法 | 平均时间复杂度 | 最坏是啊金复杂度 | 辅助存储空间 |
简单排序法 | O(n2) | O(n2) | O(1) |
快速排序 | O(n log 2n) | O(n2) | O(log2n) |
堆排序 | O(n log 2n) | O(n log 2n) | O(1) |
归并排序 | O(n log 2n) | O(n log 2n) | O(n) |
9.3 综合分析和比较各种排序方法,可以得出以下结论:
①简单排序法一般只用于n 较小的情况(例如 n<30)。当序列中的记录“基本有序”时,直接插人排序是最佳的排序方法。如果记录中的数据较多,则应采用移动次数较少的简单选择排序法。
②快速排序、堆排序和归并排序的平均时间复杂度均为 O(nlog_n),但实验结果表明,就平均时间性能而言,快速排序是所有排序方法中最好的。遗憾的是,快速排序在最坏情况下的时间性能为 O(㎡)。堆排序和归并排序的最坏时间复杂度仍为 O(nlogan),当n 较大时,归并排序的时间性能优于堆排序,但它所需的辅助空间最多。
③可以将简单排序法与性能较好的排序方法结合使用。例如,在快速排序中,当划分子区间的长度小于某值时,可以转而调用直接插入排序法;或者先将待排序序列划分成若干子序列,分别进行直接插入排序,然后再利用归并排序法,将有序子序列合并成一个完整的有序序列。
④基数排序的时间复杂度可以写成 O(dn)。因此,它最适用于 n 值很大而关键字的位数 d 较小的序列。当d远小于 n 时,其时间复杂度接近于 O(n)
⑤从排序的稳定性上来看,在所有简单排序法中,简单选择排序是不稳定的,其他各种简单排序法都是稳定的。然而,在那些时间性能较好的排序方法中,希尔排序,快速排序、堆排序都是不稳定的,只有归并排序、基数排序是稳定的。
综上所述,每一种排序方法各有特点,没有哪一种方法是绝对最优的。应根据具体情况选择合适的排序方法,也可以将多种方法结合起来使用。