面试中常用的排序算法

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Irving_zhang/article/details/78929944

排序算法是算法的入门知识,其经典思想可以用于很多算法当中。因为其实现代码较短,应用较常见。所以在面试中经常会问到排序算法及其相关的问题。但万变不离其宗,只要熟悉了思想,灵活运用也不是难事。一般在面试中最常考的是快速排序和归并排序,并且经常有面试官要求现场写出这两种排序的代码。对这两种排序的代码一定要信手拈来才行。还有插入排序、冒泡排序、堆排序、选择排序、基数排序、桶排序等。面试官对于这些排序可能会要求比较各自的优劣、各种算法的思想及其使用场景。还有要会分析算法的时间和空间复杂度。通常查找和排序算法的考察是面试的开始,如果这些问题回答不好,估计面试官都没有继续面试下去的兴趣都没了。所以想开个好头就要把常见的排序算法思想及其特点要熟练掌握,有必要熟练写出代码[1]。

对排序算法的分类方式也有很多种[2]:

1、计算的时间复杂度(最差、平均、和最好性能),依据列表(list)的大小(n)。一般而言,好的性能是O(n log n),坏的性能是O(n2)。对于一个排序理想的性能是O(n),但平均而言不可能达到。基于比较的排序算法对大多数输入而言至少需要O(n log n)。

2、空间复杂度。

3、稳定性:稳定排序算法会让原本有相等键值的纪录维持相对次序。也就是如果一个排序算法是稳定的,当有两个相等键值的纪录R和S,且在原本的列表中R出现在S之前,在排序过的列表中R也将会是在S之前。

4、排序的方法:交换、选择、插入、合并等等。

首先给出一个对比的表格,以便从整体上理解排序算法:

这里写图片描述

接下来我们按照

交换排序:冒泡排序、快速排序

选择排序:选择排序、堆排序

插入排序:插入排序

归并排序:归并排序

的顺序、分析一下这六种常见的排序算法及其使用场景。限于篇幅,某些算法的详细演示和图示请在算法导论中寻找详细的参考。值得一提的是,本文中的代码思想都源自算法导论,如果有不明白的代码请翻阅算法导论一书。本文专为面试前突击而作,套路和思想和算法导论一模一样,即以方便记忆为主。

交换排序

交换排序的基本方法是在待排序的元素中选择两个元素,将他们的值进行比较,如果反序则交换他们的位置,直到没有反序的记录为止。交换排序中常见的是冒泡排序和快速排序。

冒泡排序

冒泡排序算法的伪代码如下:

function bubble_sort (array, length) {
    var i, j;
    for(i from 0 to length-1){
        for(j from 0 to length-1-i){
            if (array[j] > array[j+1])
                swap(array[j], array[j+1])
        }
    }
}

参考伪代码不难写出代码:

 /**
 * @param arr
 * 1、冒泡排序
 * 冒泡排序时间复杂度O(n^2),比较次数多,交换次数多。因此是效率极低的算法。
 * 冒泡排序是一种稳定的算法。
 */
