数据结构与算法面试专题——引入及归并排序

 数据结构与算法引入

我们都知道数据结构与算法很重要,甚至会将其称为程序员的“内功”,但是我们花了很多时间学的算法和数据结构,好像就只是为了应对算法面试,对日常的开发工作没有什么帮助。

这点对于我们数据工程师来说,体感更甚——平时工作看起来主要是写SQL(甚至会被戏称SQL boy),算法就像是只存在于那些开箱即用的数据处理工具中而已,和我们的日常开发完全没什么关系。

但实际上,这只是因为时代浪花下,大数据领域还在成长期的自然现象。业务快速迭代时期,对于成本和效率看的不是很重,全靠平台架构的能力兜底,对于数据工程师来说,做好业务需求实现就行。

随着大数据领域的成熟,企业也都减缓了快速扩张的脚步,因此最近几年我们听的最多的一个词就是——降本增效。

现在对于数据工程师来说,不但要完成业务需求,还需要能够保质保量,高效产出。

这就需要掌握大数据基础知识,学好数据结构和算法,并且在生产实践中选择最合适的方案去落地。这也是很多早期进入大数据行业的小伙伴,逐步被淘汰的根本原因。

由于数据结构与算法经典书籍与教程太多了,我这里推荐《剑指Offer(专项突破版):数据
结构与算法名企面试题精讲》和《数据结构与算法图解》(杰伊·温格罗)两本书。

数据结构与算法面试专题,则是针对数据开发面试与工作中所必备的数据结构与算法知识进行讲解。

归并排序基础

  • 题目1:归并排序递归版
  • 题目2:归并排序递归非递归版

介绍

归并排序(Merge Sort)是建立在归并操作上的一种有效的排序算法。

该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,需要额外空间作为代价。

将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。二路归并排序就是两两排序,然后两个区域一起排序,以此类推。

归并排序是稳定的。因为在使用额外空间的时候,靠前区域的元素只要小于等于靠后区域的元素就能被放进额外空间。

其实现原理如下:

  • 首先,将待排序的数组不断地分成两半,直到每个子数组只有一个元素(一个元素本身就是有序的)。
  • 然后,对划分后的子数组进行合并。合并的过程是比较两个已排序子数组的元素,将较小的元素依次放入一个新的辅助数组中,直到其中一个子数组的元素全部放入辅助数组,再将另一个子数组剩余的元素直接复制到辅助数组的后面。
  • 最后,将辅助数组中的已排序元素复制回原数组对应的位置。
  • 通过不断地对子数组进行分割和合并操作,最终整个数组就会被排序。

归并排序的平均时间复杂度、最坏时间复杂度和最好时间复杂度均为 O (nlogn),空间复杂度为 O (n)。它是一种稳定的排序算法,即相同元素的相对顺序在排序前后保持不变。

题目1:归并排序递归版

整体是通过递归实现:左边排好序+右边排好序+merge让整体有序(借助外部数组实现)

//    归并排序(递归版)
public void mergeSort(int[] arr) {
//        临界值判断if (arr == null || arr.length < 2) {
//            企业开发最好抛出异常,避免异常数据流向下游,无法及时监控
//            throw new IllegalArgumentException("数组为空");return;}mergeSort(arr, 0, arr.length - 1);
}private void mergeSort(int[] arr, int l, int r) {if (l >= r) {return;}
//        防止出现越界int m = l + ((r - l) >> 1);
//        通过递归,直线左边和右边都有序mergeSort(arr, l, m);mergeSort(arr, m + 1, r);
//        归并处理mergeSort(arr, l, m, r);
}private void mergeSort(int[] arr, int l, int m, int r) {int[] help = new int[r - l + 1];int i = 0;
//        设置左右两个指针int p1 = l;int p2 = m + 1;while (p1 <= m && p2 <= r) {help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];}
//        要么p1越界,要么p2越界while (p1 <= m) {help[i++] = arr[p1++];}while (p2 <= r) {help[i++] = arr[p2++];}for (int j = 0; j < help.length; j++) {arr[l + j] = help[j];}
}

题目2:归并排序非递归版

