【常用算法思路分析系列】排序高频题集

最近在牛客上整理常用的一些算法思路,【常用算法思路分析系列】主要是针对一些高频算法笔试、面试题目的解题思路进行总结,大部分也给出了具体的代码实现,本篇文章是对排序相关题目的思路分析。


1、简单分类

首先对一些常用算法按空间复杂度进行分类如下:

O(1)
冒泡排序、选择排序、插入排序、希尔排序、堆排序
O(logN)~O(N)
快速排序
O(N)
归并排序
O(M)
计数排序、基数排序


2、对一个基本有序的有序的数组排序,选择哪种排序算法?

基本有序:指如果把数组排好序的话,每个元素移动的距离不超过K,并且K相对于数组长度很小。

(1)对于时间复杂度为O(N)的排序算法:
    (计数排序、基数排序)
对于上述两种算法,由于不知道数组中元素的表示范围, 不适合

(2)对于时间复杂度为O(N2)的排序算法
     插入排序
    按题说明,基本有序的元素,每个元素移动的距离不超过K,因此插入排序中每个元素向前移动的距离也不会超过K,故此时插入排序的时间复杂度为O(N*K),属于 O(N2)中这种情况下比较好的一种实现
    
(3)对于时间复杂度为O(N*logN)的排序算法    
     快速排序
    与数组原始顺序无关,每次都是随机选择一个元素进行划分,因此此时还是O(N*logN),属于O(N*logN)较好的一种。
     归并排序:
    同样,与数组原始顺序无关。也算好此时还是O(N*logN)。

最优的一种改进后的堆排序

(具体可看这篇总结文章:[大、小根堆应用总结一]堆排序的应用场景)

代码实现如下:

public static int[] heapSort(int[] A, int n, int k) {  
        if(A == null || A.length == 0 || n < k){  
            return null;  
        }  
        int[] heap = new int[k];  
        for(int i = 0; i < k; i++){  
            heap[i] = A[i];  
        }  
        buildMinHeap(heap,k);//先建立一个小堆  
        for(int i = k; i < n; i++){  
            A[i-k] = heap[0];//难处堆顶最小元素  
            heap[0] = A[i];  
            adjust(heap,0,k);  
        }  
        for(int i = n-k;i < n; i++){  
            A[i] = heap[0];  
            heap[0] = heap[k-1];  
            adjust(heap,0,--k);//缩小调整的范围  
        }  
        return A;  
    }  
    //建立一个小根堆  
    private static void buildMinHeap(int[] a, int len) {  
        for(int i = (len-1) / 2; i >= 0; i--){  
            adjust(a,i,len);  
        }  
    }  
    //往下调整,使得重新复合小根堆的性质  
    private static void adjust(int[] a, int k, int len) {  
        int temp = a[k];  
        for(int i = 2 * k + 1; i < len; i = i * 2 + 1){  
            if(i < len - 1 && a[i+1] < a[i])//如果有右孩子结点,并且右孩子结点值小于左海子结点值  
                i++;//取K较小的子节点的下标  
            if(temp <= a[i]) break;//筛选结束,不用往下调整了  
            else{//需要往下调整  
                a[k] = a[i];  
                k = i;//k指向需要调整的新的结点  
            }  
        }  
        a[k] = temp;//本趟需要调整的值最终放到最后一个需要调整的结点处  
    }  

3、判断数组中是否有重复值,要求空间复杂度为O(1)

    (1) 如果 没有空间复杂度限制,用 哈希表实现。比如使用HashMap(或者利用字符的ASCII值作为下标的int数组),此时,时间复杂度为O(N),空间复杂度为O(N)。
    (2)对于有空间复杂度限制,我们可以 先排序,再判断的思路。因为排好序后,重复值排在了相邻位置。