public static void bubbleSort(int[] arr){ 
    int len = arr.length;
    for(int i = 0; i < len - 1; i++){
        for(int j = 0; j < len - 1 - i; j++){
            if(arr[j] > arr[j + 1]){
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }

} 

冒泡排序对n个项目需要O(n^2)的比较次数,且可以原地排序。尽管这个算法是最简单了解和实现的排序算法之一,但冒泡排序的实现通常会对已经排序好的数列拙劣地运行O(n^2),它对于包含大量的元素的数列排序是很没有效率的。

快速排序

快速排序是冒泡排序的一种改进,冒泡排序排完一趟是最大值冒出来了,那么可不可以先选定一个值,然后扫描待排序序列,把小于该值的记录和大于该值的记录分成两个单独的序列,然后分别对这两个序列进行上述操作。这就是快速排序,我们把选定的那个值称为枢纽值,如果枢纽值为序列中的最大值,那么一趟快速排序就变成了一趟冒泡排序。

快速排序是基于分治模式的[3]:

分解:数组A【p..r】被划分成两个(可能空)子数组A【p..q-1】和A【q+1..r】,使得A【p..q-1】中的每个元素都小于等于A(q),而且,小于等于A【q+1..r】中的元素。下 标q 也在返个划分过程中迕行计算。

解决:通过递归调用快速排序,对子数组A【p..q-1】和A【q+1..r】排序。

合并:因为两个子数组使就地排序的,将它们的合并不需要操作:整个数组A【p..r】已排序。

/**
 * @param arr
 * @param p
 * @param r
 * 2、快排
 * 快排最坏时间复杂度O(n^2),平均时间复杂度O(nlgn)。空间复杂度为O(nlgn)。
 * 快排是一种不稳定的算法。
 */
public static void quickSort(int[] arr, int p, int r){
    if(p < r){
        int q = partition(arr, p, r);
        quickSort(arr, p, q - 1);
        quickSort(arr, q + 1, r);

    }
    return;
}

public static int partition(int[] arr, int p, int r){
    int x = arr[r];
    int i = p - 1;
    for(int j = p; j < r; j++){
        if(arr[j] < x){
            swap(arr, ++i, j);
        }
    }
    swap(arr, ++i, r);
    return i;
}

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

快速排序是最常用的一种排序算法,包括C的qsort,C++和Java的sort,都采用了快排(C++和Java的sort经过了优化,还混合了其他排序算法)。

快排最坏情况O( n^2 ),但平均效率O(n lg n),而且这个O(n lg n)几号中隐含的常数因子很小,快排可以说是最快的排序算法,并非浪得虚名。另外它还是就地排序。

举一个例子,java中arrays.sort()方法:

1)当待排序的数组中的元素个数较少时,源码中的阀值为7,采用的是插入排序。尽管插入排序的时间复杂度为0(n^2),但是当数组元素较少时,插入排序优于快速排序,因为这时快速排序的递归操作影响性能。

2)较好的选择了划分元(基准元素)。能够将数组分成大致两个相等的部分,避免出现最坏的情况。例如当数组有序的的情况下,选择第一个元素作为划分元,将使得算法的时间复杂度达到O(n^2).

 源码中选择划分元的方法:

  当数组大小为 size=7 时 ,取数组中间元素作为划分元。int n=m>>1;(此方法值得借鉴)

  当数组大小 7

选择排序

选择排序的基本思想是,每趟排序在待排序序列中,选择值较小的元素,顺序添加到有序序列的最后,直到全部记录排序完毕。常用的有简单选择排序和堆排序。

简单选择排序

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

/**
 * @param arr
 * 3、选择排序
 * 选择排序时间复杂度O(n^2)。比较次数多,交换次数少。
 * 选择排序是一种不稳定的排序算法。例如:(7) 2 5 9 3 4 [7] 1...
 * 当我们利用直接选择排序算法进行排序时,(7)和1调换,(7)就在[7]的后面了,原来的次序改变,这样就不稳定.
 */
public static void selectSort(int[] arr){
    int len = arr.length;
    for(int i = 0; i < len - 1; i++){
        int min = i;
        for(int j = i + 1; j < len; j ++){
            if(arr[min] > arr[j]){
                min = j;
            }
        }
        swap(arr, i, min);
    }
}

简单选择排序是移动次数最少的算法。原始序列为正序时,比较次数O( n^2 ),移动次数为0;逆序时,比较次数O( n^2 ),移动次数O( n )。平均情况下时间复杂度O( n^2 ),空间复杂度O( 1 )。

另外简单选择排序是不稳定的;简单选择排序是原地排序。

堆排序

