2022届中国农业银行研发中心面经
2022年9月3日
今天出了2023届校招的公告,可以点这个下面这个公众号消息查看https://mp.weixin.qq.com/s/QB1Wr0sT3RoWMWbaiiNS7A
1.投递10月14日后截止。官网:https://career.abchina.com
需要填写简历,用电脑打开比较方便。
2.base及招聘岗位:北京、广州、上海、天津、成都、西安、武汉、雄安。
雄安只招软件研发岗,产品只有广州招,大数据分析只有西安招,财务只有上海招。
智领计划岗只有北上招,适合竞赛和科研大佬,待遇可能高于其他岗。
人力资源除了雄安都招,测试除了天津雄安都招,研发八个base都招。
3.英语要求:六级425/托业715/托福85/雅思6.5
4.具体岗位对专业的要求可看上面的公众号或者投递官网,招聘的专业有:
(1)计算机相关专业:智领计划岗(硕)、测试(本)、研发(本)、大数据分析岗(硕)
(2)经济、金融、财会专业(硕士及以上):产品研发岗,base广州。
(3)会计、财务管理、审计专业(硕士及以上且本硕都是):财务管理岗,base仅上海。比较推荐,因为和研发待遇相同。
(4)人力资源相关专业(硕士及以上且本硕都是):人力资源岗,比较推荐,因为和研发待遇相同。
(5)信息统计和数理统计相关专业(硕士及以上):大数据分析岗,base西安
5.可以找我内推,但应该作用不大,不能保证简历过,而且大家都有内推。
需要的可以私信我。内推流程是自行在官网投递完毕,然后将姓名、简历中的人员编号和投递的base岗位发给我,我在工作日的时候录入内网的系统。
分割线======================================分割线
2022.05
本人211渣硕,22届秋招offer,base武汉。
即将告别校园,将笔记分享给有缘人,祝大家都能拿到理想的offer。(PS:勿问薪水)
牛客上的markdown貌似无法实现目录跳转。
招聘方式
农行每届有一次暑期实习转正,一次秋招和一次春招。
(1)实习报名大概在五月份,笔试+面试,实习时间不到两个月,实习结束后部分转正。
(2)秋招大概8月开始报名,笔试分两批,大概都在9月举行。整个流程是笔试+面试+体检。春招本人不清楚。
秋招考试内容
(1)笔试分为80道单选题和3道编程题,单选60分钟,编程90分钟。
数据库SQL题 15道
数据库基础 10道
数据结构 15道
计算机网络 10道
C和Java 15道
软件测试 5道
行测 10道
编程题只会显示代码是否运行成功,无法知道是否通过以及通过多少,可多次提交,以最后一次提交为准。
(2)秋招面试有线上和线下两种,本人参加过线上面试,包括自我介绍和问答,只有10分钟,根据简历问了三个问题。线下面试形式不同,可能会有算法题,需要描述思路。
笔记
深拷贝和浅拷贝
java的数据类型有基本类型和引用类型。
浅拷贝就是拷贝一份对象的引用,对新对象的修改,会改变原来的对象。因为两者指向的是同一个内存地址。
深拷贝就是创建一个新的对象,并复制其内容。新的对象会使用不同的内存地址,所以对新的对象的修改不会影响原来的对象。
Object中本地clone()方法,默认是浅拷贝。实现深拷贝有两种方式:
(1)实现Cloneable接口,重写clone方法。Cloneable仅仅是标识作用,如果没有实现这个接口,调用clone方***抛出CloneNotSupportedException异常。
(2)通过序列化实现深拷贝。
层次调用clone方法可以实现深拷贝,但代码量实在太大,每个类都要重写clone方法太过繁琐。使用序列化实现深拷贝更好。
注意java中可以通过引用去改变对象的状态,但因为是值传递,所以无法修改对象的地址,因此无法指向另一个对象。
三元表达式
用来完成简单的选择逻辑:根据条件判断,从两个选择中选择一种执行。
条件表达式 ? 表达式A : 表达式B; 条件表达式为true,就执行表达式A,否则执行表达式B。
lambda表达式
在数学中,函数就是有输入量和输出流的一套计算方案。
面对对象的思想是:找一个解决这个事情的对象,调用对象的方法。
相对来说,面向对象过分强调“必须通过对象的形式来做事情”。函数式思想就是尽量忽略面向对象的复杂语法,强调做什么,而不是以什么形式做。重视的是结果,不重视过程。
例子
以通过实现Runnable接口来实现多线程为例,Thread类需要以Runnable接口作为参数,为了指定run的方法体,需要Runnable接口的实现类或者使用匿名内部类。
但是这两种方式都需要覆盖重写抽象方法run。只是为了将run方法体内的代码传递给Thread类,才不得不创建一个对象。而实际上只有方法体是关键的,传递一段代码才是我们的目的。
使用lambda表达式可以简化匿名内部类的书写。同样的语义在lambda表达式中,要更加简单。
lambda表达式的标准格式
(参数列表) -> {代码语句} 圆括号内是方法的参数,参数类型可以省略。 中间的箭头表示将前面的参数传递给后面的代码 大括号内是方法体,如果只有一条语句,可以把大括号,return,和语句的分号一起省略。
public class DemoLambda { public static void main(String[] args) { //匿名内部类的方式,实现多线程 new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }).start(); //lambda表达式,实现多线程 new Thread(() -> { System.out.println(Thread.currentThread().getName()); }).start(); //lambda表达式,实现多线程 new Thread(() -> System.out.println(Thread.currentThread().getName())).start(); } }
lambda的使用前提
使用lambda必须实现了接口,并且接口中有且仅有一个抽象方法。(有且仅有一个抽象方法的接口叫函数式接口)
死锁的条件_如何避免死锁
死锁:多个线程循环等待资源,线程被无限期地阻塞,程序无法正常终止。
原因:资源的竞争,资源申请的顺序不合理。
死锁的四个必要条件
1. 互斥条件:该资源任意一个时刻只由一个线程占用。 2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
如何预防线程死锁
1. 破坏互斥条件 :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界 资源需要互斥访问)。 2. 破坏请求与保持条件 :一次性申请所有的资源。 3. 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释 放它占有的资源。 4. 破坏循环等待条件 :靠按序申请资源来预防。进程在申请资源时必须按照序号递增的顺序进行资源的申请,释放资源则反序释放。 破坏循环等待条件。
银行家算法:银行家算法是一种死锁避免算法,该算法允许进程动态申请资源。系统毎次在进行资源分配之前,先计算此次分配资源的安全性,若此次资源分不会导致系统进入不安全状态,则分配资源;
否则,不分配资源,让进程等待。银行家算法在避免死锁上非常有效,但是需要在进程运行前就知道其所需资源的最大值,且进程数也通常不是固定的。因此很难实现。
已分配给进程的资源:Allocation
进程总共需要的资源数:Claim
进程还需要的资源数:Need
资源池(空闲资源数):Available
银行家算法分配系统资源的原则
(1)当一个进程对资源的最大需求量不超过系统中的资源数时可以接纳该进程。
(2)进程可以分期请求资源,但请求的总数不能超过最大需求量。
(3)当系统空闲的资源不能满足进程尚需资源数时,对进程的请求可以推迟分配,但总能使进程在有限的时间里得到资源。
(4)当系统空闲的资源能满足进程尚需资源数时,必须测试系统现存的资源能否满足该进程尚需的最大资源数,若能满足则按当前的申请量分配资源,否则也要推迟分配。
如何优化数据库查询
(1)不使用select*,只取需要的字段,节约资源。select*很可能不会使用覆盖索引。
(2)若已知查询结果只有一条,使用limit1,这样找到记录后就不用扫描剩余的记录了。
(3)like语句,%不要放在最前面。把%放前面,并不走索引。
因为分析器会先预估走索引和不走索引的行扫描数,当数据量大时,分析器会认为不走索引比走索引的效率高。%匹配任意个字符,下划线匹配一个字符。
(4)如果插入数据过多,考虑批量插入。
(5)如果数据量较大,尽量避免同时修改或删除过多数据。
(6)exist & in 的合理利用
in先做子查询,然后将子查询作为主查询的条件。
exists先做主查询,然后将子查询作为条件,来决定主查询的结果是否保留。
要选择最外层循环小的用法,这取决于两个表的大小。
(7)尽可能使用varchar/nvarchar代替char/nchar。(nvarchar是unicode编码)
变长字段存储空间小,可以节省存储空间。对于查询来说,在一个相对较小的字段内搜索,效率更高。
(8)慎用distinct关键字
在查询一个字段或者很少字段时,distinct给查询带来优化效果。但是在字段很多的时候使用,却会大大降低查询效率。
(9)满足SQL需求的前提下,推荐优先使用Inner join(内连接),如果要使用left join,左边表数据结果尽量小,如果有条件尽量放到左边处理。
(10)优化where子句
尽量避免用or连接。可以用union all代替。
尽量避免在where子句中使用!=或<>操作符
尽量避免在where子句中对字段进行表达式操作
where子句中考虑使用默认值代替null
(11)索引
使用联合索引时,注意索引列的顺序,一般遵循最左匹配原则。
考虑在where和order by涉及的列上建立索引,尽量避免全表扫描。
删除冗余和重复索引
索引不适合建在有大量重复数据的字段上,如性别这类型数据库字段:如果索引列有大量重复数据,Mysql查询优化器推算发现不走索引的成本更低,很可能就放弃索引了。
sql
union:对两个结果集进行并集操作,不包括重复行,同时进行默认规则的排序;
union all:对两个结果集进行并集操作,包括重复行,不进行排序;
intersect:对两个结果集进行交集操作,不包括重复行,同时进行默认规则的排序;
minus:对两个结果集进行差操作,不包括重复行,同时进行默认规则的排序。
inner join:内连接,在两张表进行连接查询时,只保留两张表中完全匹配的结果集。
left join 在两张表进行连接查询时,会返回左表所有的行,即使在右表中没有匹配的记录。
right join 在两张表进行连接查询时,会返回右表所有的行,即使在左表中没有匹配的记录。
in:先执行子查询,然后将子查询的结果作为条件做主查询
exist:先执行主查询,然后将子查询作为条件,来决定主查询的结果是否保留。
<=>:两个值进行比较,不等于结果是0,等于结果是1。<=>可以比较null值,而=不能比较null值。
进程和线程的区别
进程是程序的一次执行过程,是系统运行程序的基本单位。线程比进程更小,是执行的基本单位。一个进程在其执行的过程中可以产生多个线程。
同一个进程里的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈。所以系统在产生一个线程,或是在各个线程之间切换时,负担要比进程小得多。
线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
什么是操作系统
(1)操作系统也是运行在计算机上的软件程序,用来管理计算机软件和硬件资源。
(2)操作系统屏蔽了硬件层的复杂性,所有的用户程序都是通过操作系统来调用系统内存和硬盘等硬件。
(3)操作系统的内核(Kernel)是操作系统的核心部分,它负责系统的内存管理,硬件设备的管理,文件系统的管理以及应用程序的管理。内核是连接应用程序和硬件的桥梁,决定着系统的性能和稳定性。
原码反码补码
正数的原码反码和补码相同。
负数的反码等于原码除符号位外各位都取反。负数的补码等于反码加1。
在计算机中负数用补码表示。
为什么负数用补码表示?
总结:补码的存在解决了0的符号问题,同时统一了计算机的加减法运算。
(1)原码是方便人看的,计算机做减法需要借位,实现很不方便。
(2)如果把原码相减,变成反码相加,计算机实现就很容易。但是+0和-0的反码不一样,遇到0时是用+0还是-0计算,就会产生分歧。
(3)如果使用补码,+0和-0的补码是一样的,都为全0。而1000 0000就用来表示-128的补码,因为-127的补码和-1的补码相加,和为1000 0000。
Python中单引号_双引号和三引号的区别
单引号和双引号都表示字符串,没有区别。字符串中有单引号时,用双引号表示字符串,单引号不用加转义字符。反之也是。
三引号允许字符串跨越多行,三引号还可以注释代码。
python 多线程
python有GIL(全局解释器锁)的限制,虽然Python解释器可以运行多个线程,但只有一个线程在解释器中运行。Python多线程相当于单核多线程。
如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。但python可以通过多进程实现多核任务,多个Python进程有各自独立的GIL锁,互不影响。但是进程间通信相对比较麻烦,需要使用IPC机制(管道、套接字等)。
当python调用C扩展的时候,可以在C扩展中把GIL释放掉,从而达到使用多线程并行的目的。
GIL会在I/O调用(会调用内建的操作系统C代码)之前被释放,以允许其他线程在这个线程等待I/O的时候运行。I/O密集型的Python程序比计算密集型的Python程序更能充分利用多线程的好处。
我们可以把一些计算密集型任务用C语言编写,然后把.so链接库内容加载到Python中,因为执行C代码,GIL锁会释放,这样一来,就可以做到每个核都跑一个线程的目的。
Python是如何实现内存管理的
python的内存管理由解释器负责。自动内存管理减轻了程序员的工作负担,也在一定程度上解决了内存泄露问题。
CPython解释器的内存管理有三个关键点:引用计数、标记清除、分代收集。
python如何拷贝一个对象
浅拷贝通常只复制对象本身,而深拷贝不仅会复制对象,还会递归的复制对象所关联的对象。
copy.copy()浅拷贝,copy.deepcopy()深拷贝。deepcopy函数的本质就是对象的一次序列化和一次反回序列化。还以使用copyreg.pickle模块的dumps和loads方法来自定义指定类型对象的拷贝行为。
深拷贝可能会遇到两个问题:一是一个对象如果直接或间接的引用了自身,会导致无休止的递归拷贝;二是深拷贝可能对多个对象共享的数据也进行拷贝。
deepcopy可以通过memo字典来保存已经拷贝过的对象,从而避免刚才所说的自引用递归问题。
动态语言和静态语言区别
动态语言是运行时才确定数据类型的语言,变量在使用之前无需申明类型。通常变量的值是被赋值的那个值的类型。
静态语言是编译时变量的数据类型就可以确定的语言,大多数静态语言要求在使用变量之前必须申明数据类型。
python迭代器和生成器
迭代器是一个可以记住遍历位置的对象,从集合的第一个元素开始访问,直到所有元素访问结束。迭代器只能从前往后遍历对象,不能回退。迭代器对象可以用for语句或while语句来遍历。
使用了yield的函数被称为生成器,它会返回一个迭代器的函数,是一种特殊的迭代器。在调用生成器运行的过程中,每次遇到yield时函数会暂停并保存当前运行的信息,返回yield的值,等待被重新唤醒后从当前位置继续运行。
区别:生成器是生成元素的,迭代器是访问集合元素的一中方式。迭代输出生成器的内容。生成器可以做迭代器可以做的事,但迭代器不能生成元素。
Python中为什么没有函数重载
Python是动态类型语言,函数的参数没有类型约束,也就无法根据参数类型来区分重载方法。但Python中函数的参数可以有默认值,可以使用关键字参数和可变参数,因此虽然没有函数重载,也可以让一个函数根据调用者传入的参数产生不同的行为。
python中is和==的区别
python中is比较的是对象的地址,==比较的是对象的内容。
不使用中间变量_交换两个变量的值
a = a ^ b b = a ^ b a = a ^ b
如何解决过拟合
过拟合是指模型对于训练数据拟合过当,模型在训练集上的表现很好,但在测试集和新数据上的表现较差。
(1)获得更多的训练数据,因为更多的样本能够让模型学习到更多更有效的特征,减小噪声的影响。使用更多的训练数据是解决过拟合问题最有效的手段。
可以通过一定的规则来扩充训练数据。比如,在图像分类的问题上,可以通过图像的平移、旋转、缩放等方式扩充数据;还可以使用生成式对抗网络来合成大量的新训练数据。
(2)降低模型复杂度。在数据较少时,模型过于复杂是产生过拟合的主要因素,适当降低模型复杂度可以避免模型拟合过多的采样噪声。
(3)降低数据集中样本的维度。
(4)正则化方法,给模型的参数加上一定的正则约束,比如将权值的大小加入到损失函数中。L2正则化是原来的损失函数加上权重参数的平方和。
正则化的目的是限制参数过多或者过大,避免模型更加复杂。
(5)集成学习方法。集成学习是把多个模型集成在一起,来降低单一模型的过拟合风险,如Bagging方法。
bagging,该方法通常考虑的是同质弱学习器,相互独立地并行学习这些弱学习器,并按照某种确定性的平均过程将它们组合起来。
boosting,该方法通常考虑的也是同质弱学习器。它以一种高度自适应的方法顺序地学习这些弱学习器(每个基础模型都依赖于前面的模型),并按照某种确定性的策略将它们组合起来。
监督学习和无监督学习
(1)监督学习:利用有标签数据集生成一个模型。
knn:首先求出一个样本在特征空间中的k个最相似的样本,如果k个样本大多数属于某一个类别,则该样本也属于这一类别。KNN需要将训练数据保存在内存中,它是一个非参数化的算法。
决策树:决策树是一个可用于决策的非循环图。每个节点都有一个阈值,特征值小于阈值时选择左分支,否则选择右分支。当决策树到达叶节点时,可以用该叶节点的标签决定一个样本标签。
朴素贝叶斯:朴素贝叶斯分类是贝叶斯分类中最简单,是常见的一种分类方法
(2)无监督学习:只需要包含无标签样本的数据集。
k-means:(1)选择初始化的k个样本作为初始聚类中心。(2)对于每个样本xi,计算它到k个聚类中心的距离,并将其分到距离最短的中心所在的类。 (3)对每个类别,重新计算聚类中心。 (4)重复23步,直到达到终止条件(迭代次数、最小误差变化等)。
谱聚类:把所有的数据看做空间中的点,这些点之间用边连起来,距离较远的边权值低,距离较近的边权值大。对所有数据点组成的图进行切图,让切图后不同的子图间边权重和尽可能的低,而子图内的边权重和尽可能的高,从而达到聚类的目的。
PCA:是一种统计方法。通过正交变换将一组可能存在相关性的变量转换为一组线性不相关的变量,转换后的这组变量叫主成分。PCA常用于高维数据的降维。
贝叶斯公式
贝叶斯定理会根据一件事发生的先验知识告诉你它的后验概率。数学上它表示为:一个条件样本发生的真正率占真正率和假正率之和的比例。
(1)结果B发生的情况下原因A发生的概率为:A发生的条件下B发生的概率,乘以A发生的概率,除以B发生的概率。
(2)特征为B时,类别是A的概率 = 类别是A时,特征为B的概率,乘以类别为A的概率,除以类别为B的概率。
判断链表是否有环
(1)可以使用快慢指针来做。快慢指针的初始值均设为head,每次移动慢指针一步,移动快指针两步。循环的条件为快指针不为null且快指针的next不为null。 每次移动后判断快指针和慢指针是否相等,若相等且不为null,则说明有环,返回true。若循环结束,说明没有环,返回false。
也可以在快指针和慢指针相等时break跳出循环,然后判断快指针和快指针的next是不是都不为null,若是说明有环,返回true;否则说明无环,返回false。
(2)如果要找入环点:在快慢指针相遇后,将快指针变成慢指针指向head,两个指针一起移动,每次都移动一步。两个指针相遇的点就是入环点。
如何找一个列表中重复的元素
如果只要找一个重复的元素,可以利用Set来做。遍历列表,并把元素加入集合,若集合中元素已经存在,则add方***返回false。因此当add方法返回false时,说明当前元素是重复元素,返回当前元素即可。
public int findDupicateInArray(int[] a) { Set<Integer> set = new HashSet<>(); for(int i=0;i<a.length;i++){ if(!set.add(a[i])){ return a[i]; } } return -1; }
找到所有的重复元素。
(1)可以用集合,此时需要再用一个集合存放重复元素。
(2)可以用双层循环。
从第一个元素开始,统计该元素在后面出现的次数。若出现的次数为1,则该元素为重复元素,将其加入结果。即使有有某个元素出现了多次,也只会加入结果一次。因为只有倒数第二次出现的重复元素,后面出现的次数会是1。而只有出现次数为1时才会加入结果。
public class FindDupicate { public static int[] findDupicateInArray(int[] a) { int count = 0; int[] res = new int[a.length]; int k = 0; for (int i = 0; i < a.length; i++) { for (int j=i+1; j < a.length; j++) { if(a[i] == a[j]){ count++; } } if(count == 1) res[k++] = a[i]; count = 0; } return Arrays.copyOf(res, k); } public static void main(String[] args) { int[] my_array = {1, 2, 5, 5, 5, 6, 6, 7, 2, 9, 2}; int[] res = findDupicateInArray(my_array); for (int i = 0; i < res.length; i++) { System.out.print(res[i]+" "); } } }
(3)可以用哈希表存储每个元素出现的次数
编程题
字符串排序
每个字符串至少含有一个数字字符,按如下规则降序排序:
(1)按照字符串中字母字符个数和数字字符个数(字母数字比)的比值大小进行排序。
(2)若两个字符串的字母数字比相同。则按照字符串本身大小进行排序。
例子
输入值:["abc123","abc+1234","abcabab--1"] 返回值:["abcabab--1","abc123","abc+1234"]
方法
输入是字符串数组,将其转为ArrayList集合,然后调用Collections的sort方法,传入自定义的比较器进行排序。
public class SpecifySort1 { public String[] specify_sort(String[] words){ ArrayList<String> list = new ArrayList<>(); for(int i = 0;i < words.length;i++){ list.add(words[i]); } Collections.sort(list, (x, y) ->{ double a = count(x); double b = count(y); if(a == b){ return y.compareTo(x); } if(a > b){ return -1; } return 1; }); String[] res = new String[list.size()]; for(int i=0;i<res.length;i++){ res[i] = list.get(i); } return res; } //测试 public static void main(String[] args) { String[] words = new String[3]; words[0] = "abc123"; words[1] = "abc+1234"; words[2] = "ababab--1//"; SpecifySort1 specifySort = new SpecifySort1(); String[] res = specifySort.specify_sort(words); for(String str : res){ System.out.println(str); } } //求字母数字比 private double count(String str){ char[] chars = str.toCharArray(); int alpha = 0; int digit = 0; for(char ch : chars){ if(isAlpha(ch)){ alpha++; }else if(isDigit(ch)){ digit++; } } return 1.0*alpha/digit; } private boolean isAlpha(char ch){ return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'); } private boolean isDigit(char ch){ return (ch >= '0' && ch <= '9'); } }
拆分成多个连续自然数相加
给定两个正整数:num和n,判断是否能把num分解成n个连续自然数相加的和。如果能分解,返回分解得到的n个自然数构成的列表(列表中的元素按升序排序)。如果不能分解,则返回一个空列表。
例子
输入:num = 18, n = 4; 返回值:[3,4,5,6]
方法一
等差数列的公式可知sum = n*a1 + n(n-1)/2;
已知sum和n,可以求出a1,若a1为整数则能分解;若a1不是整数则不能分解。
public Integer[] getSeq(int num, int n){ int temp = num - n*(n-1)/2; if(temp%n == 0){ int a = temp/n; Integer[] res = new Integer[n]; for(int i=0;i<n;i++){ res[i] = a + i; } return res; } return new Integer[0]; }
三进制字符串加法
你不能使用任何內建BigInteger库, 也不能直接将输入的字符串转换为整数形式。012分别换成@$&
方法
只需要对两个大整数模拟「竖式加法」的过程:将相同数位对齐,从低到高逐位相加。如果当前位和超过k,则向高位进以为,除k的余数为当前位,商为进位。
注意循环结束的条件,若i和j都小于0,但sum不为0时还要循环一次,最高位为sum,然后sum变为0,循环结束。
可以用哈希表把012分别映射成@$&。
public class TernaryStringAdd { public static String addStrings(String num1, String num2) { int n1 = num1.length(); int n2 = num2.length(); int i = n1 - 1; int j = n2 - 1; int sum = 0; StringBuilder sb = new StringBuilder(); Map<Integer, Character> map = new HashMap<>(){ { put(0, '@'); put(1, '$'); put(2, '&'); } }; while(i >= 0 || j >= 0 || sum != 0){ if(i>=0){ sum = sum + num1.charAt(i--) - '0'; } if(j>=0){ sum = sum + num2.charAt(j--) - '0'; } //sb.append(String.valueOf(sum%3));//sum%k sb.append(map.get(sum%3)); sum = sum/3; } sb.reverse();//加入StringBuilder时,低位在前,所以返回前先翻转,让高位在前。 return sb.toString(); } public static void main(String[] args) { String str1 = "0120210"; String str2 = "0120"; System.out.println(TernaryStringAdd.addStrings(str1, str2)); } }
无符号十进制转三进制
整数除n取余倒序排列,商作为下一次循环的整数,直到整数为0,结束循环。这种方法只能用于无符号整数,十进制转任何进制都可以这样做。
public static String TenToOther(int num){ int base = 3; StringBuilder sb = new StringBuilder(); while(num != 0){ sb.append(num%base); num = num/base; } sb.reverse();//注意低位在最左边,所以返回结果前,要翻转让高位在最左边。 return sb.toString(); }
有符号十进制转十六进制
给定一个整数,编写一个算法将这个数转换为十六进制数。对于负整数,我们通常使用补码运算方法。
注意:
十六进制中所有字母(a-f)都必须是小写。 十六进制字符串中不能包含多余的前导零。如果要转化的数为0,那么以单个字符'0'来表示;对于其他情况,十六进制字符串中的第一个字符将不会是0字符。 给定的数确保在32位有符号整数范围内。 不能使用任何由库提供的将数字直接转换或格式化为十六进制的方法。
示例
输入: -1 输出: "ffffffff"
方法
从小数点往左开始,四位二进制表示一位十六进制。
转换的方法就是从右往左开始每4位转成1位十六进制,从右往左不会出现前导0。但是从低位开始,所以最后需要翻转,然后返回结果。
将最低4位转为1位十六进制的方法是:将整数与0xf按位与。对num进行无符号右移4位来实现每4位处理。
class Solution { public String toHex(int num) { if(num == 0) return "0"; StringBuilder sb = new StringBuilder(); char[] hex = "0123456789abcdef".toCharArray(); while(num != 0){ int temp = num & 0xf; sb.append(hex[temp]); num >>>= 4; } return sb.reverse().toString(); } }
扑克牌排序
根据花色King,红桃,黑桃,梅花,方块排序,花色相同的再根据1到13数字排序。输入为12张牌(k2,s1,h4,s10...) 。
方法
字符串的第一个字符代表花色,剩余的字符代表数字。
k代表王,h,s,c,d分别代表红桃,黑桃,梅花,方块四种花色。
可以用哈希表,将代表花色的字母字符映射成整形数字,用数字来比较花色的大小。
Map<Character, Integer> map = new HashMap<>(){ { put('k', 4);//king put('h', 3);//红桃 put('s', 2);//黑桃 put('c', 1);//梅花 put('d', 0);//方块 } };
可以将输入的字符串数组存到ArrayList中,然后调用Collections.sort方法,传入自定义的比较器,按照先花色后数字大小的规则进行排序。
public class PokeSort { static Map<Character, Integer> map = new HashMap<>(){ { put('k', 4);//king put('h', 3);//红桃 put('s', 2);//黑桃 put('c', 1);//梅花 put('d', 0);//方块 } }; public String[] pokeSort(String[] strs){ ArrayList<String> list = new ArrayList<>(); for(String str : strs){ list.add(str); } Collections.sort(list, new Comparator<String>() { @Override public int compare(String a, String b) { char a1 = a.charAt(0);//获取花色,也就是第一个字符 a = a.substring(1);//去掉第一个字符,剩余的字符表示数字,用Integer.valueOf把字符串转换成数字。 int a2 = Integer.valueOf(a); char b1 = b.charAt(0);//获取花色,也就是第一个字符 b = b.substring(1);//去掉第一个字符,剩余的字符表示数字,用Integer.valueOf把字符串转换成数字。 int b2 = Integer.valueOf(b); if(a1 == b1){ return a2 - b2;//两张牌的花色相同时,比较数字的大小 }else{ return map.get(a1) - map.get(b1);/两张牌的花色不相同,利用哈希表比较花色的大小 } } }); String[] res = new String[list.size()]; for(int i=0;i<res.length;i++){ res[i] = list.get(i); } return res; } //测试 public static void main(String[] args) { //利用Radom随机生成数据 String[] data = new String[20]; Random random = new Random(); char[] help = new char[5]; help[0] = 'd'; help[1] = 'c'; help[2] = 's'; help[3] = 'h'; help[4] = 'k'; for(int i=0;i<data.length;i++){ int p = random.nextInt(5); int q = 1 + random.nextInt(13); String a = help[p] + String.valueOf(q); data[i] = a; System.out.print(data[i] + " "); } System.out.println(); System.out.println("==============================="); //调用排序发放,打印检查结果是否正确 PokeSort psort = new PokeSort(); String[] res = psort.pokeSort(data); for (int i = 0; i < res.length; i++) { System.out.print(res[i] +" "); } } }
距离平均值最近的三个数字
编写程序计算10个正整数的平均值,并输出距离平均值最近的三个数字(与平均值距离小的先输出)
方法
求平均值和每个元素与平均值的距离很容易。原数组为nums,存储元素与平均值的距离的数组为dis。
关键是求出dis数组中最小的三个元素的下标a,b,c。要返回的数组就是nums[a],nums[b],nums[c]。
如果用排序,比如选择排序来做,只需要做三趟。但是每趟都需要交换一次元素,并且dis和nums数组中的元素都要做交换。
若不想改变nums数组的值,只是想得到dis中最小的三个元素的下标,可以不用排序做。分别定义三个变量first,second,third,同时定义三个变量a,b,c记录它们的下标。
遍历一次dis数组,每次循环做以下处理,即可求出距离平均值最近的三个数字在数组中的下标:
当前元素cur<first时,原来第二小的数变成第三小的数,原来第一小的数变成第二小的数,cur变成了第一小的数。同时相应地更新下标。 当first<cur<second时,原来第二小的数变成第三小的数,cur变成了第二小的数。同时相应地更新下标。 当cur<third时,cur变成第三小的数,同时相应地更新下标。
代码
public int[] find(int[] nums){ int sum = 0; int n = nums.length; for(int num : nums){ sum += num; } int avg = sum/n;//avg为平均值 //求出每个元素和平均值的距离 int[] dis = new int[n]; for (int i = 0; i < n; i++) { dis[i] = Math.abs(nums[i] - avg); } int[] res = new int[3];//结果数组 //求距离平均值最小的三个元素的下标 int first = Integer.MAX_VALUE;//dis中最小的元素 int second = Integer.MAX_VALUE;//dis中第二小的元素 int third = Integer.MAX_VALUE;//dis中第三小的元素 int a = -1, b = -1, c = -1;//分别为第一,第二,第三小的元素在数组中的下标 for(int i=0;i<n;i++){ int cur = dis[i]; if(cur < first){ third = second; second = first; first = cur; c = b; b = a; a = i; }else if(cur < second){ third = second; second = cur; c = b; b = i; }else if(cur < third){ third = cur; c = i; } } res[0] = nums[a]; res[1] = nums[b]; res[2] = nums[c]; return res; }
计算中缀表达式只包含加减
方法
括号展开 + 栈
由于字符串除了数字与括号外,只有加号和减号两种运算符。如果展开表达式中所有的括号,则得到的新表达式中,数字本身不会发生变化,只是每个数字前面的符号会发生变化。
因此,我们考虑使用一个取值为 {−1,+1}的整数sign代表当前的符号。根据括号表达式的性质,它的取值:
与字符串中当前位置的运算符有关; 如果当前位置处于一系列括号之内,则也与这些括号前面的运算符有关:每当遇到一个以 −-− 号开头的括号,则意味着此后的符号都要被「翻转」。
考虑到第二点,我们需要维护一个栈stack,其中栈顶元素记录了当前位置所处的每个括号所「共同形成」的符号。例如,对于字符串1+2+(3-(4+5)):
扫描到 1+2 时,由于当前位置没有被任何括号所包含,则栈顶元素为初始值+1; 扫描到 1+2+(3 时,当前位置被一个括号所包含,该括号前面的符号为+号,因此栈顶元素依然+1; 扫描到 1+2+(3-(4 时,当前位置被两个括号所包含,分别对应着+号和-号,由于+号和−号合并的结果为−号,因此栈顶元素变为−1。
在得到栈stack之后,sign的取值就能够确定了:
如果当前遇到了+号,则更新sign=satck.peek() 如果遇到了遇到了-号,则更新sign=-satck.peek()
然后,每当遇到(时,都要将当前的sign的值压入栈中;每当遇到)时,都从栈中弹出一个元素。这样,我们能够在扫描字符串的时候,即时地更新stack中的元素。
class Solution { public int calculate(String s) { Stack<Integer> stack = new Stack<>(); int sign = 1; stack.push(1); int res = 0; int n = s.length(); int i = 0; char[] arr = s.toCharArray(); while(i<n){ if(arr[i] == ' '){ i++; }else if(arr[i] == '+'){ sign = stack.peek(); i++; }else if(arr[i] == '-'){ sign = -stack.peek(); i++; }else if(arr[i] == '('){ stack.push(sign); i++; }else if(arr[i] == ')'){ stack.pop(); i++; }else{ long num = 0; while(i < n && arr[i] >= '0' && arr[i] <= '9'){ num = num*10 + arr[i] - '0'; i++; } res += sign*num; } } return res; } }#中国农业银行研发中心##面经##中国农业银行#