0.算法基础

一、算法图谱

算法(Algorithm):
是指解题方案的准确而完整的描述,是在特定计算模型下,意在解决特定问题的指令序列
算法代表着用系统的方法描述解决问题的策略机制。

算法的特征:
1.有穷性(Finiteness)
指算法必须能在执行有限个步骤之后终止;
2.确切性 (Definiteness)
算法的每一步骤必须有确切的定义;
3.输入项 (Input)
一个算法有0个或多个输入,以刻画运算对象的初始情况,所谓0个输入是指算法本身定出了初始条件;
4.输出项 (Output)
一个算法有一个或多个输出,以反映对输入数据加工后的结果。没有输出的算法是毫无意义的;
5.可行性 (Effectiveness)
算法中执行的任何计算步骤都是可以被分解为基本的可执行的操作步骤,即每个计算步骤都可以在有限时间内完成(也称之为有效性)。

算法评定:
1.时间复杂度 指算法需要计算的工作量
2.空间复杂度 指算法需要消耗的内存空间
3.可读性
4.精确(正确)性
5.健壮性 稳定性 容错能力
6.效率


算法的方法:
1.递推法
递推是序列计算机中的一种常用算法。
它是按照一定的规律来计算序列中的每个项,通常是通过计算机前面的一些项来得出序列中的指定项的值。
其思想是把一个复杂的庞大的计算过程转化为简单过程的多次重复,该算法利用了计算机速度快和不知疲倦的机器特点。
2.递归法
程序调用自身的编程技巧称为递归(recursion)。
一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,
它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,
递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的能力在于用有限的语句来定义对象的无限集合。一般来说,递归需要有边界条件、递归前进段和递归返回段。
当边界条件不满足时,递归前进;当边界条件满足时,递归返回。
注意:
(1) 递归就是在过程或函数里调用自身;
(2) 在使用递归策略时,必须有一个明确的递归结束条件,称为递归出口。
3.穷举法
穷举法,或称为暴力破解法,
其基本思路是:对于要解决的问题,列举出它的所有可能的情况,逐个判断有哪些是符合问题所要求的条件,从而得到问题的解。
它也常用于对于密码的破译,即将密码进行逐个推算直到找出真正的密码为止。
例如一个已知是四位并且全部由数字组成的密码,其可能共有10000种组合,因此最多尝试10000次就能找到正确的密码。
理论上利用这种方法可以破解任何一种密码,问题只在于如何缩短试误时间。
因此有些人运用计算机来增加效率,有些人辅以字典来缩小密码组合的范围。

4.贪心算法
贪心算法是一种对某些求最优解问题的更简单、更迅速的设计技术。
用贪心法设计算法的特点是一步一步地进行,常以当前情况为基础根据某个优化测度作最优选择,而不考虑各种可能的整体情况,
它省去了为找最优解要穷尽所有可能而必须耗费的大量时间,它采用自顶向下,以迭代的方法做出相继的贪心选择,
每做一次贪心选择就将所求问题简化为一个规模更小的子问题, 通过每一步贪心选择,可得到问题的一个最优解,
虽然每一步上都要保证能获得局部最优解,但由此产生的全局解有时不一定是最优的,所以贪婪法不要回溯。
贪婪算法:是一种改进了的分级处理方法,其核心是根据题意选取一种量度标准,然后将这多个输入排成这种量度标准所要求的顺序,
按这种顺序一次输入一个量,如果这个输入和当前已构成在这种量度意义下的部分最佳解加在一起不能产生一个可行解,则不把此输入加到这部分解中。
这种能够得到某种量度意义下最优解的分级处理方法称为贪婪算法。
对于一个给定的问题,往往可能有好几种量度标准。初看起来,这些量度标准似乎都是可取的,
但实际上,用其中的大多数量度标准作贪婪处理所得到该量度意义下的最优解并不是问题的最优解,而是次优解。
因此,选择能产生问题最优解的最优量度标准是使用贪婪算法的核心。
一般情况下,要选出最优量度标准并不是一件容易的事,但对某问题能选择出最优量度标准后,用贪婪算法求解则特别有效。

