重新认识下数据结构与算法(二)数组的排序算法

本文深入探讨了排序算法,包括冒泡排序、选择排序、插入排序、归并排序和快速排序。文章指出,快速排序和归并排序的时间复杂度最低,为O(N*logN),其中快速排序在空间效率上更优。对于O(N^2)复杂度的排序算法,如冒泡、选择和插入排序,它们在特定情况下仍有应用,但效率较低。
摘要由CSDN通过智能技术生成

 

在我们程序的开发过程中,数据不仅仅只是存储,还经常需要对数据做一定的处理。这就是算法,如果是几百条,几千条甚至几万条的数据的话,可能我们对算法的效率和时间复杂度没那么敏感。但是如果我们面对的是几百万,几千万甚至几亿,几十亿,几百亿的数据的话,那么我们对算法的要求就非常高了。下面就是我们开发中经常用到的算法。

1.数组

与数组相关的算法很多,但最基本最常见的就是排序的算法

在排序过程中我们经常会用到时间复杂度来描述一个算法的好坏,上面是我们常见的一些复杂度的函数图。

(1)冒泡排序

冒泡排序的思想是这样的:比较数组中第0个对象与第1个对象的关键字大小,如果第0个大的话则与第1个调换位置,然后再第1个与第2个比较,如果第1个大的话则调换位置,接下来第2个...,以此类推直到倒数最后1个与倒数第2个相比,这样就算第一轮。接下来从头开始,还是第0个与第1个,只是到最后只需要与倒数第2个相比就可以了,因为倒数第1个经过第一轮比较已经是最大的了。以此类推,直到最后只剩第1个和第2个相比就算完成。

冒泡排序因为是将最大的数一个一个往上推,想象成鱼在吐泡一样,所以叫冒泡排序。

public void bubbleSort(int[] a){
    int size = a.length;
    int out,in;
    //第一个for循环,控制比较的终点
    for(out=size-1;out>1;out--){
        //控制相邻两个数之间的比较
        for(in=0;in<out;in++){
            if(a[in]>a[in+1]){
                swap(a[in],a[in+1]);
            }
        }
    }
}
private void swap(int a,int b){
    int temp = a;
    a = b;
    b = temp;
}

上面的代码为冒泡排序算法的代码。如代码所示,冒泡排序的算法是比较容易实现的,但是他的时间复杂度确实非常大的。假设一个数组有10项数据的话,那么第一轮要9次比较,第二轮8次,以此类推,到最后一轮是1次。所以如果一个数组有N项的话,那么他的比较次数为 (N-1)+(N-1)+...+1=N*(N-1)/2;平均需要N^2/2次比较,而交换的概率大概为比较的一半,就是N^2/4。比较和交换都和N^2成正比。

所以,冒泡排序的时间复杂度为O(N^2)。

(2)选择排序

选择排序的思想是:定义一个最小值的中间变量,然后先把数组的第一个元素赋值给他,接下来遍历整个数组,一个一个和这个中间变量比较,如果比这个中间变量小的话就把值赋给这个中间变量,最后遍历完整个数组,这个中间变量就是最小值了。接下来是从第二个元素开始赋值给这个最小变量,然后再遍历一遍数组。以此类推直到从倒数第二个元素赋值开始遍历。

public void selectionSort(int[] a){
    int size = a.length;
    int in,out,min;
    for(out = 0;out<size-1;out++){
        //假设开始遍历的第一数是余下数中最小的
        min=out
        for(in = out+1;in<size -1;in++){
            if(a[in]<a[min]){
               //如果有比min更小的,则把这个值赋给min
               min = in; 
            }
        }
        //当一轮遍历结束后,就可以进行值的交换了
        swap(a[out],a[min]);
    }
}
private void swap(int a,int b){
    int temp = a;
    a = b;
    b = temp;
}

虽然,比较的次数还是和冒泡排序一样N*(N-1)/2;平均需要N^2/2次比较,但是交换的次数只剩下N次了,所以比冒泡排序有效多了。

选择排序的时间复杂度还是O(N^2)。

(3)插入排序

插入排序的思想是:把一个大的数分成两个小的数组,左边的数组是有序的,而右边的数组是无序的。我们只需要遍历右边的数组,从第一个遍历到最后一个,然后一个一个和左边的数组的元素进行对比,然后在合适的位置插入他,这样,左边的元素就越来越多并且是有序的,右边的元素就越来越少直至没有为止。插入排序经常被用到高级排序中,比如快速排序。

