数据结构与算法08:二分查找和哈希算法

news2024/11/16 17:48:51

目录

【二分查找】

二分查找的特殊情况

【哈希算法】

应用一:安全加密

应用二:唯一标识

应用三:数据校验 

应用四:散列函数

应用五:负载均衡

应用六:数据分片

应用七:分布式存储(一致性哈希算法)

每日一练:搜索旋转排序数组


【二分查找】

二分查找是一种针对有序数据集合的查找算法,查找数据的时候每次都与区间的中间数据比对大小,将待查找的区间缩小为之前的一半,直到找到要查找的元素,或者区间被缩小为 0,因此也叫折半查找算法。如下图所示(left、right、mid 分别表示待查找区间的左、右、中间下标):

可以很明显的看出来,二分查找的时间复杂度是O(logn),随着数据量的增大,查找的效率会很高效。在 42 亿个数据中用二分查找,最多只需要比较 32 次,因为2^32等于42亿多。二分查找最容易理解的写法就是递归代码,如下所示:

// 二分查找:递归实现
func BinarySearch1(a []int, v int) int {
	n := len(a)
	if n == 0 {
		return -1
	}
	return BinarySearchRecursive(a, v, 0, n-1)
}
func BinarySearchRecursive(a []int, v int, low, high int) int {
	if low > high {
		return -1
	}
	mid := (low + high) / 2
	if a[mid] == v {
		return mid
	} else if a[mid] > v {
		return BinarySearchRecursive(a, v, low, mid-1)
	} else {
		return BinarySearchRecursive(a, v, mid+1, high)
	}
}

func main() {
	arr := []int{1, 3, 5, 6, 8}
	fmt.Println(BinarySearch1(arr, 6)) // 3
}

二分查找也可以用循环来实现:

// 二分查找:循环实现
func BinarySearch2(a []int, v int) int {
	n := len(a)
	if n == 0 {
		return -1
	}
	low := 0
	high := n - 1
	for low <= high { //注意:循环退出条件low<=high,而不是low<high
		mid := (low + high) / 2
		if a[mid] == v {
			return mid
		} else if a[mid] > v {
			high = mid - 1
		} else {
			low = mid + 1
		}
	}
	return -1
}

func main() {
	arr := []int{1, 3, 5, 6, 8}
	fmt.Println(BinarySearch2(arr, 6)) // 3
}

并不是所有情况下都可以用二分查找,它的应用场景是有很大局限性的,分析如下:

  • 二分查找需要使用数组结构,链表就不可以,因为二分查找需要按照下标随机访问元素,而链表随机访问的时间复杂度是 O(n)。
  • 二分查找需要数组必须是有序的,如果是个无序的数组,需要先排序,排序的最低时间复杂度是O(nlogn)。关于排序参考:数据结构与算法07:高效的排序算法
  • 二分查找不适合太小或太大的数据,数据量太小可以直接循环遍历;由于数组的内存空间要求必须连续,因此数据量太大的话使用二分查找会比较吃内存。当然这里的太小和太大没有一个固定数值,根据自己的业务情况来灵活判定。
  • 二分查找只能用在插入和删除操作不频繁的场景,比如一次排序多次查找。针对动态变化的数据集合,二分查找不再适用。

如果一个无序的数组没有频繁地插入和删除操作,那么可以进行一次排序,多次二分查找,这样排序的成本可被均摊;如果有频繁的插入和删除操作,要么每次插入和删除之后保证数据仍然有序,要么在每次二分查找之前都先进行排序,这种情况下维护有序的成本较高。

二分查找的特殊情况

上面示例中是比较简单的二分查找,不存在重复元素,实际上二分查找会有很多个特殊情况,比如当数组中存在重复的元素,然后需要查找 (第一个值/最后一个值) (等于/大于等于/小于等于)给定值的元素,实现起来就会不一样。比如还是用上面的二分查找的方法,在一个存在重复元素的数组中查找:

