跳到主要内容 Kotlin 面试算法:数组最大值、最小值及查找技巧 | 极客日志
Kotlin 算法
Kotlin 面试算法:数组最大值、最小值及查找技巧 本文介绍了四种基于 Kotlin 的数组算法面试题解决方案。包括使用蛮力法与分治法查找数组最大最小值,利用二分查找定位旋转数组最小元素,通过累加求和与异或运算寻找缺失数字,以及使用哈希表与位运算找出出现奇数次的两个数。重点分析了各方法的时间复杂度与空间复杂度,并提供了完整的 Kotlin 代码实现。
奶糖兔 发布于 2026/3/29 更新于 2026/4/13 1 浏览4.2 如何查找数组中元素的最大值和最小值
难度: ★★★☆☆
被考察系数: ★★★★☆
题目描述
给定数组 a1, a2, a3, …an,要求找出数组中的最大值和最小值。假设数组中的值两两各不相同。
分析与解答
虽然题目没有时间复杂度与空间复杂度的要求,但是给出的算法的时间复杂度肯定是越低越好。
方法一:蛮力法
查找数组中元素的最大值与最小值并非是一件困难的事情,最容易想到的方法就是蛮力法。具体过程如下:首先定义两个变量 max 与 min,分别记录数组中最大值与最小值,并将其都初始化为数组的首元素的值,然后从数组的第二个元素开始遍历数组元素,如果遇到的数组元素的值比 max 大,则该数组元素的值为当前的最大值,并将该值赋给 max,如果遇到的数组元素的值比 min 小,则该数组元素的值为当前的最小值,并将该值赋给 min。
算法性能分析:
上述方法的时间复杂度为 O(n),但很显然,以上这种方法称不上是最优算法,因为最差情况下比较的次数达到了 2n-2 次(数组第一个元素首先赋值给 max 与 min,接下来的 n-1 个元素都需要分别跟 max 与 min 比较一次,一次比较次数为 2n-2),最好的情况下比较次数为 n-1。是否可以将比较次数降低呢?回答是肯定的,分治法就是一种高效的方法。
方法二:分治法
分治法就是将一个规模为 n 的、难以直接解决的大问题,分割为 k 个规模较小的子问题,采取各个击破、分而治之的策略得到各个子问题的解,然后将各个子问题的解进行合并,从而得到原问题的解的一种方法。
本题中,当采用分治法求解时,就是将数组两两一对分组,如果数组元素个数为奇数个,就把最后一个元素单独分为一组,然后分别对每一组中相邻的两个元数进行比较,把两者中值小的数放在数组的左边,值大的数放在数组右边,只需要比较 n/2 次就可以将数组分组完成。然后可以得出结论:最小值一定在每一组的左边部分,最大值一定在每一组的右边部分,接着只需要在每一组的左边部分找最小值,右边部分找最大值,查找分别需要比较 n/2-1 次和 n/2-1 次。因此,总共比较的次数大约为 n/2× 3= 3n/2-2 次。
实现代码如下:
class MaxMin {
var max: Int = 0 private set
var min: Int = 0 private set
fun getMaxAndMin (arr: IntArray ?) {
if (arr == null ) {
println("参数不合法" )
return
}
var i = 0
val len = arr.size
max = arr[0 ]
min = arr[0 ]
i =
(i < len - ) {
(arr[i] > arr[i + ]) {
tmp = arr[i]
arr[i] = arr[i + ]
arr[i + ] = tmp
}
i +=
}
min = arr[ ]
i =
(i < len) {
(arr[i] < min) {
min = arr[i]
}
i +=
}
max = arr[ ]
i =
(i < len) {
(arr[i] > max) {
max = arr[i]
}
i +=
}
(len % == ) {
(max < arr[len - ]) max = arr[len - ]
(min > arr[len - ]) min = arr[len - ]
}
}
}
{
array = intArrayOf( , , , , , , )
m = MaxMin()
m.getMaxAndMin(array)
println( )
println( )
}
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown 转 HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
HTML 转 Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
0
while
1
if
1
val
1
1
2
0
2
while
if
2
1
3
while
if
2
if
2
1
if
1
1
if
1
1
fun main (args: Array <String >)
val
7
3
19
40
4
7
1
val
"max=${m.max} "
"min=${m.min} "
方法三:变形的分治法 除了以上所示的分治法以外,还有一种分治法的变形,其具体步骤如下:将数组分成左右两部分,先求出左半部分的最大值和最小值,再求出右半部分的最大值和最小值,然后综合起来,左右两部分的最大值中的较大值即为合并后的数组的最大值,左右两部分的最小值中的较小值即为合并后的数组的最小值,通过此种方法即可求合并后的数组的最大值与最小值。
以上过程是个递归过程,对于划分后的左右两部分,同样重复这个过程,直到划分区间内只剩一个元素或者两个元素为止。
class MaxMin {
fun getMaxMin (array: IntArray , l: Int , r: Int ) : List<Int > {
val list = mutableListOf<Int >()
val m = (l + r) / 2
val max: Int
val min: Int
if (l == r) {
list.add(array[l])
list.add(array[l])
return list
}
if (l + 1 == r) {
if (array[l] >= array[r]) {
max = array[l]
min = array[r]
} else {
max = array[r]
min = array[l]
}
list.add(min)
list.add(max)
return list
}
val lList = getMaxMin(array, l, m)
val rList = getMaxMin(array, m + 1 , r)
max = if (lList[1 ] > rList[1 ]) lList[1 ] else rList[1 ]
min = if (lList[0 ] < rList[0 ]) lList[0 ] else rList[0 ]
list.add(min)
list.add(max)
return list
}
}
fun main (args: Array <String >) {
val array = intArrayOf(7 , 3 , 19 , 40 , 4 , 7 , 1 )
val m = MaxMin()
val result = m.getMaxMin(array, 0 , array.size - 1 )
println("max=${result[1 ]} " )
println("min=${result[0 ]} " )
}
这种方法与方法二的思路从本质上讲是相同的,只不过这种方法是使用递归的方式实现的,因此,比较次数为 3n/2-2。
4.3 如何找出旋转数组中的最小元素
题目描述 把一个有序数组最开始的若干个元素搬到数组的末尾,称之为数组的旋转。输入一个排好序的数组的一个旋转,输出旋转数组的最小元素。例如数组{3, 4, 5, 1, 2}为数组{1, 2, 3, 4, 5}的一个旋转,该数组的最小值为 1。
分析与解答 其实这是一个非常基本和常用的数组操作,它的描述如下:
有一个数组 X[0…n-1],现在把它分为两个子数组:x1[0…m]和 x2[m+1…n-1],交换这两个子数组,使数组 x 由 x1x2 变成 x2x1,例如 x={1, 2, 3, 4, 5, 6, 7, 8, 9},x1={1, 2, 3, 4, 5},x2={6, 7, 8, 9},交换后,x={6, 7, 8, 9, 1, 2, 3, 4, 5}。
对于本题的解决方案,最容易想到的,也是最简单的方法就是直接遍历法。但是这种方法显然没有用到题目中旋转数组的特性,因此,它的效率比较低下。下面介绍一种比较高效的二分查找法。
通过数组的特性可以发现,数组元素首先是递增的,然后突然下降到最小值,然后再递增。虽然如此,但是还有下面三种特殊情况需要注意:
数组本身是没有发生过旋转的,是一个有序的数组,例如序列{1,2,3,4,5,6}。
数组中元素值全部相等,例如序列{1,1,1,1,1,1}。
数组中元素值大部分都相等,例如序列{1,0,1,1,1,1}。
通过旋转数组的定义可知,经过旋转之后的数组实际上可以划分为两个有序的子数组,前面的子数组的元素值都大于或者等于后面子数组的元素值。可以根据数组元素的这个特点,采用二分查找的思想不断缩小查找范围,最终找出问题的解决方案,具体实现思路如下所示:
按照二分查找的思想,给定数组 arr,首先定义两个变量 low 和 high,分别表示数组的第一个元素和最后一个元素的下标。按照题目中对旋转规则的定义,第一个元素应该是大于或者等于最后一个元素的(当旋转个数为 0,即没有旋转的时候,要单独处理,直接返回数组第一个元素)。接着遍历数组中间的元素 arr[mid],其中 mid=(high+low)/2。
如果 arr[mid] < arr[mid - 1],则 arr[mid] 一定是最小值。
如果 arr[mid + 1] < arr[mid],则 arr[mid + 1] 一定是最小值。
如果 arr[high] > arr[mid],则最小值一定在数组左半部分。
如果 arr[mid]>arr[low],则最小值一定在数组右半部分。
如果 arr[low] == arr[mid] 且 arr[high] == arr[mid],则此时无法区分最小值是在数组的左半部分还是右半部分(例如:{2,2,2,2,1,2},{2,1,2,2,2,2,2})。在这种情况下,只能分别在数组的左右两部分找最小值 minL 与 minR,最后求出 minL 与 minR 的最小值。
fun getMin (arr: IntArray , low: Int , high: Int ) : Int {
if (high < low) return arr[0 ]
if (high == low) return arr[low]
val mid = low + (high - low shr 1 )
return when {
arr[mid] < arr[mid - 1 ] -> arr[mid]
arr[mid + 1 ] < arr[mid] -> arr[mid + 1 ]
arr[high] > arr[mid] -> getMin(arr, low, mid - 1 )
arr[mid] > arr[low] -> getMin(arr, mid + 1 , high)
else -> {
Math.min(getMin(arr, low, mid - 1 ), getMin(arr, mid + 1 , high))
}
}
}
fun getMin (arr: IntArray ) : Int {
return getMin(arr, 0 , arr.size - 1 )
}
fun main (args: Array <String >) {
val array1 = intArrayOf(5 , 6 , 1 , 2 , 3 , 4 )
var min = getMin(array1)
println(min)
val array2 = intArrayOf(1 , 1 , 0 , 1 )
min = getMin(array2)
println(min)
}
一般而言,二分查找的时间复杂度为 O(log2N),对于这道题而言,大部分情况下时间复杂度为 O(log2N),只有每次都满足上述条件(5)的时候才需要对数组中所有元素都进行遍历,因此,这种方法在最坏的情况下的时间复杂度为 O(N)。
引申:如何实现旋转数组功能 先分别把两个子数组的内容交换,然后把整个数组的内容交换,即可得到问题的解。
以数组 x1{1, 2, 3, 4, 5}与数组 x2{6, 7, 8, 9}为例,交换两个数组后,x1={5, 4, 3, 2, 1},x2={9, 8, 7, 6},即 x={5, 4, 3, 2, 1, 9, 8, 7, 6}。交换整个数组后,x={6, 7, 8, 9, 1, 2, 3, 4, 5}
fun swap (arr: IntArray , l: Int , h: Int ) {
var low = l
var high = h
while (low < high) {
val tmp = arr[low]
arr[low] = arr[high]
arr[high] = tmp
low++
high--
}
}
fun rotateArr (arr: IntArray ?, div: Int ) {
if (null == arr || div < 0 || div >= arr.size) {
println("参数不合法" )
return
}
if (div == 0 || div == arr.size - 1 ) return
swap(arr, 0 , div)
swap(arr, div + 1 , arr.size - 1 )
swap(arr, 0 , arr.size - 1 )
}
fun main (args: Array <String >) {
val arr = intArrayOf(1 , 2 , 3 , 4 , 5 )
rotateArr(arr, 2 )
for (i in arr.indices) print("${arr[i]} " )
}
由于这种方法需要遍历两次数组,因此,它的时间复杂度为 O(N)。而交换两个变量的值,只需要使用一个辅助储存空间,所以,它的空间复杂度为 O(1)。
4.4 如何找出数组中丢失的数
题目描述 给定一个由 n-1 个整数组成的未排序的数组序列,其元素都是 1 到 n 中的不同的整数。请写出一个寻找数组序列中缺失整数的线性时间算法。
分析与解答
方法一:累加求和 首先分析一下数学性质。假设缺失的数字是 X,那么这 n-1 个数一定是 1~n 之间除了 X 以外的所有数,试想一下,1~n 一共 n 个数的和是可以求出来的,数组中的元素的和也是可以求出来的,二者相减,其值是不是就是缺失的数字 X 的值呢?
为了更好地说明上述方法,举一个简单的例子。假设数组序列为{2, 1, 4, 5}一共 4 个元素,n 的值为 5,要想找出这个缺失的数字,可以首先对 1~5 这五个数字求和,求和结果为 15(1+2+3+4+5=15),而数组元素的和为 array[0]+array[1]+array[2]+array[3]=2+1+4+5=12,所以,缺失的数字为 15-12=3。
通过上面的例子可以很容易形成以下具体思路:定义两个数 suma 与 sumb,其中,suma 表示的是这 n-1 个数的和,sumb 表示的是这 n 个数的和,很显然,缺失的数字的值即为 sumb-suma 的值。示例代码如下:
fun getNum (arr: IntArray ) : Int {
if (arr.isEmpty()) {
println("参数不合理" )
return -1
}
var suma = 0
var sumb = 0
var i = 0
while (i < arr.size) {
suma += arr[i]
sumb += i
i++
}
sumb += arr.size + arr.size + 1
return sumb - suma
}
fun main (args: Array <String >) {
val arr = intArrayOf(1 , 4 , 3 , 2 , 7 , 5 )
println(getNum(arr))
}
这种方法的时间复杂度为 O(N)。需要注意的是,在求和的过程中,计算结果有溢出的可能性。所以,为了避免这种情况的发生,在进行数学运算时,可以考虑位运算,毕竟位运算性能最好,下面介绍如何用位运算来解决这个问题。
方法二:异或法 在解决这个问题前,首先回顾一下异或运算的性质。简单点说,在进行异或运算时,当参与运算的两个数相同时,异或结果为假,当参与异或运算的两个数不相同时,异或结果为真。
1~n 这 n 个数异或的结果为 a=1^2^3^…^n。假设数组中缺失的数为 m,那么数组中这 n-1 个数异或的结果为 b=1^2^3^…(m-1)^(m+1)^…^n。由此可知,a^b=(1^1)^(2^2)…(m-1)^(m-1)^m^(m+1)^(m+1)…^(n^n)=m。根据这个公式可以得知本题的主要思路是:定义两个数 a 与 b,其中,a 表示的是 1~n 这 n 个数的异或运算结果,b 表示的是数组中的 n-1 个数的异或运算结果,缺失的数字的值即为 a^b 的值。
fun getNum (arr: IntArray ) : Int {
if (arr.isEmpty()) {
println("参数不合理" )
return -1
}
var a = arr[0 ]
var b = 1
val len = arr.size
for (i in 1 until len) {
a = a xor arr[i]
}
for (i in 2. .len + 1 ) {
b = b xor i
}
return a xor b
}
这种方法在计算结果 a 的时候对数组进行了一次遍历,时间复杂度为 O(N),接着在计算 b 的时候循环执行的次数为 N,时间复杂度也为 O(N)。因此,这种方法的时间复杂度为 O(N)。
4.5 如何找出数组中出现奇数次的数
题目描述 数组中有 N+2 个数,其中,N 个数出现了偶数次,两个数出现了奇数次(这两个数不相等),请用 O(1) 的空间复杂度,找出这两个数。注意:不需要知道具体位置,只需要找出这两个数。
分析与解答
方法一:Hash 法 对于本题而言,定义一个 HashMap 表,把数组元素的值作为 key,遍历整个数组,如果 key 值不存在,则将 value 设为 1,如果 key 值已经存在,则翻转该值(如果为 0,则翻转为 1;如果为 1,则翻转为 0),在完成数组遍历后,Hash 表中 value 为 1 的就是出现奇数次的数。
例如:给定数组={ 3, 5, 6, 6, 5, 7, 2, 2 };
首先遍历 3,HashMap 中的元素为:<3,1>;遍历 5,HashMap 中的元素为:<3,1>, <5,1>;
遍历 6,HashMap 中的元素为:<3,1>, <5,1>, <6,1>;遍历 6,HashMap 中的元素为:<3,1>, <5,1>, <6,0>;遍历 5,HashMap 中的元素为:<3,1>, <5,0>, <6,0>;
遍历 7,HashMap 中的元素为:<3,1>, <5,0>, <6,0>, <7,1>;
遍历 2,HashMap 中的元素为:<3,1>, <5,0>, <6,0>, <7,1>, <2,1>;遍历 2,HashMap 中的元素为:<3,1>, <5,0>, <6,0>, <7,1>, <2, 0>;
显然,出现 1 次的数组元素为 3 和 7。实现代码如下:
fun get2Num (arr: IntArray ) {
if (arr.isEmpty()) {
println("参数不合理" )
return
}
val map = hashMapOf<Int , Int >()
for (i in 0 until arr.size) {
if (!map.containsKey(arr[i])) {
map.put(arr[i], 1 )
} else {
map.put(arr[i], 0 )
}
}
map.entries.iterator().forEach {
if (it.value == 1 ) {
println(it.key)
}
}
}
fun main (args: Array <String >) {
val arr = intArrayOf(3 , 5 , 6 , 6 , 5 , 7 , 2 , 2 )
get2Num(arr)
}
这种方法对数组进行了一次遍历,时间复杂度为 O(n)。但是申请了额外的存储过程来记录数据出现的情况,因此,空间复杂度为 O(n)。
方法二:异或法 根据异或运算的性质不难发现,任何一个数字异或它自己其结果都等于 0。所以,对于本题中的数组元素而言,如果从头到尾依次异或每一个元素,那么异或运算的结果自然也就是那个只出现奇数次的数字,因为出现偶数次的数字会通过异或运算全部消掉。
但是通过异或运算,也仅仅只是消除了所有出现偶数次数的数字,最后异或运算的结果肯定是那两个出现了奇数次的数异或运算的结果。假设这两个出现奇数次的数分别为 a 与 b,根据异或运算的性质,将二者异或运算的结果记为 c,由于 a 与 b 不相等,所以,c 的值自然也不会为 0,此时只需知道 c 对应的二进制数中某一个位为 1 的位数 N,例如,十进制数 44 可以由二进制数 0010 1100 表示,此时可取 N=2 或者 3,或者 5,然后将 c 与数组中第 N 位为 1 的数进行异或,异或结果就是 a 和 b 中的一个,然后用 c 异或其中一个数,就可以求出另外一个数了。
通过上述方法为什么就能得到问题的解呢?其实很简单,因为 c 中第 N 位为 1 表示 a 或 b 中有一个数的第 N 位也为 1,假设该数为 a,那么,当将 c 与数组中第 N 位为 1 的数进行异或时,也就是将 x 与 a 外加上其他第 N 位为 1 的出现过偶数次的数进行异或,化简即为 x 与 a 异或,结果即为 b。
fun get2Num (arr: IntArray ) {
if (arr.isEmpty()) {
println("参数不合理" )
return
}
var result = 0
var position = 0
for (i in arr.indices) {
result = result xor arr[i]
}
val tmpResult = result
var i = result
while (i and 1 == 0 ) {
position++
i = i shr 1
}
arr.indices.asSequence()
.filter {
arr[it] shr position and 1 == 1
}
.forEach {
result = result xor arr[it]
}
println(result)
println(result xor tmpResult)
}
fun main (args: Array <String >) {
val arr = intArrayOf(3 , 5 , 6 , 6 , 5 , 7 , 2 , 2 )
get2Num(arr)
}
这种方法首先对数组进行了一次遍历,其时间复杂度为 O(N),接着找 result 对应二进制数中位值为 1 的位数,时间复杂度为 O(1),接着又遍历了一次数组,时间复杂度为 O(N),因此,这种方法整体的时间复杂度为 O(N)。