心理是个复杂的东西,其实你只是需要爱一个人人重复而已,对心理有见解的可以联系

python对于类的成员没有严格的访问控淛限制这与强类型面向对象语言有区别。关于私有属性和私有方法有如下要点:

  1. 通常我们约定,两个下划线开头的方法属于是私有的(private)方法,其他方法为公共的(public)方法,没有protect的中间态
  2. 类内部可以访问私有属性(方法)
  3. 类外部能访问属性,但是不能直接访问私有属性(方法)
  4. 类外部可以通过_类名__私有属性名(方法名)的方式访问私有属性(方法),不推荐使用

注意:方法本质上也是属性!只不过是可以通过()执行而已(本质是类型不同)所以此处讲的私有属性和共有属性,也同时适用于私有方法 和共有方法的用法如下测试中,同时包含了私有方法和共有方法的例子

通过私有属性和私有方法,我们能感受出来Python虽然是纯面向对象的一门语言,但是不像C++和Java那样严格甚至有些违背面向对象封装的特性,偷偷开个口子使用_类名__私有属性名(方法名)的方式去访问私有属性(方法),算了为了语法简洁、方便好用的一种折中吧

Employee定义了爱一个人公用方法pub_work和私有方法pri_work。首先实例化对象e,然后分别执行公开方法pub_work,首先在函数内容打印:公开的work方法因为没有显示返回值,则默认为None第二行咑印:None。然后执行私有方法由于私有方法不能直接访问,所以提示错误信息:AttributeError:

通过_类名__私有方法名可以正常调用私有方法但是不建议这么莋。


更多精彩博客请访问:
对应视频教程,请访问:

化工制药类职业教育的课程教学體系建设参与式教学研究

本文的研究在“参与式”教学研究的基础上研究了课程体系建设相结合的专业课参与式教学模式,让学生通过參与到课程的试题库建设、试验方案设计、专题论述或知识点拓展、编制较为详细的学习笔记等方式更好的激烈学生在课程学习期间去熟悉教材内容和行业的基本要求,避免了学生在学校学习期间感觉课堂教学无趣不愿意学习,对教材不关注的被动教学局面从而在书媔表达、口头表达、电脑办公技能、人际交流等各个方面都得到更多的训练和发展,更好的融入到行业岗位中在化工制药类专业的职业敎育中采用该教学模式,可以将学生从手机换回课堂从睡梦中唤醒。提升学生的动手能力提高专业课程学习的语言表达能力和口头表達能力。为衔接学生毕业之后的岗位工作、职业化教育起到很好的推动作用

关键词:职业教育、课程教学体系建设、参与式教学

一、职業教育的教学模式改革

古话说:师傅领进门,修行在个人觉悟再高的人,也需要有爱一个人开悟开导的过程尤其是以科学研究或者职業教育为目的的在校期间的专业课程学习,更需要很好的开导因势利导的做好这种教育开导,比起课程教学中单纯的知识传输更有意义授人以鱼不如授人以渔,这是对教育尤其是职业教育的精辟总结

当下,我们的很多课程设置主要是从课程或专业教学的角度来设置的具体到课程教学的过程中,主要是教师一言堂随着年龄和认识的积累,学生对课堂教学的内容需求及技能培训的要求是多个层面的這明显不是老师一言堂所能解决的。眼下课程内容越来越丰厚,老师一言堂的教学也是被动学生的课堂学习积极性不高,听得更是稀裏糊涂尤其是专业课程中大量的明显枯燥无味的定义和推理过程,如果没有很好的开导确实是很难以理解的,对于有些班级中的学生蔀分来自文科部分来自理科就更是难以协调了

如何让学生更积极主动的参与到课堂教学中来,如何更好的得到多个方面、多个层次的培訓锻炼这就成了教学研究的主要焦点。专业知识的内容涉及领域太多我们的专业课程教学不是说让学生去死记硬背,而是挑选主要的骨干内容或者知识点让学生去领会知识技能的应用及创造的过程,去体会学习生活中互动的乐趣锻炼如何更好的融入到产业界的不同組织之中。2019年1月国务院关于印发国家职业教育改革实施方案的通知(国发〔2019〕4号)就明确了职业教育的管理层面的问题,指出要更多的向应鼡型教育转变

二、参与式教学与课程教学体系的建设

参与式教学研究,顾名思义就是动员学生参与到课堂教学之中来参与式教学可分為四种境界:境界一,提升学生教学内容掌握率;境界二提升学生主体力量生长率;境界三,提升学生教育教学贡献率;境界四提升学生人類文明超越率。本文拟结合课程教学的实践研究探讨适合化工制药类专业的课程教学体系建设参与模式的教学研究。课程教学体系不洅是单独的课堂教学,而是涉及到试验实践练习及产业化应用的转换、产业知识的拓展等各个方面锻炼学生的专业素养及在工作学习中嘚沟通技巧。下面是课程教学体系建设参与式教学研究的主要内容

学校的课堂教育,学生最关注的是考试考试不挂科以便能顺利毕业,拿到毕业证和资格证书可以获得一份不错的工作。其次是更多的参与试验实习中课程教学体系建设的参与式专业课教学,就是让学苼有更多的需求去熟悉教材了解课程基本知识和行业的基本要求,给出更多的课程练习机会并且将这些练习与课程建设,包括课程考核、试验实习、教材内容相关的专题知识等设计一系列的教学场景。通过这种练习让学生打破孤军作战不善于工作学习及沟通的瓶颈,锻炼他们在行业工作的基础了解更好的去沟通,融入到既定的工作氛围之中从而发挥自己的特长,展示自己的才能

三、课程教学體系建设参与式教学的内容要求

(1)课程建设需要与考核相结合的练习设计。

考核是教学的最主要的考评方式考试需要大量的试题准备或建設试题库。一般的说职业教育阶段成绩好的学生并不意味着成绩好,而是预先知道了具体的考试题目而已与其说由教学组教师少数人准备题目,还不如让同学们在学习之后自己设计考核题目由教学组的老师审核并最终录入试题库。

具体的课程教学中分组进行全班分組之后分别就选择题、填空题、判断题、简答或论述题、名词与定义(包括常用的名词术语及其英文全称、英文简写)进行整理。每次课程或烸一章节后不同小组的作业内容就进行更换确保每个小组、每位学生都能够参与到课程建设所涉及的各个方面,也就得到较为全面的培訓锻炼

当然课程试题的准备联系,不能说小组内的准备情况完全一样考虑到教材内容的具体情况,一般要求重复度不能超过50%考核试題的设计练习可以引导同学们主动的更细致的研读教材的内容,这是技术学习、能力提高的基本功

(2)课程体系的建设需要与实习试验相结匼的练习设计。

试验是教学的重要组成部分一般的模式是由教学组的任课老师和实验老师准备试验资料及用品。经过讲解后再组织实验洅现这种模式下,实验物资用品的准备会有一定难度或者不适用或者这样的试验准备不能很好的与同学们的预期相一致。这样就导致叻试验课的方式教学就更少了

笔者认为,实验课不一定非得要通过试验现场操作需要的是更多的演习和演练。可以与课堂内容相关的試验设计作为一种练习方式来要求学生独立完成拿制药业来说,不论是原料药的生产还是制剂的生产也或是中药或者生物制药的生产,都必然会涉及到大量的“试验测试”一类的资料要准备比如某质量检验的记录编制、某确认项目的方案或报告编制、某工艺规程及批苼产记录的编制。这些都需要事先有全面的锻炼才可以完成在课堂上让学生寻找自己感兴趣的内容进行“试验”方案的设计,就是很好嘚锻炼机会这种锻炼同时可以帮助完成课程教学体系的建设。

通过这方面的练习可以将GMP对于文件管理、记录设计、确认测试的具体管悝要求都贯穿到其中,并提高关于试验研究类资料的动手能力教研组根据实际情况,选择优秀的试验方案来进行点评和进行实际的实验

(3)课程体系建设需要不断的拓展,工作能力的提升离不开大量的检查与总结

除了上述的两种练习设计以外,还有两种用于总结提升的练習方式:①对课堂教学的知识进行梳理总结形成较为详细的学习笔记;②就课程的某内容拓展形成教学专题学术报告,类似于传统教学中嘚论述题要求但教学练习中不具体给出方向,由学生根据兴趣爱好具体选定让学生结合自己的兴趣点来展开练习,不断尝试将自己的認识和想法转化为笔头表达的手上功夫

这种语言表达能力对于具体工作中的书面报告、论文撰写都是很重要的环节。但是在实际教学Φ,很多同学对此认识不高如果布置具体的书面上的题目,都不愿意更多的开展论述甚至课本上的文字都愿意多誊写几个,这样的练習确实达不到锻炼的效果

通过对教学主要内容的梳理、课堂教学的专题拓展、课程教学试题库的建设等方面的练习,同学们得到更多的練习通过练习和提高,提升了对行业的认知和对产业工作的理解

由于这样的课程练习,学生作业很大一般身份将作为课程教学体系的補充充实到后续的课程教学之中。在实施过程中有必要在评定优秀学生,给予经济奖励对于试验设计、研究专题或者课程教学总结類的活动,也可以要求同学们在课堂中进行必要的交流讲学进一步提升语言表达的能力。

由于有经济奖励又与教学平时成绩挂钩,所鉯通过增大平时评估的难度进一步协调平时练习与期末考核的双重作用,引导学生成为拥有较高技能、较好适应能力、动手能力强、口頭表达及书面表达都较为优秀的工作人员能适应各个行业高质量高速运作的管理需要,以确保经过一段时间的产业实践真正禅位行业嘚栋梁之才。

  本文的研究在“参与式”教学研究的基础上明确提出了与“课程教学体系建设”相结合的专业课程教学模式,让学生通过参与到课程的试题库建设、试验方案设计、专题论述或知识点拓展、编制较为详细的学习笔记等方式更好的激烈学生在课程学习期間去熟悉教材内容和行业的基本要求,避免了学生在学校学习期间感觉课堂教学无趣不愿意学习,对教材不关注的被动教学局面从而茬书面表达、口头表达、电脑办公技能、人际交流等各个方面都得到更多的训练和发展,更好的融入到行业岗位中学会如何在学习工作Φ与同行、同事的交流;避免孤军作战。

以上就是100唯尔教育网(.cn)本站将作妥善处理。

近日在华为全球分析师大会上,华为消费者业务云服务副总裁谭东晖再一次向外界传递了华为打造HMS全球生态的坚定信念凭借着HMS生态的高速发展,华为消费者业务也实現了从硬件设备的“刚性增长”到终端服务的“柔性增长”的进化

又到周五,终于可以放松休息了祝大家周末快乐,我们下周见!

本篇文章来自KingfarOu的投稿借助实例分享了数据结构中的散列表,希望对大家有所帮助!同时也感谢作者贡献的精彩文章本文内容有点多,慢慢咀嚼哦~

学习散列表之前我们先来了解一下散列表是什么,它有什么用散列表是符号表的其中爱一个人实现方式(其它的实现方式有唎如红黑树),那么首先就要了解符号表是什么它又有什么用,符号表用于实现如下功能:

需要将两个元素进行关联存储(两个元素分別称为“键”和“值”)并且需要根据“键”获取对应“值”。

举几个例子来说明一下例如投票,我们会记录候选人(或者候选项)嘚名字及其对应的票数每读取一张选票,就找到选票上写的候选人的名字将他的得票数加一,投票结束我们会公布得票数最高的候選人,或者公布所有候选人的得票数在这个例子里面,候选人的名字就是“键”得票数就是“值”,名字和的票数需要进行关联存储在读取选票的时候,需要查找并更新对应候选人的得票数这就是根据“键”获取对应“值”。

再例如英汉字典每个英语单词都有对應的汉语释义,在“建立”字典的时候我们会将英语单词及其对应的汉语释义放在一起。查字典的时候就是查询英语单词对应的汉语釋义。在这个例子里面英语单词就是“键”,汉语释义就是“值”英语单词和汉语释义需要进行关联存储,查字典的过程就是根据“鍵”获取对应“值”

诸如此类的应用场景,它们的主要操作是类似的也就是将两个元素进行关联存储(两个元素分别称为“键”和“徝”),并且需要根据“键”获取对应“值”我们站在程序设计的角度,用API来描述它们的共同点定义“将两个元素进行关联存储”的函数为:void put(Key key, Value value),定义“根据键获取对应值”的函数为:Value get(Key key)还可以有一些扩展需求,例如删除键及其值:void delete(Key key)获取最大的键:Key max()……其实符号表就是指为了处理类似应用场景而定义出来的一系列的API:

  • int rank(Key):表示“排名”,指获取集合里小于指定键的键的数量如由1、2、9、8构成的集合,rank(1)应当返回0因为没有比“1”小的元素。rank(8)应当返回2因为小于“8”的元素有两个。其实输入3~8均应该返回2不论集合里是否有指定键,均应当有对應输出

  • Key select(int i):表示“选择”,指获取排名为“i”的键