func main() {
	myArr := []int{1, 3, 4, 5, 6, 8, 8, 8, 11, 21}
	fmt.Println(BinarySearch1(myArr, 8)) //7
	fmt.Println(BinarySearch2(myArr, 8)) //7
}

如果想查找第一个出现的数字8,上面的方法就不适用了。 

(1)查找第一个等值的元素

改动思路:当中间元素刚好等于被查找的元素时,需要确认这个元素是不是第一个出现。逻辑改动如下:

  • 如果mid等于0,说明前面没有元素了,那这个元素肯定是第一个出现;
  • 如果mid-1位置的元素已经不等于要查找的元素了,那么当前mid这个位置就是第一个;
  • 如果当前元素的前一个元素也等于被查找的元素,说明当前元素不是第一个出现,说明要查找的元素应该在[low,mid-1]区间,需要更新 high = mid - 1;

改动后的代码如下:

// 二分查找: 查找第一个等值的元素
func BinarySearch3(a []int, v int) int {
	n := len(a)
	if n == 0 {
		return -1
	}
	low := 0
	high := n - 1
	for low <= high {
		mid := (low + high) / 2
		if a[mid] > v {
			high = mid - 1
		} else if a[mid] < v {
			low = mid + 1
		} else {
			//重点需要改造这里
			if mid == 0 || a[mid-1] != v {
				return mid
			} else {
				high = mid - 1
			}
		}
	}
	return -1
}
func main() {
	myArr := []int{1, 3, 4, 5, 6, 8, 8, 8, 11, 21}
    // 查找第一个出现8的元素的位置
	fmt.Println(BinarySearch3(myArr, 8)) //5
}

(2)查找最后一个等值的元素

这个情况和上面的类似,只不过在出现等值元素的时候判断条件稍微改动一下,改动的代码:

if mid == n-1 || a[mid+1] != v {
	return mid
} else {
	low = mid + 1
}

//...
// 查找最后一个出现8的元素的位置
fmt.Println(BinarySearch4(myArr, 8)) //7

(3)查找第一个大于指定值的元素

if mid != n-1 && a[mid+1] > v {
	return mid + 1
} else {
	low = mid + 1
}
//...
// 查找第一个大于8的元素:11的位置
fmt.Println(BinarySearch5(myArr, 8)) //8

(4)查找最后一个小于指定值的元素

if mid == 0 || a[mid-1] < v {
	return mid - 1
} else {
	high = mid - 1
}
//...
// 查找最后一个小于11的元素:最后一个8的位置
fmt.Println(BinarySearch6(myArr, 11)) //7

【问】现在有 1000 万个整数数据,每个数据占 8 个字节,需要快速判断某个整数是否出现在这 1000 万数据中,而且每次查找最多耗费100MB内存空间,应该怎么实现?

【答】将这1000万个整数数据存储在数组中,内存占用差不多是 80MB,符合内存的限制。然后对这 1000 万数据从小到大排序,再利用二分查找算法就可以找到想要的数据了。

源代码:search/BinarySearch.go · 浮尘/go-algo-demo - Gitee.com

【哈希算法】

哈希算法是把任意长度的原始数据通过散列算法转换成一个新的固定长度的数据输出,这个输出值就是哈希值。 转换后的哈希值可以用于检验一段数据或者一个文件的完整性,这里利用了哈希函数里的一个特性:在同一个哈希函数中输入两个相同的原始数据,它们总会得到相同的哈希值;而当这个数据文件里面的任何一点内容被修改之后,通过哈希函数所产生的哈希值也就不一样了,因此就可以判定这个数据文件是被修改过的文件。 

  • 从哈希值不能反向推导出原始数据,所以哈希算法也叫单向哈希算法;
  • 对输入数据非常敏感,哪怕原始数据只修改了一个 Bit,最后得到的哈希值也大不相同;

  • 散列冲突的概率要很小,对于不同的原始数据,哈希值相同的概率非常小;

  • 哈希算法的执行效率要尽量高效,针对较长的文本,也能快速地计算出哈希值。

哈希算法常见的应用:安全加密、唯一标识、数据校验、散列函数、负载均衡、数据分片、分布式存储。

