常用的排序算法

简单选择排序

在java编程中我们经常遇到排序问题,在我们刚刚学习编程时,我们通常使用的是以下的方式进行排序:

int temp;
for(int i = 0; i < data.length() - 1;i++){
  for(int j = i + 1;j < data.length();j++){
    if(data[j] > data[i]){
      temp = data[j];
      data[j] = data[i];
      data[i] = temp;
    }
  }
}

这种排序方式就是我们最基础的选择排序

选择排序的思想是所有排序算法中最简单易懂的:循环找出数组中最小的数置于最前。但选择排序存在两点缺陷:

  • 有许多不必要的数据交换操作

例如:将数组{3,1,5,4,0}进行升序排序,使用选择排序需要经历以下步骤

  1. {1,3,5,4,0} 将3与1进行交换
  2. {1,3,5,4,0} 5大于1,不进行交换
  3. {1,3,5,4,0} 4大于1,不进行交换
  4. {0,3,5,4,1} 0小于1,进行交换操作

到这里就会发现,在执行到第四步时,只因为0大于1,就将0置于最后,而我们需要的操作仅仅是将0置于最前。这样即增加了之后的操作数,也增加了交换次数。

  • 不稳定排序算法

当数组中的相同值在排序后的相对位置不变时,我们说这种排序算法是稳定排序算法。

在选择排序中,举例:{3,3,1}
我们在进行排序时,会将第一个3与1进行交换,两个3的相对位置发生了改变,故说它是不稳定排序算法。

算法复杂度:$O(n^2)$

循环次数:$\frac{n(n-1)}{2}$


冒泡排序

冒泡排序的思想也比较简单,也是像选择排序一样存在两个循环。在第一次循环时,从倒数第二个元素比较到第一个元素,若当前元素的值,与当前元素+1位置的值不构成排序关系时,就将两元素进行交换。第二次循环从倒数第二个元素比较到第二个元素……依次类推。

假设数组{21,34,12,34,78,12,34},要使用冒泡排序进行由小到大排序,则会经历以下步骤:

  1. {21,34,12,34,78,12,34}
  2. {21,34,12,34,12,78,34}
  3. {21,34,12,12,34,78,34}
  4. {21,34,12,12,34,78,34}
  5. {21,12,34,12,34,78,34}
  6. {12,21,34,12,34,78,34}

……
可以看出,整个排序过程中,最小的元素像冒泡一样从最后浮到的最前,所以这种排序算法被称为冒泡排序

将这种算法落实到代码上就是:

for(int i = 0;i < data.length - 1;i++){
  for(int j = data.length - 2;j >= i;j--){
    if(data[j] > data[j + 1]){
      int temp = data[j];
      data[j] = data[j+1];
      data[j+1] = temp;
    }
  }
}

当然,这时从后向前的冒泡排序,根据同样的理论,我们也可以写出从前往后的冒泡操作:

for(int i = 0;i < data.length - 1;i++){
  for(int j = 0;j < data.length - 1 - i;j++){
    if(data[j] > data[j + 1]){
      int temp = data[j];
      data[j] = data[j+1];
      data[j+1] = temp;
    }
  }
}

与选择排序不同,冒泡排序的操作中必然会导致两相同元素靠在一起,这时我们不对相同元素进行位置交换,则排序结束后相同元素的相对位置不变,故冒泡排序是一种稳定排序算法

算法复杂度

当n个元素的数组排序开始时就是顺序排列,则需要进行n - 1次比较、0次移动即可完成整个排序,此时的比较次数C和移动次数M最小:

$$C_{min} = n - 1$$

$$M_{min} = 0$$

故冒泡排序最好算法复杂度为$$O(n)$$

当数组排序开始时是逆序排列,则需要进行n-1次循环,第i次循环需要进行n-1-i次比较,每次比较需要进行三次移动,此时当比较次数和移动次数最大:

$$C_{max} = \frac{n(n-1)}{2} = O(n^2)$$

$$M_{max} = \frac{3n(n-1)}{2} = O(n^2)$$

故冒泡排序最坏算法复杂度为$$O(n^2)$$

因此,冒泡排序算法当平均算法复杂度为$$O(n^2)$$


快速排序

快速排序与冒泡排序都属于交换排序。快速排序是对冒泡排序对一种优化。快速排序对主要思想是挖坑填数分治法

分治法就是将需要解决的问题转化为更小的子问题,并将子问题逐个击破,将子问题的解进行合并,合并为原问题的解。

快速排序的基本原理是:在需要排序的数组中选取一个数作为基准数,通过两个游标分别从左向右、从右向左进行扫描,将达成排序关系A的放在基准数左边,达成排序关系B的放在基准数右边。然后左右分串再次进行类似操作。

下面举一个例子进行详细说明:

欲使用快速排序对数组{12,3,22,51,13,7,10,11}进行升序排序:

1.

基准数 i j
12 3 22 51 13 7 10 11

首先要选择基准数,为了方便起见,我们使用数组第一个值为基准数,这时设置两个游标i和j,分别从左向右、从右向左进行扫描。

2.

基准数 i j
12 3 22 51 13 7 10 11

