排序的分类可以分为两种:内排序和外排序。
在排序过程中,全部记录放在内存,则称为内排序,如果排序过程中要使用外村,则称为外排序。这里讲的排序都是属于内排序。
内排序又可以分为以下几类:
(1)插入排序:直接插入排序,二分法插入排序和希尔排序
(2)选择排序:简单选择排序,堆排序
(3)交换排序:冒泡排序,快速排序
(4)归并排序
(5)基数排序
一、插入排序
思想:每步将一个待排序的记录,按其顺序码大小插入到前面已经排序的字序列的合适位置,直到全部插入排序完成为止。
关键问题:在前面已经排序好的序列中找到合适的插入位置。
方法:
直接插入排序: 直接插入排序是稳定的排序。当文件初态为正态,则每个待插入的记录只需比较一次既能够找到合适的位置插入,故算法的时间复杂度为O(n),是最好的情况。如果初态为反序,则第i个待插入的记录需要比较i+1次才能找到合适位置,故时间复杂度为O(n2),这也是平均复杂度。
二分插入排序:它的排序方法与直接插入思想一样,只是找合适位置的方式不同。用二分法可以减少比较的次数。
希尔排序:一次插入排序是稳定的,但是在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,就会打乱其稳定性。希尔排序的时间性能优于直接排序。但是它是不稳定的排序。当n值较小时,n和n2的区别也小,所以直接插入排序的时间复杂度最好与最坏差不多。因此希尔排序经过分组,所以插入速度较快,后期虽然分组较少,但是经过之前的排序,使文件已经比较接近有序状态。所以也能提高速度。平均时间复杂度为O(nlogn)
实现:
-直接插入排序 从后面向前找到合适位置后插入。直到序列中的每个元素都插入。
package com.sort;
public class DirectSort {
public static void main(String [] args){
int[] a={58,46,59,26,45,57,87,62,12,1,64};
int tmp;
//直接插入排序
for(int i=1;i<a.length;i++){
for(int j=i; j>0;j--){
if(a[j]<a[j-1]){
tmp=a[j-1]
a[j-1]=a[j];
a[j]=tmp;
}
}
}
for(int i:a){
System.out.print(i+" ");
}
}
}
-二分法插入排序 它的比较次数与带排序记录的初始状态无关,仅依赖于记录的个数。当n较大时,比直接插入排序的最大比较次数少的多。最坏的情况为n2/2,最好的情况为n,平均移动次数为n2
static public void halfSort(int a[]){
for(int i =0;i<a.length;i++){
int low = 0;
int high = i;
int mid = 0;
int temp = a[i];
while(low<=high){
mid=(low+high)/2;
if(a[mid]<temp){
low=mid+1;
}else{
high=mid-1;
}
}
//找到要插入的位置,然后将这个位置以后的元素向后移动
for(int j = i -1;j>high;j--){
a[j+1]=a[j];
}
a[high+1]=temp;
}
}
-希尔排序 先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为d1的倍数的记录放在同一个组中,现在各组内进行直接插入排序;然后取第二个增量d2<d1重复上述的分组和排序。直至所取的增量dt为1.即所有记录放在同一组中进行直接插入排序位置。其实质是分组的直接插入排序。
public static void sort(int [] arrays){
int incrementNum = arrays.length/2;
while(incrementNum>=1){
for(int i=0;i<arrays.length;i++){
//进行插入排序
for(int j=i;j<arrays.length-incrementNum;j+=incrementNum) {
if(arrays[j]>arrays[j+incrementNum]){
int temp = arrays[j];
arrays[j] = arrays[j+incrementNum];
arrays[j+incrementNum] = temp;
}
}
}
//设置新的增量
incrementNum/=2;
}
}
二、选择排序
思想:每趟从待排序的序列里选择关键字最小的记录放置到已排序的最前位置。直到全部排完。
关键问题:在剩余的待排序记录序列中找到最小关键码记录。
方法:直接选择排序
简单选择排序是不稳定的排序。时间复杂度:T(n)=O(n2)
堆排序:也是一种不稳定的排序方法。堆排序可通过树形结构保存部分比较结果,可减少比较次数。堆排序的最坏时间复杂度为O(nlogn).因为建初始堆所需的比较次数较多、所以堆排序不适宜于记录数比较少的文件。
是一种就地排序算法(不需要额外的存储空间)
实现:
-直接选择排序它的在要排序的一组数中,选出最小的一个数与第一个位置的数交换,然后在剩下的数中再找最小的与第二个位置交换。直到最后一个数和倒数第二个数比较为止。这个是按固定的位置放置,插入排序是挨个比较。
//是不稳定的
public class simpleSelectSort{
public static void main(String [] args){
int[] a={49,38,65,97,76,13,27,49,78,34,12,64,1,8};
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;
}
}
}
-堆排序它是一种属性选择排序,是对直接选择排序的一种改进。
由堆的定义可以看出,堆顶元素必为最大项。完全二叉树可以很直观的表示堆的结构。堆顶为根,其它为左子树、右子树。初始排序时,将待排序列看作是一棵顺序存储的二叉树,调整它们的存储顺序,使之成为一个堆,堆的根节点的数最大。然后将根节点与堆的最后一个节点交换,然后前面剩下的n-1个元素再去构成一个堆。以此类推,直到只剩两个节点的堆,将它们作交换,得到一个n个节点的有序排列。从算法来看,堆排序需要经过两个过程,一、建立堆,二、是堆顶与最后一个元素交换位置。所以堆排序由两个函数组成。一个是建立堆的渗透函数,一个是反复调用渗透函数实现排序的函数。
在这里首先学习一下,大顶堆的实现。堆的建立在堆排序里是至关重要的。除了最后一层,其它层都是填满的,也正是因为这样,树里面每个节点的子女和双亲节点的序号都可以根据当前节点的序号直接求出。
parent(i)=i/2
Left(i)=2*i
right(i)=2*i+1
最大堆的性质就是,根节点值大于所有子节点,我们构建最大堆的操作,对于每个节点i,我们观察它的子节点的大小,如果它小于某个子节点, 那么则进行位置调换。然后在相应的子节点上同样进行这样的操作。直到比所有的子节点都大为止。
三、交换排序
-冒泡排序 在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数一次进行比较和调整,让较大的数往下沉,较小的向上冒。每当两相邻的数比较后,发现与排序要求相反时,就将它们互换。
冒泡最好复杂度是O(n),最差是O(n2),这也是平均时间复杂度。
public class Maopao{
public static void main(String [] args){
int[] a= {49,38,65,97,76,13,27,49,78,34,12,64,1,8};
for(int i = 0;i<a.length){
for(int j = 0;j<a.length-i-1;j++){
//这里-i主要是每遍历一次都把最大的i个数沉到最底下去了, 没有必要再替换了
if(a[j]>a[j+1]){
//一次次遍历后,最大的数就到最后去了
int temp = a[j+1];
a[j+1]=a[j];
a[j]=temp;
}
}
}
}
}
-快速排序 选择一个基准元素,通常选择第一个元素或者最后一个元素,通过一趟扫描,将待排序列分成两部分,一部分都比基准元素大或等于,一部分都比基准元素小。这时基准元素就处在了正确的位置。然后用同样的方法递归的排序划分的两部分。快速排序也是不稳定的。
public static void sort(int a[],int low,int high){
int index=a[low];
while(low<high){
index = a[low];//用子表的第一个记录做基准
while(low<high&&a[high]>=index){
high--;
}
a[low]=array[high];
}
while(a[low]<=index&&high>low){
lo++;
}
a[high]=a[low];
}
array[high]=key;
return hi;
}
public static void sort(int[] array,int low,int high){
int inidex=partition(array,low,high);
sort(array,low,index-1);
sort(array,index+1,hi);
}
四、归并排序
-归并排序 归并排序法是把两个或两个以上的有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列都是有序的。然后把有序子序列合并为整体有序序列。归并排序是稳定的排序方法,归并序列的复杂度为O(nlogn)。速度仅次于快速排序,为稳定排序算法,一般用于总体无序,但是各子项相对有序的数列。
public class MergeSort{
public static void main (String [] args){
int [] a={49,38,65,97,76,13,27,49,78,34,12,64,1,8};
//归并排序
mergeSort(a,0,a.length-1);
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++];
}
}
}
五、基数排序
-基数排序 将所有待比较数值(正整数)统一为同样的数位长度,数位较短者的前面补零。然后从最低位开始,依次进行一次排序。这样从最低位到最高位排序完成后,数列就变成了一个有序数列。基数排序是稳定的排序算法,基数排序的时间复杂度为O(d(n+r)),d为位数,r为基数。
public class baseSort{
public static void main(String[] args){
int[] a={49,38,65,97,176,213,227,49,78,34,12,164,11,18,1};
sort(a);
}
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);
}
//收集
intcount = 0;
for(intj = 0; j < 10; j++) {
while(queue.get(j).size()>0){
ArrayList queue3 =queue.get(j);
array[count] = queue3.get(0);
queue3.remove(0);
count++;
}
}
}
}
}
总结:
一、稳定性
稳定:冒泡排序,基数排序,归并排序,插入排序
不稳定:希尔排序,选择排序,快速排序,堆排序
二、平均时间复杂度
O(n^2):直接插入排序,简单选择排序,冒泡拍戏
在数据规模较小时(9w内),直接插入排序,简单选择排序差不多。当数据较大时,冒泡排序算法的时间代价最高。这种算法基本是稳定
O(nlogn):快速排序,归并排序,希尔排序,堆排序
其中快速排序是最好的,其次是归并和希尔,堆排序在数据量很大时,效果明显。
三、排序算法的选择
1.数据规模较小时
(1)待排序列基本序的情况下,可以选择直接插入排序
(2)对稳定性不作要求宜用简单选择排序,对稳定性有要求宜用插入或冒泡。
2.数据规模很大时
(1)对稳定性有要求,可考虑归并排序
(2)对稳定性没有要求,宜用堆排序
3.数据规模适中
(1)完全可以用内存空间,序列杂乱,对稳定性没有要求,快速排序,此时要付出log(N)的额外空间。
(2)序列本身可能有序,对稳定性有要求,空间允许下,宜用归并排序。
4.序列初始基本有序(正序),宜用直接插入或冒泡。
更新巩固
1.几乎有序的时候,快排的时间复杂度退化到O(n*n),无序的时候快速排序才是比较快。这个时候采用堆排序是最省时间的。
2.归并排序在任何时候的时间复杂度均为O(N*logN),空间复杂度为O(N);但是需要建立一个栈来维护,快排在最好的情况下时间复杂度为O(N*logN)。空间复杂度为O(logN)
3.希尔排序是缩小增量排序,所以对于初始序列有序还是无序没有直接关系。