1.图
1.1图的数据结构
- 边列表结构
顶点储存在一个无序的列表V中,所有边储存在一个无序的列表E中
不足:为了找到顶点的入射边,需要研究E中的所有边 - 邻接列表结构
通过将图形的边存储在较小的位置来对其进行分组,从而和每个单独顶点相关联的次机容器集合器来(有向图的情况下,输入边和输出边分别存储在不同的集合中)
不足:get_edge(u,v): O(min(deg(u), deg(v))) - 邻接图结构
让每个入射边的相反的端点作为图的主键,用边结构作为值。本质上对所有方法实现了最优的运行时间
get_edge(u, v):预期为O(1) - 邻接矩阵结构
二维数组,很简单,优势是get_edge(u, v):O(1)
1.2有向图和无向图的DFS和BFS算法
1.2.1 DFS
Algorithm DFS(G, u):
Input: A graph G and a vertex u of G
Output: A collection of vertices reachable from u, with their discovery edges
for each outgoing edge e=(u,v) of u do:
if vertex v has not been visited then:
Mark vertex v as visited(via edge e)
Recursively call DFS(G, v)
发现新顶点的边叫做树边或发现边,其他的边叫做非树边,有三种可能的非树边:
back边:连通了顶点和DFS树的祖先
forward边:连通了顶点和DFS树的孩子
cross边:连通了顶点和另一个既不是祖先和孩子的顶点
1.2.2 BFS
Algorithm(G, u):
Input:a graph of G and a vertex u of G
Output: A collection of vertices reachable from u with their discovery edges
1:初始化一个队列并将u放入队列
2:while len(queue) != 0 do:
3: 访问头节点并标记
4: 出队 pop_front()
5: for each outgoing edge e=(u, v) of u do:
6: if vertex v 没有被标记 then:
7: 将v放入队列
8: end if
9: end for
10:end while
\对于无向图:所有非树边都是cross边
对于有向图:所有非树边都是back或cross边
1.3 拓扑排序
G有拓扑排序当且仅当他是非循环的
def topological_sort(g):
topo = []
ready = []
incount = {}
for u in g.vertices():
incount[u] = g.degree(u, False) #入度
if incoutn[u] == 0:
ready.append(u)
while len(ready) > 0:
u = ready.pop()
topo.append(u)
for e in g.incident_edges(u):
v = e.opposite(u)
incount[v] -= 1
if incount[v] == 0:
ready.append(v)
return topo
\
1.4最短路径 Dijkstra算法
核心是边的逐次近似:
if D[u] + w(u,v) < D[v] then
D[v] = D[u] +w(u, v)
Algorithm ShortestPath(G, s):
Input: A weighted graph G with nonnegative edge weights and a distinguished vertex s of G
Output: The length of a shortest path from s to v for each vertex v of G
Initialize D[s] = 0 and D[v] = ∞ for each vertex v != s
Let a priority queue Q contain all the vertices of G using the D labels as keys
while Q is not empty do:
u = value returned by Q.remove_min()
for each vertex v adjacent to u such that v is in Q do:
if D[u] + w(u,v) < D[v] then
D[v] = D[u] +w(u, v)
CHange to D[v] the key of vertex v in Q
return the label D[v] of each vertex v
2.排序算法
2.1分治相关的排序
分治法设计模式:
1.分解:如果输入值的规格小于确定的阈值(比如一个或者两个元素),我们就通过直截了当的方法来解决这些问题并返回所获得的答案,否则我们把输入值分解成两个多更多的互斥子集。
2.解决子问题:递归地解决与子集相关的子问题。
3.合并:整理这些子问题的解,然后把他们合并成一个整体用以解决最开始的问题
2.1.1归并排序
可以用一个二叉树T来形象化一个归并排序算法的执行过程,称这个二叉树为归并排序树
在大小为n的序列上执行归并排序,归并二叉树的高度近似Logn
# 归一化
Merge_sort(sourceArr, tempArr, sIndex, eIndex):
if sIndex < eIndex:
mid = sIndex + (eIndex - sIndex)/2
Merge_sort(sourceArr, tempArr, sIndex, mid)
Merge_sort(sourceArr, tempArr, mid+1, eIndex)
Merge(sourceArr, tempArr, sIndex, mid, eIndex)
#合并化
Merge(sourceArr, tempArr, sIndex, midIndex, eIndex):
i = sIndex
j = midIndex + 1
k = sIndex
#取出两个有序子序列中最小的一个元素,放入新的有序数列中
while i != midIndex + 1 and j != eIndex + 1:
if sourceArr[i] < sourceArr[j]:
tempArr[k] = sourceArr[i]
i++
else:
tempArr[k] = sourceArr[j]
j++
k++
#前面的有序数组还有元素
while i != midIndex + 1:
tempArr[k++] = sourceArr[i++]
#后面的有序数组还有元素
while j != eIndex + 1:
tempArr[k++] = sourceArr[j++]
#将有序数组拷贝给原数组
for m = sIndex to eIndex:
sourceArr[m] = tempArr[m]
排序消耗的时间为O(nlogn)
2.1.2 快速排序
步骤描述见书P357
在大数据集上能取得好的效果,但是在小数据集上有着更大的开销
#快速排序:以最后一个元素为基准值
#分治
Quick_sort(A, p, r):
if p < r:
q = Partition(A, p, r)
Quick_sort(A, p,q-1)
Quick_sort(A, q+1, r)
#合并
Partition(A, p, r):
x = A[r] #最后一个元素为基准值
#i 用来维护小于x的数字 j用来维护大于x的数字
i = p-1
for j = p to r-1:
if A[j] <= x:
i += 1
exchange A[i] with A[j]
exchange A[i+1] with A[r]
return i+1
最坏情况下运行时间为O(n^2),这一最坏情况行为在排序很容易完成的时候就会发生,即在序列已经有序的时候。
接下来介绍随机快速排序,它能达到期望运行时间为O(nlogn)
主要思想是让基准值尽量接近元素的中间。
简单提一句就地算法:就是它除了原始所需的内存外,仅仅使用少量的内存,这就是就地算法
基准值选取策略:在实践中,我们另一个选择基准值的常用方法是取数组头中尾的值的中位数(启发式)
Randomized_Quick_sort(A, p, r):
if p < r:
q = Randomized_Partition(A, p, r)
Randomized_Quick_sort(A, p, q-1)
Randomized_Quick_sort(A, q+1, r)
Randomized_Partition(A, p, r):
i = Random(p, r)
exchange A[i] with A[r]
return Partition(A, p, r)
2.1.3 堆排序
命题:任何基于比较的排序算法对于n个元素的序列所花费的时间都是Ω(nlogn)
2.2线性时间排序
2.2.1 桶排序
- 将待排序的序列分到若干个桶中,每个桶内的元素再进行个别排序。
- 时间复杂度最好可能是线性O(n),桶排序不是基于比较的排序
参考:桶排序
总结来说:当键的值的范围N与序列大小n相比很小时,桶排序是高效的,但是N相比于n开始增大时,性能降低。
Algorithm bucketSort(S):
Input:具有整数键[0, N-1]的序列条目S
Output:按照键非递减次序排序的序列S
设置N个序列B,一开始为空
for e in S do
k = the key of e
从S中移除e并且将e插入桶B[k]的末端
for i = 0 to N-1 do
for e in B[i] do
从B[i]中删除e并插入序列S的末端
稳定排序:对于S中任意两个条目(ki, vi), (kj, vj),ki = kj, 排序前和排序后相对位置不变
基数排序:很简单,通俗来说就是为了保持稳定性,每一项都进行稳定排序,得到的结果很自然。如先按第二项稳定排序再对第一项,得到的结果就是第一项相等的情况下,第二项递增。
2.3其他排序
2.3.1 插入排序
insertion_sort(A):
for k = 1 to A.length:
{
cur = A[k]
j = k
while j > 0 and A[j - 1] > cur:
{
A[j] = A[j-1]
j -= 1
}
A[j] = cur
}
2.3.2 选择排序
2.3.3 堆排序
3.数组
3.1 引用数组
定义:在最底层,存储的是一块连续的内存地址序列,这些地址指向一组元素序列
3.2 紧凑数组
定义:存储的是原始数据且原始数据在内存中连续存放
3.3 动态数组和摊销
定义:允许添加元素,且对总体大小没有明显的限制
增减规则:当数组已满时,创建新数组是原数组的两倍,并将原数组元素存入;当实际元素个数小于数组大小的1/4,创建新数组是原数组的1/2,并将原数组元素存入。
摊销:通过增加某些操作的投入,来减少其他操作所需的代价,来达到整体平均的目的
分析append()时间复杂度:设动态数组从k增大到2k需要k个硬币,而我们将每个操作索取三个硬币,对不需要扩大数组的增添操作多付了两枚,我们将多收的两枚视为存入,则从k/2增长到k的过程中预留了k个硬币,正好供给我们进行从旧数组到新数组复制所需的k个硬币,综上,我们进行了k/2次append()共花费3k/2枚硬币,即每个append()操作的时间复杂度为O(1)
例题:在动态数组中调用append()时,增幅由100%调整为25%,能否证明append()的复杂度为O(1)
设原数组为k,增幅25%,则新数组为1.25k,设一次append()存储n枚硬币,增添元素消耗一枚,每次增幅后每个存储一枚
则:0.25k(n-1) = 1.25k
解得:n=6
即O(6n) 所以每次操作为O(1)
4.双向链表
头哨兵(header)和尾哨兵(tailer):占用极小的空间极大地简化操作地逻辑
#管理双向链表的基本类
class _DoublyLinkedBase:
class _Node:
__slots__ = '_element', '_prev', '_next'
def __init__(self, element, prev, next):
self._element = element
self._prev = prev
self._next = next
def __init__(self):
self._header = self._Node(None, None, None)
self._tailer = self._Node(None, None, None)
self._header._next = self._tailer
self._tailer._prev = self._header
self._size = 0
def __len__(self):
return self._size
def is_empty(self):
return self._size == 0
def _insert_between(self, e, predecessor, successor):
newest = self._Node(e, predecessor, successor)
predecessor._next = newest
successor._prev = newest
self._size += 1
return newest
def _delete_node(self, node):
predecessor = node._prev
successor = node._next
precessor._next = successor
successor._prev = precessor
self._size -= 1
element = node._element
node._prev = node._next = node._element = None
return element
5.哈希表与哈希函数
5.1 哈希函数
评价哈希函数h(k)常见的方法由两部分组成:一个哈希码,将一个键映射到一个整数;一个压缩函数,将哈希码映射到一个桶数组的索引,这个索引是范围在区间[0, N-1]的一个整数
这样设计的优势:哈希码的计算部分独立于具体的哈希表大小。这样就可以为每个对象开发一个通用的哈希码,并且可以用于任何大小的哈希表,只有压缩函数与表的大小有关。
#哈希函数python实现
from maps import MapBase #Map ADT
import random
import math
import binascii #二进制转换和ASCII编码的二进制表示转换的方法
class HashTableBase(MapBase.MapBase):
__slots__ = '_table'
def __init__(self):
self._table = []
#将位作为整数处理
def _hash_decimal(self, string):
temp = bytes(string,encoding='utg-8')#字符转化位bytes类型
return int(binascii.hexlify(temp), 16)#将bytes类型转化为16进制
#循环移位哈希码
def _hash_shifting(self, string):
mask = (1<<32) - 1 #限制为32位整数,x<<n = x*(2)^n
h = 0
for character in s:
h = (h<<5 & mask) | (h>>27)
h += ord(character) #add in value of next character
return h
#多项式哈希码
def _hash_poly(self, string):
a = 33 #a取33,37,39,41时每个用例中产生的冲突少于七个
n = len(string) - 1
result = 0
for c in str(string):
temp = ord(c)
result += temp*a**n
n -= 1
return result
#定义压缩函数
#划分方法
def _compress_division(self, hashcode, n):
return hashcode % n
#MAD方法
def _compress_mad(self, hashcode, n):
primes = [i for i in range(n, 5*n) if self.isPrime(i)]
p = random.choice(primes)
a = random.randint(1, p-1)
b = random.randint(1, p-1)
return ((hashcode * a + b) % p) % n
def isPrime(self, n):
if n <= 1:
return False
for i in 5$%range(2,int(math.sqrt(n)) + 1):
if n % i == 0:
return False
return True
5.2 冲突处理方案
分离链表:
使每个桶A[j]存储其自身的二级容器,容器存储元组(k, v),如h(k) = j 负载因子lambda < 0.9
开放寻址:
我们采用将每个元组直接存储到一个小的列表插槽中作为代替的方法,节省空间 负载因子lambda < 0.5(python中为2/3 )
线性探测及其变种:
线性探测:
是使用开放寻址处理冲突的一个简单方法是线性探测。使用这种方法时,如果我们想要将一个元组(k, v)插入桶A[j]的位置,在这里j = h(k),但是A[j]被占用,那么我们将尝试使用A[(j+i) mod N],以此重复操作。对于删除操作,我们不能简单地从插槽中移除,因为如果简单移除,随后搜寻原来插入的位置会失败(该位置在删除位置之后),一个典型办法是用一个带标记的特殊对象来代替被删除的对象。
二次探测:
反复探测A[(h(k)+f(i)) mod N] 其中f(i) = i^2,它可以避免在线性探测中发生的聚集模式,而且还创建了自己的聚集方法:二次聚集。当N是素数且桶数组填充了不到一半时,二次探测保证可以找到空闲位置。但是当N不是素数且桶数组填充超过一半时,二次探测无法保证找到空闲位置。
双哈希策略:
一种不会引起线性探测和二次探测所引起的聚集问题的策略,迭代探测桶A[(h(k) + f(i)) mod N] f(i) = i * h’(k), h’(k) 为二次哈希函数。
另一种避免聚集的开放寻址是迭代地探测桶A[(h(k) + f(i)) mod N],f(i)是一个基于伪随机数产生器的函数
6.堆
这个数据结构允许我们以对数时间复杂度来实现插入和删除操作
6.1 堆的数据结构
Heap-Order 属性:
在堆T中,对于除了根的每个位置p,存储在p中的键值大于或等于存储在p的父节点的键值,一个最小的键值永远存储在根节点中
堆必须是完全二叉树(高度小,效率高)
6.2 堆的插入
我们考虑在堆T实现的优先级队列上实现add(k,v)方法,我们把键值对(k,v)作为元组存储在树的新节点中。存储在二叉树的位置应该维持完全二叉树属性
插入元组后堆向上冒泡
def _swap(self, i, j):
self._data[i], self._data[j] = self._data[j], self._data[i]
def _upheap(self):
parent = self._parent(j)
if j > 0 and self._data[j] < self._data[parent]:
self._swap(j, parent)
self._upheap(paren)
6.3 自底向上构建堆
为了使叙述简单,我们假设键的数量为n,并且n为整数,n = 2^(h+1) - 1,也就是说,堆是一个每层都满的完全二叉树,自底向上构建包含以下h+1 = log(n+1)个步骤
第一步:我们构建(n+1)/2个基本堆,每个堆中仅存储一个元组。
第二步:我们通过将基本堆连接起来并增加一个新元组来构建(n+1)/4个堆,这种堆的每个堆中存储了三个元组,新增的元组放在根部,为保持属性,可能向下冒泡
以下步骤以此类推。
def __init__(self, contents=()):
self._data = [self._Item(k, v) for k,v in contents]
if len(self._data) > 1:
self._heapify()
def _heapify(self):
start = self._parent(len(self)-1)
for j in range(start, -1, -1):
self._downheap(j)
def _downheap(self, j):
'''
堆向下冒泡,如果不满足堆属性,选择较小的那个孩子并交换,迭代。
'''
7 搜索树
7.1 遍历二叉搜索树
遍历为中序遍历
#搜索树中序遍历
#复习一下
#1.先序遍历:首先访问树的根,然后递归地访问子树的根
Algorithm preorder(T, p):
perform the 'visit' action for position p
for each child c in T.children(p) do:
preorder(T, c)
2.后序遍历
Algorithm postorder(T, p):
for each child c in T.children(p) do:
postorder(T, c)
perform the 'visit' action for position p
3.中序遍历
Algorithm inorder(p):
if p has a left child lc then
inorder(lc)
perform the 'visit' action for position p
if p has a right child rc then
inorder(rc)
二叉树的搜索操作
#二叉树搜索的递归调用
Algorithm TreeSearch(T, p, k):
if k = p.key() then:
return p
if k > p.key() then:
return TreeSearch(T, T.right(p), k)
if k < p.key() then:
return TreeSearch(T, T.left(p), k)
return p #unsucessful search
二叉搜索树的插入操作
#插入和删除
Algorithm TreeInsert(T, k, v):
Input:A search key to be associated with value v
p = TreeSearch(T, T.root(), k)
if k = p.key() then:
Set p's value to v
else if k < p.key() then:
add node with item(k, v) as left child of p
else:
add node with item(k, v) as right child of p
7.2 AVL树
定义:任何满足高度平衡属性的二叉搜索树T被称为AVL树
高度平衡属性:对于T中每一个位置p,p的孩子的高度最多相差1
插入操作:插入后可能会导致树的不平衡,我们用z表示从插入节点p到根T的方向中第一个遇到的不平衡的节点,用y来表示z的具有更高高度的孩子(注意,y必须是p的祖先),最后假设x是y具有更高高度的孩子(不能有并列,且x必须是p的祖先或者p本身),然后进行旋转操作即可
AVL树执行操作的效率:
iter(T),reversed(T)为O(n), T.find range(start, stop)为O(s + logn),其余均为O(logn)
7.3 红黑树
具有以下属性:
根属性:根节点是黑色的
红色属性:红色节点(如果有的话)的子节点是黑色的
深度属性:具有零个或一个子节点的所有节点都具有相同的黑色深度
红黑树插入元素的操作:
解决双红色问题:
情况1:y的兄弟姐妹是黑色(或无)
情况2:y的兄弟姐妹是红色:重新着色