此时,i移动到22时,22大于基准数12,故停止扫描,j开始移动。j移动到11时,发现11小于基准数12,故停止扫描。

3.

基准数 i j
12 3 11 51 13 7 10 22

将两个数进行交换,交换后,继续从i开始扫描,并重复之前的操作。

4.

基准数 i j
12 3 11 10 13 7 51 22

10和51进行了交换。

5.

基准数 i j
12 3 11 10 7 13 51 22

7和13进行了交换。

6.

i j
7 3 11 10 12 13 51 22

当i和j相遇时,将基准数与i交换,此时基准数左边都是小于12的数,右边都是大于12的数。再将左右的数分成两个字串,使用递归再次进行之前的操作,知道字串不能再分,则完成了排序。

基准数的选择

在我们通常情况下,基准数的选择主要有以下几种方式:

  1. 使用第一个或最后一个数作为基准数,这种选择方式比较方便,但当遇到原本就是正序或倒序排列的数组时,就会降低算法性能。
  2. 随机选取数字作为基准数,这种选取方式较为安全。
  3. 三数取中,将第一个数、最后一个数、中间的数排列后取中值,也在一定程度上减少了复杂化的危险性。

以下使用三数取中法为例:

public class Main{
    public static void main(String[] args) {
        int[] data = new int[]{12,3,22,51,13,7,10,11};
        quicksort(data,0,data.length - 1);
        System.out.println(Arrays.toString(data));
    }
    public static void quicksort(int[] data,int left,int right){
        if(left >= right)
            return;
        int medium = (left + right) / 2;    //求出中值的位置

        if(data[left] > data[right])    //保证左端的值较小
            swap(data,left,right);
        if(data[medium] > data[right])  //保证中间的值较小
            swap(data,medium,right);
        if(data[medium] > data[left])   //保证左端的值为中值
            swap(data,medium,left);
        int pivotvalue = data[left];

        int i = left;
        int j = right;
        while(i < j){
            while(data[i] <= pivotvalue="" &&="" i="" <="" j)="" i++;="" while(data[j]="">= pivotvalue && i < j)
                j--;
            if(i < j)
                swap(data,i,j);

        }
        int temp = 0;
        if(data[i] > data[left])    //判断游标值大小
            temp = i - 1;
        else
            temp = i;
        swap(data,temp,left);
        quicksort(data,left,temp - 1);
        quicksort(data,temp + 1,right);


    }
    public static void swap(int[] data,int a,int b){
        int temp = data[a];
        data[a] = data[b];
        data[b] = temp;
    }
}

平均算法复杂度:$O(nlogn)$


堆排序

堆排序主要针对的是“堆”这种数据结构。它是一种选择排序

堆(heap)是计算机科学中的一类数据结构,它的表现形式是一棵完全二叉树层序遍历数组

堆结构与完全二叉树的不同在于:

  • 堆中的节点总是不大于或不小于其父节点的值,即堆结构存在有序性

所以我们可以说:

  • 堆一定是完全二叉树。
  • 完全二叉树不一定是堆。

堆通常分为两种:根节点为最小值的堆为小根堆(最小堆),根节点为最大值的堆为大根堆(最大堆)。

小跟堆
大跟堆

堆排序的主要原理为:

  1. 从下向上遍历每个非叶节点,使它与子节点形成堆关系。
  2. 将根节点与最后一个节点进行互换,此时数组最后一位为最大/最小值。
  3. 将除去原根节点之外的数据重复以上操作,逐步将每次的最大/最小值沉入数组后方,完成排序。

下面使用图示进行详细讲解:

例如,需要排序的数组为{12,3,5,14,8},我们使用堆排序进行升序排序,我们首先将数组构建为如下二叉树:

堆排序01

因为是升序排序,所以我们考虑构建大根堆,首先找到最后的非叶节点,将它与两子节点节点进行比较,最大值交换到当前位置:

堆排序02

然后继续向上寻找非叶节点,此时非叶节点为跟节点,重复之前的操作:

堆排序03

我们将此时的根节点,也就是数组中的最大值与最后一个节点进行交换:

堆排序04

此时的数组也就变成了{8,12,5,3,14},数组的最大值沉到了最后一位。接下来,不考虑最后一位,继续重复之前操作,即可完成排序。

使用代码实现如下:

public class Main{
    public static void main(String[] args) {
        int[] data = new int[]{12,3,22,51,13,7,10};
        sort(data);
        System.out.println(Arrays.toString(data));
    }

    public static void sort(int []arr){
        //1.构建大顶堆
        for(int i=arr.length/2-1;i>=0;i--){
            //从第一个非叶子结点从下至上,从右至左调整结构
            adjustHeap(arr,i,arr.length);
        }
        //2.调整堆结构+交换堆顶元素与末尾元素
        for(int j=arr.length-1;j>0;j--){
            swap(arr,0,j);//将堆顶元素与末尾元素进行交换
            adjustHeap(arr,0,j);//重新对堆进行调整
        }

    }

