IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> 算法笔记二(这些数据结构你知道吗?) -> 正文阅读

[数据结构与算法]算法笔记二(这些数据结构你知道吗?)

目录

一、数组模拟单链表

二、数组模拟双链表

三、数组模拟栈

1、表达式求值

四、数组模拟队列

五、单调栈

六、单调队列

七、KMP

八、Trie?

1.最大异或对

九、并查集

1.连通块中点的数量

2、食物链

十、哈希表

1、字符串

十一、堆

1、模拟堆


一、数组模拟单链表

单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) +?指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据。

单链表是我们很常见的一种线性表 适用于经常增删元素的场景 在我们日常使用中我们可以采用结构体的方式来实现链表 也就是把很多个 Node 串在一起 ,但是这种结构体的实现是很慢的 因为java中new一个对象所耗费的时间是很多的 ,因此我们采用数组模拟就能很好的规避。同时,数组模拟也有其缺陷---浪费一定的数组空间 但这并不在我们算法的考虑范围之内

思路:首先定义一个e【】数组用来存储元素本来的值,用一个ne【】数组来存储下一个结点next的下标 idx用于存储元素的下标

实现一个单链表,链表初始为空,支持三种操作:

  1. 向链表头插入一个数;
  2. 删除第?kk?个插入的数后面的数;
  3. 在第?kk?个插入的数后插入一个数。

现在要对该链表进行?MM?次操作,进行完所有操作后,从头到尾输出整个链表。

注意:题目中第?kk?个插入的数并不是指当前链表的第?kk?个数。例如操作过程中一共插入了?nn?个数,则按照插入的时间顺序,这?nn?个数依次为:第?11?个插入的数,第?22?个插入的数,…第?nn?个插入的数。

输入格式

第一行包含整数?MM,表示操作次数。

接下来?MM?行,每行包含一个操作命令,操作命令可能为以下几种:

  1. H x,表示向链表头插入一个数?xx。
  2. D k,表示删除第?kk?个插入的数后面的数(当?kk?为?00?时,表示删除头结点)。
  3. I k x,表示在第?kk?个插入的数后面插入一个数?xx(此操作中?kk?均大于?00)。

输出格式

共一行,将整个链表从头到尾输出。

数据范围

1≤M≤1000001≤M≤100000
所有操作保证合法。

import java.util.*;

public class Main{
    static int N = 100010;
    static int[] e = new int[N];
    static int[] ne = new int[N];
    static int idx = 0,head = -1;    
    
    public static void add_head(int x) {
        e[idx] = x; //先在e中把x存下来
        ne[idx] = head; //因为是头插,所以该元素的next必须是head
        head = idx++;  //此时真正的头节点已经是idx了 并且idx++ 用于下一次的插入元素
    }
    
    public static void add(int k, int x) { // 此函数用于在下标为k 的点后插入值为x的元素
        e[idx] = x; // 老样子,先把x存起来
        ne[idx] = ne[k]; //新的next应该是原来k的next
        ne[k] = idx++;  //k的next就是idx 并把idx++
    }
    
    public static void remove(int k) { //删除k下标之后的元素
       ne[k] = ne[ne[k]]; //让k直接指向next的next
    }
        
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int m = sc.nextInt();
        while (m -- > 0) {
            String s = sc.next();
            if (s.equals("H")) {
                int x = sc.nextInt();
                add_head(x);
            } else if (s.equals("D")) {
                int k = sc.nextInt();
                if (k == 0) head = ne[head];
                else  remove(k - 1);
            } else {
                int k = sc.nextInt();
                int x = sc.nextInt();
                add(k - 1,x);
            }
        }
        for (int i = head; i != -1;i = ne[i]) {
            System.out.print(e[i] + " ");
        }
        
    }
}

二、数组模拟双链表

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表

事实上,双链表相比单链表就多了一个指向前一个元素的指针,仅此而已。

在实现双链表时,我们采用一种简化的方式,使用 l【】数组表示向前的指针 r【】数组表示向后的指针 同时 r【0】指向双链表起始位置? l[1] 指向双链表结束位置

实现一个双链表,双链表初始为空,支持?55?种操作:

  1. 在最左侧插入一个数;
  2. 在最右侧插入一个数;
  3. 将第?kk?个插入的数删除;
  4. 在第?kk?个插入的数左侧插入一个数;
  5. 在第?kk?个插入的数右侧插入一个数

现在要对该链表进行?MM?次操作,进行完所有操作后,从左到右输出整个链表。

注意:题目中第?kk?个插入的数并不是指当前链表的第?kk?个数。例如操作过程中一共插入了?nn?个数,则按照插入的时间顺序,这?nn?个数依次为:第?11?个插入的数,第?22?个插入的数,…第?nn?个插入的数。

输入格式

第一行包含整数?MM,表示操作次数。

接下来?MM?行,每行包含一个操作命令,操作命令可能为以下几种:

  1. L x,表示在链表的最左端插入数?xx。
  2. R x,表示在链表的最右端插入数?xx。
  3. D k,表示将第?kk?个插入的数删除。
  4. IL k x,表示在第?kk?个插入的数左侧插入一个数。
  5. IR k x,表示在第?kk?个插入的数右侧插入一个数。

输出格式

共一行,将整个链表从左到右输出。

import java.util.*;