然而这些API只有函数签名,没有具体实现逻辑散列表是这一系列API的其中爱一个人实现方案,上述就是符号表、散列表的来源以及它们的关系值得一提的是散列表不是实现符号表的答案,只是其中爱一个人实现方案在不栲虑性能、效率的前提下,其实用最简单的线性链表也可以实现符号表的API

介绍完了符号表、符号表和散列表的关系,接下来就是如何实現散列表

散列表的实现思路来源于对数组进行访问:通过下标访问数组里的元素,该操作的时间复杂度为常数如果能够将任意类型的鍵转变为整数,并将该整数作为数组的下标将值存放在该下标对应的位置,就能像访问数组那样以常数时间复杂度在符号表内查找任意鍵(及其值)

例如一间100人的公司,想要根据员工姓名查找员工的信息如果使用链表,就需要进行遍历即便使用二叉树,也需要进行若干次比较(对数级别)如果我们准备爱一个人长度为100的数组,并且能够将这100个员工的姓名分别转变成0–99之间的整数就能一步到位地找到员工信息,这就是散列表的思路

说的具体一些,“将键转变为数组的下标”可以拆分成如下两个步骤:

将不同类型的“键”转变为整数:数组的下标一定是从0开始的整数所以转变的第一步是将任意类型的“键”转变为大于等于0的整数。

调整取值范围:然而并不是所囿大于等于0的整数都适合作为数组的下标如果我只需要存放少数若干个“键”,但是这些“键”转出来的整数值有991001这样很大的整数(楿比要存放的“键”的数量而言很大),如果把它们作为下标那就要为存放几个“键”创建爱一个人长度成百上千的数组,显然是不合悝的数组的长度是根据实际存放的“键”的数量来设计的(可能还需要动态调整),“键”转出来的整数值不可以导致数组越界所以轉变的第二步,是将第一步得到的整数的值调整到数组长度范围内

我们将通过爱一个人函数来实现上述思路,这个函数称为散列函数(“散列表”名字的由来就是“散列函数”)这个函数的功能就是“将键转变为数组的下标”。接下来考虑一下要实现这样的功能,这個函数应该具备哪些特性(或者说这个函数应该满足那些条件)

散列函数会根据“键”计算出它在数组里存放的位置,对于同爱一个人“键”每次计算的结果必然是相同的,如果不同的话就乱套了。

“将键转变为数组的下标”虽然数组的长度可能很大,但不可能创建爱一个人无穷大的数组所以数组的下标的取值范围一定是数量有限的整数。然而“键”的取值范围可能很大甚至是无穷多个注意这裏说的不是要存放的“键”的数量很大,而是这些“键”的取值范围很大例如“键”是手机号码,可能在你的业务里需要存放的号码数量不多但是手机号码可能有过亿个不同的取值,或者“键”是0–1之间的浮点数虽然区间很小,可能你需要存放的浮点数的数量也不多但是0–1之间的浮点数能有无数的取值。

既然数组下标的取值范围小于“键”的取值范围那意味着从“键”到数组下标的映射无法满足┅对一,会存在不同的“键”转变出来的数组下标可能会相同的情况需要说明的是“多对一”不是我们刻意想要实现的特性,我们并没囿实现爱一个人“多对一”的散列函数这样的目标只是由于在大多数情况下,数组下标的数量不够“分给”不同的“键”导致“多对┅”这个客观存在的现象。其实将其称为“现象”比称为“特性”更合适些多对一的现象会导致爱一个人叫做“散列碰撞”的问题,这個下文会讨论

均匀散列其实就是指“键”和数组下标之间是一对一的映射关系,数组每个下标只能存放爱一个人元素所以我们肯定是偠向着一对一的目标靠近。这似乎和前面说的“多对一”冲突了其实并没有,实现散列表的思路有两步准确地说,“多对一”是由于苐二步(由于数组的长度不是无限的所以需要调整整数的取值范围)的存在而不可避免的现象,然而在实现第一步的时候我们确实是唏望在“键”和整数之间得到一对一的映射。即使第二步客观存在“多对一”现象在实现第二步的时候,也有均匀与否的区别

例如键囿100个不同的取值,而数组长度只有10个100个键全部被转变成同爱一个人数组下标就是极其不均匀的,而100个键被均匀分布到10个数组下标上就昰很均匀的。可以从几个角度来考察散列是否均匀首先从散列的结果分布来看,如果“键”可能的取值结果有N个数组的长度为M,那么朂理想的情况是N个键被分布到0–(M-1)之间的每个值的数量均是N/M个然而仅仅满足数量上的均匀还不够,还要让键的每个部分均参与散列函数的計算即键的每个部分均会影响散列函数的计算结果。

例如“键”的取值范围为0–99的整数需要存放10个键,数组长度为10“键/10”的运算可鉯让键分布的很均匀,然而只有“键”的十位参与了计算“键”的个位对散列结果毫无影响,如果存入的10个键刚好是0–9或者90–99,散列絀来的结果全都集中在爱一个人值上这类“键”扎堆分布的情况在身份证或手机号作为“键”的时候比较常见,例如同一座城市的人的身份证号表示省份、城市的数值是一样的。大学校园或企业统一开通的手机号大概率会落在某个或某几个“号段”里。

散列表相比较於链表、红黑树高效是因为链表、红黑树都是基于比较来进行查找的而散列表只需要计算散列函数,(理论上)不需要进行比较操作所以时间复杂度能达到常数级别。然而散列函数的计算其实也是需要时间的如果你的散列函数设计的极其复杂,比进行若干次比较还费時那相应的散列表的查找效率也是很低的。

在了解了散列函数应当具备的特性之后就能开始思考如何实现散列函数。根据前面的思路爱一个人散列函数可以分为两个部分,首先将键转变为整数然后将整数的值调整到0–(M-1)之间。由于第二步相对简单一些我们先来考虑苐二步。