    /**
     * 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)
     * @param arr
     * @param i
     * @param length
     */
    public static void adjustHeap(int []arr,int i,int length){
        int temp = arr[i];//先取出当前元素i
        for(int k=i*2+1;ktemp){//如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
                arr[i] = arr[k];
                i = k;
            }else{
                break;
            }
        }
        arr[i] = temp;//将temp值放到最终的位置
    }

    /**
     * 交换元素
     * @param arr
     * @param a
     * @param b
     */
    public static void swap(int []arr,int a ,int b){
        int temp=arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
}

可以看出,虽然此算法叫堆排序,但实际上经过调整,对不构成完全二叉树的数据依旧可以进行排序。

堆排序算法的最优、最差、平均算法复杂度均为:$O(nlogn)$

堆排序与基本选择排序一样,均为不稳定排序算法

参考文章:
图解排序算法(三)之堆排序


简单插入排序

简单插入排序又称直接插入排序,也是一种较为简单的排序方式。

它的主要思路是,将数组中元素,依次放入有序表中,每次插入新元素,就将它与已有元素进行比较,将排序逻辑靠后的元素位置加一,将新元素插入。

代码如下:

public class Main{
    public static void main(String[] args) {
        int[] data = new int[]{12,3,22,51,13,7,10};
        data = simpleInsertSort(data);
        System.out.println(Arrays.toString(data));
    }

    private static int[] simpleInsertSort(int[] data) {
        if(data.length <= 1)="" return="" data;="" int[]="" result="new" int[data.length];="" result[0]="data[0];" for(int="" i="1;i" <="" data.length;i++){="" int="" pos="0;" j="i" -="" 1;="">= 0 ;j--){
                if(result[j] <= data[i])="" {="" pos="j" +="" 1;="" break;="" }="" else="" result[j="" 1]="result[j];" result[pos]="data[i];" return="" result;="" <="" code="">

当原数组为正序时,每次插入都只需要比较一次,所以最优算法复杂度为:$O(n)$

当原数组为倒序时,每次插入都需要与之前当所有数据进行比较,所以最差算法复杂度为:$1+2+3+…+(n-1) = \frac{1+(n-1)}{2} \times n =O(n^2)$

平均算法复杂度为:$O(n^2)$


希尔排序

希尔排序是对简单插入排序的一种优化排序算法,也被称为减小增量排序,它是最早突破$$O(n^2)$$复杂度的一批算法。

希尔排序的主要思路是:首先选定一个增量,按照增量来将原有数组划分为多个子数组,分别将各个子数组进行插入排序,然后不断减少增量,重复之前的步骤,直到增量减小为1时完成排序。

图解如下:

增量为4
4 3 1 6 5 2 8 7
第一次排序
4 2 1 6 5 3 8 7
增量为2
4 2 1 6 5 3 8 7
第二次排序
1 2 4 3 5 6 8 7
增量为1
1 2 4 3 5 6 8 7
第三次排序
1 2 3 4 5 6 7 8
增量的选择

增量的选择与减小规则应该按照实际情况进行规划,我们这里使用最简单的方式:初始增量为数组长度的一般,每次增量减小为原先增量的一半。在进行分段排序时,我们可以使用有序数组插入的方式,也可以使用交换方式。

public class ShellSort {
    public static void main(String []args){
        int []arr ={1,4,2,7,9,8,3,6};
        sort(arr);
        System.out.println(Arrays.toString(arr));
        int []arr1 ={1,4,2,7,9,8,3,6};
        sort1(arr1);
        System.out.println(Arrays.toString(arr1));
    }

    /**
     * 希尔排序 针对有序序列在插入时采用交换法
     * @param arr
     */
    public static void sort(int []arr){
        //增量gap,并逐步缩小增量
       for(int gap=arr.length/2;gap>0;gap/=2){
           //从第gap个元素,逐个对其所在组进行直接插入排序操作
           for(int i=gap;i=0 && arr[j]0;gap/=2){
            //从第gap个元素,逐个对其所在组进行直接插入排序操作
            for(int i=gap;i=0 && temp

其最坏时间复杂度依然为$O(n^2)$,一些经过优化的增量序列如Hibbard经过复杂证明可使得最坏时间复杂度为$O(n^{\frac{3}{2}})$

本段参考:

图解排序算法(二)之希尔排序

更新记录

更新时间 更新内容
2018年5月14日 选择排序
2018年5月15日 冒泡排序
2018年5月16日 快速排序
2018年5月17日 堆排序
2018年5月17日 简单插入排序
2018年5月23日 希尔排序

  转载请注明: 天井 常用的排序算法

 上一篇
二叉查找树 二叉查找树
二叉树二叉树是在程序设计中经常用到的数据结构之一,我们在数据结构中经常说的堆结构就是一种二叉树。二叉树与通常的树不同的是它规定每个节点最多只能拥有两个子节点。 完全二叉树完全二叉树是二叉树的一种形态,它要求除了最后一层外,其余层必须完全填
2018-05-26
本篇 
常用的排序算法 常用的排序算法
简单选择排序 在java编程中我们经常遇到排序问题,在我们刚刚学习编程时,我们通常使用的是以下的方式进行排序: int temp; for(int i = 0; i < data.length() - 1;i++){ for
2018-05-14
  目录