当前位置:首页 > 科技  > 软件

有了这个代码模板,合并排序手到擒来

来源: 责编: 时间:2023-10-08 07:06:01 168观看
导读排序在我们的的工程应用中无处不见,也有着非常重要的作用,比如你随意点开一个搜索引擎,搜索的结构就是经过排序而来。各种电商网站的秒杀活动,用户点击秒杀后,服务器会根据用户的请求时间进行排序。在我们的用的文档表格中

排序在我们的的工程应用中无处不见,也有着非常重要的作用,比如你随意点开一个搜索引擎,搜索的结构就是经过排序而来。各种电商网站的秒杀活动,用户点击秒杀后,服务器会根据用户的请求时间进行排序。在我们的用的文档表格中,也存在各种排序。vPV28资讯网——每日最新资讯28at.com

所以排序真的是无处不见,所以在我们面试中出现排序也不足为奇了。今天就为大家带来面试中经常出现的一种排序算法,合并排序进行深度解析。vPV28资讯网——每日最新资讯28at.com

合并排序本质上是一个后续遍历

合并排序本质上与二叉树的后序遍历非常类似的。vPV28资讯网——每日最新资讯28at.com

首先你还先回忆一下二叉树的后续遍历,后序遍历有个三个重要的特点:vPV28资讯网——每日最新资讯28at.com

  • 拿到子树的信息;
  • 利用子树的信息;
  • 整合出整棵树的信息。
// 递归function postOrder(root, array = []) {  if (root === null) return null;  postOrder(root.left, array);  postOrder(root.right, array);  array.push(root.val)}

对于合并排序来说,其实也是非常类似:vPV28资讯网——每日最新资讯28at.com

  • 拿到子数组的信息;
  • 利用子数组的信息;
  • 整合(排序)出整个数组的信息。

简单利用伪代码表示就是:vPV28资讯网——每日最新资讯28at.com

function 后序遍历/合并排序: sub = 子结构(子树/子数组) full = 整合(sub)

不管是后续遍历,还是合并排序的三个特点,这里可以总结为三个关键点:vPV28资讯网——每日最新资讯28at.com

  • 划分子结构
  • 获取子结构的信息
  • 利用子结构的信息整合成一个树/结果

1. 划分子结构

对于二叉树而言,子树的划分是天然的,已经在数据结构里面约定好了,比如 Node.left、Node.right。vPV28资讯网——每日最新资讯28at.com

root.leftroot.right 可以直接通过树的子节点拿

但是对于数组而言,在切分的时候,如果想到达最优的效率,那么将数组切为平均的两半效率应该是最高的。vPV28资讯网——每日最新资讯28at.com

const mid = begin + ((end - begin)>>1)数组a = [begin, mid) => 表示左子数组数组a = [mid, end) => 表示右子数组

2. 获取子结构的信息

对于二叉树来说,获取子结构的信息就是或者左右子节点的信息。vPV28资讯网——每日最新资讯28at.com

postOrder(root.left)postOrder(root.right)

对于合并排序来说,那么就分别需要对左子数组和右子数组进行排序。对子数组的排序,只需要递归就可以了。vPV28资讯网——每日最新资讯28at.com

merge(a, begin, mid)merge(a, mid, end)

3. 整合(排序)出整个数组/树的信息。

接下来,我们需要将从子结构里面拿到的信息进行加工。不同的需求会导致加工的方式也不太一样。vPV28资讯网——每日最新资讯28at.com

对于二叉树来说,非常简单,就是将节点值添加到结果中。vPV28资讯网——每日最新资讯28at.com

array.push(root.val)

对于合并排序而言,我们需要将两个有序的子数组,合并成一个大的有序的数组。vPV28资讯网——每日最新资讯28at.com

let i = begin;let j = mid;let to = begin;// 将两个数组合并,判断条件是,只有左右子数组中还有元素while(i < mid || j < end) {  // 读取左数组的元素:  //   - 左数组还存在元素并且左数组的开头元素小于右数组的开头元素  //    - 右数组没有元素  if ((i < mid && a[i] < a[j]) || j >=end) {    // t 为临时数组    t[to++] = a[i++];  } else {  // 读取右数组的元素    t[to++] = a[j++];    }}

最后,不管是二叉树还是合并排序都要考虑一下边界:vPV28资讯网——每日最新资讯28at.com

二叉树的边界就是节点不能为空。vPV28资讯网——每日最新资讯28at.com

