线性表
1.顺序存储结构
线性表的结点按逻辑顺序依次存放在一组地址连续的存储单元里。是随机存取的顺序存储结构。顺序存储指内存地址是一块的,随机存取指访问时可以按下标随机访问,存储和存取是不一样的。
- 便于线性表的构造和任意元素的访问
- 插入:插入新结点,之后结点后移。平均时间复杂度:O(n)
- 删除结点:删除节点,之后结点前移。平均时间复杂度:O(n)
2.线性链表
用一组任意的存储单元来依次存放线性表的结点,这组存储单元即可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的。链表中结点的逻辑次序和物理次序不一定相同。
- 单链表查找:只能从链表的头指针出发,顺链域next逐个结点往下搜索,直到搜索到第i个结点为止。因此,链表不是随机存取结构。
- 插入:先找到表的第i-1的存储位置,然后插入。新结点先连后继,再连前驱。
- 首先找到ai-1的存储位置p。然后令p–>next指向ai的直接后继结点,即把ai从链上摘下。最后释放结点ai的空间.r=p->next;p->next=r->next;delete r。
- 判断一个单向链表中是否存在环的最佳方法是快慢指针。
- 静态链表是用数组实现的,是顺序的存储结构,在物理地址上是连续的,而且需要预先分配大小。动态链表是用申请内存函数动态申请内存的,所以在链表的长度上没有限制。动态链表因为是动态申请内存的,所以每个节点的物理地址不连续,要通过指针来顺序访问。静态链表在插入、删除时也是通过修改指针域来实现的,与动态链表没有什么分别
栈和队列
1.栈
- 顺序存储栈:顺序存储结构
- 链栈:链式存储结构。插入和删除操作仅限制在链头位置上进行。栈顶指针就是链表的头指针。通常不会出现栈满的情况。 不需要判断栈满但需要判断栈空。
- 基本操作:删除栈顶元素、判断栈是否为空以及将栈置为空栈等
- 堆栈溢出一般是循环的递归调用、大数据结构的局部变量导致的
定义栈以及基本操作
- 定义一个栈
#定义一个栈类
class Stack():
# 栈的初始化
def __init__(self):
self.items = []
# 判断栈是否为空,为空返回True
def isEmpty(self):
return self.items ==[]
# 向栈内压入一个元素
def push(self, item):
self.items.append(item)
# 从栈内推出最后一个元素
def pop(self):
return self.items.pop()
# 返回栈顶元素
def peek(self):
return self.items[len(self.items)-1]
# 判断栈的大小
def size(self):
return len(self.items)
- 利用栈将字符串反转
def revString(mystr):
s = Stack() #实例化一个对象 s
outputStr = ''
for c in mystr:
s.push(c)
while not s.isEmpty():
outputStr += s.pop()
return outputStr
- 利用栈判断括号平衡:
# 利用栈判断括号平衡Balanced parentheses
def parChecker(symbolString):
s = Stack()
balanced = True
index = 0
while index < len(symbolString) and balanced:
symbol = symbolString[index]
if symbol in '([{':
s.push(symbol)
else:
if s.isEmpty():
balanced = False
else:
top = s.pop()
if not matches(top, symbol):
balanced = False
index += 1
if balanced and s.isEmpty():
return True
else:
return False
def matches(open, close):
opens = '([{'
closers = ')]}'
return opens.index(open) == closers.index(close)
- 利用栈将十进制整数转化为二进制整数
def Dec2Bin(decNumber):
s = Stack()
while decNumber > 0:
temp = decNumber%2
s.push(temp)
decNumber = decNumber // 2
binString = ''
while not s.isEmpty():
binString += str(s.pop())
return binString
- 利用栈实现多进制转换
def baseConverter(decNumber, base):
digits = '0123456789ABCDEF'
s = Stack()
while decNumber > 0:
temp = decNumber % base
s.push(temp)
decNumber = decNumber // base
newString = ''
while not s.isEmpty():
newString = newString + digits[s.pop()]
return newString
- 利用栈实现普通多项式的后缀表达式
def infixToPostfix(infixexpr):
prec = {}
prec['*'] = 3
prec['/'] = 3
prec['+'] = 2
prec['-'] = 2
prec['('] = 1
opStack = Stack()
postfixList = []
tokenList = infixexpr.split()
for token in tokenList:
if token in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' or token in '0123456789':
postfixList.append(token)
elif token == '(':
opStack.push(token)
elif token == ')':
topToken = opStack.pop()
while topToken != '(':
postfixList.append(topToken)
topToken = opStack.pop()
else:
while (not opStack.isEmpty()) and (prec[opStack.peek()] >= prec[token]):
postfixList.append(opStack.pop())
opStack.push(token)
while not opStack.isEmpty():
postfixList.append(opStack.pop())
return ''.join(postfixList)
2.队列
队列(Queue)也是一种运算受限的线性表。它只允许在表的一端进行插入,而在另一端进行删除。允许删除的一端称为队头(front),允许插入的一端称为队尾(rear)。先进先出。
- 顺序队列:顺序存储结构。当头尾指针相等时队列为空。在非空队列里,头指针始终指向队头前一个位置,而尾指针始终指向队尾元素的实际位置
- 循环队列:在循环队列中进行出队、入队操作时,头尾指针仍要加1,朝前移动。故队空和队满时头尾指针均相等。因此,我们无法通过front=rear来判断队列“空”还是“满”
- 链队列:链式存储结构。限制仅在表头删除和表尾插入的单链表。显然仅有单链表的头指针不便于在表尾做插入操作,为此再增加一个尾指针,指向链表的最后一个结点。
- 队空条件:rear==front,但是一般需要引入新的标记来说明栈满还是栈空,比如每个位置布尔值
- 队满条件:(rear+1) % QueueSize==front,其中QueueSize为循环队列的最大长度
- 计算队列长度:(rear-front+QueueSize)% QueueSize
- 入队:(rear+1)% QueueSize
- 出队:(front+1)% QueueSize
- 假设以数组A[N]为容量存放循环队列的元素,其头指针是front,当前队列有X个元素,则队列的尾指针值为(front+X mod N)
3.串
串(String)是零个或多个字符组成的有限序列。长度为零的串称为空串(Empty String),它不包含任何字符。通常将仅由一个或多个空格组成的串称为空白串(Blank String) 注意:空串和空白串的不同,例如“ ”和“”分别表示长度为1的空白串和长度为0的空串。
串的表示和实现
- 定长顺序存储表示。静态存储分配的顺序表
2.堆分配存储表示。存储空间是在程序执行过程中动态分配而得。所以也称为动态存储分配的顺序表
3.串的链式存储结构
串匹配:将主串称为目标串,子串称之为模式串。蛮力法匹配。KMP算法匹配。Boyer-Moore算法匹配。
4.数组和广义表
数组和广义表可看成是一种特殊的线性表,其特殊在于: 表中的元素本身也是一种线性表。内存连续。根据下标在O(1)时间读/写任何元素。
二维数组,多维数组,广义表,树,图都属于非线性结构
数组
数组的顺序存储:行优先顺序;列优先顺序。数组中的任一元素可以在相同的时间内存取,即顺序存储的数组是一个随机存取结构。
关联数组(Associative Array),又称映射(Map)、字典( Dictionary)是一个抽象的数据结构,它包含着类似于(键,值)的有序对。 不是线性表。
广义表
广义表(Lists,又称列表)是线性表的推广。广义表是n(n≥0)个元素a1,a2,a3,…,an的有限序列,其中ai或者是原子项,或者是一个广义表。若广义表LS(n>=1)非空,则a1是LS的表头,其余元素组成的表(a2,…an)称为LS的表尾。广义表的元素可以是广义表,也可以是原子,广义表的元素也可以为空。表尾是指除去表头后剩下的元素组成的表,表头可以为表或单元素值。所以表尾不可以是单个元素值。
三个结论
- 广义表的元素可以是子表,子表元素还可以是子表。所以广义表是一个多层次的结构,可以用图形象的表示
- 广义表可为其他表所共享。
- 广义表具有递归性
考点
- 广义表同级元素具有线性关系
- 广义表的表头为空,并不代表该广义表为空表。判断为空的条件应该是广义表长度为0.
- 已知广义表LS=((a,b,c),(d,e,f)),运用head和tail函数取出LS中原子e的运算是head(tail(head(tail(LS)))。根据表头、表尾的定义可知:任何一个非空广义表的表头是表中第一个元素,它可以是原子,也可以是子表,而其表尾必定是子表。也就是说,广义表的head操作,取出的元素是什么,那么结果就是什么。但是tail操作取出的元素外必须加一个表——“()“。tail(LS)=((d,e,f));head(tail(LS))=(d,e,f);tail(head(tail(LS)))=(e,f);head(tail(head(tail(LS))))=e。
- 二维以上的数组其实是一种特殊的广义表
- 在(非空)广义表中:1、表头head可以是原子或者一个表 2、表尾tail一定是一个表 3.广义表难以用顺序存储结构 4.广义表可以是一个多层次的结构
5.树和二叉树
一种非线性结构。树是递归结构,在树的定义中又用到了树的概念。
基本术语
1.树结点:包含一个数据元素及若干指向子树的分支;
2.孩子结点:结点的子树的根称为该结点的孩子;
3.双亲结点:B结点是A结点的孩子,则A结点是B结点的双亲;
4.兄弟结点:同一双亲的孩子结点;
5.堂兄结点:同一层上结点;
6.结点层次:根结点的层定义为1;根的孩子为第二层结点,依此类推;
7.树的高(深)度:树中最大的结点层
8.结点的度:结点子树的个数,就是有几个孩子
9.树的度: 树中最大的结点度。
10.叶子结点:也叫终端结点,是度为0的结点;
11.分枝结点:度不为0的结点(非终端结点);
12.森林:互不相交的树集合;
13.有序树:子树有序的树,如:家族树;
14.无序树:不考虑子树的顺序;
二叉树
二叉树可以为空。二叉树结点的子树要区分左子树和右子树,即使只有一棵子树也要进行区分,说明它是左子树,还是右子树。这是二叉树与树的最主要的差别。
注意区分:二叉树、二叉查找树/二叉排序树/二叉搜索树、二叉平衡(查找)树
- 二叉树:每个节点最多只有两个子树。
- 一棵深度为k,且有2^k-1个结点的二叉树,称为满二叉树。
- 若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。
- 具有n个结点的完全二叉树的深度为floor(log2n)+1。
- 深度为k的完全二叉树,至少有2的k-1次幂个叶子结点,至多有2k-1个结点。
- 平衡二叉树是一颗空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一平衡二叉树
- 二叉排序树:又称为二叉查找树or二叉搜索树。它是一颗空树或者是具有下列性质的二叉树
1)若左子树不空,则左子树上的所有节点的值均小于它的根节点的值
2)若右子树不空,则柚子树上所有的结点的值都大于她根节点的值
3)左右子树也分别为二叉排序树
4)没有键值相等的结点
二叉树遍历
先序遍历:根左右
中序遍历:左根右
后序遍历:左右根
层次遍历:一维数组存储二叉树,总是以层次遍历的顺序存储结点。层次遍历应该借助队列。
二叉树性质
1.在二叉树的第 i 层上至多有2的i次幂-1个结点
2.深度为 k 的二叉树上至多含 2的k次幂-1 个结点(k≥1)
3.树与转换后的二叉树的关系:转换后的二叉树的先序对应树的先序遍历;转换后的二叉树的中序对应树的后序遍历
6.哈夫曼树
一些概念
1.路径:从一个祖先结点到子孙结点之间的分支构成这两个结点间的路径;
2.路径长度:路径上的分支数目称为路径长度;
3.树的路径长度:从根到每个结点的路径长度之和。
4.结点的权:根据应用的需要可以给树的结点赋权值;
5.结点的带权路径长度:从根到该结点的路径长度与该结点权的乘积;
6.树的带权路径长度=树中所有叶子结点的带权路径之和;通常记作 WPL=∑wi×li
7.哈夫曼树:假设有n个权值(w1, w2, … , wn),构造有n个叶子结点的二叉树,每个叶子结点有一个 wi作为它的权值。则带权路径长度最小的二叉树称为哈夫曼树。最优二叉树。
- 霍夫曼树是满二叉树,若有n个节点,则共有(n+1)/2个码
- 假设哈夫曼树是二叉的话,则度为0的结点个数为N,度为2的结点个数为N-1,则结点总数为2N-1。哈夫曼树的结点个数必为奇数。
- 哈夫曼树不一定是完全二叉树,但一定是最优二叉树
7.图遍历与回溯
图搜索->形成搜索树
1.穷举法
2.贪心法。多步决策,每步选择使得构成一个问题的可能解,同时满足目标函数
3.回溯法,根据题意,选取度量标准,然后将可能的选择方法按度量标准所要求顺序排好,每次处理一个量,得到该意义下的最优解的分解处理
图
无向图
1.回路或环:第一个顶点和最后一个顶点相同的路径。
2.简单回路或简单环:除第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路
3.连通:顶点v至v’ 之间有路径存在
4.连通图:无向图图 G 的任意两点之间都是连通的,则称G是连通图。
5.连通分量:极大连通子图,子图中包含的顶点个数极大
6.所有顶点度的和必须为偶数
有向图
1.回路或环:第一个顶点和最后一个顶点相同的路径。
2.简单回路或简单环:除第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路。
3.连通:顶点v至v’之间有路径存在
4.强连通图:有向图G的任意两点之间都是连通的,则称G是强连通图。各个顶点间均可达。
5.强连通分量:极大连通子图
6.有向图顶点的度是顶点的入度与出度之和。邻接矩阵中第V行中的1的个数是V的出度
7.生成树:极小连通子图。包含图的所有n个结点,但只含图的n-1条边。在生成树中添加一条边之后,必定会形成回路或环。
8.完全图:有 n(n-1)/2 条边的无向图。其中n是结点个数。必定是连通图。
9.有向完全图:有n(n-1)条边的有向图。其中n是结点个数。每两个顶点之间都有两条方向相反的边连接的图。
10.一个无向图 G=(V,E) 是连通的,那么边的数目大于等于顶点的数目减一:|E|>=|V|-1,而反之不成立。如果 G=(V,E) 是有向图,那么它是强连通图的必要条件是边的数目大于等于顶点的数目:|E|>=|V|,而反之不成立。没有回路的无向图是连通的当且仅当它是树,即等价于:|E|=|V|-1。
图的存储形式
图的邻接矩阵和邻接表
1.邻接矩阵和加权邻接矩阵
- 无权有向图:出度: i行之和;入度: j列之和。
- 无权无向图:i结点的度: i行或i列之和。
- 加权邻接矩阵:相连为w,不相连为∞
2.邻接表 - 用顶点数组表、边(弧)表表示该有向图或无向图
- 顶点数组表:用数组存放所有的顶点。数组大小为图顶点数n
- 边表(边结点表):每条边用一个结点进行表示。同一个结点的所有的边形成它的边结点单链表。
- n个顶点的无向图的邻接表最多有n(n-1)个边表结点。有n个顶点的无向图最多有n(n-1)/2条边,此时为完全无向图,而在邻接表中每条边存储两次,所以有n(n-1)个结点
图的遍历
深度优先搜索利用栈
深度优先遍历类似于树的先序遍历,是树的先序遍历的推广
1.从图中某个顶点v出发,访问v
2.找到刚访问过的顶点的第一个未被访问的邻接点,访问该顶点,以该顶点为新顶点重复此步骤,直至刚访问的顶点没有未被访问的邻接点为止。
3.返回前一个访问过得且扔有未被访问的邻接点的顶点,找到该顶点的下一个未被访问的邻接点,访问该顶点。
4重复步骤2,3,直至图中所有顶点都被访问过。
广度优先遍历
图的广度优先遍历就类似于树的层序遍历
从图中某个顶点v出发,访问v。
2.依次访问v的各个未被访问过得邻接点。
3.分别从这些邻接点出发依次访问他们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问。重复步骤3,直至图中所有已被访问的顶点的邻接点都被访问到。
- 求一条从顶点i到顶点s的简单路径——深度优先搜索
- 求两个顶点之间的一条长度最短的路径——广度优先搜索
- 当各边上权值均相等时,广度优先搜索可以用来解决单源最短路径问题
生成树和最小生成树
每次遍历一个连通图将图的边分成遍历所经过的边和没有经过的边两部分,将遍历经过的边同图的顶点构成一个子图,该子图称为生成树。因此有DFS生成树和BFS生成树。
生成树是连通图的极小子图,有n个顶点的连通图的生成树必定有n-1条边,在生成树中任意增加一条边,必定产生回路。若砍去它的一条边,就会把生成树变成非连通子图
最小生成树:生成树中边的权值(代价)之和最小的树。最小生成树问题是构造连通网的最小代价生成树。
Kruskal算法:令最小生成树集合T初始状态为空,在有n个顶点的图中选取权值最小的边并从图中删去,若该边加到T中有回路则丢弃,否则留在T中;依次类推,知道T中有n-1条边为止
Prim算法:它的基本思想是以顶点为主导地位,从起始顶点出发,通过选择当前可用的最小权值边把顶点加入到生成树当中来:
1.从连通网络N={V,E}中的某一顶点U0出发,选择与它关联的具有最小权值的边(U0,V),将其顶点加入到生成树的顶点集合U中。
2.以后每一步从一个顶点在U中,而另一个顶点不在U中的各条边中选择权值最小的边(U,V),把它的顶点加入到集合U中。如此继续下去,直到网络中的所有顶点都加入到生成树顶点集合U中为止。
Prim算法,Kruskal算法和Dijkstra算法都属于贪心算法
Dijkstra算法适用于边权值为正的情况,如果边权值为负数就才用另一种最短路算法Bellman-Ford算法。该算法是指从单个源点到各个结点的最短路,该算法适用于有向图和无向图。复杂度O(n^2)
Dijkstra算法图文详解
双向连通图和关节点
若从一个连通图中删去任何一个顶点及其相关联的边,它仍为一个连通图的话,则该连通图被称为重(双)连通图。
若连通图中的某个顶点和其相关联的边被删去之后,该连通图被分割成两个或两个以上的连通分量,则称此顶点为关节点。
没有关节点的连通图称为双连通图
1.生成树的根结点,有两个或两个以上的分支,则此顶点(生成树的根)必为关节点;
2.对生成树上的任意一个非叶“顶点”,若其某棵子树中的所有“顶点”没有和其祖先相通的回边,则该“顶点”必为关节点
有向无环图及其应用
拓扑排序。在用邻接表表示图时,对有n个顶点和e条弧的有向图而言时间复杂度为O(n+e)。一个有向图能被拓扑排序的充要条件就是它是一个有向无环图。
AOV网(Activity On Vertex):用顶点表示活动,边表示活动的优先关系的有向图称为AOV网。AOV网中不允许有回路,这意味着某项活动以自己为先决条件。
拓扑有序序列:把AOV网络中各顶点按照它们相互之间的优先关系排列一个线性序列的过程。若vi是vj前驱,则vi一定在vj之前;对于没有优先关系的点,顺序任意。
拓扑排序:对AOV网络中顶点构造拓扑有序序列的过程。方法:
1.在有向图中选一个没有前驱的顶点且输出之
2.从图中删除该顶点和所有以它为尾的弧
3.重复上述两步,直至全部顶点均已输出;或者当图中不存在无前驱的顶点为止(此时说明图中有环)
采用深度优先搜索或者拓扑排序算法可以判断出一个有向图中是否有环(回路)。
深度优先搜索只要在其中记录下搜索的节点数n,当n大于图中节点数时退出,并可以得出有回路。若有回路,则拓扑排序访问不到图中所有的节点,所以也可以得出回路。广度优先搜索过程中如果访问到一个已经访问过的节点,可能是多个节点指向这个节点,不一定是存在环。
拓扑算法描述:
1.把邻接表中入度为0的顶点依此进栈
2.若栈不空,则
1)栈顶元素vj退栈并输出;
2)在邻接表中查找vj的直接后继vk,把vk的入度减1;若vk的入度为0则进栈
3.若栈空时输出的顶点个数不是n,则有向图有环;否则,拓扑排序完毕。
AOE网:带权的有向无环图,其中顶点表示事件,弧表示活动,权表示活动持续时间。在工程上常用来表示工程进度计划。
8.哈希表
常用哈希函数
1.直接定址法。
2.数字分析法。
3.平方取中法。
4.折叠法。
5.除留余数法。
6.随机数法。
冲突解决
1.开放定址法:当发生冲突时,形成一个探查序列,沿此序列逐个地址探查,知道找到一个空位置,将发生冲突的记录放到该地址中。即Hi=(H(key)+di) % m,i=1,2,……k(k<=m-1),H(key)哈希函数,m哈希表长,di增量序列。
- 线性探测再散列:di = 1,2,3.....,m-1
- 二次探测再散列:di = 1^2, 2^2, 32.....,k2 (k<=m/2)
- 伪随机探测再散列:di为伪随机数序列
2.链地址法:将所有关键字为同义词的记录存储在一个单链表中,并用一维数组存放头指针。
3.设有n个关键字具有相同的Hash函数值,则用线性探测法把这n个关键字映射到Hash表中需要做n(n-1)/2次线性探测。如果使用二次探测再散列法将这n个关键字存入哈希表,至少要进行n(n+1)/2次探测
4.Hash查找效率:装填因子=表中记录数/表容量
5.开哈希表——链地址法;闭哈希表——开放地址法
10.B-树和B+树
B树
B树即B-树,一颗m阶B树是一颗平衡的m路搜索树
特点:
- m即所有节点中孩子节点个数的最大值
- 每个非根节点所包含的关键字个数j满足:ceil(m/2) - 1 <= j <= m - 1
- 节点的子节点数会比关键字个数+1
- 根据以上两条可以得出,每个节点最多有m个分支,非根非叶节点至少有ceil(m/2)个分支
B树的查找
时间复杂度O(logn)
B树的插入
例:用1,2,6,7,11,4,8,13,10,5,17,9,16,20,3,12,14,18,19,15构建5阶B树
因为构建5阶的B树,所以每个节点的关键字个数范围为[2,4]
插入11时,该节点的关键字个数超出范围,进行分裂
之后直接插入4,8,13
当插入10时,节点关键字个数再次超出范围
将子节点分裂
直接插入5,17,9,16,插入20
关键字个数超出范围,进行分裂
继续插入3
关键字个数超出范围,进行分裂
继续插入15
关键个数超出范围,进行分裂
这时候根节点关键字个数也超出范围,继续分裂
B+树
- B+树在磁盘查找更快速,树更加矮胖,更稳定。B-树是不稳定的。
- 每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。所有叶子节点中保存路全部元素的信息,以及指向这些元素记录的指针,且叶子节点本身依关键字的大小自小而大顺序链接。
- 所有的中间节点元素都同时存在于子节点,在子节点元素节点中是最大(或最小)元素。
B+的优点
1.单一节点存储更多的元素,使得查询的IO次数更少。
2.所有查询都要查询叶到叶子节点,查询更加稳定
3.所有叶子节点形成有序链表,便于范围查询。
参考B+,B-,B*树