简介
- 上一篇介绍了基础的面试技巧和Python语言考察点,本篇主要从常用算法和数据结构入手
算法和数据结构
- 常用内置数据结构和算法
- 数据结构和算法是不分家的
- 单纯的看数据结构,就是数据存储的格式,这里的线性、链式、KV、集合等等
- 我们更关心基于存储格式的算法,列表、字典、集合属性,内置库算法等
- 常用库
collections
:方法 作用 namedtuple() 创建命名元组子类的工厂函数 deque 类似列表(list)的容器,实现了在两端快速添加(append)和弹出(pop) ChainMap 类似字典(dict)的容器类,将多个映射集合到一个视图里面 Counter 字典的子类,提供了可哈希对象的计数功能 OrderedDict 字典的子类,保存了他们被添加的顺序 defaultdict 字典的子类,提供了一个工厂函数,为字典查询提供一个默认值 UserDict 封装了字典对象,简化了字典子类化 UserList 封装了列表对象,简化了列表子类化 UserString 封装了列表对象,简化了字符串子类化 - 看个例子:
import collections # collections.namedtuple(typename, field_names, *, rename=False, defaults=None, module=None) # 返回一个新的元组子类,名为typename ;简而言之,它让tuple更可读 _some = collections.namedtuple('Point', ['x', 'y']) # 这个Point不是关键,重点是让下标从01变为xy,更可读 p = _some(11, y=22) p[0] + p[1] # 33 # 双端队列,两头插 de = collections.deque() de.append(1) de.append(2) de.appendleft(0) de # deque([0, 1, 2]) de.pop() de.popleft() # 计数器 c = collections.Counter('abababaccd') c # Counter({'a': 4, 'b': 3, 'c': 2, 'd': 1}) c.most_common() # [('a', 4), ('b', 3), ('c', 2), ('d', 1)] # OrderedDict order = collections.OrderedDict() # 保存了插入时的顺序 order['a'] = 1 order['b'] = 2 order['c'] = 3 list(order.keys()) # ['a', 'b', 'c'] 方便实现LRU Cache
- 看官方文档是个好主意
dict底层结构
- 字典使用哈希表作为底层结构
- 平均查找时间复杂度O(1)
- 二次探查解决哈希冲突问题
- 解决哈希冲突有三种方法:开放定址法、再哈希法、链地址法
- 开放定址法有通用的再散列函数(Hi=(H(key)+di)%m i=1,2,…,n)
- 其中,二次探测再散列:冲突发生时,在表的左右进行跳跃式探测(di=12,-12,22,-22,…,k2,-k2 ( k<=m/2 ),正负代表左右两侧)
- 链地址法:冲突后使用链表保存相同key的元素
- 使用哈希结构的另一个问题是如何扩容
- 类似C++中vector的容量扩张方法
- Hash表中每次发现loadFactor==1(负载因子:used/size)时,就开辟一个原来桶数组的两倍空间(称为新桶数组)
- 把原来的桶数组中元素所有转移过来到新的桶数组中
- 以上是针对链地址法的扩容操作,缺点是要移动所有元素
- 哈希表的底层使用数组(嗯,简单吧,不然怎么O(1),会直接将key按照算法转换成数组下标)
list/tuple
- 都是线性结构,支持下标访问
- list是可变对象,tuple保存的引用不可变
- 不可变指的是不能替换掉里面的对象
- 但如果这个对象本身就是可变的,可以修改
t = ([1],2,3) t[2] = 4 # TypeError: 'tuple' object does not support item assignment t[0].append(2)
- list没法作为字典的key,tuple可以(可变对象不可hash)
- list的本质还是数组,但由于能存储不同类型的对象,保存的是指向对象的指针,即这是个指针数组
实现LRU
- 这是一种缓存剔除策略(最近最少使用),学过OS的应该都知道,常见的还有LFU
- 原理呢?使用循环双端队列更新,把最新访问的key放到表头,装不下就踢出表尾
- 怎么实现呢?dict+OrderedDict
- 这种缓存置换策略一般包含三个方法即可实现:
init()
、get()
、put()
from collections import OrderedDict # OrderedDict的特点是保存了插入时的顺序 class LRUCache: def __init__(self,capacity=128): self.capacity = capacity # 限制容量 self.order = OrderedDict() def get(self, k): if k in self.order: val = self.order[k] self.order.move_to_end(k) # 内置方法,kv移到尾部,改变顺序 # 注意:我们视字典尾部为队列表头,即踢出元素时从字典头部 return val else: return -1 def put(self, k, v): if k in self.order: del self.order[k] self.order[k] = v # 更新kv,move_to_end()也可以的啦,但关键在于v可能不一样, else: if(self.capacity <= len(self.order)): # 满了 del self.order.popitem(last=False) # 踢出字典头部(最先进来的) self.order[k] = v # 插到尾部(表头)
- 这种缓存置换策略一般包含三个方法即可实现:
- 使用单元测试看看我们的算法实现对不对吧!
算法常考点
- 重点:排序+查找(可以看我的文章:重学算法C++版)
- 要求:独立手写,分析时间复杂度
- 这里先实现:链表、队列、栈、二叉树、堆
- 使用内置结构实现高级数据结构
- LeetCode刷题/《剑指offer》上的题
- 这里需要你了解常见的数据结构概念,比如什么是单链表、循环链表等
单链表反转
-
打开LeetCode
- 这个题目难度更高一点,指定区间的翻转
- 链接什么时候切断,什么时候补上去,先后顺序一定要想清楚
- 上面的思路实现比较复杂,可以每遍历到一个节点,让这个新节点来到反转部分的起始位置,遍历一遍即可:
pre
始终指向翻转位置的起点(起始指向left-1)cur
指向当前节点(起始指向left)next
始终指向cur的下一个节点
- 这个题目难度更高一点,指定区间的翻转