前言

简单归纳一下最近学习的排序算法,如果有什么错误的地方还请大家指教。

本文介绍了七种经典排序算法,包括冒泡排序,选择排序,插入排序,希尔排序,归并排序,快速排序以及堆排序,并且讨论了各种算法的进一步改进,在文章最后还对所有算法的时间和空间复杂度作了一个总结。

用Java语言可以很简洁优雅的实现各种排序算法,我们在写排序算法的时候可以下面这种模板:

public class Example {
public static void sort(Comparable[] a) {
//具体算法
}
//判断v是否小于w
public static boolean less(Comparable[] v, Comparable[] W) {
return v.compareTo(w) < 0;
}
//交换
public static void exch(Comparable[] a, int i, int j) {
Comparable temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}

此种模板适合的是实现了Comparable接口的数据类型,如String, Integer, Date 或是自己创建的实现了compareTo()方法的数据类型。此种模板实际排序的是对象的引用,然而对于没有实现该接口的类型或是一些原始数据类型则不适用(可以利用Comparator比较器来实现)。

为了简洁易懂,下面以int型数据为例来书写代码,不使用less()方法,但仍使用exch()方法来表示交换值的过程。

一、冒泡排序

冒泡排序的思想理解起来很简单,就是依次比较相邻的元素,将大的元素往后移,小的往前移。因此,数列中最大的元素总是随着交换不断浮到顶端,于是有了冒泡排序这个名字。

冒泡排序会经历 n-1 次循环(n 为数组长度),第一次循环将最大的数置于最末端,第二次将第二大的数置于倒数第二的位置,每一次循环待排序的数列长度逐渐减小,直至排序完成。

代码:

public class BubbleSort {
public static void sort(int[] a) {
for (int i = a.length - 1; i > 0; i--)
for (int j = 0; j < i; j++)
if (a[j] > a[j + 1])
exch(a, j, j + 1); //交换相邻元素
}

改进:如果在某一次循环中,发现没有进行任何交换,这说明已经完成了排序,即可直接退出。代码如下:

//改进版本
public class BubbleSort {
public static void sort(int[] a) {
for (int i = a.length - 1; i > 0; i--) {
boolean didExch = false; //利用一个变量监控是否交换
for (int j = 0; j < i; j++) {
if (a[j] > a[j + 1]) {
exch(a, j, j + 1);
didExch = true;
}
}
if (didExch == false) //如果没有发生交换,直接返回
return;
}
}

分析:对于改进后的版本,在最坏情况下所需的比较次数为(n - 1) + (n - 2) + .. + 1 = ( n-1) * n /2
~ n^2 /2,时间复杂度为O(n^2),而最好情况下(即已经是有序数列),只需进行n
- 1次比较和0次交换,时间复杂度为O(n)。

二、选择排序

选择排序的思想是:首先找到数组中最小的元素,将它和数组第一个元素交换位置,其次,在剩下元素中找到最小元素,和数组中第二个元素交换位置,以此类推,直至整个数组排序完毕。这种排序方法不断地选择剩余元素中的最小者,故得名选择排序。

代码:

public class Selection {
public static void sort(int[] a) {
for (int i = 0; i < a.length; i++) {
int min = i;
//找到最小值,并把下标记为 min
for (int j = i + 1; j < a.length; j++) {
if (a[j] < a[min])
min = j;
}
exch(a, i, min); //交换 a[i] 和 a[min]
}
}

分析:对于长度为n的数组,选择排序需要(n - 1)*n/2 ~ n^2 /2次比较和n次交换,时间复杂度为O(n^2)。

三、插入排序

插入排序和人们打牌时整理牌的思想是一致的,将每一张牌插入到其他已经有序的牌中的适当位置。在每一次循环当中,当前索引左边的元素都是有序的,直到当前索引移动到最右端,数组完成排序。

排序的实现有两种方法,第一种是通过不断交换顺序颠倒的相邻元素来找到合适位置:

public class Insertion {
public static void sort(int[] a) {
for (int i = 1; i < a.length; i++)
for (int j = i; j > 0 && a[j] < a[j - 1]; j--)
exch(a, j, j - 1); //交换顺序颠倒的元素
}

另一种是通过在内循环中将较大元素往右移动来找到插入位置,这种方法相比上面的方法访问数组的次数减少了一半

public class Insertion {
public static void sort(int[] a) {
for (int i = 1; i < a.length; i++) {
int j; //注意 j 的声明要在内循环的外面
int temp = a[i]; //保存 a[i] 的值
for (j = i; j > 0 && temp < a[j - 1]; j--) {
a[j] = a[j - 1]; //后移较大的元素
}
a[j] = temp; //把原来的 a[i] 值插入空位中
}
}

分析:以方法一为例,插入排序对于长度为n的数组,最坏情况下需要~n^2 /2次比较和~n^2
/2次交换,时间复杂度为O(n^2),最好情况下需要n-1次比较和0次交换,时间复杂度为O(n)。实际上,插入排序的需要的交换与数组中倒置元素的对数一致,因此插入排序对于规模不大,部分有序的数组排序十分有效。

四、希尔排序

希尔排序是一种基于插入排序的快速改进方法,利用的是插入排序处理部分有序数组有很好效果的特点。希尔排序处理不相邻的元素对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。

希尔排序的思想是使数组中间隔为h的元素都是有序的,这样的数组称为h有序数组。下图则是一个4-有序数组:

下面使用一个递增序列 h = 3 * h + 1
来实现希尔排序,在 n = 16
的例子中,h 可以取 1,4
和 13:

代码:

public class Shell {
public static void sort(int[] a) {
int N = a.length;
int h = 1;
while (h < N / 3)
h = 3 * h + 1; //h的递增序列
while (h >= 1) {
for (int i = h; i < N; i++) {
int j; //注意 j 的声明要在内循环的外面
int temp = a[i]; //保存 a[i] 的值
for (j = i; j >= h && temp < a[j - h]; j -= h) {
a[j] = a[j - h]; //后移较大的元素
}
a[j] = temp; //把原来的 a[i] 值插入空位中
}
h = h / 3;
}
}

分析:希尔排序的性能取决于h的递增序列,上面代码中所用的递增序列并不是最优秀的,但最坏情况下的运行时间仍少于平方级别,算法的时间复杂度为O(n^(2/3))。

五、归并排序

归并排序的思想是利用递归先将数组分成两半分别排序,再归并起来。

我们首先需要实现的是归并,最直接的办法就是将两个不同的有序数组归并到第三个数组中,可以通过不断比较将两个输入数组中的元素一个个按照顺序放入这个数组。

归并方法:先将所有元素赋值到辅助数组aux[]中,再归并回a[]中;

public static void merge(int[] a, int lo, int mid, int hi) {
int i = lo;
int j = mid + 1; for (int k = lo; k <= hi; k++)
aux[k] = a[k]; for (int k = lo; k <= hi; k++) {
if (i > mid) a[k] = aux[j++];
else if (j > hi) a[k] = aux[i++];
else if (aux[j] < aux[i]) a[k] = aux[j++];
else a[k] = aux[i++];
}
}

自顶向下的排序:

public class Merge {
private static int[] aux; //静态数组 public static void sort(int[] a) {
aux = new int[a.length]; //分配空间
sort(a, 0, a.length - 1);
} private static void sort(int[] a, int lo, int hi) {
if (hi <= lo) return;
int mid = (lo + hi) / 2;
sort(a, lo, mid);
sort(a, mid + 1, hi);
merge(a, lo, mid, hi);
public static void sort(int[] a) {
int[] aux = new int[a.length]; //创建辅助数组分配空间
sort(a, aux, 0, a.length - 1);
}
}

自底向下的排序:

private static void sort(int[] a) {
int N = a.length;
aux = new int[N]; for (int sz = 1; sz < N; sz = sz + sz) {
for (int lo = 0; lo < N - sz; lo += sz + sz) {
merge(a, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1));
}
}
}

改进:归并排序在以上基础上还可以有很多改进空间

改进1:将aux[]辅助数组作为参数传递给递归的sort()方法 (作为类的私有静态变量 和 放入归并方法中都不妥当);

public static void sort(int[] a) {
int[] aux = new int[a.length]; //创建辅助数组分配空间
sort(a, aux, 0, a.length - 1);
}

改进2:添加判断条件,如果a[mid] <= a[mid + 1],此时数组已经有序,就可以跳过merge(),此时处理已经有序的数组

