【专题讲解】排序那些事儿

写在前面:从2020年的3月疫情在家,闲着无聊开始研究研究算法导论和刷题。一下子一年过来了,也到了准备春招的时候。最开始的文章都是python写的,因为都还是按照算法岗进行准备的。后来发现似乎还是开发岗更符合实际,因此也就开始用java进行刷题。在换了一种语言以后,更加觉着其实算法是与编程语言完全无关的,更多的是一种思想。近期会重新整理我写过的算法笔记。对于古老的版本会尝试补上java版本。

排序方法比较

首先,在面试中,排序问题最常被问到的就是排序的复杂度和排序的稳定性。进一步深化的话还有针对快排和归并的分治思想
在这里插入图片描述

其中快排是核心,归并同样思路重要。堆排序是一个更加复杂的数据结构,如果希望自己手撕堆是很复杂的,可以参考【专题讲解】手撕堆

在java中你使用 Collections.sort 的时候是TimSort。你在IDE 里点进去 Arrays.sort 这个方法,你会发现是 DualPivotQuicksort。它混合了快速排序,插入排序,归并排序,桶排序,TimSort。

快排

思想比较简单,就是寻找一个哨兵,小于pivot的放到左侧,大于pivot的放在右侧。如果为了针对面试,我们会采用交换指针的方法,并且随机化pivot的方法实现。

class Solution {
    public int[] sortArray(int[] nums) {
        quickSort(nums, 0, nums.length-1);
        return nums;
    }
    public void swap(int[] arr, int i, int j ){
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

    public void quickSort(int[] arr, int l, int r){
        if (l>r) return; // 这里是一个判断,也就是base情况的处理
        int index = partition(arr, l, r);
        quickSort(arr, index+1, r);
        quickSort(arr, l, index-1);
    }

    public int partition(int[] arr , int l, int r){
        int pivot = arr[l];
        //int pivot  = new Random().nextInt(r - l + 1) + l;
        int index = l;
        for(int i = l+1; i<=r;i++){
            if (arr[i]<pivot){
                index++;
                swap(arr, index, i);
            }
        }
        swap(arr, index, l); // 这里记着最后交换回来
        return index;
    }
}

归并算法

归并算法的思想是,首先把数组拆分为两个数组,分别排序成有序数组以后再合并一起成为新的有序数组。有点递归的意思了。

class Solution {
    public int[] sortArray(int[] nums) {
        int n = nums.length;
        if (n == 1 || n == 0) return  nums;
        int[] left = Arrays.copyOfRange(nums, 0, n/2);
        int[] right = Arrays.copyOfRange(nums, n/2,n);
        int[] ans = merge(sortArray(left), sortArray(right));
        return ans;
    }

    public int[] merge(int[] a, int[] b){
        int n = a.length;
        int m = b.length;
        int[] ans = new int[n+m];
        int i = 0;
        int j = 0;
        while (i<n && j<m){
            int nums1 = a[i];
            int nums2 = b[j];
            if (nums1<=nums2){
                ans[i+j] = nums1;
                i++;
            }else{
                ans[i+j] = nums2;
                j++;
            }
        }
        while (i != n){
            ans[i+j] = a[i];
            i++;
        }
        while (j != m){
            ans[i+j] = b[j];
            j++;
        }
        return ans;
    }
}

TopK问题

这个类问题可以被认为是排序问题的一个变种问题,主要就是计算最小的第K个数或者前K个数字。比较好的思路一般是两个,一个是采用堆的方法,构造一个小顶堆,动态的维护这个堆。二是采用快排的优化策略。

堆排序

建堆的时间复杂度是 O ( K ) O(K) O(K), 后续的插入操作是 O ( N l o g K ) O(NlogK) O(NlogK)

另外这里需要补充些对于堆的内容,
建堆有2种方法

第一种方法:HeapInsert(本题就是这种方法),它可以假定我们事先不知道有多少个元素,通过不断往堆里面插入元素进行调整来构建堆。这种插入建堆的时间复杂度是O(NlogN)

第二种方法:Heapify
从最后一个非叶子节点一直到根结点进行堆化的调整。如果当前节点小于某个自己的孩子节点(大根堆中),那么当前节点和这个孩子交换。这种建堆的时间复杂度是O(N)

Heapify是一种类似下沉的操作,HeapInsert是一种类似上浮的操作。

// 保持堆的大小为K,然后遍历数组中的数字,遍历的时候做如下判断:
// 1. 若目前堆的大小小于K,将当前数字放入堆中。
// 2. 否则判断当前数字与大根堆堆顶元素的大小关系,如果当前数字比大根堆堆顶还大,这个数就直接跳过;
//    反之如果当前数字比大根堆堆顶小,先poll掉堆顶,再将该数字放入堆中。
class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        if (k == 0 || arr.length == 0) {
            return new int[0];
        }
        // 默认是小根堆,实现大根堆需要重写一下比较器。
        Queue<Integer> pq = new PriorityQueue<>((v1, v2) -> v2 - v1);
        for (int num: arr) {
            if (pq.size() < k) {
                pq.offer(num);
            } else if (num < pq.peek()) {
                pq.poll();
                pq.offer(num);
            }
        }
        
        // 返回堆中的元素
        int[] res = new int[pq.size()];
        int idx = 0;
        for(int num: pq) {
            res[idx++] = num;
        }
        return res;
    }
}

快排优化

快排已经是我们前面介绍得了,这里的核心是,我们在partition函数返回index以后,我们知道左侧和右侧的大小,对于超出K的范围的部分我们已经不用进行额外的排序了。

class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        // if (k == 0 || arr.length == 0) {
        //     return new int[0];
        // }
        return quickSort(arr, 0, arr.length-1, k-1);
    }

    public void swap(int[] arr, int i, int j ){
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

    public int[] quickSort(int[] arr, int l, int r, int k){
        if (l>r)return new int[0];
        int index = partition(arr, l, r);
        if(index == k){
            return Arrays.copyOfRange(arr, 0, k+1);
        }
        else if (index<k){
            return quickSort(arr, index+1, r,k);
        }else{
            return quickSort(arr, l, index-1, k);
        }
    }

    public int partition(int[] arr , int l, int r){
        int pivot = arr[l];
        int index = l;
        for(int i = l+1; i<=r;i++){
            if (arr[i]<pivot){
                index++;
                swap(arr, index, i);
            }
        }
        swap(arr, index, l);
        return index;
    }
}

时间复杂度的计算

首先我们对于一些常用的时间复杂度应该是需要很熟悉的,比如dp的时间复杂度,状态压缩dp的次方级别,已经二分等的对数级别的。除此以外,最长用于计算时间复杂度的就是主定理

主定理

假如有式子 T ( n ) = a T ( n b ) + f ( n ) T(n) = aT(\frac{n}{b})+f(n) T(n)=aT(bn)+f(n),其中 n n n为问题规模, a a a为递推的子问题数量, n b \frac{n}{b} bn为每个子问题的规模(假设每个子问题的规模基本一样), f ( n ) f(n) f(n)为递推以外进行的计算工作。

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值