选择排序
给定一个规模为n的数组,在n个数字中寻找最小的一个,然后与第0位置的数交换,然后在后n-1个数中找最小的,与第1个位置的数字交换。当交换了i次以后,数组左边的i个数字是有序的,右边n-i个数字无序。然后把在n-i个数字中寻找最小的,与i位置的数字交换位置。直到交换n次以后,整个数组有序。
时间复杂度:O(n²)
实现:
public class Selection{
public static void sort(int[] a) {
int t;
for(int i=0;i<a.length;i++){
int index=i;//用来保存剩余乱序数组中的最小值的位置
for(int j=i+1;j<a.length;j++){
if(a[index]>a[j]){//寻找最小值并保存位置
index=j;
}
}
//找到最小值的位置后交换
t=a[i];
a[i]=a[index];
a[index]=t;
}
}
}
插入排序
给定一个规模为n的数组,把第i号(i
public class Insertion{
public static void sort(int[] a) {
int t=0;
for(int i=1;i<a.length;i++){
int v=a[i],k;
for(k=i-1;k>=0&&v<a[k];k--){//查找左边比a[i]的元素大的元素,右移。
a[k+1]=a[k];
}
a[k+1]=v;//把a[i]放入比a[i]小的元素的后面
}
}
}
希尔排序
根据插入排序,如果n个元素中最小的数在最右端,那么该数字需要比较n-1次才可以到达目标位置,因为是逐个比较元素的大小的。如果把逐个比较改为相隔1个比较并移动、相隔1个比较并移动、相隔h个比较并移动,那么最右端的元素到目标位置的比较移动次数将会大幅度减小。所以,在每隔h个元素都比较移动完以后,n个数中将会出现n/h个有序对,他们彼此相隔h,然后把h–,然后再比较移动,直到h==1且比较移动完后,排序结束。插入排序就是h=1的希尔排序。
时间复杂度:未知
适用范围:数据规模较大,数据较离散。
实现:
public class Shell {
public static void sort(int[] a){
int h=1,t=0;
h=a.length/2;
while(h>=1){//直到相隔距离为1时,最后一次比较移动
for(int i=h-1,j;i<a.length;i++){//从i位置开始,保证之前每隔h位置的元素都是有序的,一开始从h-1开始,左边只有h个元素,所以每一组都是有序的
int base=a[i];//要插入的元素
for(j=i-h;j>=0&&a[j]>base;j-=h){//把要插入的元素与该组的其他元素比较大小,如果被比较的元素较大,则右移h个位置
a[j+h]=a[j];
}
a[j+h]=base;//把要插入的元素插入到找到的位置
}
h/=2;//相隔距离每次减半
}
// display(a);
}
}
归并排序
通过递归的方式,把n个元素等分成两部分,然后再将这两部分等分成四部分,以此类推,知道将n个元素分成n个单独的组,这n个组因为只有一个元素,所以是有序的。
然后把其中相邻的两个组合并,要求合并后的组也是有序的,于是形成了n/2个组。然后再相邻的组合并,形成n/4个组。依此类推,知道合并成只有一个组,排序完毕。
合并两个组时,可以将两个指针指向两个数组的第一个位置,然后比较大小,把小的放入临时数组,然后该指针后移。如果其中一个数组的指针到了最右,则把另一个数组剩下的元素复制到临时数组。
过程如下图:
时间复杂度:O(nlgn)
缺点:需要开辟一个与原数组相同大小的辅助数组
实现:
public class Merge {
static int[] temp=null;
public static void sort(int[] a){
int start,end;
temp=new int[a.length];
start=0;
end=a.length-1;
merge(a,start,end);
// display(a);
}
public static void merge(int[] a,int start,int end){
if(start==end)
return;
int mid=(start+end)/2;
merge(a,start,mid);//把数组分成两半,分别排序
merge(a,mid+1,end);
if(a[mid]<a[mid+1])//前一个数组中最大的数比后一个数组中最小的数小,说明两个数组合并后已经有序,所有不需要再排序
return;
mergeSort(a,start,end);
}
public static void mergeSort(int[] a,int start,int end){
int mid=(start+end)/2;
for(int i=start,j=mid+1,k=start;k<=end;k++){//i,j分别表示两个数组的第一个元素(物理上只有一个数组,但逻辑上分为两个数组)
if(i>mid) temp[k]=a[j++];//如果第一个数组比较结束,则以此把第二个数组所有元素复制到临时数组
else if(j>end) temp[k]=a[i++];//如果第二个数组比较结束,则以此把第一个数组所有元素复制到临时数组
else if(a[i]<a[j]) temp[k]=a[i++];//如果第一个数组i位置元素比较小,则把a[i]复制到临时数组,并且i右移
else temp[k]=a[j++];//如果第二个数组j位置元素比较小,则把a[j]复制到临时数组,并且j右移
}
System.arraycopy(temp, start, a, start, end-start+1);//把临时数组中的元素复制到原数组
}
public static void display(int[] a){
for(int i=0;i<a.length;i++)
System.out.print(a[i]+" ");
System.out.println();
}
}
快速排序
1、 给定n个数,从start到end进行排序
2、 从左边往右边找到i满足a[i]>a[start]
3、从右边往左边找到j满足a[j]>a[start]
4、 交换a[i]和a[j]
5、 重复3、4,直到i>=j
6、 交换a[start]和a[j],此时满足a[0~j-1]都小于a[j], a[j+1,end]都大于于a[j]
7、 从1重新执行,此时start=start,end=j-1 和start=j+1,end=end,直到start>=end
过程图:
时间复杂度(平均):O(nlgn)
适用范围:重复数字少,有序数字少
实现:
public class Quick {
public static void sort(int[] a) {
sort(a, 0, a.length - 1);
}
private static void sort(int[] a, int start, int end) {
if (start >= end)
return;
int i = partition(a, start, end);// 返回切割位置,此时i位置的元素大于[start,i-1],下雨[i+1,end]
sort(a, start, i - 1);
sort(a, i + 1, end);
}
private static int partition(int[] a, int start, int end) {// 要求返回位置的元素大于等于左边元素,返回位置的元素都小于等于右边的元素
int i = start + 1, j = end;
i = start;
j = end + 1;
int value=a[start];
while (true) {
while (a[++i] < value)
// 向右寻找比a[start]大的数
if (i == end)
break;
while (a[--j] > value);// 向左寻找比a[start]小的数,但因为最左边是a[start],所以j>start恒成立
// if (j == start)
// break;
if (i >= j)// 如果i,j相遇或i>j,则结束寻找
break;
swap(a, i, j);
}
// j就是满足a[j]大于等于左边,小于等于右边的位置
// 因为a[j]一定小于等于a[start]或者j等于start
// a[i]一定大于等于a[start]或者i等于end
// 根据前一种情况,i跟j不在同一位置的话,a[j]就是小于a[start]最右边的数,再往右的话就是a[i],而a[i]一定大于a[start]
// 如果a[j]>a[start],就意味着j到了最左边(因为j是从右往左寻找比a[start]小的数的,如果没找到且停下来了,就意味着到头了)
// 但是所谓的最左边就是a[start]所在位置,所以a[j]一定小于或等于a[start]
swap(a, j, start);
return j;
}
private static void swap(int[] a, int p, int q) {
int t = a[p];
a[p] = a[q];
a[q] = t;
}
}
堆排序
(二叉)堆有序定义:在一颗二叉树中,如果所有父节点不小于子节点,则该二叉树就是堆有序
下潜操作定义:如果一个父节点大于两个子节点或者子节点中的一个,那么把父节点与子节点中大的那个交换位置,然后把之前的父节点(现在已被交换到子节点的位置)与其新的子节点进行比较并交换,知道该节点大于其子节点,则结束操作。该操作称为下潜操作。
对于一个有序堆,序号为0的节点一定是最大的元素,把这个元素去掉,并把最后一个元素放到0位置,该堆又变成了无序堆。然后对0节点进行下潜操作,该堆重新变成了有序堆。然后继续移除、继续排序,最终该堆大小为0,原数组的位置变为一个有序序列。
该图为把一个有序堆的最大元素移除、排序最终得到有序数列的过程。
那么接下来的问题就是如何把一个无序堆构建成有序堆。
只要把一个堆自下而上进行下潜操作,那么最终就是一个有序堆。而最后一层节点没有子节点,所以不需要进行下潜操作。从倒数第二层的最后一个节点开始进行下潜操作,知道第一个元素,那么就可以形成一个有序堆,过程如下图:
算法复杂度:O(nlgn)
实现:
public class Heap {
// 如下一颗二叉树数字表示其序号
//
// 0
// 1 2
// 3 4 5 6
//
// 根据二叉树的定义,序号为n的节点的父节点(如果存在)序号为n/2⌉-1,其子节点(如果存在)序号为n*2+1和n*2+2
// 如果共有n个元素,则倒数第二次的最后一个节点序号为(n+1)/2⌉+1
public static void sort(int[] a) {
int n = a.length;
for (int k = (int) (Math.ceil((double) (n + 1) / 2) + 1); k >= 0; k--)
// 从倒数第二层的最后一个几点开始建堆
sink(a, k, n);
while (n > 1) {
swap(a, 0, --n);
sink(a, 0, n);
}
}
public static void sink(int[] a, int k, int n) {// 下潜操作
while (k * 2 + 2 < n) {
int j = k * 2 + 1;// j表示第一个子节点
if (a[j] < a[j + 1])// 如果第一个子节点小于第二个子节点
j++;// 此时j表示第二个子节点
if (a[k] > a[j])// 如果大的子节点小于父节点,则表示父节点大于两个子节点,则不需要继续下潜了
break;
swap(a, k, j);// 否则把父节点和子节点交换
k = j;
}
}
private static void swap(int[] a, int p, int q) {
int t = a[p];
a[p] = a[q];
a[q] = t;
}
}