十大经典排序算法

225 阅读7分钟

849589-20190306165258970-1789860540.png

time-complexity.png

一、冒泡排序

步骤

  1. 以正序排序 (从小到大)为例,比较两个相邻的元素,如果第一个元素的值大于第二个,则交换它们的位置
  2. 从开始的第一对到最后一对,重复上面的操作,第一次遍历完之后,最后一个是最大的元素
  3. 对前面越来越少的无序元素重复上面的操作,直到排序完成

849589-20171015223238449-2146169197.gif 时间复杂度

  • 平均时间复杂度O(n2^2)
  • 最坏时间复杂度:O(n2^2)
  • 最好时间复杂度:O(n)。当一个数组是完全有序时,可以达到O(n)的时间复杂度。

空间复杂度O(1)

稳定性稳定

public class BubbleSort implement IArraySort{
    public int[] sort(int[] sourceArray)throw Exception{
        int[] arr=Arrays.copyOf(sourceArray,sourceArray.length);
        for(int i=1;i<arr.length;i++){
			boolean flag=true;
            for(int j=0;j<arr.length-i;j++){
                //如果前一个元素大于后一个,则交换二者的位置
				if(arr[j]>arr[j+1]){
                    int temp=arr[j];
                    arr[j]=arr[j+1];
                    arr[j+1]=temp;
                    flag=false;
                }
            }
            if(flag){
                break;
            }
        }
        return arr;
    }
}

二、选择排序

步骤

在未排序的数组中,找到最小值,放在数组的第一位,然后再从未排序的数组中找到最小值,放在已排序数组的后面。(将无序分区中的最小值放在有序区的最后位置)

selectionSort.gif 时间复杂度

平均、最好、最坏时间复杂度均为O(n2^2)

空间复杂度O(1)

稳定性不稳定。 原因:比如A、B、C三人得分分别为 80、80、70,如果从小到大排序会得到 C(70)、B(80)、A(80)的结果,A跑到了B的后面,所以选择排序是不稳定的。

public class SelectSort{
    public int [] sort(int [] sources)throw Exeception{
        int [] arr=Arrays.copyOf(sources,sources.length);
        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(i!=min){
                int temp=arr[i];
                arr[i]=arr[min];
                arr[min]=temp;
            }
        }
		return arr;
    }
}

三、插入排序

步骤

  1. 将序列中的第一个元素作为有序序列,将其之后的元素看作无序序列

  2. 依次扫描无序序列中的元素,用取出的元素和有序序列按从后往前的顺序依次比较大小,将元素插入到合适的位置(如果两个元素的值相等,则插入到该元素的后面)

    数据有序程度越高,插入排序的效率越高

insertionSort.gif 时间复杂度平均、最坏时间复杂度均为O(n2^2),最好为O(n)

空间复杂度O(n)

稳定性稳定

public class SelectSort{
    public int [] sort(int [] sources)throw Exeception{
        int [] arr=Arrays.copyOf(sources,sources.length);
        for(int i=1;i<arr.length;i++){
            int temp=arr[i];//扫描到的元素
            int j=i;
            while(j>0 && temp<arr[j-1]){
                arr[j]=arr[j-1];
                j--;
            }
            if(i!=j){
                arr[j]=temp;
            }
        }
        return arr;
    }
}

四、归并排序

典型的分而治之的应用

步骤

  1. 创建一个和原数组相同大小的数组,用于临时存放数组
  2. 将数组分为两个数组,用递归将左右两个数组分别进行分裂,直到将每个元素分为一个组,然后比较数组的大小
  3. 用归并的策略将分组进行两两排序归并,直到只剩下最后一组,排序完成

分: 将数组分成一个个独立的元素;

合: 对这些一个个独立的元素进行两个为一组的排序合并,直到合并成一组。

merge.png 时间复杂度平均、最好、最坏均为O(nlogn)

因为采用了类似完全二叉树的样式,树的深度为log2n,每一层的合并操作平均为O(n),所以为O(nlogn)

空间复杂度O(n),归并排序不是原地排序,用O(n)的额外空间来存储结果

稳定稳定

package sortdemo;

