算法基础知识总结(数据结构)

二、数据结构

1、单链表

1、定义:链表是一种线性表,它不需要用地址连续的存储单元来实现。链表不需要逻辑上相邻的两个数据再物理地址上也相邻,而是通过“链”建立数据之间的关系。

2、优点:

(1)可以不用预先知道数据的个数

(2)可以充分利用计算机内存实现灵活的内存动态管理。

(3)可以在任意位置高效地实现插入和删除操作

3、缺点:

(1)要耗费更多的空间

(2)不方便随机存取,而要每次都遍历

(3)由于节点不是连续存储的,索引一个元素的时间更长

4、作用:可以用来实现栈、队列、邻接表存储图、解决散列查找的冲突问题时用到的拉链法

5、实现方式:结构体、数组

6、时间复杂度:

索引: O ( n ) O(n) O(n)

在头节点处插入/删除: O ( 1 ) O(1) O(1)

在尾结点处插入/删除:知道尾结点时: O ( 1 ) O(1) O(1) ;不知道尾结点时: O ( n ) O(n) O(n)

在中间某个位置 k 插入或删除:搜索时间 O ( k ) + O ( 1 ) O(k) + O(1) O(k)+O(1)

7、代码注意点:

(1)用数组实现效率更高,结构体创建新的节点new( ) 特别慢,当链表节点数量很大时效率不高

(2)head并不是一个节点!它是第一个节点也就是头节点的下标,永远记录的是头节点的索引!!!

(3)“idx” 是用来表示当前用到了第几个节点

(4)千万注意,由于idx是从0开始的,所以要根据题意调整传入删除和插入函数的参数:k, k - 1

(5)注意最后输出单链表时 i 从 h 开始遍历,到 -1 结束

2、双向链表

1、代码注意点:

(1)初始化的时候为了方便让0作为头节点的下标,1作为尾结点的下标

(1)注意最后输出时,i 从头节点开始,遍历到尾结点结束,不要惯性思维写成 -1 !!!

(3)l[r[k]] = idx, r[k] = idx;这一行里第一步不能忘!!新添加的元素左右两遍的元素都向新元素添加链接。当然还要让新元素向它两边的元素添加链接。

3、栈

1、定义:一种后进先出的线性数据结构

2、应用:表达式求值、回溯(DFS)、单调栈

3、实现方式:链表、数组

4、时间复杂度:

push: O ( 1 ) O(1) O(1)

pop: O ( 1 ) O(1) O(1)

5、代码注意点:

(1)tt代表的是”top“

(2)弹出元素后,tt–

4、队列

1、定义:一种先进先出的线性数据结构

2、应用:计算机执行指令时有指令缓冲队列、BFS、滑动窗口

3、实现方式:链表、数组

4、时间复杂度:入队和出队都是 O ( 1 ) O(1) O(1)

5、代码注意点:

(1)普通队列定义时,队尾是-1,对头是0

(2)循环队列定义是,队头队尾都是0,并且每次入队或者出队后都要判断是否已经队满或者队空,若是则返回初始状态。循环队列可以解决队列溢出问题。

(3)循环队列最多装N - 1个元素。模板中hh指向对一个元素,tt指向最后一个元素的后一个位置。

(3)双端队列队头和队尾都可以弹出元素或添加元素。普通队列和栈是双端队列的特例!

6、单调队列之滑动窗口:

给定一个大小为 n≤1000000 的数组。有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。你只能在窗口中看到 k 个数字。每次滑动窗口向右移动一个位置。

以下是一个例子:该数组为 [1 3 -1 -3 5 3 6 7],kk 为 33。

窗口位置最小值最大值
[1 3 -1] -3 5 3 6 7-13
1 [3 -1 -3] 5 3 6 7-33
1 3 [-1 -3 5] 3 6 7-35
1 3 -1 [-3 5 3] 6 7-35
1 3 -1 -3 [5 3 6] 736
1 3 -1 -3 5 [3 6 7]37

你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。

朴素思路:遍历每个窗口的位置然后找出窗口内的最小值和最大值。时间复杂度为 O ( n k ) O(nk) O(nk)

优化思路:可以设置一个队列 q ,它的变化符合单调性。例如在本题中:

①:q = [ ]

②:q = [1]

③:q = [1, 3]

④:q = [-1, 1, 3],达到了窗口的长度,输出最小值和最大值,然后在原始数组上滑动一个单位,[3 -1 -3 5 3 6 7],即把 1 剔除,那么就有

q = [-1, 3],注意这一部分的顺序还是可以保留的。

⑤:q = [-3, -1, 3],达到了窗口的长度,输出最小值和最大值,然后在原始数组上滑动一个单位,[-1 -3 5 3 6 7],即把 3 剔除,那么就有

