当前位置:首页 > 文章列表 > Golang > Go教程 > Go slices 怎样用 BinarySearch 查找有序切片

Go slices 怎样用 BinarySearch 查找有序切片

来源:17golang原创 2026-10-18 05:52:45 0浏览 收藏

如果一段 Go 数据已经按升序排好,想知道某个值是否存在,或者想知道新值应该插在哪里,直接使用 slices.BinarySearch。它返回两个结果:第一个是索引,第二个是 found。命中时索引指向目标值;未命中时索引仍然有用,它就是目标值保持有序时应插入的位置。

先记住这三点
  • slices.BinarySearch 要求切片按升序排列,不能把无序数据直接传进去。
  • found=false 时,返回的索引不是错误值,而是有序插入点,可能等于切片长度。
  • 结构体或自定义排序使用 slices.BinarySearchFunc,比较函数必须和切片的排序规则一致。
有序切片中 BinarySearch 返回命中索引或未命中插入位置的结构说明图
图1:用索引、目标值和插入位置说明 BinarySearch 的两个返回结果,这是静态结构图,不是运行截图。

BinarySearch 先解决的是什么问题

slices 是 Go 标准库中的切片工具包。BinarySearch 的签名可以概括为:给定一个升序切片和一个目标值,返回目标值最早出现的位置,以及是否真的找到。整数、浮点数和字符串等满足有序比较约束的元素可以直接使用它。

这里的关键不是“它会自动排序”,而是“它在已经排序的数据上查找”。如果数据来自追加写入,调用前应先完成排序;查找过程中不要把切片内容改成破坏升序的状态。

package main

import (
	"fmt"
	"slices"
)

func main() {
	// 切片必须按升序排列,BinarySearch 才能返回可靠结果。
	prices := []int{10, 20, 35, 50, 80}

	// 命中时 index 指向目标值,found 表示确实存在。
	index, found := slices.BinarySearch(prices, 35)
	fmt.Println(index, found) // 2 true

	// 未命中时 index 是保持升序所需的插入位置。
	index, found = slices.BinarySearch(prices, 40)
	fmt.Println(index, found) // 3 false
}

这个例子中,40 应放在 35 和 50 之间,所以返回索引 3。它不代表“离目标最近的元素”,也不是最后一次比较的位置,而是可以直接用于插入的边界。

怎样读懂 index 和 found

实际代码中应始终同时处理两个返回值。命中时使用 found 保护索引访问;未命中时可以把 index 交给插入逻辑、范围判断或去重逻辑。

场景返回值含义常见用途
目标已存在index 指向最早匹配项,found=true读取、更新或拒绝重复值
目标位于两个元素之间index 是中间插入点,found=false保持升序插入
目标小于首元素index=0,found=false插入到切片开头
目标大于末元素index=len(x),found=false追加到切片末尾

空切片也遵循同一规则:返回索引为 0、found=false。因此不要把 index=0 单独当成命中标志。

重复值为什么返回最早位置

有重复值时,BinarySearch 返回目标值第一次出现的位置。这一点适合做“是否存在”判断,也适合把一组相同值的范围从左边界开始定位。如果业务只需要任意一个匹配项,就不要依赖重复值中间某个位置的偶然结果。

package main

import (
	"fmt"
	"slices"
)

func main() {
	// 重复值仍需保持整体升序,返回值会指向最早的 7。
	levels := []int{3, 7, 7, 7, 12}
	index, found := slices.BinarySearch(levels, 7)

	// found 用来确认命中,index 用来读取第一处匹配。
	if found {
		fmt.Println("first match:", index, levels[index])
	}
}

结构体字段查找用 BinarySearchFunc

当切片元素是结构体,不能直接用普通 BinarySearch 比较整个结构体。这时使用 BinarySearchFunc,让比较函数只关注排序字段。比较函数返回负数、零或正数,分别表示切片元素排在目标前面、相等或排在目标后面。

package main

import (
	"fmt"
	"slices"
	"strings"
)

type User struct {
	Name string
	Age  int
}