        的运行时间变为线性;

改进3:用插入排序处理小规模的子数组:

if (hi - lo + 1 <= M) {				//长度小于等于M的子数组用插入排序
InsertSort(a, lo, hi);
return;
}

改进4:快速归并,按降序将a[]的后半部分复制到aux[],然后从两边向中间归并,这样可以省去判断半边是否用尽,

             但这样的排序是不稳定的;

public static void merge(int[] a, int[] aux, int lo, int mid, int hi) {
//升序复制前一半
for (int k = lo; k <= mid; k++)
aux[k] = a[k];
//降序复制后一半
for (int k = mid + 1, j = hi; k <= hi; k++, j--)
aux[k] = a[j]; int i = lo, j = hi;
for (int k = lo; k <= hi; k++) {
if (aux[j] < aux[i]) a[k] = aux[j--];
else a[k] = aux[i++];
}
}

分析:对于长度为n的数组,归并排序需要~ nlgn次比较,时间复杂度为O(nlgn)。(注:这里lgn表示以2为底的n的对数)

六、快速排序

快速排序是一种分治的排序算法,主要思想是利用切分,切分点左边的元素小于等于切分元素,切分点右边的元素大于等于切分元素,当左右两边的子数组有序时即完成排序,利用递归可以很好的将数组排序。

代码:

public class Quick {
public static void sort(int[] a) {
StdRandom.shuffle(a); //打乱数组
sort(a, 0, a.length - 1);
} private static void sort(int[] a, int lo, int hi) {
if (hi <= lo) return;
int j = partition(a, lo, hi);
sort(a, lo, j - 1);
sort(a, j + 1, hi);
} private static int partition(int[] a, int lo, int hi) {
int i = lo, j = hi + 1;
int v = a[lo];
while (true) {
while (a[++i] < v) if (i == hi) break;
while (v < a[--j]);
if (j <= i) break;
exch(a, i, j);
}
exch(a, lo, j);
return j;
}
}

改进:同样,快速排序也有很大改进空间

改进1:对于小数组切换到插入排序,5 ~ 15 之间的任意值即可;

改进2:对于含有大量重复元素的数组,使用三向切分;