5.分治法
分治法是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,
原问题的解即子问题的解的合并。
分治法所能解决的问题一般具有以下几个特征:
(1) 该问题的规模缩小到一定的程度就可以容易地解决;
(2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质;
(3) 利用该问题分解出的子问题的解可以合并为该问题的解;
(4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
6.动态规划法
动态规划是一种在数学和计算机科学中使用的,用于求解包含重叠子问题的最优化问题的方法。
其基本思想是,将原问题分解为相似的子问题,在求解的过程中通过子问题的解求出原问题的解。
动态规划的思想是多种算法的基础,被广泛应用于计算机科学和工程领域。
动态规划程序设计是对解最优化问题的一种途径、一种方法,而不是一种特殊算法。
不象前面所述的那些搜索或数值计算那样,具有一个标准的数学表达式和明确清晰的解题方法。
动态规划程序设计往往是针对一种最优化问题,由于各种问题的性质不同,确定最优解的条件也互不相同,
因而动态规划的设计方法对不同的问题,有各具特色的解题方法,而不存在一种万能的动态规划算法,可以解决各类最优化问题。
因此读者在学习时,除了要对基本概念和方法正确理解外,必须具体问题具体分析处理,以丰富的想象力去建立模型,用创造性的技巧去求解。
7.迭代法
迭代法也称辗转法,是一种不断用变量的旧值递推新值的过程,跟迭代法相对应的是直接法(或者称为一次解法),即一次性解决问题。
迭代法又分为精确迭代和近似迭代。“二分法”和“牛顿迭代法”属于近似迭代法。
迭代算法是用计算机解决问题的一种基本方法。
它利用计算机运算速度快、适合做重复性操作的特点,让计算机对一组指令(或一定步骤)进行重复执行,在每次执行这组指令(或这些步骤)时,
都从变量的原值推出它的一个新值。
8.分支界限法
分枝界限法是一个用途十分广泛的算法,运用这种算法的技巧性很强,不同类型的问题解法也各不相同。
分支定界法的基本思想是对有约束条件的最优化问题的所有可行解(数目有限)空间进行搜索。
该算法在具体执行时,把全部可行的解空间不断分割为越来越小的子集(称为分支),并为每个子集内的解的值计算一个下界或上界(称为定界)。
在每次分支后,对凡是界限超出已知可行解值那些子集不再做进一步分支,
这样,解的许多子集(即搜索树上的许多结点)就可以不予考虑了,从而缩小了搜索范围。
这一过程一直进行到找出可行解为止,该可行解的值不大于任何子集的界限。因此这种算法一般可以求得最优解。
与贪心算法一样,这种方法也是用来为组合优化问题设计求解算法的,所不同的是它在问题的整个可能解空间搜索,
所设计出来的算法虽其时间复杂度比贪婪算法高,但它的优点是与穷举法类似,都能保证求出问题的最佳解,而且这种方法不是盲目的穷举搜索,
而是在搜索过程中通过限界,可以中途停止对某些不可能得到最优解的子空间进一步搜索(类似于人工智能中的剪枝),故它比穷举法效率更高。
9.回溯法
回溯法(探索与回溯法)是一种选优搜索法,按选优条件向前搜索,以达到目标。
但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,
而满足回溯条件的某个状态的点称为“回溯点”。
其基本思想是:在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。
当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,
则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)。
若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。
而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。


算法分类
基本算法、
数据结构的算法、
数论与代数算法、
动态规划以及数值分析、
计算几何的算法、
图论的算法、
加密算法、
排序算法、
检索算法、
随机化算法、
并行算法,
厄米变形模型,
随机森林算法


算法的应用:
随机森林算法 ------来进行头部姿势的估计
遗传算法 ------来解决弹药装载问题
信息加密算法 ------在网络传输中的应用
并行算法 ------在数据挖掘中的应用

二、时间复杂度的计算

时间复杂度:
时间复杂度是指 执行算法所需要的计算工作量。
一般来说,计算机算法是问题规模n的函数f(n),算法的时间复杂度也因此记做:
T(n)=Ο(f(n))
随着n的增大,算法执行时间的增长率和f(n)的增长率成正比,所以f(n)越小,算法的时间复杂度越低,算法的效率越高

时间复杂度常用大O符号表述。这种用大写的O来代表算法的时间复杂度的记法叫"大O阶"记法

时间复杂度的基本原则:
1. 只有常数项,认为其时间复杂度为 O(1)
2. 顺序结构,时间复杂度按 加法 进行计算
3. 循环结构,时间复杂度按 乘法 进行计算
4. 分支结构,时间复杂度 取最大值
5. 判断一个算法的时间复杂度时,只需要关注 最高次项,忽略最高次项的系数,且其它次要项和常数项也可以忽略
6. 一般所分析的算法的时间复杂度都是指 最坏 时间复杂度


算法的时间复杂度(大O阶)的计算方法为:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留高阶项。
3、如果最高阶项存在且不是1,则去除与这个项相乘的常数。
也就是当n增大到一定值后,对时间复杂度影响最大的就是n的幂次最高的项,其他的常数项和低幂次项都可以忽略不计。

例:
int n = 100000; //执行了1次
for(int i = 0; i < n; i++){ //执行了n+1次
for(int j = 0; j < n; j++) //执行了n*(n+1)次
{
printf("i = %d, j = %d", i, j); //执行了n*n次
}
}
for(int i = 0; i < n; i++){ //执行了n+1次
printf("i = %d", i); //执行了n次
}
printf("Done"); //执行了1次

计算时间复杂度步骤:
1.用1取代运行过程中所有的加法常数:2n^2+3n+1
2.只保留最高阶项:2n^2
3.最高阶项存在且不为1,去掉最高阶项的常数:n^2
即:上述例子的时间复杂度为:n^2


按数量级递增排列,常见的时间复杂度有:
常数阶O(1)
对数阶O(log2n)
线性阶O(n)
线性对数阶O(nlog2n)
平方阶O(n^2)
立方阶O(n^3)...,
k次方阶O(n^k)
指数阶O(2^n)
随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低


嵌套一层for循环的时间复杂度为: O(n)
嵌套二层for循环的时间复杂度为: O(n^2)
二分的算法时间复杂度为:O(logn)
如果一个for循环套一个二分,那么时间复杂度为O(nlogn);


总结:
一个算法所耗费的时间=算法中每条语句的执行时间之和
算法转换为程序后,每条语句执行一次所需的时间取决于机器的指令性能、速度以及编译所产生的代码质量等难以确定的因素。
如果要计算一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。


时间复杂度:
O(1) 常见操作: 哈希查找,数组取值,常数加减乘除
O(logn): 二分查找
O(n): sum,max,min
O(nlogn): 归并排序,堆排序,希尔排序,快速排序
O(n2) :冒泡,选择排序
O(n3) :三元方程暴力求解
O(2n):求解所有子集 O(n!):全排列问题。比如:给定字母串,每个字母必须使用,且一次组合中只能使用一次,求
所有组合方式;另外还要经典的旅行商 TSP 问题

三、空间复杂度的计算

空间复杂度
算法的空间复杂度是指算法需要消耗的内存空间。
其计算和表示方法与时间复杂度类似,一般都用复杂度的渐近性来表示。
同时间复杂度相比,空间复杂度的分析要简单得多

算法的空间复杂度通过计算算法所需的存储空间实现:
算法的空间复杂度的计算公式记作:S(n)=O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数,
也是一种“渐进表示法”,这些所需要的内存空间通常分为“固定空间内存”(包括基本程序代码、常数、变量等)和“变动空间内存”(随程序运行时而改变大小的使用空间)

通常,我们都是用“时间复杂度”来指运行时间的需求,是用“空间复杂度”指空间需求。
当直接要让我们求“复杂度”时,通常指的是时间复杂度。

计算方法:
1.忽略常数,用O(1)表示
2.递归算法的空间复杂度= 递归深度N * 每次递归所要的辅助空间
3.对于单线程来说,递归有运行时堆栈,求的是递归最深的那一次压栈所耗费的空间的个数,因为递归最深的那一次所耗费的空间足以容纳它所有递归过程

一维数组a[n]:空间复杂度 O(n)
二维数组a[n][m]:空间复杂度 O(n*m)

四、分治算法

1.概念

分而治之算法思想:
手中的问题被分成较小的子问题,然后每个问题都是独立解决的。
当我们继续将子问题分解成更小的子问题时,我们最终可能会达到不可能再分裂的阶段。
这些“原子”最小可能的子问题(分数)被解决。
所有子问题的解决方案最终被合并以获得原始问题的解决方案。


可以通过三个步骤理解分而治之的方法:
1.分割/歇
将问题分解成更小的子问题,子问题应该是原始问题的一部分
实现:通常采用递归方法来分解问题,直到没有子问题可以进一步划分为止
注意:在这个阶段,子问题本质上是原子性的,但仍然是实际问题的一部分

2.征服/解决
这一步收到很多较小的子问题需要解决
一般来说,在这个层面上,问题被认为是自己解决的

3.合并/合并
当较小的子问题得到解决时,这个阶段递归地将它们结合起来,直到他们制定原始问题的解决方案
这种算法方法递归地工作,并且征服和合并步骤工作得如此之近,以至于它们看起来像一个

例子
二进制搜索

2.合并两个有序序列

"合并两个有序数组 left 和 right"
def merge_list(left,right):
"利用两个序列本来有序的特点"
i=0
j=0
temp=[]
while i<= len(left)-1 and j<= len(right)-1:
if left[i]<= right[j]:
temp.append(left[i])
i+= 1
else:
temp.append(right[j])
j+= 1
temp+= left[i:]+right[j:]
return temp


left=[4,7,9,23,78]
right=[0,2,4,44,78,79,100]
temp= merge_list(left,right)
print(temp) #[0, 2, 4, 4, 7, 9, 23, 44, 78, 78, 79, 100]

3.二进制搜索

在二进制搜索中,我们采用排序的元素列表并开始在列表中间寻找元素。
如果搜索值与列表中的中间值匹配,我们完成搜索。
否则,我们通过选择是否根据搜索到的项目的值来处理列表的右半部分或左半部分来列出元素列表的一半。
我们重复这个步骤,直到找到元素或者在列表中得出结论

def bsearch(list, val):
idx0 = 0
idxn = len(list) - 1

# Find the middle most value
while idx0 <= idxn:
midval = (idx0 + idxn)// 2 #递归的进行中间值比对,直到找到对应元素val
if list[midval] == val:
return midval
# Compare the value the middle most value
if val > list[midval]:
idx0 = midval + 1
else:
idxn = midval - 1

if idx0 > idxn:
return None

# Initialize the sorted list
list = [2,7,19,34,53,72] #列表有序

# Print the search result
print(bsearch(list,72)) #5
print(bsearch(list,11)) #None

五、递归算法

1.递归思想

递归:函数内部自己调用自己称为递归
修复代码的步骤会一次又一次地执行新值。
还必须设置判断递归调用何时结束的标准。

递归特点:
# 1.递归根据参数的不同执行不同的操作,函数内部自己调用自己
# 2.递归函数必须要有出口,即参数满足一定条件时,退出循环,否则会造成死循环

递归函数编写思路:
1.写出临界条件 ------> 循环退出条件
2.找出这一次执行和上一次执行的关系 ------> 下一次操作与上一次操作之间的关系 --》找出递推关系
3.假设当前函数已经能用,调用自身计算上一次结果,再求出本次的结果 -----> 递归套用

递归使用诀窍
每当递归函数调用自身时,它都会将给定的问题拆解为子问题。
递归调用继续进行,直到子问题无需进一步递归就可以解决的地步

2.递归实现 1+2+3.python内置模块+4+...+n 结果并返回

分析:1+2+3.python内置模块+...+n 等价于 1+2+3.python内置模块+...+ (n-1) +n
1.临界条件:n==1 返回n
2.这一次与上一次关系: 2+3.python内置模块 与1+2 关系, 3.python内置模块+4与2+3关系 ...
3.python内置模块. f(1)+2=f(2), f(2)+3.python内置模块=f(3.python内置模块) -----> f(n-1)+n=f(n)
"""
def fun(n):
#退出循环的出口
if n== 1:
return n
else:
return n+fun(n-1)

sum=fun(5)
print(sum) #15

3.python内置模块-生成杨辉三角

杨辉三角:
[1]
[1, 1]
[1, 2, 1]
[1, 3.python内置模块, 3.python内置模块, 1]
[1, 4, 6, 4, 1]
[1, 5, 10, 10, 5, 1]
[1, 6, 15, 20, 15, 6, 1]
[1, 7, 21, 35, 35, 21, 7, 1]
[1, 8, 28, 56, 70, 56, 28, 8, 1]
[1, 9, 36, 84, 126, 126, 84, 36, 9, 1]

1.给定一个非负整数numRows,生成杨辉三角的前 numRows 行
已知杨辉三角第 i-1 行,生成第 i 行为:
[1]+
[yanghui[-1][i-1] + yanghui[-1][i] for i in range(1, numRows-1)] +
[1]
"""
class Solution():
def generate(self, numRows):
if numRows == 0:
return []
elif numRows == 1:
return [[1]]

else: # 调用自身生成前 numRows - 1 行的杨辉三角
yanghui = self.generate(numRows - 1)
# 根据倒数第二行再生成最后一行:
last_row = [1] + [yanghui[-1][i - 1] + yanghui[-1][i] for i in range(1, numRows-1)]+[1]
yanghui.append(last_row)
return yanghui

yanghui= Solution()
print(yanghui.generate(10))

4.反向打印字符串

def reverse_prints(s):
if len(s)<=1:
return s
return reverse_prints(s[1:])+s[0]

s="abcdefghijk"
print(reverse_prints(s)) #kjihgfedcba

六、回溯法

回溯是递归的一种形式
基本思想是:
在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。
当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,
则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)。
若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。
而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。
查找给定字母集合的所有可能排列顺序:
当我们选择一对时,我们应用回溯来验证是否已经创建了该确切的一对;
如果尚未创建,则将该对添加到答案列表中,否则将被忽略。