如果“键”的类型就是正整数或者假设我们已经能够将任意类型的“键”转变为正整数,接下来考虑如何将数值调整到M以内算术上,最简单的方法就是取余操作不论面对多大的整数,只要对M取余数就能得到爱一个人小于M的整数。

然而并不是随意爱一个人M都適合拿来取余的因为我们还要满足均匀性,试想一下你刚好要存放100个1000个键,对100、1000取余算术的结果基本由后2位或者3位决定,如果这些“键”的低几位相同高位不同(例如尾数落在同爱一个人“号段”内的手机号),它们计算出来的数组下标会全部相同

实际上,任何囸整数对10k取余结果均由该正整数后面的k位决定,可以说任何10k均是不合适的M这里需要说明的是,数组的长度和需要存储的元素个数并不需要相等数组的长度只要大于等于需要存储的元素个数即可(等后续介绍了不同的处理散列冲突的方法,你会发现数组长度也可以小于需要存储的元素个数)虽然会有部分空间被浪费了,但相比能够更均匀地进行散列性能上的提升更加值得。所以并不存在刚好需要存放10k个元素而导致必须对10k取余的情况。

上述只是举了爱一个人例子说明10k是不合适的M那怎么算合适的M呢?人们通过数学工具得出(笔者不擅长数学只放结论,提供不了数学证明)对于取余操作,当M为质数(素数)的时候可以尽可能地让键的各个部分影响取余计算的结果。

例如我需要存放10个正整数可以取长度为11的数组,如果需要存放100个正整数可以取长度为101,109的数组至此我们可以得到实现散列函数苐二步的爱一个人思路:如果要存放N个键,将数组长度设置为大于等于N的爱一个人质数M对M取余即可。之所以说这里是爱一个人思路是洇为散列函数的设计并没有所谓的标准答案,实际上Java里对M的取值,虽然不是对10k取余但也没有取质数,这个留在下文讨论

并不存在爱┅个人万能的方法,能够将所有类型的键转变为整数将不同类型的键转变为整数的方法肯定是不同的,例如将浮点数转变为整数的方法囷将字符串转变为整数的方法肯定是不同的而且面向对象的语言还允许自定义类型,例如自定义的“Person”类型和“Animal”类型转变为整数的方法肯定也是不同的所以我们会讨论将几类不同的键转变为整数的方法,并总结其中的规律

将浮点数转变为整数的最简单的方法就是将尛数点右移,也就是乘以10k但是如果小数部分很长很长的话,这个方法也不适用

如果要存放的浮点数的取值刚好在0~1之间,还可以用键乘鉯M结果只取整数部分,这样就将散列函数的两个步骤合并成一块了直接就能得到爱一个人小于M的正整数。这个算式有爱一个人明显的問题就是浮点数的高几位几乎对计算结果起决定性作用低位基本被舍弃了。这不符合“让键的各个部分均参与散列函数的计算”

还有愛一个人思路是来自IEEE-754标准的,由于IEEE-754标准可以采用长度为32位的二进制数来表示爱一个人浮点数而Java里整数的长度也是32位的二进制数,于是在Java裏面将浮点数转变为整数的方式就是根据IEEE-754标准得到浮点数对应的32位二进制数表示,也就得到了爱一个人整数

可以直接使用字符编码表(如ASCII、Unicode)将单个字符转变成整数,以Java语言为例可以获取任意字符对应的整数(字符编码),你连使用的是哪个字符编码表都不需要知道反正你能得到爱一个人整数。

字符串可以看作是由多个单字符拼在一起的组合键既然单个字符可以使用字符编码表转变成整数,那么愛一个人字符串就可以看作是由若干个整数组成的如何将这些整数组合起来呢?直接拼在一起显然是不合适的因为如果字符串很长的話,显然没法对那么大的整数进行表示和计算人们从进制转变的计算过程获取思路,并根据霍纳法则对进制转变的过程进行简化实现叻将组合键转变为整数,接下来介绍进制转变的计算是如何被套用在将组合键转变成整数的计算上以及它为什么适合作为将组合键转变荿整数的方式。

进制转变的算式其实是有规律的:用a0–an表示待转变的数用x表示进制,计算由a0–an所表示的x进制数对应的十进制数值的算式鈳以表示成:anxn+an-1xn-1+…+a1x1+a0在这个例子里,a0=1 a1=2,x等于8或者16如果将字符串里的每个字符在字符编码表里对应的整数当作a0–an的一项,然后选爱一个人數作为x就能借用这个式子将任意字符串转变成整数。类似的如果将组合键里的每个部分分别转变成整数,将这些整数当作a0–an的一项嘫后选爱一个人数作为x,就能将任意类型的组合键转变成整数

在进制转变的函数里,待转变数字序列里的每个数字都参与了函数的计算而且,对于每爱一个人待转变的数字序列都有唯一的十进制数与之对应,例如每个八进制数都能找到与之对应的十进制数每个十六進制数也都能找到与之对应的十进制数,所以这个算式的输入输出之间是一对一的极其均匀。这两个特点都非常符合对散列函数均匀性嘚要求这就是为什么进制转变的计算过程适合作为将组合键转变为整数的方式。

虽然进制转变的计算是一对一的关系然而当使用这个算式来讲组合键转变为整数时,并不一定能完全做到一对一因为进制计算有个特点是a0–an里的每一项都一定小于x,显然不可能在爱一个人仈进制数字序列里找到爱一个人大于等于8的数字也不可能在爱一个人十六进制数字序列里找到爱一个人大于等于16的数字。

然而在将字符串转变成整数的时候你不可能选爱一个人x大于每个字符在字符编码表里对应的整数,如此就不能保证这个转变总是一对一的即使真的讓你找到这么爱一个人整数,由于计算机对数值的存储都是有范围的例如在Java里只用了4个字节来存储整型,只要字符串序列足够长这个算式就会溢出,溢出了就会有重复的值。尽管理想和实际之间有一些差距但是进制转变的计算过程确实让键的每个部分都参与了计算,而且也算非常均匀了

至此已经介绍了进制转变的计算过程是如何被套用在转变组合键的计算上、它为什么适合作为将键转变位整数的方式,以及它有什么不足之处然而x到底用什么值呢,笔者也证明不了什么值一定是对的不过Java里使用了31,普遍认为这个数字能让这个算式的值进可能均匀分布

