排序分类:
内部排序(只使用内存):
1、插入排序:直接插入排序,二分法插入排序,希尔排序;
2、选择排序:简单选择排序,堆排序;
3、交换排序:冒泡排序,快速排序;
4、归并排序;
5、基数排序;
外部排序(内存和外存结合使用)
简单分析
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
简单选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
直接插入排序 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
希尔排序 | O(nlogn)~O(n^2) | O(n^1.3) | O(n^2) | O(1) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
快速排序 | O(nlogn) | O(nlogn) | O(n^2) | O(logn)~O(n) | 不稳定 |
基数排序 | O(n) | 稳定 |
算法一:插入排序——直接插入排序(稳定)
插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
算法步骤:设数组为a[0…n]。
1. 将原序列分成有序区和无序区。a[0…i-1]为有序区,a[i…n] 为无序区。(i从1开始)
2. 从无序区中取出第一个元素,即a[i],在有序区序列中从后向前扫描。
3. 如果有序元素大于a[i],将有序元素后移到下一位置。
4. 重复步骤3,直到找到小于或者等于a[i]的有序元素,将a[i]插入到该有序元素的下一位置中。
5. 重复步骤2~4,直
时间复杂度:平均时间复杂度为O(n^2)。
//插入排序算法 稳定
public class InsertSort {
public static void main(String[] args) {
int[] a={49,38,65,97,76,13,27,49,78,34,12,64,1};
System.out.println("排序之前:");
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+" ");
}
//直接插入排序
for (int i = 1; i < a.length; i++) {
//待插入元素
int temp = a[i];
int j;
/*for (j = i-1; j>=0 && a[j]>temp; j--) {
//将大于temp的往后移动一位
a[j+1] = a[j];
}*/
for (j = i-1; j>=0; j--) {
//将大于temp的往后移动一位
if(a[j]>temp){
a[j+1] = a[j];
}else{
break;
}
}
a[j+1] = temp;
}
System.out.println();
System.out.println("排序之后:");
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+" ");
}
}
}
直接插入排序复杂度分析
- 最好情况下:在本身有序的情况下,共比较了n-1次,没有移动的记录,时间复杂度为O(n)。
- 最坏情况下:在逆序情况下:比较了 n(n-1)/2次,而移动次数为(n+2)(n-2)/2次。
- 平均比较 移动 次数:n^2/4,故直接插入排序的时间复杂度为O(n^2)。
- 直接插入排序法比冒泡和简单选择排序的性能要好一些。
算法二:插入排序——二分法插入排序(稳定)
原理:是直接插入排序的一个变种,区别是:在有序区中查找新元素插入位置时,为了减少元素比较次数提高效率,采用二分查找算法进行插入位置的确定。
具体如下(实现为升序):
设数组为a[0…n]。
1. 将原序列分成有序区和无序区。a[0…i-1]为有序区,a[i…n] 为无序区。(i从1开始)
2. 从无序区中取出第一个元素,即a[i],使用二分查找算法在有序区中查找要插入的位置索引j。
3. 将a[j]到a[i-1]的元素后移,并将a[i]赋值给a[j]。
4. 重复步骤2~3,直到无序区元素为0。
时间复杂度:平均时间复杂度为O(n^2)。
/*
* 二分插入排序
*/
static void binaryInsertSort(int []array){
int left,right,mid,temp;
for(int i=0;i<array.length;i++){
temp=array[i];
left=0;
right=i-1;
mid=0;
while(left<=right){
mid=(left+right)/2;
if(temp<array[mid]){
right=mid-1;
}else {
left = mid+1;
}
}
for(int j=i-1;j>=left;j--){
array[j+1]=array[j];
}
array[left]=temp;
}
}
算法三:插入排序——希尔排序(不稳定)
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时, 效率高, 即可以达到线性排序的效率
- 但插入排序一般来说是低效的, 因为插入排序每次只能将数据移动一位
希尔排序的基本思想是:算法先将要排序的一组数按某个增量d(n/2,n为要排序数的个数)分成若干组,每组中记录的下标相差d.对每组中全部元素进行直接插入排序,然后再用一个较小的增量(d/2)对它进行分组,在每组中再进行直接插入排序。当增量减到1时,进行直接插入排序后,排序完成。
算法步骤:
1)选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
2)按增量序列个数k,对序列进行k 趟排序;
3)每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
/*
* 希尔排序
*/
static void xierInsertSort(int []array){
int interval=array.length;
while(true){
interval=interval/2;
for(int i=0;i<array.length;i++){
directInsertSort(array,i,interval);
}
if(interval==1)
break;
}
}
/*
* 直接插入排序
*/
static void directInsertSort(int []array,int start,int interval){
int i,j,temp;
for (i = start; i < array.length; i=i+interval) {
//待插入元素
temp = array[i];
for (j = i-interval; j>=0 && array[j]>temp; j=j-interval) {
//将大于temp的往后移动interval
array[j+interval] = array[j];
}
array[j+interval] = temp;
}
}
/*
* 代码二:希尔排序
*/
public shellSort(){
int a[]={1,54,6,3,78,34,12,45,56,100};
double d1=a.length;
int temp=0;
while(true){
d1= Math.ceil(d1/2);
int d=(int) d1;
for(int x=0;x<d;x++){
for(int i=x+d;i<a.length;i+=d){
int j=i-d;
temp=a[i];
for(;j>=0&&temp<a[j];j-=d){
a[j+d]=a[j];
}
a[j+d]=temp;
}
}
if(d==1){
break;
}
}
}
希尔排序复杂度分析
- 希尔排序的关键并不是随便分组后各自排序,而是将相隔某个增量的数组组成一个子序列,实现跳跃式的移动。
- 增量的选择实验证明当增量序列为delt[k]=2^(t-k+1)-1( 0<=k<=t<=log2(n+1))时,效果较佳,但注意最后增量值必须等于1。
- 此时时间复杂度为O(n^1.5)
- 希尔排序因为是跳跃式记录,故不是一个稳定的排序算法。
算法四:选择排序——简单选择排序(不稳定)
选择排序(Selection sort)也是一种简单直观的排序算法。
算法步骤:
1)首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
2)再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
3)重复第二步,直到所有元素均排序完毕。
时间复杂度:平均时间复杂度为O(n^2)。
//选择排序 不稳定
public class SelectSort {
public static void main(String[] args) {
int[] a={49,38,65,97,76,13,27,49,78,34,12,64,1,8};
System.out.println("排序之前:");
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+" ");
}
//简单的选择排序
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(a[j]<min){ //找出最小的数
min = a[j];
n = j;
}
}
a[n] = a[i];
a[i] = min;
}
System.out.println();
System.out.println("排序之后:");
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+" ");
}
}
}
算法五:选择排序——堆排序(不稳定)
堆的定义下:具有n个元素的序列 (h1,h2,...,hn),当且仅当满足(hi>=h2i,hi>=2i+1)或(hi<=h2i,hi<=2i+1) (i=1,2,...,n/2)时称之为堆。
(1)基本思路:初始时把要排序的数的序列看作是一棵顺序存储的二叉树,调整它们的存储序,使之成为一个堆,这时堆的根节点的数最大。然后将根节点与堆的最后一个节点交换。然后对前面(n-1)个数重新调整使之成为堆。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。
时间复杂度:平均时间复杂度为O(nlogn)
(2)实例:
初始序列:46,79,56,38,40,84
建堆:
交换,从堆中踢出最大数
依次类推:最后堆中剩余的最后两个结点交换,踢出一个,排序完成。
//堆排序 不稳定
import java.util.Arrays;
public class HeapSort {
public static void main(String[] args) {
int[] a={49,38,65,97,76,13,27,49,78,34,12,64};
int arrayLength=a.length;
//循环建堆
for(int i=0;i<arrayLength-1;i++){
//建堆
buildMaxHeap(a,arrayLength-1-i);
//交换堆顶和最后一个元素
swap(a,0,arrayLength-1-i);
System.out.println(Arrays.toString(a));
}
}
//对data数组从0到lastIndex建大顶堆
public static void buildMaxHeap(int[] data, 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+1代表的k节点的右子节点存在
if(biggerIndex<lastIndex){
//若果右子节点的值较大
if(data[biggerIndex]<data[biggerIndex+1]){
//biggerIndex总是记录较大子节点的索引
biggerIndex++;
}
}
//如果k节点的值小于其较大的子节点的值
if(data[k]<data[biggerIndex]){
//交换他们
swap(data,k,biggerIndex);
//将biggerIndex赋予k,开始while循环的下一次循环,重新保证k节点的值大于其左右子节点的值
k=biggerIndex;
}else{
break;
}
}
}
}
//交换
private static void swap(int[] data, int i, int j) {
int tmp=data[i];
data[i]=data[j];
data[j]=tmp;
}
}
堆排序复杂度分析
- 构建大堆顶的时间复杂度为O(n)
- 第i次取堆顶记录重建堆需要O(logi)的时间,取n-1次,故重建堆的时间复杂度为O(nlogn)
- 无论最好,最坏,平均时间复杂度均为O(nlogn)
- 空间复杂度,只需要一个暂存单元
- 由于记录的比较与交换是跳跃式进行,故是一种不稳定的排序方法
- 由于初始构建堆所需要的比较次数较多,不适合带排序序列个数较少的情况。
算法六:交换排序——冒泡排序(稳定)
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
算法步骤:
1)比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
3)针对所有的元素重复以上的步骤,除了最后一个。
4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
优化一:如果某一轮两两比较中没有任何元素交换,这说明已经都排好序了,算法结束,可以使用一个Flag做标记,默认为false,如果发生交互则置为true,每轮结束时检测Flag,如果为true则继续,如果为false则返回。
优化二:某一轮结束位置为j,但是这一轮的最后一次交换发生在lastSwap的位置,则lastSwap到j之间是排好序的,下一轮的结束点就不必是j--了,而直接到lastSwap即可。
时间复杂度:平均时间复杂度为O(n^2)。
//冒泡排序 稳定
public class mp {
public static void main(String[] args) {
int[] a={49,38,65,97,76,13,27,49,78,34,12,64,1,8};
System.out.println("排序之前:");
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+" ");
}
//冒泡排序
for (int i = 0; i < a.length; i++) {
for(int j = 0; j<a.length-i-1; j++){
//这里-i主要是每遍历一次都把最大的i个数沉到最底下去了,没有必要再替换了
if(a[j]>a[j+1]){
int temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
}
}
}
System.out.println();
System.out.println("排序之后:");
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+" ");
}
}
}
算法七:交换排序——快速排序(不稳定)
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
(1)算法步骤:
1 从数列中挑出一个元素,称为 “基准”(pivot),
2 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
3 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
时间复杂度:平均时间复杂度为O(nlogn)
(2)实例:
//快排 不稳定
public class QuickSort {
public static void main(String[] args) {
int[] a={49,38,65,97,76,13,27,49,78,34,12,64,1,8};
System.out.println("排序之前:");
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+" ");
}
//快速排序
quick(a);
System.out.println();
System.out.println("排序之后:");
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+" ");
}
}
private static void quick(int[] a) {
if(a.length>0){
quickSort(a,0,a.length-1);
}
}
private static void quickSort(int[] a, int low, int high) {
if(low<high){ //如果不加这个判断递归会无法退出导致堆栈溢出异常
int middle = getMiddle(a,low,high);
quickSort(a, 0, middle-1);
quickSort(a, middle+1, high);
}
}
private static int getMiddle(int[] a, int low, int high) {
int temp = a[low];//基准元素
while(low<high){
//找到比基准元素小的元素位置
while(low<high && a[high]>=temp){
high--;
}
a[low] = a[high];
while(low<high && a[low]<=temp){
low++;
}
a[high] = a[low];
}
a[low] = temp;
return low;
}
}
快速排序时间复杂度分析
- 最优与平均时间复杂度O(nlogn)
- 最坏时间复杂度O(n^2)
- 空间复杂度O(logn)
- 快速排序是一种不稳定的排序方法
算法八:归并排序(稳定)
(1)基本思路:将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
平均时间复杂度为O(nlogn)
(2)实例:
// 归并排序 稳定
public class MergSort {
public static void main(String[] args) {
int[] a={49,38,65,97,76,13,27,49,78,34,12,64,1,8};
System.out.println("排序之前:");
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+" ");
}
//归并排序
mergeSort(a,0,a.length-1);
System.out.println();
System.out.println("排序之后:");
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+" ");
}
}
private static void mergeSort(int[] a, int left, int right) {
if(left<right){
int middle = (left+right)/2;
//对左边进行递归
mergeSort(a, left, middle);
//对右边进行递归
mergeSort(a, middle+1, right);
//合并
merge(a,left,middle,right);
}
}
private static void merge(int[] a, int left, int middle, int right) {
int[] tmpArr = new int[a.length];
int mid = middle+1; //右边的起始位置
int tmp = left;
int third = left;
while(left<=middle && mid<=right){
//从两个数组中选取较小的数放入中间数组
if(a[left]<=a[mid]){
tmpArr[third++] = a[left++];
}else{
tmpArr[third++] = a[mid++];
}
}
//将剩余的部分放入中间数组
while(left<=middle){
tmpArr[third++] = a[left++];
}
while(mid<=right){
tmpArr[third++] = a[mid++];
}
//将中间数组复制回原数组
while(tmp<=right){
a[tmp] = tmpArr[tmp++];
}
}
}
算法九:基数排序(稳定)
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
(1)基本思想:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
(2)实例:
//基数排序 稳定
import java.util.ArrayList;
import java.util.List;
public class RaSort {
public static void main(String[] args) {
int[] a={49,38,65,97,176,213,227,49,78,34,12,164,11,18,1};
System.out.println("排序之前:");
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+" ");
}
//基数排序
sort(a);
System.out.println();
System.out.println("排序之后:");
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+" ");
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private static void sort(int[] array) {
//找到最大数,确定要排序几趟
int max = 0;
for (int i = 0; i < array.length; i++) {
if(max<array[i]){
max = array[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 < array.length; j++) {
int x = array[j]%(int)Math.pow(10, i+1)/(int)Math.pow(10, i);
ArrayList queue2 = queue.get(x);
queue2.add(array[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);
array[count] = queue3.get(0);
queue3.remove(0);
count++;
}
}
}
}
}
参考链接: