1. 线性查找
思想
从数据结构的一端开始,按顺序遍历每个元素,直到找到目标或遍历完所有元素。适用于无序或有序的任意序列。
步骤
- 从第一个元素开始
- 将当前元素与目标值比较
- 若相等则返回当前下标,结束
- 否则移动到下一个元素,重复步骤 2–3
- 若遍历结束仍未找到,返回 -1
Kotlin 实现
/**
* 线性查找
* @param arr 待查找数组
* @param target 目标值
* @return 目标下标,未找到返回 -1
*/
fun linearSearch(arr: IntArray, target: Int): Int {
for (i in arr.indices) {
if (arr[i] == target) return i
}
return -1
}
时间复杂度与空间复杂度
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n) | 最坏遍历整个表 |
| 空间复杂度 | O(1) | 仅用少量变量 |
优化空间
- 哨兵:在表尾追加 target,循环只做比较、不必判越界,减少分支,常数优化。
- 有序表提前终止:若表有序且
arr[i] > target可立即返回 -1。 - 适用场景:无序或小规模时用线性;大规模无序用哈希,有序用二分。
哨兵写法示例(需可写副本时使用;会临时修改数组末尾,调用方应传入副本或保证可修改):
fun linearSearchWithSentinel(arr: IntArray, target: Int): Int {
val n = arr.size
if (n == 0) return -1
val last = arr[n - 1]
arr[n - 1] = target // 暂存原末尾,用 target 当哨兵
var i = 0
while (arr[i] != target) { i++ }
arr[n - 1] = last // 恢复
return if (i < n - 1 || last == target) i else -1
}
2. 二分查找
思想
在有序数组中,每次与中间元素比较,根据比较结果缩小一半查找范围,直到找到或区间为空。前提是数组有序。
步骤
- 设左边界
left = 0,右边界right = arr.size - 1 - 若
left > right,则未找到,返回 -1 - 计算中点
mid(实现时用left + (right - left) / 2防溢出) - 若
arr[mid] == target,返回mid - 若
arr[mid] > target,在左半部分查找:right = mid - 1,回到步骤 2 - 若
arr[mid] < target,在右半部分查找:left = mid + 1,回到步骤 2
Kotlin 实现
/**
* 二分查找(迭代版)
* 要求数组有序
*/
fun binarySearch(arr: IntArray, target: Int): Int {
var left = 0
var right = arr.size - 1
while (left <= right) {
val mid = left + (right - left) / 2 // 防溢出
if (arr[mid] == target) return mid
if (arr[mid] > target) { right = mid - 1 } else { left = mid + 1 }
}
return -1
}
/**
* 二分查找(递归版)
*/
fun binarySearchRecursive(arr: IntArray, target: Int, left: Int = 0, right: Int = arr.size - 1): Int {
if (left > right) return -1
val mid = left + (right - left) / 2
if (arr[mid] == target) return mid
if (arr[mid] > target) return binarySearchRecursive(arr, target, left, mid - 1)
return binarySearchRecursive(arr, target, mid + 1, right)
}
时间复杂度与空间复杂度
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(log n) | 每次折半 |
| 空间复杂度 | O(1),递归 O(log n) | 迭代 O(1),递归为栈空间 |
优化空间
- 防溢出:用
mid = left + (right - left) / 2代替(left + right) / 2。 - 避免重复比较:若需找「第一个 ≥ target」等边界,可先缩区间再判相等,减少分支。
- 递归改迭代:面试/工程更常用迭代,避免栈溢出。
常见变形:下界查找(第一个 ≥ target 的下标,常考):
/** 返回第一个 >= target 的下标,若均小于 target 则返回 arr.size */
fun lowerBound(arr: IntArray, target: Int): Int {
var left = 0
var right = arr.size
while (left < right) {
val mid = left + (right - left) / 2
if (arr[mid] < target) left = mid + 1 else right = mid
}
return left
}
/** 返回第一个 > target 的下标。等值区间为 [lowerBound, upperBound)。 */
fun upperBound(arr: IntArray, target: Int): Int {
var left = 0
var right = arr.size
while (left < right) {
val mid = left + (right - left) / 2
if (arr[mid] <= target) left = mid + 1 else right = mid
}
return left
}
3. 分块查找
思想
索引顺序查找:将表分成若干块,块内元素可无序,块间有序(第 i 块的最大值 < 第 i+1 块的最小值)。先查索引表确定目标所在块,再在该块内做顺序查找。是顺序查找与二分思想之间的折中,数据结构课常考。
前提提醒:必须保证“块间有序”(例如第 i 块最大值 < 第 i+1 块最小值),否则用索引表定位块可能出错。
步骤
- 建立索引:每块记录该块的最大关键字(起始下标可由块号 × 块长得到,故索引表通常只存每块最大值)
- 在索引表中确定 target 所在块:找到第一个「块最大值 ≥ target」的块
- 在该块对应的区间内进行顺序查找
- 找到返回下标,否则返回 -1
Kotlin 实现
/**
* 分块查找:索引表 + 块内顺序查找
* 约定:块间有序(块 i 最大值 < 块 i+1 最小值),块内无序
* @param arr 原数组
* @param blockSize 每块大小
* @param indexMax 索引表,存每块的最大值
*/
fun blockSearch(
arr: IntArray,
target: Int,
blockSize: Int = 3,
indexMax: IntArray? = null // 不传则内部自动建索引
): Int {
require(blockSize > 0) { "blockSize must be positive" }
val index = indexMax ?: buildBlockIndex(arr, blockSize)
// 1. 在索引表中确定块
var block = -1
for (i in index.indices) {
if (target <= index[i]) {
block = i
break
}
}
if (block == -1) return -1
// 2. 块内顺序查找
val start = block * blockSize
val end = minOf(start + blockSize, arr.size)
for (i in start until end) {
if (arr[i] == target) return i
}
return -1
}
/** 根据数组与块大小构建索引表(每块最大值) */
fun buildBlockIndex(arr: IntArray, blockSize: Int): IntArray {
val blockCount = (arr.size + blockSize - 1) / blockSize
return IntArray(blockCount) { b ->
val start = b * blockSize
val end = minOf(start + blockSize, arr.size)
(start until end).maxOfOrNull { arr[it] } ?: 0
}
}
时间复杂度与空间复杂度
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(s + n/s) | 找块 O(s),块内 O(n/s);s=√n 时 O(√n) |
| 空间复杂度 | O(s) | 索引表,s 为块数 |
优化空间
- 索引表用二分:若索引表按块最大值有序,找块可二分,时间降为 O(log s + n/s)。
- 块大小:取 s = √n 时查找约 O(√n);也可按 I/O 块大小或缓存行调整。
- 块内有序:块内再有序时,块内可二分,进一步降为 O(log s + log(n/s))。
4. 哈希查找
思想
通过哈希函数将关键字映射到表的下标,直接在对应位置(或拉链/探测后的位置)查找。平均情况下一次或常数次比较即可命中,是“以空间换时间”的典型,笔试面试必考。
说明:本文 Kotlin 代码示例实现的是“开放定址(线性探测)”。“链地址法”仅在概念上提及。
实现约束:示例代码用 -1 表示空位,因此 key 不应取 -1(否则会与“空”冲突)。
步骤
- 建表:根据关键字 key 计算
hash = H(key),若发生冲突则按既定策略处理(开放定址或链地址) - 查找:计算
hash = H(target),根据冲突处理策略在表中定位 - 若该位置存在且关键字等于 target,查找成功;否则按冲突策略继续找或判定不存在
Kotlin 实现
/**
* 哈希查找:简单除留余数法 + 线性探测
* 表内存 key,-1 表示空
*/
class HashTable(private val capacity: Int) {
private val keys = IntArray(capacity) { -1 }
private fun hash(key: Int): Int = (key % capacity + capacity) % capacity
fun insert(key: Int): Boolean {
var h = hash(key)
var step = 0
while (step < capacity) {
if (keys[h] == -1 || keys[h] == key) {
keys[h] = key
return true
}
h = (h + 1) % capacity
step++
}
return false // 表满
}
/** 查找:返回 key 的下标,未找到返回 -1 */
fun search(target: Int): Int {
var h = hash(target)
var step = 0
while (step < capacity) {
if (keys[h] == -1) return -1
if (keys[h] == target) return h
h = (h + 1) % capacity
step += 1
}
return -1
}
}
时间复杂度与空间复杂度
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | 平均 O(1),最坏 O(n) | 冲突多时退化为线性探测序列 |
| 空间复杂度 | O(m) | m 为散列表长度 |
优化空间
- 负载因子:α = count/m(count 为关键字个数,m 为表长;此处的 count 不是“数组长度 n”),一般 α < 0.75 时扩容,减少冲突。
- 冲突策略:链地址法实现简单、稳定;开放定址省指针但易聚集,可改用二次探测或双哈希。
- 哈希函数:好的散列(如 MurmurHash、取模质数)减少碰撞,提升平均 O(1)。
- 表长取质数:除留余数法时 m 取质数可减轻取模造成的聚集。
5. 插值查找
思想
在有序数组中,根据目标值在数值范围内的相对位置估算目标位置,而不是总取中点。数据分布较均匀时比二分查找更快。
步骤
- 设
left = 0,right = arr.size - 1 - 若
left > right或 target 不在闭区间[arr[left], arr[right]]内,返回 -1 - 用插值公式计算探测位置:
pos = left + (target - arr[left]) * (right - left) / (arr[right] - arr[left]) - 将 pos 截断到区间内(posSafe),比较
arr[posSafe]与 target - 若
arr[posSafe] == target,返回posSafe - 若
arr[posSafe] > target,在左半部分查找:right = posSafe - 1,回到步骤 2;否则在右半部分查找:left = posSafe + 1
Kotlin 实现
/**
* 插值查找
* 适用于有序且分布均匀的数组
*/
fun interpolationSearch(arr: IntArray, target: Int): Int {
var left = 0
var right = arr.size - 1
while (left <= right && target >= arr[left] && target <= arr[right]) {
if (left == right) {
return if (arr[left] == target) left else -1
}
// 分母为 0 时区间内值相同,直接判等
if (arr[right] == arr[left]) {
return if (arr[left] == target) left else -1
}
val pos = left + (target - arr[left]) * (right - left) / (arr[right] - arr[left])
val posSafe = pos.coerceIn(left, right)
if (arr[posSafe] == target) return posSafe
if (arr[posSafe] > target) right = posSafe - 1 else left = posSafe + 1
}
return -1
}
时间复杂度与空间复杂度
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | 平均 O(log log n),最坏 O(n) | 分布均匀时很快;单调等差等最坏 O(n) |
| 空间复杂度 | O(1) | 仅用少量变量 |
优化空间
- 边界与除零:
arr[right]==arr[left]时插值公式分母为 0,需单独判断或取 mid。 - 适用数据:关键字均匀分布时优于二分;分布不均时可能不如二分,可退化用二分。
- pos 越界:计算出的 pos 需
coerceIn(left, right)防止越界或震荡。
6. 斐波那契查找
思想
利用斐波那契数列分割有序数组:用斐波那契数 F(k) 将当前待查区间分成前、中、后三段(长度与 F(k-1)、F(k-2) 相关),根据与 target 的比较在某一侧继续,相当于按黄金比例取点。与黄金分割相关,适合顺序访问代价较高的存储。
步骤
- 找到不小于 n 的最小斐波那契数 F(k),令
offset = -1 - 循环:若
k > 0:i = min(offset + F(k-2), n-1)- 若
arr[i] == target,返回i - 若
arr[i] > target,在左半部分:k = k - 2,offset不变(在逻辑左段) - 若
arr[i] < target,在右半部分:k = k - 1,offset = i
- 循环结束后,若
offset + 1 < n且arr[offset + 1] == target,返回offset + 1,否则返回 -1(实现中对应k == 0的收尾判断)
Kotlin 实现
/**
* 斐波那契查找
* 要求数组有序,利用斐波那契数列分割
*/
fun fibonacciSearch(arr: IntArray, target: Int): Int {
val n = arr.size
if (n == 0) return -1
val fib = mutableListOf(1, 1)
while (fib.last() < n) {
fib.add(fib[fib.size - 1] + fib[fib.size - 2])
}
var k = fib.size - 1
var offset = -1
while (k > 0) {
val i = (offset + fib.getOrElse(k - 2) { 0 }).coerceIn(0, n - 1)
if (arr[i] == target) return i
if (arr[i] > target) { k -= 2 } else { offset = i; k -= 1 }
}
if (k == 0 && offset + 1 < n && arr[offset + 1] == target) {
return offset + 1
}
return -1
}
时间复杂度与空间复杂度
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(log n) | 按斐波那契缩小区间 |
| 空间复杂度 | O(log n) | 预存斐波那契数列(约 log n 项) |
优化空间
- 斐波那契表缓存:数列可预计算为静态表,避免每次建表。
- 只存必要项:只需到 F(k) ≥ n 的几项,空间仍为 O(log n)。
- 与二分对比:理论常数略优(黄金分割),但实现复杂,工程上二分更常用。
7. 跳跃查找
思想
在有序数组中,以固定步长(常取 √n)向前跳跃,直到当前块末尾元素 ≥ target,再在该块内做线性查找。块数约为 √n,块内长度约为 √n,总时间 O(√n)。
步骤
- 确定步长
step = √n(n 为数组长度) - 从下标 0 开始令
i=0,当i < n且arr[i] < target时执行i += step,直到i >= n或arr[i] >= target - 在区间
start = max(0, i-step)到end = min(i, n)内顺序查找(实现中对应start = i - step,end = minOf(i, n)) - 找到返回下标,否则返回 -1
Kotlin 实现
/**
* 跳跃查找
* 要求数组有序,步长取 √n
*/
fun jumpSearch(arr: IntArray, target: Int): Int {
val n = arr.size
if (n == 0) return -1
val step = kotlin.math.sqrt(n.toDouble()).toInt().coerceAtLeast(1)
var i = 0
// 跳跃到目标可能所在的块
while (i < n && arr[i] < target) {
i += step
}
// 在块内线性查找
val start = (i - step).coerceAtLeast(0)
for (j in start until minOf(i, n)) {
if (arr[j] == target) return j
if (arr[j] > target) break
}
return -1
}
时间复杂度与空间复杂度
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(√n) | 跳 O(√n) 步 + 块内 O(√n) |
| 空间复杂度 | O(1) | 仅用少量变量 |
优化空间
- 步长:取 √n 时总时间 O(√n);也可按块数或块长灵活调整。
- 块内改进:块内有序时可在块内二分,整体 O(√n) 跳 + O(log √n) = O(√n) 主导。
- 与二分对比:实现简单,但有序表一般直接用二分更优。
8. 指数查找
思想
先以 1、2、4、8… 为下标找到第一个 ≥ target 的位置,再在该区间内做二分查找。适合无界或很大的有序序列,或目标靠近开头时。
步骤
- 若
arr[0] == target,返回 0 - 设
i = 1,若i < n且arr[i] < target,则i *= 2,重复直到arr[i] >= target或 i 超出范围(越界时右端按 n-1 处理) - 在区间
[i/2, min(i, n-1)]上执行二分查找 - 返回二分查找结果
Kotlin 实现
/**
* 指数查找
* 要求数组有序,适合目标靠近开头或数组很大
*/
fun exponentialSearch(arr: IntArray, target: Int): Int {
val n = arr.size
if (n == 0) return -1
if (arr[0] == target) return 0
var i = 1
while (i < n && arr[i] < target) {
i *= 2
}
val left = i / 2
val right = minOf(i, n - 1)
return binarySearchInRange(arr, target, left, right)
}
private fun binarySearchInRange(arr: IntArray, target: Int, left: Int, right: Int): Int {
var l = left
var r = right
while (l <= r) {
val mid = l + (r - l) / 2
if (arr[mid] == target) return mid
if (arr[mid] > target) r = mid - 1 else l = mid + 1
}
return -1
}
时间复杂度与空间复杂度
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(log n) | 指数定界 + 区间内二分 |
| 空间复杂度 | O(1) | 仅用少量变量 |
优化空间
- 目标靠前:当 target 在前部时,指数扩界很快,再二分,常数优于纯二分。
- 无界/流式:在长度未知或很大的有序序列上,先指数再二分是常用模式。
- 与纯二分:随机位置 target 时总时间仍 O(log n),与二分同量级。
总结对比
时间复杂度与空间复杂度一览
| 算法 | 时间复杂度 | 空间复杂度 | 前提条件 |
|---|---|---|---|
| 线性查找 | O(n) | O(1) | 无 |
| 二分查找 | O(log n) | O(1),递归 O(log n) | 有序 |
| 分块查找 | O(√n)(s=√n 时) | O(√n) | 块间有序 |
| 哈希查找 | 平均 O(1),最坏 O(n) | O(m) | 无 |
| 插值查找 | 平均 O(log log n),最坏 O(n) | O(1) | 有序、分布均匀更优 |
| 斐波那契查找 | O(log n) | O(log n) | 有序 |
| 跳跃查找 | O(√n) | O(1) | 有序 |
| 指数查找 | O(log n) | O(1) | 有序 |
优化空间汇总
| 算法 | 可优化点简述 |
|---|---|
| 线性查找 | 哨兵减少分支;有序表可提前终止 |
| 二分查找 | 防溢出写法;递归改迭代省栈 |
| 分块查找 | 索引表二分找块;块内有序可块内二分 |
| 哈希查找 | 负载因子、表长取质数、哈希函数与冲突策略 |
| 插值查找 | 处理分母为 0、pos 越界;分布不均时退化二分 |
| 斐波那契查找 | 斐波那契表缓存;工程上多用二分 |
| 跳跃查找 | 步长与块内有序时块内二分 |
| 指数查找 | 目标靠前或无界序列时优于纯二分 |
综合对比与选用建议
| 算法 | 适用场景 | 常考 | 选用建议 |
|---|---|---|---|
| 线性查找 | 无序、小规模 | ★★★ | 默认暴力方案 |
| 二分查找 | 有序、通用 | ★★★ | 有序表首选 |
| 分块查找 | 块内无序、块间有序 | ★★★ | 动态插入多、不便全序时 |
| 哈希查找 | 关键字精确查找、无序 | ★★★ | 要平均 O(1) 时首选 |
| 插值查找 | 有序且分布均匀 | ★★ | 大数据且均匀时考虑 |
| 斐波那契查找 | 有序、理论/扩展 | ★ | 了解即可,工程少用 |
| 跳跃查找 | 有序、顺序访问友好 | ★★ | 链表式存储时考虑 |
| 指数查找 | 有序、目标靠前或无界 | ★★ | 无界或流式有序时用 |
一句话选型:无序小表用线性;有序用二分;要平均 O(1) 用哈希;块间有序用分块;其余按场景选插值/跳跃/指数,斐波那契作扩展了解。
选型决策:无序 → 规模小用线性、否则哈希;有序 → 块间有序用分块、否则二分。
常见考点与易错点
| 考点/易错点 | 说明 |
|---|---|
| 二分边界 | left <= right 与 left < right、mid ± 1 的取法要一致,否则死循环或漏元素;防溢出用 left + (right - left) / 2。 |
| 二分适用前提 | 必须有序;若题目说“有序+查找”优先想二分及其变形(下界/上界)。 |
| 哈希负载因子 | α = count/m(count 为关键字个数,m 为表长);过大冲突多,过小浪费空间;常考扩容时机(如 α > 0.75)。 |
| 哈希冲突 | 开放定址(线性/二次/双哈希)与链地址法区别、优缺点;线性探测易产生聚集。 |
| 分块条件 | 块间有序、块内无序;索引表存块最大键或区间;找块可顺序可二分。 |
| 插值查找 | 适用均匀分布;分母为 0 要特判;分布不均时可能退化为 O(n)。 |
| 返回值约定 | 未找到统一返回 -1 还是 throw、返回 null 等,代码与题目约定一致。 |
| 空表与单元素 | 各算法对 n=0、n=1 的处理要正确,避免越界或除零。 |
| 二分下界/上界 | lowerBound:第一个 ≥ target;upperBound:第一个 > target。等值区间为 [lowerBound, upperBound)。 |
| 查找与排序结合 | 无序表若需多次查找,可先排序再二分,总代价 O(n log n) + k·O(log n);或直接用哈希实现单次 O(1)。 |
最好 / 平均 / 最坏时间复杂度速查
| 算法 | 最好 | 平均 | 最坏 |
|---|---|---|---|
| 线性查找 | O(1) | O(n) | O(n) |
| 二分查找 | O(1) | O(log n) | O(log n) |
| 分块查找 | O(1) | O(√n) | O(√n) |
| 哈希查找 | O(1) | O(1) | O(n) |
| 插值查找 | O(1) | O(log log n) | O(n) |
| 斐波那契查找 | O(1) | O(log n) | O(log n) |
| 跳跃查找 | O(1) | O(√n) | O(√n) |
| 指数查找 | O(1) | O(log n) | O(log n) |
运行提示
本文以“算法方法”形式给出 Kotlin 代码。若你要在本地运行验证:把需要的方法复制到同一个 .kt 文件里,并自行添加一个 main() 调用测试即可(同一文件只能有一个 main)。