「干货总结」程序员必知必会的十大排序算法

身为程序员,十大排序是是全部合格程序员所必备和掌握的,而且热门的算法好比快排、归并排序还可能问的比较细致,对算法性能和复杂度的掌握有要求。bigsai做为一个负责任的Java和数据结构与算法方向的小博主,在这方面确定不能让读者们有所漏洞。跟着本篇走,带你捋一捋常见的十大排序算法,轻轻松松掌握!java

首先对于排序来讲大多数人对排序的概念停留在冒泡排序或者JDK中的Arrays.sort(),手写各类排序对不少人来讲都是一种奢望,更别说十大排序算法了,不过还好你遇到了本篇文章!程序员

对于排序的分类,主要不一样的维度好比复杂度来分、内外部、比较非比较等维度来分类。咱们正常讲的十大排序算法是内部排序,咱们更多将他们分为两大类:基于比较和非比较这个维度去分排序种类。面试

  • 非比较类的有桶排序、基数排序、计数排序。也有不少人将排序概括为8大排序,那就是由于基数排序、计数排序是创建在桶排序之上或者是一种特殊的桶排序,可是基数排序和计数排序有它特有的特征,因此在这里就将他们概括为10种经典排序算法。而比较类排序也可分为
  • 比较类排序也有更细致的分法,有基于交换的、基于插入的、基于选择的、基于归并的,更细致的能够看下面的脑图。

image-20201120124138560

交换类

冒泡排序

冒泡排序,又称起泡排序,它是一种基于交换的排序典型,也是快排思想的基础,冒泡排序是一种稳定排序算法,时间复杂度为O(n^2).基本思想是:循环遍历屡次每次从前日后把大元素日后调,每次肯定一个最大(最小)元素,屡次后达到排序序列。(或者从后向前把小元素往前调)。算法

具体思想为(把大元素日后调):spring

  • 从第一个元素开始日后遍历,每到一个位置判断是否比后面的元素大,若是比后面元素大,那么就交换二者大小,而后继续向后,这样的话进行一轮以后就能够保证最大的那个数被交换交换到最末的位置能够肯定
  • 第二次一样从开始起向后判断着前进,若是当前位置比后面一个位置更大的那么就和他后面的那个数交换。可是有点注意的是,此次并不须要判断到最后,只须要判断到倒数第二个位置就行(由于第一次咱们已经肯定最大的在倒数第一,此次的目的是肯定倒数第二)
  • 同理,后面的遍历长度每次减一,直到第一个元素使得整个元素有序。

例如2 5 3 1 4排序过程以下:shell

image-20201120155114930

实现代码为:设计模式

public void  maopaosort(int[] a) {
  // TODO Auto-generated method stub
  for(int i=a.length-1;i>=0;i--)
  {
    for(int j=0;j<i;j++)
    {
      if(a[j]>a[j+1])
      {
        int team=a[j];
        a[j]=a[j+1];
        a[j+1]=team;
      }
    }
  }
}

 

 

快速排序

快速排序是对冒泡排序的一种改进,采用递归分治的方法进行求解。而快排相比冒泡是一种不稳定排序,时间复杂度最坏是O(n^2),平均时间复杂度为O(nlogn),最好状况的时间复杂度为O(nlogn)。数组

对于快排来讲,基本思想是这样的微信

  • 快排须要将序列变成两个部分,就是序列左边所有小于一个数序列右面所有大于一个数,而后利用递归的思想再将左序列当成一个完整的序列再进行排序,一样把序列的右侧也当成一个完整的序列进行排序。
  • 其中这个数在这个序列中是能够随机取的,能够取最左边,能够取最右边,固然也能够取随机数。可是一般不优化状况咱们取最左边的那个数。

image-20201120133851275

实现代码为:数据结构