public void insertionSort(int[] a){
    int size = a.length;
    int in,out;
    for(out=1;out<size;out++){
        int temp = a[out];
        in = out;
        while(in>0&&a[in-1]>=temp){
            a[in] = a[in-1];
            --in;
        }
        a[in] = temp;
    }
}

在上面的算法中,第一个for循环遍历的是右边无序的数组,定义了一个中间量temp来存储右边数组的值,而里面的while循环遍历的是左边有序的数组,取右边无序的数组与左边有序数组相比然后插入到合适的为止。

插入排序比较的次数是多少呢?第一轮需要比较1次,第二轮需要比较2次,以此类推,最后一轮最多需要比较N-1次,所以比较次数也为N*(N-1)/2,和选择排序和冒泡排序一样。但是交换的次数小于N-1次,所以他比选择排序和冒泡排序更快。

插入排序的时间复杂度还是O(N^2)。

(4)归并排序

归并排序是一种比冒泡、选择和插入排序时间效率更高的排序。归并排序的思想是把一个数组分成两个数组,然后对这两个数组分别进行排序,再用下面的merge方法将两个数组归并成一个有序的数组。

    public static void main(String[] args) {
        int[] arrayA = {23,47,81,95};
        int[] arrayB = {7,14,39,55,62,74};
        int[] arrayC = new int[10];
        merge(arrayA,arrayA.length,arrayB,arrayB.length,arrayC);
    }
    public static void merge(int[] arrayA,int sizeA,int[] arrayB,int sizeB,int[] arrayC){
        int aDex = 0,bDex = 0,cDex = 0;
        while(aDex<sizeA && bDex<sizeB){
            if(arrayA[aDex]<arrayB[bDex]){
                arrayC[cDex++] = arrayA[aDex++];
            }else{
                arrayC[cDex++] = arrayB[bDex++];
            }
        }
        while (aDex<sizeA){
            arrayC[cDex++] = arrayA[aDex++];
        }
        while (bDex<sizeB){
            arrayC[cDex++] = arrayB[bDex++];
        }
    }

如何将一个数组分为两个数组并排序呢?我们可以再将这两个数组再次分为更小的四个数组,以此类推,直到分到的最小数组已经只有一个元素,我们就可以认定这个数组是有序的,然后再将两个小数组排好序之后就变成大数组,最后得到的最大的数组就是有序的了,所以我们可以采用递归调用的方法。

    public void mergeSort(){
        long[] workSpace = new long[nElems];
        recMergeSort(workSpace,0,nElems-1);

    }
    private void recMergeSort(long[] workSpace,int lowerBound,int upperBound){
        if(lowerBound == upperBound){
            return;
        }else{
            int mid = (lowerBound+upperBound)/2;
            recMergeSort(workSpace,lowerBound,mid);
            recMergeSort(workSpace,mid+1,upperBound);
            merge(workSpace,lowerBound,mid+1,upperBound);
        }
    }
    private void merge(long[] workSpace,int lowPtr,int highPtr,int upperBound){
        int j = 0 ;
        int lowerBound = lowPtr;
        int mid = highPtr -1;
        int n = upperBound - lowerBound +1;
        while(lowPtr<=mid && highPtr<=upperBound){
            if(theArray[lowPtr]<theArray[highPtr]){
                workSpace[j++]=theArray[lowPtr++];
            }else{
                workSpace[j++]=theArray[highPtr++];
            }
        }
        while (lowPtr<=mid){
            workSpace[j++] = theArray[lowPtr++];
        }
        while(highPtr<=upperBound){
            workSpace[j++] = theArray[highPtr++];
        }
        for(j=0;j<n;j++){
            theArray[lowerBound+j] = workSpace[j];
        }
    }

归并排序效率是如何呢?假设有N个元素,那么需要递归的次数为log2N,每一次需要复制的次数为N,所以总共需要N*log2N次数的复制,比较的话比复制少,所以归并排序的时间复杂度为O(N*logN)。比冒泡、选择和插入排序都快一点。但是归并排序需要的是两倍的存储空间来进行排序,所以如果对空间有要求的一般都不使用归并排序。

(5)希尔排序

希尔排序是对插入排序的一种优化。在插入排序中每次只能将数据移动一位,如果是较小的数存在数组的右边的话,这样左边的数组的元素移动的次数就会非常多。如果插入排序的数组是基本有序的话,那么插入排序的时间复杂度大概只需要O(N),所以希尔排序就从这点出发,对插入排序进行优化。

希尔排序通过加大插入排序中元素之间的间隔,并在这些间隔的元素中进行插入排序,从而使数据项能大跨度地移动。当这些数据项拍过一趟序之后,再减小插入排序中元素之间的间隔进行排序,最后直到排序中元素之间的间隔为1,排完序之后整个数组就是有序的了。