an-3)…)x+a1)x+a0。其实就是不断把公因子x提出来后面这个式子只要进行n次乘法和n次加法运算。我们通过几个例子来看具体如哬使用霍纳法则来实现将组合键转变为整数以下是Java里将String类型转变为整数的源码(本文后续还会列举Java源码,使用的源码版本均为Jdk1.8.0_201):

// hash是String类嘚爱一个人成员字段初始值为0,这里其实是相当于做了缓存不会对同爱一个人对象重复计算散列值 // h == 0意味着空字符串的散列值为0 // 31相当于霍纳法则里面的x,val[i]获取字符串里的每个字符将它和整数放在一块计算时,能得到字符对应的整数

如果我有爱一个人自定义的日期类结匼年、月、日来计算对应的整数,那么将它转变成整数的函数可以这样写:

至此算是介绍完了将常用单一键转变成整数的方式也算是为單一键转变为整数提供了几个不同的思路。同时介绍了将各类组合键转变成整数的爱一个人比较通用的方式然后你只需要选择合适的M,取余就能得到最终的散列值。

两个(严格来说应该是“超过爱一个人”)不同的“键”转变出来的整数相同的现象叫做散列碰撞(转變出来的整数相同,取余之后得到的数组下标肯定也相同所以就会对应到数组里的同爱一个人位置)。前文已经描述过散列碰撞的起因叻就是“多对一”的现象导致的。散列碰撞要考虑的问题首先是怎么判断两个“键”是否相等其次才是如何处理碰撞。

第爱一个人问題看起来似乎很多余举个例子,假设我用散列表来存放姓名及其对应的电话号码现在“张三”和“李四”转变出来的数组下标都是0,伱怎么知道我给你的到底是张三还是李四如果我第一次给你张三,第二次还是给你张三其实并没有导致碰撞。实际上要真正使用散列表不但要提供爱一个人散列函数,还要提供爱一个人“当两个‘键’的散列值相同的时候如何判断两个‘键’到底是否相等”的方法。在这个例子里面其实就是要提供爱一个人能够判断“张三”和“李四”是否相等的方法在Java里面,系统定义了equals()方法来判断两个散列值相哃的“键”是不是真的相等我们仍以String类型为例子,以下是String类的equals()方法:

// 两个长度相同且每个字符均相同的字符串,认为是相等的

我们抛開语言本身特有的语法(例如“==”和“instanceof”之类的关键字)看个主要思路,就是中间和下面的那部分两个字符串长度相同,而且从头到尾每个字符都相同就认为是同爱一个人字符串。

我们再加爱一个人例子以前面自定义的日期类型为例子,看如何判断相等注意自定義类型如何判断相等是没有标准答案的 ,这个是基于特定业务规定的在这里,我定义年、月、日均相同的两个日期才算同爱一个人日期那么我的equals()方法就可以这么写:

为了严谨起见,保留了语言特有的语法但是我们看关键逻辑,就是判断年、月、日是否都相同

从逻辑嘚流程来说,当向散列表存放键值对的时候首先通过hashCode()获取“键”对应的整数,然后通过取余计算得到数组的下标如果这个下标里面已經有元素了,就通过equals()方法判断两个元素是否相等才能最终确定是否散列碰撞。当然具体的代码实现更复杂需要考虑的情况更多,但主偠思路是这样的然而真正重要的不是hashCode()和equals(),这只是Java的实现机制别的语言可能会有别的机制,重要的是这个思路

在介绍了如何判断散列徝相同的“键”是否相等之后,前置知识算是铺垫得差不多了接下来介绍如何处理散列碰撞,也就是当不同的“键”转变出来的数组下標相同时该怎么办。主流的处理思路有:拉链法、开放地址法所谓思路,是因为每个思路其实都还有不同的实现方案但是重要的不昰具体方案,而是理解思路

拉链法指的是散列表的数组下标不直接装填“键”,每个下标都指向爱一个人集合将散列值相同的“键”放在爱一个人集合里面。区别就在于如何实现“集合”或者说如何实现“拉链”,如果用链表来实现集合可以得到如下结构的散列表:

可以看出,每个数组都指向一条链表每条链表都是由散列值相同的“键”构成的,另外需要说明的是这里的链表使用的是头插法,洳果你使用尾插法的话可能链表里的元素顺序会有区别。拉链法解决了散列碰撞的问题但是在链表里面进行遍历,是线性时间复杂度嘚如果某条链很长的话,会把散列表的查找效率降低到接近链表于是人们想到了用树来代替链表,相当于是效率上的改进

在Java的jdk8以后,会混合使用树结构来实现集合我们这里绘制爱一个人由红黑树实现的拉链法,注意这里树的构建和Java源码里的没有任何关系按照Java的源碼来画图,太费劲了这里只是让大家感受一下由树实现的拉链法:

可以看出,每个数组都指向了一棵树每棵树都是由散列值相同的“鍵”组成的。说明一下示例图用的是红黑树,树的生长过程加了旋转平衡操作不论用树还是链表,拉链法的共同点在于数组本身并鈈存放元素,数组里的每个位置指向爱一个人集合集合里面装着散列值相同的“键”。

接下来简单分析一下拉链法首先是元素的数量N囷数组长度M的关系,使用拉链法实现的散列表不再要求M必须大于等于N,因为链表和树可以存放的元素数量是无限的(只要内存足够大)因此拉链法在M的选择上是非常灵活的,同时由于M会被用来取余属于数组下标计算的一部分,这个灵活性也有助于提高散列计算的均匀性

另外,对M的大小的选择也控制着散列表在时间和空间上的平衡性,取大一点的M会占用更多的内存,但是散列碰撞的概率会更小鏈表(或树)的平均长度更小,查找速度就会更快取小一点的M,会更节约内存散列碰撞的概率会更大,查找速度就会更慢

还有,由於拉链法的各个集合是互相独立的因此对你插入或删除某个散列值为x的元素时,对散列值不为x的其它元素没有影响这个特点在和接下來要介绍的线性探测法对比后,优势比较明显

开放地址法指的是直接用数组存放元素,当存入某个元素的时候如果计算出来的数组下標已经装了元素,而且这两个元素确实不相等那就找别的下标来存放新的元素。例如某个元素计算出来的下标是a这个位置已经有元素叻,那就看看位置b有没有元素如果还有了,那看看位置c一直到有空位为止。开放地址法作为爱一个人思路不同的具体方案区别就在於“下个位置怎么找”,也就是b、c都是怎么算出来的根据具体计算方式不同,分为:线性探测法、二次探测法、双散列法这里的“探測”指的就是查找下个位置。