public void quicksort(int [] a,int left,int right)
{
  int low=left;
  int high=right;
  //下面两句的顺序必定不能混,不然会产生数组越界!!!very important!!!
  if(low>high)//做为判断是否截止条件
    return;
  int k=a[low];//额外空间k,取最左侧的一个做为衡量,最后要求左侧都比它小,右侧都比它大。
  while(low<high)//这一轮要求把左侧小于a[low],右侧大于a[low]。
  {
    while(low<high&&a[high]>=k)//右侧找到第一个小于k的中止
    {
      high--;
    }
    //这样就找到第一个比它小的了
    a[low]=a[high];//放到low位置
    while(low<high&&a[low]<=k)//在low往右找到第一个大于k的,放到右侧a[high]位置
    {
      low++;
    }
    a[high]=a[low];         
  }
  a[low]=k;//赋值而后左右递归分治求之
  quicksort(a, left, low-1);
  quicksort(a, low+1, right);       
}

 


插入类排序

直接插入排序

直接插入排序在全部排序算法中的是最简单排序方式之一。和咱们上学时候 从前日后、按高矮顺序排序,那么一堆高低无序的人群中,从第一个开始,若是前面有比本身高的,就直接插入到合适的位置。一直到队伍的最后一个完成插入整个队列才能知足有序。

直接插入排序遍历比较时间复杂度是每次O(n),交换的时间复杂度每次也是O(n),那么n次总共的时间复杂度就是O(n^2)。有人会问折半(二分)插入可否优化成O(nlogn),答案是不能的。由于二分只能减小查找复杂度每次为O(logn),而插入的时间复杂度每次为O(n)级别,这样总的时间复杂度级别仍是O(n^2).

插入排序的具体步骤:

  • 选取当前位置(当前位置前面已经有序) 目标就是将当前位置数据插入到前面合适位置。
  • 向前枚举或者二分查找,找到待插入的位置。
  • 移动数组,赋值交换,达到插入效果。

image-20201120160709469

实现代码为:

public void insertsort (int a[])
{
  int team=0;
  for(int i=1;i<a.length;i++)
  {
    System.out.println(Arrays.toString(a));
    team=a[i];
    for(int j=i-1;j>=0;j--)
    {

      if(a[j]>team)
      {
        a[j+1]=a[j];
        a[j]=team;  
      } 
      else {
        break;
      }
    }
  } 
}

 


希尔排序

直接插入排序由于是O(n^2),在数据量很大或者数据移动位次太多会致使效率过低。不少排序都会想办法拆分序列,而后组合,希尔排序就是以一种特殊的方式进行预处理,考虑到了数据量和有序性两个方面纬度来设计算法。使得序列先后之间小的尽可能在前面,大的尽可能在后面,进行若干次的分组别计算,最后一组便是一趟完整的直接插入排序。

对于一个长串,希尔首先将序列分割(非线性分割)而是按照某个数模(取余这个相似报数一、二、三、4。一、二、三、4)这样形式上在一组的分割先各组分别进行直接插入排序,这样很小的数在后面能够经过较少的次数移动到相对靠前的位置。而后慢慢合并变长,再稍稍移动。

由于每次这样插入都会使得序列变得更加有序,稍微有序序列执行直接插入排序成本并不高。因此这样可以在合并到最终的时候基本小的在前,大的在后,代价愈来愈小。这样希尔排序相比插入排序仍是能节省很多时间的。

image-20201120164448973

实现代码为:

public void shellsort (int a[])
{
  int d=a.length;
  int team=0;//临时变量
  for(;d>=1;d/=2)//共分红d组
    for(int i=d;i<a.length;i++)//到那个元素就看这个元素在的那个组便可
    {
      team=a[i];
      for(int j=i-d;j>=0;j-=d)
      {             
        if(a[j]>team)
        {
          a[j+d]=a[j];
          a[j]=team;    
        }
        else {
          break;
        }
      }
    }   
}

 

 

选择类排序

简单选择排序

简单选择排序(Selection sort)是一种简单直观的排序算法。它的工做原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,而后,再从剩余未排序元素中继续寻找最小(大)元素,而后放到已排序序列的末尾。以此类推,直到全部元素均排序完毕。

image-20201120201910761

实现代码为:

public void selectSort(int[] arr) {
  for (int i = 0; i < arr.length - 1; i++) {
    int min = i; // 最小位置
    for (int j = i + 1; j < arr.length; j++) {
      if (arr[j] < arr[min]) {
        min = j; // 更换最小位置
      }
    }
    if (min != i) {
      swap(arr, i, min); // 与第i个位置进行交换
    }
  }
}
private void swap(int[] arr, int i, int j) {
  int temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}

 


堆排序

对于堆排序,首先是创建在堆的基础上,堆是一棵彻底二叉树,还要先认识下大根堆和小根堆,彻底二叉树中全部节点均大于(或小于)它的孩子节点,因此这里就分为两种状况

  • 若是全部节点大于孩子节点值,那么这个堆叫作大根堆,堆的最大值在根节点。
  • 若是全部节点小于孩子节点值,那么这个堆叫作小根堆,堆的最小值在根节点。

在这里插入图片描述

堆排序首先就是建堆,而后再是调整。对于二叉树(数组表示),咱们从下往上进行调整,从第一个非叶子节点开始向前调整,对于调整的规则以下:

建堆是一个O(n)的时间复杂度过程,建堆完成后就须要进行删除头排序。给定数组建堆(creatHeap)

①从第一个非叶子节点开始判断交换下移(shiftDown),使得当前节点和子孩子可以保持堆的性质

②可是普通节点替换可能没问题,对若是交换打破子孩子堆结构性质,那么就要从新下移(shiftDown)被交换的节点一直到中止。

在这里插入图片描述

堆构造完成,取第一个堆顶元素为最小(最大),剩下左右孩子依然知足堆的性值,可是缺个堆顶元素,若是给孩子调上来,可能会调动太多而且可能破坏堆结构。

①因此索性把最后一个元素放到第一位。这样只须要判断交换下移(shiftDown),不过须要注意此时整个堆的大小已经发生了变化,咱们在逻辑上不会使用被抛弃的位置,因此在设计函数的时候须要附带一个堆大小的参数。

②重复以上操做,一直堆中全部元素都被取得中止。

在这里插入图片描述

而堆算法复杂度的分析上,以前建堆时间复杂度是O(n)。而每次删除堆顶而后须要向下交换,每一个个数最坏为logn个。这样复杂度就为O(nlogn).总的时间复杂度为O(n)+O(nlogn)=O(nlogn).

实现代码为:

static void swap(int arr[],int m,int n)
{
  int team=arr[m];
  arr[m]=arr[n];
  arr[n]=team;
}
//下移交换 把当前节点有效变换成一个堆(小根)
static void shiftDown(int arr[],int index,int len)//0 号位置不用
{
  int leftchild=index*2+1;//左孩子
  int rightchild=index*2+2;//右孩子
  if(leftchild>=len)
    return;
  else if(rightchild<len&&arr[rightchild]<arr[index]&&arr[rightchild]<arr[leftchild])//右孩子在范围内而且应该交换
  {
    swap(arr, index, rightchild);//交换节点值
    shiftDown(arr, rightchild, len);//可能会对孩子节点的堆有影响,向下重构
  }
  else if(arr[leftchild]<arr[index])//交换左孩子
  {
    swap(arr, index, leftchild);
    shiftDown(arr, leftchild, len);
  }
}
//将数组建立成堆
static void creatHeap(int arr[])
{
  for(int i=arr.length/2;i>=0;i--)
  {
    shiftDown(arr, i,arr.length);
  }
}
static void heapSort(int arr[])
{
  System.out.println("原始数组为         :"+Arrays.toString(arr));
  int val[]=new int[arr.length]; //临时储存结果
  //step1建堆
  creatHeap(arr);
  System.out.println("建堆后的序列为  :"+Arrays.toString(arr));
  //step2 进行n次取值建堆,每次取堆顶元素放到val数组中,最终结果即为一个递增排序的序列
  for(int i=0;i<arr.length;i++)
  {
    val[i]=arr[0];//将堆顶放入结果中
    arr[0]=arr[arr.length-1-i];//删除堆顶元素,将末尾元素放到堆顶
    shiftDown(arr, 0, arr.length-i);//将这个堆调整为合法的小根堆,注意(逻辑上的)长度有变化
  }
  //数值克隆复制
  for(int i=0;i<arr.length;i++)
  {
    arr[i]=val[i];
  }
  System.out.println("堆排序后的序列为:"+Arrays.toString(arr));

}

 

 

