如下所示,跟着y总,学了一段基本算法,感觉收获良多。下面是基础数据结构算法 学习过程中的一些心路历程及部分总结。方便自己后续查看,也希望能够帮到大家一点点。
滑动窗口
(每次都找窗口内最小值)

首先要保证的就是hh<=tt,也就是队列不为空。
假如下一步窗口移动到-3,那么1将不在窗口内,因此队列的头,应该++,即q[hh]++;
假如当前的元素小于-1,那么-1可以直接踢出窗口,因为后续再怎么着也不会选到它。即tt--,把它的索引替代。
宗旨就是把最小的放到队头,确保O(1)的复杂度就能 拿到最小值或最大值。
算法核心:
int a[] = new int[N];//记录原始元素值
int q[] = new int[N];//记录元素在a[]内的索引。
int hh=0,tt=-1;
StringBuilder sb = new StringBuilder();
for(int i=0;i<n;i++){
//保证队列不为空,且队头始终在窗口内。
while(hh<=tt&&q[hh]<i-k+1) hh++;
//保证队列不为空,将最小的值始终留在队头。
while(hh<=tt&&a[q[tt]]>a[i]) tt--;
q[++tt] = i; // 队尾为当前元素。
//窗口形成后 开始输出
if(i>=k-1)
sb.append(a[q[hh]]+" ");
}
System.out.println(sb);
kmp算法
用于匹配子串,假如是暴力匹配,挨个遍历。每次遇到问题,子串往后推进一个字符。
n为子串,m为大串。时间复杂度为O(n*m),是比较大的。

假如从0,能直接到2,那么就可以省略1的工作。
那么怎么精确的推进到呢?想到可以记录子串的前缀和后缀和。
比如ababca,next[]数组 索引从1开始。
next[1]=0,next[2]=0,next[3]=1,next[4]=2, next[5]=0,next[6] = 1
假如咱们能够在next[5]的时候,直接回溯到next[4],也就是回溯到前面的b了,下一个直接从打五角星的a开始比较。很显然后面的abca都是能匹配得上的。终结!

for(int i=1,j=0;i<=m;i++){
while(j>0&&s[i]!=p[j+1]) j=ne[j];
if(s[i]==p[j+1]) j++;
if(n==j){
j=ne[j];
sb.append(i-n+" ");
}
}
那么next数组该怎么求呢?很显然。
其实咱们要求的是前缀和后缀相等的最大长度。(前缀指:包含第一个元素的任意串、后缀指:包含最后一个元素的任意串)
也就是比如abab,next[4]为2。
首先第一个元素是不用匹配的,因为没有意义。假如匹配接着往后走。假如不匹配按照上述的规则回溯即可。
getNext代码如下:
static void getNext(int n, char[] p, int[] ne) {
for(int i=2,j=0;i<=n;i++){
// 不匹配 直接向前回溯。
while(j>0&&p[i]!=p[j+1]) j = ne[j];
// 匹配了,j++
if(p[i]==p[j+1]) j++;
// 该点的ne[i]为j,含义:当索引为i时,后续不匹配,可以回溯到j。
// 为什么回溯到前缀和后缀相等的长度可以呢?因为前缀是从第一个元素开始的,也是从索引1开始,所以长度也代表了其索引。
ne[i]=j;
}
}
与运算&
咱们知道二进制数字是由01构成的,那么在遇到某种特定问题时,可以寻找某种规律,将不同数字直接建立联系。
比方说A1到AN,找出两数之间最大的异或值。
这个时候我们可以采用trie树进行解题,根据数字的01进行排布。
那么怎么能更快的知道每个数字0或者1的分布呢?很简单。
先看一个数字,00110000。
00110000<<1,相信大家都不陌生,左移一位么,相当于数字扩大了一倍,01100000。
那看00110000>>1呢,同理,右移一位,相当于数字缩小了一倍,00011000。
那么假如咱们想得到每一位的数字该怎么求呢?
110>>2是不是001?也就是把最开头,也就是最高位的1移到0号位了?那么好,知道这一位,不就等同于知道最高位了吗?
怎么求?怎么知道?
001&1会得到什么?001&001=000;按位与的话,可以知道两个0号位的数字是相同的,即都是1。
这就可以快速的求出某位的数字为几了!
假如数字的范围为0到2的31次方,那么用如下公式即可求出每一位数字。
for(int i=30;i>=0;i--){
int s = x>>i&1; // 看最后一位是0还是1
}
并查集
先贴一个板子,感觉这个算法非常精妙。
//并查集
//初始化
for(int i=1;i<=n;i++){
p[i] = i;
length[i]=1;
}
// 查看祖宗编号,即集合编号。
static int find(int x){
if(x!=p[x]) p[x] = find(p[x]);//并查集+压缩路径
return p[x];
}
// 合并集合
static void merge(int x,int y){
//length[find(y)]+=getLength(x); 维护集合内的元素个数。
//d[px] = d[y]-p[x];
p[find(x)] = find(y);//x所在集合 接到y集合后了。
}
//判断两元素是否在同一集合。
static boolean isSameSet(int x,int y){
if(find(x)==find(y)) return true;
else return false;
}
// 获取集合内元素的个数
static int getLength(int x){
return length[find(x)];
}
图示:

假如,两个集合要合并,那么只需要将x的祖宗直接指向y的祖宗。这也解决了暴力做法,两个较大集合不好合并的问题,从原来的O(n)复杂度,降到了O(1)。
堆排序
首先堆排序,咱们需要弄清楚什么是堆?通常来说,堆具有如下性质,堆顶是最小值。无论何时堆顶都是最小的。
当然了,最大堆相反,堆顶无论何时都是最大的。
堆的数据结构是一个二叉树,根比左右两侧的孩子都要小。