非递归的实现核心,在于折腾“步长”这个概念,

  • 从步长 = 1 开始,步长的变化一定是2的某次方
  • 最后一步,如果凑不齐左组就不管了, 右组有多少算多少
  • 步长一旦超过总长度,就说明搞完了
//    归并排序(非递归版)
public void mergeSort(int[] arr) {
//        临界值判断if (arr == null || arr.length < 2) {return;}
//        步长int batch = 1;int size = arr.length;while (batch < size) {int l = 0;while (l < size) {int m = 0;if (size - l >= batch) {m = l + batch - 1;} else {break;}int r = Math.min(size - 1, m + batch);mergeSort(arr, l, m, r);if (r == size - 1) {break;} else {l = r + 1;}}if (batch > size / 2) {break;}batch <<= 1;}
}private void mergeSort(int[] arr, int l, int m, int r) {int[] help = new int[r - l + 1];int i = 0;
//        设置左右两个指针int p1 = l;int p2 = m + 1;while (p1 <= m && p2 <= r) {help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];}
//        要么p1越界,要么p2越界while (p1 <= m) {help[i++] = arr[p1++];}while (p2 <= r) {help[i++] = arr[p2++];}for (int j = 0; j < help.length; j++) {arr[l + j] = help[j];}
}

归并排序拓展

仅仅掌握归并排序的基础实现是不够的,还需要掌握如何利用它的思路去解决新的类似的问题,本文以几个拓展题为例子,带大家拓展一下。

  • 题目1:求数组小和
  • 题目2:求数组中的逆序对数量
  • 题目3:求数组中的大两倍数对数量
  • 题目4:区间和达标的子数组数量

题目1:求数组小和

题目介绍

在一个数组中,一个数左边比它小的数的总和,叫数的小和,所有数的小和累加起来,叫数组小和。求数组小和。 

例子: [1,3,4,2,5]  

1左边比1小的数:没有 

3左边比3小的数:1 

4左边比4小的数:1、3 

2左边比2小的数:1 

5左边比5小的数:1、3、4、 2 

所以数组的小和为1+1+3+1+1+3+4+2=16

实现代码

利用归并排序的特点,在归并排序过程中,不断去找每个数右边有多少个数比它大,那对应数在小和里就应该有多少个。

public int smallSum(int[] arr) {if (arr == null || arr.length < 1) {return 0;}return smallSum(arr, 0, arr.length - 1);
}private int smallSum(int[] arr, int l, int r) {if (l == r) {return 0;}int m = l + ((r - l) >> 1);return smallSum(arr, l, m) + smallSum(arr, l, m) + smallSum(arr, l, m, r);
}private int smallSum(int[] arr, int l, int m, int r) {int[] help = new int[r - l + 1];int i = 0;int p1 = l;int p2 = m + 1;int res = 0;while (p1 <= m && p2 <= r) {if (arr[p1] < arr[p2]) {res += (r - p2 + 1) * arr[p1];help[i++] = arr[p1++];} else {help[i++] = arr[p2++];}}while (p1 <= m) {help[i++] = arr[p1++];}while (p2 <= r) {help[i++] = arr[p2++];}for (int j = 0; j < help.length; j++) {arr[l + j] = help[j];}return res;
}

题目2:求数组中的逆序对数量

题目介绍

在一个数组中,任何一个前面的数a,和任何一个后面的数b,如果(a,b)是降序的,就称为逆序对, 返回数组中所有的逆序对。

实现代码

和上一题思路类似,不过是逆序。需要从右往左Merge,相等的时候先拷贝右边的。

public int reversePairSum(int[] arr) {if (arr == null || arr.length < 1) {return 0;}return reversePairSum(arr, 0, arr.length - 1);
}private int reversePairSum(int[] arr, int l, int r) {if (l == r) {return 0;}int m = l + ((r - l) >> 1);return reversePairSum(arr, l, m) + reversePairSum(arr, l, m) + reversePairSum(arr, l, m, r);
}private int reversePairSum(int[] arr, int l, int m, int r) {int[] help = new int[r - l + 1];int i = 0;
//        逆序处理int p1 = m;int p2 = r;int res = 0;while (p1 >= l && p2 > m) {if (arr[p1] > arr[p2]) {res += p2 - m;help[i--] = arr[p1--];} else {help[i--] = arr[p2--];}}while (p1 >= l) {help[i--] = arr[p1--];}while (p2 <= r) {help[i--] = arr[p2--];}for (int j = 0; j < help.length; j++) {arr[l + j] = help[j];}return res;
}

