1. 概述
排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。
这里说八大排序就是内部排序。
当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。
快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短。
2.插入排序-直接插入排序
将一个记录插入到已排序好的有序表中,从而得到一个新,记录数增1的有序表。即:先将序列的第1个记录看成是一个有序的子序列,然后从第2个记录逐个进行插入,直至整个序列有序为止。
要点:设立哨兵,作为临时存储和判断数组边界之用。
import java.util.Arrays;
/** * 直接插入排序 * 最优时间n,最差/平均n*n,空间1,大部分有序比较合适,稳定 */
public class InsertSort {
public static void main(String[] args) {
int[] a={7,3,19,40,4,8,1};
insertSort(a);
System.out.println(Arrays.toString(a));
}
public static void insertSort(int[] a){
int i,j;
int temp=0;
int n=a.length;
for(i=1;i<n;i++){
temp=a[i];
j=i;
if (a[j-1]>temp){
while (j>=1&&a[j-1]>temp){
a[j]=a[j-1];
j--;
}
}
a[j]=temp;
System.out.println("第"+i+"次"+Arrays.toString(a));
}
}
}
3.插入排序-希尔排序
基本思想:
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
操作方法:
1. 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
2. 按增量序列个数k,对序列进行k 趟排序;
3. 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
希尔排序的示例:
import java.util.Arrays;
/** * 希尔排序(减少增量排序) *最优n,最差介于n-n*n,平均nlogn,不稳定 * Created by Administrator on 2017/4/7 0007. */
public class ShellSort {
public static void main(String[] args) {
int[] a={5,4,9,8,7,6,0,1,3,2};
shellSort(a);
System.out.println(Arrays.toString(a));
}
public static void shellSort(int[] a){
int len=a.length;
int i,j;
int h;
int temp;
//步长
for(h=len/2;h>0;h=h/2){
for (i=h;i<len;i++){
temp=a[i];
for(j=i-h;j>=0;j=j-h){
if(temp<a[j]){
a[j+h]=a[j];
}else {
break;
}
}
a[j+h]=temp;
}
System.out.println("当长度为"+h+"时:"+Arrays.toString(a));
}
}
}
4.选择排序-直接选择排序
基本思想:
在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。
操作方法:
第一趟,从n 个记录中找出关键码最小的记录与第一个记录交换;
第二趟,从第二个记录开始的n-1 个记录中再选出关键码最小的记录与第二个记录交换;
以此类推…..
第i 趟,则从第i 个记录开始的n-i+1 个记录中选出关键码最小的记录与第i 个记录交换,
直到整个序列按关键码有序。
/** * 选择排序 * 最优,最差,平均时间均为n*n,存储1,不稳定,n较少好 * Created by Administrator on 2017/4/5 0005. */
public class SelectSort {
public static void main(String[] args) {
int[] a={5,4,9,8,7,6,0,1,3,2};
selectSort(a);
System.out.println(Arrays.toString(a));
}
public static void selectSort(int[] a){
int i,j;
int temp=0;
int n=a.length;
for(i=0;i<n;i++){
temp=a[i];
for(j=i+1;j<n;j++){
if(a[j]<temp){
temp=a[j];
a[j]=a[i];
a[i]=temp;
}
}
}
}
}
//优化,直接选择最大和最小进行交互
public static void betterSelectSort(int[] a){
int left=0;
int len=a.length;
int right=len-1;
int temp=0;
while (left<right){
int min=left;
int max=right;
for(int i=left;i<=right;i++){
if(a[i]<a[min]){
min=i;
}
if(a[i]>a[max]){
max=i;
}
}
temp=a[max];
a[right]=a[max];
a[max]=temp;
if(min==right){
min=max;
}
temp=a[left];
a[left]=a[min];
a[min]=temp;
left ++;
right--;
System.out.println(Arrays.toString(a));
}
}
5.选择排序-堆排序
堆排序是一种树形选择排序,是对直接选择排序的有效改进。
基本思想:
堆的定义如下:具有n个元素的序列(k1,k2,…,kn),当且仅当满足
时称之为堆。由堆的定义可以看出,堆顶元素(即第一个元素)必为最小项(小顶堆)。
若以一维数组存储一个堆,则堆对应一棵完全二叉树,且所有非叶结点的值均不大于(或不小于)其子女的值,根结点(堆顶元素)的值是最小(或最大)的。如:
(a)大顶堆序列:(96, 83,27,38,11,09)
(b) 小顶堆序列:(12,36,24,85,47,30,53,91)
初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使之成为一个堆,将堆顶元素输出,得到n 个元素中最小(或最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。称这个过程为堆排序。
因此,实现堆排序需解决两个问题:
1. 如何将n 个待排序的数建成堆;
2. 输出堆顶元素后,怎样调整剩余n-1 个元素,使其成为一个新堆。
首先讨论第二个问题:输出堆顶元素后,对剩余n-1元素重新建成堆的调整过程。
调整小顶堆的方法:
1)设有m 个元素的堆,输出堆顶元素后,剩下m-1 个元素。将堆底元素送入堆顶((最后一个元素与堆顶进行交换),堆被破坏,其原因仅是根结点不满足堆的性质。
2)将根结点与左、右子树中较小元素的进行交换。
3)若与左子树交换:如果左子树堆被破坏,即左子树的根结点不满足堆的性质,则重复方法 (2).
4)若与右子树交换,如果右子树堆被破坏,即右子树的根结点不满足堆的性质。则重复方法 (2).
5)继续对不满足堆性质的子树进行上述交换操作,直到叶子结点,堆被建成。
称这个自根结点到叶子结点的调整过程为筛选。如图:
再讨论对n 个元素初始建堆的过程。
建堆方法:对初始序列建堆的过程,就是一个反复进行筛选的过程。
1)n 个结点的完全二叉树,则最后一个结点是第个结点的子树。
2)筛选从第个结点为根的子树开始,该子树成为堆。
3)之后向前依次对各结点为根的子树进行筛选,使之成为堆,直到根结点。
如图建堆初始过程:无序序列:(49,38,65,97,76,13,27,49)
从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。所以堆排序有两个函数组成。一是建堆的渗透函数,二是反复调用渗透函数实现排序的函数。
//构建初始堆花时间约为2.5n,交换nlogn,时间复杂度为 nlogn+o(n) 性能比较接近nlogn,辅助空间为1
public static void heapSort(int[] a){
int i;
int len=a.length;
for(i=len/2-1;i>=0;i--){//建立初始堆
adjustHeap(a,i,len);
System.out.println(Arrays.toString(a));
}
for (i=len-1;i>=2;i--){
int temp=a[0];
a[0]=a[i];
a[i]=temp;
adjustHeap(a,0,i-1);
System.out.println(Arrays.toString(a));
}
}
public static void adjustHeap(int[] a,int pos,int len){
int temp=0;
int child=0;
for(temp=a[pos];2*pos+1<len;pos=child){
child=2*pos+1;
if(child<len&&a[child]>a[child+1]) {//若右孩子较小,指向右孩子
child++;
}if(a[child]<temp){ //若孩子比较小,交换
a[pos]=a[child];
}else {
break;
}
}
a[pos]=temp;
}
6.交换排序—冒泡排序
基本思想:
在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。
冒泡排序的示例:
//最坏为o(n*n),最好为o(n)
public static void dubboSort(int[] a){
int i,j;
int temp=0;
int len=a.length;
for(i=len-1;i>0;i--){
flag=true;
for(j=0;j<i;j++){
if (a[j + 1] < a[j])
{
temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
System.out.println(Arrays.toString(a));
}
}
//改进的冒泡排序
public static void dubboSort(int[] a){
int i,j;
int temp=0;
int len=a.length;
boolean flag;
for(i=len-1;i>0;i--){
flag=true;
for(j=0;j<i;j++){
if (a[j + 1] < a[j])
{
temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
flag=false;
}
}
if(flag){
break;
}
System.out.println(Arrays.toString(a));
}
}
7.交换排序—快速排序
基本思想:
1)选择一个基准元素,通常选择第一个元素或者最后一个元素,
2)通过一趟排序将待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小。另一部分记录的 元素值比基准值大。
3)此时基准元素在其排好序后的正确位置
4)然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序
//最坏时间复杂度0(n*n),平均和最好0(nlogn),空间复杂度最坏0(n),平均0(logn)
public static void quickSort(int[] a,int low,int high){
int i=low;
int j=high;
if(i>j){
return;
}
int index=a[i];
while(i<j){
while(i<j&&a[j]>index){
j--;
}
if(i<j){
a[i++]=a[j];
}
while (i<j&&a[i]<index){
i++;
}
if(i<j){
a[j--]=a[i];
}
a[i]=index;
quickSort(a,low,i-1);
quickSort(a,i+1,high);
}
System.out.println(Arrays.toString(a));
}
快速排序的优化:
1)优化选取的枢纽。避免出现极端情况
2)当待排序序列的长度分割到一定大小后,使用插入排序
原因:对于很小和部分有序的数组,快排不如插排好。当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差,此时可以使用插排而不是快排.
3)在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割
http://www.open-open.com/lib/view/open1426410169374.html
http://blog.csdn.net/insistgogo/article/details/7785038
8.归并排序
基本思想:
归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
归并排序示例:
合并方法:
设r[i…n]由两个有序子表r[i…m]和r[m+1…n]组成,两个子表长度分别为n-i +1、n-m。
j=m+1;k=i;i=i; //置两个子表的起始下标及辅助数组的起始下标
若i>m 或j>n,转⑷ //其中一个子表已合并完,比较选取结束
//选取r[i]和r[j]较小的存入辅助数组rf
如果r[i]
public static void mergeSort(int[] a,int low,int high){
if(low<high){
int mid=(low+high)/2;
mergeSort(a,low,mid);
mergeSort(a,mid+1,high);
merge(a,low,mid,high);
System.out.println(Arrays.toString(a));
}
}
public static void merge(int[] a,int low,int mid,int high){
int i,j,k,n1,n2;
n1=mid-low+1;
n2=high-mid;
int[] L=new int[n1];
int[] R=new int[n2];
for(i=0,k=low;i<n1;i++,k++){
L[i]=a[k];
}
for(i=0,k=mid+1;i<n2;i++,k++){
R[i]=a[k];
}
for(k=low,i=0,j=0;i<n1&&j<n2;k++){
if(L[i]>R[j]){
a[k]=L[i];
i++;
}else {
a[k]=R[j];
j++;
}
}
if(i<n1){
for(j=i;j<n1;j++,k++){
a[k]=L[j];
}
}
if(i<n2){
for(i=j;j<n2;i++,k++){
a[k]=R[i];
}
}
}
时间复杂度来说:
(1)平方阶(O(n2))排序
各类简单排序:直接插入、直接选择和冒泡排序;
(2)线性对数阶(O(nlog2n))排序
快速排序、堆排序和归并排序;
(3)O(n1+§))排序,§是介于0和1之间的常数。
希尔排序
(4)线性阶(O(n))排序
基数排序,此外还有桶、箱排序。
说明:
当原表有序或基本有序时,直接插入排序和冒泡排序将大大减少比较次数和移动记录的次数,时间复杂度可降至O(n);
而快速排序则相反,当原表基本有序时,将蜕化为冒泡排序,时间复杂度提高为O(n2);
原表是否有序,对简单选择排序、堆排序、归并排序和基数排序的时间复杂度影响不大。
稳定性:
排序算法的稳定性:若待排序的序列中,存在多个具有相同关键字的记录,经过排序, 这些记录的相对次序保持不变,则称该算法是稳定的;若经排序后,记录的相对 次序发生了改变,则称该算法是不稳定的。
稳定性的好处:排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排序算法稳定,可以避免多余的比较;
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序
选择排序算法准则:
每种排序算法都各有优缺点。因此,在实用时需根据不同情况适当选用,甚至可以将多种方法结合起来使用。
选择排序算法的依据
影响排序的因素有很多,平均时间复杂度低的算法并不一定就是最优的。相反,有时平均时间复杂度高的算法可能更适合某些特殊情况。同时,选择算法时还得考虑它的可读性,以利于软件的维护。一般而言,需要考虑的因素有以下四点:
1.待排序的记录数目n的大小;
2.记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小;
3.关键字的结构及其分布情况;
4.对排序稳定性的要求。
设待排序元素的个数为n.
1)当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。
快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
堆排序 : 如果内存空间允许且要求稳定性的,
归并排序:它有一定数量的数据移动,所以我们可能过与插入排序组合,先获得一定长度的序列,然后再合并,在效率上将有所提高。
2) 当n较大,内存空间允许,且要求稳定性 =》归并排序
3)当n较小,可采用直接插入或直接选择排序。
直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。
直接选择排序 :元素分布有序,如果不要求稳定性,选择直接选择排序
5)一般不使用或不直接使用传统的冒泡排序。
6)基数排序
它是一种稳定的排序算法,但有一定的局限性:
1、关键字可分解。
2、记录的关键字位数较少,如果密集更好
3、如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开排序。