常见排序算法基本原理及实现(快排,归并,堆排,直接插入.....)

Posted Ischanged

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了常见排序算法基本原理及实现(快排,归并,堆排,直接插入.....)相关的知识,希望对你有一定的参考价值。

常见排序算法总览

1.概念

1.1 排序

排序就是使待排序序列,按照其中的某个或某些关键字的大小(以什么作为比较基准),递增或递减的排列起来的操作。平时如果提到排序,通常指的是排升序(非降序)。通常意义上的排序,都是指的原地排序(in place sort)。

1.2 稳定性(重要)

两个相等的数据,如果经过排序后,排序算法能保证其相对位置不发生变化,则我们称该算法是具备稳定性的排序算法。如排序如下序列的算法就是一个稳定的排序算法。

排序的应用很广泛,只要涉及比较,选择等都可以使用排序。

2.插入排序

2.1直接插入排序-原理


待排序序列作为一整个区间,被分为 有序区间 和无序区间,**首先待排序列的第一个元素本身就是有序的,单独作为一个有序区间 ,第一个元素后面的元素作为无序区间,每次选择无序区间的第一个元素,在有序区间内选择合适的位置插入,直到无序区间里面所有的元素都插入到有序区间为止。**大致思路就是这样,一些详细的过程,我在下面画图举个实例来看一下。

假设以数组元素2,1,4,3,6为例升序排列,首先变量i从1下标指向无序区的第一个元素向前遍历数组,变量j每次从i的后一个位置向后进行遍历数组,把i位置的值放入一个临时变量tem中,当j位置的值比tem的值大时,把j位置的值向前移动到i位置扩展有序区大小,之后j继续移动寻找tem放入有序区的合适位置,和i位置的值比较,如果j位置的元素还在比i位置的大,继续移动操作,直到j为负数或者找到一个j指向的元素比i指向的小为止。这时就可以把i的值放入有序区的合适位置,这时的关键代码就是 array[j + 1] = tem;

2.2代码实现

 public static void insertSort(int[] array) 
        if(array==null) return;
        for (int i = 1; i < array.length; i++) 
            int j = i - 1;
            int tem = array[i];
            while (j >= 0) 
                if (array[j] > tem) 
                    array[j+1] = array[j];
                    j--;
                    //i--;
                 else 
              break;
                
            
    array[j + 1] = tem;

        

2.3性能分析

时间复杂度
最好:O(n),数据有序的情况下,只是遍历了一遍数组,最坏:O(n^2),数据逆序的情况下既要遍历数组也要比较数组。空间复杂度O(1),开辟了固定的数组大小。稳定性:稳定。
插入排序的特点:
初始数据越接近有序,时间效率越高。

3.希尔排序

3.1 原理

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序序列中所有记录分成我们选定的这个整数个组,从第一个元素开始分组,两个数据之间的距离为每组元素个数的分在同一个组,并对每一组内的记录进行直接插入排序。然后重复上述分组和排序的工作。最后将所有的元素分在一个组内,对最后一个组再进行最后的排序即可。希尔排序是对直接插入排序的优化,重复的进行分组之后又排序,先使局部有序之后再使整体有序。


当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。

3.2性能分析:

由严蔚敏老师的《数据结构》可知希尔排序的时间复杂度和所取得“增量”有关系,时间复杂度是“增量”的一个函数,但目前一个完美的增量序列如何确定还没有决解,由实验及部分数据可知希尔排序的时间复杂度为O(n1.3)-O(n1.5).空间复杂度为O(n).对于增量的取法,书中描述为取一个除了一之外没有公因子的数字作为增量也就是素数。

3.3代码实现

public static void shell(int[] array,int gap) 
        //参数判断
        if(array == null) return;
        for (int i = gap; i < array.length; i++) 
            int tmp = array[i];
            int j = i-gap;
            for (; j >= 0 ; j-=gap) 
                if(array[j] > tmp) 
                    array[j+gap] = array[j];
                else 
                    break;
                
            
            array[j+gap] = tmp;
        

    
