排序算法Java实现
排序算法的分类:
- 内部排序,在排序过程中,全部记录放在内存中,称为内部排序;
- 外部排序,在排序过程中需要使用外部存储(磁盘),则称为外部排序。
主要介绍内部排序:
- 插入排序:直接插入排序、二分法插入排序、希尔排序
- 选择排序:简单选择排序、堆排序
- 交换排序:冒泡排序、快速排序
- 归并排序
- 基数排序
插入排序
直接插入排序
- 基本思想:对于给定的一组记录,初始时假设第一个记录自成一个有序序列,其余记录为无序序列。接着从第二个记录开始,按照记录的大小依次将当前处理的记录插入到其之前的有序序列中,直至最后一个记录插入到有序序列中为止。
- 程序示例:
package com.hsx;
/**
* 直接插入排序
*/
public class DirectInsert {
public static void main(String[] args) {
int[] array = {38,65,97,76,13,27,49};
System.out.println("排序之前:");
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
insertSort(array);
System.out.println();
System.out.println("排序之后:");
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
}
/**
* 直接插入排序算法
* @param a
*/
public static int[] insertSort(int[] a) {
if (a != null) {
for (int i = 1; i < a.length; i++) {
// 带插入的元素
int temp = a[i];
int j;
// 从已排序的部分的最后一个元素开始比较
for (j = i - 1; j >= 0; j--) {
// 将大于带插入temp元素向后移
if (a[j] > temp) {
a[j + 1] = a[j];
}
else {
break;
}
}
a[j + 1] = temp;
}
}
}
}
- 分析:
- 稳定排序。
- 当文件的初始状态为有序的时候,每个待排序只需要比较一次就能找到合适的位置,则算法的时间复杂度为O(n);当文件的初始状态为反序的,第i个待插入的元素需要比较i+1次才能找到合适的位置插入,则算法的时间复杂度为O(n2); 直接插入排序的时间复杂度为O(n2)。
- 辅助空间O(1)。
二分法插入排序
- 基本思想:在插入第i个元素时,对前面的第0~i-1个元素(已经是排序的)进行折半,先与他们中间的元素比较,如果小,则对前半部分进行折半,否则对后半部分折半,直到left>right,然后把第i个元素的前一位与目标元素位置之间的所有元素后移,再把第i个元素放在目标位置上。
- 程序示例:
package com.hsx;
/**
* 二分插入排序
*/
public class BinaryInsert {
public static void main(String[] args) {
int[] array = {38,65,97,76,13,27,49};
System.out.println("排序之前:");
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
insertSort(array);
System.out.println();
System.out.println("排序之后:");
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
}
/**
* 二分插入排序算法
* @param a
*/
public static void insertSort(int[] a) {
if (a != null) {
for (int i = 1; i < a.length; i++) {
// 待插入的元素
int temp = a[i];
int right = i - 1;
int left = 0;
int mid = 0;
while (left <= right) {
mid = (left + right) / 2;
if (temp < a[mid]) {
right = mid - 1;
}
else {
left = mid + 1;
}
}
for (int j = i - 1; j >= left; j--) {
a[j + 1] = a[j];
}
if (left != i) {
a[left] = temp;
}
}
}
}
}
- 分析:
- 稳定排序。
- 二分插入排序的比较次数与待排序记录的初始状态无关,仅仅依赖于元素的个数。当n较大时,比直接插入排序的最大比较次数少得多,但是大于直接插入排序的最小比较次数。算法的移动次数与直接插入排序算法的相同,最坏的情况为O(n2/2)->O(n2),最好的情况为O(n),平均移动次数为O(n2)。
希尔排序
- 基本思想:先取一个小于n的整数d1作为第一个增量,把文件的全部元素分组。所有距离为d1的倍数的元素放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2
package com.hsx;
/**
* 希尔排序
*/
public class ShellInsert {
public static void main(String[] args) {
int[] array = {38,65,97,76,13,27,49};
System.out.println("排序之前:");
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
insertSort(array);
System.out.println();
System.out.println("排序之后:");
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
}
/**
* 希尔排序算法
* @param a
*/
public static void insertSort(int[] a) {
int d = a.length;
while (true) {
d = d / 2;
for (int i = 0; i < d; i++) {
for (int j = i + d; j < a.length; j = j + d) {
int temp = a[j];
int k;
for (k = j - d; k >= 0 && a[k] > temp; k = k - d) {
a[k + d] = a[k];
}
a[k + d] = temp;
}
}
if (d == 1) {
break;
}
}
}
}
- 分析:
- 不稳定排序
- 当文件初态基本有序时直接插入排序所需的比较和移动次数均较少;当n值较小时,n和n2的差别也较小,即直接插入排序的最好时间复杂度O(n)和最坏时间复杂度0(n2)差别不大;在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快;希尔排序的平均时间复杂度为O(nlogn)。
- 辅助空间O(1)。
选择排序
简单选择排序
- 基本思想:设所排序序列的元素个数为n。i取1,2,…,n-1,从所有n-i+1个元素(Ri,Ri+1,…,Rn)中找出排序码最小的元素,与第i个元素交换。执行n-1趟 后就完成了记录序列的排序。
- 程序示例:
package com.hsx;
/**
* 简单选择排序
*/
public class SimpleSelect {
public static void main(String[] args) {
int[] array = {38,65,97,76,13,27,49};
System.out.println("排序之前:");
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
selectSort(array);
System.out.println();
System.out.println("排序之后:");
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
}
/**
* 简单选择排序算法
* @param a
*/
public static void selectSort(int[] a) {
if (a != null) {
for (int i = 0; i < a.length; i++) {
int min = a[i];
int n = i; //最小索引数
for (int j = i + 1; j < a.length; j++) {
if(min > a[j]) {
min = a[j];
n = j;
}
}
a[n] = a[i];
a[i] = min;
}
}
}
}
- 分析:
- 不稳定排序。
- 时间复杂度O(n2),最好、最坏、平均都是O(n2)。
- 辅助空间O(1)。
堆排序
- 基本思想:
- 堆的定义:具有n个元素的序列 (h1,h2,…,hn),当且仅当满足(hi>=h2i,hi>=h2i+1)或(hi<=h2i,hi<=h2i+1) (i=1,2,…,n/2)时称之为堆。在这里只讨论满足前者条件的堆。由堆的定义可以看出,堆顶元素(即第一个元素)必为最大项(大顶堆)。完全二 叉树可以很直观地表示堆的结构。堆顶为根,其它为左子树、右子树。
- 思想:初始时把要排序的数的序列看作是一棵顺序存储的二叉树,调整它们的存储序,使之成为一个堆,这时堆的根节点的数最大。然后将根节点与堆的最后一个节点交换。然后对前面(n-1)个数重新调整使之成为堆。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。所以堆排序有两个函数组成。一是建堆的渗透函数,二是反复调用渗透函数实现排序的函数。
- 程序示例:
package com.hsx;
import java.util.Arrays;
/**
* 堆排序
*/
public class HeapSelect {
public static void main(String[] args) {
int[] array = {38,65,97,76,13,27,49};
System.out.println("排序之前:");
System.out.println(Arrays.toString(array));
selectSort(array);
System.out.println("排序之后:");
System.out.println(Arrays.toString(array));
}
/**
* 堆排序
* @param a
*/
public static void selectSort(int[] a) {
if (a != null) {
// 循环建堆
int length = a.length;
for (int i = 0; i < length - 1; i++) {
//建堆
buildMaxHeap(a, length - 1 - i);
//交换
swap(a, 0, length - 1 - i);
}
}
}
/**
* 建堆:建大顶堆
* @param a
* @param lastIndex
*/
public static void buildMaxHeap(int[] a, int lastIndex) {
// 从lastIndex出节点(最后一个节点)的父节点开始
for (int i = (lastIndex - 1) / 2; i >= 0; i--) {
// k保存正在判断的的节点
int k = i;
// 如果当前k节点的字节点存在
while (k * 2 + 1 <= lastIndex) {
// k节点的左节点的索引
int biggerIndex = 2 * k + 1;
// 如果biggerIndex小与lastIndex,即biggerIndex代表的右节点存在
if (biggerIndex < lastIndex) {
// 如果右及诶单的值较大
if (a[biggerIndex] < a[biggerIndex + 1]) {
// biggerIndex总是记录较大子节点的索引
biggerIndex++;
}
}
// 如果k节点的值小于其较大的子节点的值
if (a[k] < a[biggerIndex]) {
// 交换
swap(a, k, biggerIndex);
// 将biggerIndex赋予k,开始while循环的下一次循环,重新保证k节点的值大于其左右节点的值
k = biggerIndex;
}
else {
break;
}
}
}
}
/**
* 交换
* @param a
* @param j
*/
public static void swap(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
- 分析:
- 不稳定排序。
- 时间复杂度O(nlog2n),最好、最坏、平均都是O(nlog2n)。
- 辅助空间O(1)。
- 建堆过程中比较的次数较多,堆不适合用于多数据元素的比较。
交换排序
冒泡排序
- 基本思想:
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
- 程序示例:
package com.hsx;
import java.util.Arrays;
/**
* 冒泡排序
*/
public class BubbleSort {
public static void main(String[] args) {
int[] array = {38,65,97,76,13,27,49};
System.out.println("排序之前:");
System.out.println(Arrays.toString(array));
swapSort(array);
System.out.println("排序之后:");
System.out.println(Arrays.toString(array));
}
/**
* 冒泡排序算法
* @param a
*/
public static void swapSort(int[] a) {
if (a != null) {
// 比较多少次
for (int i = 0; i < a.length; i++) {
// 每两个元素之间进行比较
for (int j = 0; j < a.length - i - 1; j++) {
if (a[j] > a[j + 1]) {
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
}
}
}
- 分析:
- 稳定排序。
- 最好的情况下,文本中元素已经是排序好的,这时的时间复杂度是O(n),最坏的情况下,文本中元素是反序的,这时的时间复杂度是O(n2),平均时间复杂度是O(n2)。
- 空间复杂度O(1)[辅助空间]
快速排序
- 基本思想:设要排序的数组是A[0]……A[n-1],首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。
- 一趟快速排序的算法是:
- 设置两个变量i、j,排序开始的时候:i=0,j=n-1;
- 以第一个数组元素作为关键数据,赋值给key,即key=A[0];
- 从j开始向前搜索,即由后开始向前搜索(j–),找到第一个小于key的值A[j],将A[j]和A[i]互换;
- 从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]互换;
- 重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。
- 一趟快速排序的算法是:
- 程序示例:
package com.hsx;
import java.util.Arrays;
/**
* 快速排序
*/
public class QuickSort {
public static void main(String[] args) {
int[] array = {38,65,97,76,13,27,49};
System.out.println("排序之前:");
System.out.println(Arrays.toString(array));
swapSort(array);
System.out.println("排序之后:");
System.out.println(Arrays.toString(array));
}
/**
* 快速排序算法
* @param a
*/
public static void swapSort(int[] a) {
if (a != null) {
quickSort(a, 0, a.length - 1);
}
}
/**
* 递归调用
* @param a
* @param low
* @param high
*/
public static void quickSort(int[] a, int low, int high) {
if (low < high) {
int mid = getMid(a, low, high);
quickSort(a, low, mid - 1);
quickSort(a, mid + 1, high);
}
}
public static int getMid(int[] a, int low, int high) {
int key = a[low];
while (low < high) {
// 找到比关键字小的元素位置
while (low < high && a[high] >= key) {
high--;
}
a[low] = a[high];
while (low < high && a[low] <= key) {
low++;
}
a[high] = a[low];
}
a[low] = key;
return low;
}
}
- 分析:
- 不稳定排序。
- 最坏的情况下,每次区间的划分的结果都是关键字的左边(或右边)序列为空,而另一边区间中的元素仅比排序前少了一项,即选择的关键字是待排序中的所有元素中的最小或者最大的,这时的时间复杂度是O(n2);最好的情况下,每次区间的划分的结果都是关键字左右两边的元素的长度相等或者相差为1,即选择的关键字为待排序的元素中的中间值,此时的时间复杂度为O(nlog2n);快速排序的平均时间复杂度是O(nlog2n)。
- 空间复杂度是O(nlog2n)
归并排序
- 基本思想:将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
- 归并过程: 比较a[i]和a[j]的大小,若a[i]≤a[j],则将第一个有序表中的元素a[i]复制到r[k]中,并令i和k分别加上1;否则将第二个有序表中的元素a[j]复制到r[k]中,并令j和k分别加上1,如此循环下去,直到其中一个有序表取完,然后再将另一个有序表中剩余的元素复制到r中从下标k到下标t的单元。归并排序的算法我们通常用递归实现,先把待排序区间[s,t]以中点二分,接着把左边子区间排序,再把右边子区间排序,最后把左区间和右区间用一次归并操作合并成有序的区间[s,t]。
- 归并原理:
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- 重复步骤3直到某一指针超出序列尾,将另一序列剩下的所有元素直接复制到合并序列尾。
- 程序示例:
package com.hsx;
import java.util.Arrays;
/**
* 归并排序
*/
public class MergerSort {
public static void main(String[] args) {
int[] array = {38,65,97,76,13,27,49};
System.out.println("排序之前:");
System.out.println(Arrays.toString(array));
mergerSort(array, 0, array.length - 1);
System.out.println("排序之后:");
System.out.println(Arrays.toString(array));
}
/**
* 归并排序算法
* @param a
*/
public static void mergerSort(int[] a, int left, int right) {
if (a != null) {
if (left < right) {
int mid = (left + right) / 2;
mergerSort(a, left, mid);
mergerSort(a, mid + 1, right);
merger(a, left, mid, right);
}
}
}
public static void merger(int[] a, int left, int mid, int right) {
int[] tempA = new int[a.length];
int middle = mid + 1;
int temp = left;
int third = left;
while (left <= mid && middle <= right) {
// 从两个数字中选取较小的元素放入到中间数组
if (a[left] <= a[middle]) {
tempA[third++] = a[left++];
}
else {
tempA[third++] = a[middle++];
}
}
// 将剩余的部分放入到中间数组
while (left <= mid) {
tempA[third++] = a[left++];
}
while (middle <= right) {
tempA[third++] = a[middle++];
}
// 将中间数组复制到原数组
while (temp <= right) {
a[temp] = tempA[temp++];
}
}
}
- 分析:
- 稳定排序。
- 最好、最坏、平均时间复杂度都是O(nlog2n).
- 空间复杂度是O(1)。
基数排序
- 基本思想:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
- 程序示例:
package com.hsx;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 基数排序(桶排序)
*/
public class BucketSort {
public static void main(String[] args) {
int[] array = {38,65,97,76,13,27,49};
System.out.println("排序之前:");
System.out.println(Arrays.toString(array));
bucketSort(array);
System.out.println("排序之后:");
System.out.println(Arrays.toString(array));
}
/**
* 基数排序算法
* @param a
*/
public static void bucketSort(int[] a) {
//找到最大数,确定要排序几趟
int max = 0;
for (int i = 0; i < a.length; i++) {
if(max<a[i]){
max = a[i];
}
}
//判断位数
int times = 0;
while(max>0){
max = max/10;
times++;
}
//建立十个队列
List<ArrayList> queue = new ArrayList<ArrayList>();
for (int i = 0; i < 10; i++) {
ArrayList queue1 = new ArrayList();
queue.add(queue1);
}
//进行times次分配和收集
for (int i = 0; i < times; i++) {
//分配
for (int j = 0; j < a.length; j++) {
int x = a[j]%(int)Math.pow(10, i+1)/(int)Math.pow(10, i);
ArrayList queue2 = queue.get(x);
queue2.add(a[j]);
queue.set(x,queue2);
}
//收集
int count = 0;
for (int j = 0; j < 10; j++) {
while(queue.get(j).size()>0){
ArrayList<Integer> queue3 = queue.get(j);
a[count] = queue3.get(0);
queue3.remove(0);
count++;
}
}
}
}
}
- 分析:
- 稳定排序。
- 分配需要O(n),收集为O(r),其中r为分配后链表的个数,以r=10为例,则有0~9这样10个链表来将原来的序列分类。而d,也就是位数(如最大的数是1234,位数是4,则d=4),即”分配-收集”的趟数。因此时间复杂度为O(d*(n+r))。
- 适用情况:如果有一个序列,知道数的范围(比如1~1000),用快速排序或者堆排序,需要O(N*logN),但是如果采用基数排序,则可以达到O(4*(n+10))=O(n)的时间复杂度。算是这种情况下排序最快的。
- 空间复杂度O(rd+n)。