卡码网语言基础课(Python) | 16.出现频率最高的字母
目录
- 一、题目描述
- 二、前言
- 三、哈希表
- 1. 哈希表
- 2. 哈希函数
- 3. 哈希碰撞
- A. 拉链法
- B. 线性探测法
- 四、代码编写
一、题目描述
给定一个只包含小写字母的字符串,统计字符串中每个字母出现的频率,并找出出现频率最高的字母,如果最高频率的字母有多个,输出字典序靠前的那个字母。
- 输入描述: 包含多组测试数据,每组测试数据占一行。
- 输出描述: 有多组输出,每组输出占一行。
二、前言
我们已经学习了数组(列表)、字符串、链表等数据结构,但是大家有没有发现,如果我们想要找到其中某个元素或者节点,需要从索引为0的位置或者链表头节点开始,逐一开始比较,直到找到相等的位置或者末尾才会结束。
那是否可以避免之前的比较,直接通过要查找的记录直接找到其存储位置呢?
是有的,可以通过“哈希表”来实现,哈希表是根据关键码key的值而直接进行访问的数据结构。
哈希表的作用是快速判断一个元素是否出现在集合里,它的核心思想是在关键码和存储位置之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置,而这个对应关系,称之为散列函数(哈希函数)。
其实数组(列表)就是一张哈希表,哈希表中关键码就是数组(列表)的索引下标,然后通过下标直接访问数组(列表)中的元素。
哈希表来解决问题的时候,一般选择以下三种数据结构:数组(列表)、集合、映射。
三、哈希表
我们可以将哈希表比喻为一个大抽屉,抽屉里面有很多小格子。每个格子可以用来存放一些东西。
- 抽屉编号: 抽屉有编号,这个编号就是数据的key,我们通过这个key来找到对应的抽屉。
- 哈希函数: 哈希表使用一种特殊的函数(哈希函数),来决定数据应该放在哪个抽屉里。这个函数将数据的名字key转换成一个数字,然后根据这个数字来选择一个抽屉。
- 抽屉里的物品: 在每个抽屉里,可以放一些东西,这些东西就是我们要存储的数据。
- 解决冲突: 有时候不同的key经过哈希函数后可能会得到相同的编号,这就是冲突。哈希表有方法来处理这些冲突。
- 快速查找: 当我们需要找到某个数据时,哈希表可以通过名字key快速地找到对应的抽屉,然后取出里面的数据,这个操作非常快速,就像从抽屉中拿出东西一样。
1. 哈希表
哈希表是根据关键码的值而直接进行访问的数据结构。直白讲,数组就是一张哈希表。哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示:
那哈希表能解决什么问题呢?一般哈希表都是用来快速判断一个元素是否出现在集合里。
例如要查询一个名字是否在这所学校里。要枚举的话时间复杂度是O(n),但如果使用哈希表的话,只需要O(1)就可以做到。
我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以直到这位同学在不在这所学校里了。将学生姓名映射到哈希表尚就涉及到了hash function,也就是哈希函数。
2. 哈希函数
哈希函数,把学生的姓名直接映射为哈希表尚的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了。
哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashCode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
如果hashCode得到的数值大于哈希表的大小,也就是大于tableSize了,怎么办呢?
此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,这样就保证了学生姓名一定可以映射到哈希表上了。
此时问题又来了,哈希表我们刚刚说过,就是一个数组。
如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表同一个索引下标的位置。
3. 哈希碰撞
如图所示,小李和小王都映射到了索引下标1的位置,这一现象叫做哈希碰撞。
一般哈希碰撞有两种解决方法,拉链法和线性探测法。
A. 拉链法
刚刚小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中,这样我们就可以通过索引找到小李和小王了。数据规模是dataSize,哈下表的大小为tableSize。其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
B. 线性探测法
使用线性探测法,一定要保证tableSize大于dataSize,我们需要依靠哈希表中的空位来解决碰撞问题。
例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize,要不然哈希表上就没有空置的位置来存放冲突的数据了。
四、代码编写
首先,需要接收整数n的输入,表示共有n行测试数据,然后进行n次循环迭代,接收一行字符串作为输入,将字符串经过处理后,统计输出最高频率的字母。
n = int(input())for _ in range(n):s = input()
那接下来的问题,就转换成如何统计字符串中各位字符的频率了。
众所周知,字母一共有26个,我们可不可以定义一个长度为26的列表,列表的元素代表着各位字符的频率,初始频率都为0,列表的索引0对应着字符a,索引1代表着字符b,依次类推,索引25对应着字母z。
然后遍历整个字符串,如果遇到字符a,则对应的索引0的元素值+1,表示频率+1,当字符串遍历完毕,各个字符的频率也都统计完毕了。
示例图如下:在字符串“abcdef”当中,字符a对应索引0,所在位置元素的值为1,字符b、c、f类似,字符e对应索引4,所在位置元素的值为2,表示频率为2。
当字符串遍历过程中,假设char表示当前字符,只需要计算ord(char)-ord(‘a’)就可以计算出当前字符和字符’a’之间的Unicode码值,这个差值就是列表元素的索引。
# 列表复制,将0这个元素赋值了26次,从而创建了包含26个0的列表
temp = [0] * 26
# 遍历所有字符串
for char in s:# ord(char) - ord('a')表示当前字符和字符'a'之间的Unicode码值,也是对应列表的索引a = ord(char) - ord('a')# temp[索引]处的值 +1temp[a] += 1
经过一轮遍历之后已经完成统计,数组中各位的元素已经是a-z字母的频次了,但是如果想要找到最大值,还是需要重新遍历一遍,那我们如何找到这个最大值呢?
这需要先初始化一个字符的最大出现频率,然后从头开始遍历,逐一比对当前字符出现的频次和最大频率的大小,如果当前字符出现的频次大于最大值,则更新最大值为当前字符出现的频次,这样完整遍历一遍后,就能找到字符的最大出现频率。
# 初始化字符的最大出现频率为0
maxFreq = 0
# 循环迭代处理列表中的其他字符
for i in range(26):# 如果当前字母的出现频率大于maxFreqif temp[i] > maxFreq:# 更新maxFreq为当前字母的出现频率maxFreq = temp[i]
但是上面的操作只统计了最大频率,并没有记录最大频率所在的索引i
maxFreq = 0
# 在还没有遍历temp列表之前,不知道哪个字符是出现频率最高的,用-1来表示“尚未找到”
maxFreqChar = -1
for i in range(26):if temp[i] > maxFreq:maxFreq = temp[i]maxFreChar = i
当循环结束后,已经找到出现频率最大字符的频次以及对应的索引i,将‘a’对应的Unicode码值加上索引值,就是出现频率最大的字符的Unicode码值,再经过chr()函数转换,就能得到最终的结果。
res = chr(ord('a') + maxFreqChar)
print(res)
本题的完整代码如下:
n = int(input())for _ in range(n):s = input()temp = [0] * 26for char in s:a = ord(char) - ord('a')temp[a] += 1maxFreq = 0maxFreqChar = -1for i in range(26):if temp[i] > maxFreq:maxFreq = temp[i]maxFreqChar = ires = chr(ord('a') + maxFreqChar)print(res)