应用一:安全加密

常见的用于加密的哈希函数算法有 MD5、SHA、DES、AES;越复杂越难破解的加密算法,需要的计算时间也越长,比如 SHA-256 比 SHA-1 要更复杂更安全,相应的计算时间就会比较长。

Git 就是采用了SHA-1算法对每一个文件对象都进行了一次哈希值运算,所以每一个提交的文件都会有自己的一个哈希值。在Git里面要找到一个文件对象其实是通过哈希值来寻找的。 

MD5的哈希值是固定的 128 位二进制串,最多能表示 2^128 个数据,必然会存在哈希值相同的情况。尽管如此,如果想通过毫无规律的穷举方法来破解MD5的原数据也是很难的。一般在使用MD5的时候最好加上一个其它的字符串(salt)来改变生成后的MD5,因为很多常用密码的MD5值还是很容易被字典攻击的。

字典攻击就是数据库信息被“脱库”,黑客拿到了加密之后的密文,可以通过“猜”的方式来破解密码,把字典中的常用密码(比如000000、123456)用MD5计算哈希值,然后跟脱库后的密文比对,如果相同基本上就可以找到对应的明文密码。(注意,这里说是的是“基本上可以认为”,因为哈希算法存在散列冲突,也有密文一样但明文不一样的情况)。针对字典攻击,可以引入一个盐(salt),跟密码组合在一起,增加密码的复杂度。

应用二:唯一标识

在一个系统中,用户上传的图片很有可能存在重复,因此可以对重复的图片不再重复上传,从而节省存储空间。那么如果要在海量的图库中查找一个新增的图片是否存在,不能简单的根据图片的名称来比对,其实就算同一张图片的名称修改了,它的二进制数据时不会变的,因此可以通过校验图片的二进制数据来判断图片是否存在。

可以从图片的二进制数据开头取 100 个字节,从中间取 100 个字节,从最后再取 100 个字节,然后将这 300 个字节放到一块,通过哈希算法(比如 MD5)得到一个哈希字符串,用它作为图片的唯一标识,通过这个唯一标识来判定图片是否在图库中,这样就可以减少很多工作量。如果还想继续提高效率,可以把每个图片的二进制数据全量的哈希算法,然后和相应的图片路径都存储在数据库中,当要查看某个图片是不是在图库中的时候,先通过哈希算法对这个图片取唯一标识,然后在数据库中查找是否存在这个唯一标识。

想一想,百度网盘的“极速秒传”是怎么实现的?就是这个原理,相同的一个文件,即使很大,比如一个2GB的电影文件,别人之前已经传过了,那么你再上传也可以实现秒传。比如下面这样,它在前面几秒钟内“正在读取中...” 实际上是在用这个文件的唯一标识和数据库中已有的文件标识进行比对,比对后找出来了,直接把服务器上的那个文件链接指向你的网盘对应的文件夹里面就可以了。

应用三:数据校验 

如果在网上下载一个很大的资源文件,一般都会标注文件的MD5或者SHA1,原因是这些源文件有可能是被分割成了很多块存储在服务器上的,等把所有的小块都下载完成后再组装成一个完整的文件。为了防止网络传输过程中所有小块的完整性,可以通过哈希算法对所有的文件小块分别取哈希值并且保存在种子文件中,当文件块下载完成之后再通过相同的哈希算法对下载好的文件块逐一求哈希值,然后跟种子文件中保存的哈希值比对。

比如这个网页下载GhostWin7的系统镜像,就标注了下载后的MD5和SHA1,

应用四:散列函数

在之前 数据结构与算法05:跳表和散列表 中说过散列表这种数据结构,是使用一个散列函数把一个较大的数据映射到一个较小的散列表中,这里的散列表也叫做“Hash表”,使用的就是哈希的原理。散列函数是设计一个散列表的关键,相对哈希算法的其他应用,散列函数即使出现个别散列冲突,也可以通过开放寻址法或者链表法解决。散列函数更加关注散列后的值是否能均匀的分布。