用数组模拟堆,假设节点为x,那么左孩子为2x,右孩子为2x+1;
实际上维护堆,只需要两个操作,也就是up(int k),down(int k);这么两个操作。
数字大了,只需让数字往下走,找到属于它的位置。数字小了只需让数字网上走,努力向堆顶看齐。
那么说一下堆的常见操作:
1.插入一个数字,很简单,h[++size] = x; 然后让该数字往上走。up(x);
2.获取最小值,即获取堆顶。h[1];(索引从1开始);
3.删除堆顶。先说结论:h[1] = h[size--];down(1);
假如删除堆顶,需要改动很多值的位置。我们不妨换个思路,假如能把堆顶和其他地方的值,都交换一下,悄咪咪的再把那个值给删掉,然后再用up或down来维护一下目前堆顶的元素。不就可以了吗?
好了,思路成立。那么该换谁呢?假如h[1]、h[2]换一下,那么删除h[2]显然也不是简单的事情,咱们这时候可以想到,可以从堆的最后来删除呀。size--即可,神不知鬼不觉,就可以将带有堆顶值的元素删掉。
接下来咱们只需要down(1)来维护堆即可。
那么进阶一点的操作有没有?比如说删除第k个插入的值,或者修改第k个插入的值。
显然,想实现这个需要有一个数组能够记录,第k个插入的值,在数组内是什么索引,我们此处记为ph[];
那么此时,假如要删除这个第k个值,同样,我们还是跟堆最后一个值,交换,然后size--;紧接着再up,down,维护堆。
但是与此同时,会发生一个问题。我们让最后一个值,跟第k个插入的值交换,那么ph[]得维护吧,也就是说,咱们虽然知道最后一个值的索引,但它是第几个插入的呢??ph[i],这个i是不是无从得知了?那么ph也就没办法维护了。
因此咱们需要一个hp[]来维护,索引为i的值,对应的是第几个插入的。
比方说还是刚才的例子,
delete 5:删除第5个插入的数字。
int pos = ph[5]; //获取第5个插入的数字,在堆内的索引。
hp[pos] = hp[size]; // pos索引处 对应的是第几个插入的值。
ph[hp[size]] = pos; // 第hp[size]插入的值,现在在堆内的位置。
h[pos] = h[size--]; //将值和最后一个值进行交换。
up(pos);down(pos);
完事儿了!
哈希表
首先,咱们知道哈希Map的作业,就是存储键值对。能够在O(1)的时间复杂度内找到对应键的值。
那么它的底层原理是什么样的呢?
数字哈希和字符串哈希又有何不同呢?
首先第一点,假如数字的范围是10的-9次方到10的9次方,用数组存储空间是会溢出的,并且可能会有大量冗余空间。因此咱们想一种方法,将数字散列一下。
什么意思呢?也就是对数字进行取模存储,比方说对10的5次方取模,那么数字就会映射到0-10的5次方 - 1了。
大大减少了数组的空间,但是与此同时会有一个问题。数字是有可能重复的对吧,也就是取模后相同,不一定代表两个值相同。也就是发生了哈希冲突。
怎么解决呢?常用的方法有两种,拉链法以及开放寻址法。

拉链法顾名思义,就是在对应值的下面用链表将其连接起来。
一般拉链法会有两个关键函数:
拉链法
static int N = 100003;
static int[] h = new int[N];
static int[] e=new int[N],int[] ne = new int[N],idx;
//向哈希表中插入一个数
void insert(int x){
int k = (x%N+N)%N; //因为可能涉及到负数,因此需要先取模,再+N,再取模。
e[idx] = x;
ne[idx] = h[k];
h[k] = idx++;
}
//在哈希表查找对应的数字。
boolean query(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){
Arrays.fill(h,-1);
}
此外还有 一种方法,名为开放寻址法。不需要另外的链表,而是寻坑。多开一点数组,假如位置被占了,就往后顺延。
开放寻址法
static int N = 200003;
static Integer[] h = new Integer[N];
int find(int x){
int k = (x%N+N)%N;
while(h[k]!=null&&h[k]!=x){
k++;
if(k==N) k=0;
}
return t;
}
public static void main(String[] args){
Arrays.fill(h,null);
}
字符串哈希
顾名思义,前面咱们讲的是数字哈希。那么字符串没有办法通过简单的取模来散列。
字符串,可以用p进制的数字来表示,P可以用131。即131进制的数字。
比如说"abcd",可以表示为a131的3次方+b131的2次方+c131的1次方+d131的0次方。
假如再长一点“abcdefghijklmn”,咱们可以求出从a到任意字符的,p进制表示。
即h[r]即表示,从第一个字符到第r个字符。
那么假如想求[4,7]内的字符串呢?即defg该怎么求呢?
我们可以想到假如我们知道h[l-1],以及h[r];
可以用如下公式来进行计算。
h[l-1]*p[r-l+1]表示abc空空空空。
那么abcdefj-abc空空空空,不就可以求出defg的字符串哈希了吗?

好的代码如下:
static int P = 131;
static int h[N],p[N];
p[0]=1;
for(int i=1;i<=n;i++){
h[i] = h[i-1]*P+str[i];
p[i] = p[i-1]*P;
}
long get(int l,int r){
return h[r]-h[l]*p[r-l+1];
}

1123

被折叠的 条评论
为什么被折叠?