public static void shellSort(int[] array)   
        int gap = array.length;//初始增量为数组长度
        while (gap > 1) 
            shell(array,gap);
            gap = gap/2;//1
        
        shell(array,1);
    

由于我们所给的数据个数是不确定的,所以无法找到一个通用的式子计算出我们的增量让它为素数,所以只要遵循合理的将数组分组,每次分组时增量逐渐缩小,最后分为一组即可。上面的代码和直接插入排序的基本一样,只是这里的i=gap,下的内容也相应地换一下。

4. 选择排序

4.1直接选择排序-原理


选择排序,选择排序直接就升序来说,假设第一个元素是最小的,依次和后面的元素比较假设第一个元素比比较的元素大交换两者的位置,不大于不做任何操作,比较下一个元素,当数组元素比较完后,第一趟比较结束,第一个元素为最小值,第二趟比较假设第二个元素是最小的依次和后面的元素比较和交换重复第一步,当比较了n-1趟后元素就是有序的了。

4.2实现

   public static void selectSort(int[] array) 
        if(array==null||array.length<2)
            return;
        
        for (int i = 0; i <array.length-1 ; i++) 
            int min=i;//默认第一个元素最小
            for (int j = i+1; j <array.length; j++)
                if(array[min]>array[j])
                    min=j;

                
            
            if(min!=i)
                  int tem=array[i];
                    array[i]=array[min];
                    array[min]=tem;
            
        
    

4.3性能分析:

时间复杂度**:O(n^2)** 不管有序还是无序的情况,可以适当的优化下,就是在比较的时候如升序,前面的元素比后面的大时不要每次交换,用一个临时变量记录最小值,当一趟遍历完后再交换,但时间复杂度任然是O(n^2) 空间复杂度:o(1), 稳定性:不稳定的排。

5.堆排序

5.1原理

基本原理也是选择排序,只是不在使用遍历的方式查找无序区间的最大的数,而是通过堆来选择无序区间的最大的数,**堆逻辑上是一棵完全二叉树
满足任意结点的值都大于等于其子树中结点的值,叫做大堆,或者大根堆,或者最大堆反之,则是小堆,或者小根堆,或者最小堆,子树中节点的值没有关系。**就升序而言,我们创建一个大堆,每次堆顶的元素都是最大的,我们把它和堆的最后一个元素交换,堆的末尾元素就是最大值,也是数组的最后一个元素就是最大值,有序的元素下次交换调整时排除在排序范围内,之再调整交换堆使它继续为大根堆,之后重复上面的操作,第二次得到第二大元素,直到排序到堆顶元素为止。

注意: 排升序要建大堆;排降序要建小堆。
再简单总结下堆排序的基本思路:

a.将无序序列构建成一个堆,根据升序降序需求选择大根堆或小根堆;
b.将堆顶元素与末尾元素交换,将最大元素或者最小元素"沉"到数组末端;
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个数组有序。

5.2实现

 public static void shiftDown(int[] array,int parent,int len) 
        int child = 2*parent+1;
        while (child < len) 
            if(child+1 < len && array[child] < array[child+1]) 
                child++;
            
            //child下标 表示的就是左右孩子最大值的下标
            if(array[child] > array[parent]) 
                int tmp = array[child];
                array[child] = array[parent];
                array[parent] = tmp;
                parent = child;//如果子树不为空,操作子树就行,改变的那颗子树
                child = 2*parent+1;
            else 
                break;
            
        
    

 public static void createBigHeap(int[] array) 
        for (int i = (array.length-1-1)/2; i >= 0 ; i--) 
            shiftDown(array,i,array.length);
        
    
  /**
     * 时间复杂度:O(N*logn)
     * 空间复杂度:O(1)
     * 稳定性:不稳定
     * @param array
     */
    public static void heapSort(int[] array) 
        createBigHeap(array);
        int end = array.length-1;
        while (end > 0) 
            int tmp = array[0];
            array[0] = array[end];
            array[end] = tmp;
            shiftDown(array,0,end);
            end--;
        
    