归并类排序

在归并类排序通常只讲归并排序,可是归并排序也分二路归并、多路归并,这里就讲较多的二路归并排序,且用递归方式实现。

归并排序

归并和快排都是基于分治算法的,分治算法其实应用挺多的,不少分治会用到递归,但事实上分治和递归是两把事。分治就是分而治之,能够采用递归实现,也能够本身遍历实现非递归方式。而归并排序就是先将问题分解成代价较小的子问题,子问题再采起代价较小的合并方式完成一个排序。

至于归并的思想是这样的:

  • 第一次:整串先进行划分红一个一个单独,第一次是将序列中(1 2 3 4 5 6---)两两归并成有序,归并完(xx xx xx xx----)这样局部有序的序列。
  • 第二次就是两两归并成若干四个(1 2 3 4 5 6 7 8 ----)每一个小局部是有序的
  • 就这样一直到最后这个串串只剩一个,然而这个耗费的总次数logn。每次操做的时间复杂的又是O(n)。因此总共的时间复杂度为O(nlogn).

image-20201120173153449

合并为一个O(n)的过程:

image-20201120174526108

实现代码为:

private static void mergesort(int[] array, int left, int right) {
  int mid=(left+right)/2;
  if(left<right)
  {
    mergesort(array, left, mid);
    mergesort(array, mid+1, right);
    merge(array, left,mid, right);
  }
}

private static void merge(int[] array, int l, int mid, int r) {
  int lindex=l;int rindex=mid+1;
  int team[]=new int[r-l+1];
  int teamindex=0;
  while (lindex<=mid&&rindex<=r) {//先左右比较合并
    if(array[lindex]<=array[rindex])
    {
      team[teamindex++]=array[lindex++];
    }
    else {              
      team[teamindex++]=array[rindex++];
    }
  }
  while(lindex<=mid)//当一个越界后剩余按序列添加便可
  {
    team[teamindex++]=array[lindex++];

  }
  while(rindex<=r)
  {
    team[teamindex++]=array[rindex++];
  } 
  for(int i=0;i<teamindex;i++)
  {
    array[l+i]=team[i];
  }

}

 


桶类排序

桶排序

桶排序是一种用空间换取时间的排序,桶排序重要的是它的思想,而不是具体实现,时间复杂度最好多是线性O(n),桶排序不是基于比较的排序而是一种分配式的。桶排序从字面的意思上看:

  • 桶:若干个桶,说明此类排序将数据放入若干个桶中。
  • 桶:每一个桶有容量,桶是有必定容积的容器,因此每一个桶中可能有多个元素。
  • 桶:从总体来看,整个排序更但愿桶可以更匀称,即既不溢出(太多)又不太少。

桶排序的思想为:将待排序的序列分到若干个桶中,每一个桶内的元素再进行个别排序。 固然桶排序选择的方案跟具体的数据有关系,桶排序是一个比较普遍的概念,而且计数排序是一种特殊的桶排序,基数排序也是创建在桶排序的基础上。在数据分布均匀且每一个桶元素趋近一个时间复杂度能达到O(n),可是若是数据范围较大且相对集中就不太适合使用桶排序。

image-20201120180500488

实现一个简单桶排序:

import java.util.ArrayList;
import java.util.List;
//微信公众号:bigsai
public class bucketSort {
    public static void main(String[] args) {
        int a[]= {1,8,7,44,42,46,38,34,33,17,15,16,27,28,24};
        List[] buckets=new ArrayList[5];
        for(int i=0;i<buckets.length;i++)//初始化
        {
            buckets[i]=new ArrayList<Integer>();
        }
        for(int i=0;i<a.length;i++)//将待排序序列放入对应桶中
        {
            int index=a[i]/10;//对应的桶号
            buckets[index].add(a[i]);
        }
        for(int i=0;i<buckets.length;i++)//每一个桶内进行排序(使用系统自带快排)
        {
            buckets[i].sort(null);
            for(int j=0;j<buckets[i].size();j++)//顺便打印输出
            {
                System.out.print(buckets[i].get(j)+" ");
            }
        }   
    }
}

 