public class Main{
    static int N = 100010;
    static int idx;
    static int[] e = new int[N];
    static int[] l = new int[N];
    static int[] r = new int[N];
    
    public static void init() {
        r[0] = 1;
        l[1] = 0;
        idx = 2;
    }
    
    public static void add(int k, int x) {
        e[idx] = x; //先把元素存下来在考虑指针关系
        r[idx] = r[k];
        l[idx] = k;
        l[r[k]] = idx;
        r[k] = idx++;
    }
    
    public static void remove(int k) {  //这里是删除下标为k的元素
        l[r[k]] = l[k];
        r[l[k]] = r[k];
    }
    
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        init();
        int m = sc.nextInt();
        while (m -- > 0) {
            String s = sc.next();
            if (s.equals("L")) { //最左端插入x
                int x = sc.nextInt();
                add(0,x);
            } else if (s.equals("R")) {
                int x = sc.nextInt();
                add(l[1],x);
            } else if (s.equals("D")) { // 删除第k个元素 但我们数组中已经存了两个边界值 0 和 1 所以是 k - 1 + 2
                int k = sc.nextInt();
                remove(k + 1);
            } else if (s.equals("IL")) {
                int k = sc.nextInt();
                int x = sc.nextInt();
                add(l[k + 1],x);
            } else {
                int k = sc.nextInt();
                int x = sc.nextInt();
                add(k + 1,x);
            }
        }
        
        for (int i = r[0]; i != 1; i = r[i]) {
            System.out.print(e[i] + " ");
        }
        
    }
}

三、数组模拟栈

栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。

总的来说,栈要满足先进后出的特点

实现一个栈,栈初始为空,支持四种操作:

  1. push x?– 向栈顶插入一个数?xx;
  2. pop?– 从栈顶弹出一个数;
  3. empty?– 判断栈是否为空;
  4. query?– 查询栈顶元素。

现在要对栈进行?MM?个操作,其中的每个操作?33?和操作?44?都要输出相应的结果。

输入格式

第一行包含整数?MM,表示操作次数。

接下来?MM?行,每行包含一个操作命令,操作命令为?push xpopemptyquery?中的一种。

输出格式

对于每个?empty?和?query?操作都要输出一个查询结果,每个结果占一行。

其中,empty?操作的查询结果为?YES?或?NOquery?操作的查询结果为一个整数,表示栈顶元素的值。

数据范围

1≤M≤1000001≤M≤100000,
1≤x≤1091≤x≤109
所有操作保证合法。

输入样例:

10
push 5
query
push 6
pop
query
pop
empty
push 4
query
empty

输出样例:

5
5
YES
4
NO
import java.util.*;

public class Main{
    static int N = 100010;
    static int tt = -1;
    static int[] stk = new int[N];
    
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int m = sc.nextInt();
        while (m -- > 0) {
            String s = sc.next();
            if (s.equals("push")) {
                int x = sc.nextInt();
                stk[++tt] = x;
            } else if (s.equals("pop")) {
                tt--;
            } else if (s.equals("empty")) {
                if (tt == -1) System.out.println("YES");
                else System.out.println("NO");
            } else {
                System.out.println(stk[tt] );
            }
        }
    }
}

栈的的代码实现非常简单,仅需要维持一个尾结点即可?

1、表达式求值

给定一个表达式,其中运算符仅包含?+,-,*,/(加 减 乘 整除),可能包含括号,请你求出表达式的最终值。

注意:

  • 数据保证给定的表达式合法。
  • 题目保证符号?-?只作为减号出现,不会作为负号出现,例如,-1+2,(2+2)*(-(1+1)+2)?之类表达式均不会出现。
  • 题目保证表达式中所有数字均为正整数。
  • 题目保证表达式在中间计算过程以及结果中,均不超过?231?1231?1。
  • 题目中的整除是指向?00?取整,也就是说对于大于?00?的结果向下取整,例如?5/3=15/3=1,对于小于?00?的结果向上取整,例如?5/(1?4)=?15/(1?4)=?1。
  • C++和Java中的整除默认是向零取整;Python中的整除//默认向下取整,因此Python的eval()函数中的整除也是向下取整,在本题中不能直接使用。

输入格式

共一行,为给定表达式。

输出格式

共一行,为表达式的结果。

数据范围

表达式的长度不超过?105105。

输入样例:

(2+2)*(1+1)

输出样例:

8

分析这道题:请问在何时我们需要从栈中弹出一个操作符,两个数字进行计算呢?答案是:

当当前字符的优先级低于前一个字符时,如: 1 * 2 + 3 当我们计算到+时,我们需要对之前的数进行操作。因此栈中的操作符优先级一定是从低到高的,因为如果出现从高到低这个‘高’就会被我们处理。所以我们因次可以分出两种情况: 1、无括号,就碰到由高优先级到低优先级,正常的操作即可? 2、 有括号,从后往前计算,保证了优先级从高到低(因为栈中由低到高) 最后我们处理完栈中所有操作符即可

import java.util.*;

