目录
二、高级排序
2.1、希尔排序
2.2、归并排序
2.2.1、递归
2.2.2、归并排序
数据结构和算法(三)--排序
二、高级排序
冒泡排序,选择排序,插入排序,最坏的时间复杂度都是O(N^2),而平方阶,随着输入规模的增大,时间成本急剧上升,所以这些基本排序方法不能处理大规模的问题,接下来来学习一些高级的排序算法,争取降低算法的时间复杂度最高阶次幂。
2.1、希尔排序
希尔排序是插入排序的一种,又称"缩小增量排序",是插入排序算法的一种更高效的改进版本。
插入排序的时候,有一个很不好的事,如果已排序的分组元素为{2,5,7,9,10},未排序的分组元素为{1,8},那么下一个待插入元素为1,我们需要拿着1从后往前,依次和10,9,7,5,2进行交换位置,才能完成真正的插入,每次交换只能和相邻的元素交换位置。那么如果我们要提高效率,直观的想法就是一次交换,能把1放到更前面的位置,比如一次交换就能把1插到2和5之间,这样一次交换1就向前走了5个位置,可以减少交换次数,这样的需求如何实现呢?接下来我们来看看希尔排序的原理。
需求:
排序前:{9,1,2,5,7,4,8,6,3,5}
排序后:{1,2,3,4,5,5,6,7,8,9}
排序原理:
1.选定一个增长量h,按照增长量h作为数据分组的依据,对数据进行分组;
2.对分好组的每一组数据完成插入排序;
3.减小增长量,最小减为1,重复第二步操作。
增长量h的确定:增长量h的值没有一个固定的规则,我们这里采用以下规则:
int h=1;
while(h<数组的长度/2){
h=2h+1;
}
//循环结束后我们就可以确定h的最大值;
//h的减小规则为:h=h/2;
希尔排序API设计:
类名 | Shell |
构造方法 | Shell():创建Shell对象 |
成员方法 | 1.public static void sort(Comparable[] a):对数组内的元素进行排序 2.private static boolean greater(Comparable v, Comparable w):判断v是否大于w 3.private static void exch(Comparable[] a,int i,int j):交换a数组中,索引i和索引j处的值 |
代码实现:
public class Shell {
/**
* a.对数组a中的元素进行排序
*/
@SuppressWarnings("rawtypes")
public static void sort(Comparable[] a) {
// 1.根据数组a的长度,确定增长量h的初始值
int h = 1;
while (h < a.length / 2) {
h = 2 * h + 1;
}
// 2.希尔排序
while (h >= 1) {
// 2.1找到待插入的元素
for (int i = h; i < a.length; i++) {
// 2.2把待插入的元素插入到有序数列中
for (int j = i; j >= h; j -= h) {
// 待插入的元素是a[j],a[j-h]
if (greater(a[j - h], a[j])) {
exch(a, j - h, j);
} else {
// 待插入元素已经找到合适的位置,结束循环
break;
}
}
}
// 减小h的值
h = h / 2;
}
}
/**
* a.比较v元素是否大于w元素
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
private static boolean greater(Comparable v, Comparable w) {
return v.compareTo(w) > 0;
}
/**
* a.数组元素i和j交换位置
*/
@SuppressWarnings("rawtypes")
private static void exch(Comparable[] a, int i, int j) {
Comparable temp;
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
测试类:
import java.util.Arrays;
public class ShellTest {
public static void main(String[] args) {
Integer[] arr = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5 };
Shell.sort(arr);
System.out.println(Arrays.toString(arr));
}
}
希尔排序的时间复杂度分析
在希尔排序中,增长量h并没有固定的规则,有很多论文研究了各种不同的递增序列,但都无法证明某个序列是最好的。
我们可以使用事后分析法对希尔排序和插入排序做性能比较。
自己做一个数据文件,里面存放的是从100000到1的逆向数据,我们可以根据这个批量数据进行测试。我们可以根据这个批量数据完成测试。测试的思想:在执行排序前记录一个时间,在排序完成后记录一个时间,两个时间差就是排序时间。
希尔排序和插入排序性能比较测试代码:
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
public class SortCompare {
public static void main(String[] args) throws Exception {
List<Integer> list = new ArrayList<>();
BufferedReader reader = new BufferedReader(
new InputStreamReader(SortCompare.class.getClassLoader().getResourceAsStream("reverse_arr.txt")));
String line = null;
while ((line = reader.readLine()) != null) {
int i = Integer.parseInt(line);
list.add(i);
}
reader.close();
// 把list集合转换成数组
Integer[] arr = new Integer[list.size()];
list.toArray(arr);
shellTest(arr);
// InsertionTest(arr);
}
public static void shellTest(Integer[] arr) {
long start = System.currentTimeMillis();
Shell.sort(arr);
long end = System.currentTimeMillis();
System.out.println("希尔排序执行时间:" + (end - start) + "毫秒");// 希尔排序执行时间:30毫秒
}
public static void InsertionTest(Integer[] arr) {
long start = System.currentTimeMillis();
Insertion.sort(arr);
long end = System.currentTimeMillis();
System.out.println("插入排序执行时间:" + (end - start) + "毫秒");// 插入排序执行时间:21559毫秒
}
}
通过测试发现,在处理大批量数据时,希尔排序的性能确实高于插入排序。
2.2、归并排序
2.2.1、递归
定义:定义方法时,在方法内部调用方法本身,称之为递归。
public void show() {
show();
}
作用:它通常把一个大型复杂的问题,层层转换为一个与原问题相似的,规模较小的问题来求解。递归策略只需要少量的程序就可以描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
注意事项:在递归中,不能无限制的调用自己,必须要有边界条件,能够让递归结束,因为每一次递归调用都会在栈内存开辟新的空间,重新执行方法,如果递归的层次太深,很容易造成栈内存溢出。
2.2.2、归并排序
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
需求:
排序前:{8,4,5,7,1,3,6,2}
排序后:{1,2,3,4,5,6,7,8}
排序原理:
1.尽可能的一组数据拆分成两个元素相等的子组,并对每一个子组继续拆分,
直到拆分后的每个子组的元素个数是1为止。
2.将相邻的两个子组进行合并成一个有序的大组。
3.不断的重复步骤2,直到最终只有一个组为止。
归并排序API设计:
类名 | Merge |
构造方法 | Merge():创建Merge对象 |
成员方法 | 1.public static void sort(Comparable[] a):对数组内的元素进行排序 2.public static void sort(Comparable[] a,int lo,int hi):对数组a中从索引lo到索引hi之间的元素进行排序 3.private static void merge(Comparable[] a,int lo,int mid,int hi):从索引lo到索引mid为一个子组,从索引mid+1到索引hi为另一个子组,把数组a中的这两个子组的数据合并成一个有序的大组(从索引lo到索引hi) 4.private static boolean less(Comparable v, Comparable w):判断v是否小于w 5.private static void exch(Comparable[] a,int i,int j):交换a数组中,索引i和索引j处的值 |
成员变量 | 1.private static Comparable[] assist:完成归并操作需要的辅助数组 |
代码实现:
public class Merge {
// 辅助数组
@SuppressWarnings("rawtypes")
private static Comparable[] assist;
/**
* a.对数组a中的元素进行排序
*/
@SuppressWarnings("rawtypes")
public static void sort(Comparable[] a) {
// 1.初始化辅助数组
assist = new Comparable[a.length];
// 2.定义lo变量和hi变量记录数组中最小的索引和最大的索引
int lo = 0;
int hi = a.length - 1;
// 3.调用重载方法完成从索引lo到索引hi的元素的排序
sort(a, lo, hi);
}
/**
* a.对数组a中从索引lo到索引hi之间的元素进行排序
*/
@SuppressWarnings("rawtypes")
public static void sort(Comparable[] a, int lo, int hi) {
if (hi <= lo) {
return;
}
// 对lo到hi之间的数据进行分为两个组
int mid = lo + (hi - lo) / 2;
// 分别为每一组数据进行排序
sort(a, lo, mid);
sort(a, mid + 1, hi);
// 再把两个组的数据进行归并
merge(a, lo, mid, hi);
}
/**
* a.合并
*/
@SuppressWarnings("rawtypes")
public static void merge(Comparable[] a, int lo, int mid, int hi) {
// 定义三个指针
int i = lo;
int p1 = lo;
int p2 = mid + 1;
// 遍历+移动p1指针和p2指针,比较对应索引处的值,找出小的那个放到辅助数组的对应索引处
while (p1 <= mid && p2 <= hi) {
if (less(a[p1], a[p2])) {
assist[i++] = a[p1++];
} else {
assist[i++] = a[p2++];
}
}
// 遍历+如果p1的指针没有走完,那么顺序移动p1指针,把对应的元素放到辅助数组的对应索引处
while (p1 <= mid) {
assist[i++] = a[p1++];
}
// 遍历+如果p2的指针没有走完,那么顺序移动p2指针,把对应的元素放到辅助数组的对应索引处
while (p2 <= hi) {
assist[i++] = a[p2++];
}
// 把辅助数组中的元素拷贝到原数组中
for (int j = lo; j <= hi; j++) {
a[j] = assist[j];
}
}
/**
* a.比较v元素是否小于w元素
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
private static boolean less(Comparable v, Comparable w) {
return v.compareTo(w) < 0;
}
/**
* a.数组元素i和j交换位置
*/
@SuppressWarnings({ "rawtypes", "unused" })
private static void exch(Comparable[] a, int i, int j) {
Comparable temp;
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
测试类:
import java.util.Arrays;
public class MergeTest {
public static void main(String[] args) {
Integer[] arr = { 8, 4, 5, 7, 1, 3, 6, 2 };
Merge.sort(arr);
System.out.println(Arrays.toString(arr));
}
}
归并排序时间复杂度分析
归并排序是分治思想的最典型的例子
用树状图来描述归并,如果一个数组有8个元素,那么它将每次除以2找最小的子数组,共拆log8次,值为3,所以树共有3层,那么自顶向下第k层有2^k个子数组,每个数组的长度为2^(3-k)次比较,归并最多需要2^(3-k)次比较。因此每层的比较次数为2^k*2^(3-k)=2^3,那么3层总共为3*2^3.
假设元素的个数为n,那么使用归并排序拆分的次数为log2(n),所以共log2(n)层,那么使用log2(n)替换上面3*2^3中的3这个层数,最终得出的归并排序的时间复杂度为:log2(n)*2^(log2(n))=log2(n)*n,根据大O推到法则,忽略底数,最终归并排序的时间复杂度为O(nlogn)。
归并排序的缺点:
需要申请额外的数组空间,导致空间复杂度提升,是典型的以空间换时间的。
归并排序与希尔排序性能测试:
采用事后分析法进行性能测试。
自己做一个数据文件,里面存放的是从100000到1的逆向数据,我们可以根据这个批量数据进行测试。我们可以根据这个批量数据完成测试。测试的思想:在执行排序前记录一个时间,在排序完成后记录一个时间,两个时间差就是排序时间。
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
public class SortCompare {
public static void main(String[] args) throws Exception {
List<Integer> list = new ArrayList<>();
BufferedReader reader = new BufferedReader(
new InputStreamReader(SortCompare.class.getClassLoader().getResourceAsStream("reverse_arr.txt")));
String line = null;
while ((line = reader.readLine()) != null) {
int i = Integer.parseInt(line);
list.add(i);
}
reader.close();
// 把list集合转换成数组
Integer[] arr = new Integer[list.size()];
list.toArray(arr);
// shellTest(arr);
// InsertionTest(arr);
MergeTest(arr);
}
public static void shellTest(Integer[] arr) {
long start = System.currentTimeMillis();
Shell.sort(arr);
long end = System.currentTimeMillis();
System.out.println("希尔排序执行时间:" + (end - start) + "毫秒");// 希尔排序执行时间:30毫秒
}
public static void InsertionTest(Integer[] arr) {
long start = System.currentTimeMillis();
Insertion.sort(arr);
long end = System.currentTimeMillis();
System.out.println("插入排序执行时间:" + (end - start) + "毫秒");// 插入排序执行时间:21559毫秒
}
public static void MergeTest(Integer[] arr) {
long start = System.currentTimeMillis();
Merge.sort(arr);
long end = System.currentTimeMillis();
System.out.println("归并排序执行时间:" + (end - start) + "毫秒");// 归并排序执行时间:60毫秒
}
}
干我们这行,啥时候懈怠,就意味着长进的停止,长进的停止就意味着被淘汰,只能往前冲,直到凤凰涅槃的一天!