5.3性能分析

时间复杂度:**O(n * log(n)),**可以看成一颗完全二叉树,每个节点都要进行交换,交换之后要进行向下调。整空间复杂度:O(1)

6冒泡排序

6.1原理


在待排序序列中,通过相邻数的比较,逐渐的将较大的数据移动到待排序序列的后面,持续这个过程,直到数组整体有序。假设有n个数据只需要比较n-1趟,每一次冒泡出无序序列中的最大值,对于第一趟的比较比较的次数依然是n-1次,之后每趟比较的次数依次减少1。直到n-1趟比较完,数据有序了,下面的代码进行了一些优化,当待排序序列本身有序的时候,完成了一次排序就结束了。

 public static void bubbleSort(int[] array) 
        for (int i = 0; i < array.length-1; i++) 
            boolean flg = false;//做一个标记防止数据本身有序
            for (int j = 0; j < array.length-1-i; j++) //每一趟的比较次数都在减少所以要减i
                if(array[j] > array[j+1]) 
                    int tmp = array[j];
                    array[j] = array[j+1];
                    array[j+1] = tmp;
                    flg = true;
                
            
            if(flg == false) 
                break;
            
        
    

6.2性能分析

时间复杂度:O(N^2)数据无序的情况下,O(N)数据有序的情况下,空间复杂度O(1),稳定性:稳定的排序。

7快速排序(重要)

7.1原理-总览

  1. 从待排序区间选择一个数,作为基准值(pivot);
  2. Partition:为划分函数,遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的右边;
  3. 采用分治思想,对左右两个小区间按照同样的方式处理,直到小区间的长度 == 1,代表已经有序,或者小区间的长度 == 0,代表没有数据。

7.1.1挖坑法:

 public static int partition(int[] array,int start,int end) 
        int tmp = array[start];
        while (start < end) 
            //1、先判断后面
            while (start < end && array[end] >= tmp) //找到一个小于基准点的值
                end--;
            
            //1.1 后面的给start  array[start] = array[end]
            array[start] = array[end];

            //2、再判断前边
            while (start < end && array[start] <= tmp) 
                start++;
            
            //2.1 把这个大的给end  array[end] = array[start]
            array[end] = array[start];
        
        //start=end
        array[start] = tmp;
        return start;
    
    //类似二叉树搜索树的前序遍历,基准点就是二叉树的根结点
    public static void quickSort(int[] array,int left,int right) 
        if(left >= right) //递归结束的条件
            return;
        
        int pivot = partition(array,left,right);
        quickSort(array,left,pivot-1);
        quickSort(array,pivot+1,right);
    


    public static void main(String[] args) 
        int []arr=6,0,1,2,7;
        System.out.println(Arrays.toString(arr));
        quickSort(arr,0,arr.length-1);
        System.out.println(Arrays.toString(arr));

    

假设我以数组5,1,2,4,3,6为例排升序,大致过程如下,将就看下,第一次以5为基准一次划分如下:

接着再以同样的方式,先划分左再划分右一直递归下去,有一个数字的序列不用划分了,默认是有序的。下面以3为基准的划分,以2基准划分,以1基准划分划分完之后,最开始的基准的左边是不是有序了,再开始右边排序,之后整个序列就有序了。

7.1.2:Hoare 法:

基本思路和挖坑法一致,只是不再进行进行赋值,而是进行两个数的交换,实现了一个交换函数:

public static void quickSort1(int[] array,int left,int right) 
    if(left >= right) //递归结束的条件
        return;
    
    int pivot = partition1(array,left,right);
    quickSort(array,left,pivot-1);
    quickSort(array,pivot+1,right);

    public static int partition1(int[] array, int left, int right) 
        int i = left;
        int j = right;
        int pivot = array[left];
        while (i < j) 
            while (i < j && array[j] >= pivot) 
                j--;
            
            while (i < j && array[i] <= pivot) 
                i++;
            
            swap(array, i, j);
        
        swap(array, i, left);
        return i;
    
    // //交换的方法
