注:数据结构与算法使用Python语言实现,涉及基本数据结构、十大排序算法、递归分治、贪心动归等,意在帮大家更加容易的学习数据结构与算法以及进一步梳理这些知识点。
Python数据结构与算法前文可参考:
Python数据结构与算法(一):引言
一、线性结构
1.顺序存储:数组
2.链式存储:链表
3.线性结构对比
4.队列
6.栈
二、树形结构
1.二叉树
2.二叉树的特点
3.特殊二叉树
4.二叉树的性质
5.二叉树的存储结构
6.二叉树的遍历
三、图形结构
1.图
2.图的存储结构
3.图的遍历
4.图的基本问题
四、集合结构
1.HashMap
2.HashSet
五、算法的复杂度分析
1.算法的时间复杂度
2.常见时间复杂度
3.时间复杂度计算方法
4.算法的空间复杂度
5.空间复杂度计算方法
六、实例分析
1.斐波那契数列
2.两数之和
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。物理上的存储方式事实上就是在内存中找个初始地址,然后通过占位的形式,把一定的内存空间给占了,然后把相同数据类型的数据元素依次放在这块空地中。
顺序表的加入操作:
顺序表的删除操作:
单链表:
对于线性表来说,总得有个头有个尾,链表也不例外。我们把链表中的第一个结点的存储位置叫做头指针,最后一个结点指针为空(NULL)。
头指针与头结点的异同:头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(但也可以用来存放链表的长度)。有了头结点,对在第一元素结点前插入结点和删除第一结点起操作与其它结点的操作就统一了。头结点不一定是链表的必须要素。
单链表的插入:
单链表的插入根本用不着惊动其他结点,只需要让s.next和p.next的指针做一点改变。
s.next = p.next;
p.next = s;
我们通过图片来解读一下这两句代码。
单链表的删除
现在我们再来看单链表的删除操作。
单链表结构与顺序存储结构优缺点:我们分别从存储分配方式、时间性能、空间性能三方面来做对比。
队列的定义:
栈是一种重要的线性结构,可以这样讲,栈是前面讲过的线性表的一种具体形式。就像我们刚才的例子,栈这种后进先出的数据结构应用是非常广泛的。在生活中,例如我们的浏览器,每点击一次“后退”都是退回到最近的一次浏览网页。例如我们Word,Photoshop等的“撤销”功能也是如此。栈的本质是一个线性表,所以我们可以轻松的用python的列表来模拟一个栈。
最开始栈中不含有任何数据,叫做空栈,此时栈顶就是栈底。然后数据从栈顶进入,栈顶栈底分离,整个栈的当前容量变大。数据出栈时从栈顶弹出,栈顶下移,整个栈的当前容量变小。
定义:二叉树(Binary Tree)是n(n>=0)个结点的有限集合,该集合或者为空集(空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
节点:
二叉树中每个元素都称为节点。
度:
二叉树的度表示节点的子树或直接继承者的数目,二叉树的每一个节点最大度数为2,最小度数为0。
叶子:
叶是叶节点的缩写。叶节点是树的底部的节点,叶节点不具有子节点。
满二叉树的特点有:
完全二叉树的特点有:
注意:满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。
以下这些都不是完全二叉树:
二叉排序树:
这下凸显完全二叉树的优越性了,由于他的严格定义,在数组直接能表现出逻辑结构。
对于一般的二叉树,也可以借鉴完全二叉树的处理方式,把不存在的结点用“^”代替即可。但是考虑到一种极端的情况,回顾一下斜树,如果是一个又斜树,那么会变成这样
class Node(object):
def __init__(self, data):
self.data = data
self.lchild = None
self.rchild = None
二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
遍历的顺序为:ABDHIEJCFKG
遍历的顺序为:HDIBEJAFKCG
遍历的顺序为:HIDJEBKFGCA
遍历的顺序为:ABCDEFGHIJK
在前边讲解的线性表中,每个元素之间只有一个直接前驱和一个直接后继。在树形结构中,数据元素之间是层次关系,并且每一层上的数据元素可能和下一层中多个元素相关,但只能和上一层中一个元素相关。但这仅仅都只是一对一,一对多的简单模型,如果要研究如人与人之间关系就非常复杂了。图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
上图G1是一个无向图,G1={V1,E1},其中V1={A,B,C,D},E1={(A,B),(B,C),(C,D),(D,A),(A,C)}
上图G2是一个无向图,G2={V2,E2},其中V2={A,B,C,D},E2={,,
对于无向图G=(V,E),如果边(V1,V2)∈E,则称顶点V1和V2互为邻接点(Adjacent),即V1和V2相邻接。边(V1,V2)依附(incident)于顶点V1和V2,或者说边(V1,V2)与顶点V1和V2相关联。顶点V的度(Degree)是和V相关联的边的数目,记为TD(V),如下图,顶点A与B互为邻接点,边(A,B)依附于顶点A与B上,顶点A的度为3。
对于有向图G=(V,E),如果有
图的邻接矩阵(Adjacency Matrix)存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
有了这个二维数组组成的对称矩阵,我们就可以很容易地知道图中的信息:要判定任意两顶点是否有边无边就非常容易了;要知道某个顶点的度,其实就是这个顶点Vi在邻接矩阵中第i行(或第i列)的元素之和;求顶点Vi的所有邻接点就是将矩阵中第i行元素扫描一遍,arc[i][j]为1就是邻接点咯。
深度优先遍历(从顶点G开始):
广度优先遍历(从顶点G先开始):
无向带权图的最小生成树问题:在保证任意两个顶点之间都有路径到达的前提下,所使用的边的个数最少(也即不能有环)且权值之和最小。城市抽象成顶点,道路抽象成边在预算极其有限的情况下,要保证所有城市都能有路径到达,且修路总花费最小
方案一:▪成本:11+26+20+22+18+21+24+19=161
方案二▪成本:8+12+10+11+17+19+16+7=100
方案三▪成本:8+12+10+11+16+19+16+7=99
无向带权图最小生成树解决办法:
最短路径问题:
最短路径问题:在网图(带权图)和非网图中,最短路径的含义是不同的。网图是两顶点经过的边上权值之和最少的路径。非网图是两顶点之间经过的边数最少的路径。我们把路径起始的第一个顶点称为源点,最后一个顶点称为终点。关于最短路径的算法,最常见两种:
我们要在a[]中查找key关键字的记录:
f(张三丰) = 图书馆
除留余数法:此方法为最常用的构造散列函数方法,对于散列表长为m的散列函数计算公式为:f(key) = key mod p(p<=m)
例:假设关键字集合为{12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34},同样使用除留余数法求散列表。
HashMap 存储的是key,value的数据;HashSet 存储的是key数据,可以理解为(key,key)形式的HashMap就是HashSet,且HashSet要求所存储的元素不允许有重复,HashSet的实现方法与HashMap完全一样。
我们提到设计算法要尽量的提高效率,这里效率高一般指的是算法的执行时间。那么我们如何来度量一个算法的执行时间呢?
事后统计方法:这种方法主要是通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序的运行时间进行比较,从而确定算法效率的高低。但这种方法显然是有很大缺陷:必须依据算法事先编制好测试程序,通常需要花费大量时间和精力,完了发觉测试的是糟糕的算法,那不是功亏一篑?不同测试环境差别不是一般的大!我们把刚刚的估算方法称为事后诸葛亮。我们的计算机前辈们也不一定知道诸葛亮是谁,为了对算法的评判更为科学和便捷,他们研究出事前分析估算的方法
事前分析估算方法:在计算机程序编写前,依据统计方法对算法进行估算。
经过总结,我们发现一个高级语言编写的程序在计算机上运行时所消耗的时间取决于下列因素:
由此可见,抛开这些与计算机硬件、软件有关的因素,一个程序的运行时间依赖于算法的好坏和问题的输入规模。(所谓的问题输入规模是指输入量的多少)
用 n 表示输入数据的个数
算法时间复杂度的定义:
在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n)= O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的时间复杂度。其中f(n)是问题规模n的某个函数。
常数阶:
sum = 0
n = 100
print("I love fengwo")
print("I love fengwo")
print("I love fengwo")
print("I love fengwo")
print("I love fengwo")
print("I love fengwo")
sum = (1+n)*n/2
线性阶:一般含有非嵌套循环涉及线性阶,线性阶就是随着问题规模n的扩大,对应计算次数呈直线增长。下面这段代码,它的循环的时间复杂度为O(n),因为循环体中的代码需要执行n次。
sum = 0
for i in range(1,101):
sum = sum + i
平方阶:
n = 100
for i in range(n+1):
for j in range(n+1):
print("I love fengwo")
对数阶:
i = 1
n = 100
while i < n:
i = i * 2
由于每次i*2之后,就举例n更近一步,假设有x个2相乘后大于或等于n,则会退出循环。于是由2^x = n得到x = log(2)n,所以这个循环的时间复杂度为O(logn)。
O的定义:如果存在正的常数C和自然数N0,使得当N³N0时有f(N)£Cg(N),则称函数f(N)当N充分大时上有界,且g(N)是它的一个上界,记为f(N)=O(g(N))。即f(N)的阶不高于g(N)的阶。
由于我们更加关注算法复杂度的增长趋势,所以判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高项)的阶数。下界表示:
上下界表示:大Theta表示
常见的时间复杂度有:
计算方法
通常我们计算时间复杂度都是计算最坏情况或者平均情况
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。例如交换列表中两个元素的算法的空间复杂度为O(1)
因为该函数运行时入栈最深为1, 每运行一次只创建一个临时变量temp。
def func(n):
k = 10
if n >= k:
return n
else:
return func(n+1)
递归实现,调用fun函数,每次都创建1个变量k。调用n次,空间复杂度O(n*1)=O(n)。
有一对兔子,从出生两个月后就有繁殖能力,一对兔子每个月都生一对兔子,小兔子对长到第三个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少?意大利的著名数学家斐波那契在算盘全集中提出的该问题,后人把各个月份兔子数量称为“斐波那契数列”。
# 假设:刚开始兔子为child
# 分析:兔子的周期分为 child,young,old
# 1月:1,0,0 1
# 2月:0,1,0 1
# 3月:1,0,1 2
# 4月:1,1,1 3
# 5月:2,1,2 5
# 。。。
# 9月:13,8,13 34
# 结论:F(n) = F(n-1) + F(n-2)
非递归版:
def fib(n):
a, b = 0, 1
for i in range(n):
a, b = b, a+b
return a
递归版:
def fib(n):
if n == 1:
return 1
if n == 2:
return 1
return fib(n-1) + fib(n-2)
简单证明:这是一颗二叉树,树的高度h与n为线性关系,该算法的总运行次数为2^h,故时间复杂度为2^n
def twoSum(lst, target):
n = len(lst)
for i in range(1, n):
for j in range(i):
if lst[i] + lst[j] == target:
return (i, j)
return -1
时间复杂度:
最好情况:lst[0] + lst[1] 恰好等于target O(1)
最坏情况:遍历所有情况都没有解 O(n^2)
平均情况: 也是 O(n^2)▪
# 思路二:双指针法
# 如果两个指针指向元素的和 sum == target,那么得到要求的结果;
# 如果 sum > target,移动较大的元素指针,使 sum 变小一些;
# 如果 sum < target,移动较小的元素指针,使 sum 变大一些。
def twoSum(lst,target):
result = []
i = 0
j = len(lst)-1
while i < j:
sum = lst[i] + lst[j]
if sum == target:
result.append((i,j))
i += 1
elif sum > target:
j -= 1
else :
i += 1
return result
return -1
时间复杂度:
O(n)
空间复杂度:
O(1)
# 思路三:HashMap
# 使用字典存储键值对,键为数组中的元素,值为该元素在数组中的下标
# 从左到右扫描,如果target-lst[i]不在字典中,将键值对(lst[i], i)存入字典;
# 如果 target-lst[i]在字典中,那么target-lst[i]与lst[i]刚好可以得到结果,取出他们的索引即可
def twoSum2(lst, target):
indexForNum = dict()
for i in range(len(lst)):
if (target - lst[i]) in indexForNum:
return [indexForNum.get(target-lst[i]), i]
else:
indexForNum[lst[i]] = i
时间复杂度:
O(n)
空间复杂度:
O(n)
相应面试题可参考如下内容:
BAT大厂数据分析面试经验:“高频面经”之数据分析篇
BAT大厂数据挖掘面试经验:“高频面经”之数据结构与算法篇
BAT大厂数据开发面试经验:“高频面经”之大数据研发篇
BAT大厂机器学习算法面试经验:“高频面经”之机器学习篇
BAT大厂深度学习算法面试经验:“高频面经”之深度学习篇