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

news2025/1/20 16:30:30

 数据结构与算法引入

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

这点对于我们数据工程师来说,体感更甚——平时工作看起来主要是写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.coloradmin.cn/o/2279443.html

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

相关文章

OpenHarmony 4.1 SDK11 北向应用开发笔记

目录 声明 1、开启其他应用 2、延时切换页面 3、设置页面切换效果 4、设置背景图片和背景铺满屏幕 5、设置隐藏状态和导航条 6、设置组件大小和对齐方式 7、设置按钮类型改变按钮边框圆角半径 8、常用布局方式 9、布局技巧 声明 本笔记基于OpenHarmony 4.1 SDK11&am…

【Linux】进程的程序替换

前言&#xff1a; 在未进行进程的程序替换时&#xff0c;父子进程的数据是独立的通过页表进行映射进行实现进程数据的独立性&#xff0c;但是父子进程的代码还是共享的&#xff0c;我父进程将子进程进行创建出来不仅仅只会有父子进程只进行执行共享代码的需求&#xff0c;有的…

Java测试开发平台搭建(九)前端

1. 搭建前端vue环境 Vue3 安装 | 菜鸟教程 2. 创建项目 1.进入ui vue ui 2. create项目 3. 成功之后添加插件&#xff1a; cli-plugin-router vue-cli-plugin-vuetify 4. 添加依赖 axios 5. 点击任务开始运行 如果报错&#xff1a; 修改vue.config.jsconst { defineConfig }…

Asp .Net Core 实现微服务:集成 Ocelot+Nacos+Swagger+Cors实现网关、服务注册、服务发现

什么是 Ocelot ? Ocelot是一个开源的ASP.NET Core微服务网关&#xff0c;它提供了API网关所需的所有功能&#xff0c;如路由、认证、限流、监控等。 Ocelot是一个简单、灵活且功能强大的API网关&#xff0c;它可以与现有的服务集成&#xff0c;并帮助您保护、监控和扩展您的…

【机器学习实战入门】使用Pandas和OpenCV进行颜色检测

Python 颜色检测项目 今天的项目将非常有趣和令人兴奋。我们将与颜色打交道&#xff0c;并在项目过程中学习许多概念。颜色检测对于识别物体来说是必要的&#xff0c;它也被用作各种图像编辑和绘图应用的工具。 什么是颜色检测&#xff1f; 颜色检测是检测任何颜色名称的过程…

如何实现文本相关的显示功能

文章目录 1 概念介绍2 使用方法3 示例代码 我们在上一章回中介绍了页面之间传递数据相关的内容,本章回中将介绍如何使用Text Widget。闲话休提&#xff0c;让我们一起Talk Flutter吧。 1 概念介绍 我们在这里说的Text Widget就是显示文字内容的组件&#xff0c;其实我们一直在…

精度论文:【Focaler-IoU: More Focused Intersection over Union Loss】

Focaler-IoU: 更聚焦的交并比损失 Focaler-IoU: More Focused Intersection over Union Loss Focaler-IoU: 更聚焦的交并比损失I. 引言II. 相关工作III. 方法IV. 实验V. 结论 原文地址&#xff1a;官方论文地址 代码地址&#xff1a;官方代码地址 摘要——边界框回归在目标检…

计算机组成原理--笔记二

目录 一.计算机系统的工作原理 二.计算机的性能指标 1.存储器的性能指标 2.CPU的性能指标 3.系统整体的性能指标&#xff08;静态&#xff09; 4.系统整体的性能指标&#xff08;动态&#xff09; 三.进制计算 1.任意进制 > 十进制 2.二进制 <> 八、十六进制…

BERT与CNN结合实现糖尿病相关医学问题多分类模型

完整源码项目包获取→点击文章末尾名片&#xff01; 使用HuggingFace开发的Transformers库&#xff0c;使用BERT模型实现中文文本分类&#xff08;二分类或多分类&#xff09; 首先直接利用transformer.models.bert.BertForSequenceClassification()实现文本分类 然后手动实现B…

MySQL - 主从同步

​​​​​​1.主从同步原理&#xff1a; MySQL 主从同步是一种数据库复制技术&#xff0c;它通过将主服务器上的数据更改复制到一个或多个从服务器&#xff0c;实现数据的自动同步。 主从同步的核心原理是将主服务器上的二进制日志复制到从服务器&#xff0c;并在从服务器上执…

