面试常考排序算法(持续更新)

1.桶排序

桶排序,也叫作箱排序,是一个排序算法,也是所有排序算法中最快、最简单的排序算法。其中的思想是我们首先需要知道所有待排序元素的范围,然后需要有在这个范围内的同样数量的桶,接着把元素放到对应的桶中,最后按顺序输出。

实际上一个桶不总是放同一个元素,在很多时候一个桶里可能会放多个元素,这是不是与散列表有点类似呢?其实真正的桶排序和散列表是一样的原理。

除了对一个桶内的元素做链表存储,我们也有可能对每个桶中的元素继续使用其他排序算法进行排序,所以更多时候,桶排序会结合其他排序算法一起使用。

我们怎么在代码中实现桶排序呢?其实很简单,使用数组就好了。比如有11个桶,我们只需要声明一个长度为11的数组,然后每把一个元素往桶中放时,就把数组指定位置的值加1,最终倒序输出数组的下标,数组每个位置的值为几就输出几次下标,这样就可以实现桶排序了。

public class BucketSort {

    private int[] buckets;
    private int[] array;

    public BucketSort(int range, int[] array) {
        this.buckets = new int[range];
        this.array = array;
    }

    /**
     * 排序
     */
    public void sort() {
        if (array != null && array.length > 1)
            for (int i = 0; i < array.length; i++)
                buckets[array[i]]++;
    }

    /**
     * 从大到小排序
     */
    public void print() {
        // 倒序输出数据
        for (int i = buckets.length - 1; i >= 0; i--)
            // 元素中的值为几,就说明有多少个相同值的元素,就输出几遍
            for (int j = 0; j < buckets[i]; j++)
                System.out.println(i);
    }

    public static void main(String[] args) {
        int[] array = {5, 9, 1, 9, 5, 5, 7, 6, 1};
        BucketSort bucketSort = new BucketSort(11, array);
        bucketSort.sort();
        bucketSort.print();
    }
}

桶排序的特点: 

速度快、简单,但是也有相应的弱点,那就是空间利用率低,如果数据跨度太大,则空间可能无法承受,或者说这些元素并不适合使用通排序算法。

2.快速排序

在待排序的数列中,我们首先要找一个数字作为基准数。为了方便,我们一般选择第1个数字作为基准数。接下来我们需要把这个待排序的数列中小于基准数的元素移动到待排序的数列的左边,把大于基准数的元素移动到待排序的数列的右边。这时,左右两个分区的元素就相对有序了;接着把两个分区的元素分别按照上面的两种方法继续对每个分区找出基准数,然后移动,直到各个分区只有一个数时为止。这是典型的分治思想,即分治法。

public class QuickSort {

    private int[] array;

    public QuickSort(int[] array) {
        this.array = array;
    }

    /**
     * 排序
     */
    public void sort() {
        quickSort(array, 0, array.length - 1);
    }

    public void print() {
            for (int i = 0; i < array.length; i++)
                System.out.println(array[i]);
    }

    /**
     * 递归排序
     * @param src
     * @param begin
     * @param end
     */
    private void quickSort(int[] src, int begin, int end){
        if (begin < end){
            int key = src[begin];
            int i = begin;
            int j = end;

            while(i < j){
                while(i < j && src[j] > key){
                    j--;
                }
                if (i < j){
                    src[i] = src[j];
                    i++;
                }
                while(i < j && src[i] < key){
                    i++;
                }
                if (i < j){
                    src[j] = src[i];
                    j--;
                }
            }

            src[i] = key;

            quickSort(src, begin , i - 1);
            quickSort(src, i+1, end);
        }

    }

    public static void main(String[] args) {
        int[] array = {5, 9, 1, 9, 5, 5, 7, 6, 1};
        QuickSort quickSort = new QuickSort(array);
        quickSort.sort();
        quickSort.print();
    }
}

快速排序的特点及性能:

快速排序在最坏情况下的时间复杂度和冒泡排序一样,是O(n2),实际上每次比较都需要交换,但是这种情况并不常见。我们可以考虑一下如果每次比较都需要交换,那么数列的平均时间复杂度O(nlogn),事实上在大多数时候,排序的速度要快于这个平均时间复杂度。这种算法实际上是一种分治思想,也就是分而治之,把问题分为一个个的小部分来分别解决,再把结果组合起来。

快速排序只是使用数组原本的空间进行排序,所以所占用的空间应该是常量级的,但是由于每次划分之后是递归调用,所以递归调用在运行的过程中会消耗一定的空间,在一般情况下的空间复杂度为O(logn),在最差的情况下,若每次只完成了一个元素,那么空间复杂度为O(n)。所以我们一般认为快速排序的空间复杂度为O(logn)。