q = [-3, -1]

⑥:q = [-3, -1, 5]

. . .

大致思路就是如上,我们让队列 q 内的元素单调递增地加入,每个来自原始数组的元素都调整到正确的位置上,每当 q 的长度达到窗口的长度时就输出一次。然后根据原始数组来更新 q。但是在写代码的时候会发现想要删除 q 中的某个指定元素对c++而言有些繁琐,这就是直接操作数值的弊端,有一种思路是操作原始数组的下标,即向 q 添加符合条件的元素的下标即可。

for(int i = 0; i < n; i++){
    while(hh <= tt && i - k + 1 > q[hh]) hh++;
    while(hh <= tt && a[q[tt]] >= a[i]) tt--;
    q[++tt] = i; // queue stores index of a[i] !!!

    // won't output until checked k numbers in a[]
    // the minimum number's index should be q[hh] since now queue is an increasing queue.
    if(i - k + 1 >= 0) printf("%d ", a[q[hh]]); 
}

这段代码的作用是找到窗口内的最小值,根据第四行可以看到我们只顾向 q 中顺次添加原始数组对应的下标即可,但是这个下标在 q 中的位置是有要求的,这个要求就体现在第三行,不断地从 q 中删除那些不满足条件的下标,直到找到了合适的 q 中的位置。

同时还要注意第二行,当遍历到原始数组的第 i 个元素的时候窗口的起始位置应该在 i - k + 1,q 中存储的下标应该始终大于等于这个值,当不满足时就需更新对头。

5、KMP算法

1、作用:字符串匹配

2、核心思想:当不匹配发生时,利用匹配串自身的信息来决定下一次匹配开始的位置,避免重复检查之前已经检查过的字符。

3、关键操作:计算next数组

4、next数组的本质与作用:一个长度为 N N N 的匹配串 P P P 对应了一个同样长度的next数组记作 ne[N]。对于 ne[i] = k它的含义是:在字符串 P P Pp[0] ~ p[k - 1]p[i - k + 1] ~ p[i]的字符都相同,这意味着当第 p[i]不匹配时,重新从 p[0]开始检查到 p[k - 1]的效果和从 p[i - k + 1]检查到 p[i]的效果是等价的,所以更快的做法是直接从 p[k - 1]开始进行下一次匹配。换言之就是要让 i i i 尽可能少向左移动,当然如果遇到了最坏的情况 i i i 就只能退回到0了。

综上所述:next数组就是求出匹配串每个位置相同后缀前缀的长度的最大值。

5、匹配过程:从第一位开始一位一位检查,当不匹配时,假如匹配到了第 i i i 位,则检查 ne[i] = k从第 k + 1 位开始往后检查,重复此过程直到完全匹配。

6、时间复杂度:

(1)构件next数组: O ( k ) O(k) O(k) k为匹配串长度

(2)匹配: O ( n ) O(n) O(n) n n n 为字符串长度

7、代码注意点:

(1)存放字符串时为了方便从下标 1 开始

(2)在构件next数组时有个小技巧是可以从 i = 2开始,采用双指针算法,比较的是 p[j + 1]p[i]

(3)在匹配时应该从 i = 1开始

6、Trie树

1、作用:高效地存储和查找字符串

2、 存储:按先序遍历,遇到没有的字母就增添一个节点,在一个字符串结尾处应当打标记,否则查找时对于中间过程的字符串无法判断是否存在,打标记的方法其实就是再创建一个cnt数组,记录以每个字符串出现的次数。

简单的模拟一下:

5
I a
I ab
I abc
I abcd
Q a
    
ans = 1
cnt = [0 1 1 1 1 0 0 0 0 0]
son[10][10] = [ 
1 0 0 0 0 0 0 0 0 0 
0 2 0 0 0 0 0 0 0 0 
0 0 3 0 0 0 0 0 0 0 
0 0 0 4 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 
]

3、时间复杂度: O ( n ) O(n) O(n) n n n 为待查找的字符串的长度

4、代码注意点:

1、son[N][M]其中N是节点的个数!

2、不要忘了 ++idx

7、并查集

1、作用:合并集合;查找某个元素属于哪个集合

2、提高效率的方法:路径压缩、按秩合并

3、时间复杂度:采用按秩合并、路径压缩后的查找操作为 O ( n ) O(n) O(n)

4、应用:最小生成树的kruskal算法

5、代码注意点:

(1)初始化时把每个节点作为一棵独立的树

8、堆

1、概念(以小根堆为例):堆是一个完全二叉树并且任意一个节点都小于它的左右叶子节点。堆是部分有序的。

2、应用:堆排序、最短路算法中的堆优化dijkstra算法

3、基本操作:

(1)上滤:把小的数往上浮

