这个题目其实源于很久之前的一次 Uber 面试,码工换工作无非就是刷 leetcode ,研究如何翻转二叉树之类的算法问题,所以头一次在电话里听到这道题的时候还是挺耳目一新的。当时顺利写出来了,也通过了电面,但觉得还是有不完善的地方,比如说代码不够 “Pythonic” 等,所以趁着周天晚上闲着无事,又拿出来写了写。

HashTable 本身没啥好说的,中文叫”哈希表“或者”散列表“,具体翻译看教材编写者的个人喜好。众所周知这个数据结构用来存储”键-值“结构的数据,可以做到常数级时间复杂度的查找,在日常搬砖中算是主力工具。实现一个 HashTable 其实就是实现两个部分

  • Hash 函数。这个函数能够读入一个可被 hash 的变量,输出一个整数。在本次实现中暂不考虑这一块,用 Python 自带的 hash 函数偷个懒
  • Hash 冲突的解决机制。成熟的方法有很多种,在这里只考虑最简单的一种,即将同一个 hash 值下的不同的 key 存放在数组的同一个位置,以链表形式保存

既然是自己实现,就根据 HashTable 的查找原理选择 List 作为数据存储结构,在每个位置放置一个子 List 用于解决 hash 冲突,因此对于构造函数来说,大概应该长这个样子

class MyDict(object):def __init__(self, size=99999):self.hash_list = [list() for _ in range(size)]self.size = size

每次添加一个键值对时,将 key hash 后的整数对 List 长度取模,即得到该 key 在 List 中的位置。因为 List 的每个位置是一个子 List ,所以需要遍历该子 List ,如果已存在该键值对,则更新 value ;如果不存在,将该键值对存在尾部。

def __setitem__(self, key, value):hashed_key = hash(key) % self.sizefor item in self.hash_list[hashed_key]:if item[0] == key:item[1] = valuebreakelse:self.hash_list[hashed_key].append([key, value])

同理,在 MyDict 类中取值时,首先定位到给定 key 的位置,然后遍历其中的子 List ,若存在,返回 value;若不存在,抛出 KeyError

def __getitem__(self, key):for item in self.hash_list[hash(key) % self.size]:if item[0] == key:return item[1]raise KeyError(key)

这样一个简单的字典类(HashTable)就写好了,没有太多的功能,仅仅支持存放键值对及取值,运行效率也不算高,但已经够用了,函数的时间复杂度也是 O(1) 级别的。在这里用 Python 的魔术方法实现了这两个函数,这样就能像操作 Python 自带的 Dict 那样来操作 MyDict 。简单的示例如下。

>>> from my_dict import MyDict
>>> d = MyDict()
>>> d["a"] = 1
>>> d["b"] = 2
>>> d["c"] = 3
>>> d["a"]
1
>>> d["d"]
Traceback (most recent call last):File "<stdin>", line 1, in <module>File "/path/to/my_dict.py", line 19, in __getitem__raise KeyError(key)
KeyError: 'd'

至此 MyDict 的实现还不算结束,因为还不能判断一个 key 是否在字典中,无法遍历字典等,接下来的功能如下。

目前的 MyDict 已经有了基本的功能,但如果试图输出,就会出现如下所示的样子。

>>> from my_dict import MyDict
>>> d = MyDict()
>>> d["a"] = 1
>>> d["b"] = 2
>>> d["c"] = 3
>>> d
<my_dict.MyDict object at 0x1028fde48>
>>> print(d)
<my_dict.MyDict object at 0x1028fde48>

这时候,我们需要实现 Python 类中的 __repr__ 和 __str__ 方法。关于两者的区别,这里有个简洁明了的一句话解释

My rule of thumb: __repr__ is for developers, __str__ is for customers.

在我们这里,单独在 shell 中执行一个 d ,解释器调用的是 __repr__ ,用 print 函数输出时,调用的是 __str__ 。同时,对于 dict ,这两个方法的输出是一致的,所以我们只需要实现一个 __repr__ ,这个方法在 __str__ 缺失时会替代其被调用。