if (root === null) return null;

合并排序的边界就是:vPV28资讯网——每日最新资讯28at.com

  • 当 b >= e,说明这个区间是一个空区间,没有必要再排序;
  • 当 b + 1 === e,说明只有一个元素,也没有必要排序。
if (b > e || b + 1 >= e) {  return }

小结

对于二叉树来说,代码相对比较简单。vPV28资讯网——每日最新资讯28at.com

function postOrder(root, array = []) {  // 边界处理  if (root === null) return null;  // 第一步:划分子结构,二叉树在结构上已经划分了子结构 root.left、root.right 可以直接通过树的子节点拿  // 第二步:获取子结构信息(递归的方式)  postOrder(root.left, array);  postOrder(root.right, array);  // 第三步:整合子结构信息  array.push(root.val)}

对于二叉树来说,如何切分左右子数组?如何进行合并,合并时注意循环的条件,以及稳定排序的写法?都是在写算法时需要注意的。vPV28资讯网——每日最新资讯28at.com

function merge(a, t, b, e) { // 边界处理  if (b > e || b + 1 >= e) {    return   } /*********************核心代码****************************/  // 第一步:划分子结构  const mid = b + ((e-b)>>1);  // 第二步:获取子结构信息(递归的方式)  merge(a, t, b, mid); // 左边子结构  merge(a, t, mid, e); // 右边子结构  // 第三步:整合子结构信息  let i = b;  let j = mid;  let to = b;  // 注意:下面是一个很重要的模板❤❤❤❤❤❤❤❤❤❤❤❤ // 将两个数组合并,判断条件是,只有左右子数组中还有元素  while(i < mid || j < e) {    // 读取左数组的元素:    //   - 左数组还存在元素并且左数组的开头元素小于右数组的开头元素    //    - 右数组没有元素   if ((i < mid && a[i] < a[j]) || j >=e) {      t[to++] = a[i++];    } else {    // 读取右数组的元素      t[to++] = a[j++];      }  } /*********************核心代码****************************/  // 将合并的结果拷贝到源数组中  for (let i = b; i < e; i++) {    a[i] = t[i];  }}function mergeSort(nums) {  if (nums === null || nums.length === 0) {    return;  }  merge(nums, [], 0, nums.length)  return nums;}

接着我们利用刚才将的例子来看几个例子。vPV28资讯网——每日最新资讯28at.com

例1:排序链表

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。vPV28资讯网——每日最新资讯28at.com

这道题目就可以套用我们上面提到的模板。vPV28资讯网——每日最新资讯28at.com

第一步:划分子结构,对于链表来说划分子结构,也就是找到链表的中间节点。链表找中间节点也就是利用我上一篇文章中讲到的“快慢指针”。vPV28资讯网——每日最新资讯28at.com

let fast = head,    slaw = head;// 第一步:划分子结构,快慢指针,一个节点走一步,另外一个节点走两步,一快一慢// 这里 tail 相当于上面数组中的 end,对于链表来说,end 也就是 nullwhile(fast !== tail) {    slaw = slaw.next;    fast = fast.next;    if (fast && fast !== tail) {        fast = fast.next;    }}const mid = slaw;

第二步:获取子结构信息(递归的方式)。vPV28资讯网——每日最新资讯28at.com

// 第二步:获取子结构信息const list1 = sort(head, mid);const list2 = sort(mid, tail)

第三步:整合信息,有了两个子结构信息,也就需要将两个子结构信息合成一个,对于链表来说就是合并两个有序链表。这里合并的过程中,还可以用到到我上一篇文章说到的“链表第一板斧,假头”。vPV28资讯网——每日最新资讯28at.com

// 第三步:整合,合并两个有序链表var merge = function(head1, head2) {    const dummy = new ListNode();    let tail = dummy;    let list1 = head1;    let list2 = head2;    while(list1 && list2) {        if (list1.val < list2.val) {            tail.next = list1;            tail = list1;            list1 = list1.next;        } else {            tail.next = list2;            tail = list2;            list2 = list2.next;        }    }    if (list1) {        tail.next = list1;    }    if (list2) {        tail.next = list2;    }    return dummy.next;}

最后少不了临界条件的判断。vPV28资讯网——每日最新资讯28at.com

if (head === null) {      return head;  }  if (head.next === tail) {      head.next = null;      return head;  }

完整的代码如下:vPV28资讯网——每日最新资讯28at.com

var merge = function(head1, head2) {    const dummy = new ListNode();    let tail = dummy;    let list1 = head1;    let list2 = head2;    while(list1 && list2) {        if (list1.val < list2.val) {            tail.next = list1;            tail = list1;            list1 = list1.next;        } else {            tail.next = list2;            tail = list2;            list2 = list2.next;        }    }    if (list1) {        tail.next = list1;    }    if (list2) {        tail.next = list2;    }    return dummy.next;}function sort(head, tail) {    if (head === null) {        return head;    }    if (head.next === tail) {        head.next = null;        return head;    }    let fast = head,        slaw = head;    // 第一步:划分子结构,快慢指针,一个节点走一步,另外一个节点走两步,一快一慢  // 这里 tail 相当于上面数组中的 end,对于链表来说,end 也就是 null    while(fast !== tail) {        slaw = slaw.next;        fast = fast.next;        if (fast && fast !== tail) {            fast = fast.next;        }    }    const mid = slaw;    // 第二步:获取子结构信息    const list1 = sort(head, mid);    const list2 = sort(mid, tail)    // 第三步:整合,合并两个有序链表    return merge(list1, list2);}var sortList = function(head) {    if (head === null || head.next === null) {        return head;    }    return sort(head, null)};

例2:寻找两个正序数组的中位数

给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 vPV28资讯网——每日最新资讯28at.com

算法的时间复杂度应该为 O(log (m+n)) 。vPV28资讯网——每日最新资讯28at.com

这是一道来自百度的面试题。解法有很多,我们重点介绍基于合并模板的解法。vPV28资讯网——每日最新资讯28at.com

如果单纯的不考虑复杂度,通过合并排序,我们已经能够将两个有序的数组合并成一个有序的数组了,再取这个有序数组的中位数。vPV28资讯网——每日最新资讯28at.com

var findMedianSortedArrays = function(nums1, nums2) {    function merge(a, t, b, e) {        // 边界处理        if (b > e || b + 1 >= e) {            return         }            /*********************核心代码****************************/        // 第一步:划分子结构        const mid = b + ((e-b)>>1);        // 第二步:获取子结构信息(递归的方式)        merge(a, t, b, mid); // 左边子结构        merge(a, t, mid, e); // 右边子结构        // 第三步:整合子结构信息        let i = b;        let j = mid;        let to = b;        // 注意:下面是一个很重要的模板❤❤❤❤❤❤❤❤❤❤❤❤            // 将两个数组合并,判断条件是,只有左右子数组中还有元素        while(i < mid || j < e) {            // 读取左数组的元素:            //   - 左数组还存在元素并且左数组的开头元素小于右数组的开头元素            //    - 右数组没有元素            if ((i < mid && a[i] < a[j]) || j >=e) {            t[to++] = a[i++];            } else {            // 读取右数组的元素            t[to++] = a[j++];              }        }            /*********************核心代码****************************/        // 将合并的结果拷贝到源数组中        for (let i = b; i < e; i++) {            a[i] = t[i];        }    }    const nums = [].concat(nums1, nums2);    merge(nums, [], 0, nums.length);    const mid = nums.length>>1;    if (nums.length % 2 === 0) {        return (nums[mid-1] + nums[mid]) / 2;    }    return nums[mid];};

但是这样操作的话,时间复杂度就变成 O(N),并且空间复杂度也是 O(N)。vPV28资讯网——每日最新资讯28at.com

如果在面试现场,面试官一定会问你,有没有更好的办法?所以我们应该有效地利用两个数组的有序性解决这道题。下面我会从简单的情况开始分析。vPV28资讯网——每日最新资讯28at.com

假设我们有一个一维有序数组,如果我们要拿第 9 小的数。(注:第 1 小就是最小的数。)只需要将前面 8 个数扔掉,然后排在前面的数就是第 9 小的数。vPV28资讯网——每日最新资讯28at.com

但是现在我们有多个有序数据,怎么办了?但是非常确认的是,我们如果想拿到第 9 小的数,一定需要丢 8 个数。vPV28资讯网——每日最新资讯28at.com

那么接下来,思考一下在两个数组 A,B 中如何扔掉这 8 个数?vPV28资讯网——每日最新资讯28at.com

  1. 要扔掉 4 个数,我们需要看一下两个数组前 4 个元素(平均分配一下);此时设 A[3] = L,B[3] = W。假设 L >= W,就需要证明:当 L >= W 的时候,[0, W] 都不可能是第 9 小的数,可以扔掉。

图片图片vPV28资讯网——每日最新资讯28at.com

  1. 当我们扔掉 4 个数之后,两个有序数组已经变成如下图所示的样子,由于我们的目标是扔掉 8 个数,扔掉 4 个数之后,还需要再扔 4 个数。此时我们只需要比较数组开头的一个元素 A[0], B[M] 的大小,谁小就把谁扔掉。这里我们假设 A[0] 比较小。

图片图片vPV28资讯网——每日最新资讯28at.com

  1. 此时还剩下 3 个数需要扔掉,那么按照上面的方式在进行丢弃就行。

所以总结一下,当我们需要丢弃 K 个元素的时候。k 是偶数的时候,我们只需要比较 A[k/2-1] 和 B[k/2-1] 的大小,谁小就扔掉对应的 [0...k/2-1] 这一段;k 是奇数的时候,我们只需要比较 A[k/2] 和 B[k/2] 的大小,谁小就扔掉对应的 [0...k/2] 这一段。不过由于整数在程序中的整除特性,我们可以将奇数和偶数的情况统一起来。需要扔掉 k 个数的时候,p = (k-1)/ 2,你只需要比较 A[p] 和 B[p] 的大小即可。如果 A[p] >= B[p],那么就可以把 B[0....p] 这段都扔掉。vPV28资讯网——每日最新资讯28at.com

var findMedianSortedArrays = function(A, B) {    let len = A.length + B.length;    let alen = A.length, blen = B.length;    let i = 0, j = 0;    // 如果两个数组的总长度为0    //那么不用再找了,肯定是没有中位数的,这里直接返回一个0    if (len == 0) {        return 0;    }    // 总长度为偶数的情况:    // 如果有4个数,那么当扔掉1个数之后    // 接下来需要合并的两个数排[2,3]就是中位数    // 总长度为奇数的情况:    // 比如如果有5个数,那么当合并掉2个数之后    // 接下来的那个排[3]位的就是中位数。    // 所以这里k表示:要扔掉的数的个数    // 第一步:划分子结构    let k = (len - 1) >> 1;    // 第二步:找到子结构信息    while (k > 0) {        // 我们需要比较A[p]与B[p]        // 只不过当数组的起始位置是i和j的时候。        // 比较的元素就变成 A[i+p], B[j+p]        let p = (k - 1) >> 1;        // 这时直接比较A[i + p]和B[j+p]来决定谁可以被扔掉掉        // 注意这里扔掉的时候,只需要前移p + 1即可。        if (j + p >= blen || (i + p < alen && A[i + p] < B[j + p])) {            i += p + 1;        } else {            j += p + 1;        }        k -= p + 1;    }    // 第三步:整合信息    // 把排在前面的数取出来    let front =        (j >= blen || (i < alen && A[i] < B[j])) ? A[i++] : B[j++];    // 如果总长度为奇数,那么这个时候,front就是我们要找的中位数    if ((len & 1) == 1) {        return front;    }    // 此时总的数目为偶数,那么需要再取一个数,求平均值。    let back =         (j >= blen || (i < alen && A[i] < B[j])) ? A[i] : B[j];    return (front + back) / 2.0;};

一共要合并的长度可以认为是 N/2,然后每次取一半进行合并。因此,合并次数为 O(lgN),空间复杂度为 O(1)。vPV28资讯网——每日最新资讯28at.com

总结

通过合并排序,可以将两个有序的数组合并成一个有序的数组了。合并是一个非常经典的模板代码,你一定要理解并且背下来,很多地方都会用。比如合并有序链表,合并数组。一个小小的合并模板可就以解决这么多问题,多积累模版可以帮助我们在面试中快速答题。vPV28资讯网——每日最新资讯28at.com

参考

  • https://leetcode.cn/problems/median-of-two-sorted-arrays/solutions/258842/xun-zhao-liang-ge-you-xu-shu-zu-de-zhong-wei-s-114/
  • https://kaiwu.lagou.com/course/courseInfo.htm?courseId=685#/detail/pc?id=6697

本文链接:http://www.28at.com/showinfo-26-12359-0.html有了这个代码模板,合并排序手到擒来

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: 简单聊一聊公平锁和非公平锁,Parallel并行流

下一篇: .NET Core使用SkiaSharp快速生成二维码( 真正跨平台方案)

标签:
  • 热门焦点
Top