import java.util.Arrays;

/**
 * Created by chengxiao on 2016/12/8.
 */
public class MergeSort {
    public static void main(String []args){
        int []arr = {9,8,7,6,5,4,3,2,1};
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }
    public static void sort(int []arr){
        int []temp = new int[arr.length];//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
        sort(arr,0,arr.length-1,temp);
    }
    private static void sort(int[] arr,int left,int right,int []temp){
        if(left<right){
            int mid = (left+right)/2;
            sort(arr,left,mid,temp);//左边归并排序,使得左子序列有序
            sort(arr,mid+1,right,temp);//右边归并排序,使得右子序列有序
            merge(arr,left,mid,right,temp);//将两个有序子数组合并操作
        }
    }
    private static void merge(int[] arr,int left,int mid,int right,int[] temp){
        int i = left;//左序列指针
        int j = mid+1;//右序列指针
        int t = 0;//临时数组指针
        while (i<=mid && j<=right){
            if(arr[i]<=arr[j]){
                temp[t++] = arr[i++];
            }else {
                temp[t++] = arr[j++];
            }
        }
        while(i<=mid){//将左边剩余元素填充进temp中
            temp[t++] = arr[i++];
        }
        while(j<=right){//将右序列剩余元素填充进temp中
            temp[t++] = arr[j++];
        }
        t = 0;
        //将temp中的元素全部拷贝到原数组中
        while(left <= right){
            arr[left++] = temp[t++];
        }
    }
}

五、希尔排序

希尔排序是插入排序的改进版,他的基本思想是将大的数据集合分成若干个小组,初始增量为gap=length/2,分别对小组内的数据进行插入排序,待增量递减为1时,整个数据集合呈"基本有序"状态,然后对所有数据进行直接插入排序。

希尔排序也叫缩小增量排序。

时间复杂度平均:O(n1.3)、最坏:O(n2n^2)、最好:O(n)

空间复杂度O(1)

稳定性不稳定

public class ShellSort implements IArraySort {

        public static void shellSort(int[] arr){
        //gap增量,逐步缩小gap
        for (int gap=arr.length/2;gap>0;gap/=2){
            //从gap元素开始,逐个对其所在组进行插入排序
            for (int i = gap; i < arr.length; i++) {
                int j=i;
                while (j>=gap && arr[j]<arr[j-gap]){
                    //交换法
                    swap(arr,j,j-gap);
                    j-=gap;
                }
            }
        }
    }

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

/** shellSort的移动法
     *                  while (j-gap>=0 && temp<arr[j-gap]){
     *                     arr[j]=arr[j-gap];
     *                     j-=gap;
     *                 }
     *                 arr[j]=temp;
     * @param arr
     * @param a
     * @param b
     */

六、快速排序

快速排序是冒泡排序的一种改进方法

最坏时间复杂度的情况为:每次选取的基准数据为最小或最大的数,无法对数组进行平分递归了。该情况为数组为正序或者倒序排列

步骤: 在数列中找到一个"基准",遍历数列,使小于该基准的数排在基准之前,大于该基准的数排在基准之后,这样就确定了一个基准的位置,递归地把基准前后的子数列进行快速排序。

  1. 指定一个基准数据,用两个指针low、high分别指向队列的队首和队尾

quickSort-oneStep.png

  1. 首先比较high和基准数据tmp的值,如果high小于tmp,用high的值替换low的值,否则high向前移动一位

quickSort-twoStep.png

  1. 然后比较low和temp的值,如果low的值大于tmp的值,用low的值替换high的值,否则low向后移动一位

quickSort-threeStep.png

  1. low和high交替移动,直到二者相遇,此时确定了基准数据tmp的位置

quickSort-fourStep.png

  1. 得到基准数据的位置后,分别对该位置两侧的队列递归进行上述1-4的操作,最终就能得到想要的顺序

平均、最好时间复杂度: O(nlogn)

最坏时间复杂度: O(n2n^2)

空间复杂度: O(logn)。栈的深度,最坏情况下为O(n)

public class QuickSort implements IArraySort {