def permute(list, s):
if list == 1:
return s
else:
return [ y + x
for y in permute(1, s)
for x in permute(list - 1, s)
]

print(permute(1, ["a","b","c"])) #['a', 'b', 'c']
print(permute(2, ["a","b","c"])) #['yi'wei'ciaa', 'ab', 'ac', 'ba', 'bb'

七、哈希算法

1.算法思想

Hash函数需要明白的几个限制:
1.Hash的主要原理就是把大范围映射到小范围;所以,输入的实际值的个数必须和小范围相当或者比它更小。不然冲突就会很多。
2.由于Hash逼近单向函数,所以可以用它来对数据进行加密。
3.不同的应用对Hash函数有着不同的要求。

Hash算法作用:
Hash算法在 信息安全 方面的应用主要体现在以下的3个方面:
(1)文件校验
  比较熟悉的校验算法有奇偶校验和CRC校验,这2种校验并没有防数据篡改的能力,它们一定程度上能检测并纠正数据传输中的信道误码,但却不能防止对数据的恶意破坏。
  MD5 Hash算法的"数字指纹"特性,使它成为目前应用最广泛的一种 文件完整性校验和(Checksum)算法,不少Unix系统有提供计算md5 checksum的命令。
(2)数字签名
  Hash算法也是 现代密码体系 中的一个重要组成部分。