应用五:负载均衡

载均衡的算法有轮询、随机、加权轮询等,如果要保证在同一个客户端上每一次会话中的所有请求都路由到同一个服务器上,有两种办法:

  • 维护一张映射关系表,存储客户端会话ID与服务器编号的映射关系。客户端发出的每次请求都要先在映射表中查找应该路由到的服务器编号,然后再请求编号对应的服务器。但是存在的问题是太多的客户端会导致映射表很大,浪费内存空间;客户端下线和上线、服务器扩容和缩容 都会导致映射失效,因此维护这样一个映射表的成本很大;
  • 借助哈希算法,对客户端会话ID计算出哈希值与服务器列表的大小取模运算,最终得到的值就是应该被路由到的服务器编号。 这样就可以把同一个客户端过来的所有请求都路由到同一个后端服务器上。

应用六:数据分片

假如有 1TB 的日志文件,里面记录了用户的搜索关键词,现在想要快速统计出每个关键词被搜索的次数,没办法放到一台机器的内存中,怎么解决?

可以先对这些数据分片,然后采用多台机器并行处理,从这 1TB 的日志文件中依次读出每个搜索关键词,然后通过一个哈希函数计算出哈希值,然后再跟 n 取模得到一个值,就是应该被分配到的机器编号,同一个关键词的哈希值相同,就会分配到同一个机器上。接下来每个机器会分别计算关键词出现的次数,最后合并起来就是最终的结果。其实这个思路和上面二分查找的“分而治之”的思想类似,只不过具体解决方式是采用了哈希函数。

针对这种海量数据的处理问题,都可以采用多机分布式处理,借助数据分片的思路可以突破单机内存和CPU 等资源的限制。

应用七:分布式存储(一致性哈希算法)

现在的互联网时代都是海量数据存储,单台机器肯定承受不了这样的数据量级,一般都会采用分布式存储技术将数据分布在多台机器上。比如数据库的分库分表,就可以使用数据分片的思想,通过哈希算法对数据取哈希值,然后对机器个数取模,这个最终值就是应该存储的缓存机器编号。但是存在的问题是,如果将来机器数量扩容了,原来取模的结果就不一样了,所有的历史数据都要重新计算哈希值然后重新搬移到正确的机器上。

比如原来根据10取模, 数据1保存到节点1上, 数据2保存到节点2上;后来扩容后改为根据11取模, 因为分子发生了变化,所以取模的值都变化。

针对这种情况就需要使用一致性哈希算法,关于一致性哈希算法我在之前MySQL分库分表相关的文章中已经说过了,可以点击查看:MySQL分区分库分表和分布式集群_浮尘笔记的博客

每日一练:搜索旋转排序数组

力扣33. 搜索旋转排序数组

整数数组 nums 按升序排列,数组中的值 互不相同,nums在预先未知的某个下标 k 上进行了 旋转。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

示例 1:输入:nums = [4,5,6,7,0,1,2], target = 0,输出:4
示例 2:输入:nums = [4,5,6,7,0,1,2], target = 3,输出:-1

思路分析:题目要求必须使用时间复杂度为 O(log n) 的算法,那么首先应该想到二分查找。设定两个指针left和right,分别指向数组的第一个和最后一个元素,然后用和中间的元素 mid 比较大小。时间复杂度 O (logn),空间复杂度 O (1)。

// https://gitee.com/rxbook/go-algo-demo/blob/master/leetcode/SearchRevolveSortedArray.go
func search(nums []int, target int) int {
	left := 0
	right := len(nums) - 1
	//搜索区间 [left,right]
	for left <= right {
		mid := (left + right) / 2 //获得区间[left,right]的中间位置
		if nums[mid] == target {  //如果刚好命中,直接返回
			return mid
		}
		if nums[mid] <= nums[right] { //mid和right在同一边
			if nums[mid] < target && nums[right] >= target { //target在mid的右边
				left = mid + 1
			} else { //target在mid的左边
				right = mid - 1
			}
		} else { //mid和right不在同一边
			if nums[left] <= target && target < nums[mid] { //target在mid的左边
				right = mid - 1
			} else { //target在mid的右边
				left = mid + 1
			}
		}
	}
	return -1
}