def __repr__(self):result = []for sub_list in self.hash_list:if not sub_list:continuefor item in sub_list:result.append(str(item[0]) + ": " + str(item[1]))return "{" + ", ".join(result) + "}"

让我们继续尝试完善这个 MyDict 类。对于一个字典,除了以常数级的时间复杂度从中取值,我们经常做的另一个常数级操作是检查一个 key 是否在字典中,语法已经很熟悉了, key in dict 。实现 in 关键字的操作,需要在类中实现 __contains__ 方法

def __contains__(self, key):for item in self.hash_list[hash(key) % self.size]:if item[0] == key:return Truereturn False

很多时候,我们希望能够遍历一个字典,通过调用 .keys() 、 .values() 、 .items() 来分别遍历键、值、键值对,这就要求 MyDict 的内部结构是可迭代的,所幸之前简单粗暴的采用了 list 来存储数据,但这还不够,因为我们在遍历字典的时候并不希望把内部 list 中的空位也返回给调用者。这个时候我们需要首先实现一个迭代器,将 MyDict 中的键值对依次返回,然后用这个迭代器实现 __iter__ 方法,让其仅仅返回 key ,这样就可以有一个比较符合直觉的 for key in my_dict 调用,至于本段开始提到的三个方法,则可以调用这个迭代器或者 __iter__ 来实现

def __iterate_kv(self):for sub_list in self.hash_list:if not sub_list:continuefor item in sub_list:yield itemdef __iter__(self):for kv_pair in self.__iterate_kv():yield kv_pair[0]def keys(self):return self.__iter__()def values(self):for kv_pair in self.__iterate_kv():yield kv_pair[1]def items(self):return self.__iterate_kv()

我们还期望得知目前字典的大小,即调用 len(dict) 就可以很方便的返回字典里有多少个键值对,这就需要实现 __len__ 方法。但每次调用这个方法时,从内部的 list 中一个个的去数有多少个键值对无疑是低效的,我们可以用一个变量来记录下当前的字典大小,每次新增一个键值对时自增,这样在调用 len 函数的时候就可以直接返回了。

class MyDict(object):def __init__(self, size=99999):...self.length = 0def __setitem__(self, key, value):...for item in self.hash_list[hashed_key]:...else:self.hash_list[hashed_key].append([key, value])self.length += 1...def __len__(self):return self.length

到此为止, MyDict 的运行效果如下所示。完整版代码已经放到了 gist 上。

>>> from my_dict import MyDict
>>> d = MyDict()
>>> d["a"] = 1
>>> d["b"] = 2
>>> d["c"] = 3
>>> d
{c: 3, a: 1, b: 2}
>>> print(d)
{c: 3, a: 1, b: 2}
>>> "a" in d
True
>>> "no-exist" in d
False
>>> for k in d:
...     print(k)
...
c
a
b
>>> for k in d.keys():
...     print(k)
...
c
a
b
>>> for v in d.values():
...     print(v)
...
3
1
2
>>> for k, v in d.items():
...     print(k, v)
...
c 3
a 1
b 2
>>> len(d)
3