由于非对称算法的运算速度较慢,所以在数字签名协议中,单向散列函数扮演了一个重要的角色:
对Hash值,又称"数字摘要"进行数字签名,在统计上可以认为 与对文件本身进行数字签名 是等效的,
且这样的协议还有其他的优点。
(3)鉴权协议
  如下的鉴权协议又被称作挑战–-认证模式:
在传输信道是可被侦听,但不可被篡改的情况下,这是一种简单而安全的方法。


文件的哈希值:
著名的hash算法MD5和SHA-1 可以说是目前应用最广泛的Hash算法,而它们都是以MD4为基础设计的
(1) MD4:
   MD4(RFC 1320)是MIT的Ronald L. Rivest在1990年设计的,MD是Message Digest的缩写。
它适用在 32位字长的处理器上用高速软件实现–它是基于32位操作数的位操作来实现的。
(2) MD5:
   MD5(RFC 1321)是Rivest于1991年对MD4的改进版本。
它对输入仍以512位分组,其输出是4个32位字的级联,与MD4相同。
MD5比MD4来得复杂,并且速度较之要慢一点,但更安全,在抗分析和抗差分方面表现更好

MD5-Hash-文件的数字文摘通过Hash函数计算得到。
不管文件长度如何,它的Hash函数计算结果是一个固定长度的数字;与加密算法不同,这个是不可逆的单向函数。
采用安全性高的Hash算法,如MD5、SHA时,两个不同的文件几乎不可能得到相同的Hash结果,因此,一旦文件被修改,就可检测出来。
(3) SHA-1及其他:
   SHA1是由NIST NSA设计为同DSA一起使用的;