func main() {
	fmt.Println(search([]int{4, 5, 6, 7, 0, 1, 2}, 0)) //4
	fmt.Println(search([]int{4, 5, 6, 7, 0, 1, 2}, 3)) //-1
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/593838.html

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

相关文章

vscode:快捷输入代码片段

背景 每次调试代码输入 console.log() 的时候都会想&#xff0c;有没有什么指令我按下了就能生成这行代码&#xff0c;甚至更多我想自定义的代码&#xff0c;然后就去搜了搜果然有&#xff0c;vscode 提供了自定义代码片段的功能。 步骤 打开 vscode&#xff0c;点击 Prefer…

idea连接HiveServer2

一、 启动hive 启动hive的元数据服务 [aahadoop102 hive]$ bin/hive --service metastore根据你hive的配置方式启动hiveserver2 [aahadoop102 hive]$ bin/hive --service hiveserver2二、配置idea连接Hive服务 打开idea&#xff0c;在项目界面中的右边栏找到Database&#…

学术小白如何写好论文引言

文章目录 1.引言写作逻辑1.1 第一段:从现实出发1.2 第二段:文献综述1.3 第三段:引入研究理论和中介变量1.4 第四段:介绍调节变量的概念1.5 第五段&#xff1a;总结 Hello&#xff0c;宝子们&#xff0c;接下来&#xff0c;我们将持续不断更新一系列围绕论文写作的tips建议&…

vue3学习笔记(附加铺垫知识)

Vue3 1.铺垫知识 1.1ES6 默认导出与默认导入&#xff1a; 按需导出与按需导入&#xff1a; 直接导入并执行模块中的代码&#xff1a; 1.2Promise 回调地狱&#xff1a; 基本概念&#xff1a; 使用promise封装自己的读文件方法&#xff1a; 第一步&#xff1a; 第二步&a…

IP地址和MAC地址

1、MAC地址 MAC&#xff08;Media Access Control&#xff0c;介质访问控制&#xff09;地址&#xff0c;或称为物理地址&#xff0c;也叫硬件地址&#xff0c;用来定义网络设备的位置&#xff0c;MAC地址是网卡出厂时设定的&#xff0c;是固定的&#xff08;但可以通过在设备…

二进制安装K8S

阿里巴巴开源镜像站-OPSX镜像站-阿里云开发者社区 (aliyun.com)https://developer.aliyun.com/mirror/所有节点yum源更换为 &#xff0c;按照aliyun给的容器里面的kubenetes源和docker源&#xff0c;当然最好把之前的centos源也换成aliyun的 所有节点安装docker yum install -…

git 环境配置 + gitee拉取代码

好嘛 配环境的时候 老是忘记这个命令行 干脆自己写一个记录一下 也不用搜了 1.先从git官网下载git 安装 2.然后从gitee拉取代码的时候提示 这是因为换了新电脑没有加入新的公钥啦 哎 所以老是记不住命令行 first &#xff1a; git config --global user.name “Your Name” …

windows下上架iOS应用到appstore

windows下上架iOS应用到appstore 背景步骤申请苹果开发者账号创建唯一标示符App IDs申请发布证书申请发布描述文件创建App并填写信息选择证书编译打包上传IPA到App Store提交审核 尾巴 背景 现在由于跨平台技术的兴起&#xff0c;不使用原生技术就能开发出Android和iOS应用。A…

redis cluster集群常见错误问题记录

错误信息一&#xff1a; [ERR] Node 127.0.0.1:6379 is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0. 这个报错是因为集群配置信息有被修改后导致的&#xff0c;比如某个节点里的redis.conf配置有变…

儿童节小游戏——HTML+JS实现贪吃蛇

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

AI时代,保障安全刻不容缓!

原创 | 文 BFT机器人 01 AI诈骗层出不穷 近年来&#xff0c;深度伪造行为呈现出快速增长的趋势。据统计&#xff0c;2019年至2020年期间&#xff0c;深度伪造网络内容的数量增加了900%&#xff0c;这一令人担忧的趋势预计将在未来数年内持续发展。一些研究人员甚至预测&#xf…

Nova 和 SuperNova:无需通用电路的通用机器执行证明系统

1. 引言 前序博客有&#xff1a; Nova: Recursive Zero-Knowledge Arguments from Folding Schemes学习笔记SuperNova&#xff1a;为多指令虚拟机执行提供递归证明基于Nova/SuperNova的zkVMSangria&#xff1a;PLONK Folding2023年 ZK Hack以及ZK Summit 亮点记Sangria&…

如何把多个pdf合并成一个pdf?常见渠道一览

PDF是一种非常常见的文件格式&#xff0c;它通常用于文档的传输和共享。在日常工作中&#xff0c;我们可能需要将多个PDF文件合并成一个文件以便于管理和浏览。这篇文章将会介绍如何使用在线工具和桌面应用程序来合并PDF文件。 PDF转换器&#xff1a;常用的PDF处理软件&#xf…

Language Models as Knowledge Embeddings:语言模型用作知识嵌入 IJCAI 2022

1.相关工作 1&#xff09;基于结构的知识嵌入 进一步分成基于翻译的模型和基于语义匹配的模型 基于翻译的模型采用基于距离的评分函数&#xff0c;TransE把实体和关系嵌入到一个维度为d的共享向量空间中&#xff1b;TransH,TransR,RotatE. 语义匹配模型采用基于相似性的评分函…

【Unity】简单的边缘高亮

【Unity】简单的边缘高亮 工程文件下载地址 全部文件 使用方法 方法功能On()打开单帧高亮显示On(Color color)打开单帧高亮显示FlashingParams(Color color1, Color color2, float freq)闪烁的参数设置FlashingOn打开闪烁FlashingOn(Color color1, Color color2)从颜色1切换到…

Python实战基础16-模块

Python中的模块 Python提供了强大的模块支持&#xff0c;主要体现为不仅在python标注库中包含了大量的模块&#xff08;称为标准模块&#xff09;&#xff0c;而且还有很多第三方模块&#xff0c;另外开发者自己也可以开发自定义模块。 说的通俗点&#xff1a;模块就好比是工具…

ATA-L系列水声功率放大器-宽频带-大功率

水声功率放大器是一种专门用于水声信号放大的电子设备。水声功率放大器在水声通信中具有重要的作用&#xff0c;它可以将微弱的水声信号放大至足够强度&#xff0c;以提高信噪比和传输距离&#xff1b;同时&#xff0c;还可以进行信号处理和优化&#xff0c;以及温度控制等功能…

计算机毕业论文内容参考|基于java的电子产品垂直电商平台的设计与实现

文章目录 导文文章重点摘要前言绪论课题背景国内外现状与趋势课题内容相关技术与方法介绍导文 计算机毕业论文内容参考|基于java的电子产品垂直电商平台的设计与实现 文章重点 摘要 本文基于Java技术,设计并实现了一个电子产品垂直电商平台。该平台主要针对电子产品市场,提…

ESP32-C2系列开发板

C2是一个芯片采用4毫米x 4毫米封装&#xff0c;与272 kB内存。它运行框架&#xff0c;例如ESP-Jumpstart和ESP造雨者&#xff0c;同时它也运行ESP-IDF。ESP-IDF是Espressif面向嵌入式物联网设备的开源实时操作系统&#xff0c;受到了全球用户的信赖。它由支持Espressif以及所有…

充电桩测试设备TK4800充电机(桩)现校仪检定装置

TK4800是一套专用于检定电动汽车非车载充电机现场校验仪和电动汽车交流充电桩现场校验仪的装置&#xff0c;针对新能源汽车充电动态特性进行设计&#xff0c;支持稳态电能计量&#xff08;针对恒定负载&#xff09;和动态电能计量&#xff08;针对波动性负载&#xff09;两种模…