    @Override
    public int[] sort(int[] sourceArray) throws Exception {
        // 对 arr 进行拷贝,不改变参数内容
        int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);

        return quickSort(arr, 0, arr.length - 1);
    }

    private int[] quickSort(int[] arr, int left, int right) {
        if (left < right) {
            int partitionIndex = getIndex(arr, left, right);
            quickSort(arr, left, partitionIndex - 1);
            quickSort(arr, partitionIndex + 1, right);
        }
        return arr;
    }

        public static int getIndex(int[] arr,int low,int high){
        int temp=arr[low];
        while (low<high){
            while (low<high && temp<=arr[high]){
                high--;
            }
            arr[low]=arr[high];
            while (low<high && temp>=arr[low]){
                low++;
            }
            arr[high]=arr[low];
        }
        arr[low]=temp;
        return high;
    }

    private void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

}

七、堆排序

前置知识补充:

叶子节点:没有子节点的节点

满二叉树:树是满的。

完全二叉树:由满二叉树引申出来的,除最后一层外树是满的,且最后一层需要左靠齐。

完满二叉树:所有非叶子节点的度都是2(只要有子节点,就必须有两个)

堆:①是一个完全二叉树;②所有父节点的值都要大于等于(或小于等于)子节点的值

满二叉树:

full-binary-tree.png

完全二叉树图:

complete-binary-tree.png

堆排序(HeapSort)是利用的数据结构设计的一种排序算法。

算法描述:(以正序排序为例)

  1. 将数组构建成大顶堆
  2. 将堆顶元素arr0与数组中最后一个元素arr[length]交换,此时得到一个新的无序区(arr[0]~arr[length-1])和新的有序区(arr[length])
  3. 由于交换后新的堆可能会违反堆的性质,所有现在需要将新的无序堆进行调整,调整后将堆顶元素arr[0]与数组中倒数第二个元素arr[length-1]交换
  4. 重复2,3操作,直到新的无序区没有元素,排序完成

heapSort.png 正序排序构建大顶堆,倒序排序构造小顶堆

/**
     * 堆排序调用该方法
     * @param arr
     */
    public void heapSort(int[] arr){
        int len=arr.length;
        //第一个for循环得到第一个大顶堆
        for (int i=(len-1)/2;i>=0;i--){
            heapify(arr,len,i);
        }
        //重复len-1次
        for (int j=len-1;j>=0;j--){
            swap(arr,0,j);
            len--; //交换一个值,数组长度都需要-1,这里是关键
            heapify(arr,len,0);
        }
    }

    /**
     * 调整堆
     * @param arr 数组
     * @param len 数组长度
     * @param parentN 父节点下标
     */
    public void heapify(int[] arr,int len,int parentN){
        int leftN=parentN * 2+1; //左子节点下标
        int rightN=parentN * 2+2; //右子节点下标
        int largest=parentN;
        if (leftN<len && arr[largest]<arr[leftN]){
            largest=leftN;
        }
        if (rightN<len && arr[largest]<arr[rightN]){
            largest=rightN;
        }
        if (largest!=parentN){
            swap(arr,parentN,largest); //如果子节点比父节点的值大,则交换二者的值
            heapify(arr,len,largest);   //调整堆
        }
    }
    /**
     * 交换值
     * @param arr
     * @param a
     * @param b
     */
    public void swap(int[] arr,int a,int b){
        int temp=arr[a];
        arr[a]=arr[b];
        arr[b]=temp;
    }

八、计数排序

计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

算法描述

  • 找出待排序的数组中最大和最小的元素;
  • 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
  • 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
  • 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。

countSort.gif

九、基数排序

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

算法描述

  • 取得数组中的最大数,并取得位数;
  • arr为原始数组,从最低位开始取每个位组成radix数组;
  • 对radix进行计数排序(利用计数排序适用于小范围数的特点

cardinalitySort.gif

十、桶排序

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

算法描述

  • 设置一个定量的数组当作空桶;
  • 遍历输入数据,并且把数据一个一个放到对应的桶里去;
  • 对每个不是空的桶进行排序;
  • 从不是空的桶里把排好序的数据拼接起来。

bucketSort.png