public class Main{
    public static void eval(Deque<Character> op,Deque<Integer> num) {
        char c = op.pop();
        int b = num.pop();          //取出一个操作符,两个数进行运算
        int a = num.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);
        }
    }
    
    public static void main(String[] args) {
        HashMap<Character,Integer> map = new HashMap();  //建立map,并存入优先级
        map.put('+',1);
        map.put('-',1);
        map.put('*',2);
        map.put('/',2);
        Deque<Character> op = new LinkedList();       //准备两个栈,存操作符,存元素
        Deque<Integer> num = new LinkedList();
        Scanner sc = new Scanner(System.in);
        String s = sc.nextLine();
        for(int i = 0; i < s.length(); i ++) {
            char c = s.charAt(i);
            if (Character.isDigit(c)) {             //如果是数字我们可能涉及拼接
                int sum = 0;
                int j = i;
                while(j < s.length() && Character.isDigit(s.charAt(j))) {
                    sum = sum * 10 + s.charAt(j) - '0';
                    j++;
                }
                i = j - 1;//由于是for循环,所以i要跳到j的前一个位置
                num.push(sum);
            } else if (c == '(') {
                op.push(c);
            } else if (c == ')') {
                while (op.peek() != '(') {
                    eval(op,num);
                }
                op.pop();  //把左括号弹出去
            } else {
                while (!op.isEmpty() && op .peek() != '(' && map.get(c) <= map.get(op.peek()) ) {
                    eval(op,num);
                }
                op.push(c);
            }
        }
        while(!op.isEmpty()) {
            eval(op,num);
        }
        System.out.print(num.pop());
    }
}

四、数组模拟队列

队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。

同样,一句话总结队列就是先进先出 ,因此我们要维持头和尾两个指针

实现一个队列,队列初始为空,支持四种操作:

  1. push x?– 向队尾插入一个数?xx;
  2. pop?– 从队头弹出一个数;
  3. empty?– 判断队列是否为空;
  4. query?– 查询队头元素。

现在要对队列进行?MM?个操作,其中的每个操作?33?和操作?44?都要输出相应的结果。

输入格式

第一行包含整数?MM,表示操作次数。

接下来?MM?行,每行包含一个操作命令,操作命令为?push xpopemptyquery?中的一种。

输出格式

对于每个?empty?和?query?操作都要输出一个查询结果,每个结果占一行。

其中,empty?操作的查询结果为?YES?或?NOquery?操作的查询结果为一个整数,表示队头元素的值。

数据范围

1≤M≤1000001≤M≤100000,
1≤x≤1091≤x≤109,
所有操作保证合法。

输入样例:

10
push 6
empty
query
pop
empty
push 3
push 4
pop
query
push 6

输出样例:

NO
6
YES
4
import java.util.*;

public class Main{
    static int tt = -1,hh = 0;
    static int q[] = new int[100010];
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int m = sc.nextInt();
        while (m -- > 0) {
            String s = sc.next();
            if (s.equals("push")) {
                int x = sc.nextInt();
                q[++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(q[hh]);
            } 
        }
    }
}

五、单调栈

单调栈顾名思义里边存储的元素是单调的,一般来说就分为单调递增栈和单调递减栈。

给定一个长度为?NN?的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出??1?1。

输入格式

第一行包含整数?NN,表示数列长度。

第二行包含?NN?个整数,表示整数数列。

输出格式

共一行,包含?NN?个整数,其中第?ii?个数表示第?ii?个数的左边第一个比它小的数,如果不存在则输出??1?1。

数据范围

1≤N≤1051≤N≤105
1≤数列中元素≤1091≤数列中元素≤109

输入样例:

5
3 4 2 7 5

输出样例:

-1 3 -1 2 2

分析这道题,要我们寻找元素左侧比他小的第一个数,试想,假如我们当前元素比上一个元素小,那是不是说明上一个元素永远没有成为这个“左边第一个比他小的数”?。因此我们队列就没有存储上一个元素的必要了,因此,总的来说,栈里的元素总体上就是单调上升的

import java.util.*;

public class Main{
    static int tt = -1;
    static int[] stk = new int[100010];
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        for (int i = 0; i < n; i ++) {
            int num = sc.nextInt();
            while (tt != -1 && stk[tt] >= num) {
                tt--;     //弹出这个数
            }
            if (tt == -1) System.out.print(-1+" ");
            else System.out.print(stk[tt]+" ");
            stk[++tt] = num;
        }
    }
}

六、单调队列

单调队列又叫滑动窗口 同样的遇到这类问题我们在队列中存储的元素也是单调的

比如下面这道题:

给定一个大小为?n≤106n≤106?的数组。

有一个大小为?kk?的滑动窗口,它从数组的最左边移动到最右边。

你只能在窗口中看到?kk?个数字。

每次滑动窗口向右移动一个位置。

以下是一个例子:

该数组为?[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

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

输入格式

输入包含两行。

第一行包含两个整数?nn?和?kk,分别代表数组长度和滑动窗口的长度。

第二行有?nn?个整数,代表数组的具体数值。

同行数据之间用空格隔开。

输出格式

输出包含两个。

第一行输出,从左至右,每个位置滑动窗口中的最小值。

第二行输出,从左至右,每个位置滑动窗口中的最大值。

输入样例:

8 3
1 3 -1 -3 5 3 6 7

输出样例:

-1 -3 -3 -3 3 3
3 3 5 5 6 7

首先说明一点,java中打印行为是一个非常耗时的操作,所以这道题如果我们用普通的System.out.print打印就会超时。因此我们需要采用快输,也就是PrintWtiter类 打印完成后记得flush即可

分析这道题:如果我们遍历到当前数比前一个数小,并且他的位置还靠后,那么是不是说明我的滑动窗口?中前一个数不可能作为答案出现,因为窗口是向后滑动的,因此前一个数我们可以从队列中剔除,如果当前队列的头在滑动窗口中,我们就打印它,否则,我们把头往后移一位,再打印(因为我们的窗口每次只往后移动一位)

注意:由于要判断头还在不在窗口中,因此我们队列中存的是下标。

import java.util.*;
import java.io.*;

public class Main{
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out));
        int n = sc.nextInt();
        int k = sc.nextInt();
        int tt = -1,hh = 0;
        int[] q = new int[1000010];
        int[] a = new int[1000010];
        for (int i = 0; i < n; i ++) {
            a[i] = sc.nextInt();
        }
        for(int i = 0; i < n; i++) {
            if(hh <= tt && q[hh] < i-k+1) hh++;
            while(hh <= tt && a[i] <= a[q[tt]]) tt--;
            q[++tt] = i;
            if(i >= k-1) pw.print(a[q[hh]]+" ");
        }
        pw.println();
        tt = -1;
        hh = 0;
        for (int i = 0; i < n; i ++) {
            if(hh <= tt && q[hh] < i-k+1) hh++;
            while(hh <= tt && a[i] >= a[q[tt]]) tt--;
            q[++tt] = i;
            if (i >= k - 1) pw.print(a[q[hh]] + " ");
        }
        pw.flush();
    }
}

七、KMP

kmp是在主串中查找子串的一种匹配算法,本质上是双指针,相比传统的N^2暴力匹配,kmp利用了匹配失败包含的有效信息,总的时间复杂度是O(N)

给定一个模式串?SS,以及一个模板串?PP,所有字符串中只包含大小写英文字母以及阿拉伯数字。

模板串?PP?在模式串?SS?中多次作为子串出现。

求出模板串?PP?在模式串?SS?中所有出现的位置的起始下标。

输入格式

第一行输入整数?NN,表示字符串?PP?的长度。

第二行输入字符串?PP。

第三行输入整数?MM,表示字符串?SS?的长度。

第四行输入字符串?SS。

输出格式

共一行,输出所有出现位置的起始下标(下标从?00?开始计数),整数之间用空格隔开。

数据范围

1≤N≤1051≤N≤105
1≤M≤1061≤M≤106

输入样例:

3
aba
5
ababa

输出样例:

0 2
import java.util.*;

public class Main{
    
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        String p = sc.next();
        int m = sc.nextInt();
        String s = sc.next();
        int[] next = getNext(p);
        int p1 = 0;
        int p2 = 0;
        while (p1 < s.length() && p2 < p.length()) {
            if (s.charAt(p1) == p.charAt(p2)) {
                p1++;
                p2++;
            } else if (p2 != 0) {
                p2 = next[p2];
            } else {
                p1 ++;
            }
        }
        if (p2 == p.length()) System.out.print(p1 - p2);
        else System.out.print(-1);
    }
    
    public static int[] getNext(String s) {  //我们规定next的第一个元素一定是-1
        if (s.length() == 1) return new int[] {-1};
        int[] next = new int[s.length()];
        next[0] = -1; //第一个元素一定是-1,因为它前面没有元素
        next[1] = 0; //第二个元素一定是0,因为虽然它前边有一个元素,但最长前后缀不能包括子串本身
        int i = 2;
        int cn = 0; //cn 初始化时是 i - 1
        while (i < s.length()) {
            if (s.charAt(i - 1) == s.charAt(cn)) {
                next[i++] = ++cn;
            } else if (cn != 0) {
                cn = next[cn];
            } else {
                next[i++] = 0;
            }
        }
        return next;
    }
}

八、Trie?

又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。

维护一个字符串集合,支持两种操作:

  1. I x?向集合中插入一个字符串?xx;
  2. Q x?询问一个字符串在集合中出现了多少次。

共有?NN?个操作,输入的字符串总长度不超过?105105,字符串仅包含小写英文字母。

输入格式

第一行包含整数?NN,表示操作数。

接下来?NN?行,每行包含一个操作指令,指令为?I x?或?Q x?中的一种。

输出格式

对于每个询问指令?Q x,都要输出一个整数作为结果,表示?xx?在集合中出现的次数。

每个结果占一行。

数据范围

1≤N≤2?104

对于字典树,我们要明确它的子节点分支,例如这道题,题目中说明了只会出现小写英文字母,因此它的子节点分支就是26。我们就需要一个二维数 song【N】【26】 N 应该是所有结点的总数量。idx用于给每个结点编号,还需要一个cnt【】数组用于记录以当前下标结尾的字符串一共出现了多少次。

import java.util.*;

public class Main{
    static int N = 100010;
    static int idx;
    static int[][] song = new int[N][26];
    static int[] cnt = new int[N];
    
    public static void insert(String s) {
        int p = 0;
        for (int i = 0; i < s.length(); i ++) {
            int u = s.charAt(i) - 'a';
            if (song[p][u] == 0) {
                song[p][u] = ++idx;
            }
            p = song[p][u];
        }
        cnt[p]++;
    }
    
    public static int query(String s) {
        int p = 0;
        for (int i = 0; i < s.length(); i ++) {
            int u = s.charAt(i) - 'a';
            if (song[p][u] == 0) return 0;
            p = song[p][u];
        }
        return cnt[p];
    }
    
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        while (n -- > 0) {
            String s1 = sc.next();
            if (s1.equals("I")) {
                String s2 = sc.next();
                insert(s2);
            } else {
                String s2 = sc.next();
                System.out.println(query(s2));
            }
        }
    }
}

