数据结构
- 1. 单链表模板
- 1. 单链表(7分钟)
- 2. 双链表模板
- 1. 双链表
- 3. 模拟栈
- 1. 模拟栈(一个数组即可)
- 2. 表达式求值(20分钟)
- 4. 队列 tt = -1,hh = 0;
- 1. 模拟队列
- 5. 单调栈
- 1. 单调栈(4分钟)3.14
- 6. 单调队列
- 1. 滑动窗口例题(10分钟)
- 7. KMP
- 1. KMP字符串(10分钟)
- 二刷体会
- ★三刷体会 ne表示算上第一个和最后一个的前缀后缀相等值
- 8. Trie树
- 1. Trie字符串统计(20分钟)
- 二刷总结
- ★2. 最大异或对
- ★二刷总结
- 9. 并查集 find merge
- 1. 合并集合(5分钟)
- 2. 连通块中点的数量(每个集合有多少个元素)
- ★3. 食物链
- 法一: x,x+n,x+n+n merge(f[x+n],f[x])
- 二刷总结
- 法二:将有关系的都存储在一个部落,用到根节点的距离表示关系
- 二刷总结
- find中 d[x] += d[p[x]];
- 10. 堆 ✔ ✔
- 模板
- 1. 堆排序 ✔ ✔
- 2. 模拟堆 ✔ ✔
- 11. 哈希表 ✔ ✔
- 模板
- 例1. 模拟散列表 ✔ ✔
- 拉链法代码(链表)
- 开放寻址法代码(有空位就存,空位用null=0x3f3f3f3f)
- ★例2. 字符串哈希 ✔ ✔
- 二刷思路
1. 单链表模板
1. 单链表(7分钟)
原题链接
import java.util.*;
public class Main {
static int[] e = new int[100010];
static int[] ne = new int[100010];
static int idx,head;
public static void init() {
idx = 0;
head = -1;
}
public static void add_head(int x) {
e[idx] = x;
ne[idx] = head;
head = idx++;
}
public static void add(int k,int x) {
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx++;
}
public static void remove(int k) {
ne[k] = ne[ne[k]];
}
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int m = scan.nextInt();
init();
while(m -- > 0){
String s = scan.next();
char op = s.charAt(0);
if(op == 'H'){
int x = scan.nextInt();
add_head(x);
}else if(op == 'D'){
int k = scan.nextInt();
if(k == 0) head = ne[head];
else remove(k-1);
}else {
int k = scan.nextInt();
int x = scan.nextInt();
add(k-1,x);
}
}
for(int i = head;i != -1;i = ne[i] ){
System.out.print(e[i] + " ");
}
}
}
2. 双链表模板
void init()
{
r[0] = 1;
l[1] = 0;
idx = 2;
}
1. 双链表
原题链接
import java.util.Scanner;
public class Main{
static int N = 100010;
static int[] e = new int[N];
static int[] r = new int[N];
static int[] l = new int[N];
static int idx;
//删除第k位插入的数
public static void remove(int k){
r[l[k]] = r[k];
l[r[k]] = l[k];
}
//这是在第k位数后面插入一个数x
public static void add_all(int k,int x){
e[idx] = x;
r[idx] = r[k];
l[idx] = k;
l[r[k]] = idx;
r[k] = idx++;
}
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
int m = scan.nextInt();
r[0] = 1;l[1] = 0;idx = 2;
while(m -- > 0){
String s = scan.next();
char op = s.charAt(0);
if(op == 'L'){
int x = scan.nextInt();
add_all(0,x);
}else if(op == 'R'){
int x = scan.nextInt();
add_all(l[1],x);
}else if(op == 'D'){
int k = scan.nextInt();
remove(k + 1);
}else if(s.equals("IR")){
int k = scan.nextInt();
int x = scan.nextInt();
add_all(k + 1,x);
}else {
int k = scan.nextInt();
int x = scan.nextInt();
add_all(l[k+1],x);
}
}
for(int i = r[0];i != 1; i= r[i]){
System.out.print(e[i] + " ");
}
}
}
3. 模拟栈
1. 模拟栈(一个数组即可)
原题链接
import java.util.Scanner;
public class Main{
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
int m = scan.nextInt();
int[] stl = new int[100010];
int tt = 0;
while(m -- > 0){
String s = scan.next();
if(s.equals("push")){
int x= scan.nextInt();
stl[++tt] = x;
}else if(s.equals("pop")){
tt--;
}else if(s.equals("empty")){
if(tt > 0){
System.out.println("NO");
}else System.out.println("YES");
}else{
System.out.println(stl[tt]);
}
}
}
}
2. 表达式求值(20分钟)
原题链接
import java.util.*;
public class Main{
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
//以字符串形式输入表达式
String s = scan.next();
//map来添加运算符号进去,定义优先级
Map<Character,Integer> map = new HashMap<>();
map.put('+',1);
map.put('-',1);
map.put('*',2);
map.put('/',2);
Stack<Character> op = new Stack<>();//存运算符号
Stack<Integer> num = new Stack<>();//存数字
for(int i = 0 ; i < s.length(); i ++ ){
char c = s.charAt(i);
//判断c字符是不是数字
if(Character.isDigit(c)){
int x = 0,j = i;
//数字可能会是多位数,
while(j < s.length() && Character.isDigit(s.charAt(j))){
x = x * 10 + s.charAt(j) - '0';
j++;
}
num.push(x);//将数字x存入数字栈栈顶
i = j - 1;//重新赋值i
}else if(c == '('){
op.push(c); // 将左括号存入字符栈栈顶
}else if(c == ')'){
//如果栈顶不等于左括号,一直计算下去;
while(op.peek() != '('){
eval(op,num);
}
op.pop(); // 将左括号弹出栈顶
}else { //如果是正常字符
while(!op.empty() && op.peek() != '(' && map.get(op.peek()) >= map.get(c)){
eval(op,num);
}
op.push(c);
}
}
while(!op.empty()) eval(op,num);
System.out.println(num.peek());
}
public static void eval(Stack<Character> op,Stack<Integer> num){
int b = num.pop();
int a = num.pop();
char c = op.pop();
if(c == '+'){
num.push(a+b);
}else if(c == '-'){
num.push(a-b);
}else if(c == '*'){
num.push(a*b);
}else {
num.push(a/b);
}
}
}
4. 队列 tt = -1,hh = 0;
1. 模拟队列
原题链接
import java.util.Scanner;
public class Main{
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
int m = scan.nextInt();
//队列是在tt队尾插入元素,队头hh弹出元素
int[] dl = new int[100010];
int hh = 0;
int tt = -1;
while(m -- > 0){
String s = scan.next();
if(s.equals("push")){
int x = scan.nextInt();
dl[++tt] = x;
}else if(s.equals("pop")){
hh++;
}else if(s.equals("empty")){
if(hh <= tt) System.out.println("NO");
else System.out.println("YES");
}else {
System.out.println(dl[hh]);
}
}
}
}
5. 单调栈
常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
while (tt && check(stk[tt], i)) tt -- ;
stk[ ++ tt] = i;
}
1. 单调栈(4分钟)3.14
原题链接
原题链接
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int[] nums = new int[n+2];
int[] st = new int[n+2];
int tt = 0;
for (int i = 0; i < n; i++) nums[i] = scanner.nextInt();
for (int i = 0; i < n; i++) {
while (tt != 0 && st[tt-1] >= nums[i]) {
tt--;
}
if (tt == 0) {
System.out.print(-1 + " ");
} else {
System.out.print(st[tt-1] + " ");
}
st[tt++] = nums[i];
}
}
}
6. 单调队列
常见模型:找出滑动窗口中的最大值/最小值
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
while (hh <= tt && check_out(q[hh])) hh ++ ; // 判断队头是否滑出窗口
while (hh <= tt && check(q[tt], i)) tt -- ;
q[ ++ tt] = i;
}
1. 滑动窗口例题(10分钟)
原题链接
原题链接
import java.util.*;
import java.io.*;
public class Main
{
static final int N = (int)1e6 + 10;
static int[] a = new int[N], q = new int[N];
static int hh = 0, tt = -1;
static Scanner in = new Scanner(System.in);
static PrintWriter out = new PrintWriter(System.out);
public static void main(String[] args)
{
int n = in.nextInt(), m = in.nextInt();
for (int i = 0; i < n; i++) a[i] = in.nextInt();
for (int i = 0; i < n; i++)
{
if (hh <= tt && i - q[hh] + 1 > m) hh++;
while (hh <= tt && a[q[tt]] >= a[i]) tt--;
q[++tt] = i;
if (i >= m - 1)
out.print(a[q[hh]] + " ");
}
out.print("\n");
hh = 0;
tt = -1;
for (int i = 0; i < n; i++)
{
if (hh <= tt && i - q[hh] + 1 > m) hh++;
while (hh <= tt && a[q[tt]] <= a[i]) tt--;
q[++tt] = i;
if (i >= m - 1)
out.print(a[q[hh]] + " ");
}
out.flush();
}
}
7. KMP
// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度
求模式串的Next数组:
for (int i = 2, j = 0; i <= m; i ++ )
{
while (j && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) j ++ ;
ne[i] = j;
}
// 匹配
for (int i = 1, j = 0; i <= n; i ++ )
{
while (j && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) j ++ ;
if (j == m)
{
j = ne[j];
// 匹配成功后的逻辑
}
}
1. KMP字符串(10分钟)
原题链接
二刷体会
- next数组存储的是,但j不匹配了,j调整到匹配位置,然后j+1和i去比较
- 所以j就需要 = i - 1
- 当角标从0开始 j 就得=-1,角标从1开始,j = 0
★三刷体会 ne表示算上第一个和最后一个的前缀后缀相等值
import java.io.*;
public class Main{
public static void main(String[] args)throws IOException{
//Scanner scan = new Scanner(System.in);
BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter wt = new BufferedWriter(new OutputStreamWriter(System.out));
int N = 100010,M = 1000010;
int n = Integer.parseInt(bf.readLine());//输入p字符的长度
String P = bf.readLine();//输入p字符串
char[] p = new char[N];//创建p数组存字符
for(int i = 1 ; i <= n ; i ++ ) p[i] = P.charAt(i-1);
int m = Integer.parseInt(bf.readLine());//输入s字符串的长度
String S = bf.readLine();//输入s字符串
char[] s = new char[M];//创建s数组存字符
for(int i = 1 ; i <= m ; i ++ ) s[i] = S.charAt(i-1);
int[] ne = new int[N];//kmp核心数组next[];
//KMP核心操作这是求next数组
//这里从2开始是因为当第一个数时就是0所以不用参与。默认是0;
for(int i = 2 , j = 0; i <= n ; i ++ ){
//这里判断从j是不是大于0,如果小于0,表示还没有前缀和后缀相等
//如果出现下一个数与j+1这个数不相等就让j等于上一个数的next[];
while(j > 0 && p[i] != p[j+1]) j = ne[j];
//如果j+1等于i这个数时候,就让j++,说明有有一个相等,前缀和后缀相等长度为1.
if(p[i] == p[j+1]) j ++ ;
//然后将相等的缀长度存入next[]数组中;
ne[i] = j;
}
for(int i = 1 ,j = 0 ; i <= m ; i ++ ){
//判断j是不是大于0,小于0表示还没有前缀和后缀相等
//判断长的数组一个一个遍历比较,看看短的数组,如果不相等就让j = 上一个数的缀长度
while(j > 0 && s[i] != p[j+1]) j = ne[j];
//如果相等就让j++;继续进行下一个数的比较
if(s[i] == p[j+1]) j++;
//如果相等的数等于短的数组的长度,就说明该输出了
if(j == n){
wt.write((i - n) + " ");
//输出之后,要继续往下面遍历对比,所以让j=上一个数的缀长度,
//因为有前缀和后缀相等的部分可以重复使用
j = ne[j];
}
}
//所有write下的内容,会先存在writers中,当启用flush以后,会输出存在其中的内容。
//如果没有调用flush,则不会将writer中的内容进行输出。
wt.flush();
wt.close();
bf.close();
}
}
8. Trie树
1. Trie字符串统计(20分钟)
二刷总结
一行只存储一个数据
原题链接
import java.util.Scanner;
public class Main{
static int N = 100010,idx = 0;
static int[][] song = new int[N][26];
static int[] cnt = new int[N];
static char[] str = new char[N];
public static void insert(char[] str){
int p = 0; //下标0表示头结点,根节点
for(int i = 0 ; i < str.length; i ++ ){
// 将字符串每个字符都转化成数字;0-25
int u = str[i] - 'a';
//如果这个的儿子分支没有字符,说明这条分支还没有这个字符插入过
//就新建一个然后赋值为然后把【idx】下标赋值上去,作为每个分支的专属坐标
if(song[p][u] == 0) song[p][u] = ++idx;
//然后将p往下前进一层
p = song[p][u];
}
//最后停在那一层的那个数字就做标记,说明这是一个字符串的结束。
cnt[p]++;
}
public static int query(char[] str){
int p = 0;//从根节点开始,下标是0表示根节点,头结点
for(int i = 0 ; i < str.length; i ++){
int u = str[i] - 'a'; // 将字符串每个字符都转化成数字0-25
//如果这个点上面没有标记,就说明没有存入过这个字符,所以返回0
if(song[p][u] == 0) return 0;
//如果这个点上面能寻找到这个字符,就让他往下一层继续寻找;
p = song[p][u];
}
//最后查找完之后输出最后一个做标记的点为下标的cnt数组的值。
return cnt[p];
}
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
String sss = scan.nextLine();
while(n -- > 0){
String s = scan.nextLine();
String[] st = s.split(" ");
String s1 = st[0];
String s2 = st[1];
if(s1.equals("I")){
insert(s2.toCharArray());
}else{
System.out.println(query(s2.toCharArray()));
}
}
}
}
★2. 最大异或对
原题链接
★二刷总结
正常思路:
- 两个循环遍历,时间复杂度(N2)
for(int i = 0; i < n; i++)
{
for(int j = 0; j < n; j++)
{
res = max(res,a[i]^a[j]);
}
}
- 优化:创建一个Trie树,针对一个数,去遍历Trie树
(怎么想到用Trie树去优化呢?Trie树可以存储组合性质的信息)
import java.io.*;
public class Main{
static int N = 3100010,idx = 0;
static int[][] song = new int[N][2];
//插入
public static void add(int x){
int p = 0;//从头结点开始
for(int i = 30 ; i >= 0 ; i -- ){ //因为每一个数的二进制是有31位组成,所以需要从大开始遍历
int u = x >> i & 1;//每一个数的二进制31个二进制每一位看0还是1
if(song[p][u] == 0) song[p][u] = ++idx;//判断这一层是空的,就创建,然后赋值下标
p = song[p][u];//然后让往下前进一层
}
}
//查询
public static int query(int x){
int p = 0,res = 0;//从根节点0开始。res进就算异或后的最大值
for(int i = 30; i>= 0 ; i --){
int u = x >> i & 1;
if(song[p][1-u] != 0){ //如果该节点的u是0,则判断一下在这一层有没有跟他相反的0-1,1-0,如果相反对应位置有数
res += (1 << i);//res就将该二进制位对应异或之后的最优解1每一位顺次加起来。因为是异或相反数就是1,这是最优解
p = song[p][1-u];//然后往最优解那边前进一层。
}else{//否则就不是最优解的0匹配1,1匹配0,所以就异或之后的值是0
//res += (0 << i);因为是0所以可以省略,
p = song[p][u];//然后让他往不优解那边前进一层。
}
}
return res;//最后返回异或之后的最大值res
}
public static void main(String[] args)throws IOException{
BufferedReader re = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter wt = new BufferedWriter(new OutputStreamWriter(System.out));
int n = Integer.parseInt(re.readLine());
String[] s = re.readLine().split(" ");
for(int i = 0 ; i < n ; i ++ ){
add(Integer.parseInt(s[i]));
}
int res = 0;
for(int i = 0 ; i < n ; i ++ ){
//因为输入的是字符串所以需要转成整形。然后每一次比较res的值谁大,然后将最大值重新赋值给res
res = Math.max(res,query(Integer.parseInt(s[i])));
}
wt.write(res +" ");//最后输出res,因为快输出输出的是字符串,所以需要在后面加上“ ”;
wt.close();
}
}
9. 并查集 find merge
1. 合并集合(5分钟)
原题链接
原题链接
import java.util.Scanner;
public class Main{
static int N = 100010;
static int[] p = new int[N];
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
int n = scan.nextInt(); // 全部有n个数
int m = scan.nextInt(); // 读入m个操作
//将n个数每个数各自都在一个集合里面。都指向自己,说明现在有多少个集合
for(int i = 1 ; i <= n ; i ++) p[i] = i;
while(m -- > 0){
String s = scan.next();
int a = scan.nextInt();
int b = scan.nextInt();
//合并集合
if(s.equals("M")) p[find(a)] = find(b); //将a集合的根节点即祖先指向b集合的祖先
else{ //是否同个集合
if(find(a) == find(b))System.out.println("Yes"); //如果两个集合的祖先相同说明两个集合在同个集合中。
else System.out.println("No"); //否则相反
}
}
}
//并查集的核心操作,寻找根节点祖先 + 路径压缩
public static int find(int x){
// 如果这个集合的父节点指向的不是自己,说明不是根节点,递归寻找,
//最后找到根节点之后,把路径上的所有集合都指向根节点、
if(p[x] != x) p[x] = find(p[x]);
return p[x]; // 最后返回根节点
}
}
2. 连通块中点的数量(每个集合有多少个元素)
原题链接
import java.util.Scanner;
public class Main{
static int N = 100010;
static int[] p = new int[N];
static int[] size = new int[N];//size用来存每个集合中数的个数
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int m = scan.nextInt();
for(int i = 1 ; i <= n ;i ++){
p[i] = i;
size[i] = 1;// 一开始每个数是一个集合,各自都是个数为1;
}
while(m -- > 0){
String s = scan.next();
if(s.equals("C")){
int a = scan.nextInt();
int b = scan.nextInt();
if(find(a) == find(b)) continue; // 这里需要特判一下,如果两个数是同个集合中的数,就结束;
else{
size[find(b)] += size[find(a)]; // 只有两个数的根节点,也就是祖先的size值才是有用的,
//两个集合中的赋值给另一个祖先的,即赋值给另一个祖先的这个祖先就是合并之后的两个集合的祖先
// b的size[] + =a的size[] 因为b是合并之后的新祖先,所以要让b加上被合并的a
p[find(a)] = find(b); //合并操作,p[a]的祖先指向b,说明b是合并之后的祖先
}
}else if(s.equals("Q1")){
int a = scan.nextInt();
int b = scan.nextInt();
if(find(a) == find(b))System.out.println("Yes");
else System.out.println("No");
}else{
int a = scan.nextInt();
//只有根节点的size才是有用的,则通过find(a)找到他的根节点然后输出根节点的size;
System.out.println(size[find(a)]);
}
}
}
public static int find(int x){
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
}
★3. 食物链
原题链接
法一: x,x+n,x+n+n merge(f[x+n],f[x])
思路:
- 因为有三种物种,A吃B,B吃C,C吃A
- 如果我们用一个数组存储,那么比如1吃2,那么我们让2的角标处的值标记成1,如果3吃2,那怎么标记?一个数组指定标记不过来。
- 那么我们想用三个数组存储,其实也存储不过来,因为角标就那么几个,
- 最好的方法就是,用x,x+n,x+n+n来表示
比如1吃2,那么就可能有三种情况,
A类中的1吃B类的2 : fa[1] = fa[2+n+n]
B类中的1吃C类的2 : fa[1+n] = fa[2]
C类中的1中A类的2 : fa[1+n+n] = fa[2+n];
这样的话,就会有3*n个角标,就可以充分表达
A中的1吃B中的2(B中的2用2+n表示)
这样的话就不会出现数字冲突
A吃B
则让f[A] = B
二刷总结
a吃b
f[a+n] = f[b]
或者
f[b+n] = f[a]
/*
*/
#include <bits/stdc++.h>
using namespace std;
int fa[200000];
int n,m,k,x,y,ans;
int get(int x)
{
if(x==fa[x])
return x;
return fa[x]=get(fa[x]);
}
void merge(int x,int y)
{
fa[get(x)]=get(y);
}
int main()
{
cin>>n>>m;
for(int i=1;i<=3*n;i++)
fa[i]=i;
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&k,&x,&y);
if(x>n || y>n)
ans++;
else if(k==1)
{
if(get(x)==get(y+n) || get(x)==get(y+n+n)) //如果x,y是同类,但是x是y的捕食中的动物,或者x是y天敌中的动物,那么错误.
ans++;
else
{
merge(x,y);
merge(x+n,y+n);
merge(x+n+n,y+n+n);
}
}
else
{
if(x==y || get(x)==get(y) || get(x)==get(y+n)) //x就是y,或者他们是同类,再或者是y的同类中有x
ans++;//都是假话
else
{
merge(x,y+n+n);//y的捕食域加入x
merge(x+n,y);//x的天敌域加入y
merge(x+n+n,y+n);//x的捕食域是y的同类域.
}
}
}
cout<<ans<<endl;
}
法二:将有关系的都存储在一个部落,用到根节点的距离表示关系
二刷总结
- X吃Y 让x的祖宗等于y的祖宗 或者 y的祖宗等于x的祖宗都可以
- find查找函数中,具有压缩路径的作用,所以在写的时候,先找到此根节点,然后依次压缩,如下代码
find中 d[x] += d[p[x]];
int find(int x)
{
if (p[x] != x)
{
int t = find(p[x]);
d[x] += d[p[x]];
p[x] = t;
}
return p[x];
}
不可以如下代码
int find(int x)
{
if (p[x] != x)
{
d[x] += d[p[x]];
return p[x] = find(p[x]);
}
return p[x];
}
- 在查询合并过程中,比如查询x的父节点,应该用一个变量记录下来,不能多次find,不然找不到x的原来父节点了
- 初始化 p[i] = i; d[i] = 0;
- 合并的时候,画图即可明白彼此的距离
具体过程如下:
#include <iostream>
using namespace std;
const int N = 50010;
int n, m;
int p[N], d[N];
int find(int x)
{
if (p[x] != x)
{
int t = find(p[x]);
d[x] += d[p[x]];
p[x] = t;
}
return p[x];
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ ) p[i] = i;
int res = 0;
while (m -- )
{
int t, x, y;
scanf("%d%d%d", &t, &x, &y);
if (x > n || y > n) res ++ ;
else
{
int px = find(x), py = find(y);
if (t == 1)
{
if (px == py && (d[x] - d[y]) % 3) res ++ ;
else if (px != py)
{
p[px] = py;
d[px] = d[y] - d[x];
}
}
else
{
if (px == py && (d[x] - d[y] - 1) % 3) res ++ ;
else if (px != py)
{
p[px] = py;
d[px] = d[y] + 1 - d[x];
}
}
}
}
printf("%d\n", res);
return 0;
}
10. 堆 ✔ ✔
模板
// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
// ph[k]存储第k个插入的点在堆中的位置
// hp[k]存储堆中下标是k的点是第几个插入的
int h[N], ph[N], hp[N], size;
// 交换两个点,及其映射关系
void heap_swap(int a, int b)
{
swap(ph[hp[a]],ph[hp[b]]);
swap(hp[a], hp[b]);
swap(h[a], h[b]);
}
void down(int u)
{
int t = u;
if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2;
if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
if (u != t)
{
heap_swap(u, t);
down(t);
}
}
void up(int u)
{
while (u / 2 && h[u] < h[u / 2])
{
heap_swap(u, u / 2);
u >>= 1;
}
}
// O(n)建堆
for (int i = n / 2; i; i -- ) down(i);
- 向下调整
法一:如果孩子节点 小于当前节点 那么交换 然后递归
void down(int u)
{
int t = u;
if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2;
if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
if (u != t)
{
heap_swap(u, t);
down(t);
}
}
法二:parent = child * 2 直到 超出范围
public void createHeap() {
for (int parent = (usedSize-1-1) / 2; parent >= 0 ; parent--) {
shiftDown(parent,usedSize);
}
}
/**
* 实现 向下调整
* @param parent 每棵子树的根节点的下标
* @param len 每棵子树的结束位置
*/
private void shiftDown(int parent ,int len) {
int child = 2 * parent + 1;
//最起码是有左孩子
while (child < len) {
//判断 左孩子 和 右孩子 谁最大,前提是 必须有 右孩子
if(child+1 < len && elem[child] < elem[child+1]) {
child++;//此时 保存了最大值的下标
}
if(elem[child] > elem[parent]) {
swap(elem,child,parent);
parent = child;
child = 2*parent+1;
}else {
break;
}
}
}
private void swap(int[] array,int i,int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
堆的操作
- 删除最值
- 因为最小值,位于数组的最前端,所以我们可以让最前端等于最后端的值,(此时堆的最小值已经没了)
- 但是呢,堆里出现了两个最前端的值,所以需要再–sz删除最后段的值(不要怕,最后段的值,已经保存到最前端的结点上,)
- 最后把最顶端的点 用down
- 修改值(改,先down,后up)
孩子和父亲的关系
数组建堆
1. 堆排序 ✔ ✔
原题链接
import java.util.Scanner;
public class Main{
static int N = 100010;
static int[] h = new int[N];
static int size;
//交换函数
public static void swap(int x,int y){
int temp = h[x];
h[x] = h[y];
h[y] = temp;
}
//堆函数核心函数,堆排序函数,传入的参数u是下标
public static void down(int u){
int t = u; // t用来分身变量
//u的左分支下标小于size说明该数存在,然后如果这个数小于t,则让左下标赋值给t,即u的分身
if(u * 2 <= size && h[u * 2] < h[t]) t = u * 2;
//u的右分支下标小于size说明该数存在,然后如果这个数小于t,因为可能上面的if语句会重置t,则判断是不是小于新或旧t,
//如果小于t的话,就让右下标赋值给t ,即u的分身。
if(u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
//如果u不等于t,说明左或者右下标有比较小的值赋值给t了,分身变了
if(u != t){
//就让u跟t这两个数交换一下位置,让小的数替代u的位置
swap(u,t);
//然后继续递归t下标小的位置
down(t);
}
}
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int m = scan.nextInt();
for(int i = 1 ; i <= n ; i ++ ) h[i] = scan.nextInt(); //首先将所有数先存入数组中
size = n;//长度为n
//从n/2的位置开始将数组中的值插入堆中
//堆结构是一个完全二叉树,所有有分支的数等于没有分支的数,即最后一排的数量等于上面所有的数
//最下面一排没有分支的数不用参与插入,所以从n/2开始进行插入
for(int i = n/2 ; i >= 0; --i ) down(i);//就是让他进行向下排序处理
while(m -- > 0){
//输出前m小的m个元素
System.out.print(h[1] + " "); //每一次输出头节点
//因为是数组,删除第一个数复杂,删除最后一个元素比较简单
//所以就将最后一个元素将第一个元素覆盖掉,然后删除掉最后一个元素
h[1] = h[size--];
//然后进行堆排序,对第一个元素进行处理
down(1);
}
}
}
2. 模拟堆 ✔ ✔
原题链接
import java.util.Scanner;
public class Main{
static int N = 100010,size,m;
static int[] h = new int[N];
static int[] hp = new int[N];//自身被映射数组
static int[] ph = new int[N];//映射数组
public static void swap(int[] a,int x,int y){
int temp = a[x];
a[x] = a[y];
a[y] = temp;
}
public static void head_swap(int x,int y){
//这里因为映射数组跟被映射数组是互相指向对方,如果有两个数更换位置,映射下标也要进行更换
//ph的下标指向是按顺序插入的下标,hp所对应的值是ph的按顺序的下标,用这两个属性进行交换
swap(ph,hp[x],hp[y]);
//因为按照顺序插入ph到指向交换了,对应指向ph的hp也要进行交换
swap(hp,x,y);
//最后两个值进行交换
swap(h,x,y);
}
public static void down(int x){
int t = x;//x的分身
//判断一下左下标是不是存在
//判断一下左下标的值是不是比我t的值小 。那么就将左下标的值赋予t;否则不变
if(x * 2 <= size && h[x * 2] < h[t]) t = x * 2;
//判断一下右下标的值是不是比我t的值小。那么就将右下标的值赋予t,否则不变
if(x *2 + 1 <= size && h[x * 2 + 1] < h[t]) t = x * 2 + 1;
if(t != x){//如果x不等于他的分身
head_swap(x,t);//那就进行交换顺序
down(t);//然后一直向下进行操作
}
}
public static void up(int x){
//向上操作,判断一下根节点还不是存在
//看一下根节点是不是比我左分支或者右分支的值大,大的话就进行交换
while(x / 2 > 0 && h[x / 2] > h[x]){
head_swap(x,x/2);
x = x / 2;//相当于一直up
}
}
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
size = 0;//size是原数组的下标
m = 0;//m是映射数组的下标
while(n -- > 0){
String s = scan.next();
if(s.equals("I")){//插入操作
int x= scan.nextInt();
size ++;m ++;//插入一个数两个数组的下标都加上1;
ph[m] = size;hp[size] = m;//ph与hp数组是映射关系
h[size] = x;//将数插入到堆中最后位置
up(size);//然后up,往上面排序一遍
}else if(s.equals("PM")){ //输出当前集合中的最小值
System.out.println(h[1]);
}else if(s.equals("DM")){//删除当前集合中的最小值
//因为需要用到映射数组与被映射数组,因为需要找到k的位置在哪里,需要让映射的顺序,
//因为如果用size,size是会随时改变的,不是按顺序的,因为会被up或者down顺序会被修改
head_swap(1,size);//将最后一个数替换掉第一个最小值元素,然后数量减1,size--
size--;
down(1);//插入之后进行向下操作,因为可能不符合小根堆
}else if(s.equals("D")){//删除当前集合中第k个插入得数
int k = scan.nextInt();
k = ph[k];//ph[k] 是一步一步插入映射的下标,不会乱序,
head_swap(k,size);//然后将k与最后一个元素进行交换,然后长度减1,size--
size--;
up(k);//进行排序一遍,为了省代码量,up一遍down一遍。因为只会执行其中一个
down(k);
}else{
int k = scan.nextInt();
int x = scan.nextInt();
k = ph[k];//ph[k] 是一步一步插入映射的下标,顺序是按照插入时候的顺序
h[k] = x;//然后将第k为数修改为数x
up(k);//up一遍,down一遍
down(k);
}
}
}
}
11. 哈希表 ✔ ✔
模板
(1) 拉链法
int h[N], e[N], ne[N], idx;
// 向哈希表中插入一个数
void insert(int x)
{
int k = (x % N + N) % N;
e[idx] = x;
ne[idx] = h[k];
h[k] = idx ++ ;
}
// 在哈希表中查询某个数是否存在
bool find(int x)
{
int k = (x % N + N) % N;
for (int i = h[k]; i != -1; i = ne[i])
if (e[i] == x)
return true;
return false;
}
(2) 开放寻址法
int h[N];
// 如果x在哈希表中,返回x的下标;如果x不在哈希表中,返回x应该插入的位置
int find(int x)
{
int t = (x % N + N) % N;
while (h[t] != null && h[t] != x)
{
t ++ ;
if (t == N) t = 0;
}
return t;
}
例1. 模拟散列表 ✔ ✔
原题链接
拉链法代码(链表)
- N取大于范围的第一个质数
- k = (x % N + N) % N; (%N 为了避免超级大的值 + N 为了避免出现负数 再%N为了正数+N超出范围)
- memset(h, -1, sizeof h);
import java.util.Scanner;
public class Main{
static int N = 100003,idx;
static int[] h = new int[N];
static int[] e = new int[N];
static int[] ne = new int[N];
public static void add(int x){
int k = (x % N + N) % N;
e[idx] = x;
ne[idx] = h[k];
h[k] = idx ++ ;
}
public static boolean find(int x){
int k = (x % N + N) % N;
for(int i = h[k];i != -1;i = ne[i]){
if(e[i] == x){
return true;
}
}
return false;
}
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
idx = 0;
for(int i = 0 ; i < N ; i++ ){
h[i] = -1;
}
while(n -- > 0){
String x = scan.next();
if(x.equals("I")){
int a = scan.nextInt();
add(a);
}else{
int b = scan.nextInt();
if(find(b)) System.out.println("Yes");
else System.out.println("No");
}
}
}
}
开放寻址法代码(有空位就存,空位用null=0x3f3f3f3f)
- 大于2倍的第一个质数
#include <cstring>
#include <iostream>
using namespace std;
//开放寻址法一般开 数据范围的 2~3倍, 这样大概率就没有冲突了
const int N = 2e5 + 3; //大于数据范围的第一个质数
const int null = 0x3f3f3f3f; //规定空指针为 null 0x3f3f3f3f
int h[N];
int find(int x) {
int t = (x % N + N) % N;
while (h[t] != null && h[t] != x) {
t++;
if (t == N) {
t = 0;
}
}
return t; //如果这个位置是空的, 则返回的是他应该存储的位置
}
int n;
int main() {
cin >> n;
memset(h, 0x3f, sizeof h); //规定空指针为 0x3f3f3f3f
while (n--) {
string op;
int x;
cin >> op >> x;
if (op == "I") {
h[find(x)] = x;
} else {
if (h[find(x)] == null) {
puts("No");
} else {
puts("Yes");
}
}
}
return 0;
}
★例2. 字符串哈希 ✔ ✔
原题链接
二刷思路
- 把一段字符串转成一段数字,便可以直接比较相等
- 怎么转?进制大于26
- 样例没找全 p[r-l+1] 这个点没找到
import java.util.Scanner ;
public class Main{
//开的是long类型数组,本来是需要进行前缀hash求完之后需要进行模2的64次方来防止相同的冲突,可能超过我们给的数组大小
static int N = 100010,P = 131;//p是进制数,惊艳值
static long[] h = new long[N];//这是存放hash前缀值得数组
static long[] p = new long[N];//这是存放p的n次方的数组
public static long get(int l,int r){//这里是将运用了一点前缀和公式的方式进行计算
//求l-r区间的hash值,就要用h[r] - h[l-1],因为两者位数不用需要让h[l-1]向左边移到跟h[r]对齐
//就比如求1234的3-4区间位,1234 - 12,12左移然后就让12*10^(4-3+1)=12*10^2=1200,然后1234-1200 = 34,这样进行计算出来
//然后本题是p进制,所以需要将上面公式中的10换成p就行了
//h[0] = 0
//h[1] = h[i-1] * P + str[1] = 0*P+a = a
//h[2] = a * P + b
//h[3] = (a*P+b)*P+c = a*p[2]+b*P+c
//h[4] = (a*p[2]+b*P+c)*P+d = a*p[3]+b*p[2]+c*P+d
//比如abcd求3-4区间位,就是让h[d]-h[b],h[b]位数不用需要向左移对齐h[d],
//h[2]*P^(4-3+1)=(a*P+b)*P^2 = a*P^3+b*P^2
//然后就让h[d] - h[b]求出34区间值,(a*p[3]+b*p[2]+c*P+d) - (a*P^3+b*P^2) = c*P+d
return h[r] - h[l-1]*p[r-l+1];
}
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int m = scan.nextInt();
String s = scan.next();
p[0] = 1;//这个是p的0次方的值,需要单独写出来,非常重要
for(int i = 1 ; i <= n ; i++ ){
p[i] = p[i-1] * P;//这里对应每一个下标对应对应P的多少次方
h[i] = h[i-1] * P + s.charAt(i-1);//这里是公式,预处理前缀哈希的值,因为是P进制,所以中间乘的是P
}
while(m -- > 0){
int l1 = scan.nextInt();
int r1 = scan.nextInt();
int l2 = scan.nextInt();
int r2 = scan.nextInt();
//判断两个区间是不是相同,用get的方法返回值一样说明区间的hash值是一样的
if(get(l1,r1) == get(l2,r2)) System.out.println("Yes");
else System.out.println("No");
}
}
}