快速排序是一个不稳定的算法。

快速排序基本上被认为是相同数量级的所有排序算法中,平均性能最好的。 

3.插入排序

  • 插入排序分为两种,一种是直接插入排序,一种是二分插入排序,这里介绍的是直接插入排序。
  • 插入排序就是往数列里面插入数据元素。一般我们认为插入排序就是往一个已经排好序的待排序的数列中插入一个数,使得插入这个数之后,数列仍然有序。
  • 二分插入排序应该也是用了分治法的思想去排序的。实际上二分就是使用二分查找来找到这个插入的位置,剩余的插入思想其实就和直接插入排序一样。
  • 所以要完成插入排序,就需要找到这个待插入元素的位置

直接插入排序的整个执行过程:

  1. 首先需要明确待排序的数列由两部分组成,一部分是已排好序的部分,另一部分是待排序的部分。
  2. 接着我们每次选择待排序的部分的第1个元素,分别与前面的元素进行比较。当大于前面的元素时,可以直接进入已排好序的部分;当小于前面的元素时,需要把这个元素拿出来,将前面的元素后移一位,继续与前面的元素相比,直到比较完数组的第1个元素或者出现一个元素小于我们拿出来的这个元素,这时停止比较、移动,直接把这个元素放到当时的空位上。
  3. 一直重复步骤2,当待排序的部分已经没有元素可进行插入时,停止操作,当前的数列为已排好序的数列。

 

public class InsertSort {

    private int[] array;

    public InsertSort(int[] array){
        this.array = array;
    }

    public void sort(){
        if (array == null){
            throw new RuntimeException("array is null");
        }
        int length = array.length;
        if (length > 0){
            for (int i = 1;i<length;i++){
                int temp = array[i];
                int j = i;
                for (;j > 0 && array[j-1] > temp;j--){
                    array[j] = array[j-1];
                }
                array[j] = temp;
            }
        }
    }

    public void print(){
        for (int i=0;i < array.length; i++)
            System.out.println(array[i]);
    }

    public static void main(String[] args) {
        int[] array = {5,9,1,9,5,3,7,6,1};
        InsertSort insertSort = new InsertSort(array);
        insertSort.sort();
        insertSort.print();
    }
}

 插入排序的特点及性能:

插入排序的操作很简单,而且我们通过上面的实例及原理可以知道,插入排序在数列近似有序时,效率会比较高,因为这样会减少比较和移动的次数。

插入排序的时间复杂度是O(n2),最好的情况就是数列近似有序,这时一部分内层循环只需要比较及移动较少的次数即可完成排序。如果数列本身已经排好序,那么插入排序也可以达到线性时间复杂度及O(n),所以我们应正确的意识到,使用插入排序算法进行排序时,数列越近似有序,性能就越高。

插入排序的空间复杂度是O(1),是常量级的,由于在采用插入排序时,我们只需要使用一个额外的空间来存储这个“拿出来”的元素,所以插入排序只需要额外的一个空间去做排序,这是常量级的空间消耗。

插入排序是稳定的,由于是数组内部自己排序,把后面的部分按前后顺序一点点地比较、移动,可以保持相对顺序不变,所以插入排序是稳定的排序算法。

4.希尔排序

希尔排序也是一种插入排序算法,也叫作缩小增量排序,是直接插入排序的一种更高效的改进算法。希尔排序在插入排序的基础上,只要通过两点来改进排序算法:一是插入排序在对近似有序的数列进行排序时,排序性能会比较好;二是插入排序的性能比较低效,即每次只能将数据移动一位。

希尔排序的基本思想是:把待排序的数列按照一定的增量分割成多个子数列。但是这个子数列不是连续的,而是通过前面提到的增量,按照一定相隔的增量进行分割的,然后对各个子数列进行插入排序,接着增量逐渐减小,然后仍然对每部分进行插入排序,在减小到1之后直接使用插入排序处理数列。

需要特别强调,这里选择增量的要求是每次都要减少,直至最后一次变为1为止。

public class ShellSort {

    private int[] array;

    public ShellSort(int[] array){
        this.array = array;
    }

    public void sort(){
        int temp;
        for (int k = array.length/2;k > 0; k/=2){
            for (int i = k;i < array.length; i++){
                for (int j = i;j >= k; j -= k){
                    if(array[j-k] > array[j]){
                        temp = array[j-k];
                        array[j-k] = array[j];
                        array[j] = temp;
                    }
                }
            }
        }
    }