	//三向切分
private static void sort(int[] a, int lo, int hi) {
//对于小数组用插入排序
if (hi <= lo + 5) {InsertSort(a, lo, hi); return;} int lt = lo, i = lo + 1, gt = hi;
int v = a[lo];
while (i <= gt) {
if (a[i] < v) exch(a, lt++, i++);
else if (v < a[i]) exch(a, i, gt--);
else i++;
}
sort(a, lo, lt - 1);
sort(a, gt + 1, hi);
} //供小规模数组使用的插入排序
public static void InsertSort(int[] a, int lo, int hi) {
for (int i = lo; i <= hi; i++) {
int j; //注意 j 的声明要在内循环的外面
int temp = a[i]; //保存 a[i] 的值
for (j = i; j > lo && temp < a[j - 1]; j--) {
a[j] = a[j - 1]; //后移较大的元素
}
a[j] = temp; //把原来的 a[i] 值插入空位中
}
}

分析:对于长度为n的数组,归并排序平均需要~ 1.39nlgn次比较,最好情况下时间复杂度为O(nlgn),而最坏情况下(有序)时间复杂度为O(n^2)。

七、堆排序

先介绍堆的定义(这里以最大堆举例),在二叉堆的数组中,每个元素都要保证大于另两个特点位置的元素。在堆有序的二叉树中,每个节点都小于等于它的父节点(存在的话)。可以用长度为N+1的私有数组来表示一个大小为N的堆,不使用pq[0],堆元素放在pq[1]至pq[N]中。

当一个节点太大时,需要上浮到堆的更高层:

private void swim(int k) {
while (k > 1 && less(k / 2, k)) {
exch(k / 2, k);
k /= 2;
}
}

当一个节点太小,需要下沉到更低层时:

private void sink(int k) {
while (k * 2 <= N) {
int j = 2 * k;
if (j < N && less(j, j + 1)) j++; //j表示两个子节点里较大的那个
if (!less(k, j)) break; //注意退出循环
exch(k, j);
k = j;
}
}

堆排序的代码如下,接收一个数组,先将其转化为堆有序,然后每一次把最大的元素下沉到数组最后(原有的less()和exch()方法将a[1]~a[N]的元素排序,将这两种方法的索引减一即可得到a[0]~a[N-1]的排序)

public class Heap {
public static void sort(int[] a) {
int N = a.length;
//堆有序
for (int k = N / 2; k >= 1; k--)
sink(a, k, N); //堆排序
while (N > 1) {
exch(a, 1, N--);
sink(a, 1, N);
}
} private static void sink(int[] a, int k, int N) {
while (k * 2 <= N) {
int j = k * 2;
if (j < N && less(a, j, j + 1)) j++;
if (!less(a, k, j)) break;
exch(a, k, j);
k = j;
}
} private static void exch(int[] a, int i, int j) {
int temp = a[i - 1];
a[i - 1] = a[j - 1];
a[j - 1] = temp;
} private static boolean less(int[] a, int i, int j) {
return a[i - 1] < a[j - 1];
}
}

分析:对于长度为n的数组,堆排序只需要最多 2nlgn+2n次比较,时间复杂度为O(nlgn)。

排序算法的总结——Java实现的更多相关文章

  1. 排序算法总结(基于Java实现)

    前言 下面会讲到一些简单的排序算法(均基于java实现),并给出实现和效率分析. 使用的基类如下: 注意:抽象函数应为public的,我就不改代码了 public abstract class Sor ...

  2. 常见排序算法题(java版)

    常见排序算法题(java版) //插入排序:   package org.rut.util.algorithm.support;   import org.rut.util.algorithm.Sor ...