1.最大异或对

在给定的?NN?个整数?A1,A2……ANA1,A2……AN?中选出两个进行?xorxor(异或)运算,得到的结果最大是多少?

输入格式

第一行输入一个整数?NN。

第二行输入?NN?个整数?A1A1~ANAN。

输出格式

输出一个整数表示答案。

数据范围

1≤N≤1051≤N≤105,
0≤Ai<2310≤Ai<231

输入样例:

3
1 2 3

输出样例:

3

?

思路:我们知道,相同的两个数异或之后是1,异或根据二进制位的不同来异或,不同为1,相同为0,因此我们采用这样一种思路:二进制从高到低,我们每次都?尽量选取不同的位,那么最后得到的就一定是最小的异或对,这个查找只要实现N次,我们就可以找到最大的异或对。这个实现,就采用Trie来实现

import java.util.*;

public class Main{
    static int N = 3000010; //为什么N开这么大,因为我们是以位数构建线段树的,因此输入范围是 10^5 * 31
    static int[][] song = new int[N][2]; 
    static int idx = 0;
    
    
    public static void insert(int x) {
        int p = 0;
        for (int i = 30; i >= 0; i --) {
            int u = x >> i & 1;
            if (song[p][u] == 0) song[p][u] = ++idx;
            p = song[p][u];
        }
    }
    
    public static int query(int x) {
        int p = 0;
        int res = 0;
        for(int i = 30; i >= 0; i --) {
            int s = x >> i & 1;
            if (song[p][1-s] != 0) {
                res+= 1 << i;
                p = song[p][1-s];
            } else {
                //res+= 0 << i;
                p = song[p][s];
            }
        }
        return res;
    }
    
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] e = new int[n + 10];
        for (int i = 0; i < n; i ++) {
            e[i] = sc.nextInt();
            insert(e[i]);
        }
        int max = 0;
        for (int i = 0; i < n; i ++) {
            max = Math.max(max,query(e[i]));
        }
        System.out.print(max);
    }
}

九、并查集

并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。

并查集顾名思义,核心操作就两个,1:合并两个集合? ? ? ? 2:查询两个元素是不是在同一个集合中

一共有?nn?个数,编号是?1~n1~n,最开始每个数各自在一个集合中。

现在要进行?mm?个操作,操作共有两种:

  1. M a b,将编号为?aa?和?bb?的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
  2. Q a b,询问编号为?aa?和?bb?的两个数是否在同一个集合中;

输入格式

第一行输入整数?nn?和?mm。

接下来?mm?行,每行包含一个操作指令,指令为?M a b?或?Q a b?中的一种。

输出格式

对于每个询问指令?Q a b,都要输出一个结果,如果?aa?和?bb?在同一集合内,则输出?Yes,否则输出?No

每个结果占一行。

数据范围

1≤n,m≤1051≤n,m≤105

输入样例:

4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4

输出样例:

Yes
No
Yes
难度:简单
时/空限制:1s / 64MB
总通过数:41099
总尝试数:66005
来源:模板题,AcWing
算法标签

思路:对于并查集,我们的合并和查找其实都是建立在find函数上实现的,同时我们要在函数中要实现路径压缩 ,确保这个并查集子和父之间的距离不会太长。

import java.util.*;

public class Main{
    static int N = 100010;
    static int p[] = new int[N];
    
    public static int find(int x) {   //让每个结点都指向根节点,实现路径压缩
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();  //最开始有 1 ~ n 个数
        for (int i = 1; i <= n; i ++) {
            p[i] = i;          //初始化集合,让每个元素单独成为一个集合
        }
        int m = sc.nextInt();
        while (m -- > 0) {
            String s = sc.next();
            int a = sc.nextInt();
            int b = sc.nextInt();
            if (s.equals("M")) {  //合并两个集合,只需要让其中一个的父节点指向另一个父节点
                p[find(a)] = find(b);
            } else {
                if (find(a) == find(b)) System.out.println("Yes");
                else System.out.println("No");
            }
        }
    }
}

1.连通块中点的数量

给定一个包含?nn?个点(编号为?1~n1~n)的无向图,初始时图中没有边。

现在要进行?mm?个操作,操作共有三种:

  1. C a b,在点?aa?和点?bb?之间连一条边,aa?和?bb?可能相等;
  2. Q1 a b,询问点?aa?和点?bb?是否在同一个连通块中,aa?和?bb?可能相等;
  3. Q2 a,询问点?aa?所在连通块中点的数量;

输入格式

第一行输入整数?nn?和?mm。

接下来?mm?行,每行包含一个操作指令,指令为?C a bQ1 a b?或?Q2 a?中的一种。

输出格式

对于每个询问指令?Q1 a b,如果?aa?和?bb?在同一个连通块中,则输出?Yes,否则输出?No

对于每个询问指令?Q2 a,输出一个整数表示点?aa?所在连通块中点的数量

每个结果占一行。

数据范围

1≤n,m≤1051≤n,m≤105

输入样例:

5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5

输出样例:

Yes
2
3

?思路:这道题本质上就是并查集加了一个维持区间个数的变形,我们只需要再创建一个数组用于维持集合的个数即可。