题目3:求数组中的大两倍数对数量

题目介绍

在一个数组中,对于每个数num,求有多少个后面的数 * 2 依然<num,求总个数

比如:[3,1,7,0,2]  
3的后面有:1,0  
1的后面有:0  
7的后面有:0,2  
0的后面没有  
2的后面没有  
所以总共有5个  

实现代码

也是类似的思想,因为merge的时候,划分出来的[l,m] 和 [m+1,r]块中是能保证有序的,就可以拿左边的依次去和右边比较来实现。

public int biggerTwice(int[] arr) {if (arr == null || arr.length < 1) {return 0;}return biggerTwice(arr, 0, arr.length - 1);
}private int biggerTwice(int[] arr, int l, int r) {if (l == r) {return 0;}int m = l + ((r - l) >> 1);return biggerTwice(arr, l, m) + biggerTwice(arr, l, m) + biggerTwice(arr, l, m, r);
}private int biggerTwice(int[] arr, int l, int m, int r) {
//        [l,m]   [m+1,r]int res = 0;
//        目前囊括进来的数,是从[m+1, windowR)int windowR = m + 1;for (int i = l; i <= m; i++) {while (windowR <= r && arr[i] > (arr[windowR] * 2)) {windowR++;}res += windowR - m - 1;}int[] help = new int[r - l + 1];int i = 0;int p1 = l;int p2 = m + 1;while (p1 <= m && p2 <= r) {help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];}while (p1 <= m) {help[i++] = arr[p1++];}while (p2 <= r) {help[i++] = arr[p2++];}for (int j = 0; j < help.length; j++) {arr[l + j] = help[j];}return res;
}

题目4:区间和达标的子数组数量

题目介绍

327. 区间和的个数 - 力扣(LeetCode)

给定一个数组arr,和两个整数a和b(a<=b),求arr中有多少个子数组,累加和在[a,b]这个范围上,返回达标的子数组数量

实现代码

这个在归并的基础上,还需要借助前缀和数组来实现。

public int countRangeSum(int[] arr,int lower,int upper){if (arr==null || arr.length<1){return 0;}long[] preSum = getPreSum(arr);return countRangeSum(preSum,0,arr.length-1,lower,upper);
}
//    获取前缀和数组
//    为了避免累加后越界,改用long存储
private long[] getPreSum(int[] arr){long[] res = new long[arr.length];res[0]= arr[0];for (int i = 1; i < res.length; i++) {res[i]=arr[i]+res[i-1];}return res;
}
private int countRangeSum(long[] preSum, int l, int r, int lower, int upper) {// 临界情况判断处理if (l==r){return preSum[l]>=lower && preSum[l]<=upper ? 1:0;}int m = l +((r-l)>>1);return countRangeSum(preSum,l,m,lower,upper) + countRangeSum(preSum,m+1,r,lower,upper)+countRangeSum(preSum,l,m,r,lower,upper);
}private int countRangeSum(long[] preSum, int l, int m, int r, int lower, int upper) {int res =0;
//        通过左右两个窗口去获取以当前数结尾,有多少子数组是满足需求的int windowL=l;int windowR=l;
//        [windowL,windowR)for (int i = m+1; i <=r ; i++) {
//            获取以当前数结尾,所需要的前面子数组和应该在什么范围才能满足需求long min = preSum[i]-upper;long max = preSum[i]-lower;while (windowR<=m && preSum[windowR]<=max){windowR++;}while (windowL<=m && preSum[windowL]<min){windowL++;}res += windowR-windowL;}long[] help = new long[r - l + 1];int i = 0;int p1 = l;int p2 = m + 1;while (p1 <= m && p2 <= r) {help[i++] = preSum[p1] <= preSum[p2] ? preSum[p1++] : preSum[p2++];}while (p1 <= m) {help[i++] = preSum[p1++];}while (p2 <= r) {help[i++] = preSum[p2++];}for (int j = 0; j < help.length; j++) {preSum[l + j] = help[j];}return res;
}

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/67842.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《鸿蒙 HarmonyOS 应用开发从入门到精通(第 2 版)》学习笔记 ——HarmonyOS 环境搭建之安装DevEco Studio

