487-3279(重复的电话号码查询)(标签:优先队列,哈希表)

题目描述

企业喜欢用容易被记住的电话号码。让电话号码容易被记住的一个办法是将它写成一个容易记住的单词或者短语。例如,你需要给滑铁卢大学打电话时,可以拨打TUT-GLOP。有时,只将电话号码中部分数字拼写成单词。当你晚上回到酒店,可以通过拨打310-GINO来向Gino's订一份pizza。让电话号码容易被记住的另一个办法是以一种好记的方式对号码的数字进行分组。通过拨打必胜客的“三个十”号码3-10-10-10,你可以从他们那里订pizza。
电话号码的标准格式是七位十进制数,并在第三、第四位数字之间有一个连接符。电话拨号盘提供了从字母到数字的映射,映射关系如下:

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

输入的格式是,第一行是一个正整数,指定电话号码薄中号码的数量(最多100000)。余下的每行是一个电话号码。每个电话号码由数字,大写字母(除了Q和Z)以及连接符组成。每个电话号码中只会刚好有7个数字或者字母。

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] = ''; break;}
case('B') {s[i] = ''; break;}
case('C') {s[i] = ''; break;}
......
}

为避免使用过长代码,在转换时,可使用hash map存储字母到数字的转换关系。在C++ STL中,哈希映射的头文件为<unordered_map>。定义unordered_map类对象search_dict。并对hash map在构造函数中初始化。如下代码所示:

    Solution() {
/*初始化search_dict*/
search_dict[''] = '';
search_dict[''] = '';
search_dict['A'] = search_dict['B'] = search_dict['C'] = search_dict[''] = '';
search_dict['D'] = search_dict['E'] = search_dict['F'] = search_dict[''] = '';
search_dict['G'] = search_dict['H'] = search_dict['I'] = search_dict[''] = '';
search_dict['J'] = search_dict['K'] = search_dict['L'] = search_dict[''] = '';
search_dict['M'] = search_dict['N'] = search_dict['O'] = search_dict[''] = '';
search_dict['P'] = search_dict['R'] = search_dict['S'] = search_dict[''] = '';
search_dict['T'] = search_dict['U'] = search_dict['V'] = search_dict[''] = '';
search_dict['W'] = search_dict['X'] = search_dict['Y'] = search_dict[''] = '';
}

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() == ) {
cout << "No duplicates." << endl;
}
else {
while (dup_nums.size() != ) {
string number = dup_nums.top();
dup_nums.pop();
int dup_count = count_dict[number];
number.insert(number.begin() + , '-');
cout << number << ' ' << dup_count << endl;;
}
}

源代码

~~~Code~~~

05-24 09:09