func main() {
	users := []User{
		{Name: "Alice", Age: 30},
		{Name: "Bob", Age: 24},
		{Name: "Vera", Age: 41},
	}

	// users 按 Name 升序排列,比较函数也必须按 Name 判断先后。
	index, found := slices.BinarySearchFunc(users, User{Name: "Bob"}, func(item, target User) int {
		return strings.Compare(item.Name, target.Name)
	})
	if found {
		// 命中后再读取完整结构体,比较时忽略的 Age 不会丢失。
		fmt.Println(index, users[index].Name, users[index].Age)
	}
}

如果切片实际按年龄排序,比较函数却按姓名比较,结果就没有意义。排序字段、比较函数和后续插入逻辑必须使用同一套顺序;降序数据也不能直接套用普通升序规则。

BinarySearchFunc 按结构体 Name 字段比较并返回目标索引的关系说明图
图2:结构体切片、比较函数和命中索引之间的关系说明图,这是静态示意图,不是运行截图。

把返回位置接到去重和有序插入

返回位置最实用的场景是“先查后写”。命中时不重复插入;未命中时,把新值插入到该索引处。下面的示例用 slices.Insert 复用同一个边界,避免重新手写移动元素的细节。

package main

import (
	"fmt"
	"slices"
)

func insertUnique(values []int, value int) []int {
	// 先取得命中状态和有序插入点,不命中时 index 仍可直接使用。
	index, found := slices.BinarySearch(values, value)
	if found {
		// 已存在就保持原切片内容,避免产生重复记录。
		return values
	}

	// 插入点来自二分查找,因此结果继续保持升序。
	return slices.Insert(values, index, value)
}

func main() {
	values := []int{10, 20, 50}
	values = insertUnique(values, 35)
	values = insertUnique(values, 20)
	fmt.Println(values) // [10 20 35 50]
}

需要注意,查找本身是对数级定位,但切片中间插入仍可能移动后续元素。数据量很大、插入频率很高时,应把“查找成本”和“搬移成本”分开评估,不能只因为用了二分查找就认为整个写入过程都是 O(log n)。

几个容易踩到的边界

  • 无序切片:不要把无序数据直接传给 BinarySearch,先明确排序规则并完成排序。
  • 降序切片:普通函数要求升序;如果使用自定义函数,比较规则必须和切片实际顺序完全一致。
  • found 判断:索引为零不代表命中,必须检查第二个返回值。
  • 重复值:命中后得到的是最早匹配位置,不要据此推断所有重复值的范围已经被返回。
  • 自定义比较:比较函数返回零的条件要和业务的“相等”定义一致,否则可能把不同记录当成重复。

常见问题

BinarySearch 找不到时会返回 -1 吗?

不会。未命中时返回保持升序所需的插入位置,范围是 0 到 len(x),再通过 found=false 表示目标不存在。

为什么不能把 map 或无序切片直接拿来二分查找?

二分查找依赖元素顺序来缩小范围。无序数据不满足这个前提,返回的索引就没有可靠含义;map 也没有可供该 API 使用的连续索引顺序。

官方 API 说明在哪里?

可以直接查看标准库文档:https://pkg.go.dev/slices#BinarySearch,自定义比较函数参见 https://pkg.go.dev/slices#BinarySearchFunc。如果需要确认该包的引入背景,也可阅读 https://go.dev/doc/go1.21。

总结一下:先保证切片顺序,再同时读取 index 和 found。命中时索引用于读取,未命中时索引用于插入;元素是结构体时,把排序字段准确写进 BinarySearchFunc 的比较函数。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go sync/atomic 怎样用 atomic.Int64 记录有符号偏移量Go sync/atomic 怎样用 atomic.Int64 记录有符号偏移量
上一篇
Go sync/atomic 怎样用 atomic.Int64 记录有符号偏移量
Go slices 怎样按比较函数稳定排序结构体切片
下一篇
Go slices 怎样按比较函数稳定排序结构体切片
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    513次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    572次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    598次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    549次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    366次使用