数据结构
1、链表与邻接表
用数组模拟链表,因为传统链表每次添加元素都要new一个节点,如果数据在百万级的话,单单new节点都会超时,所以我们需要使用数组模拟链表。
1、单链表:邻接表:存储图和树
2、双链表:优化某些问题
单链表数组实现:
package 数据结构;
// head表示头结点的下标
// e[i]表示节点i的值
// ne[i]表示节点i的next指针是多少
public class ListButArray {
int N = 10000;
//head是头指针,指向下一个节点,idx为第一个可以用的节点。
int head;
int idx;
int[] e = new int[N];
int[] ne = new int[N];
//初始化
public ListButArray(){
head = -1;
idx = 0;
}
public void addFirst(int x){
//将x赋给当前可以用的节点,x的next指针指向head指针,head的下一个节点为idx,更新idx。
e[idx] = x;
ne[idx] = head;
head = idx;
idx++;
}
//将x插入到下标是k的后面
public void add(int x , int k){
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx;
idx ++;
}
//将下标是k的点的后面的点删掉
public void remove(int k){
ne[k] = ne[ne[k]];
}
}
双链表数组实现:
package 数据结构;
public class DListButArray {
int N = 100010;
int m;
int[] e = new int[N];
int[] l = new int[N];
int[] r = new int[N];
int idx;
public DListButArray(){
// 0表示左端点,1表示右端点
r[0] = 1;
l[1] = 0;
idx = 2;
}
//在下标是k的点的右边,插入x
public void add(int k , int x){
e[idx] = x;
r[idx] = r[k];
l[idx] = k;
l[r[k]] = idx;
r[k] = idx;
}
public void remove(int k){
r[l[k]] = r[k];
l[r[k]] = l[k];
}
}
2、栈与队列
栈:
package 数据结构;
public class Stk {
int N = 1000010;
int[] stk = new int[N];
int tt;
//插入
public void push(int value){
stk[++tt] = value;
}
//弹出
public int pop(){
return stk[tt--];
}
//判断栈是否为空
public boolean isEmpty(){
return tt > 0;
}
//栈顶元素
public int peek(){
return stk[tt];
}
}
单调栈:
给定一个序列,求某个元素左边离它最近的比它大/小的数
队列:
package 数据结构;
//队列
public class queue {
// 在队尾插入元素,在队头弹出元素
int N = 1000010;
int[] q = new int[N];
int hh = 0;
int tt = 0;
//插入
public void add(int value){
q[tt++] = value;
}
//弹出
public void removeFirst(){
hh++;
}
//判断队列是否为空
public boolean isEmpty(){
return hh >= tt;
}
//取出队头
public int peek(){
return q[hh];
}
}
3、kmp
1、暴力算法怎么做
2、如何去优化
求next数组:
for (int i = 2, j = 0; i <= n ; i++) {
while (j > 0 && p[i] != p[j+1]) j = next[j];
if (p[i] == p[j+1]) j++;
next[i] = j;
}
遍历原串寻找模板串:
for (int i = 1, j = 0; i <= m ; i++) {
while (j > 0 && s[i] != p[j+1]) j = next[j];
if (s[i] == p[j+1]) j++;
if (j == n) {
sb.append(i - n).append(" ");
j = next[j];
}
}
import java.io.*;
public class Main {
public static BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
public static PrintWriter pt = new PrintWriter(new OutputStreamWriter(System.out));
public static final int N = 1000010;
public static int[] next = new int[N];
public static char[] p = new char[N];
public static char[] s = new char[N];
public static void main(String[] args) throws IOException {
StringBuilder sb = new StringBuilder();
int n = Integer.parseInt(bf.readLine());
String ps = bf.readLine();;
int m = Integer.parseInt(bf.readLine());
String ss = bf.readLine();
for (int i = 0; i < ps.length(); i++) {
p[i+1] = ps.charAt(i);
}
for (int i = 0; i < ss.length(); i++) {
s[i+1] = ss.charAt(i);
}
for (int i = 2, j = 0; i <= n ; i++) {
while (j > 0 && p[i] != p[j+1]) j = next[j];
if (p[i] == p[j+1]) j++;
next[i] = j;
}
for (int i = 1, j = 0; i <= m ; i++) {
while (j > 0 && s[i] != p[j+1]) j = next[j];
if (s[i] == p[j+1]) j++;
if (j == n) {
sb.append(i - n).append(" ");
j = next[j];
}
}
pt.println(sb);
bf.close();
pt.flush();
pt.close();
}
}
4、Trie字典树
用来高效的存储和查找字符串集合的数据结构
import java.util.*;
public class Main{
static int N = 100010;
static int[][] son = new int[N][26];//节点数组 , 一共有N个节点
static int[] cnt = new int[N];//表示当前节点存储了多少个字符串
static int idx = 0;//数组的初始下标,节点从这个下标开始存储
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
int x = sc.nextInt();
for(int i = 0;i<x;i++){
String c = sc.next();
String s = sc.next();
if("I".equals(c)) insert(s.toCharArray());
if("Q".equals(c)) System.out.println(query(s.toCharArray()));
}
}
public static void insert(char[] str){
int p = 0;
for(int i = 0;i<str.length;i++){
int u = str[i] - 'a';
if(son[p][u] == 0) son[p][u] = ++idx; //开辟新节点
p = son[p][u]; //寻找下一个节点
}
cnt[p] ++ ; //该字符串计数加一
}
public static int query(char[] str){
int p = 0;
for(int i = 0;i<str.length;i++){
int u = str[i] - 'a';
if(son[p][u] == 0) return 0;
p = son[p][u];
}
return cnt[p];
}
}
5、并查集
1、将两个集合合并
2、询问两个元素是否在一个集合中
基本原理:每个集合用一棵树来表示。树根的编号就是整个集合的编号。每个节点存储它的父节点,p[x]表示x的父节点
问题1:如何判断树根:if(p[x] == x)
问题2:如何求x的集合编号:while(p[x]!=x) x = p[x];
问题3:如何合并两个集合:px是x的集合编号,py是y的集合编号。p[x] = p[y]
路径压缩优化:将路径上的所有节点直接指向根节点
import java.util.*;
public class Main{
static int N = 100010;
static int[] p = new int[N];
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++){
p[i] = i;
}
while(m-->0){
String s = sc.next();
int a = sc.nextInt();
int b = sc.nextInt();
if("M".equals(s)){
p[find(a)] = find(b);
}
if("Q".equals(s)){
if(find(a) == find(b)) System.out.println("Yes");
else System.out.println("No");
}
}
p = new int[N];
}
public static int find(int x){
if(p[x]!=x) p[x] = find(p[x]);
return p[x];
}
}
6、如何手写一个堆?
1、插入一个数 heap[++size] = x; up(size);
2、求集合当中的最小值 heap[1];
3、删除最小值 heap[1] = heap[size]; size–; down(1);
4、删除任意一个元素 heap[k] = heap[size]; size–; down(k); up(k);
5、修改任意一个元素 heap[k] = x; down(k); up(k);
import java.util.*;
public class Main{
static int N = 100010;
static int[] h = new int[N];
static int size = 0;
static int[] ph = new int[N];
static int[] hp = new int[N];
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = 0;
while(n--!=0){
String s =sc.next();
if("I".equals(s)){
int x = sc.nextInt();
size++;
m++;
ph[m] = size;
hp[size] = m;
h[size] = x;
up(size);
}else if("PM".equals(s)){
System.out.println(h[1]);
}else if("DM".equals(s)){
heap_swap(1 , size);
size--;
down(1);
}else if("D".equals(s)){
int k = sc.nextInt();
k = ph[k];
heap_swap(k , size);
size--;
down(k);
up(k);
}else if("C".equals(s)){
int k = sc.nextInt();
int x = sc.nextInt();
k = ph[k];
h[k] = x;
down(k);
up(k);
}
}
}
public static void heap_swap(int a , int b){
int tmp = ph[hp[a]];
ph[hp[a]] = ph[hp[b]];
ph[hp[b]] = tmp;
tmp = hp[a];
hp[a] = hp[b];
hp[b] = tmp;
swap(a , b);
}
public static void swap(int a , int b){
int tmp = h[a];
h[a] = h[b];
h[b] = tmp;
}
public static 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);
}
}
public static void up(int u){
while(u / 2 > 0 && h[u / 2] > h[u]){
heap_swap(u / 2 , u);
u/=2;
}
}
}
如何计算堆数组中的父子节点映射关系?
下标从0开始:
父节点:(p - 1) / 2
左节点:2 * p +1 右节点:2 * p + 2
下标从1开始:
父节点:p / 2
左节点:2 * p 右节点:2 * p + 1
总结
数据结构这一章不是特别的难,主要是怎么优化已有的API或者怎么对基础的数据结构变形,使之能够应用到对应的题目上。例如食物链就需要维护一个节点到根节点的距离,用来表示当前节点对应的类别。或者是堆排序,需要维护节点的插入顺序和节点所在的位置(这题饶了我好久)。总之,算法是死的,人是活(死)的,灵活变通,这才是算法。