(2)下滤:把大的数往下沉

4、数组实现:第 n 个节点的左右叶子节点的下标分别为 2n 和 2n + 1。

5、建堆:首先把n个数存入数组,从下标1开始存。其次,从下标 n / 2 开始到下标 1 进行下滤操作。这样才能确保每个节点都满足堆的性质。并且时间复杂度是 O ( N ) O(N) O(N) 由于堆是一个完全二叉树(不一定是完美二叉树),下标为 n / 2 的节点是最后一个有叶子节点的节点。把该节点及其之前的节点都进行一遍下滤操作即可。

6、时间复杂度:

(1)建堆: O ( l o g n ) O(logn) O(logn)

(2)删除最小值: O ( l o g n ) O(logn) O(logn)

(3)查找最小值: O ( 1 ) O(1) O(1)

7、删除堆中的第 K 个数:为了进行这个操作,我们首先需要知道第 K 个点在哪,所以需要用一个数组 ph[ k ] 来存储第 k 个插入的点在堆里面的下标。删除第 k 个数和删除第一个数的思想雷同,就是用最后一个数顶替被删除的位置然后执行下滤操作。这里涉及到堆中两个位置的交换操作。比方说我们要把堆中的第 i 个元素和第 j 个元素交换,现在已知的 i 和 j 都是堆的下标,所以直接 swap(h[i], h[j])就可以。但是,不要忘记我们还需要维护 ph[ N ] 数组,这时我们发现缺少了获得k的链接,也就是说我们还需要知道堆中的第 i 个元素和第 j 个元素分别是第几个插入堆中的。因此我们再创建一个数组 hp[k],它的值表示堆中的第 k 个元素是第几个插入到堆中的。这样就可以维护 ph[N] 数组,swap(ph[hp[i]], ph[hp[j]]),完成了这一步,不要忘了还要再维护新建立的 hp[N] 数组,它也需要交换:swap(hp[i], hp[j])到这里不难发现 ph 和 hp互为逆运算。那么在进行插入操作的时候,如果用idx表示第k个插入的元素,用cnt表示插入第k个元素后堆的大小,就有:ph[idx] = cnt, hp[cnt] = idx!!!

8、代码注意点:

1、在做堆排序时千万注意 down 函数中的数组长度是随着堆的大小更新的,不是常数 n !!!

9、散列查找(哈希表)

1、基本思想:以数据对象的关键字key为自变量,通过一个确定的函数关系 h (哈希函数)计算出对应的函数值 h(key) 作为数据对象的存储地址。

2、解决冲突的方法:开放寻址法、拉链法

3、常用技巧:

(1)通常哈希表的长度 N 是质数开放寻址法的哈希表长度通常是数据数量的两到三倍

(2)拉链法常用的哈希函数是对哈希表的长度求模

4、时间复杂度:

平均的搜索、删除、插入都是 O ( 1 ) O(1) O(1) ,最坏是 O ( n ) O(n) O(n)

5、代码注意点:

(1)拉链法的哈希函数是:(x % N + N) % N这样可以避免出现负数。

(2)用拉链法时不要忘记初始化哈希表。

(3)拉链法和链表有很大的相似性,但是拉链法的头节点应该是头节点数组 h[N]并且总是采用头插法

10、字符串前缀哈希

1、基本思想:对于一个长度为 N 的字符串 S ,建立一个下标从 1 到 N 的哈希表 h。h[i]是 S 的前 i 个字符组成的字符串的哈希值(前缀哈希)。并且该哈希值是字符串对应的一个 p 进制数。

2、例子: h ( A C Z D ) p = ( 1 × p 3 + 3 × p 2 + 26 × p 1 + 4 × p 0 )   m o d   Q h(ACZD)_p = (1 \times p^3 + 3 \times p^2 + 26 \times p^1 + 4 \times p^0) \bmod Q h(ACZD)p=(1×p3+3×p2+26×p1+4×p0)modQ

3、经验值:通常取 p = 131   o r   13331 p = 131~or~13331 p=131 or 13331 , Q = 2 64 Q = 2^{64} Q=264

4、作用:字符串匹配;计算任意一个字串的哈希值

5、代码注意点:

(1)用一个数组 p 来存储它的幂。 p [ i ] = p i p[i] = p^i p[i]=pi ,千万注意要先让 p[0] = 1 !!! 也不能把字母映射成0!

(2)字符串存在字符数组中,从下标 1 开始存储,因为 p 和 h 数组都得从下标 1 开始遍历!

(3)小技巧:用 unsigned long long类型的 p 数组和 h 数组,因为这个类型的位数就是 2 64 2^{64} 264

(4)字符串哈希是在假定了完全不存在冲突的情况下的。当然,按照经验值基本上不会出现冲突。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值