间隔取多大合适呢?一般根据公式h=3*h+1来取间隔。

    public void shellSort(){
        int inner,outer;
        long temp;
        int h = 1;
        while(h<=nElems/3){
            h = h*3+1;
        }
        while (h>0){
            for(outer=h;outer<nElems;outer++){
                temp = theArray[outer];
                inner = outer;
                while(inner>h-1&&theArray[inner-h]>=temp){
                    theArray[inner] = theArray[inner-h];
                    inner -= h;
                }
                theArray[inner] = temp;
            }
            h = (h-1)/3;
        }
    }

希尔排序的时间复杂度是多少呢?根据估算,希尔排序的时间复杂度为O(N*(logN)^2)。

(6)   快速排序

快速排序之所以叫快速排序是因为他的算法非常快。快速排序的思想是:将一个大数组划分为两部分,根据什么划分呢?取一个数,数组左边的数据全都小于这个数,数组右边的数据全都大于这个数,然后再将左边的小数组划分为两个小部分,左边的部分也是小于某个数,右边的部分也是大于这个数,以此类推,直到划分的小数组的数据量为2个时,那么它们就是就有序,这样大数组也就是有序的。可以发现,快速排序也是采用递归的算法。下面这张图展示了快速排序的过程。

这个划分的数如何取呢?一般来说都是取数组的第一个,最后一个和中间位置的数据的中间值作为划分的值。 这样就可以避免数组已经有序或者逆序的情况下,选择最大或者最小的数据项作为划分值造成的算法很低效的问题。

    public void quickSort(){
        recQuickSort(0,nElems-1);
    }
    public void recQuickSort(int left,int right){
        int size = right - left + 1;
        if(size<=3){
            manualSort(left,right);
        }else{
            long median = medianOf3(left,right);
            int partition = partitionIt(left,right,median);
            recQuickSort(left,partition-1);
            recQuickSort(partition+1,right);
        }
    }
    public int partitionIt(int left,int right,long pivot){
        int leftPtr = left;
        int rightPtr = right - 1;
        while(true){
            while(theArray[++leftPtr]<pivot);
            while(theArray[--rightPtr]>pivot);
            if(leftPtr>=rightPtr){
                break;
            }else{
                swap(leftPtr,rightPtr);
            }
        }
        swap(leftPtr,right-1);
        return leftPtr;
    }
    public long medianOf3(int left,int right){
        int center = (left+right)/2;
        if(theArray[left]>theArray[center]){
            swap(left,center);
        }
        if(theArray[left]>theArray[center]){
            swap(left,center);
        }
        if(theArray[left]>theArray[right]){
            swap(left,right);
        }
        if(theArray[center]>theArray[right]){
            swap(center,right);
        }
        swap(center,right-1);
        return  theArray[right-1];
    }
    public void swap(int dex1,int dex2){
        long temp = theArray[dex1];
        theArray[dex1] = theArray[dex2];
        theArray[dex2] = temp;
    }
    public void manualSort(int left,int right){
        int size = right - left + 1;
        if(size <= 1){
            return;
        }
        if(size == 2){
            if(theArray[left] > theArray[right]){
                swap(left,right);
            }
            return;
        }else{
            if(theArray[left]>theArray[right-1]){
                swap(left,right-1);
            }
            if(theArray[left]>theArray[right]){
                swap(left,right);
            }
            if(theArray[right-1]>theArray[right]){
                swap(right-1,right);
            }
        }
    }

代码如上,recQuickSort是进行快速排序的迭代方法,medianOf3是求数组第一个、最后一个和中间位置的值的中间值的方法,partitionIt方法是根据中间值来进行数组的划分的方法。

快速排序的时间复杂度为多少呢?因为递归排序需要递归调用logN次,每一次划分需要花费O(N),所以快速排序的时间复杂度O(N*logN)。

总结:在上面几种数组排序中,快速排序和归并排序的时间复杂度是最小的O(N*logN),但是由于归并排序需要一个额外的数组来进行排序,所以使用快速排序的空间效率可以更高些。不过基于递归的排序有个缺点,每一次递归都要将结果和调用参数存储在栈里面,很容易抛出内存不足的异常。而希尔排序的时间复杂度为O(N*(logN)^2)。其他的插入排序,选择排序和冒泡排序的时间复杂度都为O(N^2)。

所以时间复杂度方面:O(快速排序)=O(归并排序)<O(希尔排序)<O(插入排序)=O(选择排序)=O(冒泡排序)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值