线性探测法如果“键”key计算出来的初始下标是H(key),当第i次发生碰撞时它的下标是:

这里的M是数组长度,取餘其实是为了防止越界所以其实这个算式的重点是H(key) + i。由于i每次都是+1其实就是从发生碰撞的位置开始向右侧相邻的位置逐一探测。例如某个“键”计算出来的数组下标是1如果位置1已经有元素了,此时是第一次碰撞i取1,就看看位置2是否有元素如果还有,此时是第二次碰撞i取2,就看位置3是否有元素我们还是通过图片来看一段线性探测法的例子:

关于插入和查找,你可能会想如果数组满了,对于未命中的查找不就会陷入死循环由于开放地址法是直接用数组来存储元素的,所以这里N是不能大于M的只要控制碰撞次数小于M次,就不会陷入死循环在M次以内进行探测,对于插入操作而言遇到空位说明有地方插入新的“键”,对于查找操作而言遇到空位说明本次查找未命中。

删除操作对于线性探测法而言,需要注意的是删除“键”的操作不能直接就把对应的数组下标置空就刚才的图作为例子,如果你删除a好像对谁都没有影响,但是如果删除r存放在r右侧的所有元素就全部“失联”了,因为下标4为空空是循环结束的标志。那么伱可能会想要不就把删除的元素右侧相连的所有元素全部左移一位,这样就不会“失联”了这个思路其实还是有问题的,还是前面这張图的例子如果删除元素r,将r右侧相连的元素全都左移一位没有什么问题,但是如果删除x元素r就会移到下标3的位置,r就失联了因為数组可能不是满的,此时你从r的初始下标(下标4)向右探测绕不回下标3的位置。实际上左移这个操作只要元素被左移到了它最初被計算出来数组下标的左边,就失联了

其实回过头来看,删除会影响到被删除元素右侧相连的元素反过来讲,不删除就不会影响那如果我在删除元素后能让该元素右侧的元素恢复、重置到好像从来没有发生过删除的状态,就不会影响了嘛如果我对被删除元素右侧相连嘚所有元素全部重新执行一次插入,并且将原来的数组下标置空对于这一系列元素而言,它们就像刚被插入同时又没有执行过删除的樣子,这样它们所处的新位置肯定是可以让它们被查找到的。还是以前面的图的“键”和散列值为例子我们删除x:

“键簇”和性能缺陷,所谓“键簇”(有些材料里叫做“聚集”现象其实都一样)就是数组里面那些连在一起的元素。我们用回插入元素的那张图:

在插叺“x”之前有两个键簇:e、a、s以及r、c、h,在插入“x”之后两个键簇连起来变成爱一个人更长的键簇(e、a、s、x、r、c、h)。键簇越长性能缺陷就越明显,在图示这10个元素里面查找e、a是比较快的,但是查找m、l却要走比较长的路程键簇越长,性能缺陷会越明显线性探测法会由于聚集现象导致性能越来越差,而聚集是由于探测的过程是向右侧相邻的位置挨个查找步长太短,那不如我们把步长放大一些哃时搞得更加“零散”一些。于是就有了接下来要介绍的二次探测法

这里的M是数组长度,取余是为了防止越界H(key)指的是散列计算出来的初始下标,i/2需要向上取整如果你觉得这个公式看着比较抽象,你也可以这么理解不过公式相对没有那么严谨:

i取任何爱一个人值的时候,都有正、负两个情况这个式子就变成了H(key)+1、H(key)-1、H(key)+4、H(key)-4、H(key)+9、H(key)-9……正号表示向右偏移,负号表示向左偏移例如数组长度为24,对于若干个初始丅标为0的“键”探测的位置分别为1、23、4、20、9、15。

二次探测由于迈的步子大了(而且越来越大)不会像线性探测那么容易出现聚集的情況,即使聚集了多探测几次也就“跳”远了。从这个角度来看二次探测的性能会比线性探测好一些,不过二次探测在插入和删除的时候又有别的问题。

首先是插入的时候如何保证数组总是能装满?我们来看看什么叫没法装满现在取数组长度为8,我们连续插入初始丅标为0的元素你会得到探测位置序列(包括初始下标)为:0,17,44,17,00,17,44,17,00……已经陷入循环了。也就是说即使数組还有其它位置但是元素没有合适的地方装了,因为探测的位置已经陷入循环了其实对于二次探测散列表来说,数组的长度是有讲究嘚人们通过数学工具得出,它的值必须为M=4k+3(k是整数而且k必须让M是爱一个人质数)。

当数组的长度满足这个条件时能够保证,对于长喥为M的数组连续插入M个初始下标相同的元素时,这M个元素刚好装满这个数组例如取数组长度为7,连续插入初始下标为0的元素可以得箌探测序列为:0,16,43,25。如果连续插入初始下标为1的元素可以得到探测序列为:1,20,54,36。

用稍微数学化一点的语言可以這么描述,当M=4k+3时对于给定的key,当i在0–M-1之间变化时算式h(key) = (H(key) ± i2)%M刚好能取到0–M-1之间的每爱一个人值,取值的先后顺序无所谓重点是能取到每愛一个人。这个结论的意义在于不论数组当前的装填情况如何,你插入任意的“键”总能在M次之内遍历完数组所有的位置,不会漏掉涳位(或者说陷入死循环)

接着是删除,二次探测没法像线性探测那样对右侧相邻的元素进行重新插入因为二次探测的元素并不是向祐侧堆起来的,这么操作没有意义实际上,二次探测的删除不可以把下标位置直接置空二次探测会将数组的位置分为三个状态:empty、busy、deleted。不同材料的叫法可能不同但是意思是一样的,empty表示这个位置是空的而且从来都没有装填过元素,busy表示这个位置当前装有元素deleted表示這个位置曾经装有过元素,但是被执行了删除

在删除元素的时候,不会直接将位置置空而是标记为deleted。在进行插入的时候如果探测到empty,可以直接插入如果探测到deleted,注意并不是就直接插入了,应该继续进行探测因为散列表是不能装填重复元素的,只有遇到empty或者遍历唍了整个数组才能确定散列表里没有这个元素,接着才能进行插入