import java.util.*;

public class Main{
    static int N = 100010;
    static int[] p = new int[N];
    static int[] size = new int[N];
    
    public static int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int m = sc.nextInt();
        for (int i = 1; i <= n; i ++) {
            p[i] = i;
            size[i] = 1;
        }
        while (m -- > 0) {
            String s = sc.next();
            if (s.equals("C")) {
                int a = sc.nextInt();
                int b = sc.nextInt();
                if (find(a) == find(b)) continue;
                else {
                    size[find(b)] += size[find(a)];         //size操作一定要在更改父节点之前完成,
                    p[find(a)] = find(b);
                }
            } else if (s.equals("Q1")) {
                int a = sc.nextInt();
                int b = sc.nextInt();
                if (find(a) == find(b)) System.out.println("Yes");
                else System.out.println("No");
            } else {
                int a = sc.nextInt();
                System.out.println(size[find(a)]);
            }
        }
        
    }
}

2、食物链

动物王国中有三类动物?A,B,CA,B,C,这三类动物的食物链构成了有趣的环形。

AA?吃?BB,BB?吃?CC,CC?吃?AA。

现有?NN?个动物,以?1~N1~N?编号。

每个动物都是?A,B,CA,B,C?中的一种,但是我们并不知道它到底是哪一种。

有人用两种说法对这?NN?个动物所构成的食物链关系进行描述:

第一种说法是?1 X Y,表示?XX?和?YY?是同类。

第二种说法是?2 X Y,表示?XX?吃?YY。

此人对?NN?个动物,用上述两种说法,一句接一句地说出?KK?句话,这?KK?句话有的是真的,有的是假的。

当一句话满足下列三条之一时,这句话就是假话,否则就是真话。

  1. 当前的话与前面的某些真的话冲突,就是假话;
  2. 当前的话中?XX?或?YY?比?NN?大,就是假话;
  3. 当前的话表示?XX?吃?XX,就是假话。

你的任务是根据给定的?NN?和?KK?句话,输出假话的总数。

输入格式

第一行是两个整数?NN?和?KK,以一个空格分隔。

以下?KK?行每行是三个正整数?D,X,YD,X,Y,两数之间用一个空格隔开,其中?DD?表示说法的种类。

若?D=1D=1,则表示?XX?和?YY?是同类。

若?D=2D=2,则表示?XX?吃?YY。

输出格式

只有一个整数,表示假话的数目。

数据范围

1≤N≤500001≤N≤50000,
0≤K≤1000000≤K≤100000

输入样例:

100 7
1 101 1 
2 1 2
2 2 3 
2 3 3 
1 1 3 
2 3 1 
1 5 5

输出样例:

3
难度:中等
时/空限制:1s / 64MB
总通过数:24472
总尝试数:52856
来源:《算法竞赛进阶指南》, 模板题 , NOI2001 , POJ1182 , kuangbin专题
算法标签

挑战模式

食物链这道题,不容易觉察他是一道并查集的题,但如果仔细分析我们会发现:“说法”的格式只有两种,a和b同类,a吃b。因此我们可以知道在编号不同的个体中,永远都是真的,因为你无法确定他们之间的关系。只有凭借“说法”,把他们联系在一个集合中,才有可能辨别真假,在一个集合中,我们采用d【】数组来记录一种关系:任何集合中的一个点,d为1表示吃根节点,d为2表示被根结点吃,d为0表示和根节点是同类。因此,我们可以在说法中联立“集合”来辨别真假。

import java.util.*;

public class Main{
    static int N = 50010;
    static int[] p = new int[N];
    static int[] d = new int[N];  //d的定义是子节点和父节点之间的距离
    
    public static int find(int x) {  //由于进行路径压缩的时候改变了d,因此要更新d
        if (p[x] != x) {
            int pa = p[x];
            p[x] = find(p[x]);
            d[x] += d[pa];
        }
        return p[x];
    }
    
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int k = sc.nextInt();
        for (int i = 1; i <= n; i ++) {
            p[i] = i;
        }
        int res = 0;
        while (k -- > 0) {
            int op = sc.nextInt();
            int a = sc.nextInt();
            int b = sc.nextInt();
            if (a > n || b > n ) {
                res++;
                continue;
            }
            int pa = find(a);
            int pb = find(b);
            if (op == 1) {  //a 和 b 是同类
                if (pa == pb && (d[a] - d[b]) % 3 != 0 ) res++; //不在一个集合肯定是真的,只需要统计在一个集合的情况
                else if (pa != pb) {   //把a和b 合并
                    p[pa] = pb;
                    d[pa] = d[b] - d[a];   //d是子节点和父节点的距离
                }
            } else {
                if (pa == pb && (d[a] - d[b] - 1) % 3 != 0) res++;
                else if (pa != pb) {
                    p[pa] = pb;
                    d[pa] = d[b] - d[a] + 1;
                }
            }
        }
        System.out.print(res);
    }
}

十、哈希表

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表

我们通常使用的哈希表往往有两种实现方式,一种是拉链法,另一种是开放寻址法(蹲坑法)

哈希表的实现往往要一个高效的哈希函数,这个函数能让所有的元素均匀的分布在表中,并尽量降低哈希冲突,如何应对哈希冲突就造成了两种不同的实现方式,拉链法采用的是链表,冲突了就在后边开条链表接着,开放寻址法就跳过当前位置,寻找一个空着的位置。

