5.希尔排序
思想:希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序的本质是插入排序,同时它也称为缩小增量排序。将记录的下标按照一定的增量dk进行分组,对于每一组的记录我们使用直接插入排序算法。而增量dk的选择初始值一半设置为数组长度的一半,序列{dk,dk/2,dk/4...1},称之为增量序列。
代码实现如下:
public class Test5 {
public static void main(String[] args) {
int a[]=new int[]{10,9,8,7,6,5,4,3,2,1};
System.out.print("排序前的结果为: ");
for(int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
Test5 test5=new Test5();
int dk=a.length/2; //初始的增量设置为数组长度的一半
while(dk>=1){
test5.shellSort(a,dk);
dk/=2; //增量序列每次除以2,当增量序列为1时候等同于直接插入排序
}
System.out.print("排序后的结果为: ");
for(int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
}
public void shellSort(int []a,int dk){
int len=a.length,i,j;
for(i=dk;i<len;i++){
int temp=a[i];
for(j=i-dk;;){
while(j>=0 && a[j]>temp){
a[j+dk]=a[j];
j=j-dk;
}
a[j+dk]=temp;
break;
}
}
}
}
排序前的结果为: 10 9 8 7 6 5 4 3 2 1
排序后的结果为: 1 2 3 4 5 6 7 8 9 10
该代码与直接插入排序的代码几乎相同,注意增量dk每次要除以2,用上递归即可解决。
结论:在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。希尔排序与直接插入排序一样,最好的时间复杂度为,最坏的时间复杂度是。而希尔排序的平均时间复杂度是, 1<s<2, s根据所选的分组而定。希尔排序同直接插入排序一样,不需要借助额外的辅助空间,空间复杂度为。
6.归并排序
思想:归并排序法是将两个(或者两个以上)有序表合成新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
代码如下:
public class Test6 {
public static void main(String[] args) {
int a[]=new int[]{10,9,8,7,6,5,4,3,2,1};
System.out.print("排序前的结果为: ");
for(int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
Test6 test6=new Test6();
test6.mergeSort(a,0,a.length-1);
System.out.print("排序后的结果为: ");
for(int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
}
public void mergeSort(int a[],int l,int r){
if(l<r){//当子序列中只有一个元素时结束递归
int m=(l+r)/2;//划分为两个子序列
mergeSort(a, l, m);//对左侧子序列进行递归排序
mergeSort(a, m+1, r);//对右侧子序列进行递归排序
merge(a,l, m, r);//合并
}
}
public void merge(int a[] ,int l,int m,int r){//两个排好序的子序列合并为一个子序列
int temp[]=new int[a.length];//辅助数组
int p1=l,p2=m+1;
int k=l;
while(p1<=m && p2<=r){
if(a[p1]>a[p2]){
temp[k++]=a[p2++];
}else{
temp[k++]=a[p1++];
}
}
while(p1<=m){ //左边的序列没有合并完
temp[k++]=a[p1++];
}
while(p2<=l){ //右边的序列没有合并完
temp[k++]=a[p2++];
}
for(int i=l;i<=r;i++){ //将合并好有序的数组复制到原数组中去
a[i]=temp[i];
}
}
}
排序前的结果为: 10 9 8 7 6 5 4 3 2 1
排序后的结果为: 1 2 3 4 5 6 7 8 9 10
结论:归并排序是一个稳定的排序算法,并且最好最坏平均时间复杂度均为,由于排序过程时候需要额外的数组空间,故其空间复杂度为。
7.桶排序
思想:把数组a划分为n个相同大小的相同的子区间(桶),每个子区间各自排序,最后合并。计数排序是桶排序的一种特殊情况,可以把计数排序当成每个桶只有一个元素的情况。
代码如下:
import java.util.ArrayList;
import java.util.Collections;
public class Test7 {
public static void main(String[] args) {
int a[]=new int[]{21,8,6,11,36,50,27,42,0,12};
System.out.print("排序前的结果为: ");
for(int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
Test7 test7=new Test7();
test7.bucketSort(a);
System.out.print("排序后的结果为: ");
for(int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
}
public void bucketSort(int a[]){
int min=Integer.MAX_VALUE;
int max=Integer.MIN_VALUE;
int len=a.length;
for(int i=0;i<len;i++){ //获得数组中的最大值和最小值
max=Math.max(max,a[i]);
min=Math.min(min,a[i]);
}
int bucketNum=(max-min)/len+1; //注意一定要加1
ArrayList<ArrayList<Integer>> bucketArr=new ArrayList<>(bucketNum);
for(int i=0;i<bucketNum;i++){
bucketArr.add(new ArrayList<Integer>());
}
for(int i=0;i<len;i++){
int num=(a[i]-min)/len; //每个元素应该放在桶中的位置
bucketArr.get(num).add(a[i]);
}
//对某个桶进行排序
for (int i=0;i<bucketArr.size();i++){
Collections.sort(bucketArr.get(i));
}
int count=0;
for (int i=0;i<bucketArr.size();i++){ //将桶内元素复制到数组中
for(int j=0;j<bucketArr.get(i).size();j++)
a[count++]=bucketArr.get(i).get(j);
}
}
}
排序前的结果为: 21 8 6 11 36 50 27 42 0 12
排序后的结果为: 0 6 8 11 12 21 27 36 42 50
结论:N为关键字的数量,M为桶的数量。桶排序的平均时间复杂度为线性的O(N+C),其中C=N*(logN-logM)。如果相对于同样的N,桶数量M越大,其效率越高,N=M时,即极限情况下每个桶只有一个数据时。桶排序的最好效率能够达到O(N)。桶排序的空间复杂度为O(N+M),如果输入数据非常庞大,而桶的数量也非常多,则空间代价无疑是昂贵的。此外,桶排序是稳定的。
8.计数排序
思想:找出待排序的数组中最大和最小的元素,并且统计数组中每个值为i的元素出现的次数,存入数组a的第i项,对所有的计数累加(从a中的第一个元素开始,每一项和前一项相加),最后将每个元素i放在新数组的第a(i)项,每放一个元素就将a(i)减去1。
代码如下:
public class Test8 {
public static void main(String[] args) {
int a[]=new int[]{21,8,6,11,36,50,27,42,0,12};
System.out.print("排序前的结果为: ");
for(int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
Test8 test8=new Test8();
test8.countSort(a);
System.out.print("排序后的结果为: ");
for(int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
}
public void countSort(int a[]){
//1.得到数列的最大值和最小值,并算出差值d
int max =a[0];
int min =a[0];
for(int i=1; i<a.length; i++) {
if(a[i] > max) {
max = a[i];
}else if(a[i] < min) {
min = a[i];
}
}
//2.创建统计数组并统计每个元素的个数,注意统计数组的长度
int[]countArray = new int[max-min+1];
for(int i=0; i<a.length; i++) {
countArray[a[i]-min]++;
}
//3.统计数组做变形,后面的元素等于前面的元素之和
int sum = 0;
for(int i=0;i<countArray.length;i++) {
sum += countArray[i];
countArray[i] = sum;
}
//4.倒序遍历原始数列,从统计数组找到正确位置,输出到结果数组
int[] sortedArray = new int[a.length];
for(int i=a.length-1;i>=0;i--) {
sortedArray[countArray[a[i]-min]-1]=a[i];
countArray[a[i]-min]--;
}
//5.将排完序的数组赋值到原数组里
for(int i=0;i<a.length;i++){
a[i]=sortedArray[i];
}
}
}
排序前的结果为: 21 8 6 11 36 50 27 42 0 12
排序后的结果为: 0 6 8 11 12 21 27 36 42 50
结论:计数排序是一个非基于比较的排序算法,假如数列的原始规模是n,最大最小整数的差值是m,则它的时间复杂度为,快于任何比较排序算法,当然这是一种牺牲空间换取时间的做法。只考虑统计数组的大小的话,它的空间复杂度为。同时计数排序算法是一个稳定的排序算法。
9.基数排序(桶子法)
思想:现将所有比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行依次排序。这样从最低位排序一直到最高位排序完成后,数列就变成了一个有序序列。
import java.util.ArrayList;
public class Test9 {
public static void main(String[] args) {
int a[]=new int[]{21,80,6,1100,36,50,27,42,340,0,12};
System.out.print("排序前的结果为: ");
for(int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
Test9 test9=new Test9();
test9.radixSort(a);
System.out.print("排序后的结果为: ");
for(int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
}
public void radixSort(int a[]){
//确定排序的趟数,即最大整数的位数
int max=a[0];
int len=a.length;
for(int i=1;i<len;i++){
if(a[i]>max){
max=a[i];
}
}
int time=String.valueOf(max).length();
ArrayList<ArrayList<Integer>>queue=new ArrayList<>(10);
for (int i=0;i<10;i++){
queue.add(new ArrayList<>());
}
//进行time次分配和收集元素
for(int i=0;i<time;i++){
//分配数组元素
for(int j=0;j<a.length;j++){
//得到数字的第i+1位数
int x=a[j]%(int)Math.pow(10,i+1)/(int)Math.pow(10,i);
ArrayList<Integer>queue1=queue.get(x);
queue1.add(a[j]);
queue.set(x,queue1);
}
int count=0;//元素计数器
//收集队列元素
for(int k=0;k<10;k++){
while (queue.get(k).size()>0){
ArrayList<Integer>queue2=queue.get(k);
a[count++]=queue2.get(0); //将队列中第i+1次排完序的结果赋值给数组
queue2.remove(0);
}
}
}
}
}
排序前的结果为: 21 80 6 1100 36 50 27 42 340 0 12
排序后的结果为: 0 6 12 21 27 36 42 50 80 340 1100
结论:基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。时间效率 :设待排序列为n个记录,d个关键码(d表示最大数字的位数),关键码的取值范围为r,则进行链式基数排序的时间复杂度为O(d(n+r)),其中,一趟分配时间复杂度为O(n),一趟收集时间复杂度为O(r),共进行d趟分配和收集。 空间效率:需要2*r个指向队列的辅助空间,以及用与静态链表的n个指针。此外,基数排序是稳定的排序算法。
10.堆排序
思想:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后去掉该最大元素,将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次大值。如此反复执行,便能得到一个有序序列了。
代码如下:
public class Test10 {
public static void main(String[] args) {
int a[]=new int[]{21,80,6,1100,36,50,27,42,340,0,12};
System.out.print("排序前的结果为: ");
for(int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
Test10 test10=new Test10();
test10.heapSort(a);
System.out.print("排序后的结果为: ");
for(int i=0;i<a.length;i++){
System.out.print(a[i]+" ");
}
}
public void heapSort(int[] a) {
int len=a.length;
//索引从0开始,最后一个非叶子结点的下标是len/2-1
for(int i=len/2-1;i>=0;i--){
adjustHeap(a,i,len);
}
//建堆结束之后,开始调整堆
for(int j=len-1;j>0;j--){
//把大根堆的根元素与堆最后一个元素交换位置
swap(a,0,j);
//每一次的堆调整之后,都会有一个元素到达自己的最终位置
adjustHeap(a,0,j);
}
}
//交换元素
public void swap(int a[],int i,int j){
int temp=a[i];
a[i]=a[j];
a[j]=temp;
}
//i为要调整的根结点的元素下标,len为要调整的那部分的数组长度
public void adjustHeap(int a[],int i,int len){
//先取出当前元素
int temp=a[i];
//2*i+1为左子树i的左子树
for(int k=2*i+1;k<len;k=2*k+1){
//让k指向子节点中最大的结点
if(k+1<len && a[k]<a[k+1]){
k++;
}
if(a[k]>temp){
swap(a,i,k);
i=k;
}else{
break;
}
}
}
}
排序前的结果为: 21 80 6 1100 36 50 27 42 340 0 12
排序后的结果为: 0 6 12 21 27 36 42 50 80 340 1100
结论:堆排序是一种不稳定的排序算法,它的最好最坏以及平均的时间复杂度均为,由于不需要借助额外的辅助空间,它的空间复杂度为。