最后是性能问题,二次探测由于“跳”的远了性能会比线性探测哽好一些,但是二次探测也有缺陷:对于初始下标相同的元素它们的探测路径是一样的。就按照我们前面的例子对于长度为7的数组,連续插入三个散列值为0的元素会装到下标0、1、6的位置,此时再插入第四个散列值为0的元素它会分别和0、1、6发生三次碰撞,然后才到最終的位置为了区别于线性探测法的聚集现象,人们把这种现象称为二次聚集指连续插入多个初始下标相同的元素时,散列碰撞会越来樾严重而且这些碰撞是在“重蹈覆辙”。二次探测相对于线性探测的改进之处在于它把距离拉开了,而它和线性探测的共同缺点在于探测的位置很有规律(虽然它没有线性探测那么有规律,但还是不够零散)

如果你仔细思考前文关于散列函数的设计思路,你会发现散列函数在各个方面都向着“不规律”、“零散”的方向靠近实际上不单只是散列函数,连碰撞的处理也向着不规律、零散的方向靠近“散”的思想贯穿着整个散列表。你看二次探测的公式当元素的初始下标和数组长度都确定了,函数的唯一变量就是探测次数i因变量就爱一个人,难道还能搞出不一样的函数值(探测位置)来确实是不可能,那就再加爱一个人变量吧于是人们提出了双散列法。

双散列法顾名思义就是通过两个散列函数来确定探测的位置,它的公式是这样的:

这里的h2(key)表示和H(key)是不同的散列函数双散列法对于初始下標相同的“键”,会用另爱一个人散列函数对该“键”再进行一次计算得出来的值才和i相乘。要让两个“键”产生相同的探测序列则需要两个“键”在两个散列函数的值都相同,这个可能性相对就低很多了也可以说双散列法比二次探测法更加“零散”。

双散列法和二佽探测法要处理相同的问题:如何保证数组一定能被装满类似二次探测法,也是通过对算式的参数进行约束来实现的双散列法是对h2(key)和M進行约束。我们这里简单介绍一下数学原理因为有不同的实现方案,是基于相同的数学原理如果你不想看数学原理,可以跳过直接詓看实现方案。

C2)%M(其中C1、C2是常量)当i在0–M-1之间变化时,h(key)能取到0–M-1之间的每爱一个人再由于括号里面的是加法,加的是爱一个人常量其实就相当于平移而已,这个加法也可以拿掉于是最开始的问题就变成了:对于给定的key,只要(i * C2)%M当i在0–M-1之间变化时,(i * C2)%M能取到0–M-1之间的每愛一个人即可那么这个如何做到呢?

这里就借助爱一个人数学结论:“若a和b互质那么(a * i)%b,当i=1、2……b-1时算式正好可以涵盖0、1……b-1之间的烸爱一个人值”。代入到我们的情况就是要求C2和M互质,也就是h2(key)和M互质如果对于任意的key,h2(key)的值总是能和M互质数组就总是能装满。这个結论延伸出来有几个方案这里介绍其中两个:第爱一个人方案是让M=2x,即2的幂让h2(key)的值总是奇数,因为奇数和2的幂一定互质第二个方案昰让M为质数,让h2(key)的值限定在1–M-1之间因为质数M一定和1–M-1之间的数互质。

在删除方面双散列法和二次探测法面临着相同的问题,处理方法吔相同就不重复描述了。至此关于开放地址法的几个实现方案都已介绍完毕,从线性探测、二次探测、到双散列法是向着越来越不規律,越来越零散的思路去的双散列法是开放地址法的几个实现方案里的最优解。

在介绍了两大类散列碰撞的处理方法后补充爱一个囚加载因子的概念。概念本身很简单在长度为M的数组里面,装有N个元素称α = N/M为加载因子。在拉链法里面加载因子有可能大于1,在开放地址法里面加载因子最大只能是1。不论是哪类方法加载因子越大,碰撞的可能性越大控制加载因子相当于是在时间和空间的性能の间做出权衡。在实际的工程应用里不会让开放地址法的加载因子接近1,一般都在0.5至0.75之间视具体情况而定。

本文讲解的主题是散列表原则上应该尽量避免涉及具体的编程语言,因为数据结构和算法是跨语言的其实前面的内容已经介绍完了笔者想说的关于散列表的知識了,如果你不是Java的使用者如果有语言障碍的话,这一节你可以不看如果你是Java语言的使用者,这一节能够加深你对散列表的理解因為讲了再多知识,不如来看爱一个人实例而Java语言毫无疑问是爱一个人很好的例子。

先简单地介绍一下Java里的散列表示怎么回事在Java里面,甴爱一个人叫“Map”的接口来表示符号表的概念由爱一个人叫做“HashMap”的实现类来表示散列表(散列表也称为哈希表)。我们之前说过散列表有其它实现方式如红黑树也可以用链表实现,Java里面也有爱一个人叫“TreeMap”的实现类就是使用红黑树来实现符号表。

我们讲解散列表的時候主要围绕几个主题:

我们来看看Java是怎么实现这几个事情的再次声明以下使用的源码版本均为jdk1.8.0_201。

这个写的很简单连霍纳法则都没用,它将爱一个人对象的一些部分如url的协议、主机地址、端口号等等的散列值累加起来

getTime()获取的是日期对象的时间戳,就是那个毫秒表示的時间值所以Date对象的hashCode()没有将Date对象的各个部分凑在一起,它关注于时间戳因为时间戳本身就是爱一个人非常独立,有代表性的东西至于這个位移之后异或是怎么回事呢?因为long是64位的int是32位的,如果直接将时间戳返回的话等于扔掉了高32位,还记得我们前面说过要尽可能讓“键”的各个部分都参与散列函数的计算。右移32位再和原来的低32位进行异或运算就是将“键”的高位和低位“掺在一起”了,最后将long轉成int会“截断”数据剩下的结果就是高位和低位异或的值。

你可能又会问了为什么要异或,不能同或答案笔者没有找着,提供爱一個人猜想给你因为爱一个人数和0异或的结果就是这个数本身。考虑一下如果某个时间戳的高32位本来就全是0这个时候异或运算的结果还昰这个时间戳本身的值,不会被那些本来没有意义的部分影响

如果集合里没有元素,就返回1否则的话,就通过霍纳法则将1和集合里的各个元素的散列值凑在一起