在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆排序需要用到堆中定义以下三个种操作:

  1. 最大堆调整(Max_Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点。
  2. 创建最大堆(Build_Max_Heap):将堆所有数据重新排序。
  3. 堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算。
/**
         * @param arr
         * 4、堆排序
         * 堆排序有三个操作:1、维护堆性质;2、建堆;3、堆排序
         * 1、维护堆性质,时间复杂度为O(lgn)
         * 2、建堆,时间复杂度O(n)
         * 3、堆排序,n-1次调用维护堆性质函数,每次时间复杂度为O(lgn),因此总体时间复杂度为O(nlgn)。
         * 空间复杂度为O(lgn)
         * 堆排序是一种不稳定的排序。
         */
        public static void heapSort(int[] arr){
            /*
             *  第一步:将数组堆化
             *  beginIndex = 第一个非叶子节点。
             *  从第一个非叶子节点开始即可。无需从最后一个叶子节点开始。
             *  叶子节点可以看作已符合堆要求的节点,根节点就是它自己且自己以下值为最大。
             */
            int len = arr.length - 1;
            int beginIndex = (len - 1) >> 1; 
            for(int i = beginIndex; i >= 0; i--){
                maxHeapify(arr, i, len);
            }

            /*
             * 第二步:对堆化数据排序
             * 每次都是移出最顶层的根节点A[0],与最尾部节点位置调换,同时遍历长度 - 1。
             * 然后从新整理被换到根节点的末尾元素,使其符合堆的特性。
             * 直至未排序的堆长度为 0。
             */
            for(int i = len; i > 0; i--){
                swap(arr, 0, i);
                maxHeapify(arr, 0, i - 1);
            }
        }


        /**
         * 调整索引为 index 处的数据,使其符合堆的特性。
         * 
         * @param index 需要堆化处理的数据的索引
         * @param len 未排序的堆(数组)的长度
         */
        public static void maxHeapify(int[] arr, int i, int len){
            int l = 2 * i + 1;              // 左子节点索引
            int r = l + 1;                  // 右子节点索引
            int largest = i;                // 默认父节点索引为最大值索引
            if(l <= len && arr[l] > arr[i]){        //判断左子节点是否比父节点大
                largest = l;
            }
            if(r <= len && arr[r] > arr[largest]){              //判断右子节点是否比父节点大
                largest = r;
            }
            if(largest != i){
                swap(arr, i, largest);              // 如果父节点被子节点调换,
                maxHeapify(arr, largest, len);              // 则需要继续判断换下后的父节点是否符合堆的特性。
            }
        }

插入排序

插入排序不是通过交换位置,而是通过比较找到合适的位置插入元素。类似于打扑克牌,整牌的时候就是拿到一张牌,找到一个合适的位置插入。这个原理其实和插入排序是一样的。

直接插入排序

/**
 * @param arr
 * 5、插入排序
 * 插入排序平均时间复杂度为O(n^2),空间复杂度为O(1)。
 * 直接插入排序是一种稳定的算法,当数组长度较小时,效果要比快排好。
 */
public static void insertSort(int[] arr){
    int len = arr.length;
    for(int i = 1; i < len; i++){
        int temp = arr[i];
        int j = i - 1;
        // 找到合适的位置j来插入arr[i]
        while(j >= 0 && arr[j] > temp){
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = temp;
    }
}

合并排序

合并排序的基本方法是,将两个或两个以上的有序序列归并成一个有序序列。常见的算法有归并排序。

归并排序

/**
         * @param arr
         * 6、归并排序
         * 归并排序平均时间复杂度为O(nlgn),空间复杂度为O(n)。
         * 归并排序是一种稳定的算法。
         * 
         */
        public static void mergeSort(int[] arr, int p, int r){
            if (p < r){
                int q = (p + r) / 2;
                mergeSort(arr, p, q);
                mergeSort(arr, q + 1, r);
                merge(arr, p, q, r);
            }

        }

        public static void merge(int[] arr, int p, int q, int r){
            int len1 = q - p + 1;
            int len2 = r - q;
            // 创建长度为len1和len2的新数组。
            int[] arr1 = new int[len1 + 1];
            int[] arr2 = new int[len2 + 1];
            // 赋值,尾部值为无穷。
            for(int i = 0; i < len1; i++){
                arr1[i] = arr[p + i];
            }
            for(int i = 0; i < len2; i++){
                arr2[i] = arr[q + 1 + i];
            }
            arr1[len1] = Integer.MAX_VALUE;
            arr2[len2] = Integer.MAX_VALUE;
            // 比较两个新数组的元素大小,将小的元素添加的arr,进行排序。
            for(int k = 0, i = 0, j = 0; k < len1 + len2; k++){
                if(arr1[i] < arr2[j]){
                    arr[p + k] = arr1[i++];
                }else{
                    arr[p + k] = arr2[j++];
                }

            }
        }

参考文献

[1] 面试中的 10 大排序算法总结

[2] 排序算法wiki

[3] 算法导论

[4] 【算法导论】排序 (三):快速排序 深入分析

[5] Java Arrays.sort源代码解析

猜你喜欢

转载自blog.csdn.net/Irving_zhang/article/details/78929944
今日推荐