  3. 八大排序算法总结与java实现(转)

    八大排序算法总结与Java实现 原文链接: 八大排序算法总结与java实现 - iTimeTraveler 概述 直接插入排序 希尔排序 简单选择排序 堆排序 冒泡排序 快速排序 归并排序 基数排序 ...

  4. 第二章:排序算法 及其他 Java代码实现

    目录 第二章:排序算法 及其他 Java代码实现 插入排序 归并排序 选择排序算法 冒泡排序 查找算法 习题 2.3.7 第二章:排序算法 及其他 Java代码实现 --算法导论(Introducti ...

  5. 排序算法总结及Java实现

    1. 整体介绍 分类 排序大的分类可以分为两种,内排序和外排序.在排序过程中,全部记录存放在内存,则称为内排序,如果排序过程中需要使用外存,则称为外排序.主要需要理解的都是内排序算法: 内排序可以分为 ...

  6. 常见排序算法总结(java版)

    一.冒泡排序 1.原理:相邻元素两两比较,大的往后放.第一次完毕,最大值在最大索引处. 即使用相邻的两个元素一次比价,依次将最大的数放到最后. 2.代码: public static void bub ...

  7. 排序算法之冒泡排序Java实现

    排序算法之冒泡排序 舞蹈演示排序: 冒泡排序: http://t.cn/hrf58M 希尔排序:http://t.cn/hrosvb  选择排序:http://t.cn/hros6e  插入排序:ht ...

  8. 动画展现十大经典排序算法(附Java代码)

    0.算法概述 0.1 算法分类 十种常见排序算法可以分为两大类: 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序. 非比较类排序: ...

  9. 排序算法大汇总 Java实现

    一.插入类算法 排序算法的稳定性:两个大小相等的元素排序前后的相对位置不变.{31,32,2} 排序后{2,31,32},则称排序算法稳定 通用类: public class Common { pub ...

  10. 排序算法代码实现-Java

    前言 为了准备面试,从2月开始将排序算法认认真真得刷了一遍,通过看书看视频,实践打代码,还有一部分的leetcode题,自己感觉也有点进步,将笔记记录总结发出来. 冒泡排序 该排序就是一种像泡泡浮到水 ...

随机推荐

  1. 如何在Spring boot中修改默认端口

    文章目录 介绍 使用Property文件 在程序中指定 使用命令行参数 值生效的顺序 如何在Spring boot中修改默认端口 介绍 Spring boot为应用程序提供了很多属性的默认值.但是有时 ...

  2. Vue Cli 3 打包上线 静态资源404问题解决方案

    报错原因:静态资源丢失 解决方案 官方文档https://cli.vuejs.org/zh/config/#vue-config-js baseUrl 从 Vue CLI 3.3 起已弃用,请使用pu ...

  3. Rancher流水线配置文档

    2019独角兽企业重金招聘Python工程师标准>>> 一.概述 Rancher流水线从逻辑上可以分为两部分,即CI和CD. CI,可分化为克隆代码.代码打包.发布镜像三部分. CD ...

  4. web前端项目中遇到的一些问题总结(08.23更新)

    个人网站 https://iiter.cn 程序员导航站 开业啦,欢迎各位观众姥爷赏脸参观,如有意见或建议希望能够不吝赐教! 写一些最近工作中Vue项目中遇到的问题. 巴啦啦小魔仙,污卡拉,全身变,小 ...

  5. Ubuntu 14.04 配置samba

    Ubuntu 14.04 配置samba: 安装略 # vi /etc/samba/smb.conf security = user  (在[global]下任意添加) [share] path = ...

  6. Image Filter and Recover

    这是CS50的第四次大作业,顺便学习了图像的入门知识. 基础 黑白图(bitmap)的每个像素点只能取值0/1,1代表白色,0代表黑色. 常见的图片格式有JPEG/PNG/BMP,这些格式都支持RGB ...

  7. shell字符串索引

    shell中的字符串索引一会从0开始,一会从1开始,见例子: #!/bin/bash string="hello world" length=${#string} echo &qu ...

  8. C. Barcode dp

    https://codeforces.com/problemset/problem/225/C 这个题目和之前一个题目很像 https://www.cnblogs.com/EchoZQN/p/1090 ...

  9. Scrapy - Request 中的回调函数callback不执行

    回调函数callback不执行 大概率是被过滤了 两种方法: 在 allowed_domains 中加入目标url 在 scrapy.Request() 函数中将参数 dont_filter=True ...

  10. CC2530串口通信

    任何USART双向通信至少需要两个脚:接收数据输入(RX)和发送数据输出(TX). RX:接收数据串行输入.通过采样技术来区别数据和噪音,从而恢复数据. TX :发送数据输出.当发送器被禁止时,输出引 ...