文章目录
数据结构与算法 (Python 版)
知识点一 : 算法 概念
1.概述:
1.1 算法 (Algorithm)即一个计算过程, 即用来解决问题的方法.
1.2 著名 IT 科学家尼古拉斯·沃斯 (Niklaus Wirth) 认为 " 程序 = 数据结构 + 算法 "
知识点二 : 时间复杂度
1.概述:
1.1 时间复杂度: 用来评估算法运行效率的一个式子 ( 即用一个类似于公式的东西能够形象的比较两个算法的快慢 )
1.2 为什么不使用时间来体现算法的运行快慢?
- 电脑本身内存的运行效率有差异.
- 执行代码的次数有差异.(如for循环1次,10次,100次都有所不同)
2.案例:
2.1 打印一行代码
我们将一个print定义为叫O(1),我们将O理解为一个约等号,大约的意思,括号中的东西就类似于单位,1就是一个单位,类似于秒这个单位,但不是1秒,而就是1,它没有后缀.
# 只执行了一次,时间消耗大约为1,即O(1)
print("Hello World")
2.2 打印一个for循环
# i执行n次, 时间消耗大约为 1×n, 即O(n)
for i in range(n):
print("Hello World")
2.3 打印两个for循环
# i执行1次时,j执行n次 时间消耗大约为 1×n,即O(n)
# 然而i本身还要执行n次,所以是 n×(1×n), 最终的执行时间为 O(n^2)
for i in range(n):
for j in range(n):
print("Hello World")
2.4 打印三个for循环
# 同两个for循环,for循环执行n次即时间消耗O(n),三次循环即 n×n×n,故最终的消耗时间为O(n^3)
for i in range(n):
for j in range(n):
for k in range(n):
print("Hello World")
2.5 打印三行代码
# 执行三次,因为执行三次print和执行一次print的的差距几乎忽略不计,时间复杂度 O(1)
# 因为按正常逻辑来说,执行三次print的时间复杂度本来应是3×1 即O(3)
# 但是因为时间复杂度的低精度性,就直接被约算成了O(1)
print("Hello World")
print("Hello Python")
print("Hello Algorithm")
注意点1 : 执行基本操作,只要它的问题规模不上升到n这么大的时候,它的时间复杂度就是O(1)
(就像是我们睡觉只会说大约睡了几个小时,而不是说睡了几个小时几分几秒.)注意点2 : 时间复杂度是一个低精度的计算方式, 只算一个大致的内容即可.
2.6 打印双重for-print循环
# 同理两次for循环应该是n^2,又因为多打印了n次Hello World
# 正常来说,时间复杂度应该是O(n^2+n)
# 但是同样是因为时间复杂度的低精度性,故而这里的时间复杂度应该是O(n^2)
for i in range(n):
print("Hello World")
for j in range(n):
print("Hello World")
2.7 打印减半while循环
# 当n=64时输出:64,32,16,8,4,2
# 代码每执行一次,n的值都比之前少一半
# 2^6 = 64 → 1og~2~64 = 6
# 所以将时间复杂度记为O(log~2~n) 或 O(logn)
# 循环减半logn
while n > 1:
print(n)
n = n // 2
3.总结:
3.1 时间复杂度是用来估计算法运行时间的一个式子 (单位).
3.2 一般来说 (即在基本上相同条件下), 时间复杂度高的算法比复杂度低的算法慢.
3.3 常见的时间复杂度 (按效率排序):
O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n2logn) < O(n3 )
3.4 复杂问题的时间复杂度:
O(n!) O(2n) O(nn) …
3.5 快速判断算法复杂度 (适用于绝大多数简单情况,复杂情况需要根据算法执行过程判断):
- 确定问题规模 n → 如列表排序,需要先确定列表的长度
- 循环减半过程 → logn
- k 层关于 n 的循环 → nk (几层循环就是n的几次方)
知识点三 : 空间复杂度
1.概述:
1.1 空间复杂度: 用来评估算法内存占用大小的式子
(小电影100MB能看就不要去下300MB的看,代码同理,能用10MB解决的问题就不要用30MB存储去多占用空间.)
1.2 空间复杂度表示:
- 算法使用了几个变量: O(1)
- 算法使用了长度为 n 的一维列表: O(n)
- 算法使用了 m 行 n 列的二维列表: O(mn)
(空间复杂度一般来说有O(1)、O(n)、O(n2)、O(logn)、O(NlogN)几种.但是其实最常用的也就是O(1) 或者 O(n),用到O(n2)的情况都不多)
1.3 空间换时间:
在我们写算法时,时间复杂度和空间复杂度只能二选一进行优化,但最好是优化时间复杂度,毕竟在这个学习资料都要放3个T的时代,存储空间并不算昂贵,但是时间真的很宝贵。故而优化时:时间复杂度>空间复杂度.
知识点四 : 递归
1.概述:
1.1 递归的两个特点:
- 调用自身
- 结束条件
2.实例:
2.1 先打印后递归
# 标准递归函数:先打印后递归
def func1(x):
if x > 0: # 1.有结束条件
print(x)
func1(x-1) # 2.调用自身
func1(3)
2.2 先递归后打印
# 标准递归函数: 先递归后打印
def func2(x):
if x > 0: # 1.有结束条件
func2(x-1) # 2.调用自身
print(x)
func2(3)
知识点五 : 递归 汉诺塔问题
1.题目
原版: 汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞n片黄金圆盘。大梵天命令婆罗门把圆盘从下自上开始、按大小顺序重新摆放在另一根柱子上。并且规定,小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘,如图所示。问应该怎样移动,才能将圆盘移动到另一根柱子上。
数学版: 从左到右有A、B、C三根柱子,其中A柱子上面有从小叠到大的n个圆盘,现要求将A柱子上的圆盘移到C柱子上去,期间只有一个原则:一次只能移到一个盘子且大盘子不能在小盘子上面,求移动的步骤和移动的次数。
2.思路
(1) 假设 n = 2 时:
第一步: 把 圆盘1 从 A 移动到 B
第二步: 把 圆盘2 从 A 移动到 C
第三步: 把圆盘1 从 B 移动到 C
(2) 假设有n个盘子时:
第一步: 把 n-1 个圆盘 从 A 经过 C 移动到 B
第二步: 把 第n个圆盘 从 A 移到 C
第三步: 把 n-1 个圆盘 从 B 经过 A 移到到 C
3.代码
# -*- coding: utf-8 -*-
# 定义一个有参无返回值函数,n是圆盘个数, abc分别对应ABC三柱
def HanNuoTa(n, a, b, c):
if n > 0:
# 1.把 n-1 个圆盘 从 A 经过 C 移动到 B
HanNuoTa(n - 1, a, c, b)
# 2.把 第n个圆盘 从 A 移到 C
print(f"moving from {a} to {c}")
# 3.把 n-1 个圆盘 从 B 经过 A 移到到 C
HanNuoTa(n - 1, b, a, c)
HanNuoTa(3, 'A', 'B', 'C')
4.小结
- 汉诺塔移动次数的递推式: h(x) = 2h(x-1) + 1
- h (64) =18446744073709551615
- 假设婆罗门每秒钟搬一个盘子,则总计需要5800亿年
知识点六 : 查找
- 查找 : 在一些数据元素中, 通过一定的方法找出与给定关键字相同的数据元素的过程.
- 列表查找 ( 线性表查找 ) : 从列表中查找指定元素
- 输入 : 列表、待查找元素
- 输出 : 元素下标 ( 未找到元素时一般返回None或-1 )
- 内置列表查找函数 : index( )
1.顺序查找
- 顺序查找 ( Linear Search ) : 也称线性查找, 从列表第一个元素开始顺序进行搜索, 直到找到元素或搜索到列表最后一个元素为止.
- 顺序查找时间复杂度 : O(n)
# -*- coding: utf-8 -*-
def linear_search(li, val):
# enumerate(teration, start): 枚举函数, 默认包含两个参数.
# 1.iteration参数:需要遍历的参数 (如字典、列表、元组等)
# 2.start参数:开始的参数,默认为0(不写start那就是从0开始)
# 3.enumerate函数有两个返回值,第一个返回值为从start参数开始的数,第二个参数为iteration参数中的值。
for index, value in enumerate(li):
if value == val:
return index
else:
return None
2.二分查找
-
二分查找 (Binary Search) : 也称折半查找, 针对一个有序数据集合, 每次都通过跟区间的中间元素对比, 将待查找的区间缩小为之前的一半, 直到找到要查找的元素, 或者区间被缩小为 0.
-
二分查找图解 : 从列表中查找元素3.
-
二分查找时间复杂度 : O(logn)
-
二分查找与顺序查找比较 : 二分查找的效率远高于顺序查找.
-
二分查找的局限性 :
- 二分查找针对的是有序数据, 也就是说我们使用二分查找前必须对数据进行排序.
- 数据量太小和太大都不适用于二分查找, 太小可以直接用顺序查找, 太大电脑内存撑不住.
# -*- coding: utf-8 -*-
def binary_search(li, val):
left = 0
right = len(li) - 1
# 候选区有值
while left <= right:
mid = (left + right) // 2
if li[mid] == val:
return mid
# 待查找的值在mid左侧
elif li[mid] > val:
right = mid - 1
# 待查找的值在mid右侧
else:
left = mid + 1
else:
return None
知识点七 : 排序
-
排序 : 将一组 “无序” 的记录序列调整为 “有序” 的记录序列
-
列表排序 : 将无序列表变为有序列表
- 输入 : 列表
- 输出 : 有序列表
-
升序与降序
- 升序 : 按从小到大的顺序排列 (如1、3、5、6、7、9)
- 降序 : 按从大到小的顺序排列 (如9、8、6、4、3、1)
-
内置排序函数 : sort( )
-
常见排序算法 :
排序 Low B 三人组 排序NB三人组 其他排序 1.冒泡排序 1.快速排序 1.希尔排序 2.选择排序 2.堆排序 2.计数排序 3.插入排序 3.归并排序 3.基数排序
1.冒泡排序
-
冒泡排序 (Bubble Sort):
- 列表每两个相邻的数, 如果前面比后面大, 则交换这两个数
- 一趟排序完成后, 则无序区减少一个数, 有序区增加一个数
- 代码关键点 : 趟、无序区范围
- 执行 n-1 趟 ( n是列表的长度 )
-
冒泡排序的时间复杂度 : O(n2)
-
冒泡排序实例 :
初始数据:[ 30,13,25,16,47,26,19,29,10 ]
第一趟执行过程(先从30依次开始比较):
0 1 2 3 4 5 6 7 8 13 30 25 16 47 26 19 29 10 13 25 30 16 47 26 19 29 10 13 25 16 30 47 26 19 29 10 13 25 16 30 47 26 19 29 10 13 25 16 30 26 47 19 29 10 13 25 16 30 26 19 47 29 10 13 25 16 30 26 19 29 47 10 13 25 16 30 26 19 29 10 47 第一趟 : [13 25 16 30 26 19 29 10 47]
第二趟:[13 16 25 26 19 29 10 30 47]
…
第八趟: [10 13 16 19 25 26 29 30 47]
注意点1 : 冒泡排序相当于是一个接力,从最左侧的一个 记录(30)开始从左到右开始比较,遇到比自己小的数就将其甩在身后(放到30的左边),遇到比自己大的数就把接力棒交给它(40),直到这一趟中的最大记录到达最右侧,然后开始新一轮接力跑(上一趟的最大记录不再参与此趟)
# -*- coding: utf-8 -*-
import random
def bubble_sort(li):
# 第i趟
for i in range(len(li) - 1):
for j in range(len(li) - i - 1):
exchange = False
# >是执行升序排序,<是执行降序排序
if li[j] > li[j + 1]:
li[j], li[j + 1] = li[j + 1], li[j]
exchange = True
# print(f"第{i+1}趟:{li}")
# if not exchange 的意思是 if not False,因为我们之前将False这个值赋给了exchange这个变量
if not exchange:
return
li = [random.randint(0, 100) for i in range(10)]
print(f"执行冒泡排序前:{li}")
bubble_sort(li)
print(f"执行冒泡排序后:{li}")
2.选择排序
- 选择排序 (Select Sort) : 一趟排序记录最小的数, 将其放到第一个位置; 再一趟排序记录列表无序区最小的数, 放到第二个位置.
- 选择排序算法关键点 : 有序区和无序区、无序区最小数的位置
- 选择排序的时间复杂度 : O(n2)
# -*- coding: utf-8 -*-
# 选择排序简单版
# 缺陷:开辟了新的存储空间,算法的利用率低
import random
def select_sort_simple(li):
li_new = []
for i in range(len(li)):
min_val = min(li)
li_new.append(min_val)
li.remove(min_val)
return li_new
li = [random.randint(0,100) for i in range(10)]
print(f"执行简单选择排序前:{li}")
print(f"执行简单选择排序后:{select_sort_simple(li)}")
# -*- coding: utf-8 -*-
import random
# 选择排序完整版
def select_sort(li):
# i = 0,1,2,3,4,5,6,7,8
for i in range(len(li)-1):
min_loc = i
# i+1少比一次,因为我们设的最开始的最小值为i,相当于减少了它自己跟自己比的那一趟
# j = 1,2,3,4,5,6,7,8,9
for j in range(i+1, len(li)):
# 现在在li列表中j就是i的后一个下标,只要j对应的那个元素比i现在对应的那个元素小,两者就交换位置
if li[j] < li[min_loc]:
min_loc = j
li[i], li[min_loc] = li[min_loc], li[i]
# print(li)
li = [random.randint(0,100) for i in range(10)]
print(f"执行选择排序前:{li}")
select_sort(li)
print(f"执行选择排序后:{li}")
3.插入排序
-
插入排序 (InsertionSort) : 每一步将一个待排序的数据插入到前面已经排好序的有序序列中,直到插完所有元素为止.
-
插入排序的时间复杂度 : O(n2)
-
插入排序图解 :
# -*- coding: utf-8 -*-
import random
def insert_sort(li):
# i表示摸到的牌的下标
for i in range(1, len(li)):
tmp = li[i]
# j表示手里的牌的下标
j = i - 1
while j >= 0 and li[j] > tmp:
li[j + 1] = li[j]
j -= 1
li[j + 1] = tmp
print(li)
li = [random.randint(0, 100) for i in range(10)]
print(f"执行插入排序前:{li}")
insert_sort(li)
print(f"执行插入排序后:{li}")
4.快速排序
-
快速排序 (Quick sort) : 是对冒泡排序的一种改进.
-
快速排序思路:
- 取一个元素 p (第一个元素), 使元素p归位
- 列表被p分成两部分, 左边都比p小, 右边都比p大
- 递归完成排序
-
快速排序的时间复杂度: O(nlogn)
-
快速排序思路图解:
-
快速排序框架图解:
假设对以下10个数进行快速排序:
我们先模拟快速排序的过程:首先,在这个序列中随便找一个数作为基准数,通常为了方便,以第一个数作为基准数。
在初始状态下,数字6在序列的第1位。我们的目标是将6挪到序列中间的某个位置,假设这个位置是k。现在就需要寻找这个k,并且以第k位为分界点,左边的数都≤ 6,右边的数都≥ 6。那么如何找到这个位置k呢?
我们要知道,快速排序其实是冒泡排序的一种改进,冒泡排序每次对相邻的两个数进行比较,这显然是一种比较浪费时间的。
而快速排序是分别从两端开始”探测”的,先从右往左找一个小于6的数,再从左往右找一个大于6的数,然后交换他们。这里可以用两个变量i和j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i 指向序列的最左边,指向数字6。让哨兵j指向序列的最右边,指向数字8。
首先哨兵j开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵j先出动,这一点非常重要。哨兵j一步一步地向左挪动(即j = j − 1),直到找到一个小于6的数停下来。接下来哨兵i再一步一步向右挪动(即i = i + 1),直到找到一个数大于6的数停下来。最后哨兵j停在了数字5面前,哨兵i ii停在了数字7面前。
现在交换哨兵 i i i和哨兵 j j j所指向的元素的值。交换之后的序列如下。
到此,第一次交换结束。接下来开始哨兵j 继续向左挪动(再友情提醒,每次必须是哨兵j先出发)。他发现了4 < 6,停下来。哨兵i ii也继续向右挪动的,他发现了9 > 6,停下来。此时再次进行交换,交换之后的序列如下。
第二次交换结束。哨兵j jj继续向左挪动,他发现了3 < 6,又停下来。哨兵i 继续向右移动,此时哨兵i 和哨兵j 相遇了,哨兵i 和哨兵j 都走到3面前。说明此时“探测”结束。我们将基准数6和3进行交换。交换之后的序列如下。
到此第一轮“探测”真正结束。现在基准数6已经归位,此时以基准数6为分界点,6左边的数都小于等于6,6右边的数都大于等于6。回顾一下刚才的过程,其实哨兵j的使命就是要找小于基准数的数,而哨兵i的使命就是要找大于基准数的数,直到i ii和j jj碰头为止。
现在我们将第一轮“探测"结束后的序列,以6为分界点拆分成两个序列,左边的序列是“3 1 2 5 4”,右边的序列是“9 7 10 8”。接下来还需要分别处理这两个序列。因为6左边和右边的序列目前都还是很混乱的。不过不要紧,我们已经掌握了方法,接下来只要模拟刚才的方法分别处理6左边和右边的序列即可。现在先来处理6左边的序列现吧。
重复第一轮的过程,应该得到如下序列:
OK,现在3已经归位。接下来需要处理3左边的序列:
处理之后,2已经归位,序列“1”只有一个数,也不需要进行任何处理,因此“1”也归位。
对于基数右边的序列,采用和左边相同的过程;最终将会得到这样的序列,如下。
快速排序的每一轮处理其实就是将这一轮的基准数归位,直到所有的数都归位为止,排序结束。
快速排序之所以比较快,是因为与冒泡排序相比,每次的交换时跳跃式的,每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的都是 O(n2),它的平均时间复杂度为O(n log2n)。
# -*- coding: utf-8 -*-
import random
# 第一步:partition函数
def partition(li, left, right):
tmp = li[left]
while left < right:
# 从右边找比tmp小的数
while left < right and li[right] >= tmp:
# 往左走一步
right -= 1
# 把右边的值写到左边空位上
li[left] = li[right]
# print(li, "right")
while left < right and li[left] <= tmp:
left += 1
# 把左边的值写到右边的空位上
li[right] = li[left]
# print(li, "left")
# tmp归位
li[left] = tmp
return left
# 第二步:快速排序框架
def quick_sort(li, left, right):
# 至少要两个元素
if left < right:
mid = partition(li, left, right)
quick_sort(li, left, mid-1)
quick_sort(li, mid+1, right)
li = [random.randint(0, 100) for i in range(10)]
print(f"执行插入排序前:{li}")
quick_sort(li, 0, len(li) - 1)
print(f"执行插入排序后:{li}")
5.堆排序
-
5.1 堆排序前置知识点1: 树
-
树是一种数据结构 (如目录结构) ,可以递归定义.
-
树是由 n 个节点组成的集合
- 如果 n = 0, 那么这是一棵空树 ;
- 如果 n > 0, 那么存在1个节点作为树的根节点, 其他节点可以分为 m 个集合, 每个集合本身又是一棵树
-
树的基本概念1: 节点
- 根节点 : 树最顶部的一个节点, 如节点A.
- 叶子节点: 树最末端的节点, 如节点B, C, H, I, P, Q, K, L, M, N
- 子节点: 除根节点之外, 并且本身下面还连接有节点的节点, 如节点D, E, J, F, G
-
树的基本概念2: 高度 / 深度
-
树的基本概念3: 树的度
-
树的基本概念4: 孩子节点 / 父节点
-
树的基本概念5: 子树
-
-
5.2 堆排序前置知识点2: 二叉树
-
二叉树: 度不超过2的树 (即分支不能多于两条)
- 二叉树每个节点资源的有两个孩子节点
- 二叉树有两个孩子节点时, 严格区分左孩子节点和右孩子节点
-
特殊形态的二叉树:
- 满二叉树: 一个二叉树如果每一个层的节点都达到最大值, 则这个二叉树就是满二叉树.
- 完全二叉树: 叶节点只能出现在最下层和次下层, 并且最下面一层的节点都集中在该层最左边的若干位置的二叉树.
-
二叉树的存储方法 (表示方式):
- 链式存储方式
- 顺序存储方式
-
-
5.3 堆 定义: 一种特殊的完全二叉树结构
-
大根堆: 一棵完全二叉树, 满足任一节点都比其他孩子节点大
-
小根堆: 一棵完全二叉树, 满足任一节点都比其他孩子节点小
-
-
5.4 堆 向下调整:
-
假设根节点的左右子树都是堆,但根节点不满足堆的性质
-
可以通过一次向下的调整来将其变成一个堆
-
-
5.5 堆 排序过程:
-
1.建立堆
-
2.得到堆顶元素,为最大元素
-
3.去掉堆顶,将堆最后一个元素放到堆顶, 此时可通过一次向下调整重新使堆有序
-
4.堆顶元素为第二大元素
-
5.重复步骤3,直到堆变空
# -*- coding: utf-8 -*- import random def sift(li, low, high): """ :param li: 列表 :param row: 堆的根节点位置 :param high: 堆的最后一个元素的位置 :return: """ i = low # i最开始指向根节点 j = 2 * i + 1 # j最开始是左孩子 tmp = li[low] # 把堆顶存起来 while j <= high: # 只要j位置有数 if j+1 <= high and li[j+1] > li[j]: # 如果右孩子有且比较大 i = j + 1 # j指向右孩子 if li[j] > tmp: li[i] = li[j] i = j j = 2 * i + 1 else: # tmp更大,把tmp放到i的位置上 li[i] = tmp # 把tmp放到某一级领导的位置上 break else: li[i] = tmp # 把tmp放到叶子节点上 def heap_sort(li): n = len(li) for i in range((n-2)//2, -1, -1): # i表示建堆的时候调整的部分的根的下标 sift(li, i, n-1) # 建堆完成 for i in range(n-1, -1, -1): # i指向当前堆的最后一个元素 li[0], li[i] = li[i], li[0] # i-1是新的high sift(li, 0, i-1) li = [i for i in range(10)] # random.shuffle方法:对元素进行重新排序,打乱原有的顺序,返回一个随机序列(当然此处随机序列属于伪随机,即可重现), 该方法的作用类似洗牌 random.shuffle(li) print(li) heap_sort(li) print(li)
图片/数据结构与算法35.gif
-
-
5.6 堆排序时间复杂度: O(nlogn)
-
5.7 Python内置的堆排序模块heapq:
-
heapify ( x ) : 建堆
-
heappush ( heap, item ) : 建立大根堆、小根堆
-
heappop ( heap ) : 从堆中弹出并返回最小的值
# -*- coding: utf-8 -*- import heapq import random li = list(range(100)) print(li) random.shuffle(li) print(li) # 建堆 heapq.heapify(li) n = len(li) for i in range(n): # 执行一次heappop,则弹出一个元素,这个元素是列表中最小的元素 print(heapq.heappop(li), end=",")
-
-
5.8 堆排序 Topk问题
-
题目: 现在有n个数, 设计算法得到前K大的数. ( k < n )
-
思路:
- 1.取列表前k个元素建立一个小根堆. 堆顶就是目前第k大的数
- 2.依次向后变量原列表. 对于列表中的元素, 如果小于堆顶, 则忽略该元素; 如果大于堆顶, 则将堆顶更换为该元素, 并且堆堆进行一次调整;
- 3.遍历列表所有元素后,倒序弹出堆顶.
-
代码:
# -*- coding: utf-8 -*- import random def sift(li, low, high): i = low # i最开始指向根节点 j = 2 * i + 1 # j最开始是左孩子 tmp = li[low] # 把堆顶存起来 while j <= high: # 只要j位置有数 # li[j+1] < li[j] 小根堆 if j+1 <= high and li[j+1] < li[j]: # 如果右孩子有且比较大 i = j + 1 # j指向右孩子 if li[j] < tmp: # li[j] < tmp 小根堆 li[i] = li[j] i = j j = 2 * i + 1 else: # tmp更大,把tmp放到i的位置上 li[i] = tmp # 把tmp放到某一级领导的位置上 break else: li[i] = tmp # 把tmp放到叶子节点上 def topk(li, k): heap = li[0:k] for i in range((k-2)//2, -1, -1): sift(heap, i, k-1) # 1.建堆 for i in range(k, len(li)-1): if li[i] > heap[0]: heap[0] = li[i] sift(heap, 0, k-1) # 2.遍历 for i in range(k-1, -1, -1): heap[0], heap[i] = heap[i], heap[0] sift(heap, 0, i-1) # 3.出数 return heap li = list(range(100)) random.shuffle(li) print(topk(li, 10))
-
6.归并排序
-
6.1 归并定义 : 将一个分两段有序的列表合成一个有序列表的操作叫做归并
-
6.2 归并原理图解 :
-
6.3 归并排序思路 :
-
分解 : 将列表越分约小, 直至分成一个元素
-
终止条件 : 一个元素是有序的
-
合并 : 将两个有序列表归并, 列表越来越大
-
-
6.4 归并排序时间复杂度 : O(nlogn)
-
6.5 归并排序空间复杂度 : O(n)
-
6.6 归并排序代码实现 :
# -*- coding: utf-8 -*- import random # 归并代码 def merge(li, low, mid, high): i = low j = mid + 1 ltmp = [] while i <= mid and j <= high: # 只要左右两边都有数 if li[i] < li[j]: ltmp.append(li[i]) i += 1 else: ltmp.append(li[j]) j += 1 # while执行完毕,肯定是有一部分没数了 while i <= mid: ltmp.append(li[i]) i += 1 while j <= high: ltmp.append(li[j]) j += 1 li[low:high + 1] = ltmp # 归并排序代码 def merge_sort(li, low, high): # 至少有两个元素,递归思想 if low < high: mid = (low + high) // 2 merge_sort(li, low, mid) merge_sort(li, mid+1, high) merge(li, low, mid, high) li = list(range(10)) random.shuffle(li) print(li) merge_sort(li, 0, len(li)-1) print(li)
-
6.7 NB三人组小结 :
- 1.三种排序算法的时间复杂度都是 O(nlogn)
- 2.一般情况下就运行时间而言 :
- 快速排序 < 归并排序 < 堆排序
- 3.三种排序算法的缺点 :
- 快速排序 : 极端情况下排序效率低
- 归并排序 : 需要额外的内存开销
- 堆排序 : 在快的排序算法中相对较慢
7.希尔排序
-
7.1 希尔排序 (Shell Sort) : 希尔排序是插入排序的一种, 是直接插入排序算法的进阶版本.
-
7.2 希尔排序图解:
-
7.3 希尔排序的时间复杂度 : O(n1.3) ~ O(n2)
- (较为复杂, 与选取的gap序列有关)
-
7.4 希尔排序思路 :
- 首先取一个整数 d1=n/2 ,将元素分为d1个组, 每组相邻量元素直接的距离为d1, 在各组内进行直接插入排序;
- 取第二个整数d2=d1/2, 重复上述分组排序过程, 直到d1=1, 即所有元素在同一组内进行直接插入排序.
- 希尔排序每趟并不使某些元素有序,而是使整体数据越来越接近有序; 最后一趟排序使得所有数据有序.
-
7.5 希尔排序代码实现 :
# -*- coding: utf-8 -*- import random def insert_sort_gap(li, gap): for i in range(gap, len(li)): # i表示摸到的牌的下标 tmp = li[i] j = i - gap # j指的是手里的牌的下标 while j >= 0 and li[j] > tmp: li[j+gap] = li[j] j -= gap li[j+gap] = tmp def shell_sort(li): d = len(li) // 2 while d >= 1: insert_sort_gap(li, d) d //= 2 li = list(range(10)) random.shuffle(li) print(li) shell_sort(li) print(li)
8.计数排序
-
计数排序 (Count sort) : 对列表进行排序, 已知列表中的数的范围都在0到100之间, 设计时间复杂度为O(n)的算法
-
计数排序图解 :
-
计数排序代码实现 :
# -*- coding: utf-8 -*- import random def count_sort(li, max_count=100): count = [0 for _ in range(max_count+1)] for val in li: count[val] += 1 li.clear() for ind, val in enumerate(count): for i in range(val): li.append(ind) li = [random.randint(0, 100) for _ in range(10)] print(li) count_sort(li) print(li)
9.桶排序
-
桶排序 (Bucket Sort) : 首先将元素分在不同的桶中, 再对每个桶中的元素进行排序. 一般用来解决范围较大的元素排序(如1到1亿之间)
-
桶排序图解 :
-
桶排序的时间复杂度 :
- 平均情况时间复杂度: O(n+k)
- 最坏情况时间复杂度: O(n2k)
-
桶排序的空间复杂度 : O(nk)
-
桶排序代码实现 :
# -*- coding: utf-8 -*- import random def bucket_sort(i, n=100, max_num=10000): # 创建桶 buckets = [[] for _ in range(n)] for var in li: # i 表示放到几号桶中 i = min(var // (max_num // n), n-1) # 将var添加到桶中 buckets[i].append(var) # 保持桶内的顺序 for j in range(len(buckets[i])-1): if buckets[i][j] < buckets[i][j-1]: buckets[i][j], buckets[i][j-1] = buckets[i][j-1], buckets[i][j] else: break sorted_li = [] for buc in buckets: # extend函数: 用于在列表末尾一次性追加另一个序列中的多个值(用新列表扩展原来的列表) sorted_li.extend(buc) return sorted_li li = [random.randint(0,10000) for i in range(100000)] print(li) li = bucket_sort(li) print(li)
10.基数排序
-
基数排序 (Radix Sort) : 基数排序是桶排序的扩展,它的基本思想是:将整数按位数切割成不同的数字,然后按每个位数分别比较
-
基数排序时间复杂度 : O(kn) k表示数字位数
-
基数排序空间复杂度 : O(k+n)
-
基数排序实例 :
数据:[275,104,66,930,569,154,502,219,7,76]
将数据进行第一趟分配(按个位):
0 1 2 3 4 5 6 7 8 9 930 502 104 275 066 007 569 154 076 219 数据变更为:[930,502,104,154,275,66,76,7,569,219]
将变更后的数据进行第二趟分配(按十位,只有个位数则其十位以上视为0):
0 1 2 3 4 5 6 7 8 9 502 219 930 154 066 275 104 569 076 007 数据二次变更为:[502,104,7,219,930,154,66,569,275,76]
将二次变更后的数据进行第三趟分配(按百位):
0 1 2 3 4 5 6 7 8 9 007 104 219 502 930 066 154 275 569 076 收集得到最终结果:[7,66,76,104,154,219,275,502,569,930]
注意点1 : 基数排序先按个位排序,再按十位排序,再按百位排序,其是一个多关键字的排序
-
基数排序代码实现 :
# -*- coding: utf-8 -*- import random def radix_sort(li): # 最大值 9->1, 99->2, 888->3, 10000->5 max_num = max(li) it = 0 while 10 ** it <= max_num: buckets = [[] for _ in range(10)] for var in li: # 987 it=1 987//10->98 98%10->8; it=2 987//100->9 9%10=9 digit = (var // 10 ** it) % 10 buckets[digit].append(var) # 分桶完成 li.clear() for buc in buckets: li.extend(buc) # 把数重新写回li it += 1 li = list(range(100000)) random.shuffle(li) radix_sort(li) print(li)
知识点八 : 排序 练习题
练习题一
-
题目 : 给两个字符串 s 和 t , 判断 t 是否为 s 的重新排列后组成的单词
- s = “anagram”, t = “nagaram”, return true
- s = “rat”, t = “car”, return false
-
代码 :
# -*- coding: utf-8 -*- class Solution: def isAnagram(self, s, t): """ :param s: str :param t: str :return: bool """ # 方案一 return sorted(list(s)) == sorted((list(t))) # 方案二 dict1 = {} # { "a":1, "b":2 } dict2 = {} for ch in s: dict1[ch] = dict1.get(ch, 0) + 1 for ch in t: dict2[ch] = dict2.get(ch, 0) + 1 return dict1 == dict2
练习题二
-
题目 : 给定一个 m*n 的二维列表, 查找一个数是否存在. 列表有下列特性:
-
每行的列表从左到右已经排序好
-
每行第一个数比上一行最后一个数大
[
[ 1, 3, 5, 7],
[10, 11, 16, 20],
[23, 30, 34, 50]
]
-
-
代码 :
# -*- coding: utf-8 -*- class Solution: def searchMatrix(self, matrix, target): """ :param matrix: List[List[int]] :param target: int :return: bool """ h = len(matrix) if h == 0: return False w = len(matrix[0]) if w == 0: return False left = 0 right = w * h - 1 while left <= right: mid = (left + right) // 2 i = mid // w j = mid % w if matrix[i][j] == target: return True elif matrix[i][j] > target: right = mid - 1 else: left = mid + 1 else: return False
练习题三
-
题目 : 给定一个列表和一个整数, 设计算法找到两个数的下标, 使得两个数之和为给定的整数.保证肯定仅有一个结果
- 例如, 列表 [1, 2, 5, 4] 与目标整数 3, 1+2=3, 结果为 (0, 1).
-
代码 :
# -*- coding: utf-8 -*- class Solution: def binary_search(self, li, left, right, val): while left <= right: # 候选区有值 mid = (left + right) // 2 if li[mid][0] == val: return mid elif li[mid][0] > val: # 带查找的值在mid左侧 right = mid - 1 else: # li[mid] < val 带查找的值在mid右侧 left = mid + 1 else: return None def twoSum(self, nums, target): """ :param nums: List[int] :param target: int :return: List[int] """ new_nums = [[num, i] for i, num in enumerate(nums)] new_nums.sort(key=lambda x: x[0]) for i in range(len(new_nums)): a = new_nums[i][0] b = target - a if b >= a: j = self.binary_search(new_nums, i + 1, len(new_nums) - 1, b) else: j = self.binary_search(new_nums, 0, i - 1, b) if j: break return sorted([new_nums[i][1], new_nums[j][1]])
知识点九 : 数据结构 概念
1.数据结构定义
- 数据结构指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成
- 简单来说, 数据结构就是设计数据以何种方式组织并存储在计算机中, 如列表、集合与字典等都是一种数据结构.
2.数据结构分类
- 按照其逻辑结构可分为三类
- 线性结构 : 数据结构中的元素存在一对一的相互关系
- 树结构 : 数据结构中的元素存在一对多的相互关系
- 图结构 : 数据结构中的元素存在多对多的相互关系
知识点十 : 列表
1.列表定义
- 最简单的线性结构,也叫做顺序表,是一种基本的数据结构, 其他语言中称为数组 (列表和数组有一定区别).
2.列表存储
-
按照顺序存储, 是一块连续的内存,在32位机器上, 一个整数占4个字节 ( 1字节 (Byte) = 8 比特 (bit))
知识点十一 : 栈
1.栈的定义
栈 (stack) 是一个数据集合, 可以理解为只能在一段进行插入或删除操作的列表。
2.栈的特点
“先进后出”, 即 LIFO (last-in, first-out)
3.栈的基本操作
-
3.1 进栈 (压栈) : push
-
3.2 出栈 : pop
-
3.3 取栈顶 : gettop
4.栈的实现
- 4.1 进栈 : li.append
- 4.2 出栈 : li.pop
- 4.3 取栈顶 : li[-1]
5.栈的应用
-
括号匹配问题 : 给一个字符串, 其中包括小括号、中括号、大括号, 求该字符串中的括号是否匹配。
-
例如:
()()[]{} 匹配
([{()}]) 匹配
[]( 不匹配
[(]) 不匹配
# -*- coding: utf-8 -*-
class Stack:
def __init__(self):
self.stack = []
def push(self, element):
self.stack.append(element)
def pop(self):
return self.stack.pop()
def get_top(self):
if len(self.stack) > 0:
return self.stack[-1]
else:
return None
def is_empty(self):
return len(self.stack) == 0
def brace_match(s):
match = {
'}': '{',
']': '[',
')': '('
}
stack = Stack()
for ch in s:
if ch in {'(', '[', '{'}:
stack.push(ch)
else: # ch in {'(', '[', '{'}
if stack.is_empty():
return False
elif stack.get_top() == match[ch]:
stack.pop()
else: # stack.get_top() != match[ch]
return False
if stack.is_empty():
return True
else:
return False
print(brace_match('[{()}(){()}[]({}){}]'))
print(brace_match('[({}])]'))
知识点十二 : 队列
1.队列定义
队列 (Queue) 即一个数据组合, 仅允许在列表的一端进行插入, 另一端进行删除。进行插入的一端称为队尾 (rear), 插入动作称为进队或入队; 进行删除的一端称为队头 (front), 删除动作称为出队
2.队列的性质
先进先出 (First-in, First-out)
3.队列的实现
- 环形队列 : 当队尾指针front == Maxsize + 1 时, 再前进一个位置就自动到0.
- 队首指针前进1: front = (front+1) % MaxSize
- 队尾指针前进1: rear = (rear+1) % MaxSize
- 队空条件: rear == front
- 队满条件: (rear+1) % MaxSize == front
4.队列的内置模块
-
双向队列的两端都支持进队和出队操作
-
双向队列的基本操作
-
队首进队
-
队首出队
-
队尾进队
-
队尾出队
-
-
使用方法: from collections import deque
- 创建队列: queue = deque()
- 进队: append()
- 出队: popleft()
- 双向队列队首进队: appendleft()
- 双向队列队尾出队: pop
知识点十三 : 栈和队列的应用 迷宫问题
1.题目
给一个二维列表, 表示迷宫 (0表示通道,1表示围墙)。给出算法, 求一条走出迷宫的路径。
2.解法
-
栈 深度优先搜索
-
回溯法
思路: 从一个节点开始, 任意找下一个能走的点, 当找不到能走的点时, 退回上一个点寻找算法有其他方向的点。即使用栈存储当前路径
-
代码实现
# -*- coding: utf-8 -*- maze = [ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 0, 0, 1, 0, 0, 0, 1, 0, 1], [1, 0, 0, 1, 0, 0, 0, 1, 0, 1], [1, 0, 0, 0, 0, 1, 1, 0, 0, 1], [1, 0, 1, 1, 1, 0, 0, 0, 0, 1], [1, 0, 0, 0, 1, 0, 0, 0, 0, 1], [1, 0, 1, 0, 0, 0, 1, 0, 0, 1], [1, 0, 1, 1, 1, 0, 1, 1, 0, 1], [1, 1, 0, 0, 0, 0, 0, 0, 0, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ] dirs = [ lambda x, y: (x + 1, y), lambda x, y: (x - 1, y), lambda x, y: (x, y - 1), lambda x, y: (x, y + 1) ] def maze_path(x1, y1, x2, y2): stack = [] stack.append((x1, y1)) while (len(stack) > 0): curNone = stack[-1] # 当前的节点 if curNone[0] == x2 and curNone[1] == y2: # 走到终点了 for p in stack: print(p) return True # x,y 四个方向: x-1, y; x+1,y; x,y-1; x,y+1 for dir in dirs: nextNode = dir(curNone[0], curNone[1]) # 如果下一个节点能走 if maze[nextNode[0]][nextNode[1]] == 0: stack.append(nextNode) maze[nextNode[0]][nextNode[1]] = 2 # 2表示已经走过 break else: maze[nextNode[0]][nextNode[1]] = 2 stack.pop() else: print("没有路") return False maze_path(1, 1, 8, 8)
-
-
队列 广度优先搜索
-
思路: 从一个节点开始, 寻找所有接下来能继续走的点, 继续不断寻找, 直到找到出口。
-
代码实现
# -*- coding: utf-8 -*- from collections import deque maze = [ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 0, 0, 1, 0, 0, 0, 1, 0, 1], [1, 0, 0, 1, 0, 0, 0, 1, 0, 1], [1, 0, 0, 0, 0, 1, 1, 0, 0, 1], [1, 0, 1, 1, 1, 0, 0, 0, 0, 1], [1, 0, 0, 0, 1, 0, 0, 0, 0, 1], [1, 0, 1, 0, 0, 0, 1, 0, 0, 1], [1, 0, 1, 1, 1, 0, 1, 1, 0, 1], [1, 1, 0, 0, 0, 0, 0, 0, 0, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ] dirs = [ lambda x, y: (x + 1, y), lambda x, y: (x - 1, y), lambda x, y: (x, y - 1), lambda x, y: (x, y + 1) ] def print_r(path): curNode = path[-1] realpath = [] while curNode[2] != -1: realpath.append(curNode[0:2]) curNode = path[curNode[2]] realpath.append(curNode[0:2]) # 起点 realpath.reverse() for node in realpath: print(node) def maze_path_queue(x1, y1, x2, y2): queue = deque() queue.append((x1, y1, -1)) path = [] while len(queue) > 0: curNode = queue.popleft() path.append(curNode) if curNode[0] == x2 and curNode[1] == y2: # 终点 print_r(path) return True for dir in dirs: nextNode = dir(curNode[0], curNode[1]) if maze[nextNode[0]][nextNode[1]] == 0: queue.append((nextNode[0], nextNode[1], len(path) - 1)) # 后续节点进队, 记录哪个节点带他来的 maze[nextNode[0]][nextNode[1]] = 2 # 标记为已经走过 else: print("没有路") return False maze_path_queue(1, 1, 8, 8)
-
知识点十四 : 链表
1.链表介绍
-
链表是由一系列节点组成的元素集合。每个节点包含两个部分, 数据域 item 和指向下一个节点的指针next。 通过节点之间的相互连接, 最终串联成一个链表。
2.创建链表
-
头插法
# -*- coding: utf-8 -*- class Node: def __init__(self, item): self.item = item self.next = None # 头插法 def create_linklist_head(li): head = Node(li[0]) for element in li[1:]: node = Node(element) node.next = head head = node return head def print_linklist(lk): while lk: print(lk.item, end=",") lk = lk.next lk = create_linklist_head([1, 2, 3]) print_linklist(lk)
-
尾插法
# -*- coding: utf-8 -*- class Node: def __init__(self, item): self.item = item self.next = None # 尾插法 def create_linklist_tail(li): head = Node(li[0]) tail = head for element in li[1:]: node = Node(element) tail.next = node tail = node return head def print_linklist(lk): while lk: print(lk.item, end=",") lk = lk.next lk = create_linklist_tail([1, 2, 3, 6, 8]) print_linklist(lk)
3.链表节点的插入
-
p.next = curNode.next
-
curNode.next = p
4.链表节点的删除
-
p = curNode.next
-
curNode.next = curNode.next.next
-
del p
5.双链表
-
双链表的每个节点有两个指针: 一个指向后一个节点, 另一个指向前一个节点
6.双链表节点的插入
-
p.next = curNode.next
-
curNode.next.prior = p
-
p.prior = curNode
-
curNode.next = p
-
7.双链表节点的删除
-
p = curNode.next
-
curNode.next = p.next
-
p.next.prior = curNode
-
del p
-
-
8.链表复杂度解析
- 顺序表 (列表/数组) 与链表
- 按元素值查找
- 按下标查找
- 在某元素后插入
- 删除某元素
- 顺序表 (列表/数组) 与链表
-
9.链表与顺序表
- 链表在插入和删除的操作上明显快于顺序表
- 链表的内存快于更灵活的分配
- 可利用链表重新实现栈和队列
- 链表这种链式存储的数据结构对树和图的结构有很大的启发性
知识点十五 : 哈希表
1.哈希表原理
- 哈希表是一个通过哈希函数来计算数据存储位置的数据结构, 通常支持如下操作:
- insert(key, value) : 插入键值对 (key, value)
- get(key) : 如果存在键为 key 的键值对则返回其value, 否则返回空值
- delete(key) : 删除键为key的键值对
2.哈希表前身 直接寻址表
-
当关键字的全域U比较小时, 直接寻址是一种简单而有效的方法。
-
直接寻址技术的缺点:
- 当域U很大时, 需要消耗大量内存, 很不实际
- 如果域U很大而实际出现的key很少, 则大量空间被浪费
- 无法处理关键字不是数字的情况
-
直接寻址表
- key为k的元素放到k位置上
-
改进直接寻址表 : 哈希 (Hashing)
- 构建大小为m的寻址表T
- key为k的元素放到h(k)位置上
- h(k)是一个函数, 其将域U映射到表T[0,1,…,m-1]
3.哈希表定义
-
哈希表 (Hash Table, 又称散列表), 是一种线性表的存储结构。哈希表由一个直接寻址表和一个哈希函数组成。哈希函数h(k)将元素关键字k作为自变量, 返回元素的存储下标。
-
假设有一个长度为7的哈希表, 哈希函数h(k)=k%7。元素集合{14,22,3,5}的存储方式如下图。
4.哈希冲突
-
由于哈希表的大小是有限的, 而要存储的值的总数量是无限的, 因此对于任何哈希函数, 都会出现两个不用元素映射到同一个位置上的情况, 这种情况就叫做哈希冲突。
-
比如 h(k) = k%7, h(0)=h(7)=h(14)=…
5.解决哈希冲突
-
5.1 开放寻址法
- 如果哈希函数返回的位置已经有值, 则可以向后探索新的位置来存储这个值。
- 线性探查 : 如果位置 i 被占用, 则探查 i+1, i+2
- 二次探查 : 如果位置 i 被占用, 则探查 i+12, i-12, i+22, i-22, …
- 二度哈希 : 有 n 个哈希函数, 当使用第1个哈希函数 h1 发生冲突时, 则尝试使用 h2, h3, …
- 如果哈希函数返回的位置已经有值, 则可以向后探索新的位置来存储这个值。
-
5.2 拉链法
-
哈希表每个位置都连接一个链表, 当冲突发生时, 冲突的元素将被加到该位置链表的最后。
-
6.常见哈希函数
- 6.1 除法哈希法
- h(k) = k % m
- 6.2 乘法哈希法
- h(k) = floor(m*(A*key%1))
- 6.3 全域哈希法
- ha,b(k) = ((a*key + b) mod p) mod m a,b=1,2,…,p-1
7.哈希表实现
# -*- coding: utf-8 -*-
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)) + ">>"
# lk = LinkList([1, 2, 3, 4, 5])
# for element in lk:
# print(element)
# print(lk)
# 类似于集合的结构
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))
8.哈希表应用
-
8.1 集合与字典
- 字典与集合都是通过哈希表来实现的。
- a = {‘name’:‘Alex’, ‘age’:18, ‘gender’:‘Man’}
- 使用哈希表存储字典, 通过哈希函数将字典的键映射为下标。假设 h(‘name’) = 3, h(‘age’) = 1, h(‘gender’) = 4, 则哈希表存储为[None, 18, None, ‘Alex’, ‘Man’]
- 如果发送哈希冲突, 则通过拉链法或开发寻址法解决
- 字典与集合都是通过哈希表来实现的。
-
8.2 md5算法
- MD5 (Message-Digest Algorithm 5) 曾经是密码学中常用的哈希函数, 可以把任意长度的数据映射为 128位的哈希值, 其曾经包含如下特征:
- 1.同样的消息, 其MD5值必定相同;
- 2.可以快速计算出任意给定消息的MD5值;
- 3.除非暴力的枚举所有可能的消息,否则不可能从哈希值反推出消息本身;
- 4.两条消息之间即使只有微小的差别,其对应的MD5值也应该是完全不同、完全不相关的;
- 5.不能在有意义的时间内人工的构造两个不同的消息使其具有相同的MD5值。
- 应用举例 : 文件的哈希值
- 算出文件的哈希值,若两个文件的哈希值相同,则可认为这两个文件是相同的。因此:
- 1.用户可以利用它来验证下载的文件是否完整。
- 2.云存储服务商可以利用它来判断用户要上传的文件是否已经存在于服务器上,从而实现秒传的功能,同时避免存储过多相同的文件副本。
- 算出文件的哈希值,若两个文件的哈希值相同,则可认为这两个文件是相同的。因此:
- MD5 (Message-Digest Algorithm 5) 曾经是密码学中常用的哈希函数, 可以把任意长度的数据映射为 128位的哈希值, 其曾经包含如下特征:
-
8.3 SHA2算法
-
历史上MD5和SHA-1曾经是使用最为广泛的cryptographic hash function,但是随着密码学的发展,这两个哈希函数的安全性相继受到了各种挑战。
-
因此现在安全性较重要的场合推荐使用SHA-2等新的更安全的哈希函数。
-
SHA-2包含了一系列的哈希函数: SHA-224,SHA-256,SHA-384,SHA-512,
SHA-512/224,SHA- 512/256,其对应的哈希值长度分别为224,256,384 or 512位。 -
SHA-2具有和MD5类似的性质 (参见MD5算法的特征) 。
-
应用举例:
例如,在比特币系统中,所有参与者需要共同解决如下问题:对于一个给定的字符串U,给定的目标哈希值H,需要计算出一个字符串V,使得U+V的哈希值与H的差小于—个给定值D。此时,只能通过暴力枚举V来进行猜测。首先计算出结果的人可获得一定奖金。而某人首先计算成功的概率与其拥有的计算量成正比,所以其获得的奖金的期望值与其拥有的计算量成正比。
-
知识点十六 : 树
1.树的概念
-
1.1 树与二叉树
- 树是一种数据结构 (如目录结构)
- 树是一种可以递归定义的数据结构
- 树是由n个节点组成的集合
- 如果n=0,那这是一棵空树;
- 如果n>0,那存在1个节点作为树的根节点,其他节点可以分为m个集合,每个集合本身又是—棵树。
注意 : 树的相关具体概念见 知识点七排序-堆排序前置知识点
2.树的实例 模拟文件系统
# -*- coding: utf-8 -*-
class Node:
def __init__(self, name, type='dir'):
self.name = name
self.type = type # "dir" or "file"
self.children = []
self.parent = None
class FileSystemTree:
def __init__(self):
self.root = Node(" /")
self.now = self.root
def __repr__(self):
return self.name
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):
# "../var/python"
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())
3.二叉树
-
3.1 二叉树的链式存储
- 将二叉树的节点定义为一个对象,节点之间通过类似链表的链接方式来连接。
-
3.2 二叉树节点定义
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 print(root.lchild.rchild.data)
-
3.3 二叉树的遍历
-
二叉树的遍历方式:
- 前序遍历 : EACBDGF
- 中序遍历 : ABCDEGF
- 后序遍历 : BDCAFGE
- 层次遍历 : EAGCFBD
-
代码实现:
from collections import deque 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 # print(root.lchild.rchild.data) # 1.前序遍历 def pre_order(root): if root: print(root.data, end=',') pre_order(root. lchild) pre_order(root. rchild) # pre_order(root) # 2.中序遍历 def in_order(root): if root: in_order(root.lchild) print(root.data, end=',') in_order(root.rchild) # in_order(root) # 3.后序遍历 def post_order(root): if root: post_order(root.lchild) post_order(root.rchild) print(root.data, end=',') # post_order(root) # 4.层次遍历 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)
-
-
3.4 二叉搜索树的概念
- 二叉搜索树是一颗二叉树且满足性质 : 设x是二叉树的一个节点。如果 y 是x 左子树的—个节点,那么 y.key ≤ x.key ;如果 y 是 x 右子树的一个节点,那么 y.key ≥ x.key。
- 二叉搜索树的操作:查询、插入、删除
-
3.5 二叉搜索树: 插入
-
代码实现
# -*- coding: utf-8 -*- 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) 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 # 1.前序遍历 def pre_order(self, root): if root: print(root.data, end=',') self.pre_order(root.lchild) self.pre_order(root.rchild) # 2.中序遍历 def in_order(self, root): if root: self.in_order(root.lchild) print(root.data, end=',') self.in_order(root.rchild) # 3.后序遍历 def post_order(self, root): if root: self.post_order(root.lchild) self.post_order(root.rchild) print(root.data, end=',') li = list(range(10)) random.shuffle(li) tree = BST(li) tree.pre_order(tree.root) print("") tree.in_order(tree.root) print("") tree.post_order(tree.root)
-
-
3.6二叉搜索树 : 查询
-
代码实现
# -*- coding: utf-8 -*- 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) 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 query(self, node, val): if not node: return None if node.data < val: return self.query(node.rchild, val) elif node.data > val: return self.query(node.lchild,val) else: return node def query_no_rec(self, val): p = self.root while p: if p.data < val: p = p.rchild elif p.data > val: p = p.lchild else: return p return None # 1.前序遍历 def pre_order(self, root): if root: print(root.data, end=',') self.pre_order(root.lchild) self.pre_order(root.rchild) # 2.中序遍历 def in_order(self, root): if root: self.in_order(root.lchild) print(root.data, end=',') self.in_order(root.rchild) # 3.后序遍历 def post_order(self, root): if root: self.post_order(root.lchild) self.post_order(root.rchild) print(root.data, end=',') li = list(range(0, 100, 2)) random.shuffle(li) tree = BST(li) print(tree.query_no_rec(4).data)
-
-
3.7 二叉搜索树 : 删除
-
如果要删除的节点是叶子节点: 直接删除
-
如果要删除的节点只有一个孩子: 将次节点的父亲与孩子连接, 然后删除该节点。
-
如果要删除的节点有两个孩子 : 将其右子树的最小节点 (该节点最多有一个右孩子) 删除, 并替换当前节点。
-
代码实现:
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*- 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) 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 query(self, node, val): if not node: return None if node.data < val: return self.query(node.rchild, val) elif node.data > val: return self.query(node.lchild, val) else: return node def query_no_rec(self, val): p = self.root while p: if p.data < val: p = p.rchild elif p.data > val: p = p.lchild else: return p return None # 1.前序遍历 def pre_order(self, root): if root: print(root.data, end=',') self.pre_order(root.lchild) self.pre_order(root.rchild) # 2.中序遍历 def in_order(self, root): if root: self.in_order(root.lchild) print(root.data, end=',') self.in_order(root.rchild) # 3.后序遍历 def post_order(self, root): if root: self.post_order(root.lchild) self.post_order(root.rchild) print(root.data, end=',') def __remove_node_1(self, node): # 情况1: node是叶子节点 if not node.parent: self.root = None if node == node.parent.lchild: # node是它父亲的左孩子 node.parent.lchild = None else: # 右孩子 node.parent.rchild = None def _remove_node_21(self, node): # 情况2.1: node只有一个左孩子 if not node.parent: # 根节点 self.root = node.lchild node.lchild.parent = None elif node == node.parent.lchild: node.parent.lchild = node.lchild node.lchild.parent = node.parent else: node.parent.rchild = node.lchild node.lchild.parent = node.parent def __remove_node_22(self, node): # 情况2.2: node只有一个右孩子 if not node.parent: self.root = node.rchild elif node == node.parent.lchild: node.parent.lchild = node.rchild node.rchild.parent = node.parent else: node.parent.rchild = node.rchild node.rchild.parent = node.parent def delete(self, val): if self.root: # 不是空树 node = self.query_no_rec(val) if not node: # 不存在 return False if not node.lchild and not node.rchild: # 叶子节点 self.__remove_node_1(node) elif not node.rchild: # 2.1只有一个左孩子 self.__remove_node_21(node) elif not node.lchild: # 2.2只有一个右孩子 self.__remove_node_22(node) else: # 3.两个孩子都有 min_mode = node.rchild while min_mode.lchild: min_mode = min_mode.lchild node.data = min_mode.data # 删除min_node if min_mode.rchild: self.__remove_node_22(min_mode) else: self.__remove_node_1(min_mode) tree = BST([1, 4, 2, 5, 3, 8, 6, 9, 7]) tree.in_order(tree.root) print("") tree.delete(4) tree.delete(1) tree.delete(8) tree.in_order(tree.root)
-
-
3.8 二叉搜索树的效率
-
平均情况下,二叉搜索树进行搜索的时间复杂度为O(Ign)。
-
但是最坏情况下,二叉搜索树可能非常偏斜。
-
解决方案
- 1.随机化插入
- 2.AVL树
-
4.AVL树
-
4.1 AVL树的概念
-
AVL树 : AVL树是—棵自平衡的二叉搜索树。
-
AVL树具有以下性质:
- 根的左右子树的高度之差的绝对值不能超过1
- 根的左右子树都是平衡二叉树
-
-
4.2 AVL树 : 旋转
-
插入一个节点可能会破坏AVL树的平衡,可以通过旋转操作来进行修正。
-
插入一个节点后,只有从插入节点到根节点的路径上的节点的平衡可能被改变。我们需要找出第一个破坏了平衡条件的节点,称之为K。K的两颗子树的高度差2。
-
不平衡的出现可能有4种情况
-
1.不平衡是由于对 K 的左孩子的左子树插入导致的 : 右旋
-
2.不平衡是由于对 K 的右孩子的右子树插入导致的 : 左旋
-
3.不平衡是由于对 K 的右孩子的左子树插入导致的 : 右旋-左旋
-
4.不平衡是由于对 K 的左孩子的右子树插入导致的 : 左旋-右旋
-
-
代码实现 (有bug待处理)
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) 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 query(self, node, val): if not node: return None if node.data < val: return self.query(node.rchild, val) elif node.data > val: return self.query(node.lchild, val) else: return node def query_no_rec(self, val): p = self.root while p: if p.data < val: p = p.rchild elif p.data > val: p = p.lchild else: return p return None # 1.前序遍历 def pre_order(self, root): if root: print(root.data, end=',') self.pre_order(root.lchild) self.pre_order(root.rchild) # 2.中序遍历 def in_order(self, root): if root: self.in_order(root.lchild) print(root.data, end=',') self.in_order(root.rchild) # 3.后序遍历 def post_order(self, root): if root: self.post_order(root.lchild) self.post_order(root.rchild) print(root.data, end=',') def __remove_node_1(self, node): # 情况1: node是叶子节点 if not node.parent: self.root = None if node == node.parent.lchild: # node是它父亲的左孩子 node.parent.lchild = None else: # 右孩子 node.parent.rchild = None def _remove_node_21(self, node): # 情况2.1: node只有一个左孩子 if not node.parent: # 根节点 self.root = node.lchild node.lchild.parent = None elif node == node.parent.lchild: node.parent.lchild = node.lchild node.lchild.parent = node.parent else: node.parent.rchild = node.lchild node.lchild.parent = node.parent def __remove_node_22(self, node): # 情况2.2: node只有一个右孩子 if not node.parent: self.root = node.rchild elif node == node.parent.lchild: node.parent.lchild = node.rchild node.rchild.parent = node.parent else: node.parent.rchild = node.rchild node.rchild.parent = node.parent def delete(self, val): if self.root: # 不是空树 node = self.query_no_rec(val) if not node: # 不存在 return False if not node.lchild and not node.rchild: # 叶子节点 self.__remove_node_1(node) elif not node.rchild: # 2.1只有一个左孩子 self.__remove_node_21(node) elif not node.lchild: # 2.2只有一个右孩子 self.__remove_node_22(node) else: # 3.两个孩子都有 min_mode = node.rchild while min_mode.lchild: min_mode = min_mode.lchild node.data = min_mode.data # 删除min_node if min_mode.rchild: self.__remove_node_22(min_mode) else: self.__remove_node_1(min_mode) class AVLNode(BiTreeNode): def __init__(self, data): BiTreeNode.__init__(self, data) self.bf = 0 class AVLTree(BST): def __init__(self, li=None): BST.__init__(self, li) def rotate_left(self, p, c): s2 = c.lchild p.rchild = s2 if s2: s2.parent = p c.lchild = p p.parent = c p.bf = 0 c.bf = 0 return c def rotate_right(self, p, c): s2 = c.rchild p.lchild = s2 if s2: s2.parent = p c.rchild = p p.parent = c p.bf = 0 c.bf = 0 return c def rotate_right_left(self, p, c): g = c.lchild s3 = g.rchild c.lchild = s3 if s3: s3.parent = c g.rchild = c c.parent = g s2 = g.lchild p.rchild = s2 if s2: s2.parent = p g.lchild = p p.parent = g if g.bf > 0: # g.bf ==1 p.bf = -1 c.bf = 0 else: # g.bf < 0 或者说 == -1: p.bf = 0 c.bf = 1 g.bf = 0 return g def rotate_left_right(self, p, c): g = c.rchild s2 = g.lchild c.rchild = s2 if s2: s2.parent = c g.lchild = c c.parent = g s3 = g.rchild p.lchild = s3 if s3: s3.parent = p g.rchild = p p.parent = g if g.bf < 0: p.bf = 0 c.bf = 1 else: p.bf = 0 c.bf = -1 g.bf = 0 return g def insert_no_rec(self, val): # 1.和BST一样,插入 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 node = p.lchild # 存储的就是插入的节点 break elif val > p.data: if p.rchild: p = p.rchild else: p.rchild = BiTreeNode(val) p.rchild.parent = p node = p.rchild break else: # val == p.data return # 2.更新balance factor while node.parent: # node . parent不空 if node.parent.lchild == node: # 传递是从左子树来的,左子树更沉了 # 更新node.parent的bf -= 1 if node.parent.bf < 0: # 原来node.parent.bf ==.-1,更新后变成-2 # 做旋转 # 看node哪边沉 g = node.parent.parent # 为了连接旋转之后的子树 if node.bf > 0: n = self.rotate_left_right(node.parent, node) else: n = self.rotate_right(node.parent, node) # 记得:把n和g连起来 elif node.parent.bf > 0: # 原来node.parent.bf = 1,更新之后变成0 node.parent.bf = 0 break else: # 原来node . parent.bf = 0,更新之后变成-1 node.parent.bf = -1 node = node.parent continue else: # 传递是从右子树来的。右子树更沉了 # 更新node.parent.bf +1 if node.parent.bf > 0: # 原来node.parent.bf == 1,更新后变成2 # 做旋转 # 看node即边沉 g = node.parent.parent # 为了连接旋转之后的子树 x = node.parent # 旋转前的子树的根 if node.bf < 0: # node.bf = 1 n = self.rotate_right_left(node.parent, node) else: # node.bf = -1 n = self.rotate_left(node.parent, node) # 记得连起来 elif node.parent.bf < 0: # 原来node.parent.bf = -1,更新之后变成0 node.parent.bf = 0 break else: # 原来node.parent.bf = 0, 更新之后变成1 node.parent.bf = 1 node = node.parent continue # 链接旋转后的子树 n.parent = g if g: # g不是空 if x == g.lchild: g.lchild = n else: g.rchild = n break else: self.root = n break tree = AVLTree([9, 8, 7, 6, 5, 4, 3, 2, 1]) tree.pre_order(tree.root) print("") tree.in_order(tree.root)
-
-
4.3 二叉搜索树拓展 B树
-
B树(B-Tree) : B树是一棵自平衡的多路搜索树。常用于数据库的索引。
-