对于排序算法一直没有系统的梳理过,正好最近有点儿时间,仔细的梳理了下几种常用的排序算法
1、冒泡排序
/**
* 冒泡排序
* 冒泡排序是最简单的排序,它对两个数进行比较,如果较小的一个在靠后的位置则进行交换,它的交换次数是比较多的
* 冒泡排序的时间复杂度是O(n^2),空间复杂度是O(1),因为只引入了一个临时变量
*
**/
public class MaopaoSort {
public static void main(String[] args) {
int[] a = new int[]{2, 1, 5, 3, 7, 2, 4, 6, 36, 6, 82, 23, 6};
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + "\t");
}
System.out.println("");
System.out.println(">>>>>>>>>>>>>>>");
sort(a);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + "\t");
}
}
public static void sort(int a[]) {
for (int i = 0; i < a.length; i++) {
for (int j = i; j < a.length; j++) {
if (a[i] > a[j]) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
}
}
}
2、选择排序
/**
* 选择排序
* 每层内循环选择出最小的值,然后放入外层循环
* 选择排序每经历一次内循环最多发生交换,内循环内部只是进行比较,选择出最小值的游标,在外层循环进行交换
* 选择排序的时间复杂度为O(n^2),空间复杂度为O(1),只引入了一个临时变量
*
**/
public class SelectSort {
public static void main(String[] args) {
int[] a = new int[]{2, 1, 5, 3, 7, 2, 4, 6, 36, 6, 82, 23, 6};
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + "\t");
}
System.out.println("");
System.out.println(">>>>>>>>>>>>>>>");
sort(a);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + "\t");
}
}
public static void sort(int a[]) {
for (int i = 0; i < a.length; i++) {
int temp = i;
for (int j = i + 1; j < a.length; j++) {
//这个地方是关键,temp指向了最小值的位置,如果发现比temp还小的值j,则进行将j指向的地址赋值给temp
if (a[temp] > a[j]) {
temp = j;
}
}
if (temp != i) {
int temp2 = a[i];
a[i] = a[temp];
a[temp] = temp2;
}
}
}
}
3、插入排序
/**
* 插入排序
* 插入排序其实体现在两个方面:插入+移动,将当前元素插入到合适的位置,同时将它之后的元素都向后移动
* 插入排序的时间复杂服为O(n^2),空间复杂度为O(1),只引入了一个临时变量
* 插入排序对于无需混乱的场景是没有任何优势的,它的优势在于对于整体顺序性比较好的数组,排序的时间复杂度很低,可以达到O(n)
*
**/
public class InsertSort {
public static void main(String[] args) {
int[] a = new int[]{2, 1, 5, 3, 7, 2, 4, 6, 36, 6, 82, 23, 6};
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + "\t");
}
System.out.println("");
System.out.println(">>>>>>>>>>>>>>>");
sort1(a);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + "\t");
}
}
/**
* 插入排序--正统实现方式
*
* @param a
*/
public static void sort3(int a[]) {
for (int i = 1; i < a.length; i++) {
//关键操作部分,对于前面i项进行排序,将i对应的元素,插入到它应该在的位置,并且将其之后的元素向后移动
for (int j = i; j > 0; j--) {
if (a[j] < a[j - 1]) {
//如果j元素小于它前面的元素,那么交换它和它之前的元素位置
int temp = a[j];
a[j] = a[j - 1];
a[j - 1] = temp;
} else {
//一旦j大于等于前面的元素,那么就不需要再向前进行比较了,因为0~i之间的元素是已经排好序的
break;
}
}
}
}
}
4、希尔排序
/**
* 希尔排序
* 希尔排序其实就是插入排序的一个变种,通过将间隔为h的元素作为一个子数组进行排序,逐步缩小这个间隔h的值来完成整个数组的排序
* 希尔排序的时间复杂度最坏是O(n^2),平均是O(nlogn),这个时间复杂度是难以证明的
* 知乎上有一篇帖子的置顶回答思路很好:https://www.zhihu.com/question/24637339
* 可以在逆序数(一个序列中,前面元素比后面元素大,则成为一个逆序,这个序列中所有的逆序总个数称之为逆序数)这个角度来进行思考
* 像冒泡、插入排序、选择排序这种排序方法都是每次均进行比较,每次都是消除一个逆序数,但是像希尔、快排、归并这一类排序方法是
* 通过跳跃的比较对象来进行排序,他妈保底会消除一个逆序,有可能会消除多个逆序,从而导致希尔排序这一类排序可以突破O(n^2)的时间复杂度
*
**/
public class ShellSort {
public static void main(String[] args) {
int[] a = new int[]{2, 1, 5, 3, 7, 2, 4, 6, 36, 6, 82, 23, 6};
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + "\t");
}
System.out.println("");
System.out.println(">>>>>>>>>>>>>>>");
sort(a);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + "\t");
}
}
public static void sort(int a[]) {
int N = a.length;
int h = 1;
//选择以三分为基础切分段
while (h < N / 3) {
h = h * 3 + 1;
}
while (h >= 1) {
//外层循环遍历从h到数组结尾的这部分数组元素
for (int i = h; i < N; i++) {
//内层循环将相距为h的元素作为一个单元进行插入排序
for (int j = i; j >= h; j -= h) {
if (a[j] < a[j - h]) {
int temp = a[j];
a[j] = a[j - h];
a[j - h] = temp;
}
}
}
h = h / 3;
}
}
}
5、归并排序
/**
* 归并排序
* 归并排序是一种使用了分治策略进行排序的算法,分治之后使用一次循环即可完成排序,因此它的时间复杂度为O(nlogn)
* 由于需要引入一个临时数组来存储元素,因此空间复杂度为O(n)
*
**/
public class MergeSort {
//增加一个数组来保存原始数据
private static int[] aux;
public static void main(String[] args) {
int[] a = new int[]{2, 1, 5, 3, 7, 2, 4, 6, 36, 6, 82, 23, 6};
aux = new int[a.length];
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + "\t");
}
System.out.println("");
System.out.println(">>>>>>>>>>>>>>>");
sort(a, 0, a.length - 1);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + "\t");
}
}
public static void sort(int[] a, int lo, int li) {
//分治的终点是剩余一个元素
if (lo >= li) {
return;
}
//二分开始分治
int mid = lo + (li - lo) / 2;
//递归排序左半部分
sort(a, lo, mid);
//递归排序右半部分
sort(a, mid + 1, li);
//合并两边已经排序好的部分
merge(a, lo, mid, li);
}
public static void merge(int[] a, int lo, int mid, int li) {
//将原始数据保存一份,以供下面取出直接放入数组a中,保证数据不丢失
for (int k = lo; k <= li; k++) {
aux[k] = a[k];
}
int i = lo;
int j = mid + 1;
//将以mid为中间的两个已经排好序的半部分数组合并为一个有序数组
for (int k = lo; k <= li; k++) {
if (i > mid) {
//如果i已经大于mid,则说明左半部分没有元素了,直接从右边开始取数据
a[k] = aux[j++];
} else if (j > li) {
//如果j已经大于li,则说明右半部分没有元素了,直接从左边开始取数据
a[k] = aux[i++];
} else if (a[i] < a[j]) {
//左右两边元素都是充足的,则进行比较,取出两者中较小的元素放入
a[k] = aux[i++];
} else {
//左右两边元素都是充足的,则进行比较,取出两者中较小的元素放入
a[k] = aux[j++];
}
}
}
}
6、快速排序
/**
* 快速排序
* 快速排序的是一种基准值将一个数组中的元素向基准值的两边进行调换,然后在利用分治来完成排序的算法
* 快速排序比归并排序带来的提升是不需要额外添加一个临时的数组来进行排序,从而使得排序的空间复杂度控制在O(1)
* 快速排序使用了分治+遍历一次数组,因此它的时间复杂度为O(nlogn),它最坏的时间复杂度是在O(n^2)
* 快速排序在最坏的情况下即每次取到的都是最小或者最大的元素,那么此时就是冒泡排序,每次都只排好一个元素
* 因此快速排序适合元素比较混乱的排序场景,通常会选择在使用快速排序之前将所有元素重新打乱的方式来防止快速排序的时间复杂度恶化
*
**/
public class FastSort {
public static void main(String[] args) {
int[] a = new int[]{2, 1, 5, 3, 7, 2, 4, 6, 36, 6, 82, 23, 6};
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + "\t");
}
System.out.println("");
System.out.println(">>>>>>>>>>>>>>>");
sort(a, 0, a.length - 1);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + "\t");
}
}
public static void sort(int[] a, int lo, int li) {
//如果lo大于li则返回
if (li <= lo) {
return;
}
//针对基准值(目前默认取最小的元素)进行交换,小于基准值的放在左边,大于基准值的放在右边
int p = partition(a, lo, li);
//递归排序左半部分(不包含基准值,这是与归并的递归最不同的部分)
sort(a, lo, p - 1);
//递归排序右半部分(不包含基准值,这是与归并的递归最不同的部分)
sort(a, p + 1, li);
}
private static int partition(int[] a, int lo, int li) {
//默认取最小的元素为基准值
int v = a[lo];
//左侧游标,从最小元素之后的第一个元素即i+1开始遍历,
int i = lo;
//右侧游标,从最右侧元素开始遍历
int j = li + 1;
while (true) {
//i不断向右边滑动,直到找到大于基准值的元素,这个元素需要换到基准值的右边
//如果一直获取不到,则取最右侧元素
while (a[++i] < v) {
if (i == li) {
break;
}
}
//j不断向左边滑动,直到找到小于基准值的元素,这个元素需要换到基准值的左边
//如果一直获取不到,则取最左侧元素
while (v < a[--j]) {
if (j == lo) {
break;
}
}
//如果i已经大于j了的话,那么说明两边的元素都已经以基准值左右分开,可以停止循环
if (i >= j) {
break;
}
//找出了i是大于基准值的元素,j是小于基准值的元素,那么将i、j进行调换
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
//最后将基准值与j进行交换,因为j中的元素肯定是小于基准值的
int temp = a[j];
a[j] = v;
a[lo] = temp;
return j;
}
}
7、快速排序-三向切分
/**
* 快速排序-三向切分
* 三向切分是对于快速排序的一个优化,对于一些相同元素很多的集合即便进行了重新打散操作,也会导致频繁的交换这些元素带来不必要的排序,
* 这种切分之后就不需要再对中间这些相同的元素进行排序,从而提升整体排序的性能
* 这种排序的时间复杂度是O(nlogn),但是在相同元素比较多的情况下,性能要优于快速排序,空间复杂度为O(1)
*
**/
public class FastThreeSort {
public static void main(String[] args) {
int[] a = new int[]{2, 1, 5, 3, 7, 2, 4, 6, 36, 6, 82, 23, 6};
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + "\t");
}
System.out.println("");
System.out.println(">>>>>>>>>>>>>>>");
sort(a, 0, a.length - 1);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + "\t");
}
}
public static void sort(int[] a, int lo, int li) {
//如果lo大于li则推出
if (li <= lo) {
return;
}
//定义两个指针,
/**
* 定义两个指针
* lt表示最左侧小于基准值的元素,但是在下面while里面会在最后一次对lt进行++操作,因此递归的时候就需要在lt-1处开始递归
* gt表示最右侧大于基准值的元素,但是在下面while里面会在最后一次对gt进行--操作,因此递归的时候就需要在gt+1处开始递归
*/
int lt = lo;
int i = lo + 1;
int gt = li;
//基准值还是取最开始的元素
int v = a[lo];
//这个while循环的最大意义在于,如果有比较多的相同的元素,可以将数组切分为小于、等于、大于三个部分,同时保证对于等于部分不会进行递归,从而提升查询效率
while (i <= gt) {
if (a[i] < v) {
//如果是小于基准值,则交换元素
int temp = a[lt];
a[lt] = a[i];
a[i] = temp;
i++;
lt++;
} else if (a[i] > v) {
//如果大于基准值,则交换元素
int temp = a[i];
a[i] = a[gt];
a[gt] = temp;
gt--;
} else {
i++;
}
}
//递归小于部分
sort(a, lo, lt - 1);
//递归大于部分
sort(a, gt + 1, li);
}
}
8、堆排序
待补充
9、记数排序
待补充
10、桶排序
待补充
11、基数排序
待补充
12、参考资料
刷 LeetCode 对于国内 IT 企业面试帮助大吗? 这个里面有程序员小吴的一些动画,有助于理解