不用 Python 自带的 Dict 实现自己的 HashTable相关推荐

  1. python变量必须以字母和下划线_【转载】关于python中带下划线的变量和函数的意义...

    Python 的代码风格由 PEP 8 描述.这个文档描述了 Python 编程风格的方方面面.在遵守这个文档的条件下,不同程序员编写的 Python 代码可以保持最大程度的相似风格.这样就易于阅读, ...

  2. 十大被低估的python库_小白必读!十大被低估的Python自带库!

    原标题:小白必读!十大被低估的Python自带库! 大家在学习python的过程中,都会了解到python的一个强大的功能在于各种强大的第三方库函数,大家只需要通过pip install 即可安装我们 ...

  3. python(3): str list dict tuple set 常用函数整理

    多个类型对象共有方法 1.方括号切片 语法:[start:end:step]   #step默认为1 适用所有可下标操作的对象,如:str , list,tuple等 例: print([1,2,3, ...

  4. python tkinter计算器实例_使用Python自带GUI tkinter编写一个期权价格计算器

    0 准备工作 首先,确认环境中有numpy.scipy.stats和tkinter三个功能包.前两个功能包可用于Python的数学计算,比如使用numpy来生成随机数用于Monte Carlo模拟,以 ...

  5. python sqlite3 带密码_Python爬虫+Flask,带你创建个网站!

    文化不分边界 人,为什么要读书?举个例子: 当看到天边飞鸟,你会说:"落霞与孤鹜齐飞,秋水共长天一色."而不是:"卧靠,好多鸟."; 当你失恋时你低吟浅唱道:& ...

  6. 十大被低估的Python自带库!

    大家在学习python的过程中,都会了解到python的一个强大的功能在于各种强大的第三方库函数,大家只需要通过pip install 即可安装我们需要的库函数. 大家往往只是关注自己安装的pytho ...

  7. 不用Python获取小红书收藏夹内的视频

    目录 前言 一.准备工具 二.使用步骤 1.获取收藏夹(专辑)的链接 2.没有安装Xpath Helper的安装Xpath Helper 3.利用Xpath提取所有视频的标题并用Excel进行简单处理 ...

  8. Python: 生成带用户昵称的头像

    Python: 生成带用户昵称的头像 需求 新建用户后,根据用户输入的昵称生成图片.(例:注册"钉钉"用户后,头像根据输入的名字生成) 开发环境 Windows 10 Python ...

  9. python 布莱克舒尔斯_使用Python自带GUI tkinter编写一个期权价格计算器

    0 准备工作 首先,确认环境中有numpy.scipy.stats和tkinter三个功能包.前两个功能包可用于Python的数学计算,比如使用numpy来生成随机数用于Monte Carlo模拟,以 ...

最新文章

  1. 自定义一个安全的rm指令
  2. ubuntu14.04.03 vsftpd
  3. java-forkjoin框架的使用
  4. JSON字符串key缺少双引号的解决方法
  5. 定位漏水与非定位漏水的区别?
  6. python可以处理什么文件夹_Python处理文件和文件夹的10条命令
  7. nagios的搭建及配置----(中)
  8. LeetCode 987. 二叉树的垂序遍历(递归/循环)
  9. Offer年薪低于25W全额退款|阿里、腾讯内推快艇《全链路大数据分析工程师》课程招生简章...
  10. Magento:Service Temporarily Unavailable ?
  11. HDU2179--pi(麦金公式)
  12. RFID定位技术在仓库管理中的应用--新导智能
  13. 安卓手机更新过程手机乱码_关于安卓手机上自带播放器乱码问题的解决
  14. 一些有趣的Shodan搜索
  15. c中纠结不清的点(1)
  16. LinuxProbe 0x13 网站服务程序、SELinux安全子系统、个人用户主页功能、虚拟网站主机功能
  17. maven仓库类型说明 hosted/proxy/group
  18. Python摄氏度与华氏度的相互转换
  19. 旅游平台分账模式是什么样的?
  20. 联想e470锁定计算机,联想E470c电脑设置硬盘启动的方法,你值得拥有!

热门文章

  1. 小米手机老板,雷军,肯定是假程序员
  2. 关于灵魂安放,年轻人如何选择适合自己的城市呢?学长有话说
  3. python连载第11篇 if 语句
  4. matplotlib mysql_matplotlib简介
  5. js做的flash形式的幻灯图片
  6. 转转Q1手机行情:iPhone13开始“飘香” 二手5G手机市场苹果占一半
  7. 年轻人的钱包,被十一假期榨干了
  8. 特斯拉三季度生产23.8万辆 交付超24万辆
  9. iPhone 13 Pro Max Magsafe保护壳曝光:命名方式疑似确认
  10. 特斯拉在以色列开售Model 3 比国内贵出30%!