计数排序

计数排序是一种特殊的桶排序,每一个桶的大小为1,每一个桶不在用List表示,而一般用一个值用来计数。

设计具体算法的时候,先找到最小值min,再找最大值max。而后建立这个区间大小的数组,从min的位置开始计数,这样就能够最大程度的压缩空间,提升空间的使用效率。

在这里插入图片描述

public static void countSort(int a[])
{
  int min=Integer.MAX_VALUE;int max=Integer.MIN_VALUE;
  for(int i=0;i<a.length;i++)//找到max和min
  {
    if(a[i]<min) 
      min=a[i];
    if(a[i]>max)
      max=a[i];
  }
  int count[]=new int[max-min+1];//对元素进行计数
  for(int i=0;i<a.length;i++)
  {
    count[a[i]-min]++;
  }
  //排序取值
  int index=0;
  for(int i=0;i<count.length;i++)
  {
    while (count[i]-->0) {
      a[index++]=i+min;//有min才是真正值
    }
  }
}

 


基数排序

基数排序是一种很容易理解可是比较难实现(优化)的算法。基数排序也称为卡片排序,基数排序的原理就是屡次利用计数排序(计数排序是一种特殊的桶排序),可是和前面的普通桶排序和计数排序有所区别的是,基数排序并非将一个总体分配到一个桶中,而是将自身拆分红一个个组成的元素,每一个元素分别顺序分配放入桶中、顺序收集,当从前日后或者从后往前每一个位置都进行过这样顺序的分配、收集后,就得到了一个有序的数列。

image-20201113154119682

若是是数字类型排序,那么这个桶只须要装0-9大小的数字,可是若是是字符类型,那么就须要注意ASCII的范围。

因此遇到这种状况咱们基数排序思想很简单,就拿 934,241,3366,4399这几个数字进行基数排序的一趟过程来看,第一次会根据各位进行分配、收集:

image-20201113161050871

分配和收集都是有序的,第二次会根据十位进行分配、收集,这次是在第一次个位分配、收集基础上进行的,因此全部数字单看个位十位是有序的。

image-20201113161752292

而第三次就是对百位进行分配收集,这次完成以后百位及其如下是有序的。

image-20201113162803486

而最后一次的时候进行处理的时候,千位有的数字须要补零,此次完毕后后千位及之后都有序,即整个序列排序完成。

image-20201113170715860

简单实现代码为:

static void radixSort(int[] arr)//int 类型 从右往左
{
  List<Integer>bucket[]=new ArrayList[10];
  for(int i=0;i<10;i++)
  {
    bucket[i]=new ArrayList<Integer>();
  }
  //找到最大值
  int max=0;//假设都是正数
  for(int i=0;i<arr.length;i++)
  {
    if(arr[i]>max)
      max=arr[i];
  }
  int divideNum=1;//1 10 100 100……用来求对应位的数字
  while (max>0) {//max 和num 控制
    for(int num:arr)
    {
      bucket[(num/divideNum)%10].add(num);//分配 将对应位置的数字放到对应bucket中
    }
    divideNum*=10;
    max/=10;
    int idx=0;
    //收集 从新捡起数据
    for(List<Integer>list:bucket)
    {
      for(int num:list)
      {
        arr[idx++]=num;
      }
      list.clear();//收集完须要清空留下次继续使用
    }
  }
}

 


固然,基数排序还有字符串等长、不等长、一维数组优化等各类实现须要需学习

 

有完整的Java初级,高级对应的学习路线和资料!专一于java开发。分享java基础、原理性知识、JavaWeb实战、spring全家桶、设计模式、分布式及面试资料、开源项目,助力开发者成长!


欢迎关注微信公众号:码邦主

 

做者:小林啊连接:https://juejin.cn/post/6901528375023730701来源:掘金

相关文章
相关标签/搜索