蓝桥杯数据结构算法

记录学习算法的点点滴滴,来源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是区间长度,ik+1所以要保证队首元素在ik+1i之间,如果不在,则出队

  • 还有一点的是,这个单调队列存储的是下标
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算法

算法思路:和一般大单调队列和双指针算法类似,先思考暴力怎么做,如何去优化它

  1. 暴力做法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
        
  1. 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.并查集

  1. 将两个集合合并
  2. 询问两个元素是否在一个集合当中

==基本原理:==每个集合用一棵树来表示。树根的编号就是整个集合的编号。每个节点存储的是它的负节点,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.堆

如何手写一个堆?

  1. 插入一个数 heap[++size],up(size) 时间复杂度是lgn
  2. 求集合中的最小值 heap[1] ==时间复杂度O(1)
  3. 删除最小值 这个删除用了一个很妙思想,数组输出元素很难实现,曲线救国,最后一个元素覆盖第一个元素,然后删除最后一个元素这样就能实现删除最小值 heap[1]=heap[size] size-- down(1) 时间复杂度lgn
  4. 删除任意一个元素 这个也是和删除最小值的思想一样的
  5. 修改任意一个元素 heap[k]=x up(k),down(k)
  • 堆和前面的单链表,trie,并查集这些数据结构不太一样,堆是一种用数组模拟的全新的数据结构
  • 堆有两个操作:一个是down操作,很明显就是上面的元素往下成,还有一个操作就是up操作就是下面的操作往上走
  • 用堆的down和up操作就可以完成手写一个堆
  • 注意:一维数组下标要从1开始,这样就可以避免 出现一些问题

在这里插入图片描述

一维数组模拟 堆 每一个节点的 用数组下标表示 而且这个数组下标 又有 练习

建堆的两种方式

  1. 通过n次插入操作可以进行建堆
  2. 还有一种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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
蓝桥杯竞赛是中国著名的计算机竞赛之一,C语言算法是其中非常重要的一部分。 在蓝桥杯中,C语言算法主要包括数据结构、排序算法、查找算法、图算法等内容。通过这些算法的学习和掌握,可以提高程序设计的效率和性能。 首先,数据结构是C语言算法的基础,它指的是建立存储和组织数据的方式。常见的数据结构包括数组、链表、栈、队列、树、图等。对各种数据结构的理解和运用,可以使程序更加高效、方便地操作数据。 其次,排序算法是C语言算法中的重要内容。排序是将一组无序的数据按照一定规则重新排列的操作,常见的排序算法有冒泡排序、选择排序、插入排序、归并排序、快速排序等。掌握各种排序算法的特点和应用场景,可以有效提高程序的效率和响应速度。 此外,查找算法也是C语言算法中的重要部分。查找是在一组数据中寻找指定元素的过程,常用的查找算法有顺序查找、二分查找、哈希查找等。通过合适的查找算法,可以减少对数据的遍历次数,提高查找效率。 最后,图算法也是C语言算法的重点内容之一。图是由节点和边构成的一种数据结构,图算法主要解决图相关的问题,如最短路径问题、最小生成树问题等。熟练掌握图的表示方法和相关的算法,能够更好地解决实际问题。 总之,蓝桥杯竞赛中的C语言算法包括数据结构、排序算法、查找算法、图算法等,通过学习和掌握这些算法,可以提高程序设计的效率和性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值