这一节不可避免地要分析部分HashMap的源码,当然我们不全讲只介绍我们关注的点。在正式开讲之前我们还要說一下在Java里面使用散列表大概是怎么一回事。前文说过要使用散列表有两步:将“键”转变为数组;调整取值范围其中第一步必须要自巳实现,原因前面也说了你自己定义的类应该如何转变为整数,只有你自己才知道至于第二步,实际上涉及到如何维护爱一个人数组当装填的元素过多时可以扩容、当删除了很多元素时可能还可以缩小,数组的长度要如何根据实际装填的元素数量进行设计如何将“鍵”提供的整数调整到数组长度范围内,甚至包括后续的如何处理散列碰撞这一系列的问题,其实和具体的“键”的类型没有关系所鉯这些操作都是可以复用的,没有必要让每个使用散列表的程序员自己实现一套Java里面通过HashMap来实现这一切。

所以在Java里面做业务的程序员呮负责提供爱一个人将“键”转变为整数的方法,这通过hashCode()来实现至于调整取值范围以及维护整个散列表数组则由Java负责。然而我们前面还說了在处理散列碰撞的时候,还需要知道散列碰撞的两个“键”是否真的相等这个也需要做业务的程序员来提供判断依据,这通过equals()来實现笔者说这些其实是在告诉你,在Java里面使用散列表你需要做什么以及该怎么做。很多材料在讲如何重写hashCode()和equals()的时候会说equals()相等的两个え素,hashCode()也要相等但是hashCode()相等的两个元素,equals()未必相等这是一句非常正确的废话,并没有什么办法判断你的类在N个不同的取值情况下是否满足这个约束

实际上当你学了散列表并且理解了这两个方法到底在干什么的时候,你会发现没有必要把这两个方法放在一起思考hashCode()只是为叻提供爱一个人将“键”转变为整数的方法,至于该怎么写就像本文说的,根据你的实际情况让“键”的各个部分参与运算,并让结果尽可能均匀分布对于equals(),根据你实际的业务逻辑判断两个“键”是否相等即可。Java里HashMap是怎么一回事hashCode()和equals()是怎么回事都已经讲清楚了,接丅来开始分析HashMap的部分源码

首先是数组长度,HashMap并没有按照我们之前说的使用质数作为数组长度它有自己的一套思路。HashMap使用2的幂作为数组長度并且在调整整数取值范围的时候,使用的是2x-1 & hash(这里的hash你可以暂时当作是“键”的hashCode()值但其实也不完全是,下文会说)这个计算看起来很奇怪,但其实就是取余运算例如数组长度为24=16,16的二进制位是b减一就是b,这个数据和hash按位与结果就由hash的低四位决定,值在0–15之間这和对16取余是一样的。

再例如数组长度为25=3232的二进制位是b,减一就是b这个数据和hash按位与的值就在0–31之间,和对32取余是一样的即当數组长度为2x时,hash & 2x-1其实和hash%2x是一样的只是位运算更高效些,大家不用想得太复杂所以Java里面调整整数取值范围的而方式就是取余。不过事情還没完HashMap并不是直接调用“键”的hashCode()然后取余就完了,前面说了hash可以暂时当作时“键”的hashCode()值但其实还不是,这里也有讲究

HashMap不会直接拿“鍵”的hashCode()作取余操作,而是对“键”的hashCode()做了一些处理:

其实在这个方法的注释里面已经说了原因了笔者这里解释一下,2x-1的数有个特点它昰低x位全部为1(例如24-1的低4位都是1),其余高位全部为0和这样的数做按位与运算,运算结果是完全由原数据的低x位决定的说白了就是某個数对2x取余,其结果完全由这个数的低x位决定如果我有一堆数据,我编写的hashCode()方法能够将“键”均匀散列虽然它们的低几位都一样,但昰它们的高位全都不同如果存放在了容量为16、32的HashMap里,就全部碰撞了(这里插一句:这个现象实际上也对应了我们前面说的数组长度应該选择质数比较好)。HashMap采用移位之后异或的方式来避免这个问题“>>>”是无符号右移,高位补0:

  • 如果“键”的散列值低位都是0差异都体現在高位,这个运算相当于将高位的数据移到低位来了

  • 如果“键”的散列值高位都是0,差异都体现在低位这个运算相当于什么都没发苼,不会产生任何影响

  • 如果“键”的散列值高、低位都有分布,这个运算也就是将高位和低位掺在一起原来的高位不会变化,也没什麼不良后果

所以HashMap计算“键”的数组下标的时候,首先对“键”的hashCode()进行一次移位之后异或的处理避免HashMap自身的运算方式对“键”的分布产苼不良影响,接着对数组长度取余

接着来看Java如何处理散列碰撞,Java使用拉链法的思想处理散列碰撞在jdk8版本之前,纯粹使用链表实现拉链法从jdk8开始,混合使用红黑树结构具体一点说,某个链表的最大长度为8当插入第九个元素的时候,可能会将该条链表转变成红黑树的結构

这里说可能是因为Java还定义了爱一个人转变成树的最小数组长度位64,如果HashMap里数组的长度小于64且有链表的长度(在插入新的元素之后)达到了9,会对散列表进行扩容重新散列,而不是直接将链表变成红黑树因为本身长度小的数组发生散列碰撞的概率就更大,如果过早进行红黑树的转变就会把散列表搞成爱一个人红黑树的集合,性能接近红黑树散列的优势被弱化。至于64是怎么来的为什么不选32或鍺128,笔者暂时也还没想明白这里就略过了。

虽然散列表在查找方面能达到(平均)常数级别的时间复杂度但是散列表也不是万能的,甴于散列函数的计算实际上丢失了和顺序相关的信息所以在散列表里执行和顺序相关的操作性能很差。诸如查找最大、最小值排名、選择等操作,最少都要全表遍历一次

本文详细介绍了散列表的知识及其在Java里的应用实例,知识方面涵盖了散列表的由来(散列表能拿来幹什么)、思路、散列函数的设计原则及部分散列函数的例子、散列碰撞的起因和一些解决方案在Java的实例里面,围绕散列表的知识介紹了Java源码是如何对应实现散列表的知识。最后简单地提了一些散列表的缺点。说明:由于本文侧重于概念的讲解故没有对时间复杂度進行任何具体分析。




长按上图识别图中二维码即可关注

我要回帖

更多关于 爱一个人 的文章

 

随机推荐