    public void print(){
        for (int i = 0;i < array.length; i++){
            System.out.println(array[i]);
        }
    }

    /**
     * 希尔排序
     * @param args
     */
    public static void main(String[] args) {
        int[] array = {5,9,1,9,5,3,7,6,1};
        ShellSort shellSort = new ShellSort(array);
        shellSort.sort();
        shellSort.print();
    }
}

希尔排序的特点及性能:

在时间复杂度上,由于增量的序列不一定,所以时间复杂度也不确定。这在数学上还无法给出确切的结果。但是一般认为希尔排序的平均时间复杂度为O(n1.3)。当然,希尔排序的时间复杂度与其增量序列有关,我们知道,一般来说希尔排序会比插入排序快一些。

在希尔排序的实现中仍然使用了插入排序,只是进行了分组,并没有使用其他空间,所以希尔排序的空间复杂度同样是O(1),是常量级的。

希尔排序是不稳定的算法。 

5.简单选择排序

选择排序是一种非常简单的排序算法,就是在序列中依次选择最大(或者最小)的数,并将其放到待排序的数列的起始位置。

算法原理:

在待排序的数列中寻找最大的(或者最小)的一个数,与第1个元素进行交换,接着在剩余的待排序的数列中继续找最大(最小)的一个数,与第2个元素交换。以此类推,一直到待排序的数列中只有一个元素时为止。

public class SelectSort {

    private int[] array;

    public SelectSort(int[] array){
        this.array = array;
    }

    public void sort(){
        int length = array.length;
        for (int i = 0;i < length; i++){
            int minIndex = i;
            for (int j = i + 1;j < array.length; j++){
                if (array[j] < array[minIndex]){
                    minIndex = j;
                }
            }

            if (minIndex != i){
                int temp = array[minIndex];
                array[minIndex] = array[i];
                array[i] = temp;
            }
        }
    }

    public void print(){
        for (int i = 0;i < array.length; i++){
            System.out.println(array[i]);
        }
    }

    /**
     * 简单选择排序
     * @param args
     */
    public static void main(String[] args) {
        int[] array = {5,9,1,9,5,3,7,6,1};
        SelectSort selectSort = new SelectSort(array);
        selectSort.sort();
        selectSort.print();
    }
}

简单选择排序的特点及性能:

由于在简单选择排序中,我们一般在原本的待排序的数组上排序并交换,基本上使用的都是常量级的额外空间,所以其空间复杂度是O(1)。

在最好的情况下,每次要找的元素就是待排序的数列的第一个元素,这样我们只需要一次遍历且不需要交换,即可实现一趟排序;而在最坏的情况下,每次在数列中要找的元素都不是第一个元素,每次需要交换。比较的次数只与数列的长度有关,而在外部要遍历整个数列,也与长度有关,所以这样的双重循环不管在什么情况下,时间复杂度都是O(n2)。但是由于选择排序不需要一个一个地向前移动,而是直接交换,而比较所消耗的CPU要比交换所消耗的CPU要小一些,所以选择排序是时间复杂度要比冒泡排序好一些。

选择排序是一个不稳定的排序算法。 

6.归并排序

算法原理:

归并操作的工作原理如下:

第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列

第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置 

第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置

重复步骤3直到某一指针超出序列尾

将另一序列剩下的所有元素直接复制到合并序列尾

 

package com.wyc.merge;

public class MergeSort {

    int[] array;

    public MergeSort(int[] array) {
        this.array = array;
    }

    public void sort() {
        int[] temp = new int[array.length];//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
        sort(array, 0, array.length - 1, temp);
    }

    private 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 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++];
        }
    }

    public void print() {
        int i = 0;
        while (i < array.length) {
            System.out.println(array[i++]);
        }
    }

    public static void main(String[] args) {
        int[] arr = {3,1,9,6,8,12, 4, 5, 65, 3, 2, 3, 5, 57, 2};
        System.out.println(Arrays.toString(arr));
        MergeSort mergeSort = new MergeSort(arr);
        mergeSort.sort();
        System.out.println(Arrays.toString(arr));


    }
}

归并排序是稳定排序,它也是一种十分高效的排序,能利用完全二叉树特性的排序一般性能都不会太差。java中Arrays.sort()采用了一种名为TimSort的排序算法,就是归并排序的优化版本。从上文的图中可看出,每次合并操作的平均时间复杂度为O(n),而完全二叉树的深度为|log2n|。总的平均时间复杂度为O(nlogn)。而且,归并排序的最好,最坏,平均时间复杂度均为O(nlogn) 


最后用表格总结一下: 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值