作为一款开发工具&#xff0c;除了具有基本的代码开发、编译构建及调测等功能外&#xff0c;DevEco Studio还具有如下特点&#xff1a; 高效智能代码编辑&#xff1a;支持Java、XML、ArkTS、JS、C/C等语言的代码高亮、代码智能补齐、代码错误检查、代码自动跳转、代码格式化、…

电脑办公技巧之如何在 Word 文档中添加文字或图片水印

Microsoft Word是全球最广泛使用的文字处理软件之一&#xff0c;它为用户提供了丰富的编辑功能来美化和保护文档。其中&#xff0c;“水印”是一种特别有用的功能&#xff0c;它可以用于标识文档状态&#xff08;如“草稿”或“机密”&#xff09;、公司标志或是版权信息等。本…

学习记录之原型,原型链

构造函数创建对象 Person和普通函数没有区别&#xff0c;之所以是构造函数在于它是通过new关键字调用的&#xff0c;p就是通过构造函数Person创建的实列对象 function Person(age, name) {this.age age;this.name name;}let p new Person(18, 张三);prototype prototype n…

logback日志自定义占位符

前言 在大型系统运维中&#xff0c;很大程度上是需要依赖日志的。在java大型web工程中&#xff0c;一般都会使用slf4jlogback这一个组合来实现日志的管理。 logback中很多现成的占位符可以可以直接使用&#xff0c;比如线程号【%t】、时间【%d】、日志等级【%p】&#xff0c;…

Android系统开发(八):从麦克风到扬声器,音频HAL框架的奇妙之旅

引言&#xff1a;音浪太强&#xff0c;我稳如老 HAL&#xff01; 如果有一天你的耳机里传来的不是《咱们屯里人》&#xff0c;而是金属碰撞般的杂音&#xff0c;那你可能已经感受到了 Android 音频硬件抽象层 (HAL) 出问题的后果&#xff01;在 Android 音频架构中&#xff0c…

数据恢复常用方法(三)如何辨别固态硬盘故障类型

数据恢复首先需要辨别固态硬盘故障类型&#xff0c;只有先确认故障类型&#xff0c;才能进行下一步动作 如下是一种常见的场景&#xff0c;固态硬盘无法识别&#xff0c;接入电源与数据线&#xff0c;电脑的磁盘管理不显示任何信息。 第一步&#xff1a;确认硬件状态&#xff…

【大数据】机器学习----------强化学习机器学习阶段尾声

一、强化学习的基本概念 注&#xff1a; 圈图与折线图引用知乎博主斜杠青年 1. 任务与奖赏 任务&#xff1a;强化学习的目标是让智能体&#xff08;agent&#xff09;在一个环境&#xff08;environment&#xff09;中采取一系列行动&#xff08;actions&#xff09;以完成一个…

StarRocks 3.4 发布--AI 场景新支点,Lakehouse 能力再升级

自 StarRocks 3.0 起&#xff0c;社区明确了以 Lakehouse 为核心的发展方向。Lakehouse 的价值在于融合数据湖与数据仓库的优势&#xff0c;能有效应对大数据量增长带来的存储成本压力&#xff0c;做到 single source of truth 的同时继续拥有极速的查询性能&#xff0c;同时也…

【技巧】优雅的使用 pnpm+Monorepo 单体仓库构建一个高效、灵活的多项目架构

单体仓库&#xff08;Monorepo&#xff09;搭建指南&#xff1a;从零开始 单体仓库&#xff08;Monorepo&#xff09;是一种将多个相关项目集中管理在一个仓库中的开发模式。它可以帮助开发者共享代码、统一配置&#xff0c;并简化依赖管理。本文将通过实际代码示例&#xff0…