public  static void swap(int[]array,int i,int j)
        int tem=array[i];
        array[i]=array[j];
        array[j]=tem;


7.2性能分析

时间复杂度:可以把快速排序看成二叉树的前序遍历,每个节点做为基准都遍历了都排好序了,整个数组就有序了,最好情况下O(n * log(n))每次递归都将待排序序列均匀分割,树的深度和每一层的遍历区间相乘。最坏情况下O(n^2),此时数据有序,就是一个单分支的树了,空间复杂度最坏情况下:O(n)。稳定性:不稳定。

7.3快速排序的优化

对于分治算法,当每次划分时,算法若都能分成两个等长的子序列时,那么分治算法效率会达到最大。也就是说,基准的选择是很重要的。选择基准的方式决定了两个分割后两个子序列的长度,进而对整个算法的效率产生决定性影响。最理想的方法是,选择的基准恰好能把待排序序列分成两个等长的子序列。

7.3.1固定位置法:

基本的快速排序选取第一个或最后一个元素作为基准。但不是一种好方法,**因为如果当数据有序的时候,这样的算法效率很低,每次排序完只能排除一个数字,时间复杂度很大,**就像上面写的快速排序就是固定位置法的快速排序,因此下面介绍效率更高的两种基准选取的方法。

7.3.2随机选取基准法(了解)

思想:利用随机数,取待排序列中任意一个元素作为基准

public static void quickSort(int[] array,int left,int right) 
        if(left >= right) //递归结束的条件
            return;
        

       // 1、随机选择基准-》先将left下标的值换一下
        //rand  交换array[left]  array[rand]
        Random random = new Random();
        int rand = random.nextInt(right-left)+left+1;
      swap(array,left,rand);
        int pivot = partition(array,left,right);
        quickSort(array,left,pivot-1);
        quickSort(array,pivot+1,right);
    

随机选取基准法,较固定位置法效率有所改变,但存在极大的偶然性,不排除你每次选的基准是待排序序列里面的最大值或者最小值,也不能将待排序序列进行均匀的分割。

7.3.3三数取中(median-of-three)(优化有序的数据)

其实最好的做法就是找到待排序数组的中间值以它进行划分,当这样是很难得到的,一般的做法是使用左端、右端和中心位置上的三个元素的中值作为基准值。显然使用三数中值分割法消除了预排序输入的不好情形。

  public static void medianOfThree(int[] array,int left,int right) 
        int mid = (left+right)/2;
        if(array[mid] > array[left]) 
            int tmp = array[mid];
            array[mid] = array[left];
            array[left] = tmp;
        //array[mid] <= array[start]
        if(array[left] > array[right]) 
            int tmp = array[left];
            array[left] = array[right];
            array[right] = tmp;
        //array[start] <= array[right]
        if(array[mid] > array[right]) 
            int tmp = array[mid]以上是关于常见排序算法基本原理及实现(快排,归并,堆排,直接插入.....)的主要内容,如果未能解决你的问题,请参考以下文章

常见排序算法基本原理及实现(快排,归并,堆排,直接插入.....)

8种面试经典排序详解--选择,插入,希尔,冒泡,堆排,3种快排及非递归,归并及非递归,计数(图+C语言代码+时间复杂度)

8种面试经典排序详解--选择,插入,希尔,冒泡,堆排,3种快排及非递归,归并及非递归,计数(图+C语言代码+时间复杂度)

8种面试经典!排序详解--选择,插入,希尔,冒泡,堆排,3种快排,快排非递归,归并,归并非递归,计数(图+C语言代码+时间复杂度)

排序算法的实现(归并,快排,堆排,希尔排序 O(N*log(N)))

七大排序算法(插排,希尔,选择排序,堆排,冒泡,快排,归并)--图文详解