一篇文章搞懂java十大经典算法

程序=数据结构+算法
刚步入学习程序的第一门课,我们就看到了这句话。
1)算法:解决问题的流程/步骤(顺序、分支、循环…)
2)数据结构:将数据结构按照某种特定的结构来保存
通俗的说,算法相当于逻辑,属于人们对特定模式抽象出来的核心,比如排序、查找,可以看作是以中模式。对应于业务来说,以中逻辑(可能由其他原子逻辑结合而成),一旦确定下来,便可看作常量,固定不变。
以下,是java十大经典排序算法:
1.插入排序(Insertion Sort)
插入排序的算法描述的是一种简单直观的排序算法。它的原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在是线上,通常采用in-place排序(即只需要用到0(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
一般来说,插入排序都采用in-place在数组史昂实现。具体算法描述如下:

  1. 从第一个元素开始,该元素可以认为已经被排序;

  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描;

  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置;

  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;

  5. 将新元素插入到该位置后;

  6. 重复步骤2~5。
    代码实现:
    public static int[] insertionSort(int[] array){
    if(array.length==0){
    return array;
    }
    int current;
    for(int i=0;i<array.length-1;i++){
    current = array[i+1];
    int preIndex = i;
    while (preIndex>=0&&current<array[preIndex]){
    array[preIndex+1] = array[preIndex];
    preIndex–;
    }
    array[preIndex+1] = current;
    }
    return array;
    }
    算法分析:
    最佳情况:T(n) = O(n)
    最坏情况:T(n) = O(n^2)
    平均情况:T(n) = O(n^2)
    注:n^2就是n的平方
    2.选择排序(Selection sort)
    选择排序(Selection sort)是以中简单那直观的排序算法。它的基本思想是:首先在末排序的数列中找到最小(or最大)元素,然后讲其存放到数列的起始位置;接着,再从剩余末排序的元素中继续寻找最小(or最大)元素,然后放到已排序序列的末尾,直到所有元素均排序完毕。
    算法描述:
    1.初始状态:无序区为R[1…n],有序区为空;
    2.第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1…i-1]和R[i…n]。该趟排序从当前无序区中选出最小的记录 R[k],将它与无序区的第1个记录R[i]交换,使R[1…i]和R[i+1…n]分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
    3.循环n-1次,排序完成。
    代码实现:
    public static void selectSort(int[] a, int n) {
    int i; // 有序区的末尾位置
    int j; // 无序区的起始位置
    int min; // 无序区中最小元素位置

    for(i=0; i<n; i++) {
        min=i;
    
        // 找出"a[i+1] ... a[n]"之间的最小元素,并赋值给min。
        for(j=i+1; j<n; j++) {
            if(a[j] < a[min])
                min=j;
        }
    
        // 若min!=i,则交换 a[i] 和 a[min]。
        // 交换之后,保证了a[0] ... a[i] 之间的元素是有序的。
        if(min != i) {
            int tmp = a[i];
            a[i] = a[min];
            a[min] = tmp;
        }
    }
    

    }
    算法分析:
    选择排序的时间复杂度是O(N2)。选择排序是稳定的算法,它满足稳定算法的定义。
    算法稳定性 – 假设在数列种存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍在a[j]之前。则这个算法是稳定的。
    3.冒泡排序(Bubble sort)
    重复的走访过要排序的数列,依次比较两个元素,如果他们的顺序错误,就把它们交换过来。走访数列的工作是重复的进行,直到没有再需要的交换。原理是临近的数字两两进行比较,按照从小到大或者从大到小的顺序进行交换。
    算法描述:
    1.初始状态:无序区为R[1…n],有序区为空;
    2.第i趟排序(i=0,1,2,3,…n-1)开始时,当前无序区和有序区分别为R[0…n-i]和R[n-i+1…n]。对每一对相邻元素进行比较,从开始第一对到结尾的最后一对,如果第一个比第二个大,就交换它们两个,这样在最后的元素应该会是最大的数,使R[1…n-i-1]和R[n-i…n]分别变为记录个数减少1个的新无序区和记录个数增加1个的新有序区;
    3.循环n-1次,直到排序完成。
    代码实现:
    public static void bubbleSort(int[] a){
    int temp;
    for(int i = 0; i < a.length - 1; i++){ //外层循环,从数组第一个元素到倒数第二个元素,时间复杂度为N
    for(int j = 0; j < a.length - 1 -i; j++){ //内层循环,从数组第一个元素到剩余的元素(减去有序区的元素)
    if(a[j] > a[j+1]){
    temp = a[j+1];
    a[j+1] = a[j];
    a[j] = temp; //相邻元素只要前面的比后面的大,就交换位置
    }
    }
    }
    }
    算法分析:
    冒泡排序在代码实现上是最简单的,不需要什么思考,两层for循环嵌套,比大小交换。因为冒泡通常的例子都是让大的往后移。时间复杂度为O(n2)。
    4.希尔排序(Shell sort)
    插入排序的算法复杂度为O(n2),但如果序列为正序可提高到O(n),而且直接插入排序算法比较简单,希尔排序利用这两点得到了一种改进后的插入排序。希尔排序是插入排序的改进版,优先比较距离远的元素,减少交换此数。
    void ShellInsert(int* pDataArray, int d, int iDataNum)
    {
    for (int i = d; i < iDataNum; i += 1) //从第2个数据开始插入
    {
    int j = i - d;
    int temp = pDataArray[i]; //记录要插入的数据
    while (j >= 0 && pDataArray[j] > temp) //从后向前,找到比其小的数的位置
    {
    pDataArray[j+d] = pDataArray[j]; //向后挪动
    j -= d;
    }

    if (j != i - d)    //存在比其小的数
    	pDataArray[j+d] = temp;
    

    }
    }
    算法分析:
    平均时间复杂度:希尔排序的时间复杂度和其增量序列有关系,例如希尔增量时间复杂度为O(N2),而Hibbard增量的希尔排序的时间复杂度为O(N1.5),希尔排序时间复杂度的下界是Nlg2n。希尔排序没有快速排序算法块O(N(lgN)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。但是比O(N²)复杂度的算法快得多。并且希尔排序非擦汗给你容易实现,算法代码短而简单。此外,希尔算法在最坏的情况下和平均情况下执行效率象查不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差。
    5.堆排序(Heap sort)
    堆排序是完全二叉树,分为最大堆和最小堆,最大堆要求节点的元素都要不小于其他孩子(最小堆要求节点元素都不大于其他左右孩子),对左右孩子的大小关系不做要求,所以处于最大堆的根节点的元素一定是这个堆中的最大值。堆排序算法就是抓住了堆的这一特点,每次都取堆顶的元素,将其放在序列最后面,然后将剩余的元素重新调整为最大堆,一次类推,最终得到排序的序列。
    public static void heapSort(int[] a){
    int N = a.length;
    for(int k = N/2; k >= 1; k–){ //for循环用来构造堆,最终生成最大堆
    sink(a,k,N);
    }
    while(N > 1){ //循环排序无序区
    exch(a,1,N–); //堆顶a[1]与堆的最后一个元素a[N]交换位置,并且去掉最后一个元素到有序区,减小新堆
    sink(a,1,N); //重新生成为最大堆
    }
    }

/**

  • 从上至下堆有序化
    /
    private static void sink(int[] a,int k,int N){
    while(2
    k <= N) {
    int j = 2*k;
    if(j < N && a[j] < a[j+1]){ //j<n保证j+1不越界,a[j]和a[j+1]是a[k]的左右子节点,这里是为了选取两个子节点较大的一个,a[j]大取a[j],a[j]小取a[j++]>= a[j]) { //如果父节点大于等于值大的子节点,堆有序,终止循环
    break;
    }
    if(a[k] >= a[j]) { //如果父节点大于等于值大的子节点,堆有序,终止循环
    break;
    }
    exch(a,k,j); //交换值大的子节点和父节点的值,达到堆有序
    k = j; //子节点,作为下一个循环的父节点,继续下沉
    }
    }

/**

  • 交换两个元素
    */
    private static void exch(int[] a,int i,int j){
    int temp = a[i];
    a[i] = a[j];
    a[j] = temp;
    }
    }
    </n保证j+1不越界,a[j]和a[j+1]是a[k]的左右子节点,这里是为了选取两个子节点较大的一个,a[j]大取a[j],a[j]小取a[j++]>
    算法分析:
    它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。
    6.归并排序(Merge sort)
    将两个有序数列合并成一个有序数列,我们称之为“归并”。递归的把有序类均分为两个子序列,使子序列有序,合并子序列
    1.把长度为n的输入序列分成长度为n/2的子序列;
    2.对这两个子序列分别采用归并排序;
    3.将两个排序好的子序列合并成一个最终的排序序列。
    java实现:
    private static int[] aux; //归并所需的辅助数组
    public static void mergeSort(int[] a){
    aux = new int[a.length];
    sort(a,0,a.length-1); //开始递归排序
    }

/**

  • 递归的排序归并
    */
    private static void sort(int[] a,int left,int right){
    if(right <= left){ //排序从左到右,确保右边比左边大
    return;
    }
    int mid = (left + right)/2; //找出中间位置
    sort(a,left,mid); //将左半边排序
    sort(a,mid+1,right); //将右半边排序
    merge(a,left,mid,right); //归并结果
    }

/**

  • 原地归并方法
    */
    private static void merge(int[] a,int left,int mid,int right){ //将a[left…mid]和a[mid+1…right]归并
    int i = left,j = mid + 1; //左右半边起始位置
    for(int k = left; k <= right; k++){ //将a[left…right]复制到aux[left…right]
    aux(k) = a(k);
    }
    for(int k = left; k <= right; k++){ //归并回到a[left…right]
    if(i > mid){ //i比mid大代表左半边用完,只有右半边有元素
    a[k] = aux[j++]; //右边元素给a[k]
    }else if(j > right){ //j比right大代表右半边用完,只有左半边有元素
    a[k] = aux[i++]; //左边元素给a[k]
    }else if(aux[j] < aux[i]){ //如果右边元素大于左边
    a[k] = aux[j++]; //右边元素给a[k]
    }else{ //否则左边大于等于右边
    a[k] = aux[i++]; //左边元素给a[k]
    }
    }
    }
    并归排序是分治法的典型应用,高效而为你顶,但是归并排序需要一个数组长度的辅助空间,再空间成本高德时候不合适使用。
    6.快速排序(Quick sort)
    通过一趟排序,将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分的关键字晓,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
    1.从数列中挑出一个元素,称为 “基准”(pivot);
    2.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
    3.递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
    public static void quickSort(int[] a){
    sort(a,0,a.length-1);
    }

/**

  • 递归进行快速排序
    */
    private static void sort(int[] a,int left,int right){
    if(right <= left){ //排序从左到右,确保右边比左边大
    return;
    }
    int j = partition(a,left,right); //切分
    sort(a,left,j-1); //将左半边排序
    sort(a,j+1,right); //将右半边排序
    }

/**

  • 快速排序切分
    */
    private static int partition(int[] a,int left,int right){
    int i = left,j = right + 1; //左右扫描指针
    int v = a[left]; //选取切分元素,这里选第一个,实际数据可以自行挑选
    while(true){
    while(a[++i] < v){ //a[i]<v时增大i,只要比v小继续往右扫描 i="=" v="">< a[–j]){ //a[j]>v时减小j,只要比v大继续往左扫描
    if(j == left){ //扫描到左边则终止
    break;
    }
    }
    while(v < a[–j]){ //a[j]>v时减小j,只要比v大继续往左扫描
    if(j == left){ //扫描到左边则终止
    break;
    }
    }
    if(i >= j){ //如果左右指针交叉,终止循环
    break;
    }
    exch(a,i,j); //不满足上述条件(左边比v大,右边比v小,指针未交叉),左右元素交换位置
    }
    exch(a,left,j); //将切分元素v放入正确的位置
    return j; //a[left…j-1]<=a[j]<=a[j+1…right],并返回j
    }

/**

  • 交换两个元素
    */
    private static void exch(int[] a,int i,int j){
    int temp = a[i];
    a[i] = a[j];
    a[j] = temp;
    }</v时增大i,只要比v小继续往右扫描>
    快速排序是通常情况下的最优选择,高效,只需要lgN级别的辅助空间,但是快速排序不稳定,受切分点的影响很大。
    七中排序总结
    七种排序性能表
    其中插入排序,选择排序,冒泡排序都是简单排序,时间复杂度是O(N2),其中插入排序和冒泡排序适合原始序列有序的数组,选择排序的交换和赋值次数会比较少,可以根据不同环境和数据的实际情况和长度选择具体的排序。整体插入排序优于选择排序优于冒泡排序。希尔排序是插入排序的优化,突破了前三个排序O(N2)的时间复杂度,但是本质还是插入排序,突破比较相邻元素的惯性思维,直接比较一定间隔的元素,大幅度减少了逆序调整的比较次数和交换次数,从而达到比较理想的算法复杂度,适合对中等规模数组进行排序。堆排序是利用了最大堆的特点,始终把堆顶元素(最大元素)和最后一个元素替换,再重新构造最大堆,重复执行达到排序的效果。堆结构的特性让算法的复杂度降低到NlgN级别,但是有不方便索引元素的确定,缓存命中率较低。而归并排序则是充分运用了分治原理,把大问题不断的拆分成小问题,进行排序,再合并小数组达到整体排序的目标,归并排序即高效又可靠,唯一的缺点是需要数组长度的辅助空间,在空间成本低的时候适合使用。快速排序则解决了归并排序占用空间的问题,在数组内部用很小的辅助栈,即可完成对元素的分离,再去解决分离后的更小的数组,正常情况下拥有和归并相同级别的时间复杂度,但是得注意选取好切分元素。 实际上一个复杂的序列可能用不止一种排序,例如分治和快速排序在分割到很小的序列时再进行分割反而效率不如插入排序等简单排序,可以设置一定的阈值,先用分治或者快速排序的方式分割数组,再转换成插入等简单排序完成最终的排序。
  • 1
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值