✔ ★算法基础笔记(Acwing)(二)—— 数据结构(17道题)【java版本】

数据结构

  • 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分钟)

原题链接

二刷体会
  1. next数组存储的是,但j不匹配了,j调整到匹配位置,然后j+1和i去比较
  2. 所以j就需要 = i - 1
  3. 当角标从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. 最大异或对

原题链接

★二刷总结

正常思路:

  1. 两个循环遍历,时间复杂度(N2
for(int i = 0; i < n; i++)
{
	for(int j = 0; j < n; j++)
	{
		res = max(res,a[i]^a[j]);
	}
}	
  1. 优化:创建一个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])

思路:

  1. 因为有三种物种,A吃B,B吃C,C吃A
  2. 如果我们用一个数组存储,那么比如1吃2,那么我们让2的角标处的值标记成1,如果3吃2,那怎么标记?一个数组指定标记不过来。
  3. 那么我们想用三个数组存储,其实也存储不过来,因为角标就那么几个,
  4. 最好的方法就是,用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 
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;
}
法二:将有关系的都存储在一个部落,用到根节点的距离表示关系
二刷总结
  1. X吃Y 让x的祖宗等于y的祖宗 或者 y的祖宗等于x的祖宗都可以
  2. 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];
}

  1. 在查询合并过程中,比如查询x的父节点,应该用一个变量记录下来,不能多次find,不然找不到x的原来父节点了
  2. 初始化 p[i] = i; d[i] = 0;
  3. 合并的时候,画图即可明白彼此的距离

具体过程如下:

#include 

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);
  1. 向下调整
    法一:如果孩子节点 小于当前节点 那么交换 然后递归
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;
    }

堆的操作

  1. 删除最值
  1. 因为最小值,位于数组的最前端,所以我们可以让最前端等于最后端的值,(此时堆的最小值已经没了)
  2. 但是呢,堆里出现了两个最前端的值,所以需要再–sz删除最后段的值(不要怕,最后段的值,已经保存到最前端的结点上,)
  3. 最后把最顶端的点 用down
  1. 修改值(改,先down,后up)

在这里插入图片描述

✔ ★算法基础笔记(Acwing)(二)—— 数据结构(17道题)【java版本】_第1张图片

孩子和父亲的关系
✔ ★算法基础笔记(Acwing)(二)—— 数据结构(17道题)【java版本】_第2张图片

数组建堆
在这里插入图片描述

1. 堆排序 ✔ ✔

原题链接

✔ ★算法基础笔记(Acwing)(二)—— 数据结构(17道题)【java版本】_第3张图片

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. 模拟堆 ✔ ✔

原题链接

✔ ★算法基础笔记(Acwing)(二)—— 数据结构(17道题)【java版本】_第4张图片

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. 模拟散列表 ✔ ✔

原题链接

✔ ★算法基础笔记(Acwing)(二)—— 数据结构(17道题)【java版本】_第5张图片

拉链法代码(链表)
  1. N取大于范围的第一个质数
  2. k = (x % N + N) % N; (%N 为了避免超级大的值 + N 为了避免出现负数 再%N为了正数+N超出范围)
  3. 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)
  1. 大于2倍的第一个质数
#include 
#include 

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. 字符串哈希 ✔ ✔

原题链接

二刷思路
  1. 把一段字符串转成一段数字,便可以直接比较相等
  2. 怎么转?进制大于26
  3. 样例没找全 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");
        }
    }
}

你可能感兴趣的:(算法基础,java,算法,笔记)