基于python的博客系统设计与实现

摘要&#xff1a;目前&#xff0c;对于信息的获取是十分的重要&#xff0c;我们要做到的不是裹足不前&#xff0c;而是应该主动获取和共享给所有人。博客系统就能够实现信息获取与分享的功能&#xff0c;博主在发表文章后&#xff0c;互联网上的其他用户便可以看到&#xff0c;…

Spring Boot AOP实现动态数据脱敏

依赖&配置 <!-- Spring Boot AOP起步依赖 --> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId> </dependency>/*** Author: 说淑人* Date: 2025/1/18 23:03* Desc…

SparkSQL函数综合实践

文章目录 1. 实战概述2. 实战步骤2.1 创建项目2.2 添加依赖2.3 设置源目录2.4 创建日志属性文件2.5 创建hive配置文件2.6 创建数据分析对象2.6.1 导入相关类2.6.2 创建获取Spark会话方法2.6.3 创建表方法2.6.4 准备数据文件2.6.5 创建加载数据方法2.6.6 创建薪水排行榜方法2.6.…

ElasticSearch DSL查询之排序和分页

一、排序功能 1. 默认排序 在 Elasticsearch 中&#xff0c;默认情况下&#xff0c;查询结果是根据 相关度 评分&#xff08;score&#xff09;进行排序的。我们之前已经了解过&#xff0c;相关度评分是通过 Elasticsearch 根据查询条件与文档内容的匹配程度自动计算得出的。…

《汽车维修技师》是什么级别的期刊?是正规期刊吗?能评职称吗?

​问题解答&#xff1a; 问&#xff1a;《汽车维修技师》是不是核心期刊&#xff1f; 答&#xff1a;不是&#xff0c;是知网收录的正规学术期刊。 问&#xff1a;《汽车维修技师》级别&#xff1f; 答&#xff1a;省级。主管单位&#xff1a;北方联合出版传媒&#xff08;…

【中国电信-安全大脑产品介绍】

座右铭&#xff1a;人生的道路上无论如何选择总会有遗憾的&#xff01; 文章目录 前言一、安全大脑介绍二、中国电信-安全大脑产品分类1.防护版2.审计版 三、安全大脑-部署方案总结 前言 安全占据我们日常生活中首要地位&#xff0c;它时时刻刻提醒着我们出入平安。当然网络安…

洛谷P8837

[传智杯 #3 决赛] 商店 - 洛谷 代码区&#xff1a; #include<stdio.h> #include<stdlib.h> int cmp(const void*a,const void *b){return *(int*)b-*(int*)a; } int main(){int n,m;scanf("%d%d",&n,&m);int w[n];int c[m];for(int i0;i<n;…

多线程杂谈:惊群现象、CAS、安全的单例

引言 本文是一篇杂谈&#xff0c;帮助大家了解多线程可能会出现的面试题。 目录 引言 惊群现象 结合条件变量 CAS原子操作&#xff08;cmp & swap&#xff09; 线程控制&#xff1a;两个线程交替打印奇偶数 智能指针线程安全 单例模式线程安全 最简单的单例&…

三分钟简单了解HTML的一些语句

1.图片建议建立一个文件夹如下图所示 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"keywords"><title>魔神羽落</title><style>.testone{background-color: #ff53e…

HCIP笔记4--OSPF域内路由计算

1. 域内LSA 1.1 一类LSA 一类LSA: 路由器直连状态&#xff0c;Router LSA。 串口需要两端配置好IP,才会产生一类LSA; 以太网口只需要一端配置了IP就会直接产生一类LSA。 LSA通用头部 Type: Router 直连路由LS id: 12.1.1.1 路由器router idAdv rtr: 12.1.1.1 通告的路由器&…

k8s基础(7)—Kubernetes-Secret

Secret概述&#xff1a; Secret 是一种包含少量敏感信息例如密码、令牌或密钥的对象。 这样的信息可能会被放在 Pod 规约中或者镜像中。 使用 Secret 意味着你不需要在应用程序代码中包含机密数据。 由于创建 Secret 可以独立于使用它们的 Pod&#xff0c; 因此在创建、查看和…