b站视频:路飞IT学城
清华计算机博士带你学习Python算法+数据结构_哔哩哔哩_bilibili
文章目录
#61 链表总结
###### 链表——复杂度分析
# 顺序表(列表/数组)与 链表
# 按元素值查找 #注:复杂度都是O(n)
# 按下标查找 #注:顺序表快 顺序表O(1),链表O(n)
# 在某元素后插入 #注:顺序表O(n) ,链表O(1)
# 删除某元素 #注:顺序表O(n) ,链表O(1)
#注:顺序表:Python里的列表 ,C里的数组 是 挨个存的
# 顺序表(列表/数组)与 链表
# 按元素值查找
# 注:复杂度都是O(n),一个一个查找,找到为止 (顺序表 不考虑排序)
# 按下标查找
# 注:顺序表快 顺序表O(1),链表O(n)因为下标是10,得从头开始数
# 在某元素后插入 顺序表O(n) ,链表O(1)
# 删除某元素 顺序表O(n) ,链表O(1)
#注:链表相对于 顺序表,在插入和删除 2方面非常快
#62 哈希表
#注:哈希表(散列表)
#注:Python的字典、集合 都是用哈希表 实现的
###### 哈希表
# 哈希表一个通过哈希函数来计算数据存 储位置的数据结构,通常支持如下操作:
# insert(key, value):插入键值对(key,value)
# get(key):如果存在键为key的键值对则返回其value,否则返回空值
# delete(key):删除键为key的键值对
#注:这个操作 就是字典的操作
#注:哈希表有key、有value。没有value 只有key 那就是集合(而且key不能重复)
###### 直接寻址表
# 当关键字的全域U比较小时,直接寻址是一种简单而有效的方法。
#注:左边的圆 是 U全域 和 key。根据这个U建了一个T 列表
#注:插入很快,查找很快,删除很快(把元素置成空)
# 直接寻址技术缺点:
# ·当域U很大时,需要消耗大量内存,很不实际 #注:T列表的大小 根据U的,很大的U 列表很大 浪费内存
# ·如果域U很大而实际出现的key很少,则大量空间被浪费 #注:U很大,但字典值很少,浪费内存
# ·无法处理关键字不是数字的情况 #注:key是字符串 有问题
#注:直接寻址表 上面 + 哈希函数,就成为 哈希表
###### 哈希
# 直接寻址表:key为k的元素放到k位置上 #注:比如 key是2的 就放到2的位置上。哈希不是
# 改进直接寻址表:哈希(Hashing)
# ·构建大小为m的寻址表T
# ·key为k的元素放到h(k)位置上
# ·h(k)是一个函数,其将域U映射到表T[0,1,...,m-1] #注:即 输出的值 是0到m-1,m是T列表的长度
#注:之前T 是根据U的大小建的,现在T 自己选个大小。之前 key是k的元素放到k的位置上,现在key为k的元素放到h(k)位置上
#注:h是个函数,能传的参数 是U域里所有的值,输出的值是 0到m-1, m是T列表的大小
#注:h是哈希函数
###### 哈希表
# 哈希表(Hash Table,又称为散列表),是一种线性表的存储结构。哈希表由一个直接寻址表和一个哈希函数组成。哈希函数h(k)将元素关键字k作为自变量,返回元素的存储下标。
#注:哈希表是线性的,不是图 也不是树
# 假设有一个长度为7的哈希表,哈希函数h(k)=k%7。元素集合{14,22,3,5}的存储方式如下图。
# 注:如果是直接寻址表,14存不了,只能存0到6
# 14 % 7 = 0 存到 0这个位置
# 22 % 7 = 1 存到1这个位置
# 3……、5……
# #注:哈希函数h(k)的取值范围 只能是0到6 (因为取余)
# #注:这就是非常经典的哈希函数,除法哈希
#注:再插入一个0 怎么办? 0 这个位置 已经有值了
###### 哈希冲突
# 由于哈希表的大小是有限的,而要存储的值的总数量是无限的,因此对于任何哈希函数,都会出现两个不同元素映射到同一个位置上的情况,这种情况叫做哈希冲突。
# 比如h(k)=k%7, h(0)=h(7)=h(14)=...
#注:哈希冲突一定会存在,因为开的哈希表 大小是有限的,肯定会有哈希函数2个key 对应1个位置。
#注:哈希函数 2个key 都到了一个位置上,这种现象叫做哈希冲突
###### 解决哈希冲突 -- 开放寻址法
# 开放寻址法:如果哈希函数返回的位置已经有值,则可以向后探查新的位置来存储这个值。
# 线性探查:如果位置i被占用,则探查i+1, i+2,……
# 二次探查:如果位置i被占用,则探查i+1^2 ,i-1^2 ,i+2^2 ,i-2^2 ,……
# 二度哈希:有n个哈希函数,当使用第1个哈希函数h1发生冲突时,则尝试使用h2, h3,……
#注:如果插入 有值 ,插入到其他地方。线性探查:i+1……;二次探查;二度哈希
#-----------
### 注:开放寻址法的 线性查找
#注:哈希表如何查值?比如:22,先找它的哈希函数 h(22),22%7=1,那就到1的位置上去找,直接找 找到了
#注:现在 比如说找0,0%7=0,0 位置找不到,怎么办?先在这查,如果不是 往后找,如果不是,往后找,直到找到空位为止。找到空位如果还没有找到,那么这个哈希表里面肯定没有0
#注:查找也需要 按照 线性探查方式 查找。插入的探查方式 和 查找的探查方式一样,到空位为止
#注:线性探查的效率并不是很高,它的装载因子过大。它的好多东西都特别密,很多东西都在一块
#-----------
### 注:开放寻址表的 二次探查
#注:i+1, i-1, i+4, i-4. i+9, i-9……
#注:它是跳着的,越跳越多
#-----------
### 注:开放寻址表 二度哈希
#注:有好多个哈希函数,如果一个哈希函数冲突了,换另一个
#注:开放寻址表 不太好,哈希表可能满了
###### 解决哈希冲突 -- 拉链法
# 拉链法:哈希表每个位置都连接一个链表,当冲突发生时,冲突的元素将被加到该位置链表的最后。
#注:拉链法:哈希表的一个格子里不是存一个元素,而是存一个链表
#注:冲突元素 如果发现冲突了,就放到这个链表后面 或者前面
#注:插入元素,如果位置是空的,就往后接一个;如果不是空,就在链表最后面或者最前面插一个 (头插尾插 都可以)
#注:查找的话 ,比如 查 155 在这个表里是否有。首先带入哈希函数,发现值是11,那就到11这个位置上来找,然后 就遍历这个链表, 91 不是 ,155 找到了
#注:删除:删除 先找到之后 再删。找到155 后 把它删除,链表的删除 也是O(1)的
#注:拉链法很经典,虽然没有直接寻址表快,但是直接寻址表 可能实现不了
#注:查找复杂度 不好说:如果哈希函数 足够好 ,节点足够平均分到哈希表上,整个的 有n个数,表长度是m。一个位置 有n/m个元素,那最多 只要查n/m次
#注:图示的 哈希函数 是对 16取余数
###### 哈希表 -- 常见哈希函数 *
# 除法哈希法:
# ·h(k) = k % m
# 乘法哈希法:
# ·h(k) = floor(m*(A*key%1))
# 全域哈希法:
# ·ha,b(k) = ((a*key + b) mod p) mod m a,b=1,2,...,p-1
#注:除法哈希 很经典,最好理解的一种哈希。直接对哈希表的长度m 取余数,拿到的值 就是它的下标
#注:乘法哈希 k是 关键字,m是 哈希表大小 ,A是一个值(A是一个小数), A*key%1, A乘以k对1取模,对1取模 就是取它的小数部分,再向下取整。floor是向下取整,比如说 3.6 向下取3
#注:全域哈希 ha,b(k) = ((a*key + b) mod p) mod m a,b=1,2,...,p-1
#注:a、b 2个参数 a*key + b,mod p 对p取模 (%),括起来对 m取模
#63 哈希表实现
#注:写哈希表 之前,先写个链表
#注:链表的创建
class LinkList: #注:链表类
class Node: #注:链表里的节点
def __init__(self, item=None):
self.item = item
self.next = None
class LinkListIterator: #注:这个类是一个迭代器 因为 支持__next__
def __init__(self, node):
self.node = node
def __next__(self):
if self.node: #注:如果node不是空
cur_node = self.node
self.node = cur_node.next
return cur_node.item
else:
raise StopIteration
def __iter__(self):
return self
def __init__(self, iterable=None): #注:构造函数。传一个列表
self.head = None
self.tail = None
if iterable: #注:如果有列表
self.extend(iterable)
#注:extend()接受一个列表参数 [1,2].extend([1,2,3]) [1,2,1,2,3]
#注:append()接受一个对象参数 [1,2].append([1,2,3]) [1,2,[1,2,3]]
def append(self, obj): #注:尾插
s = LinkList.Node(obj) #注:创建节点
if not self.head: #注:如果head是空
self.head = s
self.tail = s
else: #注:如果head不是空,插到尾巴上
self.tail.next = s
self.tail = s
def extend(self, iterable): #注:循环调appdend 就有extend了
for obj in iterable:
self.append(obj)
def find(self, obj): #注:在链表里查找,for循环查
for n in self: #注:self是linklist对象,self是迭代的支持这种写法
if n == obj:
return True
else:
return False
def __iter__(self): #注:写迭代器的 支持迭代
return self.LinkListIterator(self.head)
def __repr__(self): #注:转换成字符串
return "<<"+", ".join(map(str, self))+">>"
#注:map对于可迭代对象的每个元素 转换成字符串str
lk = LinkList([1,2,3,4,5]) #注:可迭代对象
for element in lk:
print(element)
#结果为
# 1
# 2
# 3
# 4
# 5
print(lk)
#结果 <<1, 2, 3, 4, 5>>
#链表创建 精简代码
class LinkList:
class Node:
def __init__(self, item=None):
self.item = item
self.next = None
class LinkListIterator:
def __init__(self, node):
self.node = node
def __next__(self):
if self.node:
cur_node = self.node
self.node = cur_node.next
return cur_node.item
else:
raise StopIteration
def __iter__(self):
return self
def __init__(self, iterable=None):
self.head = None
self.tail = None
if iterable:
self.extend(iterable)
def append(self, obj):
s = LinkList.Node(obj)
if not self.head:
self.head = s
self.tail = s
else:
self.tail.next = s
self.tail = s
def extend(self, iterable):
for obj in iterable:
self.append(obj)
def find(self, obj):
for n in self:
if n == obj:
return True
else:
return False
def __iter__(self):
return self.LinkListIterator(self.head)
def __repr__(self):
return "<<"+", ".join(map(str, self))+">>"
#注:在这基础上 写 哈希表
# 类似于集合的结构
class HashTable:
def __init__(self, size=101): #注:哈希表的构造函数,size哈希表的大小
self.size = size
self.T = [LinkList() for i in range(self.size)] #注:开一个T列表,每个位置都是一个链表(拉链法)
#注:刚开始T列表 每一个位置都是一个空链表 LinkList()
def h(self, k): #注:哈希函数
return k % self.size #注:对self.size取模
def insert(self, k): #注:插入
#注:计算哈希函数 返回的哈希值
i = self.h(k)
#注:k这个元素 要插到i这个位置上去
#注:判断这个元素在不在里面
if self.find(k): #注:如果找到了,我就不插入。达到哈希 去重的目的
print("Duplicated Insert.") #注:重复插入 提醒
else: #注:如果没找到,插入
self.T[i].append(k) #注:插入
def find(self, k): #注:先写 查找函数
i = self.h(k) #注:先找到k的哈希值
return self.T[i].find(k) #注:T[i] 是个链表
#注:哈希表 删除功能 没写。写删除的话 链表就得支持删除的功能
ht = HashTable() #注:创建HashTable对象
ht.insert(0)
ht.insert(1)
# ht.insert(0)
# #注:输入第3条语句时 ,提示 Duplicated Insert.
ht.insert(3)
ht.insert(102)
ht.insert(508)
print(",".join(map(str, ht.T)))
#注:打印这个哈希表,1和102在一个链表里,因为 哈希表的长度是101,102对101取余剩1
#注:508对101取余剩3
# <<0>>,<<1, 102>>,<<>>,<<3, 508>>,<<>>,<<>>,<<>>,…………
print(ht.find(3))
#结果为 True
print(ht.find(102)) #注:也能找到,它是个链表, 先去1那个位置上找,发现1 不是,102 是,找到了
#结果为 True
print(ht.find(203))
#结果为 False #注:因为发现 1 不是,102 不是,没了 返回一个false
#注:集合实现 跟它差不错
#哈希表 精简代码
# 类似于集合的结构
class HashTable:
def __init__(self, size=101):
self.size = size
self.T = [LinkList() for i in range(self.size)]
def h(self, k):
return k % self.size
def insert(self, k):
i = self.h(k)
if self.find(k):
print("Duplicated Insert.")
else:
self.T[i].append(k)
def find(self, k):
i = self.h(k)
return self.T[i].find(k)
ht = HashTable()
ht.insert(0)
ht.insert(1)
ht.insert(3)
ht.insert(102)
ht.insert(508)
#print(",".join(map(str, ht.T)))
print(ht.find(203))
#64 哈希表应用
#注:哈希表 的应用:Python里的 集合和字典 底层就是哈希表
###### 哈希表的应用 -- 集合与字典
# 字典与集合都是通过哈希表来实现的。
# ·a = {'name': 'Alex', 'age': 18, 'gender': 'Man'}
# 使用哈希表存储字典,通过哈希函数将字典的键映射为下标。假设h('name') = 3, h('age') = 1, h('gender') = 4,则哈希表存储为[None, 18, None, 'Alex', 'Man'] #注:3 1 4 号位置
# 如果发生哈希冲突,则通过拉链法或开发寻址法解决
#注:用哈希表来存,首先 把key传到 哈希函数里去(比如说key='name'),得到一个值 比如说等于3 ( h('name')=3 ),把'Alex'放到3号位置上 ……
#注:之前讲的哈希函数 它的参数 都是整数,字符串 也可以转换成一个整数,以某种方式
a = {'name': 'Alex', 'age': 18, 'gender': 'Man'}
h('name') = 3, h('age') = 1, h('gender') = 4
[None, 18, None, 'Alex', 'Man'] #注:哈希表存储
#注:如果说 字典 条目比较少的话,哈希函数够好,一个内存里 基本上不会拉链拉特别长,可能拉一个到两个,这样 你的按键查找会非常快,包括集合的查找
#注:一个列表 跟一个集合 去查,肯定是集合要快,因为这是 哈希表的查找速度指定的
#注:哈希表的应用 -- md5算法
#注:密码学 主要2个方面:1、加密 解密 2、哈希 比如说md5
#注:md5值 用的最多的是文件,可以用md5值 判断文件是不是一样的,如果这2个文件哈希值一样 其实不能说 这2个文件肯定一样,只能说很大很大概率 这2个文件一样。
#注:因为 md5值 也是一个哈希,有哈希 就一定有哈希冲突。文件可以哈希、字符串也可以哈希,什么都可以md5,哈希值一定是有限的,虽然是128位,2的128次方,但是必然会有2个文件的哈希值 是一样的。
#注:但是 md5值 要求 不能 有限的时间 人工构造,偶然碰上 概率太小
#注:防止的是 人工构造 ,或者说是欺骗
#注:md5 已经被破解了。在安全的方面不是特别重要的 地方可以用:比如 不太重要的网站、比较文件 ;但是 保密特别强的场合 不要用了
#注:2 云存储服务商 比如说 百度云 上传 电影
#注:密码学的哈希函数
#注:SHA-224 就是224位 ,256 就是256 位
#注:SHA-2、MD5 也是 哈希的一个应用,只不过 它的应用不是在存储方面,主要 用在 安全方面的一些校验 、比对 上
#注:哈希值不能反解的,相当于 证明SHA2 安全性 是不可破解的
#注:Python hashlib库里 有对应的函数
#注:哈希表 是一个 很高效的 做查找的数据结构
#65 树的概念
#注:之前讲了很多 线性的 数据结构:列表、链表、栈、队列、哈希表
#注:接下来看 树状数据结构
#注:树是一种数据结构 比如:目录结构
#注:树是一种可以递归定义的数据结构
#注:树是由n个节点组成的集合:
# 如果n=0,那这是一棵空树 #注:相当于递归的终止条件
# 如果n>0,那存在1个节点作为树的根节点,其他节点可以分为m个集合,每个集合本身又是一棵树
#注:n > 0,A是根节点,下面分了m个,m是6。最后都递归完了 汇拢在一起 发现是树
# 一些概念
# 根节点 #注:汇拢起来,最头上那个节点
# 叶子节点 #注:没有孩子的都叫做 叶子节点,因为叶子到头了
# 树的深度(高度) #注:示图 是4 往下走几层。A的深度是1,E的深度是2,J的深度是3,Q的深度是4
# 树的度 #注:节点的度 就是这个节点 分几个叉,比如E节点分2个叉 这个节点的度是2;F节点的度是3
#注:树的度 是这个树里边 所有节点的 最大的那个度。图示 树的度是 6
# 孩子节点/父节点 #注:A是D的父亲节点,D是A的一个孩子节点
# 子树 #注:E I J P Q 也是一棵树,以E为根节点的一棵树,这个树 是整个树的一棵子树,因为它是递归定义的,所以任意一个节点也是一棵子树 比如 节点 B
#66 树的实例:模拟文件系统
#注:树是由节点组成的,树的核心是节点
class Node: #注:先定义树的节点
def __init__(self, name, type='dir'):
self.name = name #注:文件名
self.type = type #"dir" or "file" #注:文件类型 默认dir
#注:节点和节点之间的关联
self.children = []
#注:链表有一个self.next 找它的下一个但是 树 这个下面可能有很多 不是有一个
#注:所以搞一个列表,好多next都放到这个列表里
self.parent = None #注:相当于 指向它的父母
# 链式存储
def __repr__(self):
return self.name #注:只打印 文件名
n = Node("hello") #注:文件夹 hello
n2 = Node("world")
n.children.append(n2) #注:这样 把hello文件夹下面的world文件夹 连起来
#注:这个操作 就相当于单链表,只不过 这里有很多个next
#注:这就是从 hello 找到 world,children 把n 连到n2
n2.parent = n #注:父母只有一个,树的结构决定的,所以不是列表
#注:这样 n2 也可以往回找了
#注:这样就有点像 双链表了。children往下找,相当于 链表 往后找;parent往上找,相当于链表的往前找
#注:一般来说 实现树的时候 children都是有的,parent属性有没有 看自己的需求。实现一棵只是往下链的树也行,只是不能往回找。往回找他的父亲的话,加一个parent
#注:linux文件系统
# /abc.txt
# /var/
class FileSystemTree: #注:写 树的类
def __init__(self): #注:初始化
self.root = Node("/") #注:首先 维护一个根
self.now = self.root #注:还需要 存现在 在哪个目录下
#注:模拟构造操作系统的命令
def mkdir(self, name): #注:传一个name
# name 以 / 结尾
if name[-1] != "/": #注:如果name结尾不是斜杠
name += "/" #注:就补上 /
node = Node(name) #注:创建一个文件夹
#注:再把这个文件夹 跟现在这个文件夹 连起来
self.now.children.append(node) #注:正着连上
node.parent = self.now #注:反着连过去
def ls(self): #注:展示
return self.now.children #注:打印当前目录的children
def cd(self, name): #注:切换目录 绝对路径与相对路径。这里 只可以进入下一级
"../var/python/"
#注:先按斜杠split 然后再一条一条进去,先执行.. 再循环执行这个函数
#注:如果支持绝对路径,还是先按照斜杠split,然后 第一个斜杠 前面肯定是个空,判断如果有空,你不是从now 走,而是从root开始走
#注:相对路径是从now开始走,绝对路径是从root开始走
#注:如果是文件进来的话,还要控制文件,不能让他append东西
if name[-1] != "/":
name += "/"
if name == "../": #注:cd支持向上返回一级
self.now = self.now.parent
return
for child in self.now.children: #注:如果children里有这个name
if child.name == name:
self.now = child #注:就切换目录
return
raise ValueError("invalid dir") #注:如果没有 就报错
tree = FileSystemTree() #注:新建一个树
tree.mkdir("var/")
tree.mkdir("bin/")
tree.mkdir("usr/")
print(tree.root.children)
#结果为 [var/, bin/, usr/]
print(tree.ls())
#结果为 [var/, bin/, usr/]
tree.cd("bin/") #注:进入 bin目录
tree.mkdir("python/") #注:在bin目录下创建 文件夹python
print(tree.ls())
#结果为 [python/]
tree.cd("../")
print(tree.ls()) #注:cd 回去
#结果为 [var/, bin/, usr/]
#注:树 在绝大部分的存储,都是跟链表一样 链式存储。通过往下走 往后指childre,往上 指parent。通过节点和节点之间相互连接的方式 组成树的数据结构
#精简代码
class Node:
def __init__(self, name, type='dir'):
self.name = name
self.type = type #"dir" or "file"
self.children = []
self.parent = None
# 链式存储
def __repr__(self):
return self.name
class FileSystemTree:
def __init__(self):
self.root = Node("/")
self.now = self.root
def mkdir(self, name):
# name 以 / 结尾
if name[-1] != "/":
name += "/"
node = Node(name)
self.now.children.append(node)
node.parent = self.now
def ls(self):
return self.now.children
def cd(self, name):
if name[-1] != "/":
name += "/"
if name == "../":
self.now = self.now.parent
return
for child in self.now.children:
if child.name == name:
self.now = child
return
raise ValueError("invalid dir")
tree = FileSystemTree()
tree.mkdir("var/")
tree.mkdir("bin/")
tree.mkdir("usr/")
tree.cd("bin/")
tree.mkdir("python/")
tree.cd("../")
print(tree.ls())
#结果为 [var/, bin/, usr/]
#67 二叉树概念
#注:二叉树 特殊的树,度不超过2的树,一个节点 最多分2个叉 有区别 左孩子和右孩子
#注:堆排序那里讲了 二叉树的线性存储方式,堆的存储 把数存成一个列表 [0, 1, 2, 3, ……],父亲找孩子 是 0 找 2i+1 ,2i+2。这种存储方式比较适用于完全二叉树(右边少,左边不会少东西)
#注:这个树 A左边少了好多节点,那这些节点就为空的,如果这个树非常不完全 差很多,那这些空的地方 就浪费空间,所以用另一种存储方式 链式存储
#注:跟树的存储方式是一样的。 只不过 刚才的树 是self.children是个列表,这里二叉树 不用列表 lchid ,rchild 2孩子
#注: lchid ,rchild 2 孩子 左孩子 右孩子 ,往下指2个指针。如果还需要往上面的话,加一个self.parent,不需要 就不用了
#注:data用来存节点的数据
###### 二叉树
# 二叉树的链式存储:将二叉树的节点定义为一个对象,节点之间通过类似链表的链接方式来连接。
# 节点定义:
class BitreeNode: #注:二叉树的节点
def __init__(self, data): #注:构造函数
self.data = data #注:存放数据
self.lchild = None # 左孩子
self.rchild = None # 右孩子
a = BitreeNode("A") #注:手动创建节点
b = BitreeNode("B")
c = BitreeNode("C")
d = BitreeNode("D")
e = BitreeNode("E")
f = BitreeNode("F")
g = BitreeNode("G")
e.lchild = a #注:连起来 ,e的左孩子
e.rchild = g
a.rchild = c
c.lchild = b
c.rchild = d
g.rchild = f #注:这个树连接完事了
root = e
print(root.lchild.rchild.data) #注:打印root左孩子的右孩子 其实就是节点
#结果为 C
#精简代码
class BitreeNode:
def __init__(self, data):
self.data = data
self.lchild = None # 左孩子
self.rchild = None # 右孩子
#68 二叉树遍历
#注:线性的数据结构 都比较好遍历 :列表 好遍历 for循环,链表 也好遍历 一直去next 取到空为止,栈、队列都好遍历
#注:二叉树怎么遍历?注:二叉树的遍历方式 有4种
###### 二叉树的遍历方式:
# 前序遍历:EACBDGF
# 中序遍历:ABCDEGF
# 后序遍历:BDCAFGE
# 层次遍历:EAGCFBD
class BitreeNode:
def __init__(self, data):
self.data = data
self.lchild = None # 左孩子
self.rchild = None # 右孩子
a = BitreeNode("A") #注:创建节点
b = BitreeNode("B")
c = BitreeNode("C")
d = BitreeNode("D")
e = BitreeNode("E")
f = BitreeNode("F")
g = BitreeNode("G")
e.lchild = a
e.rchild = g
a.rchild = c
c.lchild = b
c.rchild = d
g.rchild = f
root = e
######------注:二叉树的 前序遍历
#注:这个其实是 递归的定义的
def pre_order(root): #注:传一个节点进去
if root: #注:如果root不是空 (是空 就不打印了)
#注:root是空 相当于 是递归的终止条件
print(root.data, end=',') #注:先打印根节点
pre_order(root.lchild) #注:然后访问它的左子树
pre_order(root.rchild) #注:然后访问它的右子树
#注:pre_order(root) 如果不是空,先访问根,再访问左子树,再访问右子树
pre_order(root)
#结果为 E,A,C,B,D,G,F, #注:每个节点都打印一次
# 前序遍历:EACBDGF
#注:pre_order(root) 如果不是空,先访问根,再访问左子树,再访问右子树
#注:图示里面 先访问E,访问完了E, 再去访问 左子树A 递归的(A B C D) 再去访问右子树 (G F)
#注:在(A B C D) 里 ,先访问A ,然后递归左(没有) 递归右(B C D)
#注:(B C D) 里面 先访问 C ,再递归左 B 递归右 D
#注:访问G F ,先访问G ,递归左(没有),递归右(F)
#注:顺序 EACBDGF 前序遍历
######------注:二叉树的 中序遍历
#注:中序遍历 和 前序遍历不同的是 访问次序不一样
def in_order(root):
if root:
in_order(root.lchild) #注:先 递归左子树 访问左子树
print(root.data, end=",") #注:然后访问自己
in_order(root.rchild) #注:然后 递归右子树
#注:先递归左子树 访问自己 递归右子树
in_order(root)
#结果为 A,B,C,D,E,G,F,
#注:和前序遍历 差别是 访问的顺序不一样了。没有前序遍历那么直观
#注:中序遍历不太直观
#注:先递归左子树 访问自己 递归右子树
#注:所以E在中间…… (先把根节点放中间) 如图
######------注:二叉树的 后序遍历
#注:前序遍历 访问自己在最前面;中序遍历 访问自己在中间;后序遍历 访问自己在最后
def post_order(root):
if root:
post_order(root.lchild) #注:先 递归左
post_order(root.rchild) #注:然后 递归右
print(root.data, end=",") #注:最后 打印自己
#注:后序遍历,先 递归左;然后 递归右;最后 打印自己
post_order(root)
#结果为 B,D,C,A,F,G,E,
#注:先左(A B C D) 后右(G F) 最后自己 E ……
#注:先确定打印的 即最后的 放最后面
#注:这三种遍历 在面试中 被问到的比较多
#注:它还有一个别的类型的问题
#注:知道一棵二叉树的 前序遍历和 中序遍历,确定这棵树 并给出 后续遍历
#注:一但给2个遍历的序列,就能确定这个树
# 前序遍历:EACBDGF
# 中序遍历:ABCDEGF
#注:前序遍历 先访问自己,第1个一定是根 :E是根
#注:中序序列 根在最中间:知道E是根了 对应的去看中序序列;A B C D 是E的左孩子,G F 是E的右孩子
#注:前序序列 :A B C D 里,A是根 递归的
#注:再看 中序序列 :A B C D ,说明A 左子树 是空的,右子树 是 B C D
#注:接下来看B C D, 看前序序列 :C B D ,C是根
#注:再看中序序列, C是根 B C D ,B是C的左孩子,D是C的右孩子
#注:A、B、C、D 出来了,看G F
#注:看前序序列 G F,G是根
#注:F 看中序序列,G F ,F在右边,所以F是G的右孩子,G的左孩子是空
#注:这就是 前序序列 和 中序序列 还原出来这棵树,有了树之后,就可以画 后序序列了
#注:这是 给前中 可以推;同样给后序和中序 也可以画出来
#注:前序的时候 看第一个是根,后序的时候 最后一个是根,所以倒着看 同样的道理
#注:面试:实现 二叉树的 三种遍历方式:递归的写法。给2个序列,确定最后一个序列,或者说 给你2个序列,让你写函数 来构造出来这棵树
######------注:二叉树的 层次遍历
#注:很好理解 按层来 E A G C F B D ,一层一层的 每一层 从左到右
#注:怎么写?用到 队列 :刚开始访问E 没有问题,E出队 接下来E的孩子们进队 A G进队,现在队列里是A G
#注:A出队,A的孩子C进队, 队列里剩下 G C;G出队 ,G的孩子F进队,队列里剩下 C F;接下来 C出队 ,B D 进队;F 出队,没有孩子进队;B出队 没有孩子进队;D出队 没有孩子进队
#注:所以这个序列就是 层次遍历的序列 :EAGCFBD
#注:层次遍历 不光适用于 二叉树 ,多叉树 也可以。二叉树 左孩子进队 右孩子进队;多叉树 children 挨个进队
from collections import deque #注:使用之前queue队列
def level_order(root):
queue = deque() #注:首先创建空队列
queue.append(root) #注:然后 root进队
while len(queue) > 0: # 只要队不空,一直访问
#注:先出队一个元素 ,然后把它的孩子进队
node = queue.popleft() #注:出队
print(node.data, end=',') #注:打印
if node.lchild: #注:判断node的左孩子是不是有
queue.append(node.lchild) #注:如果有,就append node左孩子
if node.rchild: #注:如果node的右孩子有,它也进队
queue.append(node.rchild)
level_order(root)
#结果为 E,A,G,C,F,B,D,
#精简代码
class BitreeNode:
def __init__(self, data):
self.data = data
self.lchild = None # 左孩子
self.rchild = None # 右孩子
a = BitreeNode("A") #注:创建节点
b = BitreeNode("B")
c = BitreeNode("C")
d = BitreeNode("D")
e = BitreeNode("E")
f = BitreeNode("F")
g = BitreeNode("G")
e.lchild = a
e.rchild = g
a.rchild = c
c.lchild = b
c.rchild = d
g.rchild = f
root = e
#------ 前序遍历
def post_order(root):
if root:
post_order(root.lchild)
post_order(root.rchild)
print(root.data, end=",")
pre_order(root)
# 前序遍历:EACBDGF
#------ 中序遍历
def in_order(root):
if root:
in_order(root.lchild)
print(root.data, end=",")
in_order(root.rchild)
in_order(root)
# 中序遍历:ABCDEGF
#------ 后序遍历
def post_order(root):
if root:
post_order(root.lchild)
post_order(root.rchild)
print(root.data, end=",")
post_order(root)
# 后序遍历:BDCAFGE
#------ 层次遍历
from collections import deque
def level_order(root):
queue = deque()
queue.append(root)
while len(queue) > 0: # 只要队不空
node = queue.popleft()
print(node.data, end=',')
if node.lchild:
queue.append(node.lchild)
if node.rchild:
queue.append(node.rchild)
level_order(root)
# 层次遍历:EAGCFBD
#注:层次遍历 不光适用于 二叉树 还适用于 多叉树
#69 二叉搜索树的概念
#注:具体的 二叉树 一种用法:二叉搜索树
###### 二叉搜索树 (binary search tree bst)
# 二叉搜索树是一棵 二叉树 且满足性质:设x是二叉树的一个节点。如果y是x左子树的一个节点,那么y.key ≤ x.key;如果y是x右子树的一个节点,那么y.key ≥ x.key。
# 二叉搜索树的操作:查询、插入、删除
#注:y.key 就是y上存的那个值,可以存键值对 ,也可以只存一个键。左子树上的 所有节点的值 都比它小,右子树上的 所有节点的值 都比它大
#注:一个节点 左子树上的 所有节点的值 都比它小,右子树上的 所有节点的值 都比它大。如果所有节点 都满足这个性质,那么 他就是二叉搜索树
#注:有了这个性质后,可以做的操作:查询、插入、删除
#注:有了这个性质后,查询 非常好查:比如说 查11 在不在这个树里面,先和根比,11比根小,所以如果11在的话 一定在根的左边;那就到左边查,跟5比,发现11比5大,如果11存在,那一定在5的右边;然后发现11 找到了
#注:找7 ,跟17比 ,往左边找;跟5比 往右边找;跟11比 往左边找;跟9比 往左边找;跟8比往左边找 但是8左边是空了,没有了 找不到了
#注:所以 查询操作 只要执行 树的深度次 就可以了
#注:插入操作: 插入32 ,插肯定往叶子节点插 ,往最下面这层插;32 跟17比 ,往右边走,插到它的右子树上来;跟35 比,插到它的左边来;跟29比 ,插到它的右边 ,右边只要没有 ,就插到这来
#注:插入和查询差不多,反正就是 往深处走,走到一个地方,只要这个地方没有了 ,那就插在这
#注:查询 和 插入 复杂度 大概 logn 跟二分查找不一样 但是差不多,每次都少一半
#70 二叉搜索树:插入
class BiTreeNode: #注:树的节点
def __init__(self, data):
self.data = data
self.lchild = None # 左孩子
self.rchild = None # 右孩子
self.parent = None #注:查询和插入操作 不用它也行,但删除操作要用
#注:加了parent就是双链表
class BST:
def __init__(self, li=None): #注:构造函数 可以传一个列表进来
self.root = None #注:构造函数 先创造一个根节点
if li: #注:如果li不是None
for val in li: #注:就使用非递归的方法 把val插进来 。因为递归方法 慢
self.insert_no_rec(val)
#注:递归的写法
def insert(self, node, val): #注:插入函数 ,node就是递归的插到哪个节点上去
if not node: #注:如果node是空,就找到这个位置了,把值插入
node = BiTreeNode(val) #注:相当于创建一个节点,但还没和树连起来
#注:如果node不为空,分3种情况
elif val < node.data: #注:如果val比node节点小,往左走,递归调左孩子
node.lchild = self.insert(node.lchild, val) #注:左孩子传进来,递归左孩子。并把值传给node.lchild 因为返回它自己
#注:这是 插到node的左孩子里,让它两连起来;还要连parent
node.lchild.parent = node #注:它的左孩子的父亲是它
elif val > node.data: #注:如果 val > node.data , 递归右孩子,从右孩子里面找
node.rchild = self.insert(node.rchild, val)
node.rchild.parent = node #注:连parent
# else: #注:第3种情况 如果说 等于了
#注:如果说 等于的话,其实可以,但是一般情况下 默认键(值) 不能相等。等于 可以统一规定去右边(或者左边)。查找的话 只能说查找一个
#注:一般说 要求是 键值对,键不能重复。如果有这种情况 但是键会重复 ,可以给这个节点 加一个数据域,叫做count ,等于了 给这个节点count +1
return node
#注:这是 递归的写法
#注:非递归的写法 一般来说 非递归的比递归的快一点
def insert_no_rec(self, val):
#注:首先 一层层往下找(指针也好,其他也好),找一个p
p = self.root #注:p刚开始等于root
#注:如果来的节点 比它小,那p就往它左孩子走,……右孩子走
if not p: #注:空树的情况下,特殊处理下
self.root = BiTreeNode(val) #注:直接给root赋值一个就可以了
return
while True: #注:如果不是空树,循环 3种情况
if val < p.data: #注:往左子树上走;需要判断它左子树上 有没有
#注:如果左子树上有,那么p=左子树;如果左子树是None,就把val插到这
if p.lchild: #注:如果p的左子树 不是None
p = p.lchild #注:p往左子树上走一下
else: #注:左孩子不存在
p.lchild = BiTreeNode(val) #注:就把值插到这
p.lchild.parent = p #注:双向的指定 ,连接parent
return
elif val > p.data: #注:第2种情况,和上面一样的,把l换成r
if p.rchild: #注:如果 右孩子存在
p = p.rchild #注:p就往右孩子上走
else: #注:如果 右孩子不存在
p.rchild = BiTreeNode(val) #注:它的右孩子 就等于 一个新的节点
p.rchild.parent = p #注:再建立parent连接
return
else: #注:等于的情况,什么都不干
return
#注:Python的集合也是这样,插入的时候 如果重复,什么都不提示,但是它没有插入
#注:前面讲的 三种遍历:前序遍历、中序遍历、后序遍历
def pre_order(self,root):
if root:
print(root.data, end=",")
self.pre_order(root.lchild)
self.pre_order(root.rchild)
def in_order(self,root):
if root:
self.in_order(root.lchild)
print(root.data, end=",")
self.in_order(root.rchild)
def post_order(self,root):
if root:
self.post_order(root.lchild)
self.post_order(root.rchild)
print(root.data, end=",")
tree = BST([4,6,7,9,2,1,3,5,8])
tree.pre_order(tree.root)
print("")
tree.in_order(tree.root)
print("")
tree.post_order(tree.root)
#结果为
# 4,2,1,3,6,5,7,9,8,
# 1,2,3,4,5,6,7,8,9,
# 1,3,2,5,8,9,7,6,4,
#注:由前序和中序,可以画出这棵树,这肯定是一个满足二叉搜索树的情况
#注:中序遍历 1,2,3,4,5,6,7,8,9, 是排好序的,是巧合吗?
import random
li = list(range(500))
random.shuffle(li)
tree = BST(li)
tree.in_order(tree.root) #注:中序序列 排好序的,不是巧合
#结果为 0,1,2,3,4,5,6,7,8,9,10
#注:二叉搜索树的 中序序列 一定是升序的
#注:中序序列 先左,再自己,再右。所以 先输出的 一定是整个序列最小的,因为 左孩子永远是最小的
#注:在 二叉搜索树里,左比中小,右比中大;左先出去 小的先出,然后中 右,从小到大
#精简代码
import random
class BiTreeNode:
def __init__(self, data):
self.data = data
self.lchild = None # 左孩子
self.rchild = None # 右孩子
self.parent = None
class BST:
def __init__(self, li=None):
self.root = None
if li:
for val in li:
self.insert_no_rec(val)
# 递归写法
def insert(self, node, val):
if not node:
node = BiTreeNode(val)
elif val < node.data:
node.lchild = self.insert(node.lchild, val)
node.lchild.parent = node
elif val > node.data:
node.rchild = self.insert(node.rchild, val)
node.rchild.parent = node
return node
# 不递归的写法
def insert_no_rec(self, val):
p = self.root
if not p: # 空树
self.root = BiTreeNode(val)
return
while True:
if val < p.data:
if p.lchild:
p = p.lchild
else: # 左孩子不存在
p.lchild = BiTreeNode(val)
p.lchild.parent = p
return
elif val > p.data:
if p.rchild:
p = p.rchild
else:
p.rchild = BiTreeNode(val)
p.rchild.parent = p
return
else:
return
# 三种排序
def pre_order(self, root):
if root:
print(root.data, end=',')
self.pre_order(root.lchild)
self.pre_order(root.rchild)
def in_order(self, root):
if root:
self.in_order(root.lchild)
print(root.data, end=',')
self.in_order(root.rchild)
def post_order(self, root):
if root:
self.post_order(root.lchild)
self.post_order(root.rchild)
print(root.data, end=',')
li = list(range(500))
random.shuffle(li)
tree = BST(li)
tree.pre_order(tree.root)
print("")
tree.in_order(tree.root)
print("")
tree.post_order(tree.root)
#结果为
# 前序遍历……
# 中序遍历0,1,2,3,4,5,6,7,……
# 后序遍历……