[Python学习日记-78] 基于 TCP 的 socket 开发项目 —— 模拟 SSH 远程执行命令

[Python学习日记-78] 基于 TCP 的 socket 开发项目 —— 模拟 SSH 远程执行命令 简介 项目分析 如何执行系统命令并拿到结果 代码实现 简介 在Python学习日记-77中我们介绍了 socket 基于 TCP 和基于 UDP 的套接字&#xff0c;还实现了服务器端和客户端的通信&#xff0c;本…

STM32Cubemx配置RS485通信

文章目录 一、RS485协议概念讲解RS485 协议概念1. **差分信号传输**2. **半双工通信**3. **多点通信**4. **最大通信距离和速度**5. **终端电阻与偏置电阻**6. **RS485 接口的工作模式**7. **RS485 协议的数据帧结构**8. **RS485 的优点与应用** 总结 二、TTL电平和RS485的关系…

STM32使用DSP库 Keil方式添加

文章目录 前言一、添加DSP库二、使能FPU及配置1. 使能FPU2. 增加编译的宏3.增加头文件的检索路径三. 验证1. 源码中添加2.代码测试前言 添加DSP有两种方案,本文采用的是是Keil 中添加。 一、添加DSP库 在创建好的工程中添加DSP库:步骤如下: 步骤1:选择运行环境管理; 步…

Kotlin Bytedeco OpenCV 图像图像54 透视变换 图像矫正

Kotlin Bytedeco OpenCV 图像图像54 透视变换 图像矫正 1 添加依赖2 测试代码3 测试结果 在OpenCV中&#xff0c;仿射变换&#xff08;Affine Transformation&#xff09;和透视变换&#xff08;Perspective Transformation&#xff09;是两种常用的图像几何变换方法。 变换方…

【Flink系列】10. Flink SQL

10. Flink SQL Table API和SQL是最上层的API&#xff0c;在Flink中这两种API被集成在一起&#xff0c;SQL执行的对象也是Flink中的表&#xff08;Table&#xff09;&#xff0c;所以我们一般会认为它们是一体的。Flink是批流统一的处理框架&#xff0c;无论是批处理&#xff08…

【STM32-学习笔记-11-】RTC实时时钟

文章目录 RTC实时时钟一、RTC简介二、RTC框图三、RTC基本结构四、RTC操作注意事项五、RTC函数六、配置RTCMyRTC.c 七、示例&#xff1a;实时时钟①、main.c②、MyRTC.c③、MyRTC.h RTC实时时钟 一、RTC简介 RTC&#xff08;Real Time Clock&#xff09;实时时钟 RTC是一个独立…

Spring的IoC、Bean、DI的简单实现,难度:※※※

目录 场景描述 第一步&#xff1a;初始化Maven项目 第二步&#xff1a;Maven导入Spring包&#xff08;给代码&#xff09; 第三步&#xff1a;创建Spring配置文件 第四步 创建Bean 第五步 简单使用Bean &#xff08;有代码&#xff09; 第六步 通过依赖注入使用Bean&…

Tensor 基本操作1 | PyTorch 深度学习实战

目录 创建 Tensor常用操作unsqueezesqueezeSoftmax代码1代码2代码3 argmaxitem 创建 Tensor 使用 Torch 接口创建 Tensor import torch参考&#xff1a;https://pytorch.org/tutorials/beginner/basics/tensorqs_tutorial.html 常用操作 unsqueeze 将多维数组解套&#xf…

自然语言处理——自注意力机制

一、文字表示方法 在自然语言处理中&#xff0c;如何用数据表示文字是基础问题。独热编码&#xff08;One-hot Encoding &#xff09;是一种简单的方法&#xff0c;例如对于 “我”“你”“他”“猫”“狗” 等字&#xff0c;会将其编码为如 “我 [1 0 0 0 0 ……]”“你 [0 …

嵌入式硬件篇---PID控制

文章目录 前言第一部分&#xff1a;连续PID1.比例&#xff08;Proportional&#xff0c;P&#xff09;控制2.积分&#xff08;Integral&#xff0c;I&#xff09;控制3.微分&#xff08;Derivative&#xff0c;D&#xff09;控制4.PID的工作原理5..实质6.分析7.各种PID控制器P控…