C++ 11新标准实现POJ No.1002-487-3279
487-3279(重复的电话号码查询)(标签:优先队列,哈希表)
题目描述
电话号码的标准格式是七位十进制数,并在第三、第四位数字之间有一个连接符。电话拨号盘提供了从字母到数字的映射,映射关系如下:
A, B, 和C 映射到 2
D, E, 和F 映射到 3
G, H, 和I 映射到 4
J, K, 和L 映射到 5
M, N, 和O 映射到 6
P, R, 和S 映射到 7
T, U, 和V 映射到 8
W, X, 和Y 映射到 9
Q和Z没有映射到任何数字,连字符不需要拨号,可以任意添加和删除。 TUT-GLOP的标准格式是888-4567,310-GINO的标准格式是310-4466,3-10-10-10的标准格式是310-1010。
如果两个号码有相同的标准格式,那么他们就是等同的(相同的拨号)
你的公司正在为本地的公司编写一个电话号码薄。作为质量控制的一部分,你想要检查是否有两个和多个公司拥有相同的电话号码。
Input
Output
No duplicates.
Sample Input
12 4873279 ITS-EASY 888-4567 3-10-10-10 888-GLOP TUT-GLOP 967-11-11 310-GINO F101010 888-1200 -4-8-7-3-2-7-9- 487-3279
Sample Output
310-1010 2 487-3279 4 888-4567 3
解题思路
首先,经过对题目的分析可知,解决此题目的一般思路可划分模块如下:
Step1.输入号码薄中号码的数量(Input第一行)
Step2.若已输入的数量还未达到满容量,就输入一个未标准化(标准化的号码是指7位数字的号码,e.g.123-4567)的字符串;否则,跳到Step6
Step3.对上述输入的字符串进行标准化的转换
Step4.将标准化后的电话号码存储起来,保存到数据结构bank中,以便在后续继续输入其它号码时判断是否有重复号码
Step5.回到Step2
Step6.将bank中的号码进行一遍扫描,且记录多次重复出现的号码和出现次数到另一个数据结构preout中
Step7.对preout进行排序,并输出相应出现次数
观察以上步骤可发现,在Step2~Step5中,会对全部电话号码遍历一遍;在Step6中,扫描重复记录时遍历第二遍;并且在Step7中,我们对preout进行排序时,又将其中的一部分重复号码遍历了第三遍。所以,考虑是否可以在一次遍历中,将之前的三次遍历所做的事情一次性做完?于是,得到改进后的整体逻辑思路如下:
Step1.输入号码薄中号码的数量(Input第一行)
Step2.若已输入的数量还未达到满容量,就输入一个未标准化(标准化的号码是指7位数字的号码,e.g.123-4567)的字符串;否则,跳到Step6
Step3.对上述输入的字符串进行标准化的转换
Step4.将标准化后的电话号码存储起来,保存到数据结构bank中,若bank中已有这个号码,则标记这个号码出现的次数为上次出现的次数加一(++count);同时,若号码的出现次数等于2时,将号码插入到preout有序数据结构的适当位置,表示这个号码有重复,即将在程序的最后(Step6)顺序输出(数据结构bank和preout将在后文具体分析)
Step5.回到Step2
Step6.输出重复号码和相应出现次数
接下来就以上的步骤进行分步阐述:
Step1:(略)
Step2:(略)
Step3:对字符串的标准化包括去除连字符“-”和将非标准化号码中的字母转换为相应的数字。
去除连字符可对输入的非标准号码进行一次遍历去除。
转换为标准化号码时,会使用过多的switch-case语句,如下代码所示:
switch (s[i]){ case('A') {s[i] = '2'; break;} case('B') {s[i] = '2'; break;} case('C') {s[i] = '2'; break;} ...... }
为避免使用过长代码,在转换时,可使用hash map存储字母到数字的转换关系。在C++ STL中,哈希映射的头文件为<unordered_map>。定义unordered_map类对象search_dict。并对hash map在构造函数中初始化。如下代码所示:
Solution() { /*初始化search_dict*/ search_dict['0'] = '0'; search_dict['1'] = '1'; search_dict['A'] = search_dict['B'] = search_dict['C'] = search_dict['2'] = '2'; search_dict['D'] = search_dict['E'] = search_dict['F'] = search_dict['3'] = '3'; search_dict['G'] = search_dict['H'] = search_dict['I'] = search_dict['4'] = '4'; search_dict['J'] = search_dict['K'] = search_dict['L'] = search_dict['5'] = '5'; search_dict['M'] = search_dict['N'] = search_dict['O'] = search_dict['6'] = '6'; search_dict['P'] = search_dict['R'] = search_dict['S'] = search_dict['7'] = '7'; search_dict['T'] = search_dict['U'] = search_dict['V'] = search_dict['8'] = '8'; search_dict['W'] = search_dict['X'] = search_dict['Y'] = search_dict['9'] = '9'; }
Step4:将转换后的标准化号码存储起来。因为每输入一个号码,要对此号码转换成标准号码后判断是否有重复,若在存储时选用简单的线性表结构,在查询重复操作时会产生O(n)的时间复杂度,因此,此处选用哈希表存储更高效。哈希表的key存储标准化后的号码,value存储号码出现的次数。在每次输入并转换成标准号码后,检查此号码是否在hash表中,若不在,则添加进去,并且value置为1。若已经存在,则value+1。
并同时进行判断,当value=2时,说明号码有重复,应当在程序末尾输出这个号码和对应的出现次数。所以应当新建一个数据结构进行重复号码的存储。value=2时,将号码追加进去。此处,又要做选择题了,应当用什么样的数据结构呢?这里可以选择可变长数组vector,对每个value=2的号码无序放入,等到程序末尾要输出时再排序;还有一个思路是用最小优先队列——即二叉堆来存储。二叉堆每次加入一个新元素或输出一个最小值的时间为log(N),但是连续加入或弹出N个元素的时间复杂度为N,而非Nlog(N)。所以理论上要比变长数组存储最后输出时排序稍快。并且如这篇博文所述,vector容量不够的话,会重新开辟一块更大容量的空间,并且将原数组中的内容拷贝到新的空间。这就导致,变长数组会浪费更多的程序运行时间。所以,此处选用优先队列存储将要输出的重复号码值。
Step5:(略)
Step6:在优先队列不为空时,连续将队列头的元素pop出来并且查询hash表中出现的次数,输出到标准输出即可。
(2019-11-22更新)在输出排序时,也可以选用二叉搜索树作为数据结构。这样,在顺序输出时,直接对二叉搜索树做中序遍历即可得到顺序排列的号码。选用二叉搜索树与优先队列的摊还时间复杂度是一样的。
if (dup_nums.size() == 0) { cout << "No duplicates." << endl; } else { while (dup_nums.size() != 0) { string number = dup_nums.top(); dup_nums.pop(); int dup_count = count_dict[number]; number.insert(number.begin() + 3, '-'); cout << number << ' ' << dup_count << endl;; } }