• 梳理总纲


    1.Talk is cheap, show you my code.
    Please pay attention to the remark.

    package solutions.algorithms.interview.merge;

    import java.util.Arrays;

    /**
    * 算法上下文:DC 、 DFS 、 BFS 、 DP 、 GD
    * 语法分析三工具:状态机、文法、函数
    * LeetCode三连
    * 其中前三是字节三轮面试的算法题
    * 中间两道也是某头部第一三轮面试的算法题
    * 最后一道是阿里一面的算法(不跑测试用例,说思路)
    * 均与分而治之相关
    */
    public class MergeSolution {

    /**
    * 面试时是会可能让你手写测试用例,因此前期刷题根基不稳的情况可以考虑取巧,手写必然使你的代码输出结果符合预期的测试用例。
    * @param strings
    */
    public static void main(String[] strings) {
    //测试1:有序二路归并
    int[] num1 = {1, 3, 5, 0, 0};
    int[] num2 = {2, 4, 6};
    mergeTwoArrays(num1, num2, 3, 2);
    System.out.println(Arrays.toString(num1));
    //测试2:链表归并排序
    ListNode node = new ListNode(3);
    ListNode head = node;
    node = node.next = new ListNode(5);
    node = node.next = new ListNode(2);
    node = node.next = new ListNode(4);
    node = node.next = new ListNode(-3);
    node = node.next = new ListNode(-133);
    node = node.next = new ListNode(13);
    System.out.println(mergeSortLinkedList(head));
    num2 = new int[]{4,89,1,13,4,6};
    quickSort(num2, 0, num2.length - 1);
    System.out.println(Arrays.toString(num2));
    //测试3:多路归并
    int[][] ints = {
    {1, 3, 5, 7, 9},
    {2, 4, 6, 8, 10},
    {11, 14, 16, 17, 19}
    };
    //测试3.1:降维+分治法
    // System.out.println(Arrays.toString(mergeSort(ints, 0, ints.length - 1)));
    //测试3.2:败者树归并
    System.out.println(Arrays.toString(mergeSortByLoseTree(ints)));

    int[] arr1 = {1, 3, 5, 7, 9};
    int[] arr2 = {2, 4, 6, 8, 10};
    //测试4:有序二路第K大数
    System.out.println(kthMergeArrays(arr1, arr2, 7));
    //测试5:有序二路中位数
    System.out.println(middleVals(arr1, arr2));
    //测试6:快速排序
    int[] arrays = {4, 6, 7, 12, 5, 3, 2, 1, 7};
    quickSort(arrays, 0, arrays.length - 1);
    System.out.println(Arrays.toString(arrays));
    }

    /**
    * 一个正序数组num1的有效元素长度为m,总长度为(m + n),
    * 另一个正序数组num2的有效元素长度为n,总长度为n,
    * 在num1上合并两个数组。
    * 归并两个有序数组
    * 1.常规做法:
    * 构造一个新的大数组,用两指针分别指向两个子数组通过比较合并元素到新数组(二路归并思路),最后把大数组复制回数组1即可;
    * 时间复杂度:O(n + m)
    * 空间复杂度:O(n + m)
    * 2.针对该题还有优化空间吗?
    * 既然已经开辟了num1这个大数组的空间,其原意就是原地合并,所以可以考虑一下如何实现?
    * 为了可以利用已经存在有效元素的数组1其余的空闲空间完成归并,可以考虑从数组末端完成归并。
    * 时间复杂度:O(m + n)
    * 空间复杂度:O(1)
    * 如以下代码实现。
    * @param num1
    * @param num2
    * @param m
    * @param n
    */
    public static void mergeTwoArrays(int[] num1, int[] num2, int m, int n) {
    int p1 = m - 1, p2 = n - 1;
    int i = m + n - 1;
    while (p1 >= 0 && p2 >= 0) {
    num1[i --] = num1[p1] > num2[p2] ? num1[p1 --] : num2[p2 --];
    }
    while (p1 >= 0) {
    num1[i --] = num1[p1 --];
    }
    while (p2 >= 0) {
    num2[i --] = num2[p2 --];
    }
    }

    private static class ListNode {

    int val;

    ListNode next;

    ListNode(int val) {
    this.val = val;
    }

    @Override
    public String toString() {
    ListNode node = this;
    StringBuilder builder = new StringBuilder();
    while (null != node) {
    builder.append(node.val).append("->");
    node = node.next;
    }
    builder.append(node);
    return builder.toString();
    }
    }

    /**
    * 给出一个单链表,以O(n log n)的时间复杂度完成排序
    * 显然,这题是往排序方面去考虑,因为题目没有表示结点值的数据特征,所以只能往经典排序方向去找一种时间复杂度为O(log n)的排序算法。
    * 主流的无非两种:快速排序和归并排序。
    * 快速排序为啥不采纳?它的时间复杂度与枢纽元素的位置相关,所以并非稳定的O(n log n)的时间复杂度去考虑,而且需要两个方向相反的指针,题目给出的是单链表,使用它实现排序无疑是额外增加不少消耗。
    * 故只能在链表上完成归并排序.
    * 时间复杂度:O(n log n)
    * 空间复杂度:O(n)
    * @param node
    * @return
    */
    public static ListNode mergeSortLinkedList(ListNode node) {
    if (null == node) {
    return null;
    }
    if (null == node.next) {
    return node;
    }
    ListNode head = node, slow = node, fast = node;
    //快慢指针法找到中位结点,注意只有两个结点的时候要可以收敛才能跳出递归。
    while (null != fast.next) {
    fast = fast.next;
    if (null == fast.next) {
    break;
    }
    slow = slow.next;
    fast = fast.next;
    }
    ListNode mid = slow.next;
    slow.next = null;
    ListNode list1 = mergeSortLinkedList(head);
    ListNode list2 = mergeSortLinkedList(mid);
    return mergeSort(list1, list2);
    }

    private static ListNode mergeSort(ListNode head, ListNode mid) {
    //虚拟结点
    ListNode node = new ListNode(0);
    ListNode ret = node;
    while (null != head && null != mid) {
    if (head.val < mid.val) {
    node = node.next = head;
    head = head.next;
    }else {
    node = node.next = mid;
    mid = mid.next;
    }
    }
    if (null != head) {
    node.next = head;
    }
    if (null != mid) {
    node.next = mid;
    }
    return ret.next;
    }

    /**
    * 三面:多路归并排序
    * 给K个长度为N的有序数组,合并成一个有序数组。
    * 我:最初给出对应K个数组的K个指针,通过指针指向元素比较得出当前最小值,最小值指针前移,填入返回数组中,遍历(kn)就得到结果,时间复杂度:O(k^2n)但是面试时面试官直接指出给错了复杂度。。
    * 面试官:还有优化空间吗?
    * 我:(错了后当场脑塞了一会)还有利用数组头元素构造有序,后面维护有序再逐一取出即可。(思路是对的,但傻上心头的时候直接说给它排序,被面试官说这样明显还增加了时间复杂度的。。)
    * 最后我还是太妄自菲薄了,当场否掉了自己的想法。这部分自主惨败收尾。
    * 面试官:应该用某个数据结构处理,下去再想吧。
    * 我:..应该用最小堆!!(下线过了一会后)
    * 事实上这是一个方法。用最小堆维护K路数组,用对应的当前指针指向元素维护堆结构,取最小值后将其用对应的下一个元素代替并调整堆,如若没有下一元素就把堆的大小减一,再从剩余元素取下一最小值。如果纯粹在内存考虑时间复杂度,遍历(kn)次,每次有log k的调整堆消耗,时间复杂度为 O(kn log k)
    * 事后查了一下,还有另一种数据结构可以达到几乎一样的优化程度取最小值的,就是胜者树和败者树,都是树形选择排序的变形。
    * 我:还有没有其他更优的办法?
    * 我:这就是命了,其实这方法并不陌生,也是以前跟同事讨论过处理归并的一种方法,所以发挥这么差的一次经历后,我真的经历一次挺痛苦的复盘:不要错过以前经历过的任何细节。
    * 当时讨论的背景其实是一个二维表,每行每列都有序,按顺序输出所有元素。这二维数组的每一行不就是相当于一个一维有序数组嘛,不就是等价K路归并...吗..。
    * 当时我提出的思路就是在一维实施归并,把一维数组当成一个元素,降维处理成每两个数组实施二路归并。元素总遍历次数为
    * k / 2 * n + k / 4 * 2n + k / 8 * 4n + ... + k / k * kn / 2 = kn/2 log k 次,相对最小堆和胜败树可以减半。但有利也有弊端:纯粹在内存排序才有优势; 如果应用于外部排序,会额外增加磁盘IO次数。
    *
    * @param arrs
    * @param start
    * @param end
    * @return
    */
    public static int[] mergeSort(int[][] arrs, int start, int end) {
    if (start >= end) {
    return arrs[end];
    }
    int mid = start + ((end - start) >>> 1);
    int[] arr1 = mergeSort(arrs, start, mid);
    int[] arr2 = mergeSort(arrs, mid + 1, end);
    return mergeTwoArrays(arr1, arr2);
    }

    private static int[] mergeTwoArrays(int[] arr1, int[] arr2) {
    int p1 = 0;
    int p2 = 0;
    int length1 = arr1.length;
    int length2 = arr2.length;
    int[] rs = new int[length1 + length2];
    int r = 0;
    while (p1 < length1 && p2 < length2) {
    rs[r ++] = arr1[p1] <= arr2[p2] ? arr1[p1 ++] : arr2[p2 ++];
    }
    while (p1 < length1) {
    rs[r ++] = arr1[p1 ++];
    }
    while (p2 < length2) {
    rs[r ++] = arr2[p2 ++];
    }
    return rs;
    }

    /**
    * 败者树示例
    * @param arrs
    * @return
    */
    public static int[] mergeSortByLoseTree(int[][] arrs) {
    int k = arrs.length;
    if (k == 0) {
    return new int[0];
    }
    int n = arrs[0].length;
    int c = k * n;
    int[] rs = new int[c];
    //败者树,完全二叉树,可以用数组存储,存储败者元素下标
    int[] loseTrees = new int[k];
    int[] prs = new int[k];
    int[] externals = new int[k + 1];
    for (int i = 0; i < k; i++) {
    externals[i] = arrs[i][prs[i]];
    }
    externals[k] = Integer.MIN_VALUE;
    //建立败者树
    for (int i = 0; i < k; i++) {
    loseTrees[i] = k;
    }
    for (int i = (k - 1); i >= 0; i--) {
    adjustLoseTrees(loseTrees, externals, i, k);
    }
    int i = 0;
    int p;
    //循环(kn)次填充当前最小值
    while (i < c) {
    p = loseTrees[0];
    rs[i ++] = arrs[p][prs[p] ++];
    externals[p] = prs[p] >= arrs[p].length ? Integer.MAX_VALUE : arrs[p][prs[p]];
    adjustLoseTrees(loseTrees, externals, p, k);
    }
    return rs;
    }

    /**
    * 维护败者树的调整操作
    * @param loseTrees
    * @param externals
    * @param s
    */
    private static void adjustLoseTrees(int[] loseTrees, int[] externals, int s, int k) {
    int tmp;
    //t是结点(s + k)(映射externals[s])的父结点
    int t = s + ((k - s) >> 1);
    while (t > 0) {
    if (externals[s] > externals[loseTrees[t]]) {
    tmp = s;
    s = loseTrees[t];
    loseTrees[t] = tmp;
    }
    t >>>= 1;
    }
    loseTrees[0] = s;
    }

    /**
    * 一面:两个有序数组,求合并后的第K大元素。
    * 面试官:有什么思路?
    * 我:合并两个数组后取第K个元素。
    * 面试官:复杂度怎么样?
    * 我:设数组1长度为m,数组2长度为n,时间复杂度为O(m + n),空间复杂度为O(m + n);
    * 面试官:还有优化空间吗?
    * 我:纯粹需要找到第K大元素,可以用二路指针比较遍历找到第K大直接返回即可,时间复杂度为O(K),空间复杂度为O(1).(实际上当时差点懵了,因为觉得O(m + n)已经是线性复杂度,应该是最优了,差点不再尝试深层考虑)
    * 面试官:实现一下。
    * @param arr1
    * @param arr2
    * @param k
    * @return
    */
    public static int kthMergeArrays(int[] arr1, int[] arr2, int k) {
    assert arr1 != null && arr2 != null && k >= 0;
    int m = arr1.length;
    int n = arr2.length;
    //参数合法性校验
    if (k > m + n) {
    //不存在第K大
    return -1;
    }
    int p1 = 0;
    int p2 = 0;
    int p = 0;
    int t;
    while (p1 < m && p2 < n) {
    t = arr1[p1] < arr2[p2] ? arr1[p1 ++] : arr2[p2 ++];
    if (++p == k) {
    return t;
    }
    }
    while (p1 < m) {
    t = arr1[p1 ++];
    if (++p == k) {
    return t;
    }
    }
    while (p2 < n) {
    t = arr1[p2 ++];
    if (++p == k) {
    return t;
    }
    }
    return -1;
    }

    /**
    * 三面:两个正序数组,求两数组归并后的中位数元素。
    * 面试官:说一下思路?
    * 我:(想了一下。)可以用数组二路归并,归并到两数组总长度的一半时返回对应当前指针指向元素,即中位数。
    * 面试官:空间上用了数组存储,还有优化空间吗?
    * 我:(提示到我但当时还没接收到)我想想。
    * 面试官:先给你20分钟实现一下。
    * 我:(啪啪啪打起代码)...(写完了再检查)
    * 面试官:中间这里的变量t1,t2什么意思?
    * 我:刚刚您提示了我其实只需要返回中位数,就用了哨兵变量监视当前指针元素即可,到了中间长度返回即可。
    * 面试官:面试完了。
    * @param arr1
    * @param arr2
    * @return
    */
    public static double middleVals(int[] arr1, int[] arr2) {
    assert null != arr1 && null != arr2;
    int m = arr1.length;
    int n = arr2.length;
    int total = m + n;
    int limit = total >>> 1;
    double rs = 0D;
    int p1 = 0;
    int p2 = 0;
    //事实上可以省略,直接p1 + p2,但还要还原当时面试写法
    int p = 0;
    int t1 = -1, t2 = -1;
    while (p1 < m && p2 < n) {
    if (arr1[p1] < arr2[p2]) {
    t1 = arr1[p1 ++];
    }else {
    t2 = arr2[p2 ++];
    }
    if (limit == ++ p) {
    return (total & 1) == 0 ? (double)(t1 + t2) / 2 : Math.max(t1, t2);
    }
    }
    while (p1 < m) {
    t1 = arr1[p1 ++];
    if (limit == ++ p) {
    return (total & 1) == 0 ? (double)(t1 + t2) / 2 : Math.max(t1, t2);
    }
    }
    while (p2 < n) {
    t2 = arr2[p2 ++];
    if (limit == ++ p) {
    return (total & 1) == 0 ? (double)(t1 + t2) / 2 : Math.max(t1, t2);
    }
    }
    return -1;
    }
    //---------------quickSort---------------------------------------------------
    /**
    * 快速排序
    * @param arr
    * @param start
    * @param end
    */
    private static void quickSort(int[] arr, int start ,int end) {
    if (start >= end) {
    return;
    }
    int partition = partition(arr, start, end);
    quickSort(arr, start, partition - 1);
    quickSort(arr, partition + 1, end);
    }

    private static int partition(int[] arr, int start, int end) {
    int sp = start;
    int ep = end;
    int target = arr[sp];
    while (sp < ep) {
    while (sp < ep && arr[ep] >= target) {
    ep --;
    }
    arr[sp] = arr[ep];
    while (sp < ep && arr[sp] <= target) {
    sp ++;
    }
    arr[ep] = arr[sp];
    }
    arr[sp] = target;
    return sp;
    }
    }

    2.
    就一句提炼:归并排序的思想需要重点理清一下,抽象层面说它就是分而治之的一个入门典例。
    现实情况:分治法,几乎是实际应用中多数非线性问题的重要解决思路。如上文,笔者四、五月时面试头部,前三轮都是归并。允许笔者大胆猜测一下,题库这类分治法的题目最近也是高频,当然大家可以一起集思广益,一起求证。希望大家可以收获自己满意的offer!
  • 相关阅读:
    CSS伪元素:before/CSS伪元素:before/:after content 显示Font Awesome字体图标:after content 显示Font Awesome字体图标
    window.requestAnimationFrame
    HTML5 canvas clearRect() 方法
    canvas save()和canvas restore()状态的保存和恢复使用方法及实例
    git -分支管理(创建、推送、删除)
    微信小程序设置域名、不校验域名
    微信小程序访问豆瓣api403问题解决方发法
    干货:Vue粒子特效(vue-particles插件)
    chmod命令详细用法
    Python新式类和旧式类的区别
  • 原文地址:https://www.cnblogs.com/kentkit/p/14975034.html
Copyright © 2020-2023  润新知