此时,问题就转化为了, 空间复杂度限制为O(1)的情况下,考察经典排序算法,怎么实现一个最快的算法。
根据上面空间复杂度的统计,可以知道使用非递归实现的堆排序最快。 (具体可看这篇总结文章: [大、小根堆应用总结一]堆排序的应用场景
实现代码如下:

public static boolean checkDuplicate(int[] a, int n) {
        if(a == null || a.length == 0 || a.length == 1)
            return false;
        heapSort(a,n);
        for(int i = 1; i < n; i++){
            if(a[i] == a[i-1]){
                return true;
            }
        }
        return false;
    }

    private static void heapSort(int[] a,int n){
        for(int i = (n-1) / 2; i >= 0; i--){
            adjustDown(a,i,n);
        }
        int temp;
        for(int i = n-1; i > 0; i--){//只需要n-1趟
            temp = a[0];//交换堆顶元素
            a[0] = a[i];
            a[i] = temp;
            adjustDown(a,0,i);
        }
    }

    private static void adjustDown(int[] a , int k,int n){
        int temp = a[k];
        for(int i = 2 * k + 1; i < n; i = i * 2 + 1){
            if(i < n-1 && a[i] < a[i+1])//有右孩子结点,并且有孩子结点值大于左海子结点值,将i指向右孩子
                i++;
            if(temp >= a[i]) 
                break;
            else{//需要向下调整
                a[k] = a[i];
                k = i;//指向新的可能需要调整的结点
            }
        }
        a[k] = temp;
    }

4、把两个有序数组合并成一个数组,第一个数组空间正好可以容纳两个数组的元素

比如数组A、B有序,A可以容纳两个数组的元素。
思路:
可以从 A、B的末尾开始遍历比较,把较大的元素放到A的末尾,依次下去...
这里的关键是从A、B的末尾开始,这样可以尽量把A有用的部分保留下来。
代码实现如下:

public static int[] mergeAB(int[] A, int[] B, int n, int m) {
        if(A == null || B == null || A.length < n+m){
            return null;
        }
        int k = n + m - 1;
        int i = n - 1;//A的下标指示器
        int j = m - 1;//B的下标
        while(i >= 0 && j >= 0){
            if(A[i] < B[j]){
                A[k--] = B[j];
                j--;
            }else{
                A[k--] = A[i];
                i--;
            }
        }
        if(j >= 0){//表示数据B中还有元素,将B中剩余的元素放到A的前面
            while(k >= 0 && j >= 0){
                A[k--] = B[j--];
            }
        }
        return A;
    }

5、荷兰国旗问题

对只包含0,1,2三种元素值的数组进行排序,使得所有的0都在1的左边,所有的1在中间,所有的2在1的右边。要求使用交换、原地排序,而不是利用计数进行排序。

本题主要过程与快排划分过程类似,定义两个指针i0和i2,分别指向0区域和2区域,从头开始遍历,遇到1,继续;遇到0,交换0区域的后一位(即1区域的第一位)和当前遍历指向的元素,然后0区域向后扩大一位;遇到2,交换2区域的前一位(即1区域)和当前遍历指向的元素,2区域向前扩大一位。

时间复杂度为O(n),空间复杂度为O(1)。代码实现如下:

public class ThreeColor {
   
    public static void main(String[] args) {
        int[] a = {1,1,0,2,1,0,1,0,2,1,2,1,1,0,2,2,1};
        sortThreeColor(a,a.length);
        for(int i = 0; i < a.length; i++){
            System.out.print(a[i]+" ");
        }
    }

    public static int[] sortThreeColor(int[] A, int n) {
        int i0 = -1;//指向0区域的指针,0区域初始大小为0
        int i2 = n;//指向2区域的指针,2区域初始大小为0
        int temp;
        for(int i = 0; i < i2; i++){//注意!!!,这里是i

6、有序矩阵(二维数组)查找

现在有一个行和列都排好序的矩阵,请设计一个高效算法,快速查找矩阵中是否含有值x。
给定一个int矩阵mat,同时给定矩阵大小nxm及待查找的数x,请返回一个bool值,代表矩阵中是否存在x。所有矩阵中数字及x均为int范围内整数。保证nm均小于等于1000。
具体思路分析请看:[剑指Offer]二维数组中的查找

时间复杂度为O(m+n),代码如下:

public static boolean findX(int[][] mat, int n, int m, int x) {
        //从矩阵的左下角开始查找(只能是从左下角或者右上角,只有这两个角符合二叉排序树的特征)
        int i = n - 1;
        int j = 0;
        while(i >= 0 && j < m){
            if(mat[i][j] == x)
                return true;
            if(x < mat[i][j]){
                i--;
            }else{
                j++;
            }
        }
        return false;
    }

7、最短排序子数组:对于一个数组,请设计一个高效算法计算需要排序的最短子数组的长度

给定一个int数组A和数组的大小n,计算这个数组需要排序的最短子数组长度。(原序列位置从0开始标号,若原序列有序,返回0)。保证A中元素均为正整数。
测试样例:
[1,4,6,5,9,10],6
返回:2

解法:
需要分别计算出前面已拍好序的位置,和后面已排好序的位置。
时间复杂度为O(N),空间复杂度为O(1),代码如下:
public static int shortestSubsequence(int[] A, int n) {
        //left和right初始顺序一个相等!!!表示A可能初始有序
        int left = 0;//跟随从右到左过程中需要排序的位置
        int right = 0;//跟随从左到右过程中的一个需要排序的位置
        if(A == null || n == 0 || n == 1){
            return 0;
        }
        int max = A[0];
        int min = A[n-1];
        for(int i = 0; i < n; i++){//先从左到右遍历一遍,记录遍历中的最大值,将其与当前值进行比较
            if(A[i] > max)
                max = A[i];
            if(A[i] < max)
                right = i;//相当于是记录到最右边需要排序的位置
        }
        for(int i = n-1; i >= 0; i--){//再从右往左遍历,记录遍历中的最小值,将其与当前值进行比较
            if(A[i] < min)
                min = A[i];
            if(A[i] > min)
                left = i;//相当于是记录到最左边需要排序的位置
        }
        if(left == right)
            return 0;
        else
            return right - left + 1;
    }

8、相邻两数最大差值

有一个整形数组A,请设计一个复杂度为O(n)的算法,算出排序后相邻两数的最大差值。
最优解时间复杂度为O(N),空间复杂度为O(N),代码如下:

public static int maxGap(int[] A, int n) {
        if(A == null || n < 2)
            return 0;
        int min = A[0];
        int max = A[0];
        for(int i = 1; i < n; i++){
            if(A[i] > max)
                max = A[i];
            if(A[i] < min)
                min = A[i];
        }
        float gap = (max - min) * 1.0f / n;//将max-min分为n等分
        boolean[] hasNum = new boolean[n + 1];//当前桶编号是否有元素在里面
        int[] maxs = new int[n + 1];//存放某个桶中的最大值
        int[] mins = new int[n + 1];//存放某个桶中的最小值
        for(int i = 0; i < n; i++){
            int p = (int) ((A[i] - min) / gap);    //计算当前元素所属桶编号
            if(hasNum[p]){//如果该桶编号已经有值
                maxs[p] = maxs[p] < A[i] ? A[i] : maxs[p];
                mins[p] = mins[p] > A[i] ? A[i] : mins[p];
            }else{
                maxs[p] = A[i];
                mins[p] = A[i];
            }
            hasNum[p] = true;
        }
        int i = 0;
        int res = 0;
        int lastMax = 0;
        while(i <= n){
            if(hasNum[i++]){//找到第一个有元素的桶
                lastMax = maxs[i-1];
//                i++;
                break;
            }
        }
        while(i <= n){
            if(hasNum[i]){
                res = mins[i] - lastMax > res ? mins[i] - lastMax : res;
                lastMax = maxs[i];
            }
            i++;
        }
        return res;
    }


上面有些题目思路没有具体分析,但在代码实现中已加入了注释,手动模拟一遍应该没有问题。下一篇将总结【常用算法思路分析系列】字符串相关的题目。






你可能感兴趣的:(数据结构与算法,算法整理(Java版))