记录学习算法的点点滴滴,来源acwing
主要用数组去模拟数据结构
1.单链表
数组模拟单链表,用两个数组,这两个数组是通过 下标进行相互关联的
- 举一个很形象的例子
这两个数组就是 下标关联起来 的,==图中所有节点 都是通过下标来索引的 ==下标是0的的点
数组模拟单链表,物理是数组的形式,但逻辑上是链表的形式,最关键的就是idx (表示当前用到了那个节点) 节点的编号都是从0开始,与数组下标对应的。
关键就是要知道 逻辑上 是链表 是一个错综复杂的东西,真的比较神奇
N=100010
#模拟单链表中的头节点
head=0
#模拟单链表中节点的val值
e=[0]*N
#模拟单链表中节点的指针值
ne=[0]*N
#idx 存储当前已经用到了哪个点(相当于就是一个动态的数组下标)
#初始化单链表
def init():
head=-1
idx=0
#将x插入头节点
def add_head(x):
e[idx]=x
ne[idx]=head
head=idx
idx+=1
#将x插入到下标是k的点后面
def add(k,t):
e[idx]=t
ne[idx]=ne[k]
ne[k]=idx
idx+=1
#将下标是k的点后面的点删除掉
def remove(k):
ne[k]=ne[ne[k]]
def main():
m=int(input())
init()
while m:
op=input()
if op=="H":
x=int(input())
add_head(x)
elif op=="D":
k=int(input())
#这里需要特判一下k=0表示删除头节点
#删除头节点就是让下一点成为头节点
if not k:
head=ne[head]
remove(k-1)
else:
k,x=map(int,input().split())
add(k-1,x)
i=head
while i!=-1:
print(e[i],end=" ")
i=ne[i]
main()
2.双链表
数组模拟双链表的话其实和模拟单链表的化差不多的
- 双链表的话,其实就是又多增加了一个指针数组
- 单链表只有一个指针数组,ne
- 双链表有两个指针数组,l,r
- 此处的双链标没有新开指针表示头节点,尾节点,而是使用一个下标为0,下标为1表示头节点尾节点
N=10010
e=[0]*N
l=[0]*N
r=[0]*N
idx=0
def init():
#0表示开头,1表示结尾
r[0]=1
l[1]=0
idx=2
#在下表为k的后面加一个节点x
def add(k,x):
e[idx]=x
#双指针 四步
r[idx]=r[k]
l[idx]=k
#注意后面这两步骤不能交换顺序,否则会出现问题
l[r[k]]=idx
r[k]=idx
#删除第k个点,只有两个指针操作
def remove():
#类似于单链表
r[l[k]]=r[k]
l[r[k]]=l[k]
- 插入点的示意图:插入操作只有一种就可以了,在节点的左边插入可以等价转换为在节点的右边插入 add(l[k],x)
- 删除节点k的示意图
注意:用数组进行模拟数据结构的话,比用结构体模拟数据结构好用多了,而且代码会段很多
3.栈
- 在python中的list本身就可以相当于是一个栈,具有先进后出的特性
#插入操作
.append()
#弹出操作
.pop()
#获取栈顶元素
num[-1]
#是否为空
return len(num)==0
- 如果用数组来模拟栈的话,也挺好实现的
N=100010
st=[0]*N
tt=0 #指针
#插入操作(下标从1开始)
st[++tt]=x
#弹出
tt-=1
#判断栈是否为空
return tt==0
#获取栈顶元素
st[tt]
4.队列
- 在队尾插入元素,在对头弹出元素
- 所以需要两个指针
- 指针初始化0还是-1,看自己习惯,这个没有太大的关系
#在队尾插入元素,在对头弹出元素
q=[0]*N
hh=0 #队头弹出
tt=-1 #队尾插入元素
#插入
q[++tt]=x
#弹出
hh+=1
#判断队列是否为空
return hh>tt
#取出队头元素
q[hh]
q[tt]
#还可以获取队列长度
lens=tt-hh+1
5.单调栈
- 应用不多,给出一个序列,让你给出每一个数的左边离它最近的数
- 基本模型就是这样,感觉和双指针的优化有点类似,都是找到某一些性质(单调)来进行优化
- 都是从暴力出发,然后挖掘是否能找到某些性质,减少状态数量
确实,通过挖掘一些信息,可以发现栈里面存在一种单调关系,即栈里面不存在逆序关系
N=100010
st=[0]*N
tt=0
def main():
n=int(input())
for i in range(n):
x=int(input())
while tt and x<=st[tt]:
tt-=1
if tt:
print(st[tt])
else:
print(-1)
#最后一步枚举到最后一个元素,需要进行入栈操作
tt+=1
st[tt]=x
5.单调队列
这种题目的应用背景也是属于滑动窗口求取最大值最小值的应用,还是挺好的
- 把这个单调队列模型抽象出来,还是有一定的难度的
- 单调队列和单调栈思考方式一样,先思考一下暴力的方式,
从图中的例子可以看出,在圈出来的滑动窗口中,只要-3存在,3和-1 永远都不会作为答案输出,这一点和单调队列非常类似
- 只要队列里面存在这样的情况()逆序的过程,就可以把大的删除掉,删掉之后就是一个严格单调上升的队列
- 那么最小值的话,就是q[hh]
- 那么最大值的话,就是q[tt]
- 找一个值的话,可以进行二分操作
- 思路:
先考虑暴力怎么左,然后考虑把没有用的元素删掉,再看有没有单调性,如果有看如何去优化它
注意:多重背包也可以进行滑动窗口优化
问题:如何知道队首元素什么时候出队
i 是右端点, k 是区间长度, i − k + 1 所以要保证队首元素在 i − k + 1 − i 之间,如果不在,则出队 i是右端点,k是区间长度,i-k+1 所以要保证队首元素在 i-k+1-i之间 ,如果不在,则出队 i是右端点,k是区间长度,i−k+1所以要保证队首元素在i−k+1−i之间,如果不在,则出队
- 还有一点的是,这个单调队列存储的是下标
N=1000010
n=0
q=[0]*N
def main():
n,k=map(int,input().split())
a=list(map(int,input().split()))
hh,tt=0,-1
lens=len(a)
for i in lens:
#判断队头是否滑出窗口每次 最多一次,所以不用while 用if就可以
if hh<=tt and i-k+1>q[hh]:
hh+=1
#如果队列非空并且 当前元素与队尾元素相比的话,还更小,那么队尾元素就可以删除
while hh<=tt and a[q[tt]]>=a[i]:
tt-=1
#进队列
#需要先加入当前这个数,因为当前这个数可能足够小,可以把队列滑动窗口清空
tt+=1
q[tt]=i
if i>=k-1:
print("%d"%a[q[hh]])
- 当然求取滑动窗口的最大值,就是对称的写法,很类似
6.KMP算法
算法思路:和一般大单调队列和双指针算法类似,先思考暴力怎么做,如何去优化它
- 暴力做法bp
def bp():
#s匹配串
#p模式串
s=input()
p=input()
lens_s=len(s)
lens_p=len(p)
for i in range(lens_p):
k=0
flag=1
while k<lens_s:
if s[k]==p[i+k]:
pass
else:
flag=0
break
if flag:
break
- kmp算法 就是充分利用了bp暴力的信息,减少匹配次数,就是利用前缀和后缀的思想,直接从后缀开始匹配,相等的值越大,往后移动的距离越短
- 这个指针的移动也是逻辑上的,根据我们的逻辑而直观地展示出来地一种方法,最小移动距离是逻辑上的
- 预处理地next的数组的含义是
N=10010,M=100010
next=[0]*N
def main():
p=input()# p是模板串
s=input()# s是模式串
# 模板串p在模式串s多次作为字串出现
# 求出模板穿 在 模式串 s中 所有出现位置的起始下标
# n是子串的长度 m是
n,m=map(int,input())
#预处理
p="0"+p
s="0"+s
#求取next过程
i,j=2,0
while i<=n:
while j and p[i]!=p[j+1]:
j=next[j]
if p[i]=p[j+1]:
j+=1
next[i]=j
#kmp匹配过程
i,j=1,0
while i<=m:
#循环结束有两种条件:,一种是退无可退,另外一种是匹配上了
#所以要进行判断循环结束的条件
#退无可退的话,就重新开始
while j and s[i]!=p[j+1]:
j=next[j]
if s[i]==p[j+1]:
j+=1
if j==n:#匹配成功
# 因为下标从0开始计数,所以i-n+1 还要减1
print("%d"%(i-n))
j=next[j]
i+=1
- 案例:自己动手推导一下,还是很有帮助的
时间复杂度:O(2m)的时间复杂度,在循环中,j最多加到m,所以内层j最多移动m次,总的次数就是2m 就是和双指针算法类似,先暴力算法,然后挖掘一些性质,但是这个性质真的不好挖掘,太难了
7.Trie
Trie:高效地存储和查找字符串集合的数据结构
- 还是物理是二维数组的形式,但是逻辑上是树的形式,不要想物理结构,就想成是逻辑结构**树** ,不那么抽象
- 就想成是一个有很多节点的树,每个节点都有一个idx,这个就是二维数组的值
- 和单链表中的idx非常类似,也是标记每一个节点,只不过单链表中这个标记是通过数组下标来标记
- 这个trie的idx不是二维数组下标,而是数组的值
例题如下:
分析:字符串长度为1e5,所以看成树的节点的个数最多是1e5,考虑到树的特性,每个节点又有儿子节点,而这道题目,每个节点最多只有26个儿子节点,所以开一个二维数组
- 注意二维数组不仅仅是一个二维数组,而是存在一个逻辑上的映射关系,树的映射
N=100010
#son
son=[[0]*26 for i in range(N)]
cnt=[0]*N
#下标是0的点,既是根节点,也是空节点
inx=0 #指向当前那个节点
def insert(str):
p=0#根节点
for i in str:
#求得子节点所对应的编号
u=ord(i)-ord("a")
if not son[p][u]:
#如果没有被创建,则创建出来
idx+=1
son[p][u]=idx
#从根节点走到下一节点
p=son[p][u]
#以idx结尾的肯定是唯一的,还是很好理解的 表示以这个点结尾的单词的数量
cnt[p]+=1
def query(str):
p=0
for i in str:
u=ord(i)-ord("a")
if not son[p][u]:
return 0
p=son[p][u]
return cnt[p]
def main():
n=int(input())
while n:
op=input()
strs=input()
if op[0]=='I':
insert(strs)
else:
print("%d"%(query(strs)))
n-=1
return 0
main()
加深理解的最好方法,还是要手动模拟一遍,根据案例模拟一下
8.并查集
- 将两个集合合并
- 询问两个元素是否在一个集合当中
==基本原理:==每个集合用一棵树来表示。树根的编号就是整个集合的编号。每个节点存储的是它的负节点,p[x]表示它的父节点
这里用到了一维数组。一维数组表示的不仅仅是一维数组,而是表示成很多颗树,每个节点都是用数组的下标表示的,节点里面存储的值是它的父节点(说白了也是==数组下标)
- 这个有点类似于单链表模拟的时候,的ne[N]数组,有很大的相似性
N=100010
n,m=0,0
p=[0]*N
def find(x):
if p[x]!=x:
return p[x]
def main():
n,m=map(int,input().split())
for i in range(1,n+1):
p[i]=i
op=input()
a,b=map(int,input().split())
if op[0]=="M":
p[find(a)]=find(b)
else:
if find(a)==find(b):
print("Yes")
else:
print("NO")
return 0
main()
- 这个就是最基本的并查集操作,一个find函数用到了状态压缩,因为在回溯的时候,就相当于是指针,进行了相关的链接
并查集的扩展题目
这个相当于最开始的并查集就相当于是增加了一个额外的操作,要求维护每个联通块中点的数量信息
- 这个题目 就是要求保证 根节点的size是有意义的,其它的没有意义
N=100010
p=[0]*N
size=[0]*N
def find(x):
if p[x]!=x:
p[x]=find(p[x])
return p[x]
def main():
n,m=map(int,input().split())
for i in range(1,n+1):
p[i]=1
size[i]=1
while m:
op=input()
if op[0]=="C":
a,b=map(int.input().split())
if find(a)==find(b):
#表明属于同一集合
continue
size[find(b)]+=size[find(a)]
p[find(a)]=find(b)
elif op[1]=="1":
a,b=map(int,input().split())
if find(a)==find(b):
print("Yes")
else:
print("No")
else:
a=int(input())
print("%d"%(size[find(a)]))
main()
- 做并查集的时候,可以维护每个集合的数量
- 当然并查集还可以维护很多东西,具体的根据具体的题目具体分析
9.堆
如何手写一个堆?
- 插入一个数 heap[++size],up(size) 时间复杂度是lgn
- 求集合中的最小值 heap[1] ==时间复杂度O(1)
- 删除最小值 这个删除用了一个很妙思想,数组输出元素很难实现,曲线救国,最后一个元素覆盖第一个元素,然后删除最后一个元素这样就能实现删除最小值 heap[1]=heap[size] size-- down(1) 时间复杂度lgn
- 删除任意一个元素 这个也是和删除最小值的思想一样的
- 修改任意一个元素 heap[k]=x up(k),down(k)
- 堆和前面的单链表,trie,并查集这些数据结构不太一样,堆是一种用数组模拟的全新的数据结构
- 堆有两个操作:一个是down操作,很明显就是上面的元素往下成,还有一个操作就是up操作就是下面的操作往上走
- 用堆的down和up操作就可以完成手写一个堆
- 注意:一维数组下标要从1开始,这样就可以避免 出现一些问题
一维数组模拟 堆 每一个节点的 用数组下标表示 而且这个数组下标 又有 练习
建堆的两种方式
- 通过n次插入操作可以进行建堆
- 还有一种O(n)的时间进行建堆
中间是等比等差(错位相减)的形式,可以证明<1,所以时间复杂度是小于O(n)的
我们进行思考的时候,一定不要想成数组的形式,一定要思考成树的形式
# 手写一个堆,实现堆排序
N=100010
n,m=0,0
size=0
#down函数的核心操作就是比较三个数的大小
def down(u):
#t存储的是最小的下标
t=u
if u*2<=size and h[u*2]<h[t]:
t=u*2
if u*2+1<=size and h[u*2+1]<h[t] t=u*2+1
if u!=t:
swap(h[u],h[t])
down(t)
def up(u):
while u//2 and h[u//2]>h[u]:
swap(h[u//2],h[u])
u//=2
def main():
n,m=map(int,input().split())
h=list(map(int,input().split))
h.insert(0,0)
h.append(0)
size=n
i=n//2
while i:
down(i)
i-=1
while m:
print("%d"%(h[1]))
h[1]=h[size]
size-=1
down(1)
m-=1
上述是 手写一个堆,但是python中也有对应的库函数
#对应插入操作
from heapq import heappush
#建堆
from heapq import heapify
#删除最小值并返回 最小的元素
from heapq import heappop
10.哈希表
10.1存储结构
10.2字符串哈希方式
11.trie树例题
题目描述
首先思考一下暴力做法,很明显双重循环
res=0
for i in range(0,n):
for j in range