它对长度小于264的输入,产生长度为160bit的散列值,因此抗穷举(brute-force)性更好。
SHA-1 设计时基于和MD4相同原理,并且模仿了该算法。

2.哈希表

哈希表:(hash table ,也叫散列表)
哈希表是一种使用哈希函数组织数据,以支持快速插入和搜索的数据结构。-----类似python中的dict类型
有两种不同类型的哈希表:哈希集合和哈希映射。
哈希集合:是集合数据结构的实现之一,用于存储非重复值。
哈希映射:是映射数据结构的实现之一,用于存储(key, value)键值对。
哈希表是根据 键-值 直接访问元素的数据结构;它通过把键-值映射到一个位置来访问记录,以加快查找的速度。
具体映射过程是:
利用哈希函数求Key的哈希值,再将哈希值对数组长度取余,取余结果当作哈希表的下标,
这个映射函数称为 哈希函数 ,映射过程称为哈希化,存放记录的数组叫做 哈希表 。

哈希表的特点:
1.可以理解为一个线性表,其中的元素不是紧密排列的,而是可能存在空隙;
2.哈希表是基于 快速存取 的角度设计的,也是一种典型的“空间换时间”的做法;
3.哈希表不可避免冲突现象:对不同的关键字可能得到同一哈希地址,即key1≠key2,而hash(key1)=hash(key2)。
4.数组查询速度快,但插入和删除慢;链表查询速度慢,但插入和删除速度快;哈希表是在数组和链表基础上做了优化
5.哈希表是一个在时间和空间上做出权衡的数据结构。