维护一个集合,支持如下几种操作:

  1. I x,插入一个数?xx;
  2. Q x,询问数?xx?是否在集合中出现过;

现在要进行?NN?次操作,对于每个询问操作输出对应的结果。

输入格式

第一行包含整数?NN,表示操作数量。

接下来?NN?行,每行包含一个操作指令,操作指令为?I xQ x?中的一种。

输出格式

对于每个询问指令?Q x,输出一个询问结果,如果?xx?在集合中出现过,则输出?Yes,否则输出?No

每个结果占一行。

数据范围

1≤N≤1051≤N≤105
?109≤x≤109?109≤x≤109

输入样例:

5
I 1
I 2
I 3
Q 2
Q 5

输出样例:

Yes
No

首先来看拉链法:?

import java.util.*;

public class Main{
    static int N = 100010;               //采用拉链法实现,因此需要静态链表
    static int idx = 0;
    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;    //获取x % N 之后的下标,因为x有可能是负数,因此%完之后 + 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]) {  //我们的idx从0 开始,因此-1 是一个特殊值,初始化链表的时候默认为-1
            if (e[i] == x) return true;
        }
        return false;
    }
    
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        for (int i = 0; i < N; i ++) {  //一定要记得初始话,否则find找不到-1  死循环!
            h[i] = -1;
        }
        while (n -- > 0) {
            String s = sc.next();
            int x = sc.nextInt();
            if (s.equals("I")) add(x);
            else {
                if (find(x)) System.out.println("Yes");
                else System.out.println("No");
            }
        }
    }
}

开放寻址法:

import java.util.*;

public class Main{
    static int N = 200003;
    static int nulls = 0x3f3f3f3f;
    static int[] h = new int[N];
    
    public static int find(int x) {
        int k = (x % N + N) % N;
        while (h[k] != nulls && h[k] != x) {
           k ++;
           if (k == N) k = 0;
        }
        return k;
    }
    
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        for (int i = 0; i < N; i ++) {
            h[i] = nulls;
        }
        while (n -- > 0) {
            String s = sc.next();
            int x = sc.nextInt();
            int k = find(x);
            if (s.equals("I")) {
                h[k] = x;
            } else {
                if (h[k] == nulls) System.out.println("No");
                else System.out.println("Yes");
            }
        }
    }
}

1、字符串哈希

?哈希:哈希可以理解为一种映射关系(或者可以理解为一种函数关系),在数字哈希里面是数字到数字的映射,在字符串哈希里是字符串到数字的映射,这种巧妙的映射关系保证了哈希算法在处理某些问题时具有高效性。
在字符串哈希为字符串到数字的映射关系。
我们假设一种映射关系为字符串到P进制的数的映射关系(P是任意的),那么就有了如何映射的问题:
在映射完成后要保证能完成一系列的字符串查找,比较,删除等操作,所以必须保证这种映射关系是一一映射的,不能有冲突(这也是字符串哈希和数字哈希的一大区别,对于数字哈希我们有专门的处理冲突的手段,因为应用范围的不一样,所以字符串哈希我们不会处理冲突),为了让冲突的最小化,所以我们这里取P = 131 or 13331,并且将字符串映射到2 64 2^{64}264范围内的数字在这时的冲突几率非常小。
总结:?p = 131 or 13331, 在2 64 2^{64}264范围内的数字映射

字符串前缀哈希:?要想处理出来字符串任意区间内的hash, 我们首先要处理出来所有的前缀hash,例如:ABCDE我们先处理出来h[1]代表A的hash,h[2]代表B的·······以此类推,处理办法:进制的求算公式:
例如十进制的数字:145:P = 10.
1 ? 1 0 2 + 4 ? 10 + 5 ? 1 0 0 1*10^2+4*10+5*10^01?102+4?10+5?100?总结公式为下列:
a × P 4 + b × P 3 + c × P 2 + d × P 1 + e × P 0 a×P ^4+b×P ^3+c×P ^2+d×P ^1+e×P ^0a×P4+b×P3+c×P2+d×P1+e×P0
要处理某一个前缀的hash值,要把字符串当成一个P进制的数,然后每一位的字母的ASCII值就代表这一位的数字,则abcde的hash值为上述式子

import java.util.*;

public class Main{
    static int P = 131; //131 默认不出现哈希冲突
    static int N = 100010;
    static long[] p = new long[N];  //采用long类型是因为存在指数爆炸,long自动取模2 ^ 64
    static long[] h = new long[N];
    
    public static long find(int l, int r) {  //返回 l 到 r 的哈希值
        return h[r] - h[l - 1] * p[r - l + 1]; 
    }
    
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int m = sc.nextInt();
        String s = sc.next();
        p[0] = 1;// p 的 0 次方是 1
        for (int i = 1; i <= n ; i ++) {
            p[i] = p[i - 1] * P ;
            h[i] = h[i - 1] * P + s.charAt(i - 1);
        }
        while ( m -- > 0) {
            int l1 = sc.nextInt();
            int r1 = sc.nextInt();
            int l2 = sc.nextInt();
            int r2 = sc.nextInt();
            if (find(l1,r1) == find(l2,r2)) System.out.println("Yes");
            else System.out.println("No");
        }
    }
}

