按类型分,可以分成以下几种:
(1)插入排序:直接插入排序
(2)交换排序:冒泡排序、快速排序
(3)选择排序:选择排序、堆排序
(4)归并排序:归并排序
1.直接插入排序
思想:类似人们整理桥牌的方法,将每一张牌插入到其他已经有序的牌中的适当位置。在计算机的实现中,为了要给插入的元素腾出空间,我们需要将其余元素在插入之前都向右移动一位。
代码实现:
// 插入排序
public static Comparable[] insertionSort(Comparable[] a) {
for (int i = 1; i < a.length; i++) {
// 将a[i]插入到a[i-1]、a[i-2]、a[i-3]...之中
for (int j = i; j > 0 && a[j].compareTo(a[j - 1]) < 0; j--) {
Comparable temp = a[j];
a[j] = a[j - 1];
a[j - 1] = temp;
}
}
return a;
}
插入排序所需的时间取决于输入中元素的初始顺序。这里使用的是Comparable,所以适用于任何实现了Comparable接口的数据类型。例如,Java中封装数字的类型Integer和Double,以及String和其他许多高级数据类型(如File和URL)都实现了Comparable接口。
2.冒泡排序
思想:在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。
举个栗子:680 307 32 155
经过第一次排序后:307 680 32 155 --> 307 32 680 155 --> 307 32 155 680
代码实现:
// 冒泡排序
public static Comparable[] bubbleSort(Comparable[] a) {
for (int i = 0; i < a.length - 1; i++) {
for (int j = 0; j < a.length - 1 - i; j++) {
// 这里-i主要是每遍历一次都把最大的i个数沉到最底下去了,没有必要再替换了
if (a[j].compareTo(a[j + 1]) > 0) {
Comparable temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
return a;
}
3.快速排序
思想:快速排序是一种分治的排序算法。它将一个数组分成两个子数组,将两部分独立地排序。选择一个基准元素,通常选择第一个元素或者最后一个元素,通过一趟扫描,将待排序列分成两部分,一部分比基准元素小,一部分大于等于基准元素,此时基准元素在其排好序后的正确位置,然后再用同样的方法递归地排序划分的两部分。
代码实现:
// 快速排序
public static Comparable[] quickSort(Comparable[] a, int low, int high) {
if (high > low) {//如果不加这个判断递归会无法退出导致堆栈溢出异常
int dp = partition(a, low, high);// 切分,将数组一分为二
quickSort(a, low, dp - 1);// 将左半部分a[low...dp-1]排序
quickSort(a, dp + 1, high);// 将右半部分a[dp+1...high]排序
}
return a;
}
private static int partition(Comparable[] a, int low, int high) {
Comparable temp = a[low];// 将数组的第一位当做基准元素
while (low < high) {
// 找到比基准元素小的位置
while (low < high && a[high].compareTo(temp) > 0) {
high--;
}
a[low] = a[high];// 将比基准元素小的移到低端
// 找到比基准元素大或等于的位置
while (low < high && a[low].compareTo(temp) <= 0) {
low++;
}
a[high] = a[low];// 将比基准元素大的移到高端
}
a[low] = temp;
return low;
}
快速排序可能是应用最广泛的排序算法了。快速排序流行的原因是它实现简单、适应于各种不同的输入数据且在一般应用中比其他排序算法都要快得多。快速排序引人注目的特点包括它是原地排序(只需要一个很小的辅助栈),且将长度为N的数组排序所需要的时间和NlgN成正比。
4.选择排序
思想:首先找到数组中最小的那个元素,其次,将它和数组的第一个元素交换位置(如果第一个元素就是最小元素那么就和自己交换)。再次,在剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。如此反复,不断地选择剩余元素之中的最小者,直到将整个数组排序。
代码实现:
// 选择排序
public static Comparable[] selectionSort(Comparable[] a) {
for (int i = 0; i < a.length; i++) {
int min = i;// 保存最小元素的索引
// 找到第i+1次搜索最小元素的索引
for (int j = i + 1; j < a.length; j++) {
if (a[min].compareTo(a[j]) > 0) {
min = j;
}
}
// 将a[i]与a[i+1...N]中最小的元素交换
Comparable temp = a[min];
a[min] = a[i];
a[i] = temp;
}
return a;
}
选择排序有两个鲜明的特点:1、运行时间与输入无关。为了找出最小的元素而扫描一遍数组并不能为下一遍扫描提供什么信息。这种性质在某些情况下也是缺点,因为你会发现一个已经有序的数组或者主键全部相等的数组和一个元素随机排列的数组所用的排序时间竟然一样长!2、数据移动是最少的。每次交换都会改变两个数组元素的值,因此用了N次交换(交换次数和数组大小是线性关系)。
5.堆排序
堆的定义:具有n个元素的序列(h1,h2,…,hn),当且仅当满足(hi>=h2i,hi>=2i+1)或(hi<=h2i,hi<=2i+1) (i=1,2,…,n/2)时称之为堆。在这里只讨论满足前者条件的堆。由堆的定义可以看出,堆顶元素(即第一个元素)必为最大项(大顶堆)。完全二叉树(树里面除了最后一层其他都是满的)可以很直观地表示堆的结构。堆顶为根,其它为左子树、右子树。思想:堆排序是一种树形选择排序。那么它就需要计算一个节点的父节点位置,和一个节点的左孩子和右孩子节点的位置,其公式分别为:父=(i-1)/2,左=2i+1,右=2i+2(i从0开始)。初始时把要排序的数的序列看作是一棵顺序存储的二叉树,调整它们的存储序,使之成为一个堆,这时堆的根节点的数最大。然后将根节点与堆的最后一个节点交换。然后对前面(n-1)个数重新调整使之成为堆。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。所以堆排序有两个函数组成。一是建堆的渗透函数,二是反复调用渗透函数实现排序的函数。
// 堆排序
public static Comparable[] heapSort(Comparable[] a) {
for (int i = 0; i < a.length - 1; i++) {
buildMaxHeap(a, a.length - 1 - i);// 建立大顶堆
swap(a, 0, a.length - 1 - i);// 交换堆顶与堆的最后一个元素位置
}
return a;
}
private static void buildMaxHeap(Comparable[] a, int lastIndex) {
// 从lastIndex节点的父节点开始建堆
for (int i = (lastIndex - 1) / 2; i >= 0; i--) {
int k = i; // 保存正在判断的节点
// 为每个节点建立大顶堆,只要这个根节点还有子节点
while ((2 * k + 1) <= lastIndex) {
int biggerIndex = 2 * k + 1; // 假设左节点的值时最大的
if (biggerIndex < lastIndex)// 判断是否有右节点存在
{
// 选出子节点中最大的值
if (a[biggerIndex].compareTo(a[biggerIndex + 1]) < 0) {
biggerIndex++;
}
}
// 将跟节点与子节点进行比较
if (a[k].compareTo(a[biggerIndex]) < 0) {
swap(a, k, biggerIndex);
k = biggerIndex;
} else {
break;
}
}
}
}
private static void swap(Comparable[] a, int i, int j) {
Comparable temp = a[i];
a[i] = a[j];
a[j] = temp;
}
6.归并排序
思想:归并排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
代码实现:
// 归并排序
private static Comparable[] aux;// 归并所需的辅助数组,声明为全局,避免每次都创建一个新的数组
public static Comparable[] mergeSort(Comparable[] a, int low, int high) {
aux = new Comparable[a.length];// 一次性分配空间
sort(a, low, high);
return a;
}
private static void sort(Comparable[] a, int low, int high) {
if (high > low) {
int mid = low + (high - low) / 2;
mergeSort(a, low, mid);// 将左半边排序
mergeSort(a, mid + 1, high);// 将右半边排序
merge(a, low, mid, high);// 归并结果
}
}
private static void merge(Comparable[] a, int low, int mid, int high) {
// 将a[low...mid]和a[mid+1...high]归并
int i = low, j = mid + 1;
for (int k = low; k <= high; k++) {// 将a[low...high]复制到aux[low...high]
aux[k] = a[k];
}
for (int k = low; k <= high; k++) {// 归并到a[low...high]
if (i > mid) { //左半边元素没了,直接取右半边元素归并到a[]
a[k] = aux[j++];
} else if (j > high) {//右半边元素没了,直接取左半边元素归并到a[]
a[k] = aux[i++];
} else if (aux[i].compareTo(aux[j]) > 0) {//左半边元素大于右半边元素,将右半边元素归并到a[]
a[k] = aux[j++];
} else {//左半边元素小于等于右半边元素,将左半边元素归并到a[]
a[k] = aux[i++];
}
}
}
归并排序最吸引人的性质是它能够保证将任意长度为N的数组排序所需时间和NlogN成正比。主要缺点是它所需的额外空间和N成正比。
7.性能特点比较
排序法 | 平均时间 | 最坏情况 | 最好情况 | 稳定度 | 额外空间 | 备注 |
直接插入排序 | O(n²) | O(n²) | O(n) | 稳定 | O(1) | 大部分已排序时较好(简单) |
冒泡排序 | O(n²) | O(n²) | O(n) | 稳定 | O(1) | n小时较好(较复杂) |
快速排序 | O(nlogn) | O(n²) | O(nlogn) | 不稳定 | O(logn) | n大时较好,基本有序时反而不好(较复杂) |
选择排序 | O(n²) | O(n²) | O(n²) | 不稳定 | O(1) | n小时较好(简单) |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | 不稳定 | O(1) | n大时较好(较复杂) |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | 稳定 | O(n) | n大时较好(较复杂) |