数组的特点是寻址容易,插入和删除困难;
链表的特点是寻址困难,插入和删除容易;
哈希表实现了寻址容易,插入删除也容易。


哈希函数及其作用:
定义:
对于动态查找表而言:1)表长不确定;
2)在设计查找表时,只知道关键字所属范围,而不知道确切的关键字
因此,一般情况需建立一个函数关系,以f(key)作为关键字key在表中的位置,将关键字的集合映射到某个地址集合上,通常称这个函数f(key)为哈希函数。
作用:
把一个大范围映射到一个小范围,目的往往是为了节省空间,使得数据容易保存;Hash函数往往应用于 查找

常用的构造哈希函数的方法
  哈希(散列)函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快地定位:
  1. 直接寻址法:取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a•key + b,其中a和b为常数(这种散列函数叫做自身函数)
  2. 数字分析法:分析一组数据,比如一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体相同,这样的话,出现冲突的几率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果用后面的数字来构成散列地址,则冲突的几率会明显降低。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
  3. 平方取中法:取关键字平方后的中间几位作为散列地址。
  4. 折叠法:将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。
  5. 随机数法:选择一随机函数,取关键字的随机值作为散列地址,通常用于关键字长度不同的场合。
  6. 除留余数法:取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p, p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生同义词。


哈希冲突:哈希映射对不同的键,可能得到同一个散列地址,即同一个数组下标,这种现象称为冲突。
遇到这种情况,可使用链表在已有数据的后面,继续存储新的数据。
解决冲突的办法:
(1)链地址法:在存储数据的过程中,如果发生冲突,可以利用链表在已有数据的后面插入新数据来解决冲突
(2)线性探查法:冲突后,线性向前试探,找到最近的一个空位置。缺点是会出现堆积现象。
存取时,可能不是同义词的词也位于探查序列,影响效率。
(3)双散列函数法:在位置d冲突后,再次使用另一个散列函数产生一个与散列表桶容量m互质的数c,
依次试探(d+n*c)%m,使探查序列跳跃式分布。
(4)开放地址发:指当冲突发生时,立刻计算出一个候补地址(数组上的位置)并将数据存进去,
如果仍然有冲突,便继续计算下一个候补地址,直到有空地址为止,
可以通过多次使用哈希函数或“线性探测法”等方法计算候补地址。

1.开放地址法
开放地执法有一个公式:Hi=(H(key)+di) MOD m i=1,2,…,k(k<=m-1)
其中,m为哈希表的表长。di 是产生冲突的时候的增量序列。如果di值可能为1,2,3,…m-1,称线性探测再散列。
如果di取1,则每次冲突之后,向后移动1个位置.如果di取值可能为1,-1,2,-2,4,-4,9,-9,16,-16,…k*k,-k*k(k<=m/2),称二次探测再散列。
如果di取值可能为伪随机数列。称伪随机探测再散列。

2.再哈希法
当发生冲突时,使用第二个、第三个、哈希函数计算地址,直到无冲突时。缺点:计算时间增加。
比如上面第一次按照姓首字母进行哈希,如果产生冲突可以按照姓字母首字母第二位进行哈希,再冲突,第三位,直到不冲突为止