十一、堆

(Heap)是计算机科学中一类特殊的数据结构的统称。通常是一个可以被看做一棵完全二叉树的数组对象。

关于堆操作的实现细节及原理,我都在之前一篇博客总结过了,这里仅给出实现代码

(214条消息) 浅浅理解一下堆_yan扬的博客-CSDN博客https://blog.csdn.net/qq_59539549/article/details/123259521?spm=1001.2014.3001.5502

输入一个长度为?nn?的整数数列,从小到大输出前?mm?小的数。

输入格式

第一行包含整数?nn?和?mm。

第二行包含?nn?个整数,表示整数数列。

输出格式

共一行,包含?mm?个整数,表示整数数列中前?mm?小的数。

数据范围

1≤m≤n≤1051≤m≤n≤105,
1≤数列中元素≤1091≤数列中元素≤109

输入样例:

5 3
4 5 1 3 2

输出样例:

1 2 3

堆排序,首先我们要建堆,因此需要向下调整的操作(事实上也可以向上调整,但经过证明向下调整建堆的时间复杂度是O(N)?,而向上调整建堆的时间复杂度毫无疑问是O(N * log N)

每一次我们从堆中选一个最大的数,交换到最后,并且缩小堆的size,一直重复,知道所有的数都被选出,堆排序完成,毫无疑问时间复杂度O(N * log N)

import java.util.*;

public class Main{
    static int N = 100010;
    static int[] h = new int[N];
    static int size;
    
    public static void swap(int i,int j) {
        int t = h[i];
        h[i] = h[j];
        h[j] = t;
    }
    
    public static void down(int i) {
        int left = 2 * i + 1;
        while (left < size) {
            int less = left + 1 < size && h[left] > h[left + 1] ? left + 1 : left;
            less = h[less] < h[i] ? less : i;
            if (less == i) break;
            swap(i,less);
            i = less;
            left = 2 * i + 1;
        }
    }
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int m = sc.nextInt();
        for (int i = 0; i < n; i ++){
            h[i] = sc.nextInt();
            size++;
        }
        for (int i = (size -2) / 2; i >= 0; i --) {
            down(i);
        }
        while (m -- > 0) {
            System.out.print(h[0] + " ");
            h[0] = h[--size];
            down(0);
        }
    }
}

1、模拟堆

维护一个集合,初始时集合为空,支持如下几种操作:

  1. I x,插入一个数?xx;
  2. PM,输出当前集合中的最小值;
  3. DM,删除当前集合中的最小值(数据保证此时的最小值唯一);
  4. D k,删除第?kk?个插入的数;
  5. C k x,修改第?kk?个插入的数,将其变为?xx;

现在要进行?NN?次操作,对于所有第?22?个操作,输出当前集合的最小值。

输入格式

第一行包含整数?NN。

接下来?NN?行,每行包含一个操作指令,操作指令为?I xPMDMD k?或?C k x?中的一种。

输出格式

对于每个输出指令?PM,输出一个结果,表示当前集合中的最小值。

每个结果占一行。

数据范围

1≤N≤1051≤N≤105
?109≤x≤109?109≤x≤109
数据保证合法。

输入样例:

8
I -10
PM
I -10
D 1
C 2 8
I 6
PM
DM

输出样例:

-10
6

?思路:比起普通的堆,我们多了第k个元素的这种映射,因此我们需要一个数组ph记录从第几个到堆的映射,hp从堆到第几个的映射。

import java.util.*;

public class Main{
    static int N = 100010;
    static int[] h = new int[N];
    static int[] ph = new int[N];
    static int[] hp = new int[N];
    static int size,m;
    
    public static void swap(int[] arr,int i,int j) {
        int t = arr[i];
        arr[i] = arr[j];
        arr[j] = t;
    }
    
    public static void heap_swap(int i, int j) {
        swap(ph,hp[i],hp[j]);
        swap(hp,i,j);
        swap(h,i,j);
    }
    
   public static void up(int i) {
        int pa = (i - 1) / 2;
        while (h[i] < h[pa]) {
            heap_swap(pa,i);
            i = pa;
            pa = (i - 1) / 2;
        }
    }
    
   public static void down(int i) {
        int left = 2 * i + 1;
        while (left < size) {
            int less = (left + 1 < size && h[left+1] < h[left]) ? left + 1 : left;
            less = h[less] < h[i] ? less : i;
            if (less == i) return;
            heap_swap(i,less);
            i = less;
            left = 2 * i + 1;
        }
    }
    
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        while (n -- > 0) {
            String s = sc.next();
            if(s.equals("I")) {
                int x = sc.nextInt();
                ph[m] = size;
                hp[size] = m;
                h[size] = x;
                up(size);
                size++;
                m++;
            } else if (s.equals("PM")) {
                System.out.println(h[0]);
            } else if (s.equals("DM")) {
                heap_swap(0,-- size);
                down(0);
            } else if (s.equals("D")) {
                int k = sc.nextInt();
                k = ph[k-1];
                heap_swap(k,size-1);
                size--;
                up(k);
                down(k);
            } else {
                int k = sc.nextInt();
                int x = sc.nextInt();
                k = ph[k - 1];
                h[k] = x;
                up(k);
                down(k);
            }
        }
    }
}

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2022-05-19 12:02:44  更:2022-05-19 12:03:05 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/30 2:32:02-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码