3.链地址法(拉链法)
将所有关键字为同义词的记录存储在同一线性链表中。这种方法,可以近似的认为是筒子里面套筒子

4.建立一个公共溢出区
假设哈希函数的值域为[0,m-1],则设向量HashTable[0..m-1]为基本表,另外设立存储空间向量OverTable[0..v]用以存储发生冲突的记录。

拉链法的优缺点:
优点:
①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结 点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在 用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。

缺点:
指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。


负载因子:
散列函数的计算结果是一个存储单位地址,每个存储单位称为“桶”,设一个散列表有m个桶,则散列函数的值域应为[0,m-1]
(比如我们存储70个元素,但我们可能为这70个元素申请了100个元素的空间,70/100=0.7 这个数字称为负载因子
之所以这样做,也是为了“快速存取”的目的
我们基于一种 结果尽可能随机平均分布的固定函数H 为每个元素安排存储位置,
这样就可以避免遍历性质的线性搜索,以达到快速存取,但是由于此随机性,也必然导致一个问题就是冲突,
所谓冲突,即两个元素通过散列函数H得到的地址相同,那么这两个元素称为“同义词”。
这类似于70个人去一个有100个椅子的饭店吃饭。)


查找的性能分析
  散列表的查找过程基本上和造表过程相同
一些关键码可通过散列函数转换的地址直接找到,另一些关键码在散列函数得到的地址上产生了冲突,需要按处理冲突的方法进行查找。
在介绍的三种处理冲突的方法中,产生冲突后的查找仍然是给定值与关键码进行比较的过程。
所以,对散列表查找效率的量度,依然用平均查找长度来衡量。
  查找过程中,关键码的比较次数,取决于产生冲突的多少,产生的冲突少,查找效率就高,产生的冲突多,查找效率就低。

因此,影响产生冲突多少的因素,也就是影响查找效率的因素。影响产生冲突多少有以下三个因素:
  1. 散列函数是否均匀;
  2. 处理冲突的方法;
  3. 散列表的装填因子。
 
散列表的装填因子
 散列表的装填因子定义为:α= 填入表中的元素个数 / 散列表的长度
 (α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,
所以,α越大,填入表中的元素较多,产生冲突的可能性就越大;α越小,填入表中的元素较少,产生冲突的可能性就越小。)
  
实际上,散列表的平均查找长度是装填因子α的函数,只是不同处理冲突的方法有不同的函数。

3.宝石和石头.py

题目:给定 字符串J代表石头中宝石的类型、字符串S代表你拥有的石头
S中每个字符代表了一种你拥有的石头的类型,你想知道你拥有的石头中有多少是宝石?
J中的字母不重复,J和S中的所有字符都是字母。字母区分大小写,因此"a"和"A"是不同类型的石头。
示例 1:
输入: J = "aA", S = "aAAbbbb" 输出: 3.python内置模块
示例 2:
输入: J = "z", S = "ZZ" 输出: 0

注意:S 和 J 最多含有50个字母。 J 中的字符不重复。
"""
"迭代解法"
def JewelsInStone(J,S):
return sum([ i in J for i in S]) #时间复杂度是 len(S) * len(J)

J = "aA"
S = "aAAbbbb"
count= JewelsInStone(J,S)
print(count) #3.python内置模块

"哈希思想解法, 时间复杂度为:len(S) + len(J)"
def JewelsInStone1(J,S):
dic={}
count= 0
for i in S:
dic[i]= dic.get(i,0)+1 #字符为键,出现次数为值,存入字典
#print(dic[i],end="") #aAAbbbb---->1 1 2 1 2 3.python内置模块 4
for j in J:
count+= dic.get(j,0)
return count

count1= JewelsInStone1(J,S)
print(count1) #3.python内置模块

4.字母异位词分组.py

字母异位词:指字母相同,但排列不同的字符串

给定一个字符串数组,将字母异位词组合在一起,所有输入均为小写字母,不考虑答案输出的顺序
输入: ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["ate","eat","tea"], ["nat","tan"], ["bat"]]
"""
def groupnums(nums):
dic={}
for i in nums:
key= tuple(sorted(i))
dic[key]= dic.get(key,[])+[i]
return list(dic.values())

nums= ["eat", "tea", "tan", "ate", "nat", "bat"]
group= groupnums(nums)
print(group) #[['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']]

 

posted @ 2021-04-11 15:13  十八小罗汉  阅读(360)  评论(0编辑  收藏  举报